From e52ecea9d93135b8b532dfc5eb14786cc6581a91 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Wed, 21 Jan 2026 19:48:50 -0300 Subject: [PATCH] portal: collect names for access requests --- backend/atlas_portal/db.py | 4 ++ .../atlas_portal/routes/access_requests.py | 58 ++++++++++++++++--- backend/atlas_portal/routes/admin_access.py | 4 +- frontend/src/views/AccountView.vue | 14 +++++ frontend/src/views/RequestAccessView.vue | 36 +++++++++++- 5 files changed, 107 insertions(+), 9 deletions(-) diff --git a/backend/atlas_portal/db.py b/backend/atlas_portal/db.py index 944bc6b..c827b23 100644 --- a/backend/atlas_portal/db.py +++ b/backend/atlas_portal/db.py @@ -30,6 +30,8 @@ def ensure_schema() -> None: CREATE TABLE IF NOT EXISTS access_requests ( request_code TEXT PRIMARY KEY, username TEXT NOT NULL, + first_name TEXT, + last_name TEXT, contact_email TEXT, note TEXT, status TEXT NOT NULL, @@ -52,6 +54,8 @@ def ensure_schema() -> None: conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMPTZ") conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMPTZ") conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS welcome_email_sent_at TIMESTAMPTZ") + conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS first_name TEXT") + conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS last_name TEXT") conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS approval_flags TEXT[]") conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS approval_note TEXT") conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS denial_note TEXT") diff --git a/backend/atlas_portal/routes/access_requests.py b/backend/atlas_portal/routes/access_requests.py index b09221a..1386d17 100644 --- a/backend/atlas_portal/routes/access_requests.py +++ b/backend/atlas_portal/routes/access_requests.py @@ -22,12 +22,29 @@ from ..provisioning import provision_access_request, provision_tasks_complete from .. import settings -def _extract_request_payload() -> tuple[str, str, str]: +def _extract_request_payload() -> tuple[str, str, str, str, str]: payload = request.get_json(silent=True) or {} username = (payload.get("username") or "").strip() email = (payload.get("email") or "").strip() note = (payload.get("note") or "").strip() - return username, email, note + first_name = (payload.get("first_name") or "").strip() + last_name = (payload.get("last_name") or "").strip() + return username, email, note, first_name, last_name + + +def _normalize_name(value: str) -> str: + return " ".join(value.strip().split()) + + +def _validate_name(value: str, *, label: str, required: bool) -> str | None: + cleaned = _normalize_name(value) + if not cleaned: + return f"{label} is required" if required else None + if len(cleaned) > 80: + return f"{label} must be 1-80 characters" + if any(ch in "\r\n\t" for ch in cleaned): + return f"{label} contains invalid whitespace" + return None def _validate_username(username: str) -> str | None: @@ -362,7 +379,9 @@ def register(app) -> None: return jsonify({"error": "server not configured"}), 503 ip = _client_ip() - username, email, note = _extract_request_payload() + username, email, note, first_name, last_name = _extract_request_payload() + first_name = _normalize_name(first_name) + last_name = _normalize_name(last_name) rate_key = ip if username: @@ -378,6 +397,12 @@ def register(app) -> None: username_error = _validate_username(username) if username_error: return jsonify({"error": username_error}), 400 + name_error = _validate_name(first_name, label="first name", required=False) + if name_error: + return jsonify({"error": name_error}), 400 + name_error = _validate_name(last_name, label="last name", required=True) + if name_error: + return jsonify({"error": name_error}), 400 if not email: return jsonify({"error": "email is required"}), 400 if "@" not in email: @@ -418,12 +443,22 @@ def register(app) -> None: UPDATE access_requests SET contact_email = %s, note = %s, + first_name = %s, + last_name = %s, email_verification_token_hash = %s, email_verification_sent_at = NOW(), email_verified_at = NULL WHERE request_code = %s AND status = %s """, - (email, note or None, token_hash, request_code, EMAIL_VERIFY_PENDING_STATUS), + ( + email, + note or None, + first_name or None, + last_name or None, + token_hash, + request_code, + EMAIL_VERIFY_PENDING_STATUS, + ), ) verify_url = _verify_url(request_code, token) @@ -447,12 +482,21 @@ def register(app) -> None: conn.execute( """ INSERT INTO access_requests - (request_code, username, contact_email, note, status, + (request_code, username, contact_email, note, first_name, last_name, status, email_verification_token_hash, email_verification_sent_at) VALUES - (%s, %s, %s, %s, %s, %s, NOW()) + (%s, %s, %s, %s, %s, %s, %s, %s, NOW()) """, - (request_code, username, email, note or None, EMAIL_VERIFY_PENDING_STATUS, token_hash), + ( + request_code, + username, + email, + note or None, + first_name or None, + last_name or None, + EMAIL_VERIFY_PENDING_STATUS, + token_hash, + ), ) except psycopg.errors.UniqueViolation: conn.rollback() diff --git a/backend/atlas_portal/routes/admin_access.py b/backend/atlas_portal/routes/admin_access.py index b1223e3..510c004 100644 --- a/backend/atlas_portal/routes/admin_access.py +++ b/backend/atlas_portal/routes/admin_access.py @@ -27,7 +27,7 @@ def register(app) -> None: with connect() as conn: rows = conn.execute( """ - SELECT request_code, username, contact_email, note, status, created_at + SELECT request_code, username, contact_email, first_name, last_name, note, status, created_at FROM access_requests WHERE status = 'pending' ORDER BY created_at ASC @@ -44,6 +44,8 @@ def register(app) -> None: "id": row["request_code"], "username": row["username"], "email": row.get("contact_email") or "", + "first_name": row.get("first_name") or "", + "last_name": row.get("last_name") or "", "request_code": row["request_code"], "created_at": (row.get("created_at").isoformat() if row.get("created_at") else ""), "note": row.get("note") or "", diff --git a/frontend/src/views/AccountView.vue b/frontend/src/views/AccountView.vue index 16af426..f0f33d8 100644 --- a/frontend/src/views/AccountView.vue +++ b/frontend/src/views/AccountView.vue @@ -354,6 +354,8 @@
+
Name
+
{{ formatName(req) }}
User
{{ req.username }}
Email
@@ -658,6 +660,18 @@ function hasFlag(username, flag) { return Array.isArray(selected) && selected.includes(flag); } +function formatName(req) { + if (!req) return "unknown"; + const parts = []; + if (req.first_name && String(req.first_name).trim()) { + parts.push(String(req.first_name).trim()); + } + if (req.last_name && String(req.last_name).trim()) { + parts.push(String(req.last_name).trim()); + } + return parts.length ? parts.join(" ") : "unknown"; +} + function toggleFlag(username, flag, event) { const checked = Boolean(event?.target?.checked); const selected = Array.isArray(admin.selectedFlags[username]) ? [...admin.selectedFlags[username]] : []; diff --git a/frontend/src/views/RequestAccessView.vue b/frontend/src/views/RequestAccessView.vue index 506837c..f099759 100644 --- a/frontend/src/views/RequestAccessView.vue +++ b/frontend/src/views/RequestAccessView.vue @@ -41,6 +41,32 @@
+ + + +