# File: plugins/auth/routes.py from datetime import datetime from flask import ( Blueprint, render_template, redirect, flash, url_for, request ) from flask_login import ( login_user, logout_user, login_required, current_user ) from sqlalchemy.exc import SQLAlchemyError from app import db from .models import User, Invitation from .forms import LoginForm, RegistrationForm, InviteForm bp = Blueprint( 'auth', __name__, url_prefix='/auth', template_folder='templates' ) @bp.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('home')) form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by( email=form.email.data.lower() ).first() if user and user.check_password(form.password.data): now = datetime.utcnow() # Block banned or suspended accounts if user.is_banned or (user.suspended_until and user.suspended_until > now): flash('Your account is not active.', 'danger') return redirect(url_for('auth.login')) login_user(user, remember=form.remember_me.data) next_page = request.args.get('next') return redirect(next_page or url_for('home')) flash('Invalid email or password.', 'danger') return render_template('auth/login.html', form=form) @bp.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('home')) @bp.route('/register', methods=['GET', 'POST']) def register(): if current_user.is_authenticated: return redirect(url_for('home')) # Pull invite code from query param 'invite' invite_code = request.args.get('invite', '').strip() # Look up a valid, unused invitation invitation_obj = Invitation.query.filter_by( code=invite_code, is_active=True, is_used=False ).first() # Secure the GET: only allow viewing form if invite is valid if request.method == 'GET': if not invitation_obj: flash('Registration is by invitation only. Provide a valid invite link.', 'warning') return redirect(url_for('home')) form = RegistrationForm(invitation_code=invite_code) if form.validate_on_submit(): # Re-validate invitation on POST invitation = Invitation.query.filter_by( code=form.invitation_code.data, is_active=True, is_used=False ).first() if not invitation: flash('Invalid or expired invitation code.', 'warning') return render_template('auth/register.html', form=form) # Create the new user user = User( email=form.email.data.lower(), username=form.username.data ) user.set_password(form.password.data) db.session.add(user) db.session.flush() # Mark the invitation as used invitation.mark_used(user, request.remote_addr) db.session.commit() flash('Account created! Please log in.', 'success') return redirect(url_for('auth.login')) return render_template('auth/register.html', form=form) @bp.route('/invite', methods=['GET', 'POST']) @login_required def send_invite(): now = datetime.utcnow() # Block banned or suspended users from generating invites if current_user.is_banned or (current_user.suspended_until and current_user.suspended_until > now): flash('Your account cannot send invitations.', 'danger') return redirect(url_for('home')) form = InviteForm() invites_left = current_user.invites_remaining if form.validate_on_submit(): if invites_left < 1: flash('No invites remaining.', 'danger') else: # Create the invitation record inv = Invitation( recipient_email=form.email.data, created_by=current_user.id, sender_ip=request.remote_addr ) db.session.add(inv) current_user.invites_remaining = invites_left - 1 try: db.session.commit() except SQLAlchemyError: db.session.rollback() flash('An error occurred creating the invitation.', 'danger') return redirect(url_for('auth.send_invite')) # Build shareable link and messages link = url_for('auth.register', invite=inv.code, _external=True) blurb = ( "I'd like to invite you to join Nature In Pots Community! " f"Use this link to register: {link}" ) email_preview = ( "Subject: Join me on Nature In Pots Community\n\n" "Hi there,\n\n" "I'd love for you to join me on Nature In Pots Community. " "Please click this link to sign up:\n\n" f"{link}\n\n" "See you inside!\n" ) return render_template( 'auth/invite.html', form=form, invites=current_user.invites_remaining, inv_link=link, inv_blurb=blurb, email_preview=email_preview ) # GET: simply render the form if allowed return render_template( 'auth/invite.html', form=form, invites=invites_left )