Skip to content

Commit cf087d0

Browse files
committed
feat: enhance user registration and login flow with additional fields and checks
1 parent 9c7317f commit cf087d0

9 files changed

Lines changed: 393 additions & 95 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ platform/dev_ca/
1111
.env
1212
docs-internal/
1313
dist/
14-
build/
14+
build/
15+
*.old

platform/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ DB_BACKEND=postgres
66
SSH_CA_KEY_PATH=./dev_ca/ssh_user_ca
77
SSH_CERT_TTL_SECONDS=300
88
LOCAL_AUTH_ENABLED=1
9+
REGISTRATION_ENABLED=1
910
DEV_AUTH_ENABLED=0
1011
ALLOWED_LOGIN_DOMAINS=example.com
1112
FIRST_USER_ADMIN=1

platform/app/auth.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
1-
from flask import Blueprint, Response, flash, redirect, render_template, request, session, url_for
1+
from flask import Blueprint, Response, current_app, flash, redirect, render_template, request, session, url_for
22

33
from app.models.base import utcnow
44

5-
from app.services.auth import login_federated_user, login_local_user, login_or_create_user, logout_user, register_local_user
5+
from app.services.auth import (
6+
ADMIN_ROLE,
7+
current_user,
8+
current_user_roles,
9+
login_federated_user,
10+
login_local_user,
11+
login_or_create_user,
12+
logout_user,
13+
register_local_user,
14+
)
615
from app.services.sso import enabled_sso_providers, oidc_authorization_url, oidc_callback_payload, saml_acs_payload, saml_login_url, saml_metadata_xml
716

817
bp = Blueprint("auth", __name__)
918

1019

1120
@bp.get("/login")
1221
def login():
22+
if current_user() is not None:
23+
return redirect(_post_login_redirect())
1324
return render_template("auth/login.html", sso=enabled_sso_providers())
1425

1526

@@ -26,29 +37,47 @@ def login_post():
2637
session["user_id"] = str(user.id)
2738
session["authenticated_at"] = utcnow().isoformat()
2839
session.permanent = True
29-
return redirect(url_for("portal.index"))
40+
return redirect(_post_login_redirect())
3041

3142

3243
@bp.get("/register")
3344
def register():
45+
if current_user() is not None:
46+
return redirect(_post_login_redirect())
47+
if not _registration_available():
48+
flash("registration_disabled", "warning")
49+
return redirect(url_for("auth.login"))
3450
return render_template("auth/register.html")
3551

3652

3753
@bp.post("/register")
3854
def register_post():
55+
if not _registration_available():
56+
flash("registration_disabled", "warning")
57+
return redirect(url_for("auth.login"))
58+
59+
first_name = request.form.get("first_name")
60+
last_name = request.form.get("last_name")
61+
display_name = request.form.get("display_name") or " ".join(
62+
part for part in [(first_name or "").strip(), (last_name or "").strip()] if part
63+
)
3964
user, error = register_local_user(
4065
email=request.form.get("email", ""),
4166
password=request.form.get("password", ""),
42-
display_name=request.form.get("display_name"),
67+
display_name=display_name,
4368
)
69+
if user is not None and (first_name or last_name):
70+
from app.services.auth import update_user_profile
71+
72+
update_user_profile(str(user.id), user.display_name, user.external_id, first_name=first_name, last_name=last_name)
4473
if error:
4574
flash(error, "danger")
4675
return redirect(url_for("auth.register"))
4776

4877
session["user_id"] = str(user.id)
4978
session["authenticated_at"] = utcnow().isoformat()
5079
session.permanent = True
51-
return redirect(url_for("portal.index"))
80+
return redirect(_post_login_redirect())
5281

5382

5483
@bp.post("/dev-login")
@@ -64,7 +93,7 @@ def dev_login_post():
6493
session["user_id"] = str(user.id)
6594
session["authenticated_at"] = utcnow().isoformat()
6695
session.permanent = True
67-
return redirect(url_for("portal.index"))
96+
return redirect(_post_login_redirect())
6897

6998

7099
@bp.get("/oidc/login")
@@ -134,4 +163,14 @@ def _finish_sso_login(payload: dict):
134163
session["user_id"] = str(user.id)
135164
session["authenticated_at"] = utcnow().isoformat()
136165
session.permanent = True
137-
return redirect(url_for("portal.index"))
166+
return redirect(_post_login_redirect())
167+
168+
169+
def _post_login_redirect():
170+
if ADMIN_ROLE in current_user_roles():
171+
return url_for("admin.dashboard")
172+
return url_for("portal.index")
173+
174+
175+
def _registration_available() -> bool:
176+
return current_app.config["LOCAL_AUTH_ENABLED"] and current_app.config["REGISTRATION_ENABLED"]

