feat(ssr): load Pokemon lists and forward auth cookies on server

Update auth middleware to pass incoming request cookies to api.me()
Refactor API service to support custom headers via ApiRequestOptions
Use useAsyncData in PokemonList to fetch initial data during SSR
Ensure graceful fallback to client-side fetching on SSR failure
This commit is contained in:
2026-05-06 10:50:51 +08:00
parent 35ee164794
commit 425f2f4d5f
4 changed files with 105 additions and 25 deletions

View File

@@ -5,6 +5,11 @@ let serverApiBaseUrl = 'http://localhost:3001';
const authTokenKey = 'pokopia_auth_token';
const authChangeEvent = 'pokopia-auth-change';
export interface ApiRequestOptions {
signal?: AbortSignal;
headers?: HeadersInit;
}
export type TranslationField = 'name' | 'title' | 'details' | 'genus' | 'effect' | 'mosslaxEffect';
export type TranslationMap = Record<string, Partial<Record<TranslationField, string>>>;
@@ -1104,12 +1109,15 @@ export function notifyAuthChange(): void {
}
}
function requestHeaders(): HeadersInit {
function requestHeaders(extraHeaders?: HeadersInit): Headers {
const headers = new Headers(extraHeaders);
const token = getAuthToken();
return {
'X-Locale': getCurrentLocale(),
...(token ? { Authorization: `Bearer ${token}` } : {})
};
headers.set('X-Locale', headers.get('X-Locale') ?? getCurrentLocale());
if (token && !headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
}
export function notificationWebSocketUrl(ticket: string): string {
@@ -1134,11 +1142,24 @@ async function getErrorMessage(response: Response): Promise<string> {
return `Request failed (${response.status})`;
}
async function getJson<T>(path: string, signal?: AbortSignal): Promise<T> {
function normalizeRequestOptions(options?: AbortSignal | ApiRequestOptions): ApiRequestOptions {
if (!options) {
return {};
}
if ('aborted' in options && 'addEventListener' in options) {
return { signal: options };
}
return options;
}
async function getJson<T>(path: string, options?: AbortSignal | ApiRequestOptions): Promise<T> {
const requestOptions = normalizeRequestOptions(options);
const response = await fetch(apiUrl(path), {
credentials: 'include',
headers: requestHeaders(),
signal
headers: requestHeaders(requestOptions.headers),
signal: requestOptions.signal
});
if (!response.ok) {
@@ -1149,13 +1170,13 @@ async function getJson<T>(path: string, signal?: AbortSignal): Promise<T> {
}
async function sendJson<T>(path: string, method: 'PATCH' | 'POST' | 'PUT', body: unknown): Promise<T> {
const headers = requestHeaders();
headers.set('Content-Type', 'application/json');
const response = await fetch(apiUrl(path), {
credentials: 'include',
method,
headers: {
'Content-Type': 'application/json',
...requestHeaders()
},
headers,
body: JSON.stringify(body)
});
@@ -1261,7 +1282,7 @@ export const api = {
sendJson<{ message: string }>('/api/auth/request-password-reset', 'POST', payload),
resetPassword: (payload: { token: string; password: string }) =>
sendJson<{ message: string }>('/api/auth/reset-password', 'POST', payload),
me: () => getJson<{ user: AuthUser }>('/api/auth/me'),
me: (options?: ApiRequestOptions) => getJson<{ user: AuthUser }>('/api/auth/me', options),
updateMe: (payload: UserProfilePayload) => sendJson<{ user: AuthUser }>('/api/auth/me', 'PATCH', payload),
changePassword: (payload: ChangePasswordPayload) =>
sendJson<{ message: string }>('/api/auth/me/password', 'PATCH', payload),