from datetime import datetime from flask import Blueprint, render_template, redirect, url_for, flash, request, make_response, current_app from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import check_password_hash import jwt from app.extensions import db from app.models.user import PortalUser bp = Blueprint('auth', __name__) def _issue_portal_cookie(user, response): """Sign and attach the platform-wide JWT cookie to a response.""" import time expiry_hours = current_app.config['JWT_EXPIRY_HOURS'] now = int(time.time()) payload = { 'sub': user.username, 'user_id': user.id, 'email': user.email, 'role': user.role, 'apps': user.get_accessible_apps(), 'iss': 'enterprise-digital-platform', 'iat': now, 'exp': now + expiry_hours * 3600, } token = jwt.encode(payload, current_app.config['PORTAL_JWT_SECRET'], algorithm='HS256') response.set_cookie( current_app.config['PORTAL_COOKIE_NAME'], token, httponly=True, samesite='Lax', max_age=expiry_hours * 3600, path='/', ) return response @bp.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('dashboard.index')) if request.method == 'POST': username = request.form.get('username', '').strip() password = request.form.get('password', '') if not username or not password: flash('Please enter username and password.', 'danger') return render_template('auth/login.html') user = PortalUser.query.filter_by(username=username, is_active=True).first() if user and check_password_hash(user.password_hash, password): user.last_login = datetime.utcnow() db.session.commit() login_user(user, remember=False) next_page = request.args.get('next') or url_for('dashboard.index') resp = make_response(redirect(next_page)) _issue_portal_cookie(user, resp) return resp flash('Invalid username or password.', 'danger') return render_template('auth/login.html') @bp.route('/logout') @login_required def logout(): logout_user() resp = make_response(redirect(url_for('auth.login'))) resp.delete_cookie(current_app.config['PORTAL_COOKIE_NAME'], path='/') flash('You have been signed out.', 'info') return resp @bp.route('/portal-return') def portal_return(): """ Re-establish a portal Flask-Login session from the JWT cookie. Sub-apps link here instead of '/' so the user is always properly logged in to the portal even if the session cookie expired while they were working in a sub-app. """ # Already logged in — just go to the dashboard if current_user.is_authenticated: return redirect(url_for('dashboard.index')) token = request.cookies.get(current_app.config['PORTAL_COOKIE_NAME']) if not token: return redirect(url_for('auth.login')) try: payload = jwt.decode( token, current_app.config['PORTAL_JWT_SECRET'], algorithms=['HS256'], options={'require': ['exp', 'sub', 'user_id']}, ) except jwt.InvalidTokenError: return redirect(url_for('auth.login')) user = PortalUser.query.get(payload.get('user_id')) if not user or not user.is_active: return redirect(url_for('auth.login')) login_user(user, remember=False) resp = make_response(redirect(url_for('dashboard.index'))) _issue_portal_cookie(user, resp) # refresh the JWT while we're here return resp