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>(
|
async function request<T>(
|
||||||
path: string,
|
path: string,
|
||||||
options: RequestInit = {},
|
options: RequestInit = {},
|
||||||
|
retry = true,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
@@ -38,6 +51,13 @@ async function request<T>(
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
|
// تلاش یکباره برای تازهسازی توکن، سپس اجرای مجدد همان درخواست
|
||||||
|
if (retry) {
|
||||||
|
const newToken = await refreshOnce();
|
||||||
|
if (newToken) {
|
||||||
|
return request<T>(path, options, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
useAuthStore.getState().logout();
|
useAuthStore.getState().logout();
|
||||||
window.location.replace('/admin/login');
|
window.location.replace('/admin/login');
|
||||||
throw new ApiError(401, 'ERR_UNAUTHORIZED', 'نشست منقضی شده است');
|
throw new ApiError(401, 'ERR_UNAUTHORIZED', 'نشست منقضی شده است');
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function LoginPage() {
|
|||||||
});
|
});
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (!res.ok) { toast.error(json?.errors?.[0]?.message ?? 'خطا در ورود'); return; }
|
if (!res.ok) { toast.error(json?.errors?.[0]?.message ?? 'خطا در ورود'); return; }
|
||||||
login(json.access_token);
|
login(json.access_token, json.refresh_token);
|
||||||
toast.success('خوش آمدید');
|
toast.success('خوش آمدید');
|
||||||
} catch { toast.error('خطا در اتصال به سرور'); }
|
} catch { toast.error('خطا در اتصال به سرور'); }
|
||||||
finally { setPwLoading(false); }
|
finally { setPwLoading(false); }
|
||||||
@@ -121,7 +121,7 @@ export default function LoginPage() {
|
|||||||
});
|
});
|
||||||
const lJson = await lRes.json();
|
const lJson = await lRes.json();
|
||||||
if (!lRes.ok) { toast.error(lJson?.errors?.[0]?.message ?? 'کاربری با این شماره یافت نشد'); return; }
|
if (!lRes.ok) { toast.error(lJson?.errors?.[0]?.message ?? 'کاربری با این شماره یافت نشد'); return; }
|
||||||
login(lJson.access_token);
|
login(lJson.access_token, lJson.refresh_token);
|
||||||
toast.success('خوش آمدید');
|
toast.success('خوش آمدید');
|
||||||
} catch { toast.error('خطا در اتصال به سرور'); }
|
} catch { toast.error('خطا در اتصال به سرور'); }
|
||||||
finally { setSmsLoading(false); }
|
finally { setSmsLoading(false); }
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface ContextItem {
|
|||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
token: string | null;
|
token: string | null;
|
||||||
|
refreshToken: string | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
userUuid: string | null;
|
userUuid: string | null;
|
||||||
userName: string | null;
|
userName: string | null;
|
||||||
@@ -23,8 +24,9 @@ interface AuthState {
|
|||||||
context: ContextItem | null;
|
context: ContextItem | null;
|
||||||
availableContexts: ContextItem[];
|
availableContexts: ContextItem[];
|
||||||
|
|
||||||
login: (token: string) => void;
|
login: (token: string, refreshToken?: string | null) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
|
refresh: () => Promise<string | null>;
|
||||||
fetchMe: () => Promise<void>;
|
fetchMe: () => Promise<void>;
|
||||||
switchContext: (dbUuid: string) => Promise<void>;
|
switchContext: (dbUuid: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
token: null,
|
token: null,
|
||||||
|
refreshToken: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
userUuid: null,
|
userUuid: null,
|
||||||
userName: null,
|
userName: null,
|
||||||
@@ -66,14 +69,22 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
context: null,
|
context: null,
|
||||||
availableContexts: [],
|
availableContexts: [],
|
||||||
|
|
||||||
login: (token) => {
|
login: (token, refreshToken = null) => {
|
||||||
set({ token, isAuthenticated: true });
|
set({ token, refreshToken, isAuthenticated: true });
|
||||||
get().fetchMe();
|
get().fetchMe();
|
||||||
},
|
},
|
||||||
|
|
||||||
logout: () =>
|
logout: () => {
|
||||||
|
const rt = get().refreshToken;
|
||||||
|
if (rt) {
|
||||||
|
apiFetch('/oauth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ refresh_token: rt }),
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
set({
|
set({
|
||||||
token: null,
|
token: null,
|
||||||
|
refreshToken: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
userUuid: null,
|
userUuid: null,
|
||||||
userName: null,
|
userName: null,
|
||||||
@@ -83,7 +94,30 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
doctorUuid: null,
|
doctorUuid: null,
|
||||||
context: null,
|
context: null,
|
||||||
availableContexts: [],
|
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 () => {
|
fetchMe: async () => {
|
||||||
try {
|
try {
|
||||||
@@ -125,6 +159,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
name: 'clinicpro-auth',
|
name: 'clinicpro-auth',
|
||||||
partialize: (s) => ({
|
partialize: (s) => ({
|
||||||
token: s.token,
|
token: s.token,
|
||||||
|
refreshToken: s.refreshToken,
|
||||||
isAuthenticated: s.isAuthenticated,
|
isAuthenticated: s.isAuthenticated,
|
||||||
userUuid: s.userUuid,
|
userUuid: s.userUuid,
|
||||||
userName: s.userName,
|
userName: s.userName,
|
||||||
|
|||||||
Reference in New Issue
Block a user