platform/app/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Config:
2121
SERVER_ENROLLMENT_MAX_TTL_MINUTES = int(os.environ.get("SERVER_ENROLLMENT_MAX_TTL_MINUTES", "1440"))
2222
SERVER_ENROLLMENT_MAX_USES_LIMIT = int(os.environ.get("SERVER_ENROLLMENT_MAX_USES_LIMIT", "100"))
2323
LOCAL_AUTH_ENABLED = os.environ.get("LOCAL_AUTH_ENABLED", "1") == "1"
24+
REGISTRATION_ENABLED = os.environ.get("REGISTRATION_ENABLED", "1") == "1"
2425
DEV_AUTH_ENABLED = os.environ.get("DEV_AUTH_ENABLED", "0") == "1"
2526
ALLOWED_LOGIN_DOMAINS = [
2627
domain.strip().lower()

platform/app/services/auth.py

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818

1919
def login_local_user(email: str, password: str) -> tuple[User | None, str | None]:
20+
if not current_app.config["LOCAL_AUTH_ENABLED"]:
21+
return None, "local_auth_disabled"
2022
normalized_email = email.strip().lower()
2123
user = User.query.filter_by(email=normalized_email).one_or_none()
2224
if user is None or not user.password_hash:
@@ -152,10 +154,19 @@ def _complete_login(user: User, provider: str) -> tuple[User | None, str | None]
152154
return user, None
153155

154156

155-
def create_local_user(email: str, password: str, display_name: str | None, roles: list[str]) -> tuple[dict, int]:
157+
def create_local_user(
158+
email: str,
159+
password: str,
160+
display_name: str | None,
161+
roles: list[str],
162+
first_name: str | None = None,
163+
last_name: str | None = None,
164+
) -> tuple[dict, int]:
165+
display_name = _display_name(display_name, first_name, last_name, email)
156166
user, error = register_local_user(email, password, display_name)
157167
if error:
158168
return {"error": error}, 400
169+
user.external_id = _profile_external_id(user.external_id, first_name, last_name)
159170
for role in roles:
160171
if role:
161172
_ensure_role(user, role)
@@ -279,18 +290,29 @@ def set_user_status(user_id: str, status: str) -> tuple[dict, int]:
279290
return {"user_id": str(user.id), "status": status}, 200
280291

281292

282-
def update_user_profile(user_id: str, display_name: str | None, external_id: str | None) -> tuple[dict, int]:
293+
def update_user_profile(
294+
user_id: str,
295+
display_name: str | None,
296+
external_id: str | None,
297+
first_name: str | None = None,
298+
last_name: str | None = None,
299+
) -> tuple[dict, int]:
283300
user, error_response = _user_from_id(user_id)
284301
if error_response:
285302
return error_response
286-
user.display_name = display_name or user.email
287-
user.external_id = external_id or None
303+
user.display_name = _display_name(display_name, first_name, last_name, user.email)
304+
user.external_id = _profile_external_id(external_id, first_name, last_name)
288305
record_audit_event(
289306
event_type="user.profile_updated",
290307
outcome="success",
291308
target_type="user",
292309
target_id=user.id,
293-
metadata={"display_name": user.display_name, "external_id": user.external_id},
310+
metadata={
311+
"display_name": user.display_name,
312+
"external_id": external_id,
313+
"first_name": (first_name or "").strip(),
314+
"last_name": (last_name or "").strip(),
315+
},
294316
)
295317
db.session.commit()
296318
return {"user_id": str(user.id)}, 200
@@ -367,11 +389,16 @@ def list_users_with_roles() -> list[dict]:
367389
.all()
368390
)
369391
servers = accessible_servers_for_user(user.id)
392+
first_name, last_name, directory_id = _profile_parts(user.external_id)
370393
result.append(
371394
{
372395
"id": str(user.id),
373396
"email": user.email,
374397
"display_name": user.display_name,
398+
"first_name": first_name,
399+
"last_name": last_name,
400+
"directory_id": directory_id,
401+
"initials": _initials(first_name, last_name, user.display_name, user.email),
375402
"status": user.status,
376403
"auth_provider": user.auth_provider,
377404
"external_id": user.external_id,
@@ -392,6 +419,52 @@ def list_users_with_roles() -> list[dict]:
392419
return result
393420

394421

422+
def _display_name(display_name: str | None, first_name: str | None, last_name: str | None, email: str) -> str:
423+
explicit = (display_name or "").strip()
424+
if explicit:
425+
return explicit
426+
full_name = " ".join(part for part in [(first_name or "").strip(), (last_name or "").strip()] if part)
427+
return full_name or email
428+
429+
430+
def _profile_external_id(external_id: str | None, first_name: str | None, last_name: str | None) -> str | None:
431+
directory_id = (external_id or "").strip()
432+
parts = []
433+
if directory_id:
434+
parts.append(directory_id)
435+
first_name = (first_name or "").strip()
436+
last_name = (last_name or "").strip()
437+
if first_name:
438+
parts.append(f"first_name={first_name}")
439+
if last_name:
440+
parts.append(f"last_name={last_name}")
441+
return "|".join(parts) or None
442+
443+
444+
def _profile_parts(external_id: str | None) -> tuple[str, str, str | None]:
445+
first_name = ""
446+
last_name = ""
447+
directory_parts = []
448+
for part in (external_id or "").split("|"):
449+
if part.startswith("first_name="):
450+
first_name = part.removeprefix("first_name=")
451+
elif part.startswith("last_name="):
452+
last_name = part.removeprefix("last_name=")
453+
elif part:
454+
directory_parts.append(part)
455+
return first_name, last_name, "|".join(directory_parts) or None
456+
457+
458+
def _initials(first_name: str, last_name: str, display_name: str | None, email: str) -> str:
459+
if first_name or last_name:
460+
return f"{first_name[:1]}{last_name[:1]}".upper() or "U"
461+
name = (display_name or email or "U").strip()
462+
parts = [part for part in name.split() if part]
463+
if len(parts) >= 2:
464+
return f"{parts[0][:1]}{parts[-1][:1]}".upper()
465+
return name[:2].upper()
466+
467+
395468
def _domain_allowed(email: str) -> bool:
396469
if "@" not in email:
397470
return False

platform/app/templates/admin/settings.html

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ <h1 class="page-title">Settings</h1>
174174

175175
<div style="display:flex;align-items:center;gap:10px;margin-top:6px;">
176176
<button type="submit" class="btn-primary" style="font-size:13px;">Save Configuration</button>
177-
<form method="POST" action="{{ url_for(' admin.settings_integration_test', provider=integration.provider)
177+
<form method="POST" action="{{ url_for('admin.settings_integration_test', provider=integration.provider)
178178
}}" style="display:inline;">
179179
<button type="submit" class="btn-ghost" style="font-size:13px;">Test Connection</button>
180180
</form>
@@ -413,6 +413,22 @@ <h1 class="page-title">Settings</h1>
413413
{% endif %}
414414
</div>
415415

