changes broken for now, in progress

This commit is contained in:
2025-07-11 05:02:57 -05:00
parent ab2060c711
commit 6d75a8e4bb
389 changed files with 1227 additions and 207 deletions

View File

@ -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')

View File

@ -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
)

View File

@ -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 recipients 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 %}