Finished most of admin console
Basic CRUD operations for managing registered admin users Encrypted personal information Still missing sections on managing tests and results Also missing dashboards/index/category landing pages
This commit is contained in:
parent
935cba0939
commit
93367b6e70
9
.gitignore
vendored
9
.gitignore
vendored
@ -138,3 +138,12 @@ dmypy.json
|
|||||||
# Cython debug symbols
|
# Cython debug symbols
|
||||||
cython_debug/
|
cython_debug/
|
||||||
|
|
||||||
|
# Ignore Dev Environment Files
|
||||||
|
db/data/
|
||||||
|
dev/
|
||||||
|
.vscode/
|
||||||
|
out/
|
||||||
|
ref-test/testing.py
|
||||||
|
|
||||||
|
# Ignore Encryption Keyfile
|
||||||
|
.encryption.key
|
79
REFERENCES.md
Normal file
79
REFERENCES.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# References for Learning Flask, etc
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
### Docker/Docker-Compose
|
||||||
|
|
||||||
|
- [Docker Compose Reference](https://docs.docker.com/compose/compose-file/compose-file-v3/)
|
||||||
|
|
||||||
|
### MongoDB/PyMongo
|
||||||
|
|
||||||
|
- [MongoDB Shell Commands](https://docs.mongodb.com/manual/reference/)
|
||||||
|
- [PyMongo Driver](https://pymongo.readthedocs.io/en/stable/)
|
||||||
|
|
||||||
|
## Source Code
|
||||||
|
|
||||||
|
- [MongoDB Docker Image entrypoint shell script](https://github.com/docker-library/mongo/blob/master/5.0/docker-entrypoint.sh) (Context: Tried to replicate the command to create a new user in the original entrypoint script in the custom initialisation script in this app.)
|
||||||
|
|
||||||
|
## Guides
|
||||||
|
|
||||||
|
### Flask General
|
||||||
|
|
||||||
|
- [How to structure Flask Projects](https://www.digitalocean.com/community/tutorials/how-to-structure-large-flask-applications)
|
||||||
|
- [Tables](https://www.blog.pythonlibrary.org/2017/12/14/flask-101-adding-editing-and-displaying-data/)
|
||||||
|
- [Tables, but interactive](https://blog.miguelgrinberg.com/post/beautiful-interactive-tables-for-your-flask-templates)
|
||||||
|
|
||||||
|
## Stack Exchange/Overflow
|
||||||
|
|
||||||
|
### MongoDB
|
||||||
|
|
||||||
|
- [Creating MongoDB Database on Container Start](https://stackoverflow.com/questions/42912755/how-to-create-a-db-for-mongodb-container-on-start-up)
|
||||||
|
- [Passing Environment Variables to Docker Container Entrypoint](https://stackoverflow.com/questions/64606674/how-can-i-pass-environment-variables-to-mongo-docker-entrypoint-initdb-d)
|
||||||
|
- [Integrating Flask-Login with MongoDB](https://stackoverflow.com/questions/54992412/flask-login-usermixin-class-with-a-mongodb) (**This does not work with the app as is, and is possibly something that needs more research and development in the future**)
|
||||||
|
- [Setting up a Postfix email notification system](https://medium.com/@vietgoeswest/a-simple-outbound-email-service-for-your-app-in-15-minutes-cc4da70a2af7)
|
||||||
|
|
||||||
|
## YouTube Tutorials
|
||||||
|
|
||||||
|
### General Flask Introduction
|
||||||
|
|
||||||
|
- [Part 1: Basics](https://www.youtube.com/watch?v=mqhxxeeTbu0)
|
||||||
|
- [Part 2: HTML Templates](https://www.youtube.com/watch?v=xIgPMguqyws)
|
||||||
|
- [Part 3: Bootstrap and Jinja](https://www.youtube.com/watch?v=4nzI4RKwb5I)
|
||||||
|
- [Part 4: HTTP Methods and Data Handling](https://www.youtube.com/watch?v=9MHYHgh4jYc)
|
||||||
|
- [Part 5: Sessions](https://www.youtube.com/watch?v=iIhAfX4iek0)
|
||||||
|
- [Part 6: Flashing Alerts](https://www.youtube.com/watch?v=qbnqNWXf_tU)
|
||||||
|
- [Part 9: Static Files](https://www.youtube.com/watch?v=tXpFERibRaU)
|
||||||
|
- [Part 10: Blueprints and Sub-Files](https://www.youtube.com/watch?v=WteIH6J9v64)
|
||||||
|
- [All in One with some more precise methods and techniques](https://www.youtube.com/watch?v=GW_2O9CrnSU)
|
||||||
|
|
||||||
|
Note: These tutorials use an outdated version of Bootstrap where some of the elements no longer apply.
|
||||||
|
|
||||||
|
### Build a User Log-In System using Python and MongoDB
|
||||||
|
|
||||||
|
- [Part 1](https://www.youtube.com/watch?v=w1STSSumoVk)
|
||||||
|
- [Part 2](https://www.youtube.com/watch?v=mISFEwojJmE)
|
||||||
|
- [Part 3](https://www.youtube.com/watch?v=tIoiR3N34i8)
|
||||||
|
- [Part 4](https://www.youtube.com/watch?v=5oC4-j3WWIk)
|
||||||
|
|
||||||
|
This method uses a basic HTML form and handles log-ins manually (rather than `flask-login` or `flask-auth`).
|
||||||
|
It uses `sessions` to handle user interactions (rather than cookies).
|
||||||
|
A better method would be to layer this with other Flask modules, such as `flask-login`, `flask-auth`, `flask-wtforms`, etc.
|
||||||
|
This tutorial is nevertheless really useful for integrating with **MongoDB**.
|
||||||
|
|
||||||
|
### Creating a User Login Session using Python, Flask, and MongoDB
|
||||||
|
|
||||||
|
- [Tutorial](https://www.youtube.com/watch?v=vVx1737auSE)
|
||||||
|
|
||||||
|
A much simpler and more rudimentary introduction to Flask and MongoDB.
|
||||||
|
|
||||||
|
### `flask-login`
|
||||||
|
|
||||||
|
- [Intro to `flask-login`](https://www.youtube.com/watch?v=2dEM-s3mRLE)
|
||||||
|
- [Build a User Login System with `flask-login`, `flask-wtforms`, `flask-bootstrap`, and `flask-sqlalchemy`](https://www.youtube.com/watch?v=8aTnmsDMldY)
|
||||||
|
|
||||||
|
A much more robust method that uses the various Flask modules to make a more powerful framework.
|
||||||
|
Uses SQL rather than MongoDB.
|
||||||
|
|
||||||
|
### Flask techniques
|
||||||
|
|
||||||
|
- [Create a `config.py` file](https://www.youtube.com/watch?v=GW_2O9CrnSU)
|
14
database/initdb.d/init-mongo.sh
Normal file
14
database/initdb.d/init-mongo.sh
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
set -e
|
||||||
|
mongo=( mongo --host 127.0.0.1 --port 27017 --quiet )
|
||||||
|
|
||||||
|
if [ "$MONGO_INITDB_ROOT_USERNAME" ] && [ "$MONGO_INITDB_ROOT_PASSWORD" ] && [ "$MONGO_INITDB_USERNAME" ] && [ "$MONGO_INITDB_PASSWORD" ]; then
|
||||||
|
rootAuthDatabase='admin'
|
||||||
|
|
||||||
|
"${mongo[@]}" "$rootAuthDatabase" <<-EOJS
|
||||||
|
db.createUser({
|
||||||
|
user: $(_js_escape "$MONGO_INITDB_USERNAME"),
|
||||||
|
pwd: $(_js_escape "$MONGO_INITDB_PASSWORD"),
|
||||||
|
roles: [ { role: 'readWrite', db: $(_js_escape "$MONGO_INITDB_DATABASE") } ]
|
||||||
|
})
|
||||||
|
EOJS
|
||||||
|
fi
|
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
ref_test_db:
|
||||||
|
container_name: ref_test_db
|
||||||
|
image: mongo:5.0.4-focal
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# - ./database/data:/data # Uncomment later when persistence is required.
|
||||||
|
- ./database/initdb.d/:/docker-entrypoint-initdb.d/
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
ports:
|
||||||
|
- 27017:27017
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
ref_test_postfix:
|
||||||
|
container_name: ref_test_postfix
|
||||||
|
image: catatnight/postfix:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:25:25
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend:
|
||||||
|
external: false
|
||||||
|
backend:
|
||||||
|
external: false
|
5
ref-test/Dockerfile
Normal file
5
ref-test/Dockerfile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
FROM python:3.10-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
CMD [ "gunicorn", "-b", "0.0.0.0:5000", "app:app" ]
|
0
ref-test/admin/__init__.py
Normal file
0
ref-test/admin/__init__.py
Normal file
148
ref-test/admin/auth.py
Normal file
148
ref-test/admin/auth.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
from flask import Blueprint, render_template, request, session, redirect
|
||||||
|
from flask.helpers import flash, url_for
|
||||||
|
from flask.json import jsonify
|
||||||
|
from .user.models import User
|
||||||
|
from uuid import uuid4
|
||||||
|
from security.database import decrypt_find_one, encrypted_update
|
||||||
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
|
from main import db
|
||||||
|
|
||||||
|
from .views import admin_account_required, disable_on_registration, login_required, disable_if_logged_in, get_id_from_cookie
|
||||||
|
|
||||||
|
auth = Blueprint(
|
||||||
|
'admin_auth',
|
||||||
|
__name__,
|
||||||
|
template_folder='templates',
|
||||||
|
static_folder='static'
|
||||||
|
)
|
||||||
|
|
||||||
|
@auth.route('/account/', methods = ['GET', 'POST'])
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def account():
|
||||||
|
from .forms import UpdateAccountForm
|
||||||
|
form = UpdateAccountForm()
|
||||||
|
_id = get_id_from_cookie()
|
||||||
|
user = decrypt_find_one(db.users, {'_id': _id})
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template('/admin/auth/account.html', form = form, user = user)
|
||||||
|
if request.method == 'POST':
|
||||||
|
if form.validate_on_submit():
|
||||||
|
password_confirm = request.form.get('password_confirm')
|
||||||
|
if not check_password_hash(user['password'], password_confirm):
|
||||||
|
return jsonify({ 'error': 'The password you entered is incorrect.' }), 401
|
||||||
|
entry = User(
|
||||||
|
_id = _id,
|
||||||
|
password = request.form.get('password'),
|
||||||
|
email = request.form.get('email')
|
||||||
|
)
|
||||||
|
return entry.update()
|
||||||
|
else:
|
||||||
|
errors = [*form.password_confirm.errors, *form.password_reenter.errors, *form.password.errors, *form.email.errors]
|
||||||
|
return jsonify({ 'error': errors}), 400
|
||||||
|
|
||||||
|
@auth.route('/login/', methods=['GET','POST'])
|
||||||
|
@admin_account_required
|
||||||
|
@disable_if_logged_in
|
||||||
|
def login():
|
||||||
|
from .forms import LoginForm
|
||||||
|
form = LoginForm()
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template('/admin/auth/login.html', form=form)
|
||||||
|
if request.method == 'POST':
|
||||||
|
if form.validate_on_submit():
|
||||||
|
entry = User(
|
||||||
|
username = request.form.get('username').lower(),
|
||||||
|
password = request.form.get('password'),
|
||||||
|
remember = request.form.get('remember')
|
||||||
|
)
|
||||||
|
return entry.login()
|
||||||
|
else:
|
||||||
|
errors = [*form.username.errors, *form.password.errors]
|
||||||
|
return jsonify({ 'error': errors}), 400
|
||||||
|
|
||||||
|
@auth.route('/logout/')
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
_id = get_id_from_cookie()
|
||||||
|
return User(_id=_id).logout()
|
||||||
|
|
||||||
|
@auth.route('/register/', methods=['GET','POST'])
|
||||||
|
@disable_on_registration
|
||||||
|
def register():
|
||||||
|
from .forms import RegistrationForm
|
||||||
|
form = RegistrationForm()
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template('/admin/auth/register.html', form=form)
|
||||||
|
if request.method == 'POST':
|
||||||
|
if form.validate_on_submit():
|
||||||
|
entry = User(
|
||||||
|
_id = uuid4().hex,
|
||||||
|
username = request.form.get('username').lower(),
|
||||||
|
email = request.form.get('email'),
|
||||||
|
password = request.form.get('password'),
|
||||||
|
)
|
||||||
|
return entry.register()
|
||||||
|
else:
|
||||||
|
errors = [*form.username.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors]
|
||||||
|
return jsonify({ 'error': errors}), 400
|
||||||
|
|
||||||
|
@auth.route('/reset/', methods = ['GET', 'POST'])
|
||||||
|
@admin_account_required
|
||||||
|
@disable_if_logged_in
|
||||||
|
def reset():
|
||||||
|
from .forms import ResetPasswordForm
|
||||||
|
form = ResetPasswordForm()
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template('/admin/auth/reset.html', form=form)
|
||||||
|
if request.method == 'POST':
|
||||||
|
if form.validate_on_submit():
|
||||||
|
entry = User(
|
||||||
|
username = request.form.get('username').lower(),
|
||||||
|
email = request.form.get('email'),
|
||||||
|
)
|
||||||
|
return entry.reset_password()
|
||||||
|
else:
|
||||||
|
errors = [*form.username.errors, *form.email.errors]
|
||||||
|
return jsonify({ 'error': errors}), 400
|
||||||
|
|
||||||
|
@auth.route('/reset/<token1>/<token2>/', methods = ['GET'])
|
||||||
|
@admin_account_required
|
||||||
|
@disable_if_logged_in
|
||||||
|
def reset_gateway(token1,token2):
|
||||||
|
from main import db
|
||||||
|
user = decrypt_find_one( db.users, {'reset_token' : token1} )
|
||||||
|
if not user:
|
||||||
|
return redirect(url_for('admin_auth.login'))
|
||||||
|
encrypted_update( db.users, {'reset_token': token1}, {'$unset': {'reset_token' : '', 'verification_token': ''}})
|
||||||
|
if not user['verification_token'] == token2:
|
||||||
|
flash('The verification of your password reset request failed and the token has been invalidated. Please make a new reset password request.', 'error'), 401
|
||||||
|
return redirect(url_for('admin_auth.reset'))
|
||||||
|
session['_id'] = user['_id']
|
||||||
|
session['reset_validated'] = True
|
||||||
|
return redirect(url_for('admin_auth.update_password_'))
|
||||||
|
|
||||||
|
@auth.route('/reset/update/', methods = ['GET','POST'])
|
||||||
|
@admin_account_required
|
||||||
|
@disable_if_logged_in
|
||||||
|
def update_password_():
|
||||||
|
from .forms import UpdatePasswordForm
|
||||||
|
form = UpdatePasswordForm()
|
||||||
|
if request.method == 'GET':
|
||||||
|
if 'reset_validated' not in session:
|
||||||
|
return redirect(url_for('admin_auth.login'))
|
||||||
|
session.pop('reset_validated')
|
||||||
|
return render_template('/admin/auth/update-password.html', form=form)
|
||||||
|
if request.method == 'POST':
|
||||||
|
if form.validate_on_submit():
|
||||||
|
entry = User(
|
||||||
|
_id = session['_id'],
|
||||||
|
password = request.form.get('password')
|
||||||
|
)
|
||||||
|
session.pop('_id')
|
||||||
|
return entry.update()
|
||||||
|
else:
|
||||||
|
errors = [*form.password.errors, *form.password_reenter.errors]
|
||||||
|
return jsonify({ 'error': errors}), 400
|
44
ref-test/admin/forms.py
Normal file
44
ref-test/admin/forms.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, PasswordField, BooleanField
|
||||||
|
from wtforms.validators import InputRequired, Email, Length, EqualTo, Optional
|
||||||
|
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)])
|
||||||
|
password = PasswordField('Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
|
||||||
|
remember = BooleanField('Remember Log In', render_kw={'checked': True})
|
||||||
|
|
||||||
|
class RegistrationForm(FlaskForm):
|
||||||
|
username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)])
|
||||||
|
email = StringField('Email Address', validators=[InputRequired(), Email(message='You must enter a valid email address.'), Length(max=50)])
|
||||||
|
password = PasswordField('Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
|
||||||
|
password_reenter = PasswordField('Re-Enter Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.'), EqualTo('password', message='Passwords do not match.')])
|
||||||
|
|
||||||
|
class ResetPasswordForm(FlaskForm):
|
||||||
|
username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)])
|
||||||
|
email = StringField('Email Address', validators=[InputRequired(), Email(message='You must enter a valid email address.'), Length(max=50)])
|
||||||
|
|
||||||
|
class UpdatePasswordForm(FlaskForm):
|
||||||
|
password = PasswordField('Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
|
||||||
|
password_reenter = PasswordField('Re-Enter Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.'), EqualTo('password', message='Passwords do not match.')])
|
||||||
|
|
||||||
|
class CreateUserForm(FlaskForm):
|
||||||
|
username = StringField('Username', validators=[InputRequired(), Length(min=4, max=15)])
|
||||||
|
email = StringField('Email Address', validators=[InputRequired(), Email(message='You must enter a valid email address.'), Length(max=50)])
|
||||||
|
password = PasswordField('Password (Optional)', validators=[Optional(),Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
|
||||||
|
|
||||||
|
class DeleteUserForm(FlaskForm):
|
||||||
|
password = PasswordField('Confirm Your Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
|
||||||
|
notify = BooleanField('Notify deletion by email', render_kw={'checked': True})
|
||||||
|
|
||||||
|
class UpdateUserForm(FlaskForm):
|
||||||
|
user_password = PasswordField('Confirm Your Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
|
||||||
|
email = StringField('Email Address', validators=[Optional(), Email(message='You must enter a valid email address.'), Length(max=50)])
|
||||||
|
password = PasswordField('Change Password', validators=[Optional(),Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
|
||||||
|
password_reenter = PasswordField('Re-Enter New Password', validators=[EqualTo('password', message='Passwords do not match.')])
|
||||||
|
notify = BooleanField('Notify changes by email', render_kw={'checked': True})
|
||||||
|
|
||||||
|
class UpdateAccountForm(FlaskForm):
|
||||||
|
password_confirm = PasswordField('Current Password', validators=[InputRequired(), Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
|
||||||
|
email = StringField('Email Address', validators=[Optional(), Email(message='You must enter a valid email address.'), Length(max=50)])
|
||||||
|
password = PasswordField('Change Password', validators=[Optional(),Length(min=6, max=30, message='The password must be between 6 and 20 characters long.')])
|
||||||
|
password_reenter = PasswordField('Re-Enter New Password', validators=[EqualTo('password', message='Passwords do not match.')])
|
15
ref-test/admin/results.py
Normal file
15
ref-test/admin/results.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from flask import Blueprint, render_template
|
||||||
|
from .views import login_required, admin_account_required
|
||||||
|
|
||||||
|
results = Blueprint(
|
||||||
|
'results',
|
||||||
|
__name__,
|
||||||
|
template_folder='templates',
|
||||||
|
static_folder='static'
|
||||||
|
)
|
||||||
|
|
||||||
|
@results.route('/')
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def _results():
|
||||||
|
return render_template('/admin/results.html')
|
173
ref-test/admin/static/css/style.css
Normal file
173
ref-test/admin/static/css/style.css
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
body {
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer {
|
||||||
|
background-color: lightgray;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin-heading {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input,
|
||||||
|
.form-label-group label {
|
||||||
|
padding: var(--input-padding-y) var(--input-padding-x);
|
||||||
|
font-size: 16pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group label {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0; /* Override default `<label>` margin */
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #495057;
|
||||||
|
cursor: text; /* Match the input under the label */
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: .25rem;
|
||||||
|
transition: all .1s ease-in-out;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0%;
|
||||||
|
border-bottom: 2px solid #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input::-webkit-input-placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input:-ms-input-placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input::-ms-input-placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input::-moz-placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input::placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input:not(:placeholder-shown) {
|
||||||
|
padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
|
||||||
|
padding-bottom: calc(var(--input-padding-y) / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label-group input:not(:placeholder-shown) ~ label {
|
||||||
|
padding-top: calc(var(--input-padding-y) / 3);
|
||||||
|
padding-bottom: calc(var(--input-padding-y) / 3);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox input {
|
||||||
|
transform: scale(1.5);
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signin-forgot-password {
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submission-button {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submission-button button, .form-submission-button a {
|
||||||
|
margin: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submission-button button span, .form-submission-button button svg, .form-submission-button a span, .form-submission-button a svg {
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.dataTable {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-table-row {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-row-actions {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-row-actions button {
|
||||||
|
margin: 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cookie-alert {
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dismiss-cookie-alert {
|
||||||
|
margin-top: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for Edge
|
||||||
|
-------------------------------------------------- */
|
||||||
|
@supports (-ms-ime-align: auto) {
|
||||||
|
.form-label-group label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.form-label-group input::-ms-input-placeholder {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for IE
|
||||||
|
-------------------------------------------------- */
|
||||||
|
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||||
|
.form-label-group label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.form-label-group input:-ms-input-placeholder {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
2
ref-test/admin/static/js/jquery-3.6.0.min.js
vendored
Normal file
2
ref-test/admin/static/js/jquery-3.6.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
375
ref-test/admin/static/js/script.js
Normal file
375
ref-test/admin/static/js/script.js
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
// Menu Highlight Scripts
|
||||||
|
const menuItems = document.getElementsByClassName('nav-link');
|
||||||
|
for(let i = 0; i < menuItems.length; i++) {
|
||||||
|
if(menuItems[i].pathname == window.location.pathname) {
|
||||||
|
menuItems[i].classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dropdownItems = document.getElementsByClassName('dropdown-item');
|
||||||
|
for(let i = 0; i< dropdownItems.length; i++) {
|
||||||
|
if(dropdownItems[i].pathname == window.location.pathname) {
|
||||||
|
dropdownItems[i].classList.add('active');
|
||||||
|
$( "#" + dropdownItems[i].id ).closest( '.dropdown' ).find('.dropdown-toggle').addClass('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form Processing Scripts
|
||||||
|
$('form[name=form-register]').submit(function(event) {
|
||||||
|
|
||||||
|
var $form = $(this);
|
||||||
|
var alert = document.getElementById('alert-box');
|
||||||
|
var data = $form.serialize();
|
||||||
|
|
||||||
|
alert.innerHTML = ''
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: window.location.pathname,
|
||||||
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.href = "/admin/login/";
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (response.responseJSON.error instanceof Array) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form[name=form-login]').submit(function(event) {
|
||||||
|
|
||||||
|
var $form = $(this);
|
||||||
|
var alert = document.getElementById('alert-box');
|
||||||
|
var data = $form.serialize();
|
||||||
|
|
||||||
|
alert.innerHTML = ''
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: window.location.pathname,
|
||||||
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.href = "/admin/dashboard/";
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (response.responseJSON.error instanceof Array) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form[name=form-reset]').submit(function(event) {
|
||||||
|
|
||||||
|
var $form = $(this);
|
||||||
|
var alert = document.getElementById('alert-box');
|
||||||
|
var data = $form.serialize();
|
||||||
|
|
||||||
|
alert.innerHTML = ''
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: window.location.pathname,
|
||||||
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.href = "/admin/login/";
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (response.responseJSON.error instanceof Array) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form[name=form-update-password]').submit(function(event) {
|
||||||
|
|
||||||
|
var $form = $(this);
|
||||||
|
var alert = document.getElementById('alert-box');
|
||||||
|
var data = $form.serialize();
|
||||||
|
console.log(data)
|
||||||
|
alert.innerHTML = ''
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: window.location.pathname,
|
||||||
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.href = "/admin/login/";
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (response.responseJSON.error instanceof Array) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form[name=form-create-user]').submit(function(event) {
|
||||||
|
|
||||||
|
var $form = $(this);
|
||||||
|
var alert = document.getElementById('alert-box');
|
||||||
|
var data = $form.serialize();
|
||||||
|
|
||||||
|
alert.innerHTML = ''
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: window.location.pathname,
|
||||||
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (response.responseJSON.error instanceof Array) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form[name=form-delete-user]').submit(function(event) {
|
||||||
|
|
||||||
|
var $form = $(this);
|
||||||
|
var alert = document.getElementById('alert-box');
|
||||||
|
var data = $form.serialize();
|
||||||
|
|
||||||
|
alert.innerHTML = ''
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: window.location.pathname,
|
||||||
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.href = '/admin/settings/users/';
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (response.responseJSON.error instanceof Array) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form[name=form-update-user]').submit(function(event) {
|
||||||
|
|
||||||
|
var $form = $(this);
|
||||||
|
var alert = document.getElementById('alert-box');
|
||||||
|
var data = $form.serialize();
|
||||||
|
|
||||||
|
alert.innerHTML = ''
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: window.location.pathname,
|
||||||
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.href = '/admin/settings/users';
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (response.responseJSON.error instanceof Array) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form[name=form-update-account]').submit(function(event) {
|
||||||
|
|
||||||
|
var $form = $(this);
|
||||||
|
var alert = document.getElementById('alert-box');
|
||||||
|
var data = $form.serialize();
|
||||||
|
|
||||||
|
alert.innerHTML = ''
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: window.location.pathname,
|
||||||
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
window.location.href = '/admin/dashboard/';
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
if (typeof response.responseJSON.error === 'string' || response.responseJSON.error instanceof String) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (response.responseJSON.error instanceof Array) {
|
||||||
|
for (var i = 0; i < response.responseJSON.error.length; i ++) {
|
||||||
|
alert.innerHTML = alert.innerHTML + `
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
${response.responseJSON.error[i]}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dismiss Cookie Alert
|
||||||
|
$('#dismiss-cookie-alert').click(function(event){
|
||||||
|
|
||||||
|
console.log('Foo')
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/cookies/',
|
||||||
|
type: 'GET',
|
||||||
|
data: {
|
||||||
|
time: Date.now()
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response){
|
||||||
|
console.log(response)
|
||||||
|
},
|
||||||
|
error: function(response){
|
||||||
|
console.log(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
})
|
56
ref-test/admin/templates/admin/auth/account.html
Normal file
56
ref-test/admin/templates/admin/auth/account.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{% extends "admin/components/input-forms.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="form-container">
|
||||||
|
<form name="form-update-account" class="form-signin">
|
||||||
|
{% include "admin/components/server-alerts.html" %}
|
||||||
|
<h2 class="form-signin-heading">Update Your Account</h2>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="form-label-group">
|
||||||
|
Please confirm <strong>your current password</strong> before making any changes to your user account.
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password_confirm(class_="form-control", placeholder="Current Password", value = user.email, autofocus=true) }}
|
||||||
|
{{ form.password_confirm.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
You can use this panel to update your email address or password.
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.email(class_="form-control", placeholder="Email Address", value = user.email) }}
|
||||||
|
{{ form.email.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password(class_="form-control", placeholder="Password") }}
|
||||||
|
{{ form.password.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password_reenter(class_="form-control", placeholder="Password") }}
|
||||||
|
{{ form.password_reenter.label }}
|
||||||
|
</div>
|
||||||
|
{% include "admin/components/client-alerts.html" %}
|
||||||
|
<div class="container form-submission-button">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-center">
|
||||||
|
<a href="{{ url_for('admin_views.users') }}" class="btn btn-md btn-danger btn-block" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
Cancel
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-md btn-primary btn-block" type="submit">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-lines-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M6 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-5 6s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zM11 3.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5zm.5 2.5a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1h-4zm2 3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1h-2zm0 3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1h-2z"/>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
Update
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
32
ref-test/admin/templates/admin/auth/login.html
Normal file
32
ref-test/admin/templates/admin/auth/login.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% extends "admin/components/input-forms.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="form-container">
|
||||||
|
<form name="form-login" class="form-signin">
|
||||||
|
{% include "admin/components/server-alerts.html" %}
|
||||||
|
<h2 class="form-signin-heading">Log In</h2>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.username(class_="form-control", autofocus=true, placeholder="Enter Username") }}
|
||||||
|
{{ form.username.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password(class_="form-control", placeholder="Enter Password") }}
|
||||||
|
{{ form.password.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.remember(class_="form-check-input") }}
|
||||||
|
{{ form.remember.label }}
|
||||||
|
</div>
|
||||||
|
{% include "admin/components/client-alerts.html" %}
|
||||||
|
<div class="container form-submission-button">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-center">
|
||||||
|
<button class="btn btn-md btn-success btn-block" type="submit">Log In</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('admin_auth.reset') }}" class="signin-forgot-password">Reset Password</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
43
ref-test/admin/templates/admin/auth/register.html
Normal file
43
ref-test/admin/templates/admin/auth/register.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{% extends "admin/components/input-forms.html" %}
|
||||||
|
|
||||||
|
{% block navbar %}
|
||||||
|
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a href="{{ url_for('admin_views.home') }}" class="navbar-brand mb-0 h1">RefTest | Admin</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="form-container">
|
||||||
|
<form name="form-register" action="" method="" class="form-signin">
|
||||||
|
{% include "admin/components/server-alerts.html" %}
|
||||||
|
<h2 class="form-signin-heading">Register an Account</h2>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.username(class_="form-control", autofocus=true, placeholder="Username") }}
|
||||||
|
{{ form.username.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.email(class_="form-control", placeholder="Email Address") }}
|
||||||
|
{{ form.email.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password(class_="form-control", placeholder="Password") }}
|
||||||
|
{{ form.password.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password_reenter(class_="form-control", placeholder="Password") }}
|
||||||
|
{{ form.password_reenter.label }}
|
||||||
|
</div>
|
||||||
|
{% include "admin/components/client-alerts.html" %}
|
||||||
|
<div class="container form-submission-button">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-center">
|
||||||
|
<button class="btn btn-md btn-success btn-block" type="submit">Register</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
27
ref-test/admin/templates/admin/auth/reset.html
Normal file
27
ref-test/admin/templates/admin/auth/reset.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{% extends "admin/components/input-forms.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="form-container">
|
||||||
|
<form name="form-reset" class="form-signin">
|
||||||
|
{% include "admin/components/server-alerts.html" %}
|
||||||
|
<h2 class="form-signin-heading">Reset Password</h2>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.username(class_="form-control", autofocus=true, placeholder="Enter Username") }}
|
||||||
|
{{ form.username.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.email(class_="form-control", placeholder="Enter Email Address") }}
|
||||||
|
{{ form.email.label }}
|
||||||
|
</div>
|
||||||
|
{% include "admin/components/client-alerts.html" %}
|
||||||
|
<div class="container form-submission-button">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-center">
|
||||||
|
<button class="btn btn-md btn-success btn-block" type="submit">Reset Password</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
29
ref-test/admin/templates/admin/auth/update-password.html
Normal file
29
ref-test/admin/templates/admin/auth/update-password.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% extends "admin/components/input-forms.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="form-container">
|
||||||
|
<form name="form-update-password" class="form-signin">
|
||||||
|
{% include "admin/components/server-alerts.html" %}
|
||||||
|
<h2 class="form-signin-heading">Update Password</h2>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password(class_="form-control", placeholder="Password") }}
|
||||||
|
{{ form.password.label }}
|
||||||
|
{{ form.password.errors[0] }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password_reenter(class_="form-control", placeholder="Password") }}
|
||||||
|
{{ form.password_reenter.label }}
|
||||||
|
{{ form.password_reenter.errors[0] }}
|
||||||
|
</div>
|
||||||
|
{% include "admin/components/client-alerts.html" %}
|
||||||
|
<div class="container form-submission-button">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-center">
|
||||||
|
<button class="btn btn-md btn-success btn-block" type="submit">Update Password</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
66
ref-test/admin/templates/admin/components/base.html
Normal file
66
ref-test/admin/templates/admin/components/base.html
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
|
||||||
|
crossorigin="anonymous">
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.1/font/bootstrap-icons.css">
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('.static', filename='css/style.css') }}"
|
||||||
|
/>
|
||||||
|
{% block datatable_css %}
|
||||||
|
{% endblock %}
|
||||||
|
<title>{% block title %} SKA Referee Test | Admin Console {% endblock %}</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
|
||||||
|
{% block navbar %}
|
||||||
|
{% include "admin/components/navbar.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% block top_alerts %}
|
||||||
|
{% include "admin/components/server-alerts.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="container site-footer">
|
||||||
|
{% include "admin/components/footer.html" %}
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- JQuery, Popper, and Bootstrap js dependencies -->
|
||||||
|
<script
|
||||||
|
src="https://code.jquery.com/jquery-3.6.0.min.js"
|
||||||
|
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
|
||||||
|
crossorigin="anonymous">
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
window.jQuery || document.write('<script src="js/jquery-3.6.0.min.js"><\/script>')
|
||||||
|
</script>
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js"
|
||||||
|
integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB"
|
||||||
|
crossorigin="anonymous">
|
||||||
|
</script>
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"
|
||||||
|
integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
></script>
|
||||||
|
<!-- Custom js -->
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="{{ url_for('.static', filename='js/script.js') }}"
|
||||||
|
></script>
|
||||||
|
{% block datatable_scripts %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1 @@
|
|||||||
|
<div id="alert-box"></div>
|
2
ref-test/admin/templates/admin/components/footer.html
Normal file
2
ref-test/admin/templates/admin/components/footer.html
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<p>This web app was developed by Vivek Santayana. The source code for the web app, excluding any data pertaining to the questions in the quiz, is freely available at the repository under an MIT License.</p>
|
||||||
|
<p>All questions in the test are © The Scottish Korfball Association {{ now.year }}. All rights are reserved.</p>
|
@ -0,0 +1,4 @@
|
|||||||
|
{% extends "admin/components/base.html" %}
|
||||||
|
{% import "bootstrap/wtf.html" as wtf %}
|
||||||
|
{% block top_alerts %}
|
||||||
|
{% endblock %}
|
79
ref-test/admin/templates/admin/components/navbar.html
Normal file
79
ref-test/admin/templates/admin/components/navbar.html
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a href="{{ url_for('admin_views.home') }}" class="navbar-brand mb-0 h1">RefTest | Admin</a>
|
||||||
|
<button
|
||||||
|
class="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbar"
|
||||||
|
aria-controls="navbar"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle Navigation"
|
||||||
|
>
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse justify-content-end" id="navbar">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
{% if not check_login() %}
|
||||||
|
<li class="nav-item" id="nav-login">
|
||||||
|
<a href="{{ url_for('admin_auth.login') }}" id="link-login" class="nav-link">Log In</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if check_login() %}
|
||||||
|
<li class="nav-item" id="nav-results">
|
||||||
|
<a href="{{ url_for('results._results') }}" id="link-results" class="nav-link">View Results</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" id="nav-tests">
|
||||||
|
<a href="{{ url_for('admin_views.tests') }}" id="link-tests" class="nav-link">Manage Tests</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown" id="nav-settings">
|
||||||
|
<a
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
id="dropdown-account"
|
||||||
|
role="button"
|
||||||
|
href="{{ url_for('admin_views.settings') }}"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="dropdown-menu"
|
||||||
|
aria-labelledby="dropdown-account"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin_views.users') }}" id="link-users" class="dropdown-item">Manage Users</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin_views.questions') }}" id="link-questions" class="dropdown-item">Manage Questions</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown" id="nav-account">
|
||||||
|
<a
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
id="dropdown-account"
|
||||||
|
role="button"
|
||||||
|
href="{{ url_for('admin_auth.account') }}"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Account
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
class="dropdown-menu"
|
||||||
|
aria-labelledby="dropdown-account"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin_auth.account') }}" id="link-account" class="dropdown-item">Account Settings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('admin_auth.logout') }}" id="link-logout" class="dropdown-item">Log Out</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
41
ref-test/admin/templates/admin/components/server-alerts.html
Normal file
41
ref-test/admin/templates/admin/components/server-alerts.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% set cookie_flash_flag = namespace(value=False) %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
{% if category == "error" %}
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill" title="Danger"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% elif category == "success" %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-check2-circle" title="Success"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% elif category == "warning" %}
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-info-circle-fill" title="Warning"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% elif category == "cookie_alert" %}
|
||||||
|
{% if not cookie_flash_flag.value %}
|
||||||
|
<div class="alert alert-primary alert-dismissible fade show" id="cookie-alert" role="alert">
|
||||||
|
<i class="bi bi-info-circle-fill" title="Alert"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" id="dismiss-cookie-alert" class="btn btn-success" data-bs-dismiss="alert" aria-label="Close">Accept</button>
|
||||||
|
</div>
|
||||||
|
{% set cookie_flash_flag.value = True %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-primary alert-dismissible fade show" role="alert">
|
||||||
|
<i class="bi bi-info-circle-fill" title="Alert"></i>
|
||||||
|
{{ message|safe }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
5
ref-test/admin/templates/admin/index.html
Normal file
5
ref-test/admin/templates/admin/index.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% extends "admin/components/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
{% endblock %}
|
1
ref-test/admin/templates/admin/results.html
Normal file
1
ref-test/admin/templates/admin/results.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
{% extends "admin/components/base.html" %}
|
44
ref-test/admin/templates/admin/settings/delete-user.html
Normal file
44
ref-test/admin/templates/admin/settings/delete-user.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{% extends "admin/components/input-forms.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="form-container">
|
||||||
|
<form name="form-delete-user" class="form-signin">
|
||||||
|
{% include "admin/components/server-alerts.html" %}
|
||||||
|
<h2 class="form-signin-heading">Delete User ‘{{ user.username }}’?</h2>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<p>This action cannot be undone. Deleting an account will mean {{ user.username }} will no longer be able to log in to the admin console.</p>
|
||||||
|
<p>Are you sure you want to proceed?</p>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password(class_="form-control", placeholder="Confirm Your Password", autofocus=true) }}
|
||||||
|
{{ form.password.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.notify(class_="form-check-input") }}
|
||||||
|
{{ form.notify.label }}
|
||||||
|
</div>
|
||||||
|
{% include "admin/components/client-alerts.html" %}
|
||||||
|
<div class="container form-submission-button">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-center">
|
||||||
|
<a href="{{ url_for('admin_views.users') }}" autofocus="true" class="btn btn-md btn-primary btn-block" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
Cancel
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-md btn-danger btn-block" type="submit">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-x-fill" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm6.146-2.854a.5.5 0 0 1 .708 0L14 6.293l1.146-1.147a.5.5 0 0 1 .708.708L14.707 7l1.147 1.146a.5.5 0 0 1-.708.708L14 7.707l-1.146 1.147a.5.5 0 0 1-.708-.708L13.293 7l-1.147-1.146a.5.5 0 0 1 0-.708z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
Delete
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
1
ref-test/admin/templates/admin/settings/index.html
Normal file
1
ref-test/admin/templates/admin/settings/index.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
{% extends "admin/components/base.html" %}
|
1
ref-test/admin/templates/admin/settings/questions.html
Normal file
1
ref-test/admin/templates/admin/settings/questions.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
{% extends "admin/components/base.html" %}
|
57
ref-test/admin/templates/admin/settings/update-user.html
Normal file
57
ref-test/admin/templates/admin/settings/update-user.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{% extends "admin/components/input-forms.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="form-container">
|
||||||
|
<form name="form-update-user" class="form-signin">
|
||||||
|
{% include "admin/components/server-alerts.html" %}
|
||||||
|
<h2 class="form-signin-heading">Update User ‘{{ user.username }}’</h2>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.email(class_="form-control", placeholder="Email Address", value = user.email) }}
|
||||||
|
{{ form.email.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password(class_="form-control", placeholder="Password") }}
|
||||||
|
{{ form.password.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password_reenter(class_="form-control", placeholder="Password") }}
|
||||||
|
{{ form.password_reenter.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.notify(class_="form-check-input") }}
|
||||||
|
{{ form.notify.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
Please confirm <strong>your password</strong> before committing any changes to a user account.
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.user_password(class_="form-control", placeholder="Your Password", value = user.email) }}
|
||||||
|
{{ form.user_password.label }}
|
||||||
|
</div>
|
||||||
|
{% include "admin/components/client-alerts.html" %}
|
||||||
|
<div class="container form-submission-button">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-center">
|
||||||
|
<a href="{{ url_for('admin_views.users') }}" class="btn btn-md btn-danger btn-block" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-circle-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
Cancel
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-md btn-primary btn-block" type="submit">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-lines-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M6 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-5 6s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zM11 3.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5zm.5 2.5a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1h-4zm2 3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1h-2zm0 3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1h-2z"/>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
Update
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1 @@
|
|||||||
|
{% extends "admin/components/base.html" %}
|
157
ref-test/admin/templates/admin/settings/users.html
Normal file
157
ref-test/admin/templates/admin/settings/users.html
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
{% extends "admin/components/base.html" %}
|
||||||
|
{% block title %} SKA Referee Test | Manage Users {% endblock %}
|
||||||
|
{% block datatable_css %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.3/css/dataTables.bootstrap5.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/buttons/2.0.1/css/buttons.bootstrap5.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/colreorder/1.5.5/css/colReorder.bootstrap5.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/fixedheader/3.2.0/css/fixedHeader.bootstrap5.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/keytable/2.6.4/css/keyTable.bootstrap5.min.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/responsive/2.2.9/css/responsive.bootstrap5.min.css"/>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Manage Users</h1>
|
||||||
|
|
||||||
|
<table id="user-table" class="table table-striped" style="width:100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
<th data-priority="1">
|
||||||
|
Username
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Email Address
|
||||||
|
</th>
|
||||||
|
<th data-priority="1">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr class="user-table-row">
|
||||||
|
<td>
|
||||||
|
{% if user._id == get_id_from_cookie() %}
|
||||||
|
<div class="text-success" title="Current User">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi success bi-caret-right-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ user.username }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ user.email }}
|
||||||
|
</td>
|
||||||
|
<td class="user-row-actions">
|
||||||
|
<a
|
||||||
|
href="
|
||||||
|
{% if not user._id == get_id_from_cookie() %}
|
||||||
|
{{ url_for('admin_views.update_user', _id = user._id ) }}
|
||||||
|
{% else %}
|
||||||
|
{{ url_for('admin_auth.account') }}
|
||||||
|
{% endif %}
|
||||||
|
"
|
||||||
|
class="btn btn-primary"
|
||||||
|
title="Update User"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-lines-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M6 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-5 6s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zM11 3.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5zm.5 2.5a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1h-4zm2 3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1h-2zm0 3a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1h-2z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="
|
||||||
|
{% if not user._id == get_id_from_cookie() %}
|
||||||
|
{{ url_for('admin_views.delete_user', _id = user._id ) }}
|
||||||
|
{% else %}
|
||||||
|
#
|
||||||
|
{% endif %}
|
||||||
|
"
|
||||||
|
class="btn btn-danger {% if user._id == get_id_from_cookie() %} disabled {% endif %}"
|
||||||
|
title="Delete User"
|
||||||
|
{% if user._id == get_id_from_cookie() %} onclick="return false" {% endif %}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-x-fill" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm6.146-2.854a.5.5 0 0 1 .708 0L14 6.293l1.146-1.147a.5.5 0 0 1 .708.708L14.707 7l1.147 1.146a.5.5 0 0 1-.708.708L14 7.707l-1.146 1.147a.5.5 0 0 1-.708-.708L13.293 7l-1.147-1.146a.5.5 0 0 1 0-.708z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="form-container">
|
||||||
|
<form name="form-create-user" class="form-signin">
|
||||||
|
<h2 class="form-signin-heading">Create User</h2>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.username(class_="form-control", placeholder="Enter Username") }}
|
||||||
|
{{ form.username.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.email(class_="form-control", placeholder="Enter Email") }}
|
||||||
|
{{ form.email.label }}
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
If you do not enter a password, a random one will be generated.
|
||||||
|
</div>
|
||||||
|
<div class="form-label-group">
|
||||||
|
{{ form.password(class_="form-control", placeholder="Enter Password") }}
|
||||||
|
{{ form.password.label }}
|
||||||
|
</div>
|
||||||
|
{% include "admin/components/client-alerts.html" %}
|
||||||
|
<div class="container form-submission-button">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-center">
|
||||||
|
<button title="Create User" class="btn btn-md btn-success btn-block" type="submit">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-plus-fill" viewBox="0 0 20 20">
|
||||||
|
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||||
|
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
</svg>
|
||||||
|
Create User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block datatable_scripts %}
|
||||||
|
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/2.5.0/jszip.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/pdfmake.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/vfs_fonts.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/1.11.3/js/dataTables.bootstrap5.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/dataTables.buttons.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.bootstrap5.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.colVis.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.html5.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/buttons/2.0.1/js/buttons.print.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/colreorder/1.5.5/js/dataTables.colReorder.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/fixedheader/3.2.0/js/dataTables.fixedHeader.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/keytable/2.6.4/js/dataTables.keyTable.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/responsive/2.2.9/js/dataTables.responsive.min.js"></script>
|
||||||
|
<script type="text/javascript" src="https://cdn.datatables.net/responsive/2.2.9/js/responsive.bootstrap5.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#user-table').DataTable({
|
||||||
|
'columnDefs': [
|
||||||
|
{'sortable': false, 'targets': [0,3]}
|
||||||
|
],
|
||||||
|
'order': [[1, 'asc'], [2, 'asc']],
|
||||||
|
'buttons': [
|
||||||
|
'copy', 'excel', 'pdf'
|
||||||
|
],
|
||||||
|
'responsive': 'true',
|
||||||
|
'colReorder': 'true',
|
||||||
|
'fixedHeader': 'true'
|
||||||
|
});
|
||||||
|
} );
|
||||||
|
$('#user-table').show();
|
||||||
|
$(window).trigger('resize');
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
1
ref-test/admin/templates/admin/tests.html
Normal file
1
ref-test/admin/templates/admin/tests.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
{% extends "admin/components/base.html" %}
|
0
ref-test/admin/user/__init__.py
Normal file
0
ref-test/admin/user/__init__.py
Normal file
185
ref-test/admin/user/models.py
Normal file
185
ref-test/admin/user/models.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
from flask import flash, make_response, Response
|
||||||
|
from flask.helpers import url_for
|
||||||
|
from flask.json import jsonify
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from werkzeug.utils import redirect
|
||||||
|
from flask_mail import Message
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
from security import encrypt, decrypt
|
||||||
|
from security.database import decrypt_find_one, encrypted_update
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from main import db, mail
|
||||||
|
|
||||||
|
class User:
|
||||||
|
|
||||||
|
def __init__(self, _id=None, username=None, password=None, email=None, remember=False):
|
||||||
|
self._id = _id
|
||||||
|
self.username = username
|
||||||
|
self.email = email
|
||||||
|
self.password = password
|
||||||
|
self.remember = remember
|
||||||
|
|
||||||
|
def start_session(self, resp:Response):
|
||||||
|
resp.set_cookie(
|
||||||
|
key = '_id',
|
||||||
|
value = self._id,
|
||||||
|
max_age = timedelta(days=14) if self.remember else 'Session',
|
||||||
|
path = '/',
|
||||||
|
expires = datetime.utcnow() + timedelta(days=14) if self.remember else 'Session'
|
||||||
|
)
|
||||||
|
if self.remember:
|
||||||
|
resp.set_cookie (
|
||||||
|
key = 'remember',
|
||||||
|
value = 'True',
|
||||||
|
max_age = timedelta(days=14),
|
||||||
|
path = '/',
|
||||||
|
expires = datetime.utcnow() + timedelta(days=14)
|
||||||
|
)
|
||||||
|
|
||||||
|
def register(self):
|
||||||
|
from ..views import get_id_from_cookie
|
||||||
|
user = {
|
||||||
|
'_id': self._id,
|
||||||
|
'email': encrypt(self.email),
|
||||||
|
'password': generate_password_hash(self.password, method='sha256'),
|
||||||
|
'username': encrypt(self.username)
|
||||||
|
}
|
||||||
|
if decrypt_find_one(db.users, { 'username': self.username }):
|
||||||
|
return jsonify({ 'error': f'Username {self.username} is not available.' }), 400
|
||||||
|
if db.users.insert_one(user):
|
||||||
|
flash(f'User {self.username} has been created successfully. You may now use it to log in and administer the tests.', 'success')
|
||||||
|
resp = make_response(jsonify(user), 200)
|
||||||
|
if not get_id_from_cookie:
|
||||||
|
self.start_session(resp)
|
||||||
|
return resp
|
||||||
|
return jsonify({ 'error': f'Registration failed. An error occurred.' }), 400
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
user = decrypt_find_one( db.users, { 'username': self.username })
|
||||||
|
if not user:
|
||||||
|
return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401
|
||||||
|
if not check_password_hash( user['password'], self.password ):
|
||||||
|
return jsonify({ 'error': f'The password you entered is incorrect.' }), 401
|
||||||
|
resp = make_response(jsonify({ 'success': f'Successfully logged in user {self.username}.' }), 200)
|
||||||
|
self._id = user['_id']
|
||||||
|
self.start_session(resp)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
resp = make_response(redirect(url_for('admin_auth.login')))
|
||||||
|
resp.set_cookie(
|
||||||
|
key = '_id',
|
||||||
|
value = '',
|
||||||
|
max_age = timedelta(days=-1),
|
||||||
|
path = '/',
|
||||||
|
expires= datetime.utcnow() + timedelta(days=-1)
|
||||||
|
)
|
||||||
|
resp.set_cookie (
|
||||||
|
key = 'cookie_consent',
|
||||||
|
value = 'True',
|
||||||
|
max_age = 'Session',
|
||||||
|
path = '/',
|
||||||
|
expires = 'Session'
|
||||||
|
)
|
||||||
|
resp.set_cookie (
|
||||||
|
key = 'remember',
|
||||||
|
value = 'True',
|
||||||
|
max_age = timedelta(days=-1),
|
||||||
|
path = '/',
|
||||||
|
expires = datetime.utcnow() + timedelta(days=-1)
|
||||||
|
)
|
||||||
|
flash('You have been logged out. All cookies pertaining to your account have been deleted. Have a nice day.', 'alert')
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def reset_password(self):
|
||||||
|
user = decrypt_find_one(db.users, { 'username': self.username })
|
||||||
|
if not user:
|
||||||
|
return jsonify({ 'error': f'Username {self.username} does not exist.' }), 401
|
||||||
|
if not decrypt(user['email']) == self.email:
|
||||||
|
return jsonify({ 'error': f'The email address {self.email} does not match the user account {self.username}.' }), 401
|
||||||
|
new_password = secrets.token_hex(12)
|
||||||
|
reset_token = secrets.token_urlsafe(16)
|
||||||
|
verification_token = secrets.token_urlsafe(16)
|
||||||
|
user['password'] = generate_password_hash(new_password, method='sha256')
|
||||||
|
if encrypted_update( { 'username': self.username }, { '$set': {'password': user['password'], 'reset_token': reset_token, 'verification_token': verification_token } } ):
|
||||||
|
flash(f'Your password has been reset. Instructions to recover your account have been sent to {self.email}. Please be sure to check your spam folder in case you have not received the email.', 'alert')
|
||||||
|
email = Message(
|
||||||
|
subject = 'RefTest | Password Reset',
|
||||||
|
recipients = [self.email],
|
||||||
|
body = f"""
|
||||||
|
Hello {user['username']}, \n\n
|
||||||
|
This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app.\n\n
|
||||||
|
If you did not make this request, please ignore this email.\n\n
|
||||||
|
If you did make this request, then you have two options to recover your account.\n\n
|
||||||
|
For the time being, your password has been reset to the following:\n\n
|
||||||
|
{new_password}\n\n
|
||||||
|
You may use this to log back in to your account, and subsequently change your password to something more suitable.\n\n
|
||||||
|
Alternatively, you may visit the following private link using your unique token to override your password. Copy and paste the following link in a web browser. Please note that this token is only valid once:\n\n
|
||||||
|
{url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}\n\n
|
||||||
|
Have a nice day.
|
||||||
|
""",
|
||||||
|
html = f"""
|
||||||
|
<p>Hello {user['username']},</p>
|
||||||
|
<p>This email was generated because we received a request to reset the password for your administrator account for the SKA RefTest app. </p>
|
||||||
|
<p>If you did not make this request, please ignore this email.</p>
|
||||||
|
<p>If you did make this request, then you have two options to recover your account.</p>
|
||||||
|
<p>For the time being, your password has been reset to the following:</p>
|
||||||
|
<strong>{new_password}</strong>
|
||||||
|
<p>You may use this to log back in to your account, and subsequently change your password to something more suitable.</p>
|
||||||
|
<p>Alternatively, you may visit the following private link using your unique token to override your password. Click on the following link, or copy and paste it into a browser. <strong>Please note that this token is only valid once</strong>:</p>
|
||||||
|
<p><a href='{url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}'>{url_for('admin_auth.reset_gateway', token1 = reset_token, token2 = verification_token, _external = True)}</a></p>
|
||||||
|
<p>Have a nice day.</p>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
mail.send(email)
|
||||||
|
return jsonify({ 'success': 'Password reset request has been processed.'}), 200
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
from ..views import get_id_from_cookie
|
||||||
|
retrieved_user = decrypt_find_one(db.users, { '_id': self._id })
|
||||||
|
if not retrieved_user:
|
||||||
|
return jsonify({ 'error': f'User {retrieved_user["username"]} does not exist.' }), 401
|
||||||
|
user = {}
|
||||||
|
updated = []
|
||||||
|
if not self.email == '' and self.email is not None:
|
||||||
|
user['email'] = self.email
|
||||||
|
updated.append('email')
|
||||||
|
if not self.password == '' and self.password is not None:
|
||||||
|
user['password'] = generate_password_hash(self.password, method='sha256')
|
||||||
|
updated.append('password')
|
||||||
|
output = ''
|
||||||
|
if len(updated) == 0:
|
||||||
|
flash(f'There were no changes requested for your account.', 'alert'), 200
|
||||||
|
return jsonify({'success': 'There were no changes requested for your account.'}), 200
|
||||||
|
elif len(updated) == 1:
|
||||||
|
output = updated[0]
|
||||||
|
elif len(updated) == 2:
|
||||||
|
output = ' and '.join(updated)
|
||||||
|
elif len(updated) > 2:
|
||||||
|
output = updated[0]
|
||||||
|
for index in range(1,len(updated)):
|
||||||
|
if index < len(updated) - 2:
|
||||||
|
output = ', '.join([output, updated[index]])
|
||||||
|
elif index == len(updated) - 2:
|
||||||
|
output = ', and '.join([output, updated[index]])
|
||||||
|
else:
|
||||||
|
output = ''.join([output, updated[index]])
|
||||||
|
encrypted_update(db.users, {'_id': self._id}, { '$set': user })
|
||||||
|
if self._id == get_id_from_cookie():
|
||||||
|
_output = 'Your '
|
||||||
|
elif retrieved_user['username'][-1] == 's':
|
||||||
|
_output = '’'.join([retrieved_user['username'], ''])
|
||||||
|
else:
|
||||||
|
_output = '’'.join([retrieved_user['username'], 's'])
|
||||||
|
_output = f'{_output} {output} {"has" if len(updated) == 1 else "have"} been updated.'
|
||||||
|
flash(_output)
|
||||||
|
return jsonify({'success': _output}), 200
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
retrieved_user = decrypt_find_one(db.users, { '_id': self._id })
|
||||||
|
if not retrieved_user:
|
||||||
|
return jsonify({ 'error': f'User does not exist.' }), 401
|
||||||
|
db.users.find_one_and_delete({'_id': self._id})
|
||||||
|
flash(f'User {retrieved_user["username"]} has been deleted.')
|
||||||
|
return jsonify({'success': f'User {retrieved_user["username"]} has been deleted.'}), 200
|
266
ref-test/admin/views.py
Normal file
266
ref-test/admin/views.py
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
from flask import Blueprint, render_template, flash, redirect, request, jsonify, abort
|
||||||
|
from flask.helpers import url_for
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from werkzeug.security import check_password_hash
|
||||||
|
from security.database import decrypt_find, decrypt_find_one
|
||||||
|
from .user.models import User
|
||||||
|
from flask_mail import Message
|
||||||
|
from main import db
|
||||||
|
from uuid import uuid4
|
||||||
|
import secrets
|
||||||
|
from main import mail
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from .forms import CreateUserForm
|
||||||
|
|
||||||
|
cookie_consent = Blueprint(
|
||||||
|
'cookie_consent',
|
||||||
|
__name__
|
||||||
|
)
|
||||||
|
@cookie_consent.route('/')
|
||||||
|
def _cookies():
|
||||||
|
resp = redirect('/')
|
||||||
|
resp.set_cookie(
|
||||||
|
key = 'cookie_consent',
|
||||||
|
value = 'True',
|
||||||
|
max_age = timedelta(days=14) if request.cookies.get('remember') == 'True' else 'Session',
|
||||||
|
path = '/',
|
||||||
|
expires = datetime.utcnow() + timedelta(days=14) if request.cookies.get('remember') else 'Session'
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
views = Blueprint(
|
||||||
|
'admin_views',
|
||||||
|
__name__,
|
||||||
|
template_folder='templates',
|
||||||
|
static_folder='static'
|
||||||
|
)
|
||||||
|
|
||||||
|
def admin_account_required(function):
|
||||||
|
@wraps(function)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not db.users.find_one({}):
|
||||||
|
flash('No administrator accounts have been registered. Please register an administrator account.', 'alert')
|
||||||
|
return redirect(url_for('admin_auth.register'))
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
def disable_on_registration(function):
|
||||||
|
@wraps(function)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if db.users.find_one({}):
|
||||||
|
return abort(404)
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
def get_id_from_cookie():
|
||||||
|
return request.cookies.get('_id')
|
||||||
|
|
||||||
|
def get_user_from_db(_id):
|
||||||
|
return db.users.find_one({'_id': _id})
|
||||||
|
|
||||||
|
def check_login():
|
||||||
|
_id = get_id_from_cookie()
|
||||||
|
return True if get_user_from_db(_id) else False
|
||||||
|
|
||||||
|
def login_required(function):
|
||||||
|
@wraps(function)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not check_login():
|
||||||
|
flash('Please log in to view this page.', 'alert')
|
||||||
|
return redirect(url_for('admin_auth.login'))
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
def disable_if_logged_in(function):
|
||||||
|
@wraps(function)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if check_login():
|
||||||
|
return abort(404)
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
@views.route('/')
|
||||||
|
@views.route('/home/')
|
||||||
|
@views.route('/dashboard/')
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def home():
|
||||||
|
return render_template('/admin/index.html')
|
||||||
|
|
||||||
|
@views.route('/settings/')
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def settings():
|
||||||
|
return render_template('/admin/settings/index.html')
|
||||||
|
|
||||||
|
@views.route('/settings/users/', methods=['GET','POST'])
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def users():
|
||||||
|
form = CreateUserForm()
|
||||||
|
if request.method == 'GET':
|
||||||
|
users_list = decrypt_find(db.users, {})
|
||||||
|
return render_template('/admin/settings/users.html', users = users_list, form = form)
|
||||||
|
if request.method == 'POST':
|
||||||
|
if form.validate_on_submit():
|
||||||
|
entry = User(
|
||||||
|
_id = uuid4().hex,
|
||||||
|
username = request.form.get('username').lower(),
|
||||||
|
email = request.form.get('email'),
|
||||||
|
password = request.form.get('password') if not request.form.get('password') == '' else secrets.token_hex(12),
|
||||||
|
)
|
||||||
|
email = Message(
|
||||||
|
subject = 'RefTest | Registration Confirmation',
|
||||||
|
recipients = [entry.email],
|
||||||
|
body = f"""
|
||||||
|
Hello {entry.username}, \n\n
|
||||||
|
You have been registered as an administrator for the SKA RefTest App!\n\n
|
||||||
|
You can access your account using the username '{entry.username}'.\n\n
|
||||||
|
Your password is as follows:\n\n
|
||||||
|
{entry.password}\n\n
|
||||||
|
You can change your password by logging in to the admin console by copying the following URL into a web browser:\n\n
|
||||||
|
{url_for('admin_views.home', _external = True)}\n\n
|
||||||
|
Have a nice day.
|
||||||
|
""",
|
||||||
|
html = f"""
|
||||||
|
<p>Hello {entry.username},</p>
|
||||||
|
<p>You have been registered as an administrator for the SKA RefTest App!</p>
|
||||||
|
<p>You can access your account using the username '{entry.username}'.</p>
|
||||||
|
<p>Your password is as follows:</p>
|
||||||
|
<strong>{entry.password}</strong>
|
||||||
|
<p>You can change your password by logging in to the admin console at the link below:</p>
|
||||||
|
<p><a href='{url_for('admin_views.home', _external = True)}'>{url_for('admin_views.home', _external = True)}</a></p>
|
||||||
|
<p>Have a nice day.</p>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
mail.send(email)
|
||||||
|
return entry.register()
|
||||||
|
else:
|
||||||
|
errors = [*form.username.errors, *form.email.errors, *form.password.errors]
|
||||||
|
return jsonify({ 'error': errors}), 400
|
||||||
|
|
||||||
|
@views.route('/settings/users/delete/<string:_id>', methods = ['GET', 'POST'])
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def delete_user(_id:str):
|
||||||
|
if _id == get_id_from_cookie():
|
||||||
|
flash('Cannot delete your own user account.', 'error')
|
||||||
|
return redirect(url_for('admin_views.users'))
|
||||||
|
from .forms import DeleteUserForm
|
||||||
|
form = DeleteUserForm()
|
||||||
|
user = decrypt_find_one(db.users, {'_id': _id})
|
||||||
|
if request.method == 'GET':
|
||||||
|
if not user:
|
||||||
|
return abort(404)
|
||||||
|
return render_template('/admin/settings/delete-user.html', form = form, _id = _id, user = user)
|
||||||
|
if request.method == 'POST':
|
||||||
|
if not user:
|
||||||
|
return jsonify({ 'error': 'User does not exist.' }), 404
|
||||||
|
if form.validate_on_submit():
|
||||||
|
_user = decrypt_find_one(db.users, {'_id': get_id_from_cookie()})
|
||||||
|
password = request.form.get('password')
|
||||||
|
if not check_password_hash(_user['password'], password):
|
||||||
|
return jsonify({ 'error': 'The password you entered is incorrect.' }), 401
|
||||||
|
if request.form.get('notify'):
|
||||||
|
email = Message(
|
||||||
|
subject = 'RefTest | Account Deletion',
|
||||||
|
recipients = [user['email']],
|
||||||
|
bcc = [_user['email']],
|
||||||
|
body = f"""
|
||||||
|
Hello {user['username']}, \n\n
|
||||||
|
Your administrator account for the SKA RefTest App has been deleted by {_user['username']}. All data about your account has been deleted.\n\n
|
||||||
|
If you believe this was done in error, please contact them immediately.\n\n
|
||||||
|
If you would like to register to administer the app, please ask an existing administrator to create a new account.\n\n
|
||||||
|
Have a nice day.
|
||||||
|
""",
|
||||||
|
html = f"""
|
||||||
|
<p>Hello {user['username']},</p>
|
||||||
|
<p>Your administrator account for the SKA RefTest App has been deleted by {_user['username']}. All data about your account has been deleted.</p>
|
||||||
|
<p>If you believe this was done in error, please contact them immediately.</p>
|
||||||
|
<p>If you would like to register to administer the app, please ask an existing administrator to create a new account.</p>
|
||||||
|
<p>Have a nice day.</p>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
mail.send(email)
|
||||||
|
user = User(
|
||||||
|
_id = user['_id']
|
||||||
|
)
|
||||||
|
return user.delete()
|
||||||
|
else: return abort(400)
|
||||||
|
|
||||||
|
@views.route('/settings/users/update/<string:_id>', methods = ['GET', 'POST'])
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def update_user(_id:str):
|
||||||
|
if _id == get_id_from_cookie():
|
||||||
|
flash('Cannot delete your own user account.', 'error')
|
||||||
|
return redirect(url_for('admin_views.users'))
|
||||||
|
from .forms import UpdateUserForm
|
||||||
|
form = UpdateUserForm()
|
||||||
|
user = decrypt_find_one( db.users, {'_id': _id})
|
||||||
|
if request.method == 'GET':
|
||||||
|
if not user:
|
||||||
|
return abort(404)
|
||||||
|
return render_template('/admin/settings/update-user.html', form = form, _id = _id, user = user)
|
||||||
|
if request.method == 'POST':
|
||||||
|
if not user:
|
||||||
|
return jsonify({ 'error': 'User does not exist.' }), 404
|
||||||
|
if form.validate_on_submit():
|
||||||
|
_user = decrypt_find_one(db.users, {'_id': get_id_from_cookie()})
|
||||||
|
password = request.form.get('password')
|
||||||
|
if not check_password_hash(_user['password'], password):
|
||||||
|
return jsonify({ 'error': 'The password you entered is incorrect.' }), 401
|
||||||
|
if request.form.get('notify'):
|
||||||
|
recipient = request.form.get('email') if not request.form.get('email') == '' else user['email']
|
||||||
|
email = Message(
|
||||||
|
subject = 'RefTest | Account Update',
|
||||||
|
recipients = [recipient],
|
||||||
|
bcc = [_user['email']],
|
||||||
|
body = f"""
|
||||||
|
Hello {user['username']}, \n\n
|
||||||
|
Your administrator account for the SKA RefTest App has been updated by {_user['username']}.\n\n
|
||||||
|
Your new account details are as follows:\n\n
|
||||||
|
Email: {recipient}\n
|
||||||
|
Password: {request.form.get('password')}\n\n
|
||||||
|
You can update your email and password by logging in to the app.\n\n
|
||||||
|
Have a nice day.
|
||||||
|
""",
|
||||||
|
html = f"""
|
||||||
|
<p>Hello {user['username']},</p>
|
||||||
|
<p>Your administrator account for the SKA RefTest App has been updated by {_user['username']}.</p>
|
||||||
|
<p>Your new account details are as follows:</p>
|
||||||
|
<p>Email: {recipient} <br/>Password: {request.form.get('password')}</p>
|
||||||
|
<p>You can update your email and password by logging in to the app.</p>
|
||||||
|
<p>Have a nice day.</p>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
mail.send(email)
|
||||||
|
entry = User(
|
||||||
|
_id = _id,
|
||||||
|
email = request.form.get('email'),
|
||||||
|
password = request.form.get('password')
|
||||||
|
)
|
||||||
|
return entry.update()
|
||||||
|
else:
|
||||||
|
errors = [*form.user_password.errors, *form.email.errors, *form.password.errors, *form.password_reenter.errors]
|
||||||
|
return jsonify({ 'error': errors}), 400
|
||||||
|
|
||||||
|
@views.route('/settings/questions/')
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def questions():
|
||||||
|
return render_template('/admin/settings/questions.html')
|
||||||
|
|
||||||
|
@views.route('/settings/questions/upload/')
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def upload_questions():
|
||||||
|
return render_template('/admin/settings/upload-questions.html')
|
||||||
|
|
||||||
|
@views.route('/tests/')
|
||||||
|
@admin_account_required
|
||||||
|
@login_required
|
||||||
|
def tests():
|
||||||
|
return render_template('/admin/tests.html')
|
51
ref-test/config.py
Normal file
51
ref-test/config.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
DEBUG = False
|
||||||
|
TESTING = False
|
||||||
|
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
MONGO_INITDB_DATABASE = os.getenv('MONGO_INITDB_DATABASE')
|
||||||
|
from urllib import parse
|
||||||
|
MONGO_URI = f'mongodb://{os.getenv("MONGO_INITDB_USERNAME")}:{parse.quote_plus(os.getenv("MONGO_INITDB_PASSWORD"))}@{os.getenv("MONGO_DB_HOST_ALIAS")}:{os.getenv("MONGO_PORT")}/'
|
||||||
|
|
||||||
|
APP_HOST = '0.0.0.0'
|
||||||
|
|
||||||
|
SESSION_COOKIE_SECURE = True
|
||||||
|
|
||||||
|
MAIL_SERVER = os.getenv("MAIL_SERVER")
|
||||||
|
MAIL_PORT = int(os.getenv("MAIL_PORT"))
|
||||||
|
MAIL_USE_TLS = False
|
||||||
|
MAIL_USE_SSL = False
|
||||||
|
MAIL_DEBUG = False
|
||||||
|
MAIL_USERNAME = os.getenv("MAIL_USERNAME")
|
||||||
|
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD")
|
||||||
|
MAIL_DEFAULT_SENDER = os.getenv("MAIL_DEFAULT_SENDER")
|
||||||
|
MAIL_MAX_EMAILS = int(os.getenv("MAIL_MAX_EMAILS"))
|
||||||
|
MAIL_SUPPRESS_SEND = False
|
||||||
|
MAIL_ASCII_ATTACHMENTS = bool(os.getenv("MAIL_ASCII_ATTACHMENTS"))
|
||||||
|
|
||||||
|
class ProductionConfig(Config):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DevelopmentConfig(Config):
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
DEBUG = True
|
||||||
|
SESSION_COOKIE_SECURE = False
|
||||||
|
MONGO_INITDB_DATABASE = os.getenv('MONGO_INITDB_DATABASE')
|
||||||
|
from urllib import parse
|
||||||
|
MONGO_URI = f'mongodb://{os.getenv("MONGO_INITDB_USERNAME")}:{parse.quote_plus(os.getenv("MONGO_INITDB_PASSWORD"))}@localhost:{os.getenv("MONGO_PORT")}/'
|
||||||
|
APP_HOST = '127.0.0.1'
|
||||||
|
MAIL_DEBUG = True
|
||||||
|
MAIL_SUPPRESS_SEND = False
|
||||||
|
|
||||||
|
class TestingConfig(Config):
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
TESTING = True
|
||||||
|
SESSION_COOKIE_SECURE = False
|
||||||
|
MAIL_DEBUG = True
|
||||||
|
MAIL_SUPPRESS_SEND = False
|
83
ref-test/main.py
Normal file
83
ref-test/main.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import Flask, flash, request
|
||||||
|
from flask.helpers import url_for
|
||||||
|
from flask.json import jsonify
|
||||||
|
from flask_bootstrap import Bootstrap
|
||||||
|
from pymongo import MongoClient
|
||||||
|
from pymongo.errors import ConnectionFailure
|
||||||
|
from flask_wtf.csrf import CSRFProtect, CSRFError
|
||||||
|
from flask_mail import Mail
|
||||||
|
|
||||||
|
from security import check_keyfile_exists, generate_keyfile
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object('config.DevelopmentConfig')
|
||||||
|
|
||||||
|
Bootstrap(app)
|
||||||
|
csrf = CSRFProtect(app)
|
||||||
|
|
||||||
|
@app.errorhandler(CSRFError)
|
||||||
|
def csrf_error_handler(error):
|
||||||
|
return jsonify({ 'error': 'Could not validate a secure connection.'} ), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
mongo = MongoClient(app.config['MONGO_URI'])
|
||||||
|
db = mongo[app.config['MONGO_INITDB_DATABASE']]
|
||||||
|
except ConnectionFailure as error:
|
||||||
|
print(error)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mail = Mail(app)
|
||||||
|
except Exception as error:
|
||||||
|
print(error)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
if not check_keyfile_exists():
|
||||||
|
generate_keyfile()
|
||||||
|
|
||||||
|
from admin.views import views as admin_views, cookie_consent
|
||||||
|
from admin.auth import auth as admin_auth
|
||||||
|
from admin.results import results
|
||||||
|
from quiz.views import views as quiz_views
|
||||||
|
from quiz.auth import auth as quiz_auth
|
||||||
|
|
||||||
|
from admin.user.views import user as admin_user
|
||||||
|
|
||||||
|
app.register_blueprint(quiz_views, url_prefix = '/')
|
||||||
|
app.register_blueprint(quiz_auth, url_prefix = '/')
|
||||||
|
app.register_blueprint(admin_views, url_prefix = '/admin/')
|
||||||
|
app.register_blueprint(admin_auth, url_prefix = '/admin/')
|
||||||
|
app.register_blueprint(results, url_prefix = '/admin/results/')
|
||||||
|
app.register_blueprint(admin_user, url_prefix = '/admin/user/')
|
||||||
|
|
||||||
|
app.register_blueprint(cookie_consent, url_prefix = '/cookies/')
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def check_cookie_consent():
|
||||||
|
if request.cookies.get('cookie_consent') == 'True':
|
||||||
|
return
|
||||||
|
if any([ request.path.startswith(x) for x in [ '/admin/static/', '/static/', '/cookies/' ] ]):
|
||||||
|
return
|
||||||
|
flash(f'<strong>Cookie Consent</strong>: This web site only stores minimal, functional cookies. By using this site, you consent to this use of cookies. For more information, see our <a href="{url_for("quiz_views.privacy")}">privacy policy</a>.', 'cookie_alert')
|
||||||
|
|
||||||
|
from admin.views import check_login, get_user_from_db, get_id_from_cookie
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_now():
|
||||||
|
return {'now': datetime.utcnow()}
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def _check_login():
|
||||||
|
return dict(check_login = check_login)
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def _get_user_from_db():
|
||||||
|
return dict(get_user_from_db = get_user_from_db)
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def _get_id_from_cookie():
|
||||||
|
return dict(get_id_from_cookie = get_id_from_cookie)
|
||||||
|
|
||||||
|
app.run(host=app.config['APP_HOST'])
|
0
ref-test/quiz/__init__.py
Normal file
0
ref-test/quiz/__init__.py
Normal file
8
ref-test/quiz/auth.py
Normal file
8
ref-test/quiz/auth.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
auth = Blueprint(
|
||||||
|
'quiz_auth',
|
||||||
|
__name__,
|
||||||
|
template_folder='templates',
|
||||||
|
static_folder='static'
|
||||||
|
)
|
9
ref-test/quiz/forms.py
Normal file
9
ref-test/quiz/forms.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, PasswordField, BooleanField
|
||||||
|
from wtforms.validators import InputRequired, Email, Length
|
||||||
|
|
||||||
|
class StartQuiz(FlaskForm):
|
||||||
|
given_name = StringField('Given Name', validators=[InputRequired(), Length(max=15)])
|
||||||
|
surname = StringField('Surname', validators=[InputRequired(), Length(max=15)])
|
||||||
|
email = StringField('Email Address', validators=[InputRequired(), Email(message='You must enter a valid email address.'), Length(max=50)])
|
||||||
|
club = StringField('Affiliated Club', validators=[InputRequired(), Length(max=50)])
|
0
ref-test/quiz/models.py
Normal file
0
ref-test/quiz/models.py
Normal file
0
ref-test/quiz/static/css/style.css
Normal file
0
ref-test/quiz/static/css/style.css
Normal file
0
ref-test/quiz/static/js/script.js
Normal file
0
ref-test/quiz/static/js/script.js
Normal file
43
ref-test/quiz/templates/quiz/base.html
Normal file
43
ref-test/quiz/templates/quiz/base.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
|
||||||
|
crossorigin="anonymous">
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.4/css/fontawesome.min.css"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<title>{% block title %} Site Title {% endblock %}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="navbar fixed-top navbar-expand-md navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a href="/" class="navbar-brand mb-0 h1">SKA Refereeing Test </a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- JQuery, Popper, and Bootstrap js dependencies -->
|
||||||
|
<script
|
||||||
|
src="https://code.jquery.com/jquery-3.6.0.slim.min.js"
|
||||||
|
integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI="
|
||||||
|
crossorigin="anonymous">
|
||||||
|
</script>
|
||||||
|
<script
|
||||||
|
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||||
|
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||||
|
crossorigin="anonymous">
|
||||||
|
</script>
|
||||||
|
<!-- Custom js -->
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="{{ url_for('.static', filename='js/script.js') }}"
|
||||||
|
></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
29
ref-test/quiz/views.py
Normal file
29
ref-test/quiz/views.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
views = Blueprint(
|
||||||
|
'quiz_views',
|
||||||
|
__name__,
|
||||||
|
template_folder='templates',
|
||||||
|
static_folder='static'
|
||||||
|
)
|
||||||
|
|
||||||
|
@views.route('/')
|
||||||
|
@views.route('/home/')
|
||||||
|
def home():
|
||||||
|
return f'<h1>Ref Test Home Page</h1>'
|
||||||
|
|
||||||
|
@views.route('/privacy/')
|
||||||
|
def privacy():
|
||||||
|
return f"""<h1>Privacy Policy</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Website stores data using cookies.</li>
|
||||||
|
<li>Site Administrators</li>
|
||||||
|
<li>This web site only uses functional cookies to store information on log-in.</li>
|
||||||
|
<li>User information for administrators will be encrypted and stored in a secure database for the purposes of administering this web site, and will be expunged when the user account is deleted.</li>
|
||||||
|
<li>Test Candidate</li>
|
||||||
|
<li>The web site will not be tracking your log in, and all information about your test attempt will be stored on your device until it is submitted to the server.</li>
|
||||||
|
<li>Data from your test as well as identifying information about the candidate will be encrypted and stored in a secure database on the server, and will be retained for a period of HOW MANY? years for the SKA's records.</li>
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
"""
|
38
ref-test/security/__init__.py
Normal file
38
ref-test/security/__init__.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from os import path
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
def generate_keyfile():
|
||||||
|
with open('./security/.encryption.key', 'wb') as keyfile:
|
||||||
|
key = Fernet.generate_key()
|
||||||
|
keyfile.write(key)
|
||||||
|
|
||||||
|
def load_key():
|
||||||
|
with open('./security/.encryption.key', 'rb') as keyfile:
|
||||||
|
key = keyfile.read()
|
||||||
|
return key
|
||||||
|
|
||||||
|
def check_keyfile_exists():
|
||||||
|
return path.isfile('./security/.encryption.key')
|
||||||
|
|
||||||
|
def encrypt(input:str):
|
||||||
|
input = input.encode()
|
||||||
|
if not check_keyfile_exists():
|
||||||
|
generate_keyfile()
|
||||||
|
_encryption_key = load_key()
|
||||||
|
fernet = Fernet(_encryption_key)
|
||||||
|
output = fernet.encrypt(input)
|
||||||
|
return output.decode()
|
||||||
|
|
||||||
|
def decrypt(input):
|
||||||
|
if not check_keyfile_exists():
|
||||||
|
raise EncryptionKeyMissing
|
||||||
|
input = input.encode()
|
||||||
|
_encryption_key = load_key()
|
||||||
|
fernet = Fernet(_encryption_key)
|
||||||
|
output = fernet.decrypt(input)
|
||||||
|
return output.decode()
|
||||||
|
|
||||||
|
class EncryptionKeyMissing(Exception):
|
||||||
|
def __init__(self, message='There is no encryption keyfile.'):
|
||||||
|
self.message = message
|
||||||
|
super().__init__(self.message)
|
46
ref-test/security/database.py
Normal file
46
ref-test/security/database.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from pymongo import collection
|
||||||
|
from . import encrypt, decrypt
|
||||||
|
encrypted_parameters = ['username', 'email', 'name', 'club']
|
||||||
|
|
||||||
|
def decrypt_find(collection:collection, query:dict):
|
||||||
|
cursor = collection.find({})
|
||||||
|
output_list = []
|
||||||
|
for document in cursor:
|
||||||
|
decrypted_document = {}
|
||||||
|
for key in document:
|
||||||
|
if key not in encrypted_parameters:
|
||||||
|
decrypted_document[key] = document[key]
|
||||||
|
else:
|
||||||
|
decrypted_document[key] = decrypt(document[key])
|
||||||
|
if not query:
|
||||||
|
output_list.append(decrypted_document)
|
||||||
|
else:
|
||||||
|
if set(query.items()).issubset(set(decrypted_document.items())):
|
||||||
|
output_list.append(decrypted_document)
|
||||||
|
return output_list
|
||||||
|
|
||||||
|
def decrypt_find_one(collection:collection, query:dict={}):
|
||||||
|
cursor = decrypt_find(collection=collection, query=query)
|
||||||
|
if cursor: return cursor[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def encrypted_update(collection:collection, query:dict={}, update:dict={}):
|
||||||
|
document = decrypt_find_one(collection=collection, query=query)
|
||||||
|
for update_action in update:
|
||||||
|
key_pairs = update[update_action]
|
||||||
|
if type(key_pairs) is not dict:
|
||||||
|
raise ValueError
|
||||||
|
if update_action == '$set':
|
||||||
|
for key in key_pairs:
|
||||||
|
if key == '_id':
|
||||||
|
raise ValueError
|
||||||
|
document[key] = key_pairs[key]
|
||||||
|
if update_action == '$unset':
|
||||||
|
for key in key_pairs:
|
||||||
|
if key == '_id':
|
||||||
|
raise ValueError
|
||||||
|
if key in document:
|
||||||
|
del document[key]
|
||||||
|
for key in document:
|
||||||
|
document[key] = encrypt(document[key]) if key in encrypted_parameters else document[key]
|
||||||
|
return collection.find_one_and_replace( { '_id': document['_id'] }, document)
|
Loading…
Reference in New Issue
Block a user