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 <noreply@anthropic.com>
main
Talha Ahmed 2026-08-19 21:41:21 +05:00
parent a6ae242709
commit b77c2cc9f7
3 changed files with 12 additions and 88 deletions

View File

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

View File

@ -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)

View File

@ -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 (
<div className="p-6 max-w-3xl mx-auto space-y-6">
@ -16,9 +16,7 @@ export default function Settings() {
<p className="text-sm text-subink">Application defaults and security posture.</p>
</header>
{user && (emailEnabled
? <ChangePasswordByCode username={user.username} />
: <ChangePassword username={user.username} />)}
{user && <ChangePassword username={user.username} />}
<Section title="Processing defaults">
<dl className="divide-y divide-line">
@ -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 (
<Section title="Change my password"
subtitle={`A 6-digit code is emailed to ${username} — enter it with your new password.`}>
<form onSubmit={submit} className="p-4 space-y-3">
<div className="flex items-center gap-3 flex-wrap">
<button type="button" className="btn-ghost" disabled={send.isPending}
onClick={() => send.mutate()}>
{send.isPending ? <Spinner /> : <KeyRound size={15} />}
{sent ? "Resend code" : "Email me a code"}
</button>
{send.isSuccess && <span className="text-xs text-ok">Code sent check your inbox (valid 10 minutes).</span>}
{send.isError && <span className="text-xs text-bad">{(send.error as Error).message}</span>}
</div>
{sent && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 items-end">
<label className="text-sm">
<span className="block text-xs font-medium text-subink mb-1">6-digit code</span>
<input className="input num tracking-[0.3em] text-center" inputMode="numeric"
maxLength={6} autoFocus placeholder="••••••" value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))} />
</label>
<label className="text-sm">
<span className="block text-xs font-medium text-subink mb-1">New password (min 8)</span>
<input className="input" type="password" autoComplete="new-password"
value={next} onChange={(e) => setNext(e.target.value)} />
</label>
<label className="text-sm">
<span className="block text-xs font-medium text-subink mb-1">Repeat new password</span>
<input className="input" type="password" autoComplete="new-password"
value={repeat} onChange={(e) => setRepeat(e.target.value)} />
</label>
<div className="sm:col-span-3 flex items-center gap-3 flex-wrap">
<button className="btn-primary" type="submit" disabled={!ready || reset.isPending}>
{reset.isPending ? <Spinner /> : <KeyRound size={15} />} Update password
</button>
{mismatch && <span className="text-xs text-bad">Passwords don't match.</span>}
{reset.isError && <span className="text-xs text-bad">{(reset.error as Error).message}</span>}
</div>
</div>
)}
{reset.isSuccess && !sent &&
<p className="text-xs text-ok">Password updated use it from your next sign-in.</p>}
</form>
</Section>
);
}
function ChangePassword({ username }: { username: string }) {
const [current, setCurrent] = useState("");
const [next, setNext] = useState("");
@ -156,7 +81,7 @@ function ChangePassword({ username }: { username: string }) {
return (
<Section title="Change my password"
subtitle={`Signed in as ${username}. Forgot the current one? An administrator can reset it on the server (manage.py set-password).`}>
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.`}>
<form onSubmit={submit} className="p-4 grid grid-cols-1 sm:grid-cols-3 gap-3 items-end">
<label className="text-sm">
<span className="block text-xs font-medium text-subink mb-1">Current password</span>