mail drin
fehlende routen drin
prod-reife erreicht.
TODOS:
Deine aktuellen offenen TODOs
1. Rate-Limiting (Spam-Schutz VOR dem reCAPTCHA)
Das reCAPTCHA hält zwar Bots ab, aber wenn jemand böswillig ein Script schreibt, das 1.000 POST-Requests pro Sekunde an /api/v1/submissions sendet, muss dein Backend jedes Mal Google kontaktieren und die Datenbank befragen. Das frisst Server-Ressourcen.
Todo: Einbinden von Flask-Limiter und Absichern der API-Route (z.B. max. 10 Submissions pro Minute pro IP-Adresse oder Tenant).
2. Billing & Feature-Enforcement (Limits pro Tenant durchsetzen)
Die Mandanten-Architektur und das config-Feld im JSON existieren bereits. Aber wir erzwingen diese Limits aktuell nicht im Backend.
Todo: Im Dashboard (templates/admin/tenants/view.html) UI-Controls einbauen, um Feature-Flags für einen Tenant festzulegen:
max_forms (z.B. 1 für Free-User, 10 für Pro-User).
allow_instant_mail (Dürfen E-Mails an den Tenant gesendet werden?).
allow_autoresponder (Darf der Autoresponder für Endkunden genutzt werden?).
Todo: Diese Limits hart im Backend (beim Erstellen von Forms und beim Mailversand) kontrollieren. (Der Mailer prüft das zwar schon halbwegs, das UI zum Pflegen fehlt aber noch komplett).
3. E-Mail Templates aufwerten (HTML statt Plain-Text)
Bisher versendet der Service E-Mails als reinen Text (plain). Für eine professionelle SaaS-Lösung sollten diese Mails ansprechend aussehen.
Todo: Im services/mailer.py den MIMEMultipart so erweitern, dass er saubere HTML-Mails aus einem Jinja2-Template (templates/emails/...) generiert, inklusive Branding/Logo deines Services.
4. Frontend-Snippet "Graceful Degradation"
Das embed.js arbeitet aktuell sehr gut. Aber was ist, wenn das Formeleon-Backend mal für 5 Minuten down ist?
Todo: Einen sauberen Timeout im fetch einbauen und dem Endkunden auf der Webseite eine schöne, fallback HTML-Meldung anzeigen ("Entschuldigung, das Formular ist momentan nicht erreichbar"), anstatt dass sich das Script lautlos in der Console mit einem CORS/Network-Error verabschiedet.
5. CI/CD Pipeline (Deployment)
Wenn der Code fertig ist, willst du ihn sicher nicht immer händisch per git pull auf deinen Produktionsserver schieben und dort docker compose up tippen.
Todo: Eine simple Pipeline (Github Actions / Gitea Actions) anlegen, die den Code testet und automatisch auf dem Zielserver ausrollt.
817 lines
27 KiB
Python
817 lines
27 KiB
Python
# ==============================================================================
|
|
# Formeleon - Multi-tenant Form Generator
|
|
# Author: Björn Runden <br@runden.net>
|
|
# Module: routes/admin.py
|
|
# Description: Admin Dashboard routes for form management.
|
|
# Provides CRUD operations for tenants, forms, and submissions.
|
|
# ==============================================================================
|
|
|
|
"""
|
|
Admin Dashboard routes for Formeleon.
|
|
|
|
This module provides the management interface for:
|
|
- Tenant management (CRUD)
|
|
- Form configuration (CRUD)
|
|
- Submission viewing and export
|
|
- User management
|
|
- Dashboard analytics
|
|
|
|
Routes:
|
|
GET /admin/ - Dashboard overview
|
|
GET /admin/tenants/ - List all tenants
|
|
GET /admin/tenants/<id> - View/edit tenant
|
|
GET /admin/forms/ - List all forms
|
|
GET /admin/forms/<id> - View/edit form
|
|
GET /admin/forms/create - Create new form
|
|
GET /admin/submissions/ - List submissions
|
|
GET /admin/submissions/<id> - View submission details
|
|
GET /admin/users/ - List users
|
|
GET /admin/users/<id> - View/edit user
|
|
|
|
Authentication is handled via the auth middleware or dev mode bypass.
|
|
"""
|
|
|
|
import uuid
|
|
import logging
|
|
from typing import Optional, List, Dict, Any
|
|
from datetime import datetime, timezone
|
|
|
|
from flask import Blueprint, request, redirect, url_for, session, current_app, jsonify
|
|
from flask import render_template, flash, abort
|
|
|
|
from models import db, Tenant, FormConfig, Submission, User, UserRole, TenantStatus, SubmissionStatus
|
|
from models import AuditLog
|
|
from services.recaptcha import RecaptchaService
|
|
|
|
from services.tenant_admin_service import TenantAdminService
|
|
from services.form_admin_service import FormAdminService
|
|
from services.user_admin_service import UserAdminService
|
|
from services.exporter import ExporterService
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Create admin blueprint
|
|
admin_bp = Blueprint('admin', __name__, template_folder='../templates', static_folder='../static')
|
|
|
|
|
|
def login_required(func):
|
|
"""Decorator to require login for admin routes."""
|
|
def wrapper(*args, **kwargs):
|
|
if not (session.get('user_id') and session.get('authenticated')):
|
|
return redirect(url_for('auth.login', next=request.url))
|
|
return func(*args, **kwargs)
|
|
wrapper.__name__ = func.__name__
|
|
return wrapper
|
|
|
|
|
|
def admin_required(func):
|
|
"""Decorator to require admin privileges."""
|
|
def wrapper(*args, **kwargs):
|
|
if not (session.get('user_id') and session.get('authenticated')):
|
|
return redirect(url_for('auth.login', next=request.url))
|
|
|
|
user = User.query.get(session['user_id'])
|
|
if not user or not user.is_admin():
|
|
abort(403, description=_('Admin privileges required'))
|
|
|
|
return func(*args, **kwargs)
|
|
wrapper.__name__ = func.__name__
|
|
return wrapper
|
|
|
|
|
|
def get_current_user() -> Optional[User]:
|
|
"""Get current authenticated user."""
|
|
if 'user_id' in session:
|
|
return User.query.get(session['user_id'])
|
|
return None
|
|
|
|
|
|
def get_current_tenant() -> Optional[Tenant]:
|
|
"""Get current tenant from session or user context."""
|
|
if 'tenant_id' in session:
|
|
return Tenant.query.get(session['tenant_id'])
|
|
|
|
user = get_current_user()
|
|
if user and user.tenants:
|
|
return user.tenants[0]
|
|
|
|
return None
|
|
|
|
|
|
@admin_bp.before_request
|
|
def before_request():
|
|
"""Before request handler for admin routes."""
|
|
# Ensure user exists in database if authenticated in session
|
|
if session.get('user_id'):
|
|
user = User.query.get(session['user_id'])
|
|
if not user:
|
|
# Clean up invalid session
|
|
session.clear()
|
|
return redirect(url_for('auth.login'))
|
|
|
|
|
|
@admin_bp.route('/')
|
|
@login_required
|
|
def dashboard():
|
|
"""
|
|
Admin dashboard overview.
|
|
|
|
Shows:
|
|
- Number of tenants
|
|
- Number of forms
|
|
- Recent submissions
|
|
- Quick access links
|
|
"""
|
|
user = get_current_user()
|
|
|
|
# Get statistics
|
|
tenant_count = Tenant.query.count()
|
|
form_count = FormConfig.query.count()
|
|
submission_count = Submission.query.count()
|
|
|
|
# Recent submissions
|
|
recent_submissions = Submission.query.filter(Submission.status != SubmissionStatus.ARCHIVED).order_by(
|
|
Submission.created_at.desc()
|
|
).limit(7).all()
|
|
|
|
# Recent forms
|
|
recent_forms = FormConfig.query.order_by(
|
|
FormConfig.created_at.desc()
|
|
).limit(7).all()
|
|
|
|
# Tenant list
|
|
tenants = Tenant.query.order_by(Tenant.created_at.desc()).limit(5).all()
|
|
|
|
return render_template('admin/dashboard.html',
|
|
user=user,
|
|
tenant_count=tenant_count,
|
|
form_count=form_count,
|
|
submission_count=submission_count,
|
|
recent_submissions=recent_submissions,
|
|
recent_forms=recent_forms,
|
|
tenants=tenants,
|
|
dev_mode=current_app.config.get('DEV_MODE', False))
|
|
|
|
|
|
@admin_bp.route('/tenants/')
|
|
@login_required
|
|
def list_tenants():
|
|
"""List all tenants."""
|
|
user = get_current_user()
|
|
|
|
tenants = TenantAdminService.get_tenants(user)
|
|
|
|
return render_template('admin/tenants/list.html',
|
|
user=user,
|
|
tenants=tenants,
|
|
dev_mode=current_app.config.get('DEV_MODE', False))
|
|
|
|
|
|
@admin_bp.route('/tenants/create', methods=['GET', 'POST'])
|
|
@admin_required
|
|
def create_tenant():
|
|
"""Create a new tenant."""
|
|
user = get_current_user()
|
|
|
|
if request.method == 'POST':
|
|
# Validate form data
|
|
name = request.form.get('name', '').strip()
|
|
public_key = request.form.get('public_key', '').strip()
|
|
allowed_origins = request.form.get('allowed_origins', '*').strip()
|
|
status = request.form.get('status', 'active')
|
|
|
|
tenant = TenantAdminService.create_tenant(
|
|
name, public_key, allowed_origins, status, user
|
|
)
|
|
|
|
flash('Tenant created successfully!', 'success')
|
|
return redirect(url_for('admin.list_tenants'))
|
|
|
|
return render_template('admin/tenants/create.html',
|
|
user=user,
|
|
dev_mode=current_app.config.get('DEV_MODE', False))
|
|
|
|
|
|
@admin_bp.route('/tenants/<tenant_id>', methods=['GET', 'POST'])
|
|
@login_required
|
|
def view_tenant(tenant_id):
|
|
"""View and edit a tenant."""
|
|
user = get_current_user()
|
|
tenant = Tenant.query.get(tenant_id)
|
|
|
|
if not tenant:
|
|
abort(404, description='Tenant not found')
|
|
|
|
# Check permissions
|
|
if user.role != UserRole.ADMIN and tenant not in user.tenants:
|
|
abort(403, description='Access denied')
|
|
|
|
if request.method == 'POST':
|
|
if not user.is_admin():
|
|
abort(403, description='Admin privileges required to edit tenant')
|
|
|
|
TenantAdminService.update_tenant(
|
|
tenant,
|
|
request.form.get('name'),
|
|
request.form.get('public_key'),
|
|
request.form.get('allowed_origins'),
|
|
request.form.get('status'),
|
|
user
|
|
)
|
|
|
|
flash('Tenant updated successfully!', 'success')
|
|
return redirect(url_for('admin.view_tenant', tenant_id=tenant.id))
|
|
|
|
# Get tenant forms
|
|
forms = FormConfig.query.filter_by(tenant_id=tenant.id).order_by(
|
|
FormConfig.created_at.desc()
|
|
).all()
|
|
|
|
return render_template('admin/tenants/view.html',
|
|
user=user,
|
|
tenant=tenant,
|
|
forms=forms,
|
|
dev_mode=current_app.config.get('DEV_MODE', False),
|
|
is_admin=user.is_admin())
|
|
|
|
|
|
@admin_bp.route('/tenants/<tenant_id>/delete', methods=['POST'])
|
|
@admin_required
|
|
def delete_tenant(tenant_id):
|
|
"""Delete a tenant."""
|
|
user = get_current_user()
|
|
tenant = Tenant.query.get(tenant_id)
|
|
|
|
if not tenant:
|
|
abort(404, description='Tenant not found')
|
|
|
|
TenantAdminService.delete_tenant(tenant, user)
|
|
|
|
flash('Tenant deleted successfully!', 'success')
|
|
return redirect(url_for('admin.list_tenants'))
|
|
|
|
|
|
@admin_bp.route('/forms/')
|
|
@login_required
|
|
def list_forms():
|
|
"""List all forms for the current tenant/user."""
|
|
user = get_current_user()
|
|
tenant = get_current_tenant()
|
|
|
|
forms = FormAdminService.get_forms(user, tenant)
|
|
|
|
return render_template('admin/forms/list.html',
|
|
user=user,
|
|
forms=forms,
|
|
tenant=tenant,
|
|
dev_mode=current_app.config.get('DEV_MODE', False),
|
|
is_admin=user.is_admin())
|
|
|
|
|
|
@admin_bp.route('/forms/create', methods=['GET', 'POST'])
|
|
@login_required
|
|
def create_form():
|
|
"""Create a new form."""
|
|
user = get_current_user()
|
|
tenant = get_current_tenant()
|
|
|
|
if not tenant:
|
|
flash('No tenant selected. Please select a tenant.', 'error')
|
|
return redirect(url_for('admin.dashboard'))
|
|
|
|
if request.method == 'POST':
|
|
# Get form data
|
|
name = request.form.get('name', '').strip()
|
|
description = request.form.get('description', '').strip()
|
|
|
|
# Parse form structure from JSON
|
|
form_structure_str = request.form.get('form_structure', '{}')
|
|
try:
|
|
import json
|
|
form_structure = json.loads(form_structure_str)
|
|
except json.JSONDecodeError:
|
|
form_structure = {'version': '1.0', 'fields': []}
|
|
|
|
# Parse settings
|
|
settings_str = request.form.get('settings', '{}')
|
|
try:
|
|
settings = json.loads(settings_str)
|
|
except json.JSONDecodeError:
|
|
settings = {}
|
|
|
|
form = FormAdminService.create_form(
|
|
tenant, name, description, form_structure, settings,
|
|
request.form.get('is_active', 'true').lower() == 'true',
|
|
user
|
|
)
|
|
|
|
flash('Form created successfully!', 'success')
|
|
return redirect(url_for('admin.view_form', form_id=form.id))
|
|
|
|
# Provide default form structure for new forms
|
|
default_form_structure = {
|
|
'version': '1.0',
|
|
'fields': [
|
|
{
|
|
'name': 'name',
|
|
'type': 'text',
|
|
'label': 'Your Name',
|
|
'required': True,
|
|
'validation': {'min_length': 2, 'max_length': 100}
|
|
},
|
|
{
|
|
'name': 'email',
|
|
'type': 'email',
|
|
'label': 'Email Address',
|
|
'required': True
|
|
},
|
|
{
|
|
'name': 'message',
|
|
'type': 'textarea',
|
|
'label': 'Message',
|
|
'required': True
|
|
}
|
|
],
|
|
'submit_button': {
|
|
'text': 'Submit',
|
|
'style': 'primary'
|
|
}
|
|
}
|
|
|
|
return render_template('admin/forms/create.html',
|
|
user=user,
|
|
tenant=tenant,
|
|
default_form_structure=default_form_structure,
|
|
dev_mode=current_app.config.get('DEV_MODE', False))
|
|
|
|
|
|
@admin_bp.route('/forms/<form_id>', methods=['GET', 'POST'])
|
|
@login_required
|
|
def view_form(form_id):
|
|
"""View and edit a form."""
|
|
user = get_current_user()
|
|
form = FormConfig.query.get(form_id)
|
|
|
|
if not form:
|
|
abort(404, description='Form not found')
|
|
|
|
# Check permissions
|
|
tenant = Tenant.query.get(form.tenant_id)
|
|
if not tenant:
|
|
abort(404, description='Tenant not found')
|
|
|
|
if user.role != UserRole.ADMIN and tenant not in user.tenants:
|
|
abort(403, description='Access denied')
|
|
|
|
if request.method == 'POST':
|
|
form_structure_str = request.form.get('form_structure', '{}')
|
|
form_structure = None
|
|
try:
|
|
import json
|
|
form_structure = json.loads(form_structure_str)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
settings_str = request.form.get('settings', '{}')
|
|
settings = None
|
|
try:
|
|
import json
|
|
settings = json.loads(settings_str)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
FormAdminService.update_form(
|
|
form, request.form.get('name'), request.form.get('description'),
|
|
request.form.get('is_active', 'true').lower() == 'true',
|
|
form_structure, settings, user, tenant
|
|
)
|
|
|
|
flash('Form updated successfully!', 'success')
|
|
return redirect(url_for('admin.view_form', form_id=form.id))
|
|
|
|
# Get form submissions
|
|
submissions = Submission.query.filter_by(form_config_id=form.id).order_by(
|
|
Submission.created_at.desc()
|
|
).limit(10).all()
|
|
|
|
return render_template('admin/forms/view.html',
|
|
user=user,
|
|
form=form,
|
|
tenant=tenant,
|
|
submissions=submissions,
|
|
dev_mode=current_app.config.get('DEV_MODE', False),
|
|
is_admin=user.is_admin())
|
|
|
|
|
|
@admin_bp.route('/forms/<form_id>/delete', methods=['POST'])
|
|
@login_required
|
|
def delete_form(form_id):
|
|
"""Delete a form."""
|
|
user = get_current_user()
|
|
form = FormConfig.query.get(form_id)
|
|
|
|
if not form:
|
|
abort(404, description='Form not found')
|
|
|
|
tenant = Tenant.query.get(form.tenant_id)
|
|
if not tenant:
|
|
abort(404, description='Tenant not found')
|
|
|
|
# Check permissions
|
|
if user.role != UserRole.ADMIN and tenant not in user.tenants:
|
|
abort(403, description='Access denied')
|
|
|
|
FormAdminService.delete_form(form, user, tenant)
|
|
|
|
flash('Form deleted successfully!', 'success')
|
|
return redirect(url_for('admin.list_forms'))
|
|
|
|
|
|
@admin_bp.route('/submissions/')
|
|
@login_required
|
|
def list_submissions():
|
|
"""List form submissions."""
|
|
user = get_current_user()
|
|
tenant = get_current_tenant()
|
|
|
|
# Check if we should show archived submissions
|
|
show_archived = request.args.get('show_archived') == 'true'
|
|
|
|
# Base query based on user access
|
|
if user.role == UserRole.ADMIN:
|
|
query = Submission.query.join(FormConfig)
|
|
elif tenant:
|
|
query = Submission.query.filter_by(tenant_id=tenant.id)
|
|
else:
|
|
# Return empty template if no access
|
|
return render_template('admin/submissions/list.html', user=user, submissions=[], tenant=tenant)
|
|
|
|
# Filter out archived unless requested
|
|
if not show_archived:
|
|
query = query.filter(Submission.status != SubmissionStatus.ARCHIVED)
|
|
|
|
submissions = query.order_by(Submission.created_at.desc()).all()
|
|
|
|
return render_template('admin/submissions/list.html',
|
|
user=user,
|
|
submissions=submissions,
|
|
tenant=tenant,
|
|
show_archived=show_archived,
|
|
dev_mode=current_app.config.get('DEV_MODE', False),
|
|
is_admin=user.is_admin())
|
|
|
|
@admin_bp.route('/submissions/export')
|
|
@login_required
|
|
def export_submissions():
|
|
"""Export submissions as CSV or JSON."""
|
|
user = get_current_user()
|
|
tenant = get_current_tenant()
|
|
|
|
format_type = request.args.get('format', 'csv')
|
|
form_id = request.args.get('form_id')
|
|
|
|
# Security: Build query based on user access
|
|
if user.role == UserRole.ADMIN:
|
|
query = Submission.query
|
|
elif tenant:
|
|
query = Submission.query.filter_by(tenant_id=tenant.id)
|
|
else:
|
|
abort(403)
|
|
|
|
if form_id:
|
|
query = query.filter_by(form_config_id=form_id)
|
|
|
|
# Exclude archived from default exports
|
|
query = query.filter(Submission.status != SubmissionStatus.ARCHIVED)
|
|
|
|
submissions = query.order_by(Submission.created_at.desc()).all()
|
|
|
|
filename = f"formeleon_export_{datetime.now().strftime('%Y%m%d')}"
|
|
if form_id:
|
|
filename += f"_{form_id}"
|
|
|
|
if format_type == 'json':
|
|
return ExporterService.generate_json_response(submissions, f"{filename}.json")
|
|
else:
|
|
return ExporterService.generate_csv_response(submissions, f"{filename}.csv")
|
|
|
|
@admin_bp.route('/submissions/bulk-action', methods=['POST'])
|
|
@login_required
|
|
def bulk_action_submissions():
|
|
"""Perform bulk actions (like archive) on multiple submissions."""
|
|
user = get_current_user()
|
|
tenant = get_current_tenant()
|
|
|
|
action = request.form.get('action')
|
|
submission_ids = request.form.getlist('submission_ids')
|
|
|
|
if not action or not submission_ids:
|
|
flash('No action or items selected.', 'warning')
|
|
return redirect(url_for('admin.list_submissions'))
|
|
|
|
# Security check: Ensure user can modify these submissions
|
|
if user.role == UserRole.ADMIN:
|
|
subs_to_update = Submission.query.filter(Submission.id.in_(submission_ids)).all()
|
|
elif tenant:
|
|
subs_to_update = Submission.query.filter(Submission.id.in_(submission_ids), Submission.tenant_id == tenant.id).all()
|
|
else:
|
|
abort(403)
|
|
|
|
count = 0
|
|
if action == 'archive':
|
|
for sub in subs_to_update:
|
|
sub.status = SubmissionStatus.ARCHIVED
|
|
count += 1
|
|
|
|
db.session.commit()
|
|
flash(f'Successfully archived {count} submissions.', 'success')
|
|
|
|
elif action == 'unarchive':
|
|
for sub in subs_to_update:
|
|
sub.status = SubmissionStatus.PROCESSED
|
|
count += 1
|
|
|
|
db.session.commit()
|
|
flash(f'Successfully unarchived {count} submissions.', 'success')
|
|
|
|
return redirect(url_for('admin.list_submissions'))
|
|
|
|
|
|
@admin_bp.route('/submissions/<submission_id>')
|
|
@login_required
|
|
def view_submission(submission_id):
|
|
"""View submission details."""
|
|
user = get_current_user()
|
|
submission = Submission.query.get(submission_id)
|
|
|
|
if not submission:
|
|
abort(404, description='Submission not found')
|
|
|
|
# Check permissions
|
|
form = FormConfig.query.get(submission.form_config_id)
|
|
tenant = Tenant.query.get(submission.tenant_id)
|
|
|
|
if not form or not tenant:
|
|
abort(404, description='Related form or tenant not found')
|
|
|
|
if user.role != UserRole.ADMIN and tenant not in user.tenants:
|
|
abort(403, description='Access denied')
|
|
|
|
return render_template('admin/submissions/view.html',
|
|
user=user,
|
|
submission=submission,
|
|
form=form,
|
|
tenant=tenant,
|
|
dev_mode=current_app.config.get('DEV_MODE', False),
|
|
is_admin=user.is_admin())
|
|
|
|
|
|
@admin_bp.route('/submissions/<submission_id>/status', methods=['POST'])
|
|
@login_required
|
|
def update_submission_status(submission_id):
|
|
"""Update submission status."""
|
|
user = get_current_user()
|
|
submission = Submission.query.get(submission_id)
|
|
|
|
if not submission:
|
|
abort(404, description='Submission not found')
|
|
|
|
tenant = Tenant.query.get(submission.tenant_id)
|
|
if not tenant:
|
|
abort(404, description='Tenant not found')
|
|
|
|
if user.role != UserRole.ADMIN and tenant not in user.tenants:
|
|
abort(403, description='Access denied')
|
|
|
|
status = request.form.get('status')
|
|
if status in [s.value for s in SubmissionStatus]:
|
|
submission.status = SubmissionStatus(status)
|
|
db.session.commit()
|
|
|
|
# Audit log
|
|
AuditLog.log_action(
|
|
action='update_status',
|
|
resource_type='submission',
|
|
user=user,
|
|
tenant=tenant,
|
|
resource_id=submission.id,
|
|
details={'status': status}
|
|
)
|
|
|
|
flash('Submission status updated!', 'success')
|
|
|
|
return redirect(url_for('admin.view_submission', submission_id=submission.id))
|
|
|
|
|
|
@admin_bp.route('/users/')
|
|
@admin_required
|
|
def list_users():
|
|
"""List all users (admin only)."""
|
|
admin_user = get_current_user()
|
|
users = UserAdminService.get_users()
|
|
|
|
return render_template('admin/users/list.html',
|
|
admin_user=admin_user,
|
|
users=users,
|
|
dev_mode=current_app.config.get('DEV_MODE', False),
|
|
is_admin=admin_user.is_admin())
|
|
|
|
|
|
@admin_bp.route('/users/<user_id>', methods=['GET', 'POST'])
|
|
@admin_required
|
|
def view_user(user_id):
|
|
"""View and edit a user (admin only)."""
|
|
admin_user = get_current_user()
|
|
target_user = User.query.get(user_id)
|
|
|
|
if not target_user:
|
|
abort(404, description='User not found')
|
|
|
|
if request.method == 'POST':
|
|
UserAdminService.update_user(
|
|
target_user, request.form.get('email'), request.form.get('name'),
|
|
request.form.get('role'), admin_user
|
|
)
|
|
|
|
flash('User updated successfully!', 'success')
|
|
return redirect(url_for('admin.view_user', user_id=target_user.id))
|
|
|
|
# Get user tenants
|
|
tenants = target_user.tenants
|
|
|
|
return render_template('admin/users/view.html',
|
|
admin_user=admin_user,
|
|
target_user=target_user,
|
|
tenants=tenants,
|
|
dev_mode=current_app.config.get('DEV_MODE', False),
|
|
UserRole=UserRole)
|
|
|
|
|
|
@admin_bp.route('/users/<user_id>/delete', methods=['POST'])
|
|
@admin_required
|
|
def delete_user(user_id):
|
|
"""Delete a user (admin only)."""
|
|
user = get_current_user()
|
|
target_user = User.query.get(user_id)
|
|
|
|
if not target_user:
|
|
abort(404, description='User not found')
|
|
|
|
# Don't allow deleting self
|
|
if target_user.id == user.id:
|
|
flash('Cannot delete your own account!', 'error')
|
|
return redirect(url_for('admin.view_user', user_id=target_user.id))
|
|
|
|
UserAdminService.delete_user(target_user, user)
|
|
|
|
flash('User deleted successfully!', 'success')
|
|
return redirect(url_for('admin.list_users'))
|
|
|
|
|
|
@admin_bp.route('/users/<user_id>/tenants', methods=['GET', 'POST'])
|
|
@admin_required
|
|
def manage_user_tenants(user_id):
|
|
"""Manage tenant assignments for a user (admin only)."""
|
|
admin_user = get_current_user()
|
|
target_user = User.query.get(user_id)
|
|
|
|
if not target_user:
|
|
abort(404, description='User not found')
|
|
|
|
if request.method == 'POST':
|
|
# Add or remove tenant assignments
|
|
action = request.form.get('action')
|
|
tenant_id = request.form.get('tenant_id')
|
|
|
|
if action == 'add' and tenant_id:
|
|
tenant = Tenant.query.get(tenant_id)
|
|
if tenant and tenant not in target_user.tenants:
|
|
target_user.tenants.append(tenant)
|
|
db.session.commit()
|
|
|
|
AuditLog.log_action(
|
|
action='assign_tenant',
|
|
resource_type='user',
|
|
user=admin_user,
|
|
resource_id=target_user.id,
|
|
details={'tenant_id': tenant_id, 'tenant_name': tenant.name}
|
|
)
|
|
flash(f'User assigned to tenant {tenant.name}!', 'success')
|
|
|
|
elif action == 'remove' and tenant_id:
|
|
tenant = Tenant.query.get(tenant_id)
|
|
if tenant and tenant in target_user.tenants:
|
|
target_user.tenants.remove(tenant)
|
|
db.session.commit()
|
|
|
|
AuditLog.log_action(
|
|
action='remove_tenant',
|
|
resource_type='user',
|
|
user=admin_user,
|
|
resource_id=target_user.id,
|
|
details={'tenant_id': tenant_id, 'tenant_name': tenant.name}
|
|
)
|
|
flash(f'User removed from tenant {tenant.name}!', 'success')
|
|
|
|
return redirect(url_for('admin.manage_user_tenants', user_id=target_user.id))
|
|
|
|
# Get all tenants and user's current tenants
|
|
all_tenants = Tenant.query.order_by(Tenant.name).all()
|
|
user_tenants = list(target_user.tenants)
|
|
|
|
return render_template('admin/users/tenants.html',
|
|
admin_user=admin_user,
|
|
target_user=target_user,
|
|
all_tenants=all_tenants,
|
|
user_tenants=user_tenants,
|
|
dev_mode=current_app.config.get('DEV_MODE', False))
|
|
|
|
|
|
@admin_bp.route('/users/<user_id>/role', methods=['POST'])
|
|
@admin_required
|
|
def update_user_role(user_id):
|
|
"""Update user role (admin only)."""
|
|
admin_user = get_current_user()
|
|
target_user = User.query.get(user_id)
|
|
|
|
if not target_user:
|
|
abort(404, description='User not found')
|
|
|
|
# Don't allow changing own role
|
|
if target_user.id == admin_user.id:
|
|
flash('Cannot change your own role!', 'error')
|
|
return redirect(url_for('admin.view_user', user_id=target_user.id))
|
|
|
|
new_role = request.form.get('role')
|
|
if new_role and new_role in [r.value for r in UserRole]:
|
|
old_role = target_user.role.value
|
|
target_user.role = UserRole(new_role)
|
|
db.session.commit()
|
|
|
|
AuditLog.log_action(
|
|
action='update_role',
|
|
resource_type='user',
|
|
user=admin_user,
|
|
resource_id=target_user.id,
|
|
details={'old_role': old_role, 'new_role': new_role}
|
|
)
|
|
flash(f'User role updated to {new_role}!', 'success')
|
|
else:
|
|
flash('Invalid role!', 'error')
|
|
|
|
return redirect(url_for('admin.view_user', user_id=target_user.id))
|
|
|
|
|
|
@admin_bp.route('/profile')
|
|
@login_required
|
|
def profile():
|
|
"""User profile page."""
|
|
user = get_current_user()
|
|
|
|
if not user:
|
|
return redirect(url_for('auth.login'))
|
|
|
|
return render_template('admin/profile.html',
|
|
user=user,
|
|
dev_mode=current_app.config.get('DEV_MODE', False))
|
|
|
|
|
|
@admin_bp.route('/api/recaptcha-config')
|
|
@login_required
|
|
def get_recaptcha_config():
|
|
"""Get reCAPTCHA configuration for client-side rendering."""
|
|
recaptcha_service = RecaptchaService()
|
|
|
|
return jsonify({
|
|
'enabled': recaptcha_service.is_enabled(),
|
|
'site_key': recaptcha_service.get_site_key(),
|
|
'dev_mode': recaptcha_service.is_dev_mode(),
|
|
'mock_score': recaptcha_service.get_dev_mode_score() if recaptcha_service.is_dev_mode() else None
|
|
})
|
|
|
|
|
|
@admin_bp.route('/settings')
|
|
@admin_required
|
|
def settings():
|
|
"""Application settings (admin only)."""
|
|
user = get_current_user()
|
|
|
|
# Get system statistics
|
|
tenant_count = Tenant.query.count()
|
|
user_count = User.query.count()
|
|
form_count = FormConfig.query.count()
|
|
submission_count = Submission.query.count()
|
|
|
|
# Get audit logs
|
|
audit_logs = AuditLog.query.order_by(AuditLog.created_at.desc()).limit(50).all()
|
|
|
|
return render_template('admin/settings.html',
|
|
user=user,
|
|
tenant_count=tenant_count,
|
|
user_count=user_count,
|
|
form_count=form_count,
|
|
submission_count=submission_count,
|
|
audit_logs=audit_logs,
|
|
dev_mode=current_app.config.get('DEV_MODE', False))
|