167 lines
5.3 KiB
Python
167 lines
5.3 KiB
Python
# 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
|
|
)
|