Skip to content

Commit 98cb14e

Browse files
Merge pull request #23 from NYCU-SDC/fix/fix-refresh-token-logic
Fix refresh token logic
2 parents 8530fbf + 7a50fd2 commit 98cb14e

File tree

9 files changed

+60
-200
lines changed

9 files changed

+60
-200
lines changed

src/features/auth/components/CallbackPage.tsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ function getSafeRedirectTarget(): string | null {
2323
}
2424
}
2525

26-
function getRefreshTokenFromUrl(): string | null {
27-
const params = new URLSearchParams(window.location.search);
28-
return params.get("refreshToken") ?? params.get("rt");
29-
}
30-
3126
export const CallbackPage = () => {
3227
const navigate = useNavigate();
3328
const queryClient = useQueryClient();
@@ -38,11 +33,6 @@ export const CallbackPage = () => {
3833
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
3934

4035
try {
41-
const refreshToken = getRefreshTokenFromUrl();
42-
if (refreshToken) {
43-
authService.setStoredRefreshToken(refreshToken);
44-
}
45-
4636
let user: AuthUser | null = null;
4737

4838
for (let attempt = 0; attempt < 3; attempt += 1) {

src/features/auth/hooks/useAuth.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { authService } from "@/features/auth/services/authService";
22
import type { UserOnboardingRequest } from "@nycu-sdc/core-system-sdk";
3+
import { authRefreshToken } from "@nycu-sdc/core-system-sdk";
34
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5+
import { useEffect } from "react";
6+
7+
const DEFAULT_AUTH_REFRESH_INTERVAL = 5 * 60 * 1000;
48

59
export const useMe = () =>
610
useQuery({
@@ -30,3 +34,13 @@ export const useAuth = () => {
3034
isLoading
3135
};
3236
};
37+
38+
export const useAuthRefreshInterval = () => {
39+
useEffect(() => {
40+
const interval = setInterval(() => {
41+
authRefreshToken("");
42+
}, DEFAULT_AUTH_REFRESH_INTERVAL);
43+
44+
return () => clearInterval(interval);
45+
}, []);
46+
};
Lines changed: 4 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { assertOk } from "@/shared/utils/apiError";
22
import type { UserOnboardingRequest, UserUser } from "@nycu-sdc/core-system-sdk";
3-
import { authLogout, authRefreshToken, userGetMe, userUpdateUsername } from "@nycu-sdc/core-system-sdk";
3+
import { authLogout, userGetMe, userUpdateUsername } from "@nycu-sdc/core-system-sdk";
44

55
export type OAuthProvider = "google" | "nycu";
66

@@ -10,105 +10,11 @@ const defaultRequestOptions: RequestInit = {
1010
credentials: "include"
1111
};
1212

13-
const REFRESH_TOKEN_STORAGE_KEY = "core-system.refresh-token";
14-
const ACCESS_TOKEN_EXPIRY_STORAGE_KEY = "core-system.access-token-exp-ms";
15-
let refreshInFlight: Promise<boolean> | null = null;
16-
17-
const parseJwtExpirationMs = (jwt: string): number | null => {
18-
try {
19-
const [, payloadBase64Url] = jwt.split(".");
20-
if (!payloadBase64Url) return null;
21-
const payloadBase64 = payloadBase64Url.replace(/-/g, "+").replace(/_/g, "/");
22-
const padded = payloadBase64 + "=".repeat((4 - (payloadBase64.length % 4)) % 4);
23-
const payloadJson = atob(padded);
24-
const payload = JSON.parse(payloadJson) as { exp?: number };
25-
if (!payload.exp || typeof payload.exp !== "number") return null;
26-
return payload.exp * 1000;
27-
} catch {
28-
return null;
29-
}
30-
};
31-
32-
export const withAuthRefreshRetry = async <T extends { status: number }>(request: () => Promise<T>): Promise<T> => {
33-
const firstResponse = await request();
34-
if (firstResponse.status !== 401) return firstResponse;
35-
36-
if (!refreshInFlight) {
37-
refreshInFlight = authService
38-
.refreshAccessToken()
39-
.catch(() => false)
40-
.finally(() => {
41-
refreshInFlight = null;
42-
});
43-
}
44-
45-
const refreshed = await refreshInFlight;
46-
if (!refreshed) return firstResponse;
47-
48-
return request();
49-
};
50-
5113
const normalizeProvider = (provider: OAuthProvider): string => {
5214
return provider.toLowerCase();
5315
};
5416

5517
export const authService = {
56-
getStoredRefreshToken(): string | null {
57-
if (typeof window === "undefined") return null;
58-
return window.localStorage.getItem(REFRESH_TOKEN_STORAGE_KEY);
59-
},
60-
61-
setStoredRefreshToken(refreshToken: string) {
62-
if (typeof window === "undefined") return;
63-
window.localStorage.setItem(REFRESH_TOKEN_STORAGE_KEY, refreshToken);
64-
},
65-
66-
getStoredAccessTokenExpiryMs(): number | null {
67-
if (typeof window === "undefined") return null;
68-
const raw = window.localStorage.getItem(ACCESS_TOKEN_EXPIRY_STORAGE_KEY);
69-
if (!raw) return null;
70-
const value = Number(raw);
71-
return Number.isFinite(value) ? value : null;
72-
},
73-
74-
setStoredAccessTokenExpiryMs(expirationMs: number) {
75-
if (typeof window === "undefined") return;
76-
window.localStorage.setItem(ACCESS_TOKEN_EXPIRY_STORAGE_KEY, String(expirationMs));
77-
},
78-
79-
clearStoredAccessTokenExpiryMs() {
80-
if (typeof window === "undefined") return;
81-
window.localStorage.removeItem(ACCESS_TOKEN_EXPIRY_STORAGE_KEY);
82-
},
83-
84-
clearStoredRefreshToken() {
85-
if (typeof window === "undefined") return;
86-
window.localStorage.removeItem(REFRESH_TOKEN_STORAGE_KEY);
87-
this.clearStoredAccessTokenExpiryMs();
88-
},
89-
90-
async refreshAccessToken(): Promise<boolean> {
91-
const refreshToken = this.getStoredRefreshToken();
92-
if (!refreshToken) return false;
93-
94-
const res = await authRefreshToken(refreshToken, defaultRequestOptions);
95-
if (res.status === 404) {
96-
this.clearStoredRefreshToken();
97-
throw new Error("Refresh token expired");
98-
}
99-
100-
assertOk(res.status, "Failed to refresh access token", res.data);
101-
if (res.data?.refreshToken) {
102-
this.setStoredRefreshToken(res.data.refreshToken);
103-
}
104-
const accessTokenExpMs = parseJwtExpirationMs(res.data.accessToken);
105-
if (accessTokenExpMs) {
106-
this.setStoredAccessTokenExpiryMs(accessTokenExpMs);
107-
}
108-
109-
return true;
110-
},
111-
11218
redirectToOAuthLogin(
11319
provider: OAuthProvider,
11420
options: {
@@ -132,19 +38,18 @@ export const authService = {
13238
},
13339

13440
async logout(): Promise<void> {
135-
const res = await withAuthRefreshRetry(() => authLogout(defaultRequestOptions));
41+
const res = await authLogout(defaultRequestOptions);
13642
assertOk(res.status, "Failed to logout", res.data);
137-
this.clearStoredRefreshToken();
13843
},
13944

14045
async getCurrentUser<T extends UserUser = UserUser>(): Promise<T> {
141-
const res = await withAuthRefreshRetry(() => userGetMe(defaultRequestOptions));
46+
const res = await userGetMe(defaultRequestOptions);
14247
assertOk(res.status, "Failed to get current user", res.data);
14348
return res.data as T;
14449
},
14550

14651
async updateOnboarding(data: UserOnboardingRequest): Promise<void> {
147-
const res = await withAuthRefreshRetry(() => userUpdateUsername(data, defaultRequestOptions));
52+
const res = await userUpdateUsername(data, defaultRequestOptions);
14853
assertOk(res.status, "Failed to update onboarding", res.data);
14954
}
15055
};

src/features/dashboard/services/api.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { withAuthRefreshRetry } from "@/features/auth/services/authService";
21
import { assertOk } from "@/shared/utils/apiError";
32
import type { SlugGetSlugHistory200, SlugStatus, UnitOrgMemberRequest, UnitOrganization, UnitUpdateOrgRequest } from "@nycu-sdc/core-system-sdk";
43
import {
@@ -17,48 +16,48 @@ const defaultRequestOptions: RequestInit = {
1716
};
1817

1918
export const getOrg = async (slug: string): Promise<UnitOrganization> => {
20-
const res = await withAuthRefreshRetry(() => unitGetOrgById(slug, defaultRequestOptions));
19+
const res = await unitGetOrgById(slug, defaultRequestOptions);
2120
assertOk(res.status, "Failed to load organization", res.data);
2221
return res.data;
2322
};
2423

2524
export const listOrgMembers = async (slug: string) => {
26-
const res = await withAuthRefreshRetry(() => unitListOrgMembers(slug, defaultRequestOptions));
25+
const res = await unitListOrgMembers(slug, defaultRequestOptions);
2726
assertOk(res.status, "Failed to load members", res.data);
2827
return res.data;
2928
};
3029

3130
export const updateOrg = async (slug: string, req: UnitUpdateOrgRequest) => {
32-
const res = await withAuthRefreshRetry(() => unitUpdateOrg(slug, req, defaultRequestOptions));
31+
const res = await unitUpdateOrg(slug, req, defaultRequestOptions);
3332
assertOk(res.status, "Failed to update organization", res.data);
3433
return res.data;
3534
};
3635

3736
export const addOrgMember = async (slug: string, req: UnitOrgMemberRequest) => {
38-
const res = await withAuthRefreshRetry(() => unitAddOrgMember(slug, req, defaultRequestOptions));
37+
const res = await unitAddOrgMember(slug, req, defaultRequestOptions);
3938
assertOk(res.status, "Failed to add member", res.data);
4039
return res.data;
4140
};
4241

4342
export const removeOrgMember = async (slug: string, memberId: string): Promise<void> => {
44-
const res = await withAuthRefreshRetry(() => unitRemoveOrgMember(slug, memberId, defaultRequestOptions));
43+
const res = await unitRemoveOrgMember(slug, memberId, defaultRequestOptions);
4544
assertOk(res.status, "Failed to remove member", res.data);
4645
};
4746

4847
export const listMyOrgs = async (): Promise<UnitOrganization[]> => {
49-
const res = await withAuthRefreshRetry(() => unitListOrganizationsOfCurrentUser(defaultRequestOptions));
48+
const res = await unitListOrganizationsOfCurrentUser(defaultRequestOptions);
5049
assertOk(res.status, "Failed to load my organizations", res.data);
5150
return res.data;
5251
};
5352

5453
export const getSlugStatus = async (slug: string): Promise<SlugStatus> => {
55-
const res = await withAuthRefreshRetry(() => slugGetSlugStatus(slug, defaultRequestOptions));
54+
const res = await slugGetSlugStatus(slug, defaultRequestOptions);
5655
assertOk(res.status, "Failed to get slug status", res.data);
5756
return res.data as SlugStatus;
5857
};
5958

6059
export const getSlugHistory = async (slug: string): Promise<SlugGetSlugHistory200> => {
61-
const res = await withAuthRefreshRetry(() => slugGetSlugHistory(slug, defaultRequestOptions));
60+
const res = await slugGetSlugHistory(slug, defaultRequestOptions);
6261
assertOk(res.status, "Failed to get slug history", res.data);
6362
return res.data as SlugGetSlugHistory200;
6463
};

src/features/form/components/AdminFormDetailPages/EditPage.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,6 @@ export const AdminFormEditPage = ({ formData }: AdminFormEditPageProps) => {
9292
isMergeNode: pass1.filter(n => n.next === node.id).length + pass1.filter(n => n.nextTrue === node.id).length + pass1.filter(n => n.nextFalse === node.id).length > 1
9393
}));
9494

95-
console.log("Post-processed nodes:", res);
96-
9795
return res;
9896
};
9997

0 commit comments

Comments
 (0)