diff --git a/frontend/vite-mock-plugin.ts b/frontend/vite-mock-plugin.ts new file mode 100644 index 0000000..f9ad4fa --- /dev/null +++ b/frontend/vite-mock-plugin.ts @@ -0,0 +1,133 @@ +/** + * Dev-only Vite plugin that intercepts /api/v1/* and returns mock responses. + * Login: any username + password "password" succeeds. + */ +import type { Plugin } from 'vite'; +import type { IncomingMessage, ServerResponse } from 'http'; + +function readBody(req: IncomingMessage): Promise { + return new Promise((resolve) => { + let data = ''; + req.on('data', (chunk) => (data += chunk)); + req.on('end', () => { + try { + resolve(data ? JSON.parse(data) : {}); + } catch { + resolve({}); + } + }); + }); +} + +function json(res: ServerResponse, status: number, body: unknown) { + const payload = JSON.stringify(body); + res.writeHead(status, { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload), + }); + res.end(payload); +} + +function noContent(res: ServerResponse) { + res.writeHead(204); + res.end(); +} + +const MOCK_ACCESS_TOKEN = 'mock-access-token'; +const MOCK_REFRESH_TOKEN = 'mock-refresh-token'; + +const TOKEN_PAIR = { + access_token: MOCK_ACCESS_TOKEN, + refresh_token: MOCK_REFRESH_TOKEN, + expires_in: 900, +}; + +const ME = { + id: 1, + name: 'admin', + is_admin: true, + can_create: true, + is_blocked: false, +}; + +export function mockApiPlugin(): Plugin { + return { + name: 'mock-api', + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + const url = req.url ?? ''; + const method = req.method ?? 'GET'; + + if (!url.startsWith('/api/v1')) { + return next(); + } + + const path = url.replace('/api/v1', '').split('?')[0]; + + // POST /auth/login + if (method === 'POST' && path === '/auth/login') { + const body = (await readBody(req)) as Record; + if (body.password === 'password') { + return json(res, 200, TOKEN_PAIR); + } + return json(res, 401, { code: 'unauthorized', message: 'Invalid credentials' }); + } + + // POST /auth/refresh + if (method === 'POST' && path === '/auth/refresh') { + return json(res, 200, TOKEN_PAIR); + } + + // POST /auth/logout + if (method === 'POST' && path === '/auth/logout') { + return noContent(res); + } + + // GET /auth/sessions + if (method === 'GET' && path === '/auth/sessions') { + return json(res, 200, { + items: [ + { + id: 1, + user_agent: 'Mock Browser', + started_at: new Date().toISOString(), + expires_at: null, + last_activity: new Date().toISOString(), + is_current: true, + }, + ], + total: 1, + }); + } + + // GET /users/me + if (method === 'GET' && path === '/users/me') { + return json(res, 200, ME); + } + + // GET /files + if (method === 'GET' && path === '/files') { + return json(res, 200, { items: [], next_cursor: null, prev_cursor: null }); + } + + // GET /tags + if (method === 'GET' && path === '/tags') { + return json(res, 200, { items: [], total: 0, offset: 0, limit: 50 }); + } + + // GET /categories + if (method === 'GET' && path === '/categories') { + return json(res, 200, { items: [], total: 0, offset: 0, limit: 50 }); + } + + // GET /pools + if (method === 'GET' && path === '/pools') { + return json(res, 200, { items: [], total: 0, offset: 0, limit: 50 }); + } + + // Fallback: 404 + return json(res, 404, { code: 'not_found', message: `Mock: no handler for ${method} ${path}` }); + }); + }, + }; +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index bf699a8..fa89ef0 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,8 @@ import { sveltekit } from '@sveltejs/kit/vite'; import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vite'; +import { mockApiPlugin } from './vite-mock-plugin'; export default defineConfig({ - plugins: [tailwindcss(), sveltekit()] + plugins: [tailwindcss(), sveltekit(), mockApiPlugin()] });