@@ -7,3 +7,11 @@ Rophako is [Azulian](http://www.kirsle.net/wizards/translator.html) for | |||
"website." Pronounce it however you like. I pronounce it "roe-fa-koe." | |||
This project is under heavy construction. | |||
# Installation | |||
`pip install -r requirements.txt` | |||
These may need to be installed for the dependencies to build: | |||
**Fedora:** `libffi-devel` |
@@ -22,8 +22,12 @@ SITE_NAME = "example.com" | |||
# Do NOT use that one. It was just an example! Make your own. | |||
SECRET_KEY = 'for the love of Arceus, change this key!' | |||
# Password strength: number of iterations for bcrypt to hash passwords. | |||
BCRYPT_ITERATIONS = 12 | |||
# Rophako uses a flat file JSON database system, and the Redis caching server | |||
# sits between Ropahko and the filesystem. | |||
DB_ROOT = "db" | |||
REDIS_HOST = "localhost" | |||
REDIS_PORT = 6379 | |||
REDIS_DB = 0 |
@@ -1,2 +1,3 @@ | |||
flask | |||
redis | |||
redis | |||
bcrypt |
@@ -1,17 +1,23 @@ | |||
__version__ = '0.01' | |||
from flask import Flask, g, request, session, render_template, send_file | |||
from flask import Flask, g, request, session, render_template, send_file, abort | |||
import jinja2 | |||
import os.path | |||
import time | |||
import config | |||
import rophako.utils | |||
app = Flask(__name__) | |||
app = Flask(__name__, | |||
static_url_path="/.static", | |||
) | |||
app.DEBUG = config.DEBUG | |||
app.SECRET_KEY = config.SECRET_KEY | |||
app.secret_key = config.SECRET_KEY | |||
# Load all the blueprints! | |||
from rophako.modules.admin import mod as AdminModule | |||
from rophako.modules.account import mod as AccountModule | |||
app.register_blueprint(AdminModule) | |||
app.register_blueprint(AccountModule) | |||
# Custom Jinja handler to support custom- and default-template folders for | |||
@@ -21,25 +27,65 @@ app.jinja_loader = jinja2.ChoiceLoader([ | |||
jinja2.FileSystemLoader("rophako/www"), # Default | |||
]) | |||
app.jinja_env.globals["csrf_token"] = rophako.utils.generate_csrf_token | |||
@app.before_request | |||
def before_request(): | |||
"""Called before all requests. Initialize global template variables.""" | |||
# CSRF protection. | |||
if request.method == "POST": | |||
token = session.pop("_csrf", None) | |||
if not token or str(token) != str(request.form.get("token")): | |||
abort(403) | |||
# Default template vars. | |||
g.info = { | |||
"time": time.time(), | |||
"app": { | |||
"name": "Rophako", | |||
"version": __version__, | |||
"author": "Noah Petherbridge", | |||
}, | |||
"uri": request.path.split("/")[1:], | |||
"session": { | |||
"login": False, # Not logged in, until proven otherwise. | |||
"username": "guest", | |||
"uid": 0, | |||
"name": "Guest", | |||
"role": "user", | |||
} | |||
} | |||
# Default session vars. | |||
if not "login" in session: | |||
session.update(g.info["session"]) | |||
# Refresh their login status from the DB. | |||
if session["login"]: | |||
import rophako.model.user as User | |||
if not User.exists(uid=session["uid"]): | |||
# Weird! Log them out. | |||
from rophako.modules.account import logout | |||
logout() | |||
return | |||
db = User.get_user(uid=session["uid"]) | |||
session["username"] = db["username"] | |||
session["name"] = db["name"] | |||
session["role"] = db["role"] | |||
# Copy session params into g.info. The only people who should touch the | |||
# session are the login/out pages. | |||
for key in session: | |||
g.info["session"][key] = session[key] | |||
@app.context_processor | |||
def after_request(): | |||
"""Called just before render_template. Inject g.info into the template vars.""" | |||
g.info["time_elapsed"] = "%.03f" % (time.time() - g.info["time"]) | |||
return g.info | |||
@@ -51,8 +97,6 @@ def catchall(path): | |||
# Search for this file. | |||
for root in ["site/www", "rophako/www"]: | |||
abspath = os.path.abspath("{}/{}".format(root, path)) | |||
print abspath | |||
print abspath + ".html" | |||
if os.path.isfile(abspath): | |||
return send_file(abspath) | |||
elif not "." in path and os.path.isfile(abspath + ".html"): | |||
@@ -72,6 +116,7 @@ def not_found(error): | |||
print "NOT FOUND" | |||
return render_template('errors/404.html', **g.info), 404 | |||
# Domain specific endpoints. | |||
if config.SITE_NAME == "kirsle.net": | |||
import rophako.modules.kirsle_legacy |
@@ -0,0 +1,209 @@ | |||
# -*- coding: utf-8 -*- | |||
"""JSON flat file database system.""" | |||
import codecs | |||
import os | |||
import os.path | |||
import glob | |||
import re | |||
from fcntl import flock, LOCK_EX, LOCK_SH, LOCK_UN | |||
import redis | |||
import json | |||
import time | |||
import config | |||
from rophako.log import logger | |||
redis_client = None | |||
cache_lifetime = 60*60 # 1 hour | |||
def get(document): | |||
"""Get a specific document from the DB.""" | |||
logger.debug("JsonDB: GET {}".format(document)) | |||
# Exists? | |||
if not exists(document): | |||
logger.debug("Requested document doesn't exist") | |||
return None | |||
path = mkpath(document) | |||
stat = os.stat(path) | |||
# Do we have it cached? | |||
data = get_cache(document) | |||
if data: | |||
# Check if the cache is fresh. | |||
if stat.st_mtime > get_cache(document+"_mtime"): | |||
del_cache(document) | |||
del_cache(document+"_mtime") | |||
else: | |||
return data | |||
# Get the JSON data. | |||
data = read_json(path) | |||
# Cache and return it. | |||
set_cache(document, data, expires=cache_lifetime) | |||
set_cache(document+"_mtime", stat.st_mtime, expires=cache_lifetime) | |||
return data | |||
def commit(document, data): | |||
"""Insert/update a document in the DB.""" | |||
# Need to create the file? | |||
path = mkpath(document) | |||
if not os.path.isfile(path): | |||
parts = path.split("/") | |||
parts.pop() # Remove the file part | |||
directory = list() | |||
# Create all the folders. | |||
for part in parts: | |||
directory.append(part) | |||
segment = "/".join(directory) | |||
if len(segment) > 0 and not os.path.isdir(segment): | |||
logger.debug("JsonDB: mkdir {}".format(segment)) | |||
os.mkdir(segment, 0755) | |||
# Update the cached document. | |||
set_cache(document, data, expires=cache_lifetime) | |||
set_cache(document+"_mtime", time.time(), expires=cache_lifetime) | |||
# Write the JSON. | |||
write_json(path, data) | |||
def delete(document): | |||
"""Delete a document from the DB.""" | |||
path = mkpath(document) | |||
if os.path.isfile(path): | |||
logger.info("Delete DB document: {}".format(path)) | |||
os.unlink(path) | |||
def exists(document): | |||
"""Query whether a document exists.""" | |||
path = mkpath(document) | |||
return os.path.isfile(path) | |||
def list_docs(path): | |||
"""List all the documents at the path.""" | |||
path = mkpath("{}/*".format(path)) | |||
docs = list() | |||
for item in glob.glob(path): | |||
name = re.sub(r'\.json$', '', item) | |||
name = name.split("/")[-1] | |||
docs.append(name) | |||
return docs | |||
def mkpath(document): | |||
"""Turn a DB path into a JSON file path.""" | |||
if document.endswith(".json"): | |||
# Let's not do that. | |||
raise Exception("mkpath: document path already includes .json extension!") | |||
return "{}/{}.json".format(config.DB_ROOT, str(document)) | |||
def read_json(path): | |||
"""Slurp, decode and return the data from a JSON document.""" | |||
path = str(path) | |||
if not os.path.isfile(path): | |||
raise Exception("Can't read JSON file {}: file not found!".format(path)) | |||
# Open and lock the file. | |||
fh = codecs.open(path, 'r', 'utf-8') | |||
flock(fh, LOCK_SH) | |||
text = fh.read() | |||
flock(fh, LOCK_UN) | |||
fh.close() | |||
# Decode. | |||
try: | |||
data = json.loads(text) | |||
except: | |||
logger.error("Couldn't decode JSON data from {}".format(path)) | |||
data = None | |||
return data | |||
def write_json(path, data): | |||
"""Write a JSON document.""" | |||
path = str(path) | |||
logger.debug("JsonDB: WRITE > {}".format(path)) | |||
# Open and lock the file. | |||
fh = None | |||
if os.path.isfile(path): | |||
fh = codecs.open(path, 'r+', 'utf-8') | |||
else: | |||
fh = codecs.open(path, 'w', 'utf-8') | |||
flock(fh, LOCK_EX) | |||
# Write it. | |||
fh.truncate(0) | |||
fh.write(json.dumps(data, sort_keys=True, indent=4, separators=(',', ': '))) | |||
# Unlock and close. | |||
flock(fh, LOCK_UN) | |||
fh.close() | |||
############################################################################ | |||
# Redis Caching Functions # | |||
############################################################################ | |||
def get_redis(): | |||
"""Connect to Redis or return the existing connection.""" | |||
global redis_client | |||
if not redis_client: | |||
redis_client = redis.StrictRedis( | |||
host = config.REDIS_HOST, | |||
port = config.REDIS_PORT, | |||
db = config.REDIS_DB, | |||
) | |||
return redis_client | |||
def set_cache(key, value, expires=None): | |||
"""Set a key in the Redis cache.""" | |||
key = config.REDIS_PREFIX + key | |||
try: | |||
client = get_redis() | |||
client.set(key, json.dumps(value)) | |||
# Expiration date? | |||
if expires: | |||
client.expire(key, expires) | |||
except: | |||
logger.error("Redis exception: couldn't set_cache {}".format(key)) | |||
def get_cache(key): | |||
"""Get a cached item.""" | |||
key = config.REDIS_PREFIX + key | |||
value = None | |||
try: | |||
client = get_redis() | |||
value = client.get(key) | |||
if value: | |||
value = json.loads(value) | |||
except: | |||
logger.warning("Redis exception: couldn't get_cache {}".format(key)) | |||
value = None | |||
return value | |||
def del_cache(key): | |||
"""Delete a cached item.""" | |||
key = config.REDIS_PREFIX + key | |||
client = get_redis() | |||
client.delete(key) |
@@ -0,0 +1,33 @@ | |||
# -*- coding: utf-8 -*- | |||
"""Debug and logging functions.""" | |||
from flask import g, request | |||
import logging | |||
import config | |||
class LogHandler(logging.Handler): | |||
"""A custom logging handler.""" | |||
def emit(self, record): | |||
# The initial log line, which has the $prefix$ in it. | |||
line = self.format(record) | |||
# Is the user logged in? | |||
name = "-nobody-" | |||
line = line.replace('$prefix$', '') | |||
print line | |||
# Set up the logger. | |||
logger = logging.getLogger("rophako") | |||
handler = LogHandler() | |||
handler.setFormatter(logging.Formatter("[%(asctime)s] [%(levelname)s] $prefix$%(message)s")) | |||
logger.addHandler(handler) | |||
# Log level. | |||
if config.DEBUG: | |||
logger.setLevel(logging.DEBUG) | |||
else: | |||
logger.setLevel(logging.INFO) |
@@ -0,0 +1,3 @@ | |||
# -*- coding: utf-8 -*- | |||
"""Database models.""" |
@@ -0,0 +1,162 @@ | |||
# -*- coding: utf-8 -*- | |||
"""User account models.""" | |||
import bcrypt | |||
import time | |||
import config | |||
import rophako.jsondb as JsonDB | |||
from rophako.log import logger | |||
def create(username, password, name=None, uid=None, role="user"): | |||
"""Create a new user account. | |||
Returns the user ID number assigned to this user.""" | |||
# Name defaults to username. | |||
if name is None: | |||
name = username | |||
username = username.lower() | |||
# Provided with a user ID? | |||
if uid is not None: | |||
# See if it's available. | |||
if exists(uid=uid): | |||
logger.warning("Wanted to use UID {} for user {} but it wasn't available.".format(uid, username)) | |||
uid = None | |||
# Need to generate a UID? | |||
if uid is None: | |||
uid = get_next_uid() | |||
uid = int(uid) | |||
# Username musn't exist. | |||
if exists(username): | |||
# The front-end shouldn't let this happen. | |||
raise Exception("Can't create username {}: already exists!".format(username)) | |||
# Crypt their password. | |||
hashedpass = hash_password(password) | |||
logger.info("Create user {} with username {}".format(uid, username)) | |||
# Create the user file. | |||
JsonDB.commit("users/by-id/{}".format(uid), dict( | |||
uid=uid, | |||
username=username, | |||
name=name, | |||
role=role, | |||
password=hashedpass, | |||
created=time.time(), | |||
)) | |||
# And their username to ID map. | |||
JsonDB.commit("users/by-name/{}".format(username), dict( | |||
uid=uid, | |||
)) | |||
return uid | |||
def update_user(uid, data): | |||
"""Update the user's data.""" | |||
if not exists(uid=uid): | |||
raise Exception("Can't update user {}: doesn't exist!".format(uid)) | |||
db = get_user(uid=uid) | |||
# Change of username? | |||
if "username" in data and len(data["username"]) and data["username"] != db["username"]: | |||
JsonDB.delete("users/by-name/{}".format(db["username"])) | |||
JsonDB.commit("users/by-name/{}".format(data["username"]), dict( | |||
uid=int(uid), | |||
)) | |||
db.update(data) | |||
JsonDB.commit("users/by-id/{}".format(uid), db) | |||
def delete_user(uid): | |||
"""Delete a user account.""" | |||
if not exists(uid=uid): | |||
return | |||
db = get_user(uid=uid) | |||
username = db["username"] | |||
# Mark the account deleted. | |||
update_user(uid, dict( | |||
username="", | |||
name="", | |||
role="deleted", | |||
password="!", | |||
)) | |||
# Delete their username. | |||
JsonDB.delete("users/by-name/{}".format(username)) | |||
def list_users(): | |||
"""Get a sorted list of all users.""" | |||
uids = JsonDB.list_docs("users/by-id") | |||
users = list() | |||
for uid in sorted(map(lambda x: int(x), uids)): | |||
db = get_user(uid=uid) | |||
if db["role"] == "deleted": continue | |||
users.append(db) | |||
return users | |||
def get_uid(username): | |||
"""Turn a username into a user ID.""" | |||
db = JsonDB.get("users/by-name/{}".format(username)) | |||
if db: | |||
return int(db["uid"]) | |||
return None | |||
def get_user(uid=None, username=None): | |||
"""Get a user's DB file, or None if not found.""" | |||
if username: | |||
uid = get_uid(username) | |||
logger.debug("get_user: resolved username {} to UID {}".format(username, uid)) | |||
return JsonDB.get("users/by-id/{}".format(uid)) | |||
def exists(uid=None, username=None): | |||
"""Query whether a user ID or name exists.""" | |||
if uid: | |||
return JsonDB.exists("users/by-id/{}".format(uid)) | |||
elif username: | |||
return JsonDB.exists("users/by-name/{}".format(username.lower())) | |||
def hash_password(password): | |||
return bcrypt.hashpw(str(password), bcrypt.gensalt(config.BCRYPT_ITERATIONS)) | |||
def check_auth(username, password): | |||
"""Check the authentication credentials for the username and password. | |||
Returns a boolean true or false. On error, an error is logged.""" | |||
# Check if the username exists. | |||
if not exists(username=username): | |||
logger.error("User authentication failed: username {} not found!".format(username)) | |||
return False | |||
# Get the user's file. | |||
db = get_user(username=username) | |||
print db | |||
# Check the password. | |||
return bcrypt.hashpw(str(password), str(db["password"])) == db["password"] | |||
def get_next_uid(): | |||
"""Get the next available user ID.""" | |||
uid = 1 | |||
while exists(uid=uid): | |||
uid += 1 | |||
return uid |
@@ -1,9 +1,129 @@ | |||
# -*- coding: utf-8 -*- | |||
from flask import Blueprint | |||
"""Endpoints for user login and out.""" | |||
from flask import Blueprint, request, redirect, url_for, session, flash | |||
import re | |||
import rophako.model.user as User | |||
from rophako.utils import template | |||
mod = Blueprint("account", __name__, url_prefix="/account") | |||
@mod.route("/") | |||
def index(): | |||
return "Test" | |||
return redirect(url_for(".login")) | |||
@mod.route("/login", methods=["GET", "POST"]) | |||
def login(): | |||
"""Log into an account.""" | |||
if request.method == "POST": | |||
username = request.form.get("username", "") | |||
password = request.form.get("password", "") | |||
# Lowercase the username. | |||
username = username.lower() | |||
if User.check_auth(username, password): | |||
# OK! | |||
db = User.get_user(username=username) | |||
session["login"] = True | |||
session["username"] = username | |||
session["uid"] = db["uid"] | |||
session["name"] = db["name"] | |||
session["role"] = db["role"] | |||
return redirect(url_for("index")) | |||
else: | |||
flash("Authentication failed.") | |||
return redirect(url_for(".login")) | |||
return template("account/login.html") | |||
@mod.route("/logout") | |||
def logout(): | |||
"""Log out the user.""" | |||
session["login"] = False | |||
session["username"] = "guest" | |||
session["uid"] = 0 | |||
session["name"] = "Guest" | |||
session["role"] = "user" | |||
flash("You have been signed out.") | |||
return redirect(url_for(".login")) | |||
@mod.route("/setup", methods=["GET", "POST"]) | |||
def setup(): | |||
"""Initial setup to create the Admin user account.""" | |||
# This can't be done if users already exist on the CMS! | |||
if User.exists(uid=1): | |||
flash("This website has already been configured (users already created).") | |||
return redirect(url_for("index")) | |||
if request.method == "POST": | |||
# Submitting the form. | |||
username = request.form.get("username", "") | |||
name = request.form.get("name", "") | |||
pw1 = request.form.get("password1", "") | |||
pw2 = request.form.get("password2", "") | |||
# Default name = username. | |||
if name == "": | |||
name = username | |||
# Lowercase the user. | |||
username = username.lower() | |||
if User.exists(username=username): | |||
flash("That username already exists.") | |||
return redirect(url_for(".setup")) | |||
# Validate the form. | |||
errors = validate_create_form(username, pw1, pw2) | |||
if errors: | |||
for error in errors: | |||
flash(error) | |||
return redirect(url_for(".setup")) | |||
# Create the account. | |||
uid = User.create( | |||
username=username, | |||
password=pw1, | |||
name=name, | |||
role="admin", | |||
) | |||
flash("Admin user created! Please log in now.".format(uid)) | |||
return redirect(url_for(".login")) | |||
return template("account/setup.html") | |||
def validate_create_form(username, pw1=None, pw2=None, skip_passwd=False): | |||
"""Validate the submission of a create-user form. | |||
Returns a list of error messages if there were errors, otherwise | |||
it returns None.""" | |||
errors = list() | |||
if len(username) == 0: | |||
errors.append("You must provide a username.") | |||
if re.search(r'[^A-Za-z0-9-_]', username): | |||
errors.append("Usernames can only contain letters, numbers, dashes or underscores.") | |||
if not skip_passwd: | |||
if len(pw1) < 3: | |||
errors.append("You should use at least 3 characters in your password.") | |||
if pw1 != pw2: | |||
errors.append("Your passwords don't match.") | |||
if len(errors): | |||
return errors | |||
else: | |||
return None |
@@ -0,0 +1,183 @@ | |||
# -*- coding: utf-8 -*- | |||
"""Endpoints for admin functions.""" | |||
from flask import g, Blueprint, request, redirect, url_for, session, flash | |||
import re | |||
import rophako.model.user as User | |||
from rophako.modules.account import validate_create_form | |||
from rophako.utils import template, admin_required | |||
mod = Blueprint("admin", __name__, url_prefix="/admin") | |||
@mod.route("/") | |||
@admin_required | |||
def index(): | |||
return template("admin/index.html") | |||
@mod.route("/users") | |||
@admin_required | |||
def users(): | |||
# Get the list of existing users. | |||
users = User.list_users() | |||
return template("admin/users.html", | |||
users=users, | |||
) | |||
@mod.route("/users/create", methods=["POST"]) | |||
@admin_required | |||
def create_user(): | |||
# Submitting the form. | |||
username = request.form.get("username", "") | |||
name = request.form.get("name", "") | |||
pw1 = request.form.get("password1", "") | |||
pw2 = request.form.get("password2", "") | |||
role = request.form.get("role", "") | |||
# Default name = username. | |||
if name == "": | |||
name = username | |||
# Lowercase the user. | |||
username = username.lower() | |||
if User.exists(username=username): | |||
flash("That username already exists.") | |||
return redirect(url_for(".users")) | |||
# Validate the form. | |||
errors = validate_create_form(username, pw1, pw2) | |||
if errors: | |||
for error in errors: | |||
flash(error) | |||
return redirect(url_for(".users")) | |||
# Create the account. | |||
uid = User.create( | |||
username=username, | |||
password=pw1, | |||
name=name, | |||
role=role, | |||
) | |||
flash("User created!") | |||
return redirect(url_for(".users")) | |||
@mod.route("/users/edit/<uid>", methods=["GET", "POST"]) | |||
@admin_required | |||
def edit_user(uid): | |||
uid = int(uid) | |||
user = User.get_user(uid=uid) | |||
# Submitting? | |||
if request.method == "POST": | |||
action = request.form.get("action", "") | |||
username = request.form.get("username", "") | |||
name = request.form.get("name", "") | |||
pw1 = request.form.get("password1", "") | |||
pw2 = request.form.get("password2", "") | |||
role = request.form.get("role", "") | |||
username = username.lower() | |||
if action == "save": | |||
# Validate... | |||
errors = None | |||
# Don't allow them to change the username to one that exists. | |||
if username != user["username"]: | |||
if User.exists(username=username): | |||
flash("That username already exists.") | |||
return redirect(url_for(".edit_user", uid=uid)) | |||
# Password provided? | |||
if len(pw1) > 0: | |||
errors = validate_create_form(username, pw1, pw2) | |||
elif username != user["username"]: | |||
# Just validate the username, then. | |||
errors = validate_create_form(username, skip_passwd=True) | |||
if errors: | |||
for error in errors: | |||
flash(error) | |||
return redirect(url_for(".edit_user", uid=uid)) | |||
# Update the user. | |||
user["username"] = username | |||
user["name"] = name or username | |||
user["role"] = role | |||
if len(pw1) > 0: | |||
user["password"] = User.hash_password(pw1) | |||
User.update_user(uid, user) | |||
flash("User account updated!") | |||
return redirect(url_for(".users")) | |||
elif action == "delete": | |||
# Don't let them delete themself! | |||
if uid == g.info["session"]["uid"]: | |||
flash("You shouldn't delete yourself!") | |||
return redirect(url_for(".edit_user", uid=uid)) | |||
User.delete_user(uid) | |||
flash("User deleted!") | |||
return redirect(url_for(".users")) | |||
return template("admin/edit_user.html", | |||
info=user, | |||
) | |||
@mod.route("/impersonate/<int:uid>") | |||
@admin_required | |||
def impersonate(uid): | |||
"""Impersonate a user.""" | |||
# Check that they exist. | |||
if not User.exists(uid=uid): | |||
flash("That user ID wasn't found.") | |||
return redirect(url_for(".users")) | |||
db = User.get_user(uid=uid) | |||
if db["role"] == "deleted": | |||
flash("That user was deleted!") | |||
return redirect(url_for(".users")) | |||
# Log them in! | |||
orig_uid = session["uid"] | |||
session.update( | |||
login=True, | |||
uid=uid, | |||
username=db["username"], | |||
name=db["name"], | |||
role=db["role"], | |||
impersonator=orig_uid, | |||
) | |||
flash("Now logged in as {}".format(db["name"])) | |||
return redirect(url_for("index")) | |||
@mod.route("/unimpersonate") | |||
def unimpersonate(): | |||
"""Unimpersonate a user.""" | |||
# Must be impersonating, first! | |||
if not "impersonator" in session: | |||
flash("Stop messing around.") | |||
return redirect(url_for("index")) | |||
uid = session.pop("impersonator") | |||
db = User.get_user(uid=uid) | |||
session.update( | |||
login=True, | |||
uid=uid, | |||
username=db["username"], | |||
name=db["name"], | |||
role=db["role"], | |||
) | |||
flash("No longer impersonating.") | |||
return redirect(url_for("index")) |
@@ -0,0 +1,51 @@ | |||
# -*- coding: utf-8 -*- | |||
from flask import g, session, request, render_template | |||
from functools import wraps | |||
import uuid | |||
from rophako.log import logger | |||
def login_required(f): | |||
"""Wrapper for pages that require a logged-in user.""" | |||
@wraps(f) | |||
def decorated_function(*args, **kwargs): | |||
if not g.info["session"]["login"]: | |||
session["redirect_url"] = request.url | |||
flash("You must be logged in to do that!") | |||
return redirect(url_for("account.login")) | |||
return f(*args, **kwargs) | |||
return decorated_function | |||
def admin_required(f): | |||
"""Wrapper for admin-only pages. Implies login_required.""" | |||
@wraps(f) | |||
def decorated_function(*args, **kwargs): | |||
if not g.info["session"]["login"]: | |||
# Not even logged in? | |||
session["redirect_url"] = request.url | |||
flash("You must be logged in to do that!") | |||
return redirect(url_for("account.login")) | |||
if g.info["session"]["role"] != "admin": | |||
logger.warning("User tried to access an Admin page, but wasn't allowed!") | |||
return redirect(url_for("index")) | |||
return f(*args, **kwargs) | |||
return decorated_function | |||
def template(name, **kwargs): | |||
"""Render a template to the browser.""" | |||
html = render_template(name, **kwargs) | |||
return html | |||
def generate_csrf_token(): | |||
"""Generator for CSRF tokens.""" | |||
if "_csrf" not in session: | |||
session["_csrf"] = str(uuid.uuid4()) | |||
return session["_csrf"] |
@@ -0,0 +1,23 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Log In{% endblock %} | |||
{% block content %} | |||
<h1>Log In</h1> | |||
<form action="{{ url_for('account.login') }}" method="POST"> | |||
<input type="hidden" name="token" value="{{ csrf_token() }}"> | |||
<fieldset> | |||
<legend>Log In</legend> | |||
<strong>Username:</strong><br> | |||
<input type="text" size="20" name="username" id="username"><p> | |||
<strong>Passphrase:</strong><br> | |||
<input type="password" size="20" name="password"><p> | |||
<button type="submit">Log In</button> | |||
</fieldset> | |||
</form> | |||
{% endblock %} |
@@ -0,0 +1,42 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Initial Setup{% endblock %} | |||
{% block scripts %} | |||
<script type="text/javascript" src="/rophako/setup.js"></script> | |||
{% endblock %} | |||
{% block content %} | |||
<h1>Welcome to Rophako!</h1> | |||
This is the initial setup for the Rophako CMS. The main purpose of this is | |||
to create the initial Administrator user account.<p> | |||
<form id="setup_form" action="{{ url_for('account.setup') }}" method="POST"> | |||
<input type="hidden" name="token" value="{{ csrf_token() }}"> | |||
<fieldset> | |||
<legend>Admin User</legend> | |||
Your site needs at least one admin user to log in and manage the site. | |||
You can use any username/password combination you want, but "admin" is | |||
a typical username.<p> | |||
<strong>Username:</strong><br> | |||
<input type="text" size="40" name="username" id="username" placeholder="admin"><p> | |||
<strong>Real name:</strong><br> | |||
<input type="text" size="40" name="name" placeholder="John Doe"><p> | |||
<strong>Passphrase:</strong><br> | |||
This can be as long as you want. Pick something | |||
<a href="http://xkcd.com/936/" target="_blank">secure!</a><br> | |||
<input type="password" size="40" id="pw1" name="password1" placeholder="correct horse battery staple"><p> | |||
<strong>Confirm Passphrase:</strong><br> | |||
<input type="password" size="40" id="pw2" name="password2" placeholder="correct horse battery staple"><p> | |||
<button type="submit">Next</button> | |||
</fieldset> | |||
</form> | |||
{% endblock %} |
@@ -0,0 +1,44 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Admin Center{% endblock %} | |||
{% block scripts %} | |||
<script type="text/javascript"> | |||
$(document).ready(function() { | |||
$("#delete_button").click(function() { | |||
return window.confirm("Are you sure?"); | |||
}); | |||
}); | |||
</script> | |||
{% endblock %} | |||
{% block content %} | |||
<h1>Edit User #{{ info["uid"] }}</h1> | |||
<form action="{{ url_for('admin.edit_user', uid=info['uid']) }}" method="POST"> | |||
<input type="hidden" name="token" value="{{ csrf_token() }}"> | |||
<fieldset> | |||
<legend>User Details</legend> | |||
<strong>Username:</strong><br> | |||
<input type="text" size="20" name="username" value="{{ info['username'] }}"><p> | |||
<strong>Real name:</strong><br> | |||
<input type="text" size="20" name="name" value="{{ info['name'] }}"><p> | |||
<strong>Reset Password:</strong><br> | |||
<input type="password" size="20" name="password1"><br> | |||
<input type="password" size="20" name="password2"><p> | |||
<strong>Role:</strong><br> | |||
<select name="role"> | |||
<option value="user"{% if info["role"] == "user" %} selected{% endif %}>User</option> | |||
<option value="admin"{% if info["role"] == "admin" %} selected{% endif %}>Admin</option> | |||
</select><p> | |||
<button type="submit" name="action" value="save">Save Changes</button> | |||
<button type="submit" name="action" id="delete_button" value="delete">Delete User</button> | |||
</fieldset> | |||
</form> | |||
{% endblock %} |
@@ -0,0 +1,12 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Admin Center{% endblock %} | |||
{% block content %} | |||
<h1>Admin Center</h1> | |||
<ul> | |||
<li><a href="{{ url_for('admin.users') }}">View and Manage Users</a></li> | |||
</ul> | |||
{% endblock %} |
@@ -0,0 +1,68 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Admin Center{% endblock %} | |||
{% block content %} | |||
<h1>User Management</h1> | |||
<h2>Create New User</h2> | |||
<form action="{{ url_for('admin.create_user') }}" method="POST"> | |||
<input type="hidden" name="token" value="{{ csrf_token() }}"> | |||
<fieldset> | |||
<legend>Create New User</legend> | |||
<strong>Username:</strong><br> | |||
<input type="text" size="40" name="username" placeholder="soandso"><p> | |||
<strong>Real name:</strong><br> | |||
<input type="text" size="40" name="name" placeholder="John Smith"><p> | |||
<strong>Passphrase:</strong><br> | |||
<input type="password" size="40" name="password1" placeholder="correct horse battery staple"><p> | |||
<strong>Confirm:</strong><br> | |||
<input type="password" size="40" name="password2" placeholder="correct horse battery staple"><p> | |||
<strong>Role:</strong><br> | |||
<select name="role"> | |||
<option value="user" selected>User</option> | |||
<option value="admin">Admin</option> | |||
</select> | |||
<button type="submit">Create</button> | |||
</fieldset> | |||
<h2>User List</h2> | |||
<table class="table table-wide"> | |||
<thead> | |||
<tr> | |||
<th width="100">User ID</th> | |||
<th width="300">Username</th> | |||
<th>Real name</th> | |||
<th width="100">Role</th> | |||
<th width="100">Log in</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{% for user in users %} | |||
<tr> | |||
<td>{{ user["uid"] }}</td> | |||
<td><a href="{{ url_for('admin.edit_user', uid=user['uid']) }}">{{ user["username"] }}</a></td> | |||
<td>{{ user["name"] }}</td> | |||
<td>{{ user["role"] }}</td> | |||
<td> | |||
{% if user["role"] != "admin" %} | |||
<a href="{{ url_for('admin.impersonate', uid=user['uid']) }}">Log in as</a> | |||
{% else %} | |||
n/a | |||
{% endif %} | |||
</td> | |||
</tr> | |||
{% endfor %} | |||
</tbody> | |||
</table> | |||
{% endblock %} |
@@ -17,6 +17,13 @@ | |||
<li><a href="/">Home</a></li> | |||
<li><a href="#">About Rophako</a></li> | |||
<li><a href="#">Download</a></li> | |||
<li class="header">:: Site Admin</li> | |||
{% if session["login"] %} | |||
<li><a href="{{ url_for('account.logout') }}">Log out</a></li> | |||
{% else %} | |||
<li><a href="{{ url_for('account.login') }}">Log in</a></li> | |||
{% endif %} | |||
</ul> | |||
</nav> | |||
@@ -35,5 +42,8 @@ | |||
</a> | |||
</footer> | |||
<script type="text/javascript" src="/js/jquery-2.1.0.min.js"></script> | |||
{% block scripts %}{% endblock %} | |||
</body> | |||
</html> |
@@ -0,0 +1,31 @@ | |||
/* rophako cms | |||
----------- | |||
Script for /account/setup | |||
*/ | |||
$(document).ready(function() { | |||
$("#setup_form").submit(function() { | |||
var username = $("#username").val(), | |||
pw1 = $("#pw1").val(), | |||
pw2 = $("#pw2").val(); | |||
if (username.length === 0) { | |||
window.alert("The username is required."); | |||
} | |||
else if (username.match(/[^A-Za-z0-9_-]+/)) { | |||
window.alert("The username should only contain numbers, letters, underscores or dashes."); | |||
} | |||
else if (pw1.length < 3) { | |||
window.alert("Your password should have at least three characters."); | |||
} | |||
else if (pw1 !== pw2) { | |||
window.alert("Your passwords don't match."); | |||
} | |||
else { | |||
// All good! | |||
return true; | |||
} | |||
return false; | |||
}); | |||
}) |