mirror of
https://github.com/CorentinTh/it-tools.git
synced 2026-10-10 05:40:23 +00:00
fix: use access plugin for favorites sync (#3)
This commit is contained in:
parent
b530304a15
commit
086ad26069
7 changed files with 140 additions and 9 deletions
38
functions/_lib/favorites.test.ts
Normal file
38
functions/_lib/favorites.test.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { getAuthenticatedUserId } from './favorites';
|
||||
|
||||
describe('getAuthenticatedUserId', () => {
|
||||
it('prefers the Access JWT email payload when available', () => {
|
||||
const request = new Request('https://example.com', {
|
||||
headers: {
|
||||
'cf-access-authenticated-user-email': 'fallback@example.com',
|
||||
},
|
||||
});
|
||||
|
||||
expect(getAuthenticatedUserId(request, {
|
||||
cloudflareAccess: {
|
||||
JWT: {
|
||||
payload: {
|
||||
email: 'Shake.Chen@gmail.com',
|
||||
aud: 'aud',
|
||||
exp: 0,
|
||||
iat: 0,
|
||||
iss: 'https://example.cloudflareaccess.com',
|
||||
sub: 'sub',
|
||||
},
|
||||
getIdentity: async () => undefined,
|
||||
},
|
||||
},
|
||||
} as any)).toBe('shake.chen@gmail.com');
|
||||
});
|
||||
|
||||
it('falls back to the request header when plugin data is unavailable', () => {
|
||||
const request = new Request('https://example.com', {
|
||||
headers: {
|
||||
'cf-access-authenticated-user-email': 'Fallback@example.com',
|
||||
},
|
||||
});
|
||||
|
||||
expect(getAuthenticatedUserId(request)).toBe('fallback@example.com');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import type { PluginData } from '@cloudflare/pages-plugin-cloudflare-access';
|
||||
import { normalizeFavoriteToolPaths } from '../../shared/favorites';
|
||||
|
||||
export function getAuthenticatedUserId(request: Request) {
|
||||
const email = request.headers.get('cf-access-authenticated-user-email')?.trim().toLowerCase();
|
||||
export function getAuthenticatedUserId(request: Request, data?: PluginData) {
|
||||
const email = data?.cloudflareAccess.JWT.payload.email?.trim().toLowerCase()
|
||||
?? request.headers.get('cf-access-authenticated-user-email')?.trim().toLowerCase();
|
||||
|
||||
return email || null;
|
||||
}
|
||||
|
|
|
|||
9
functions/api/_middleware.ts
Normal file
9
functions/api/_middleware.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import cloudflareAccessPlugin from '@cloudflare/pages-plugin-cloudflare-access';
|
||||
|
||||
const ACCESS_DOMAIN = 'https://it-tools-9zo.cloudflareaccess.com';
|
||||
const ACCESS_AUD = '13d5ba4d30a50a328018a03252fc3c874254f3fe49bcd90cce9afc80cedac485';
|
||||
|
||||
export const onRequest = cloudflareAccessPlugin({
|
||||
domain: ACCESS_DOMAIN,
|
||||
aud: ACCESS_AUD,
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { getAuthenticatedUserId, normalizeStoredFavoriteToolPaths } from '../_lib/favorites';
|
||||
import type { D1Database } from '../_lib/d1';
|
||||
import type { PluginData } from '@cloudflare/pages-plugin-cloudflare-access';
|
||||
|
||||
type Env = {
|
||||
DB: D1Database;
|
||||
|
|
@ -42,8 +43,8 @@ async function saveFavoriteToolPaths(env: Env, userId: string, favoriteToolPaths
|
|||
return normalizedFavoriteToolPaths;
|
||||
}
|
||||
|
||||
export async function onRequestGet({ request, env }: { request: Request; env: Env }) {
|
||||
const userId = getAuthenticatedUserId(request);
|
||||
export async function onRequestGet({ request, env, data }: { request: Request; env: Env; data: PluginData }) {
|
||||
const userId = getAuthenticatedUserId(request, data);
|
||||
|
||||
if (!userId) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
|
@ -54,8 +55,8 @@ export async function onRequestGet({ request, env }: { request: Request; env: En
|
|||
return json({ favoriteToolPaths });
|
||||
}
|
||||
|
||||
export async function onRequestPut({ request, env }: { request: Request; env: Env }) {
|
||||
const userId = getAuthenticatedUserId(request);
|
||||
export async function onRequestPut({ request, env, data }: { request: Request; env: Env; data: PluginData }) {
|
||||
const userId = getAuthenticatedUserId(request, data);
|
||||
|
||||
if (!userId) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { getAuthenticatedUserId } from '../_lib/favorites';
|
||||
import type { PluginData } from '@cloudflare/pages-plugin-cloudflare-access';
|
||||
|
||||
export async function onRequestGet({ request }: { request: Request }) {
|
||||
const userId = getAuthenticatedUserId(request);
|
||||
export async function onRequestGet({ request, data }: { request: Request; data: PluginData }) {
|
||||
const userId = getAuthenticatedUserId(request, data);
|
||||
|
||||
if (!userId) {
|
||||
return Response.json({ authenticated: false }, { status: 401, headers: { 'cache-control': 'no-store' } });
|
||||
|
|
|
|||
75
src/tools/tools.store.test.ts
Normal file
75
src/tools/tools.store.test.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
const fetchMock = vi.fn();
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (_key: string, fallback?: string) => fallback ?? _key,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('./index', () => ({
|
||||
toolsWithCategory: [
|
||||
{
|
||||
path: '/token-generator',
|
||||
name: 'Token 生成器',
|
||||
description: 'Generate tokens',
|
||||
category: 'General',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
describe('useToolStore favorites persistence', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
window.localStorage.clear();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it('persists a favorite to the remote store even if the user clicks before hydration finishes', async () => {
|
||||
let resolveFavoritesGet!: (response: Response) => void;
|
||||
|
||||
fetchMock.mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
|
||||
if (init?.method === 'PUT') {
|
||||
return Promise.resolve(new Response('', { status: 204 }));
|
||||
}
|
||||
|
||||
if (url.endsWith('/api/favorites')) {
|
||||
return new Promise<Response>((resolve) => {
|
||||
resolveFavoritesGet = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unexpected fetch call: ${url}`));
|
||||
});
|
||||
|
||||
const { useToolStore } = await import('./tools.store');
|
||||
const store = useToolStore();
|
||||
const favoriteTool = { path: '/token-generator' } as any;
|
||||
|
||||
const addFavoritePromise = store.addToolToFavorites({ tool: favoriteTool });
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(window.localStorage.getItem('favoriteToolsName')).toBeNull();
|
||||
|
||||
resolveFavoritesGet(new Response(JSON.stringify({ favoriteToolPaths: [] }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}));
|
||||
|
||||
await addFavoritePromise;
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(fetchMock).toHaveBeenLastCalledWith('/api/favorites', expect.objectContaining({
|
||||
method: 'PUT',
|
||||
}));
|
||||
expect(window.localStorage.getItem('favoriteToolsName')).toBe(JSON.stringify(['/token-generator']));
|
||||
});
|
||||
});
|
||||
|
|
@ -60,8 +60,13 @@ async function saveFavoriteToolPaths(paths: string[]) {
|
|||
}
|
||||
|
||||
function queueFavoritePersistence() {
|
||||
const currentFavoriteToolPaths = normalizeFavoriteToolPaths(favoriteToolPaths.value);
|
||||
|
||||
favoritePersistenceQueue = favoritePersistenceQueue.then(async () => {
|
||||
const currentFavoriteToolPaths = normalizeFavoriteToolPaths(favoriteToolPaths.value);
|
||||
if (favoriteSyncMode.value === 'loading') {
|
||||
await hydrateFavoriteToolPaths();
|
||||
favoriteToolPaths.value = currentFavoriteToolPaths;
|
||||
}
|
||||
|
||||
if (favoriteSyncMode.value !== 'remote') {
|
||||
writeCachedFavoriteToolPaths(currentFavoriteToolPaths);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue