changes broken for now, in progress
This commit is contained in:
@ -13,6 +13,7 @@ class User(db.Model, UserMixin):
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(25), unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.Text, nullable=False)
|
||||
role = db.Column(db.String(50), default='user')
|
||||
|
@ -1,81 +1,100 @@
|
||||
# plugins/auth/routes.py
|
||||
# File: plugins/auth/routes.py
|
||||
|
||||
from datetime import datetime
|
||||
from flask import (
|
||||
Blueprint, render_template, redirect, flash, url_for, request
|
||||
Blueprint, render_template, redirect,
|
||||
flash, url_for, request
|
||||
)
|
||||
from flask_login import (
|
||||
login_user, logout_user, login_required, current_user
|
||||
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, AdjustInvitesForm
|
||||
)
|
||||
from .forms import LoginForm, RegistrationForm, InviteForm
|
||||
|
||||
bp = Blueprint(
|
||||
'auth',
|
||||
__name__,
|
||||
template_folder='templates',
|
||||
url_prefix='/auth'
|
||||
'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()
|
||||
user = User.query.filter_by(
|
||||
email=form.email.data.lower()
|
||||
).first()
|
||||
if user and user.check_password(form.password.data):
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
flash('Logged in successfully.', 'success')
|
||||
next_page = request.args.get('next') or url_for('home')
|
||||
return redirect(next_page)
|
||||
flash('Invalid email or password.', 'danger')
|
||||
return render_template('auth/login.html', form=form)
|
||||
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()
|
||||
flash('Logged out.', 'info')
|
||||
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():
|
||||
# Validate invitation
|
||||
# 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('Registration is by invitation only. Provide a valid code.', 'warning')
|
||||
flash('Invalid or expired invitation code.', 'warning')
|
||||
return render_template('auth/register.html', form=form)
|
||||
|
||||
if invitation.recipient_email and \
|
||||
invitation.recipient_email.lower() != form.email.data.lower():
|
||||
flash('This invitation is not valid for that email address.', 'warning')
|
||||
return render_template('auth/register.html', form=form)
|
||||
|
||||
# Create the user
|
||||
user = User(email=form.email.data.lower())
|
||||
# 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 invitation used
|
||||
# Mark the invitation as used
|
||||
invitation.mark_used(user, request.remote_addr)
|
||||
db.session.commit()
|
||||
|
||||
@ -84,55 +103,64 @@ def register():
|
||||
|
||||
return render_template('auth/register.html', form=form)
|
||||
|
||||
|
||||
@bp.route('/invite', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def send_invite():
|
||||
form = InviteForm()
|
||||
|
||||
# Block banned/suspended users from sending
|
||||
if current_user.is_banned or current_user.is_deleted or (
|
||||
current_user.suspended_until and current_user.suspended_until > datetime.utcnow()
|
||||
):
|
||||
flash('You are not permitted to send invitations.', 'warning')
|
||||
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 current_user.invites_remaining < 1:
|
||||
flash('No invites remaining. Ask an admin to increase your quota.', 'danger')
|
||||
if invites_left < 1:
|
||||
flash('No invites remaining.', 'danger')
|
||||
else:
|
||||
recipient = form.email.data.strip().lower()
|
||||
# Create the invitation record
|
||||
inv = Invitation(
|
||||
recipient_email=recipient,
|
||||
recipient_email=form.email.data,
|
||||
created_by=current_user.id,
|
||||
sender_ip=request.remote_addr
|
||||
)
|
||||
db.session.add(inv)
|
||||
current_user.invites_remaining -= 1
|
||||
db.session.commit()
|
||||
flash(f'Invitation sent to {recipient}.', 'success')
|
||||
return redirect(url_for('auth.send_invite'))
|
||||
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=current_user.invites_remaining
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/admin/adjust_invites/<int:user_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def adjust_invites(user_id):
|
||||
# assume current_user has admin rights
|
||||
user = User.query.get_or_404(user_id)
|
||||
form = AdjustInvitesForm()
|
||||
if form.validate_on_submit():
|
||||
user.invites_remaining = max(0, user.invites_remaining + form.delta.data)
|
||||
db.session.commit()
|
||||
flash(f"{user.email}'s invite quota adjusted by {form.delta.data}.", 'info')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
return render_template(
|
||||
'auth/adjust_invites.html',
|
||||
form=form,
|
||||
user=user
|
||||
invites=invites_left
|
||||
)
|
||||
|
@ -1,19 +1,44 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% block title %}Send Invitation{% endblock %}
|
||||
{# File: plugins/auth/templates/auth/invite.html #}
|
||||
{% extends 'core/base.html' %}
|
||||
{% block title %}Send Invitation – Nature In Pots{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4" style="max-width: 500px;">
|
||||
<h2>Send Invitation</h2>
|
||||
<p>You have <strong>{{ invites }}</strong> invites remaining.</p>
|
||||
<form method="POST" action="{{ url_for('auth.send_invite') }}">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
{{ form.email.label(class="form-label") }}
|
||||
{{ form.email(class="form-control", placeholder="recipient@example.com") }}
|
||||
{% for error in form.email.errors %}
|
||||
<div class="text-danger small">{{ error }}</div>
|
||||
{% endfor %}
|
||||
<h1>Send Invitation</h1>
|
||||
|
||||
<p>You have <strong>{{ invites }}</strong> invites remaining.</p>
|
||||
|
||||
<form method="post" class="mb-4">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.email.label(class="form-label") }}
|
||||
{{ form.email(class="form-control", placeholder="Recipient email (optional)") }}
|
||||
<small class="form-text text-muted">
|
||||
Enter the recipient’s email if you want to include it in the invitation record.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Generate Invitation</button>
|
||||
</form>
|
||||
|
||||
{% if inv_link %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
Your Invitation Link
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">{{ form.submit.label.text }}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input type="text"
|
||||
class="form-control mb-3"
|
||||
readonly
|
||||
value="{{ inv_link }}"
|
||||
onclick="this.select();">
|
||||
<label class="form-label">Suggested Message</label>
|
||||
<textarea class="form-control mb-3" rows="3" readonly
|
||||
onclick="this.select();">{{ inv_blurb }}</textarea>
|
||||
<label class="form-label">Email Template</label>
|
||||
<textarea class="form-control" rows="6" readonly
|
||||
onclick="this.select();">{{ email_preview }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
Reference in New Issue
Block a user