ionian/packages/client/src/state/user.ts

168 lines
No EOL
4.1 KiB
TypeScript

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
}
});