From b77c2cc9f7fa152240eb6ee1cedc73f635a973df Mon Sep 17 00:00:00 2001 From: Talha Ahmed Date: Wed, 19 Aug 2026 21:41:21 +0500 Subject: [PATCH] Settings changes password with the CURRENT password; email code is the login-screen forgot-password flow only Also fix: the auth middleware now attaches the signed-in identity on open paths too (a signed-in request-code call previously saw no user and demanded a username). Co-Authored-By: Claude Fable 5 --- ar-aging-app/README.md | 8 +- ar-aging-app/backend/app/api/auth.py | 11 ++- ar-aging-app/frontend/src/pages/Settings.tsx | 81 +------------------- 3 files changed, 12 insertions(+), 88 deletions(-) diff --git a/ar-aging-app/README.md b/ar-aging-app/README.md index 990da79..1563305 100644 --- a/ar-aging-app/README.md +++ b/ar-aging-app/README.md @@ -48,10 +48,10 @@ Key production behaviors: under another name is skipped. A month can never count a file twice. - **Login** (`AR_AUTH`) — per-user accounts via `manage.py add-user`; journal review/approval and FX confirmations record the signed-in user's verified name. -- **Password self-service** — users change or recover passwords with a 6-digit code emailed - to their account address (Settings, or "Forgot password?" on the login screen). Sent via - the company Mail API (`AR_MAIL_API_*`), SMTP fallback; `manage.py set-password` remains - the admin override. +- **Password self-service** — Settings changes the password with the current one; a + forgotten password is recovered from the login screen ("Forgot password?") via a 6-digit + code emailed to the account address (company Mail API `AR_MAIL_API_*`, SMTP fallback); + `manage.py set-password` remains the admin override. - **Exchange rates** — "Fetch month-end rates" pulls central-bank rates (Frankfurter, free, keyless; `AR_FX_PROVIDER`); fetched rates still require human confirmation (Control C5). - **Completed closings are locked** read-only; corrections need an explicit Reopen. diff --git a/ar-aging-app/backend/app/api/auth.py b/ar-aging-app/backend/app/api/auth.py index 9362b27..17eed93 100644 --- a/ar-aging-app/backend/app/api/auth.py +++ b/ar-aging-app/backend/app/api/auth.py @@ -173,15 +173,14 @@ def _user_from_request(request: Request) -> AuthUser | None: async def auth_middleware(request: Request, call_next): """Guards every /api/* route except OPEN_PATHS. Registered in api/main.py.""" path = request.url.path.rstrip("/") or "/" - if path.startswith("/api") and path not in OPEN_PATHS: + if path.startswith("/api"): + # Identity is attached whenever a valid token is present — including on open + # paths, so e.g. a signed-in password-code request knows who is asking. user = _user_from_request(request) - if user is not None: - request.state.user = user - elif auth_required(): + request.state.user = user + if user is None and path not in OPEN_PATHS and auth_required(): return JSONResponse({"detail": "Not signed in (or the session expired). " "Sign in to continue."}, status_code=401) - else: - request.state.user = None return await call_next(request) diff --git a/ar-aging-app/frontend/src/pages/Settings.tsx b/ar-aging-app/frontend/src/pages/Settings.tsx index ed65a54..08b6827 100644 --- a/ar-aging-app/frontend/src/pages/Settings.tsx +++ b/ar-aging-app/frontend/src/pages/Settings.tsx @@ -7,7 +7,7 @@ import { useAuth } from "../auth"; export default function Settings() { const { data: health } = useQuery({ queryKey: ["health"], queryFn: api.health }); - const { user, emailEnabled } = useAuth(); + const { user } = useAuth(); return (
@@ -16,9 +16,7 @@ export default function Settings() {

Application defaults and security posture.

- {user && (emailEnabled - ? - : )} + {user && }
@@ -63,79 +61,6 @@ export default function Settings() { ); } -/** Code-based update: a 6-digit code is emailed to the signed-in account, then the new - * password is set with it — shown when the server has email configured. */ -function ChangePasswordByCode({ username }: { username: string }) { - const [sent, setSent] = useState(false); - const [code, setCode] = useState(""); - const [next, setNext] = useState(""); - const [repeat, setRepeat] = useState(""); - - const send = useMutation({ - mutationFn: () => api.requestPasswordCode(), - onSuccess: () => setSent(true), - }); - const reset = useMutation({ - mutationFn: () => api.resetPassword(code.trim(), next), - onSuccess: () => { setCode(""); setNext(""); setRepeat(""); setSent(false); }, - }); - - const mismatch = repeat.length > 0 && next !== repeat; - const ready = code.length === 6 && next.length >= 8 && next === repeat; - - const submit = (e: FormEvent) => { - e.preventDefault(); - if (ready) reset.mutate(); - }; - - return ( -
-
-
- - {send.isSuccess && Code sent — check your inbox (valid 10 minutes).} - {send.isError && {(send.error as Error).message}} -
- - {sent && ( -
- - - -
- - {mismatch && Passwords don't match.} - {reset.isError && {(reset.error as Error).message}} -
-
- )} - {reset.isSuccess && !sent && -

Password updated — use it from your next sign-in.

} -
-
- ); -} - function ChangePassword({ username }: { username: string }) { const [current, setCurrent] = useState(""); const [next, setNext] = useState(""); @@ -156,7 +81,7 @@ function ChangePassword({ username }: { username: string }) { return (
+ subtitle={`Signed in as ${username}. Forgot the current one? Sign out and use "Forgot password?" on the login screen — a code is emailed to you.`}>