Browse Source

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

pull/2/head
Noah Petherbridge 5 years ago
parent
commit
80c20ec87b

+ 8
- 0
README.md 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`

+ 4
- 0
config-sample.py 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

+ 2
- 1
requirements.txt View File

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

+ 50
- 5
rophako/__init__.py 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
- 0
rophako/jsondb.py 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
- 0
rophako/log.py 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)

+ 3
- 0
rophako/model/__init__.py View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

"""Database models."""

+ 162
- 0
rophako/model/user.py 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

+ 122
- 2
rophako/modules/account.py 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
- 0
rophako/modules/admin.py 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
- 0
rophako/utils.py 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"]

+ 23
- 0
rophako/www/account/login.html 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 %}

+ 42
- 0
rophako/www/account/setup.html 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 %}

+ 44
- 0
rophako/www/admin/edit_user.html 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 %}

+ 12
- 0
rophako/www/admin/index.html 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 %}

+ 68
- 0
rophako/www/admin/users.html 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
- 0
rophako/www/js/jquery-2.1.0.min.js
File diff suppressed because it is too large
View File


+ 10
- 0
rophako/www/layout.html 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>

+ 31
- 0
rophako/www/rophako/setup.js 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;
});
})

Loading…
Cancel
Save