feat(auth): implement refresh token handling and update login method

This commit is contained in:
hamed
2026-06-25 17:37:55 +03:30
parent 694ee28787
commit 9b608aaeac
3 changed files with 62 additions and 7 deletions
+20
View File
@@ -23,9 +23,22 @@ export class ApiError extends Error {
}
}
// چند درخواست همزمان که ۴۰۱ می‌گیرند، فقط یک‌بار refresh را اجرا کنند
let refreshPromise: Promise<string | null> | null = null;
function refreshOnce(): Promise<string | null> {
if (!refreshPromise) {
refreshPromise = useAuthStore.getState().refresh().finally(() => {
refreshPromise = null;
});
}
return refreshPromise;
}
async function request<T>(
path: string,
options: RequestInit = {},
retry = true,
): Promise<T> {
const token = getToken();
const headers: Record<string, string> = {
@@ -38,6 +51,13 @@ async function request<T>(
if (!res.ok) {
if (res.status === 401) {
// تلاش یک‌باره برای تازه‌سازی توکن، سپس اجرای مجدد همان درخواست
if (retry) {
const newToken = await refreshOnce();
if (newToken) {
return request<T>(path, options, false);
}
}
useAuthStore.getState().logout();
window.location.replace('/admin/login');
throw new ApiError(401, 'ERR_UNAUTHORIZED', 'نشست منقضی شده است');
+2 -2
View File
@@ -72,7 +72,7 @@ export default function LoginPage() {
});
const json = await res.json();
if (!res.ok) { toast.error(json?.errors?.[0]?.message ?? 'خطا در ورود'); return; }
login(json.access_token);
login(json.access_token, json.refresh_token);
toast.success('خوش آمدید');
} catch { toast.error('خطا در اتصال به سرور'); }
finally { setPwLoading(false); }
@@ -121,7 +121,7 @@ export default function LoginPage() {
});
const lJson = await lRes.json();
if (!lRes.ok) { toast.error(lJson?.errors?.[0]?.message ?? 'کاربری با این شماره یافت نشد'); return; }
login(lJson.access_token);
login(lJson.access_token, lJson.refresh_token);
toast.success('خوش آمدید');
} catch { toast.error('خطا در اتصال به سرور'); }
finally { setSmsLoading(false); }
+40 -5
View File
@@ -13,6 +13,7 @@ export interface ContextItem {
interface AuthState {
token: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
userUuid: string | null;
userName: string | null;
@@ -23,8 +24,9 @@ interface AuthState {
context: ContextItem | null;
availableContexts: ContextItem[];
login: (token: string) => void;
login: (token: string, refreshToken?: string | null) => void;
logout: () => void;
refresh: () => Promise<string | null>;
fetchMe: () => Promise<void>;
switchContext: (dbUuid: string) => Promise<void>;
}
@@ -56,6 +58,7 @@ export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
token: null,
refreshToken: null,
isAuthenticated: false,
userUuid: null,
userName: null,
@@ -66,14 +69,22 @@ export const useAuthStore = create<AuthState>()(
context: null,
availableContexts: [],
login: (token) => {
set({ token, isAuthenticated: true });
login: (token, refreshToken = null) => {
set({ token, refreshToken, isAuthenticated: true });
get().fetchMe();
},
logout: () =>
logout: () => {
const rt = get().refreshToken;
if (rt) {
apiFetch('/oauth/logout', {
method: 'POST',
body: JSON.stringify({ refresh_token: rt }),
}).catch(() => {});
}
set({
token: null,
refreshToken: null,
isAuthenticated: false,
userUuid: null,
userName: null,
@@ -83,7 +94,30 @@ export const useAuthStore = create<AuthState>()(
doctorUuid: null,
context: null,
availableContexts: [],
}),
});
},
refresh: async () => {
const rt = get().refreshToken;
if (!rt) return null;
try {
const res = await fetch('/oauth/token/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: rt }),
});
const json = await res.json();
if (!res.ok || !json?.access_token) return null;
set({
token: json.access_token,
refreshToken: json.refresh_token ?? rt,
isAuthenticated: true,
});
return json.access_token as string;
} catch {
return null;
}
},
fetchMe: async () => {
try {
@@ -125,6 +159,7 @@ export const useAuthStore = create<AuthState>()(
name: 'clinicpro-auth',
partialize: (s) => ({
token: s.token,
refreshToken: s.refreshToken,
isAuthenticated: s.isAuthenticated,
userUuid: s.userUuid,
userName: s.userName,