i18 drin
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.
This commit is contained in:
1 parent
7f81932fa5
commit
5fff17531d
31 files changed
+2683
-550
No files matched your search
+96
-12
@@ -46,6 +46,7 @@ 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__)
|
||||
@@ -72,7 +73,7 @@ def admin_required(func):
|
||||
|
||||
user = User.query.get(session['user_id'])
|
||||
if not user or not user.is_admin():
|
||||
abort(403, description='Admin privileges required')
|
||||
abort(403, description=_('Admin privileges required'))
|
||||
|
||||
return func(*args, **kwargs)
|
||||
wrapper.__name__ = func.__name__
|
||||
@@ -130,14 +131,14 @@ def dashboard():
|
||||
submission_count = Submission.query.count()
|
||||
|
||||
# Recent submissions
|
||||
recent_submissions = Submission.query.order_by(
|
||||
recent_submissions = Submission.query.filter(Submission.status != SubmissionStatus.ARCHIVED).order_by(
|
||||
Submission.created_at.desc()
|
||||
).limit(10).all()
|
||||
).limit(7).all()
|
||||
|
||||
# Recent forms
|
||||
recent_forms = FormConfig.query.order_by(
|
||||
FormConfig.created_at.desc()
|
||||
).limit(5).all()
|
||||
).limit(7).all()
|
||||
|
||||
# Tenant list
|
||||
tenants = Tenant.query.order_by(Tenant.created_at.desc()).limit(5).all()
|
||||
@@ -434,25 +435,108 @@ def list_submissions():
|
||||
user = get_current_user()
|
||||
tenant = get_current_tenant()
|
||||
|
||||
# Get submissions based on user access
|
||||
# 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:
|
||||
submissions = Submission.query.join(FormConfig).order_by(
|
||||
Submission.created_at.desc()
|
||||
).all()
|
||||
query = Submission.query.join(FormConfig)
|
||||
elif tenant:
|
||||
submissions = Submission.query.filter_by(tenant_id=tenant.id).order_by(
|
||||
Submission.created_at.desc()
|
||||
).all()
|
||||
query = Submission.query.filter_by(tenant_id=tenant.id)
|
||||
else:
|
||||
submissions = []
|
||||
# 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
|
||||
|
||||
Reference in new issue
Block a user