From 70930857a3aed4b5061ddcbb9c3befaf35230c6d Mon Sep 17 00:00:00 2001 From: monoid Date: Thu, 6 Nov 2025 01:23:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20useLogin=20=ED=9B=85=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B6=84=ED=95=B4=20=ED=95=A0=EB=8B=B9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EB=A1=9C=EB=94=A9=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20API=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/components/layout/nav.tsx | 2 +- packages/client/src/hook/useTagFilters.ts | 2 +- packages/client/src/page/contentInfoPage.tsx | 2 +- packages/client/src/page/differencePage.tsx | 6 +- packages/client/src/page/profilesPage.tsx | 7 +- packages/client/src/page/settingPage.tsx | 2 +- packages/client/src/state/api.ts | 17 ++++ packages/client/src/state/user.ts | 99 +++++++++---------- packages/server/src/login.ts | 9 ++ 9 files changed, 88 insertions(+), 58 deletions(-) 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),