ui and bug fixers 29/09

This commit is contained in:
2026-09-29 14:49:11 +05:30
parent d24072e20a
commit b2646c6cb3
85 changed files with 30050 additions and 750 deletions
+43 -1
View File
@@ -1,7 +1,13 @@
// Auth endpoints (docs/11-BACKEND-PHASE1.md §2.0). ERPCore proxies the AuthHex IdP and
// delivers the session as httpOnly cookies — there is no token for JS to hold or attach.
import { apiRequest } from "@/lib/api-client"
import { AuthSession, LoginRequest, RegisterRequest } from "@/types/auth"
import {
AuthSession,
ForgotPasswordResponse,
LoginRequest,
RegisterRequest,
VerifyRecoveryOtpResponse,
} from "@/types/auth"
import { MeResponse } from "@/types/rbac"
export const authApi = {
@@ -31,6 +37,42 @@ export const authApi = {
return apiRequest<void>("/auth/logout", { method: "POST", body: { userId } })
},
/**
* Changes the signed-in user's password (also clears a pending first-login forced change).
* The server ends the session on success, so the caller must send the user back to /login.
*/
changePassword(currentPassword: string, newPassword: string): Promise<void> {
return apiRequest<void>("/auth/change-password", {
method: "POST",
body: { currentPassword, newPassword },
csrf: true,
})
},
/** Forgot password, step 1: emails a recovery OTP. Returns the reference the next steps need. */
forgotPassword(identifier: string): Promise<ForgotPasswordResponse> {
return apiRequest<ForgotPasswordResponse>("/auth/recovery/forgot-password", {
method: "POST",
body: { identifier, useResetLink: false, numberOfDigits: 6 },
})
},
/** Forgot password, step 2: checks the emailed OTP against the reference. */
verifyRecoveryOtp(referenceNumber: string, otpCode: string): Promise<VerifyRecoveryOtpResponse> {
return apiRequest<VerifyRecoveryOtpResponse>("/auth/recovery/verify-otp", {
method: "POST",
body: { referenceNumber, otpCode },
})
},
/** Forgot password, step 3: sets the new password for a verified reference. */
resetPassword(referenceNumber: string, newPassword: string, confirmPassword: string): Promise<void> {
return apiRequest<void>("/auth/recovery/reset-password", {
method: "POST",
body: { referenceNumber, newPassword, confirmPassword },
})
},
/** Exchanges the path-scoped erp_rt cookie for a fresh session. */
refresh(): Promise<AuthSession> {
return apiRequest<AuthSession>("/auth/refresh-token", { method: "POST", body: {} })
@@ -0,0 +1,67 @@
// Carries the forgot-password flow's state (email → OTP → reset) across its three pages.
// sessionStorage keeps it per-tab and gone once the tab closes; the reference number is only
// useful together with the emailed OTP, so nothing sensitive is held here.
import { useSyncExternalStore } from "react"
const KEY = "erpcore.passwordRecovery"
export interface PasswordRecoveryState {
email: string
referenceNumber: string
/** Set once the OTP has been accepted — the reset page requires it. */
verified: boolean
}
const listeners = new Set<() => void>()
// useSyncExternalStore needs a stable snapshot, so re-parse only when the stored string changes.
let cachedRaw: string | null = null
let cachedState: PasswordRecoveryState | null = null
function readRaw(): string | null {
try {
return window.sessionStorage.getItem(KEY)
} catch {
return null
}
}
function getSnapshot(): PasswordRecoveryState | null {
const raw = readRaw()
if (raw !== cachedRaw) {
cachedRaw = raw
try {
cachedState = raw ? (JSON.parse(raw) as PasswordRecoveryState) : null
} catch {
cachedState = null
}
}
return cachedState
}
function subscribe(listener: () => void): () => void {
listeners.add(listener)
return () => listeners.delete(listener)
}
function write(raw: string | null): void {
try {
if (raw === null) window.sessionStorage.removeItem(KEY)
else window.sessionStorage.setItem(KEY, raw)
} catch {
// Storage blocked — the next page finds no state and sends the user back to step 1.
}
listeners.forEach((l) => l())
}
/** The in-progress recovery; `undefined` while server-rendering, `null` when none is in progress. */
export function useRecoveryState(): PasswordRecoveryState | null | undefined {
return useSyncExternalStore(subscribe, getSnapshot, () => undefined)
}
export function setRecoveryState(state: PasswordRecoveryState): void {
write(JSON.stringify(state))
}
export function clearRecoveryState(): void {
write(null)
}