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 PRODUCT_DISPLAY_NAME import sanitize bp = Blueprint('action_create_account', __name__) DATA_DIR = '/data' ACCOUNTS_FILE = f'{DATA_DIR}/authorized_accounts.json' 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'{PRODUCT_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_create_account') if password != password_confirm: flash('Passwords do not match.', 'error') return redirect('/view/view_create_account') if len(password) < 8: flash('Password must be at least 8 characters.', 'error') return redirect('/view/view_create_account') 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_create_account') if account.get('hashed_password'): flash('This account is already set up. Please log in instead.', 'error') return redirect('/view/view_create_account') 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_create_account') session['pending_create_account'] = { 'email': account['email_address'], 'hashed_password': hashed.decode('utf-8'), 'timezone': tz, 'code': code, 'expires': expires, } return redirect('/view/view_verify_email')