Skip to content

Commit 5f80e8a

Browse files
committed
fix(auth): accept single-label email domains and readable 422s
- RegisterIn.email: replace strict EmailStr with validated string; email-validator rejected 'user@localhost' (no dot) and reserved names, causing 422 on register/login with dev-style emails. Now normalized (trim+lowercase) with structural checks instead. - UI: render Pydantic list-form 422 details readably instead of '[object Object]'; applied to login and run-error paths. - dev secret_key padded to >=32 bytes to satisfy PyJWT's InsecureKeyLengthWarning for HS256. - gitignore *.egg-info/
1 parent 80f8013 commit 5f80e8a

4 files changed

Lines changed: 26 additions & 5 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
__pycache__/
22
*.pyc
3+
*.egg-info/
34
.coverage
45
htmlcov/
56
.pytest_cache/

‎app/core/config.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class Settings(BaseSettings):
4747
)
4848

4949
db_url: str = "sqlite:///./paw.db"
50-
secret_key: str = "dev-only-insecure-key-change-me"
50+
secret_key: str = "dev-only-insecure-key-change-me-0123456789abcdef"
5151
fernet_key: str = ""
5252
"""Fernet key for the secrets store; empty = derived from
5353
``secret_key`` (dev convenience, stable across restarts)."""

‎app/schemas.py‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,29 @@
44

55
from datetime import datetime
66

7-
from pydantic import BaseModel, EmailStr, Field
7+
from pydantic import BaseModel, Field, field_validator
88

99
# --- auth ---
1010

1111

1212
class RegisterIn(BaseModel):
13-
email: EmailStr
13+
# Deliberately a plain validated string, not EmailStr: email-validator
14+
# rejects single-label domains ("user@localhost", common in dev) and
15+
# reserved names outright, which made login/register 422 in practice.
16+
email: str = Field(min_length=3, max_length=254)
1417
password: str = Field(min_length=8, max_length=128)
1518

19+
@field_validator("email")
20+
@classmethod
21+
def _normalize_email(cls, v: str) -> str:
22+
v = v.strip().lower()
23+
if v.count("@") != 1 or not v.split("@")[0]:
24+
raise ValueError("must contain exactly one @ with a local part")
25+
local, domain = v.split("@")
26+
if not domain or " " in v or any(c in v for c in "\"'<>,;:\\"):
27+
raise ValueError("invalid email address")
28+
return v
29+
1630

1731
class TokenPair(BaseModel):
1832
access_token: str

‎app/static/index.html‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ <h1>python-agent-web</h1>
7979
msgs.appendChild(div); msgs.scrollTop = msgs.scrollHeight; return div;
8080
}
8181

82+
function errMsg(res) {
83+
return res.json().then(j => {
84+
const d = j.detail ?? j;
85+
return Array.isArray(d) ? d.map(e => `${(e.loc||[]).slice(1).join('.')}: ${e.msg}`).join('; ') : String(d);
86+
}).catch(() => res.statusText || 'request failed');
87+
}
8288
$('loginBtn').onclick = async () => {
8389
const email = $('email').value.trim(), password = $('password').value;
8490
let res = await fetch('/auth/login', {method:'POST', headers:{'Content-Type':'application/json'},
@@ -87,7 +93,7 @@ <h1>python-agent-web</h1>
8793
res = await fetch('/auth/register', {method:'POST', headers:{'Content-Type':'application/json'},
8894
body: JSON.stringify({email, password})});
8995
}
90-
if (!res.ok) { $('authMsg').textContent = 'auth failed: ' + (await res.json()).detail; return; }
96+
if (!res.ok) { $('authMsg').textContent = 'auth failed: ' + await errMsg(res); return; }
9197
const tokens = await res.json();
9298
api.token = tokens.access_token; sessionStorage.setItem('token', api.token); showApp();
9399
};
@@ -152,7 +158,7 @@ <h1>python-agent-web</h1>
152158
}
153159
const res = await api.req(`/conversations/${currentConv}/runs`,
154160
{method:'POST', body: JSON.stringify({prompt})});
155-
if (!res.ok) { add('err', (await res.json()).detail); return; }
161+
if (!res.ok) { add('err', await errMsg(res)); return; }
156162
const run = await res.json();
157163
startStream(run.id);
158164
}

0 commit comments

Comments
 (0)