diff --git a/packages/client/src/page/differencePage.tsx b/packages/client/src/page/differencePage.tsx
index 0d49d9b..1d4ae70 100644
--- a/packages/client/src/page/differencePage.tsx
+++ b/packages/client/src/page/differencePage.tsx
@@ -4,16 +4,18 @@ import { Separator } from "@/components/ui/separator";
import { useDifferenceDoc, commit, commitAll } from "@/hook/useDifference";
import { useLogin } from "@/state/user";
import { Fragment } from "react/jsx-runtime";
+import { Link } from "wouter";
export function DifferencePage() {
const { data, isLoading, error } = useDifferenceDoc();
const userInfo = useLogin();
if (!userInfo) {
- return
+ return
Not logged in
+ Go to Login
}
@@ -26,7 +28,7 @@ export function DifferencePage() {
Difference
Scanned Files List
@@ -41,11 +43,11 @@ export function DifferencePage() {
{x.map((y) => (
{y.path}
+ className="flex-1 text-sm text-wrap">{y.path}
diff --git a/packages/client/src/state/api.ts b/packages/client/src/state/api.ts
index cc994f0..369b5fc 100644
--- a/packages/client/src/state/api.ts
+++ b/packages/client/src/state/api.ts
@@ -14,7 +14,7 @@ export type LogoutResponse = {
};
export type RefreshResponse = LoginResponse & {
- refresh: boolean;
+ refresh: true;
};
export type ErrorFormat = {
diff --git a/packages/client/src/state/user.ts b/packages/client/src/state/user.ts
index 69ad2bb..1c13051 100644
--- a/packages/client/src/state/user.ts
+++ b/packages/client/src/state/user.ts
@@ -1,5 +1,3 @@
-import { atom, useAtomValue } from "../lib/atom.ts";
-import { createStore } from 'jotai';
import { LoginRequest } from "dbtype/mod.ts";
import {
ApiError,
@@ -9,41 +7,63 @@ import {
refreshService,
resetPasswordService,
} from "./api.ts";
-
-// Create a store for setting atom values outside components
-const store = createStore();
+import { useSyncExternalStore } from "react";
let localObj: LoginResponse | null = null;
+
function getUserSessions() {
- if (localObj === null) {
- const storagestr = localStorage.getItem("UserLoginContext") as string | null;
- const storage = storagestr !== null ? (JSON.parse(storagestr) as LoginResponse | null) : null;
+ 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 !== null && localObj.accessExpired > Math.floor(Date.now() / 1000)) {
- return {
- username: localObj.username,
- permission: localObj.permission,
- };
- }
- return null;
+ 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();
- if (r.refresh) {
- localObj = {
- ...r,
- };
- } else {
- localObj = {
- accessExpired: 0,
- username: "",
- permission: r.permission,
- };
- }
- localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
+ setUserSessions(r);
+ window.dispatchEvent(new AuthEvent("refresh", { username: r.username, permission: r.permission }));
return {
username: r.username,
permission: r.permission,
@@ -52,51 +72,49 @@ export async function refresh() {
if (e instanceof ApiError) {
console.error(`Refresh failed: ${e.detail}`);
}
- localObj = { accessExpired: 0, username: "", permission: [] };
- localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
- return { username: "", permission: [] };
+ setUserSessions(null);
+ window.dispatchEvent(new AuthEvent("logout", null));
+ return {
+ username: "",
+ permission: [],
+ };
}
}
export const doLogout = async () => {
try {
const res = await logoutService();
- localObj = {
- accessExpired: 0,
- username: "",
+ setUserSessions(null);
+ window.dispatchEvent(new AuthEvent("logout", null));
+ return {
+ username: res.username,
permission: res.permission,
};
- window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
- store.set(userLoginStateAtom, localObj);
- return {
- username: localObj.username,
- permission: localObj.permission,
- };
+
} catch (error) {
console.error(`Server Error ${error}`);
- // Even if logout fails, clear client-side session
- localObj = { accessExpired: 0, username: "", permission: [] };
- window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
- store.set(userLoginStateAtom, localObj);
- return {
- username: "",
- permission: [],
- };
+ setUserSessions(null);
+ return { username: "", permission: [] };
}
};
-export const doLogin = async (userLoginInfo: LoginRequest): Promise => {
+export const doLogin = async (userLoginInfo: LoginRequest): Promise<{
+ ok: true;
+ data: LoginResponse;
+} | {
+ ok: false;
+ error: string;
+}> => {
try {
const b = await loginService(userLoginInfo);
- localObj = b;
- store.set(userLoginStateAtom, b);
- window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
- return b;
+ 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 e.detail ?? e.message;
+ return { ok: false, error: e.detail ?? e.message };
}
- return "An unknown error occurred.";
+ return { ok: false, error: "An unknown error occurred." };
}
};
@@ -111,23 +129,40 @@ export const doResetPassword = async (username: string, oldpassword: string, new
return await resetPasswordService(username, oldpassword, newpassword);
} catch (e) {
if (e instanceof ApiError) {
- return e.detail ?? e.message;
+ return { ok: false as const, error: e.detail ?? e.message };
}
- return "An unknown error occurred.";
+ return { ok: false as const, error: "An unknown error occurred." };
}
};
-export async function getInitialValue() {
- const user = getUserSessions();
- if (user) {
- return user;
- }
- return refresh();
-}
-
-export const userLoginStateAtom = atom(getUserSessions());
export function useLogin() {
- const val = useAtomValue(userLoginStateAtom);
- return val;
-}
\ No newline at end of file
+ const hook = useSyncExternalStore(
+ (onChange) => {
+ const listener = () => {
+ onChange();
+ };
+ window.addEventListener("auth", listener);
+ return () => window.removeEventListener("auth", listener);
+ },
+ getUserSessions,
+ );
+ return hook;
+}
+
+
+
+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
diff --git a/packages/server/src/login.ts b/packages/server/src/login.ts
index 5f1fc2e..9e3fc3e 100644
--- a/packages/server/src/login.ts
+++ b/packages/server/src/login.ts
@@ -25,7 +25,7 @@ type LoginResponse = {
type RefreshResponse = {
accessExpired: number;
- refresh: boolean;
+ refresh: true;
} & PayloadInfo;
type RefreshPayloadInfo = { username: string };
@@ -45,8 +45,8 @@ const ResetBodySchema = t.Object({
const SettingsBodySchema = t.Record(t.String(), t.Unknown());
-const accessExpiredTime = 60 * 60 * 2; // 2 hours
-const refreshExpiredTime = 60 * 60 * 24 * 14; // 14 days
+const accessExpiredTime = 60 * 60 * 2 * 1000; // 2 hours
+const refreshExpiredTime = 60 * 60 * 24 * 14 * 1000; // 14 days
async function createAccessToken(payload: PayloadInfo, secret: string) {
return await new SignJWT(payload)
@@ -83,7 +83,7 @@ async function verifyToken(token: string, secret: string): Promise {
export const accessTokenName = "access_token";
export const refreshTokenName = "refresh_token";
-function setToken(cookie: CookieJar, token_name: string, token_payload: string | null, expiredSeconds: number) {
+function setToken(cookie: CookieJar, token_name: string, token_payload: string | null, expiredMilliseconds: number) {
if (token_payload === null) {
cookie[token_name]?.remove();
return;
@@ -94,10 +94,14 @@ function setToken(cookie: CookieJar, token_name: string, token_payload: string |
httpOnly: true,
secure: setting.secure,
sameSite: "strict",
- expires: new Date(Date.now() + expiredSeconds * 1000),
+ expires: new Date(Date.now() + expiredMilliseconds),
});
}
+function removeToken(cookie: CookieJar, token_name: string) {
+ cookie[token_name]?.remove();
+}
+
const isUserState = (obj: unknown): obj is PayloadInfo => {
if (typeof obj !== "object" || obj === null) {
return false;
@@ -237,15 +241,15 @@ export const createLoginRouter = (userController: UserAccessor) => {
return {
username: user.username,
permission,
- accessExpired: Math.floor(Date.now() / 1000) + accessExpiredTime,
+ accessExpired: Math.floor(Date.now() ) + accessExpiredTime,
} satisfies LoginResponse;
}, {
body: LoginBodySchema,
})
.post("/logout", ({ cookie, set }) => {
const setting = get_setting();
- setToken(cookie, accessTokenName, null, 0);
- setToken(cookie, refreshTokenName, null, 0);
+ removeToken(cookie, accessTokenName);
+ removeToken(cookie, refreshTokenName);
set.status = 200;
return {
ok: true,
@@ -261,7 +265,7 @@ export const createLoginRouter = (userController: UserAccessor) => {
return {
...auth.user,
refresh: true,
- accessExpired: Math.floor(Date.now() / 1000) + accessExpiredTime,
+ accessExpired: Math.floor(Date.now()) + accessExpiredTime,
} satisfies RefreshResponse;
})
.post("/reset", async ({ body }) => {