password expiry policy added

This commit is contained in:
suryac 2026-11-12 11:16:58 +05:30
parent dc0b46fbd9
commit 1bbe205a79
5 changed files with 209 additions and 182 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "krishna-sales-pwa", "name": "krishna-sales-pwa",
"private": true, "private": true,
"version": "1.0.4", "version": "1.0.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -271,5 +271,5 @@ export const HIDDEN_FORM_FIELDS = [
export const SELECT_OPTION_LIMIT = 200; export const SELECT_OPTION_LIMIT = 200;
export const PDF_MAX_SKUS_PER_CHUNK = 50; export const PDF_MAX_SKUS_PER_CHUNK = 50;
export const APP_VERSION = '1.0.4'; export const APP_VERSION = '1.0.5';
export const FORM_DRAFT_EXPIRY_MS = 30 * 60 * 1000; export const FORM_DRAFT_EXPIRY_MS = 30 * 60 * 1000;

View File

@ -81,6 +81,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
login: async (email, password, orgId) => { login: async (email, password, orgId) => {
const res = await loginAll(email, password, orgId); const res = await loginAll(email, password, orgId);
applyUserSession(res); applyUserSession(res);
return (res.user?.password_status as PasswordStatus | undefined) ?? null;
}, },
completePasswordChange: async (email, currentPassword, newPassword, orgId) => { completePasswordChange: async (email, currentPassword, newPassword, orgId) => {
const res = await completePasswordChangeAll(email, currentPassword, newPassword, orgId); const res = await completePasswordChangeAll(email, currentPassword, newPassword, orgId);

View File

@ -13,7 +13,8 @@ export interface AuthValue {
roles: string[]; roles: string[];
userEmail: string | null; userEmail: string | null;
passwordStatus: PasswordStatus | null; passwordStatus: PasswordStatus | null;
login: (email: string, password: string, orgId?: string) => Promise<void>; /** Resolves with the account's password status so the caller can react to must_change. */
login: (email: string, password: string, orgId?: string) => Promise<PasswordStatus | null>;
completePasswordChange: (email: string, currentPassword: string, newPassword: string, orgId?: string) => Promise<void>; completePasswordChange: (email: string, currentPassword: string, newPassword: string, orgId?: string) => Promise<void>;
logout: () => void; logout: () => void;
} }

View File

@ -1,6 +1,7 @@
import { useState, type FormEvent } from 'react'; import { useState, type FormEvent } from 'react';
import { Navigate, useNavigate, Link } from 'react-router-dom'; import { Navigate, useNavigate, Link } from 'react-router-dom';
import { Eye, EyeOff, Lock, Mail, AlertCircle, ArrowLeft, ArrowRight } from 'lucide-react'; import { Eye, EyeOff, Lock, Mail, AlertCircle, ArrowLeft, ArrowRight } from 'lucide-react';
import { changePassword } from '../api/auth';
import { useAuth, type PasswordChangeRequiredInfo } from '../auth/context'; import { useAuth, type PasswordChangeRequiredInfo } from '../auth/context';
import { Button } from '../components/buttons'; import { Button } from '../components/buttons';
import { AuthShell } from '../components/auth/AuthShell'; import { AuthShell } from '../components/auth/AuthShell';
@ -15,7 +16,7 @@ import {
/** Login gate. Redirects to /daily once authenticated. */ /** Login gate. Redirects to /daily once authenticated. */
export function LoginPage() { export function LoginPage() {
const { authed, login, completePasswordChange } = useAuth(); const { authed, login, logout, completePasswordChange } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
@ -37,9 +38,13 @@ export function LoginPage() {
confirmPassword?: boolean; confirmPassword?: boolean;
}>({}); }>({});
// True when sign-in succeeded but the account is flagged must_change: the session exists,
// so the new password is set via the authenticated endpoint rather than change-expired-password.
const [sessionChange, setSessionChange] = useState(false);
const policy = usePasswordPolicy(!!changeRequired); const policy = usePasswordPolicy(!!changeRequired);
if (authed) return <Navigate to="/daily" replace />; if (authed && !changeRequired) return <Navigate to="/daily" replace />;
async function submit(e: FormEvent) { async function submit(e: FormEvent) {
e.preventDefault(); e.preventDefault();
@ -64,7 +69,15 @@ export function LoginPage() {
setBusy(true); setBusy(true);
try { try {
await login(email, password); const status = await login(email, password);
if (status?.must_change || (status?.days_left !== undefined && status.days_left <= 0)) {
setSessionChange(true);
setChangeRequired({
message: 'Your password has expired. Please choose a new password to continue.',
reason: 'expired',
});
return;
}
navigate('/daily', { replace: true }); navigate('/daily', { replace: true });
} catch (err: any) { } catch (err: any) {
// Backend signals that password policy or expiry requires a new password first // Backend signals that password policy or expiry requires a new password first
@ -130,7 +143,15 @@ export function LoginPage() {
setBusy(true); setBusy(true);
try { try {
if (sessionChange) {
// Changing the password invalidates the current session, so sign in again with the new one.
await changePassword(undefined, newPassword);
await login(email, newPassword);
setSessionChange(false);
setChangeRequired(null);
} else {
await completePasswordChange(email, password, newPassword); await completePasswordChange(email, password, newPassword);
}
navigate('/daily', { replace: true }); navigate('/daily', { replace: true });
} catch (err: any) { } catch (err: any) {
setError(err?.message ?? 'Could not update password. Please try again.'); setError(err?.message ?? 'Could not update password. Please try again.');
@ -140,6 +161,10 @@ export function LoginPage() {
} }
function backToSignIn() { function backToSignIn() {
if (sessionChange) {
logout();
setSessionChange(false);
}
setChangeRequired(null); setChangeRequired(null);
setNewPassword(''); setNewPassword('');
setConfirmPassword(''); setConfirmPassword('');