From f66d726e07aa9d7ae0271dcf5f80c2d79915882f Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Sat, 23 May 2026 22:06:43 +0100 Subject: [PATCH] feat: required Terms + Data Processing consent on register, consent timestamps in admin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Register form: two required checkboxes (Terms of Service / Privacy Policy + UK GDPR data processing) - Zod schema uses z.literal(true) — form won't submit until both are checked - Backend: validates accept_terms + accept_data_processing flags (400 if missing) - User.save() writes created_at, consent_terms_at, consent_data_processing_at to MongoDB - Admin UsersTab: Registered column, email verified badge, consent timestamps in edit dialog - Fix: EU-hosted → UK hosted badge in register form Co-Authored-By: Claude Sonnet 4.6 --- backend/app/models/user.py | 12 +++++- backend/app/routes/auth.py | 10 ++++- src/components/admin/UsersTab.tsx | 41 ++++++++++++++++++- src/pages/Register.tsx | 68 ++++++++++++++++++++++++++++++- 4 files changed, 126 insertions(+), 5 deletions(-) diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 336341d6..67a6d782 100755 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -4,7 +4,8 @@ from app.db import get_db class User: def __init__(self, username, email, password_hash=None, role="user", auth_type="local", - email_verified=False, email_verify_token=None, email_verify_expires=None): + email_verified=False, email_verify_token=None, email_verify_expires=None, + consent_terms_at=None, consent_data_processing_at=None): self.username = username self.email = email self.password_hash = password_hash @@ -13,6 +14,8 @@ class User: self.email_verified = email_verified self.email_verify_token = email_verify_token self.email_verify_expires = email_verify_expires + self.consent_terms_at = consent_terms_at + self.consent_data_processing_at = consent_data_processing_at @staticmethod def hash_password(password): @@ -114,7 +117,9 @@ class User: return result["credits_balance"] if result else 0 async def save(self): + from datetime import timezone db = await get_db() + now = datetime.now(timezone.utc) user_data = { "username": self.username, "email": self.email, @@ -123,11 +128,16 @@ class User: "auth_type": self.auth_type, "credits_balance": 0, "email_verified": self.email_verified, + "created_at": now, } if self.email_verify_token: user_data["email_verify_token"] = self.email_verify_token if self.email_verify_expires: user_data["email_verify_expires"] = self.email_verify_expires + if self.consent_terms_at: + user_data["consent_terms_at"] = self.consent_terms_at + if self.consent_data_processing_at: + user_data["consent_data_processing_at"] = self.consent_data_processing_at result = await db.users.insert_one(user_data) return result.inserted_id diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index d03374ad..812e3a6e 100755 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -1,6 +1,6 @@ import secrets import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from quart import Blueprint, request, jsonify from app.auth.quart_jwt import create_access_token, jwt_required, get_jwt_identity @@ -23,9 +23,15 @@ async def register(): if not data or not data.get('username') or not data.get('email') or not data.get('password'): return jsonify({"message": "Missing required fields"}), 400 + if not data.get('accept_terms'): + return jsonify({"message": "You must accept the Terms of Service"}), 400 + if not data.get('accept_data_processing'): + return jsonify({"message": "You must consent to data processing"}), 400 + username = data['username'].strip() email = data['email'].strip().lower() password = data['password'] + now_utc = datetime.now(timezone.utc) if len(username) < 3: return jsonify({"message": "Username must be at least 3 characters"}), 400 @@ -51,6 +57,8 @@ async def register(): email_verified=False, email_verify_token=verify_token, email_verify_expires=verify_expires, + consent_terms_at=now_utc, + consent_data_processing_at=now_utc, ) user_id = await new_user.save() diff --git a/src/components/admin/UsersTab.tsx b/src/components/admin/UsersTab.tsx index 877b4545..83d938d0 100644 --- a/src/components/admin/UsersTab.tsx +++ b/src/components/admin/UsersTab.tsx @@ -23,6 +23,10 @@ interface User { quota?: { monthly_usd?: number }; cost_mtd?: number; credits_balance?: number; + created_at?: string; + consent_terms_at?: string; + consent_data_processing_at?: string; + email_verified?: boolean; } export default function UsersTab() { @@ -165,6 +169,7 @@ export default function UsersTab() { User + Registered Role Status Credits @@ -176,7 +181,7 @@ export default function UsersTab() { {users.length === 0 && ( - + No users found @@ -186,6 +191,12 @@ export default function UsersTab() {
{u.username}
{u.email}
+ {u.email_verified === false && ( + unverified + )} +
+ + {u.created_at ? new Date(u.created_at).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) : '—'} @@ -245,6 +256,34 @@ export default function UsersTab() { Edit User — {editUser?.username}
+ {/* Registration info */} +
+
+ Registered + + {editUser?.created_at ? new Date(editUser.created_at).toLocaleString('en-GB') : '—'} + +
+
+ Email verified + + {editUser?.email_verified ? 'Yes' : 'No'} + +
+
+ Terms accepted + + {editUser?.consent_terms_at ? new Date(editUser.consent_terms_at).toLocaleString('en-GB') : '—'} + +
+
+ Data processing consent + + {editUser?.consent_data_processing_at ? new Date(editUser.consent_data_processing_at).toLocaleString('en-GB') : '—'} + +
+
+