ui and bug fixers 29/09
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user