feat(auth): implement refresh token handling and update login method
This commit is contained in:
@@ -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', 'نشست منقضی شده است');
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user