import { LoginRequest } from "dbtype/mod.ts"; import { ApiError, loginService, LoginResponse, logoutService, refreshService, resetPasswordService, } from "./api.ts"; import { useEffect, useState } from "react"; let localObj: LoginResponse | null = null; function getUserSessions() { if (!localObj) { const storagestr = localStorage.getItem("UserLoginContext"); const storage = storagestr ? (JSON.parse(storagestr) as LoginResponse | null) : null; // update localObj from storage localObj = storage; } if (localObj && localObj.accessExpired > Math.floor(Date.now())) { return { username: localObj.username, permission: localObj.permission, accessExpired: localObj.accessExpired, }; } return null; } function setUserSessions(user: LoginResponse | null) { localObj = user; if (user) { localStorage.setItem("UserLoginContext", JSON.stringify(user)); } else { localStorage.removeItem("UserLoginContext"); } } class AuthEvent extends CustomEvent<{ type: "login" | "logout" | "refresh"; user: { username: string; permission: string[] } | null; }> { constructor(type: "login" | "logout" | "refresh", user: { username: string; permission: string[] } | null) { super("auth", { detail: { type, user } }); } } declare global { interface WindowEventMap { auth: AuthEvent; } function addEventListener( type: "auth", listener: (this: Window, ev: AuthEvent) => void, options?: boolean | AddEventListenerOptions ): void; function removeEventListener( type: "auth", listener: (this: Window, ev: AuthEvent) => void, options?: boolean | EventListenerOptions): void; } export async function refresh() { try { const r = await refreshService(); setUserSessions(r); window.dispatchEvent(new AuthEvent("refresh", { username: r.username, permission: r.permission })); return { username: r.username, permission: r.permission, }; } catch (e) { if (e instanceof ApiError) { console.error(`Refresh failed: ${e.detail}`); } setUserSessions(null); window.dispatchEvent(new AuthEvent("logout", null)); return { username: "", permission: [], }; } } export const doLogout = async () => { try { const res = await logoutService(); setUserSessions(null); window.dispatchEvent(new AuthEvent("logout", null)); return { username: res.username, permission: res.permission, }; } catch (error) { console.error(`Server Error ${error}`); setUserSessions(null); return { username: "", permission: [] }; } }; export const doLogin = async (userLoginInfo: LoginRequest): Promise<{ ok: true; data: LoginResponse; } | { ok: false; error: string; }> => { try { const b = await loginService(userLoginInfo); setUserSessions(b); window.dispatchEvent(new AuthEvent("login", { username: b.username, permission: b.permission })); return { ok: true, data: b }; } catch (e) { if (e instanceof ApiError) { return { ok: false, error: e.detail ?? e.message }; } return { ok: false, error: "An unknown error occurred." }; } }; Object.assign(window, { doLogin, doLogout, refresh, }); export const doResetPassword = async (username: string, oldpassword: string, newpassword: string) => { try { return await resetPasswordService(username, oldpassword, newpassword); } catch (e) { if (e instanceof ApiError) { return { ok: false as const, error: e.detail ?? e.message }; } return { ok: false as const, error: "An unknown error occurred." }; } }; export function useLogin() { const [user, setUser] = useState(getUserSessions()); useEffect(() => { const listener = (_e: AuthEvent) => { setUser(getUserSessions()); }; window.addEventListener("auth", listener); return () => { window.removeEventListener("auth", listener); }; }, []); return user; } document.addEventListener("visibilitychange", async () => { if (document.visibilityState === "visible") { const session = getUserSessions(); if (!session) { await refresh(); } if (session && session.accessExpired - Date.now() < 5 * 60 * 1000) { // access token will expire in 5 minutes, refresh it await refresh(); } // If the session is still valid, do nothing } });