diff --git a/packages/client/src/components/layout/nav.tsx b/packages/client/src/components/layout/nav.tsx
index f77c561..d7fdd3a 100644
--- a/packages/client/src/components/layout/nav.tsx
+++ b/packages/client/src/components/layout/nav.tsx
@@ -41,7 +41,7 @@ const createNavItem = (key: NavLinkKey, name: string, className?: string): NavIt
};
function useNavItemsData() {
- const loginInfo = useLogin();
+ const { user: loginInfo } = useLogin();
const isLoggedIn = Boolean(loginInfo);
return useMemo(() => {
diff --git a/packages/client/src/hook/useTagFilters.ts b/packages/client/src/hook/useTagFilters.ts
index 5d0fb15..74b20f0 100644
--- a/packages/client/src/hook/useTagFilters.ts
+++ b/packages/client/src/hook/useTagFilters.ts
@@ -43,7 +43,7 @@ function applyTagFilters(tags: TagRecord[], filters: TagFilters) {
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveCheck: never = orderBy;
- return filtered;
+ return _exhaustiveCheck;
}
break;
}
diff --git a/packages/client/src/page/contentInfoPage.tsx b/packages/client/src/page/contentInfoPage.tsx
index 0257f0d..9515c32 100644
--- a/packages/client/src/page/contentInfoPage.tsx
+++ b/packages/client/src/page/contentInfoPage.tsx
@@ -41,7 +41,7 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
const rehashDoc = useRehashDoc();
const rescanDoc = useRescanDoc();
const deleteDoc = useDeleteDoc();
- const user = useLogin();
+ const { user } = useLogin();
const username = user?.username;
const isAdmin = username === "admin";
diff --git a/packages/client/src/page/differencePage.tsx b/packages/client/src/page/differencePage.tsx
index 1d4ae70..78dcaa1 100644
--- a/packages/client/src/page/differencePage.tsx
+++ b/packages/client/src/page/differencePage.tsx
@@ -8,7 +8,11 @@ import { Link } from "wouter";
export function DifferencePage() {
const { data, isLoading, error } = useDifferenceDoc();
- const userInfo = useLogin();
+ const { user: userInfo, isLoading: userLoading } = useLogin();
+
+ if (userLoading) {
+ return
Loading...
;
+ }
if (!userInfo) {
return
diff --git a/packages/client/src/page/profilesPage.tsx b/packages/client/src/page/profilesPage.tsx
index 8039a06..6733229 100644
--- a/packages/client/src/page/profilesPage.tsx
+++ b/packages/client/src/page/profilesPage.tsx
@@ -3,7 +3,12 @@ import { useLogin } from "@/state/user";
import { Redirect } from "wouter";
export function ProfilePage() {
- const userInfo = useLogin();
+ const { user: userInfo, isLoading } = useLogin();
+
+ if (isLoading) {
+ return
Loading...
;
+ }
+
if (!userInfo) {
console.error("User session expired. Redirecting to login page.");
return
;
diff --git a/packages/client/src/page/settingPage.tsx b/packages/client/src/page/settingPage.tsx
index 433bc10..a32a7c1 100644
--- a/packages/client/src/page/settingPage.tsx
+++ b/packages/client/src/page/settingPage.tsx
@@ -10,7 +10,7 @@ import { ServerSettingCard } from "@/components/ServerSettingCard";
export function SettingPage() {
- const login = useLogin();
+ const { user: login } = useLogin();
const isAdmin = login?.username === "admin";
const { data: serverSetting, error: serverError, isLoading: serverLoading, mutate } = useServerSettings(isAdmin);
diff --git a/packages/client/src/state/api.ts b/packages/client/src/state/api.ts
index 369b5fc..5c4a835 100644
--- a/packages/client/src/state/api.ts
+++ b/packages/client/src/state/api.ts
@@ -90,3 +90,20 @@ export async function resetPasswordService(username: string, oldpassword: string
}
return b;
}
+
+export async function getProfileService(): Promise<{
+ username: string;
+ permission: string[];
+ authenticated: boolean
+}> {
+ const u = makeApiUrl("/api/user/profile");
+ const res = await fetch(u, {
+ method: "GET",
+ credentials: "include",
+ });
+ const b = await res.json();
+ if (!res.ok) {
+ throw new ApiError(b as ErrorFormat);
+ }
+ return b;
+}
\ No newline at end of file
diff --git a/packages/client/src/state/user.ts b/packages/client/src/state/user.ts
index 07be4d0..150f0e8 100644
--- a/packages/client/src/state/user.ts
+++ b/packages/client/src/state/user.ts
@@ -6,35 +6,14 @@ import {
logoutService,
refreshService,
resetPasswordService,
+ getProfileService,
} from "./api.ts";
-import { useEffect, useState } from "react";
+import { useEffect } from "react";
+import useSWR, { mutate as mutateSWR } from "swr";
-let localObj: LoginResponse | null = null;
+type ProfileResponse = Awaited
>;
-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");
- }
-}
+export const PROFILE_SWR_KEY = "/api/user/profile";
class AuthEvent extends CustomEvent<{
type: "login" | "logout" | "refresh";
@@ -62,8 +41,8 @@ declare global {
export async function refresh() {
try {
const r = await refreshService();
- setUserSessions(r);
window.dispatchEvent(new AuthEvent("refresh", { username: r.username, permission: r.permission }));
+ void mutateSWR(PROFILE_SWR_KEY);
return {
username: r.username,
permission: r.permission,
@@ -72,8 +51,8 @@ export async function refresh() {
if (e instanceof ApiError) {
console.error(`Refresh failed: ${e.detail}`);
}
- setUserSessions(null);
window.dispatchEvent(new AuthEvent("logout", null));
+ void mutateSWR(PROFILE_SWR_KEY, { username: "", permission: [], authenticated: false }, false);
return {
username: "",
permission: [],
@@ -84,8 +63,8 @@ export async function refresh() {
export const doLogout = async () => {
try {
const res = await logoutService();
- setUserSessions(null);
window.dispatchEvent(new AuthEvent("logout", null));
+ void mutateSWR(PROFILE_SWR_KEY, { username: "", permission: [], authenticated: false }, false);
return {
username: res.username,
permission: res.permission,
@@ -93,7 +72,7 @@ export const doLogout = async () => {
} catch (error) {
console.error(`Server Error ${error}`);
- setUserSessions(null);
+ void mutateSWR(PROFILE_SWR_KEY, { username: "", permission: [], authenticated: false }, false);
return { username: "", permission: [] };
}
};
@@ -107,8 +86,8 @@ export const doLogin = async (userLoginInfo: LoginRequest): Promise<{
}> => {
try {
const b = await loginService(userLoginInfo);
- setUserSessions(b);
window.dispatchEvent(new AuthEvent("login", { username: b.username, permission: b.permission }));
+ void mutateSWR(PROFILE_SWR_KEY);
return { ok: true, data: b };
} catch (e) {
if (e instanceof ApiError) {
@@ -135,34 +114,50 @@ export const doResetPassword = async (username: string, oldpassword: string, new
}
};
-
export function useLogin() {
- const [user, setUser] = useState(getUserSessions());
+ const { data, error, isLoading, mutate } = useSWR(
+ PROFILE_SWR_KEY,
+ async () => {
+ try {
+ return await getProfileService();
+ } catch (err) {
+ if (err instanceof ApiError && err.code === 401) {
+ return {
+ authenticated: false,
+ permission: [],
+ username: "",
+ };
+ }
+ throw err;
+ }
+ },
+ {
+ revalidateOnFocus: true,
+ shouldRetryOnError: false,
+ },
+ );
+
useEffect(() => {
- const listener = (_e: AuthEvent) => {
- setUser(getUserSessions());
+ const listener = () => {
+ void mutate();
};
window.addEventListener("auth", listener);
return () => {
window.removeEventListener("auth", listener);
};
- }, []);
- return user;
-}
+ }, [mutate]);
+ const user = data?.authenticated ? {
+ permission: data.permission,
+ username: data.username,
+ } : null;
+ const resolvedError = error instanceof ApiError && error.code === 401 ? undefined : error;
-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
- }
-});
\ No newline at end of file
+ return {
+ error: resolvedError,
+ isLoading,
+ refreshProfile: mutate,
+ user,
+ };
+}
\ No newline at end of file
diff --git a/packages/server/src/login.ts b/packages/server/src/login.ts
index 87b6ef4..2caa253 100644
--- a/packages/server/src/login.ts
+++ b/packages/server/src/login.ts
@@ -238,6 +238,15 @@ async function authenticate(
export const createLoginRouter = (userController: UserAccessor) => {
const router = new Hono();
+ router.get("/profile", async (c) => {
+ const auth = c.get("auth");
+ return c.json({
+ username: auth.user.username,
+ permission: auth.user.permission,
+ authenticated: auth.authenticated,
+ });
+ });
+
router.post(
"/login",
sValidator("json", LoginBodySchema),