i18 drin
Formeleon Deployment Pipeline / Deploy to Dev Environment (push) Successful in 1m1s
Formeleon Deployment Pipeline / Deploy to Production Environment (push) Skipped

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:
br committed 2026-09-29 05:16:01 +02:00
1 parent 7f81932fa5
commit 5fff17531d
31 files changed
+2683 -550

No files matched your search

+96 -12
View File
@@ -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