JsonDB, In'tl setup, user login and admin pages

This commit is contained in:
Noah 2014-02-19 23:51:18 -08:00
parent f86d1df276
commit 80c20ec87b
19 changed files with 1061 additions and 8 deletions

View File

@ -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`

View File

@ -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

View File

@ -1,2 +1,3 @@
flask
redis
redis
bcrypt

View File

@ -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

209
rophako/jsondb.py Normal file
View File

@ -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)

33
rophako/log.py Normal file
View File

@ -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)

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
"""Database models."""

162
rophako/model/user.py Normal file
View File

@ -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

View File

@ -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

183
rophako/modules/admin.py Normal file
View File

@ -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"))

51
rophako/utils.py Normal file
View File

@ -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"]

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

4
rophako/www/js/jquery-2.1.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -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>

View File

@ -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;
});
})