416+
<div
417+
style="padding:14px 16px;background:var(--c-bg);border:1px solid var(--c-s2);border-radius:8px;display:flex;align-items:flex-start;gap:12px;">
418+
<div style="flex:1;">
419+
<div style="font-weight:600;font-size:13px;color:#e8eaed;">Self-Service Registration</div>
420+
<div style="font-size:12px;color:var(--c-s4);margin-top:3px;line-height:1.5;">Allow new users to create
421+
local accounts from the login page. Admin-created users and SSO provisioning are unaffected.</div>
422+
</div>
423+
{% if config.REGISTRATION_ENABLED and config.LOCAL_AUTH_ENABLED %}
424+
<span class="sbadge sbadge-green">Enabled</span>
425+
{% elif config.REGISTRATION_ENABLED and not config.LOCAL_AUTH_ENABLED %}
426+
<span class="sbadge sbadge-amber">Blocked by local auth</span>
427+
{% else %}
428+
<span class="sbadge sbadge-muted">Disabled</span>
429+
{% endif %}
430+
</div>
431+
416432
<div
417433
style="padding:14px 16px;background:var(--c-bg);border:1px solid {% if config.DEV_AUTH_ENABLED %}rgba(245,158,11,.3){% else %}var(--c-s2){% endif %};border-radius:8px;display:flex;align-items:flex-start;gap:12px;">
418434
<div style="flex:1;">
@@ -449,7 +465,8 @@ <h1 class="page-title">Settings</h1>
449465
</div>
450466
<div
451467
style="margin-top:16px;padding:10px 12px;background:rgba(245,158,11,.06);border:1px solid rgba(245,158,11,.2);border-radius:8px;font-size:11px;color:var(--c-s4);line-height:1.6;">
452-
Auth method flags are read-only in the UI. Set <code>LOCAL_AUTH_ENABLED</code>, <code>DEV_AUTH_ENABLED</code>
468+
Auth method flags are read-only in the UI. Set <code>LOCAL_AUTH_ENABLED</code>,
469+
<code>REGISTRATION_ENABLED</code>, <code>DEV_AUTH_ENABLED</code>
453470
in your environment or <code>.env</code> file and restart the platform.
454471
</div>
455472
</div>
@@ -636,4 +653,4 @@ <h1 class="page-title">Settings</h1>
636653
btn.classList.add('sso-active');
637654
}
638655
</script>
639-
{% endblock %}
656+
{% endblock %}

0 commit comments

Comments
 (0)