from __future__ import annotations from flask import Blueprint, current_app, flash, redirect, render_template, url_for from flask_login import current_user, login_required, login_user, logout_user from ..extensions import db, limiter from ..forms import LoginForm, PasswordResetForm, RegistrationForm, ResetRequestForm from ..models import PasswordResetToken, User from ..services.i18n import translate as _ from ..services.mail import MailService from ..services.settings import get_bool_setting auth_bp = Blueprint('auth', __name__) @auth_bp.route('/login', methods=['GET', 'POST']) @limiter.limit('5 per minute') def login(): if current_user.is_authenticated: return redirect(url_for('main.dashboard')) form = LoginForm() if form.validate_on_submit(): if form.website.data: flash(_('flash.suspicious_request'), 'danger') return redirect(url_for('auth.login')) user = User.query.filter_by(email=form.email.data.lower()).first() if user and user.check_password(form.password.data) and user.is_active_user: login_user(user, remember=form.remember_me.data) flash(_('flash.login_success'), 'success') return redirect(url_for('main.dashboard')) flash(_('flash.invalid_credentials'), 'danger') return render_template('auth/login.html', form=form) @auth_bp.route('/register', methods=['GET', 'POST']) def register(): if not get_bool_setting('registration_enabled', current_app.config['REGISTRATION_ENABLED']): flash(_('flash.registration_disabled'), 'warning') return redirect(url_for('auth.login')) form = RegistrationForm() if form.validate_on_submit(): if form.website.data: flash(_('flash.suspicious_request'), 'danger') return redirect(url_for('auth.register')) if User.query.filter_by(email=form.email.data.lower()).first(): flash(_('flash.email_exists'), 'danger') else: user = User( email=form.email.data.lower(), full_name=form.full_name.data, language=current_app.config['DEFAULT_LANGUAGE'], must_change_password=False, ) user.set_password(form.password.data) db.session.add(user) db.session.commit() flash(_('flash.account_created'), 'success') return redirect(url_for('auth.login')) return render_template('auth/register.html', form=form) @auth_bp.route('/logout') @login_required def logout(): logout_user() flash(_('flash.logged_out'), 'success') return redirect(url_for('auth.login')) @auth_bp.route('/forgot-password', methods=['GET', 'POST']) @limiter.limit('3 per minute') def forgot_password(): form = ResetRequestForm() if form.validate_on_submit(): if form.website.data: flash(_('flash.suspicious_request'), 'danger') return redirect(url_for('auth.forgot_password')) user = User.query.filter_by(email=form.email.data.lower()).first() if user: reset = PasswordResetToken.issue(user) db.session.add(reset) db.session.commit() reset_link = url_for('auth.reset_password', token=reset.token, _external=True) MailService().send_template(user.email, 'Password reset', 'password_reset', reset_link=reset_link, user=user) current_app.logger.info('Reset link for %s: %s', user.email, reset_link) flash(_('flash.reset_link_generated'), 'info') return redirect(url_for('auth.login')) return render_template('auth/forgot_password.html', form=form) @auth_bp.route('/reset-password/', methods=['GET', 'POST']) def reset_password(token: str): reset_entry = PasswordResetToken.query.filter_by(token=token).first_or_404() if not reset_entry.is_valid(): flash(_('flash.reset_invalid'), 'danger') return redirect(url_for('auth.login')) form = PasswordResetForm() if form.validate_on_submit(): reset_entry.user.set_password(form.password.data) reset_entry.user.must_change_password = False reset_entry.used_at = db.func.now() db.session.commit() flash(_('flash.password_updated'), 'success') return redirect(url_for('auth.login')) return render_template('auth/reset_password.html', form=form)