JsonDB, In'tl setup, user login and admin pages
This commit is contained in:
parent
f86d1df276
commit
80c20ec87b
|
@ -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."
|
"website." Pronounce it however you like. I pronounce it "roe-fa-koe."
|
||||||
|
|
||||||
This project is under heavy construction.
|
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.
|
# Do NOT use that one. It was just an example! Make your own.
|
||||||
SECRET_KEY = 'for the love of Arceus, change this key!'
|
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
|
# Rophako uses a flat file JSON database system, and the Redis caching server
|
||||||
# sits between Ropahko and the filesystem.
|
# sits between Ropahko and the filesystem.
|
||||||
|
DB_ROOT = "db"
|
||||||
REDIS_HOST = "localhost"
|
REDIS_HOST = "localhost"
|
||||||
REDIS_PORT = 6379
|
REDIS_PORT = 6379
|
||||||
REDIS_DB = 0
|
REDIS_DB = 0
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
flask
|
flask
|
||||||
redis
|
redis
|
||||||
|
bcrypt
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
__version__ = '0.01'
|
__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 jinja2
|
||||||
import os.path
|
import os.path
|
||||||
|
import time
|
||||||
|
|
||||||
import config
|
import config
|
||||||
|
import rophako.utils
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__,
|
||||||
|
static_url_path="/.static",
|
||||||
|
)
|
||||||
app.DEBUG = config.DEBUG
|
app.DEBUG = config.DEBUG
|
||||||
app.SECRET_KEY = config.SECRET_KEY
|
app.secret_key = config.SECRET_KEY
|
||||||
|
|
||||||
# Load all the blueprints!
|
# Load all the blueprints!
|
||||||
|
from rophako.modules.admin import mod as AdminModule
|
||||||
from rophako.modules.account import mod as AccountModule
|
from rophako.modules.account import mod as AccountModule
|
||||||
|
app.register_blueprint(AdminModule)
|
||||||
app.register_blueprint(AccountModule)
|
app.register_blueprint(AccountModule)
|
||||||
|
|
||||||
# Custom Jinja handler to support custom- and default-template folders for
|
# 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
|
jinja2.FileSystemLoader("rophako/www"), # Default
|
||||||
])
|
])
|
||||||
|
|
||||||
|
app.jinja_env.globals["csrf_token"] = rophako.utils.generate_csrf_token
|
||||||
|
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def before_request():
|
def before_request():
|
||||||
"""Called before all requests. Initialize global template variables."""
|
"""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.
|
# Default template vars.
|
||||||
g.info = {
|
g.info = {
|
||||||
|
"time": time.time(),
|
||||||
"app": {
|
"app": {
|
||||||
"name": "Rophako",
|
"name": "Rophako",
|
||||||
"version": __version__,
|
"version": __version__,
|
||||||
"author": "Noah Petherbridge",
|
"author": "Noah Petherbridge",
|
||||||
},
|
},
|
||||||
"uri": request.path.split("/")[1:],
|
"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
|
@app.context_processor
|
||||||
def after_request():
|
def after_request():
|
||||||
"""Called just before render_template. Inject g.info into the template vars."""
|
"""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
|
return g.info
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,8 +97,6 @@ def catchall(path):
|
||||||
# Search for this file.
|
# Search for this file.
|
||||||
for root in ["site/www", "rophako/www"]:
|
for root in ["site/www", "rophako/www"]:
|
||||||
abspath = os.path.abspath("{}/{}".format(root, path))
|
abspath = os.path.abspath("{}/{}".format(root, path))
|
||||||
print abspath
|
|
||||||
print abspath + ".html"
|
|
||||||
if os.path.isfile(abspath):
|
if os.path.isfile(abspath):
|
||||||
return send_file(abspath)
|
return send_file(abspath)
|
||||||
elif not "." in path and os.path.isfile(abspath + ".html"):
|
elif not "." in path and os.path.isfile(abspath + ".html"):
|
||||||
|
@ -72,6 +116,7 @@ def not_found(error):
|
||||||
print "NOT FOUND"
|
print "NOT FOUND"
|
||||||
return render_template('errors/404.html', **g.info), 404
|
return render_template('errors/404.html', **g.info), 404
|
||||||
|
|
||||||
|
|
||||||
# Domain specific endpoints.
|
# Domain specific endpoints.
|
||||||
if config.SITE_NAME == "kirsle.net":
|
if config.SITE_NAME == "kirsle.net":
|
||||||
import rophako.modules.kirsle_legacy
|
import rophako.modules.kirsle_legacy
|
209
rophako/jsondb.py
Normal file
209
rophako/jsondb.py
Normal 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
33
rophako/log.py
Normal 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
rophako/model/__init__.py
Normal file
3
rophako/model/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""Database models."""
|
162
rophako/model/user.py
Normal file
162
rophako/model/user.py
Normal 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
|
|
@ -1,9 +1,129 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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 = Blueprint("account", __name__, url_prefix="/account")
|
||||||
|
|
||||||
@mod.route("/")
|
@mod.route("/")
|
||||||
def index():
|
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
183
rophako/modules/admin.py
Normal 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
51
rophako/utils.py
Normal 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
rophako/www/account/login.html
Normal file
23
rophako/www/account/login.html
Normal 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
rophako/www/account/setup.html
Normal file
42
rophako/www/account/setup.html
Normal 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
rophako/www/admin/edit_user.html
Normal file
44
rophako/www/admin/edit_user.html
Normal 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
rophako/www/admin/index.html
Normal file
12
rophako/www/admin/index.html
Normal 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
rophako/www/admin/users.html
Normal file
68
rophako/www/admin/users.html
Normal 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
4
rophako/www/js/jquery-2.1.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -17,6 +17,13 @@
|
||||||
<li><a href="/">Home</a></li>
|
<li><a href="/">Home</a></li>
|
||||||
<li><a href="#">About Rophako</a></li>
|
<li><a href="#">About Rophako</a></li>
|
||||||
<li><a href="#">Download</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>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
@ -35,5 +42,8 @@
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/js/jquery-2.1.0.min.js"></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
31
rophako/www/rophako/setup.js
Normal file
31
rophako/www/rophako/setup.js
Normal 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…
Reference in New Issue
Block a user