diff --git a/package.json b/package.json index f85a14d..75c5cee 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "krishna-sales-pwa", "private": true, - "version": "1.0.4", + "version": "1.0.5", "type": "module", "scripts": { "dev": "vite", @@ -47,4 +47,4 @@ "vite": "^8.1.1", "vite-plugin-pwa": "^1.3.0" } -} +} \ No newline at end of file diff --git a/src/api/config.ts b/src/api/config.ts index 89f96bf..113a4d2 100644 --- a/src/api/config.ts +++ b/src/api/config.ts @@ -271,5 +271,5 @@ export const HIDDEN_FORM_FIELDS = [ export const SELECT_OPTION_LIMIT = 200; 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; diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index a20446e..f22f18a 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -81,6 +81,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { login: async (email, password, orgId) => { const res = await loginAll(email, password, orgId); applyUserSession(res); + return (res.user?.password_status as PasswordStatus | undefined) ?? null; }, completePasswordChange: async (email, currentPassword, newPassword, orgId) => { const res = await completePasswordChangeAll(email, currentPassword, newPassword, orgId); diff --git a/src/auth/context.ts b/src/auth/context.ts index a9ef4bb..0d1318f 100644 --- a/src/auth/context.ts +++ b/src/auth/context.ts @@ -13,7 +13,8 @@ export interface AuthValue { roles: string[]; userEmail: string | null; passwordStatus: PasswordStatus | null; - login: (email: string, password: string, orgId?: string) => Promise; + /** Resolves with the account's password status so the caller can react to must_change. */ + login: (email: string, password: string, orgId?: string) => Promise; completePasswordChange: (email: string, currentPassword: string, newPassword: string, orgId?: string) => Promise; logout: () => void; } diff --git a/src/screens/LoginPage.tsx b/src/screens/LoginPage.tsx index b45505c..dffadc8 100644 --- a/src/screens/LoginPage.tsx +++ b/src/screens/LoginPage.tsx @@ -1,6 +1,7 @@ import { useState, type FormEvent } from 'react'; import { Navigate, useNavigate, Link } from 'react-router-dom'; import { Eye, EyeOff, Lock, Mail, AlertCircle, ArrowLeft, ArrowRight } from 'lucide-react'; +import { changePassword } from '../api/auth'; import { useAuth, type PasswordChangeRequiredInfo } from '../auth/context'; import { Button } from '../components/buttons'; import { AuthShell } from '../components/auth/AuthShell'; @@ -15,7 +16,7 @@ import { /** Login gate. Redirects to /daily once authenticated. */ export function LoginPage() { - const { authed, login, completePasswordChange } = useAuth(); + const { authed, login, logout, completePasswordChange } = useAuth(); const navigate = useNavigate(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -37,9 +38,13 @@ export function LoginPage() { 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); - if (authed) return ; + if (authed && !changeRequired) return ; async function submit(e: FormEvent) { e.preventDefault(); @@ -56,15 +61,23 @@ export function LoginPage() { errs.email && errs.password ? 'Please enter both your email and password.' : errs.email - ? 'Email address is required.' - : 'Password is required.' + ? 'Email address is required.' + : 'Password is required.' ); return; } setBusy(true); 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 }); } catch (err: any) { // Backend signals that password policy or expiry requires a new password first @@ -98,8 +111,8 @@ export function LoginPage() { errs.newPassword && errs.confirmPassword ? 'Please enter and confirm your new password.' : errs.newPassword - ? 'New password is required.' - : 'Please confirm your new password.' + ? 'New password is required.' + : 'Please confirm your new password.' ); return; } @@ -130,7 +143,15 @@ export function LoginPage() { setBusy(true); try { - await completePasswordChange(email, password, newPassword); + 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); + } navigate('/daily', { replace: true }); } catch (err: any) { setError(err?.message ?? 'Could not update password. Please try again.'); @@ -140,6 +161,10 @@ export function LoginPage() { } function backToSignIn() { + if (sessionChange) { + logout(); + setSessionChange(false); + } setChangeRequired(null); setNewPassword(''); setConfirmPassword(''); @@ -152,195 +177,195 @@ export function LoginPage() { title={changeRequired ? 'Set a New Password' : 'Welcome Back'} subtitle={changeRequired ? changeRequired.message : 'Sign in to continue to Krishna Sales'} > - {error && ( -
- - {error} + {error && ( +
+ + {error} +
+ )} + + {changeRequired ? ( + /* Forced Password Change Form */ +
+
+ + + You must update your password before continuing to Krishna Sales. +
- )} - {changeRequired ? ( - /* Forced Password Change Form */ - -
- - - You must update your password before continuing to Krishna Sales. - -
- - {/* New Password */} -
-
- - { - setNewPassword(e.target.value); - if (fieldErrors.newPassword) setFieldErrors((prev) => ({ ...prev, newPassword: false })); - setError(null); - }} - autoFocus - disabled={busy} - className={cn( - 'h-12 w-full pl-11 pr-11 rounded-full outline-none transition-all text-sm', - fieldErrors.newPassword - ? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400' - : 'border border-slate-200 bg-slate-100/80 text-slate-900 placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400' - )} - aria-label="New password" - placeholder="New password" - /> - -
- -
- - {/* Confirm New Password */} -
-
- - { - setConfirmPassword(e.target.value); - if (fieldErrors.confirmPassword) setFieldErrors((prev) => ({ ...prev, confirmPassword: false })); - setError(null); - }} - disabled={busy} - className={cn( - 'h-12 w-full pl-11 pr-11 rounded-full outline-none transition-all text-sm', - fieldErrors.confirmPassword - ? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400' - : 'border border-slate-200 bg-slate-100/80 text-slate-900 placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400' - )} - aria-label="Confirm new password" - placeholder="Confirm new password" - /> - -
-
- - - - -
- ) : ( - /* Standard Login Form */ -
+ {/* New Password */} +
- + { - setEmail(e.target.value); - if (fieldErrors.email) setFieldErrors((prev) => ({ ...prev, email: false })); + setNewPassword(e.target.value); + if (fieldErrors.newPassword) setFieldErrors((prev) => ({ ...prev, newPassword: false })); setError(null); }} autoFocus disabled={busy} className={cn( - 'h-12 w-full pl-11 pr-4 rounded-full outline-none transition-all text-sm', - fieldErrors.email + 'h-12 w-full pl-11 pr-11 rounded-full outline-none transition-all text-sm', + fieldErrors.newPassword ? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400' : 'border border-slate-200 bg-slate-100/80 text-slate-900 placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400' )} - placeholder="Email" + aria-label="New password" + placeholder="New password" /> -
- -
-
- - { - setPassword(e.target.value); - if (fieldErrors.password) setFieldErrors((prev) => ({ ...prev, password: false })); - setError(null); - }} - disabled={busy} - className={cn( - 'h-12 w-full pl-11 pr-11 rounded-full outline-none transition-all text-sm', - fieldErrors.password - ? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400' - : 'border border-slate-200 bg-slate-100/80 text-slate-900 placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400' - )} - placeholder="Password" - /> - -
- setShowNewPassword((s) => !s)} + disabled={busy} + tabIndex={-1} + className="absolute right-4 text-slate-600 hover:text-slate-800 transition-colors cursor-pointer" > - Forgot password? - + {showNewPassword ? : } +
+ +
- +
+ + + + + + + ) : ( + /* Standard Login Form */ +
+
+ + { + setEmail(e.target.value); + if (fieldErrors.email) setFieldErrors((prev) => ({ ...prev, email: false })); + setError(null); + }} + autoFocus disabled={busy} - className="mt-1 !rounded-full !h-12 !text-base bg-gradient-to-r from-[#3aa75a] to-[#0f5a30] hover:brightness-110 text-white border-transparent shadow-lg shadow-emerald-900/20" + className={cn( + 'h-12 w-full pl-11 pr-4 rounded-full outline-none transition-all text-sm', + fieldErrors.email + ? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400' + : 'border border-slate-200 bg-slate-100/80 text-slate-900 placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400' + )} + placeholder="Email" + /> +
+ +
+
+ + { + setPassword(e.target.value); + if (fieldErrors.password) setFieldErrors((prev) => ({ ...prev, password: false })); + setError(null); + }} + disabled={busy} + className={cn( + 'h-12 w-full pl-11 pr-11 rounded-full outline-none transition-all text-sm', + fieldErrors.password + ? 'border-2 border-rose-500 bg-rose-50/20 text-rose-900 focus:ring-2 focus:ring-rose-400' + : 'border border-slate-200 bg-slate-100/80 text-slate-900 placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-300 focus:border-emerald-400' + )} + placeholder="Password" + /> + +
+ - {busy ? 'Signing in…' : (<>Sign In )} - - - )} + Forgot password? + +
+ + + + )} ); }