Development
This commit is contained in:
parent
d8d1d46fd2
commit
eed1d295dc
69 changed files with 3355 additions and 3230 deletions
0
docker/routlin-dash/app/pages/accountcreate/__init__.py
Normal file
0
docker/routlin-dash/app/pages/accountcreate/__init__.py
Normal file
105
docker/routlin-dash/app/pages/accountcreate/action.py
Normal file
105
docker/routlin-dash/app/pages/accountcreate/action.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
from flask import Blueprint, request, session, redirect, flash
|
||||
import json, os, bcrypt, secrets, smtplib
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from email.message import EmailMessage
|
||||
from auth import require_level
|
||||
from config_utils import WEB_APP_DISPLAY_NAME, ACCOUNTS_FILE
|
||||
import sanitize
|
||||
|
||||
bp = Blueprint('accountcreate', __name__)
|
||||
|
||||
CODE_TTL_MIN = 15
|
||||
|
||||
|
||||
def _load_accounts():
|
||||
try:
|
||||
with open(ACCOUNTS_FILE) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {'accounts': []}
|
||||
|
||||
|
||||
def _send_verification_email(to_address, code):
|
||||
host = os.environ.get('SMTP_HOST', '')
|
||||
port = int(os.environ.get('SMTP_PORT', 587))
|
||||
user = os.environ.get('SMTP_USER', '')
|
||||
password = os.environ.get('SMTP_PASSWORD', '')
|
||||
from_addr = os.environ.get('SMTP_FROM', user)
|
||||
|
||||
if not host:
|
||||
raise RuntimeError('SMTP_HOST is not configured.')
|
||||
|
||||
msg = EmailMessage()
|
||||
msg['Subject'] = f'{WEB_APP_DISPLAY_NAME} - Email Verification'
|
||||
msg['From'] = from_addr
|
||||
msg['To'] = to_address
|
||||
msg.set_content(
|
||||
f'Your verification code is: {code}\n\n'
|
||||
f'This code expires in {CODE_TTL_MIN} minutes.\n\n'
|
||||
f'If you did not request this, you can ignore this email.'
|
||||
)
|
||||
|
||||
with smtplib.SMTP(host, port) as smtp:
|
||||
smtp.ehlo()
|
||||
if port != 465:
|
||||
smtp.starttls()
|
||||
if user and password:
|
||||
smtp.login(user, password)
|
||||
smtp.send_message(msg)
|
||||
|
||||
|
||||
@bp.route('/action/create_account', methods=['POST'])
|
||||
@require_level('nothing')
|
||||
def create_account():
|
||||
# Abort if already logged in
|
||||
if session.get('access_level', 'nothing') != 'nothing':
|
||||
return redirect('/view/view_overview')
|
||||
|
||||
email = sanitize.email(request.form.get('email', ''))
|
||||
password = request.form.get('password', '')
|
||||
password_confirm = request.form.get('password_confirm', '')
|
||||
tz = sanitize.timezone(request.form.get('timezone', '').strip())
|
||||
|
||||
if not email or not password or not password_confirm or not tz:
|
||||
flash('All fields are required.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
|
||||
if password != password_confirm:
|
||||
flash('Passwords do not match.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
|
||||
if len(password) < 8:
|
||||
flash('Password must be at least 8 characters.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
|
||||
accounts = _load_accounts().get('accounts', [])
|
||||
account = next((a for a in accounts if a.get('email_address', '').lower() == email), None)
|
||||
|
||||
if account is None:
|
||||
flash('Email address not recognised. Contact your manager.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
|
||||
if account.get('hashed_password'):
|
||||
flash('This account is already set up. Please log in instead.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
|
||||
code = f'{secrets.randbelow(1000000):06d}'
|
||||
expires = (datetime.now(tz=timezone.utc) + timedelta(minutes=CODE_TTL_MIN)).isoformat()
|
||||
|
||||
try:
|
||||
_send_verification_email(account['email_address'], code)
|
||||
except Exception as exc:
|
||||
flash(f'Could not send verification email: {exc}', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
|
||||
session['pending_create_account'] = {
|
||||
'email': account['email_address'],
|
||||
'hashed_password': hashed.decode('utf-8'),
|
||||
'timezone': tz,
|
||||
'code': code,
|
||||
'expires': expires,
|
||||
}
|
||||
|
||||
return redirect('/view/view_verifyemail')
|
||||
103
docker/routlin-dash/app/pages/accountcreate/content.json
Normal file
103
docker/routlin-dash/app/pages/accountcreate/content.json
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"id": "view_createaccount",
|
||||
"client_requirement": "client_is_nothing=",
|
||||
"items": [
|
||||
{
|
||||
"type": "auth_wrapper",
|
||||
"client_requirement": "client_is_nothing=",
|
||||
"items": [
|
||||
{
|
||||
"type": "auth_card",
|
||||
"items": [
|
||||
{
|
||||
"type": "h1",
|
||||
"text": "Complete Your Account"
|
||||
},
|
||||
{
|
||||
"type": "p",
|
||||
"text": "If your email has been pre-registered by a manager, setup your account below."
|
||||
},
|
||||
{
|
||||
"type": "hr"
|
||||
},
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/create_account",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Email Address",
|
||||
"name": "email",
|
||||
"input_type": "text",
|
||||
"placeholder": "you@example.com",
|
||||
"hint": "Must match your pre-registered email address."
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "New Password",
|
||||
"name": "password",
|
||||
"input_type": "password",
|
||||
"placeholder": "Choose a strong password"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Confirm Password",
|
||||
"name": "password_confirm",
|
||||
"input_type": "password",
|
||||
"placeholder": "Repeat your password"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Timezone",
|
||||
"name": "timezone",
|
||||
"input_type": "select",
|
||||
"value": "",
|
||||
"options": "%TIMEZONE_OPTIONS%",
|
||||
"hint": "Used to display timestamps in your local time."
|
||||
},
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/create_account",
|
||||
"method": "post",
|
||||
"text": "Create Account",
|
||||
"class": "btn-full"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "p",
|
||||
"text": "Already have an account?",
|
||||
"link": {
|
||||
"action": "/view/view_login",
|
||||
"text": "Log In"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
"type": "h1",
|
||||
"text": "Already logged in."
|
||||
},
|
||||
{
|
||||
"type": "p",
|
||||
"text": "Your account is already active."
|
||||
},
|
||||
{
|
||||
"type": "spacer"
|
||||
},
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/view/overview",
|
||||
"text": "Go to Overview"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue