diff --git a/frontend/templates/signup.html b/frontend/templates/signup.html
index 1cb3a043..6f83d664 100644
--- a/frontend/templates/signup.html
+++ b/frontend/templates/signup.html
@@ -32,6 +32,14 @@
error: '',
async submit() {
this.error = '';
+ if (this.username.length < 3 || this.username.length > 64) {
+ this.error = 'Username must be between 3 and 64 characters.';
+ return;
+ }
+ if (!/^[a-zA-Z0-9_-]+$/.test(this.username)) {
+ this.error = 'Username may only contain letters, numbers, hyphens, and underscores. Dots and other special characters are not allowed.';
+ return;
+ }
if (this.password !== this.password_confirm) {
this.error = 'Passwords do not match.';
return;
@@ -58,7 +66,12 @@
}
} else {
const data = await resp.json();
- this.error = data.detail || 'Registration failed. Please try again.';
+ const detail = data.detail;
+ if (Array.isArray(detail)) {
+ this.error = detail.map(e => e.msg || String(e)).join(' ') || 'Registration failed. Please try again.';
+ } else {
+ this.error = detail || 'Registration failed. Please try again.';
+ }
}
} catch(e) {
this.error = 'Network error. Please try again.';
diff --git a/tests/test_local_auth.py b/tests/test_local_auth.py
index 8a53604c..babbbc30 100644
--- a/tests/test_local_auth.py
+++ b/tests/test_local_auth.py
@@ -322,6 +322,35 @@ def test_signup_duplicate_username(la_client, active_user):
assert "Username" in resp.json()["detail"]
+@pytest.mark.integration
+def test_signup_invalid_username_with_dot(la_client):
+ """POST /api/auth/signup returns 422 with a list detail when username contains a dot.
+
+ This is a regression test for the bug where ``data.detail`` was an array,
+ causing the frontend to display ``[object Object]`` instead of a message.
+ """
+ with patch("app.api.local_auth.settings") as mock_settings:
+ mock_settings.allow_local_signup = True
+ mock_settings.multi_user_enabled = True
+ mock_settings.email_host = "smtp.example.com"
+ resp = la_client.post(
+ "/api/auth/signup",
+ json={
+ "email": "a@example.com",
+ "username": "christian.louis",
+ "password": "password1",
+ "password_confirm": "password1",
+ },
+ )
+ assert resp.status_code == 422
+ detail = resp.json()["detail"]
+ # FastAPI returns a list of validation errors for Pydantic constraint failures.
+ # Each entry must be a dict with a "msg" key so the frontend can extract a readable message.
+ assert isinstance(detail, list), "detail should be a list for Pydantic validation errors"
+ assert len(detail) > 0
+ assert "msg" in detail[0]
+
+
@pytest.mark.integration
def test_signup_smtp_failure_cleans_up(la_client, la_session):
"""POST /api/auth/signup cleans up user records if email send fails."""