feat: implement login functionality with error handling and session management

This commit is contained in:
2026-07-13 10:40:40 +05:30
parent e0167caf85
commit 36302ff564
5 changed files with 162 additions and 16 deletions
+30
View File
@@ -0,0 +1,30 @@
import { callAuthFunction } from "@/lib/api/http"
export interface AuthUser {
id: string
fullName: string | null
userName: string | null
email: string | null
mobileNumber: string | null
emailVerified: boolean
mobileNumberVerified: boolean
isMfaEnabled: boolean
roleId: string | null
userTypeId: string | null
}
export interface LoginResult {
accessToken: string
refreshToken: string
expiresIn: number
user: AuthUser
}
/** POST /api/loginUser (forced functionName "loginUser" server-side). */
export function loginUser(params: { identifier: string; password: string; deviceName?: string }) {
return callAuthFunction<LoginResult>("/api/loginUser", "loginUser", {
identifier: params.identifier,
password: params.password,
deviceName: params.deviceName ?? (typeof navigator !== "undefined" ? navigator.userAgent : "Unknown Device"),
})
}
+67
View File
@@ -0,0 +1,67 @@
// Thin client for the AuthHex service's function-dispatch envelope
// (POST { functionName, payload, reference } -> { statusCode, success, message, data }).
// See D:\HexDive\ERP_Auth_Service\API_DOCUMENTATION.md for the full contract.
const AUTH_API_BASE_URL =
process.env.NEXT_PUBLIC_AUTH_API_BASE_URL?.replace(/\/$/, "") ?? "https://localhost:7111"
interface ApiEnvelope<T> {
statusCode: number
success: boolean
message: string | null
data: T | null
}
export class AuthApiError extends Error {
statusCode: number
constructor(message: string, statusCode: number) {
super(message)
this.name = "AuthApiError"
this.statusCode = statusCode
}
}
function makeReference() {
return typeof crypto !== "undefined" && "randomUUID" in crypto
? crypto.randomUUID()
: `req-${Date.now()}-${Math.random().toString(36).slice(2)}`
}
/**
* Calls one function on an AuthHex route (e.g. POST /api/loginUser).
* The service's own `success` flag is not reliable on error paths (it defaults
* to true even when statusCode is 4xx/5xx), so failure is judged from
* `!response.ok || body.statusCode !== 200`, not from `body.success`.
*/
export async function callAuthFunction<T>(
route: string,
functionName: string,
payload: Record<string, unknown> = {}
): Promise<T> {
let response: Response
try {
response = await fetch(`${AUTH_API_BASE_URL}${route}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ functionName, payload, reference: makeReference() }),
})
} catch {
throw new AuthApiError(
"Could not reach the authentication service. Check your connection and try again.",
0
)
}
let body: ApiEnvelope<T> | null = null
try {
body = (await response.json()) as ApiEnvelope<T>
} catch {
// non-JSON body — body stays null, handled below
}
if (!response.ok || !body || body.statusCode !== 200) {
throw new AuthApiError(body?.message || `Request failed (${response.status})`, body?.statusCode ?? response.status)
}
return body.data as T
}
+39
View File
@@ -0,0 +1,39 @@
import type { AuthUser, LoginResult } from "@/lib/api/auth"
const ACCESS_TOKEN_KEY = "hexa_erp_access_token"
const REFRESH_TOKEN_KEY = "hexa_erp_refresh_token"
const USER_KEY = "hexa_erp_user"
/** remember=true persists across browser restarts (localStorage); otherwise session-only. */
export function persistSession(result: LoginResult, remember: boolean) {
const store = remember ? window.localStorage : window.sessionStorage
const other = remember ? window.sessionStorage : window.localStorage
for (const key of [ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, USER_KEY]) other.removeItem(key)
store.setItem(ACCESS_TOKEN_KEY, result.accessToken)
store.setItem(REFRESH_TOKEN_KEY, result.refreshToken)
store.setItem(USER_KEY, JSON.stringify(result.user))
}
export function getAccessToken(): string | null {
if (typeof window === "undefined") return null
return window.localStorage.getItem(ACCESS_TOKEN_KEY) ?? window.sessionStorage.getItem(ACCESS_TOKEN_KEY)
}
export function getStoredUser(): AuthUser | null {
if (typeof window === "undefined") return null
const raw = window.localStorage.getItem(USER_KEY) ?? window.sessionStorage.getItem(USER_KEY)
if (!raw) return null
try {
return JSON.parse(raw) as AuthUser
} catch {
return null
}
}
export function clearSession() {
for (const key of [ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, USER_KEY]) {
window.localStorage.removeItem(key)
window.sessionStorage.removeItem(key)
}
}
+6 -13
View File
@@ -15,21 +15,14 @@ export const emailSchema = z.preprocess(
z.string().min(1, "Email is required").email("Enter a valid email")
)
// Login schema (email + password) for reuse across the app
// Login schema (email + password) for reuse across the app.
// Password strength is intentionally NOT re-validated here: an existing
// account's password may predate current complexity rules, and only the
// server can judge whether credentials are actually correct. Complexity
// rules belong on registration/reset-password, not on login.
export const loginSchema = z.object({
email: emailSchema,
password: requiredString("Password is required")
.refine((val) => val.length >= 8, { message: "Password must be at least 8 characters" })
.refine((val) => /[A-Z]/.test(val), {
message: "Password must contain at least one uppercase letter",
})
.refine((val) => /[a-z]/.test(val), {
message: "Password must contain at least one lowercase letter",
})
.refine((val) => /[0-9]/.test(val), { message: "Password must contain at least one number" })
.refine((val) => /[!@#$%^&*(),.?":{}|<>\[\]\\/`~;'+=-]/.test(val), {
message: "Password must contain at least one special character",
}),
password: requiredString("Password is required"),
})
export type LoginValues = z.infer<typeof loginSchema>