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."""