UI 수정, SMS 클래스 추가

This commit is contained in:
comicgum
2026-09-10 07:03:11 +09:00
parent 4187407e96
commit 4a6c9d2afe
9 changed files with 356 additions and 62 deletions
+59 -42
View File
@@ -41,21 +41,29 @@ type JwtPayload = {
permission: number;
}
// "idle": not restored yet, "loading": restoring from the refresh cookie,
// "authed": access token available, "guest": no valid session
export type AuthStatus = "idle" | "loading" | "authed" | "guest";
type AuthState = {
accessToken: string | null;
tokenPayload: JwtPayload | null;
authStatus: AuthStatus;
setAccessToken: (token: string) => void;
clearAuth: () => void;
setAuthStatus: (status: AuthStatus) => void;
};
export const useAuthStore = create<AuthState>((set: any) => ({
accessToken: null,
tokenPayload: null,
authStatus: "idle",
setAccessToken: (token: string) => {
set({ accessToken: token, tokenPayload: jwtDecode<JwtPayload>(token) });
set({ accessToken: token, tokenPayload: jwtDecode<JwtPayload>(token), authStatus: "authed" });
},
clearAuth: () => set({ accessToken: null, tokenPayload: null }),
clearAuth: () => set({ accessToken: null, tokenPayload: null, authStatus: "guest" }),
setAuthStatus: (status: AuthStatus) => set({ authStatus: status }),
}));
type UiLoadingState = {
@@ -143,11 +151,31 @@ api.interceptors.request.use((config) => {
});
// refresh token part
let g_isRefreshing = false;
let g_queue_refresh_tok_value: {
resolve: (token: string) => void;
reject: (err: any) => void;
}[] = [];
// A single in-flight refresh call is shared by the 401 retry path and by the
// startup restore, so concurrent 401s never hit /api/sval2.do more than once.
let g_refreshPromise: Promise<string> | null = null;
const requestNewAccessToken = async (): Promise<string> => {
const resp = await api.post(TOK_REFRESH_URI);
// The refresh endpoint answers 200 with an error code when the account is gone or stopped
const newToken = resp?.data?.result?.sval1;
if (resp?.data?.errCode != 0 || !newToken)
throw new Error("token refresh failed, errCode: " + resp?.data?.errCode);
useAuthStore.getState().setAccessToken(newToken);
return newToken;
};
export const refreshAccessToken = (): Promise<string> => {
if (g_refreshPromise == null) {
g_refreshPromise = requestNewAccessToken().finally(() => {
g_refreshPromise = null;
});
}
return g_refreshPromise;
};
api.interceptors.response.use(
(response) => {
@@ -165,58 +193,36 @@ api.interceptors.response.use(
return Promise.reject(error);
}
if (original._retry) {
return Promise.reject(error);
}
const status = error.response?.status;
if (status !== 401) {
return Promise.reject(error);
}
// The refresh call itself failed: drop the session and let the caller decide where to go
if (original.url === TOK_REFRESH_URI) {
useAuthStore.getState().clearAuth();
window.location.href = SIGN_IN_PAGE_URI;
return Promise.reject(error);
}
if (g_isRefreshing) {
return new Promise((resolve, reject) => {
g_queue_refresh_tok_value.push({ resolve, reject });
}).then((newToken) => {
original.headers = original.headers ?? {};
original.headers.Authorization = `Bearer ${newToken}`;
return api(original);
});
// Already retried once with a fresh token, do not loop
if (original._retry) {
return Promise.reject(error);
}
original._retry = true;
g_isRefreshing = true;
try {
const resp = await api.post(TOK_REFRESH_URI);
const newToken = resp.data.result.sval1;
useAuthStore.getState().setAccessToken(newToken);
g_queue_refresh_tok_value.forEach(({ resolve }) => resolve(newToken));
g_queue_refresh_tok_value = [];
const newToken = await refreshAccessToken();
original.headers = original.headers ?? {};
original.headers.Authorization = `Bearer ${newToken}`;
return api(original);
} catch (refreshError) {
g_queue_refresh_tok_value.forEach(({ reject }) => reject(refreshError));
g_queue_refresh_tok_value = [];
useAuthStore.getState().clearAuth();
window.location.href = SIGN_IN_PAGE_URI;
return Promise.reject(refreshError);
} finally {
g_isRefreshing = false;
}
}
);
@@ -225,17 +231,28 @@ export const clearAuthStore = async () => {
useAuthStore.getState().clearAuth();
};
export const refreshAuthStore = async () => {
if (useAuthStore.getState().tokenPayload != null)
return;
/**
* Restores the session from the sval2 refresh cookie on a full page load.
* Returns true when an access token is available afterwards.
*/
export const bootstrapAuth = async (): Promise<boolean> => {
if (useAuthStore.getState().accessToken != null)
return true;
useAuthStore.getState().setAuthStatus("loading");
try {
const resp = await api.post(TOK_REFRESH_URI);
const newToken = resp.data.result.sval1;
useAuthStore.getState().setAccessToken(newToken);
await refreshAccessToken();
return true;
}
catch (e) {}
catch (e) {
useAuthStore.getState().clearAuth();
return false;
}
};
export const refreshAuthStore = async () => {
await bootstrapAuth();
};
//////////////////////////////////////////////////////////////////////////////