From cc7d9fc19e0259a805f4da4ddf952dd4f887758e Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 6 Mar 2026 01:23:17 +0000 Subject: [PATCH] fix(app): add dev server auth token to all API endpoints Generate random VITE_DEV_API_TOKEN in dev.sh, validate Bearer token in shared server/dev-auth.ts middleware. Applied to all Vite plugins (fs, dev-chats, rss, web-search, tmdb, music-search) and claude-proxy. Client-side uses apiFetch() wrapper to attach the token automatically. Co-Authored-By: Claude Opus 4.6 --- packages/app/dev-dist/sw.js | 133 ++++++++++++++++-- packages/app/scripts/dev.sh | 5 + packages/app/server/claude-proxy.ts | 3 + packages/app/server/dev-auth.ts | 18 +++ packages/app/src/adapters/claude-adapter.ts | 3 +- .../app/src/adapters/openrouter-adapter.ts | 3 +- packages/app/src/composables/useAI.ts | 9 +- .../app/src/composables/useCodeContext.ts | 9 +- .../app/src/composables/useImageFallback.ts | 6 +- packages/app/src/composables/usePlayer.ts | 3 +- packages/app/src/composables/useRssFetch.ts | 3 +- .../app/src/composables/useSimilarContent.ts | 3 +- packages/app/src/composables/useWebSearch.ts | 4 +- packages/app/src/pages/BrowsePage.vue | 7 +- packages/app/src/stores/chat.ts | 5 +- packages/app/src/utils/api-fetch.ts | 14 ++ packages/app/vite-dev-chats.ts | 2 + packages/app/vite-fs.ts | 5 + packages/app/vite-music-search.ts | 3 + packages/app/vite-rss.ts | 2 + packages/app/vite-tmdb.ts | 2 + packages/app/vite-web-search.ts | 2 + 22 files changed, 214 insertions(+), 30 deletions(-) create mode 100644 packages/app/server/dev-auth.ts create mode 100644 packages/app/src/utils/api-fetch.ts diff --git a/packages/app/dev-dist/sw.js b/packages/app/dev-dist/sw.js index fe5dab96..1558dfd4 100644 --- a/packages/app/dev-dist/sw.js +++ b/packages/app/dev-dist/sw.js @@ -1,9 +1,124 @@ -// Self-destructing service worker: unregisters itself and clears caches -self.addEventListener('install', () => self.skipWaiting()) -self.addEventListener('activate', async () => { - const keys = await caches.keys() - await Promise.all(keys.map(k => caches.delete(k))) - const clients = await self.clients.matchAll({ type: 'window' }) - for (const client of clients) client.navigate(client.url) - await self.registration.unregister() -}) +/** + * Copyright 2018 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// If the loader is already loaded, just stop. +if (!self.define) { + let registry = {}; + + // Used for `eval` and `importScripts` where we can't get script URL by other means. + // In both cases, it's safe to use a global var because those functions are synchronous. + let nextDefineUri; + + const singleRequire = (uri, parentUri) => { + uri = new URL(uri + ".js", parentUri).href; + return registry[uri] || ( + + new Promise(resolve => { + if ("document" in self) { + const script = document.createElement("script"); + script.src = uri; + script.onload = resolve; + document.head.appendChild(script); + } else { + nextDefineUri = uri; + importScripts(uri); + resolve(); + } + }) + + .then(() => { + let promise = registry[uri]; + if (!promise) { + throw new Error(`Module ${uri} didn’t register its module`); + } + return promise; + }) + ); + }; + + self.define = (depsNames, factory) => { + const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href; + if (registry[uri]) { + // Module is already loading or loaded. + return; + } + let exports = {}; + const require = depUri => singleRequire(depUri, uri); + const specialDeps = { + module: { uri }, + exports, + require + }; + registry[uri] = Promise.all(depsNames.map( + depName => specialDeps[depName] || require(depName) + )).then(deps => { + factory(...deps); + return exports; + }); + }; +} +define(['./workbox-f97094b3'], (function (workbox) { 'use strict'; + + self.skipWaiting(); + workbox.clientsClaim(); + + /** + * The precacheAndRoute() method efficiently caches and responds to + * requests for URLs in the manifest. + * See https://goo.gl/S9QRab + */ + workbox.precacheAndRoute([{ + "url": "registerSW.js", + "revision": "3ca0b8505b4bec776b69afdba2768812" + }, { + "url": "index.html", + "revision": "0.rat89nkoims" + }], {}); + workbox.cleanupOutdatedCaches(); + workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { + allowlist: [/^\/$/] + })); + workbox.registerRoute(/^https:\/\/api\.anthropic\.com\/.*/i, new workbox.NetworkOnly(), 'GET'); + workbox.registerRoute(/^https:\/\/openrouter\.ai\/.*/i, new workbox.NetworkOnly(), 'GET'); + workbox.registerRoute(/\/api\/web-search\?.*/i, new workbox.NetworkOnly(), 'GET'); + workbox.registerRoute(/\/api\/rss-articles\?.*/i, new workbox.NetworkOnly(), 'GET'); + workbox.registerRoute(/\/api\/tmdb\/.*/i, new workbox.StaleWhileRevalidate({ + "cacheName": "tmdb-cache", + plugins: [new workbox.ExpirationPlugin({ + maxEntries: 200, + maxAgeSeconds: 86400 + })] + }), 'GET'); + workbox.registerRoute(/^https:\/\/image\.tmdb\.org\/.*/i, new workbox.CacheFirst({ + "cacheName": "tmdb-images", + plugins: [new workbox.ExpirationPlugin({ + maxEntries: 500, + maxAgeSeconds: 604800 + })] + }), 'GET'); + workbox.registerRoute(/^https:\/\/upload\.wikimedia\.org\/.*/i, new workbox.CacheFirst({ + "cacheName": "wiki-images", + plugins: [new workbox.ExpirationPlugin({ + maxEntries: 200, + maxAgeSeconds: 604800 + })] + }), 'GET'); + workbox.registerRoute(/^https:\/\/d12wklypp119aj\.cloudfront\.net\/image\/.*/i, new workbox.CacheFirst({ + "cacheName": "wavlake-images", + plugins: [new workbox.ExpirationPlugin({ + maxEntries: 300, + maxAgeSeconds: 604800 + })] + }), 'GET'); + +})); diff --git a/packages/app/scripts/dev.sh b/packages/app/scripts/dev.sh index 9f786baa..284116db 100755 --- a/packages/app/scripts/dev.sh +++ b/packages/app/scripts/dev.sh @@ -9,6 +9,11 @@ APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$APP_DIR" +# Generate a dev API token if not already set +if [ -z "$VITE_DEV_API_TOKEN" ]; then + export VITE_DEV_API_TOKEN=$(openssl rand -hex 16) +fi + cleanup() { kill 0 2>/dev/null || true wait 2>/dev/null || true diff --git a/packages/app/server/claude-proxy.ts b/packages/app/server/claude-proxy.ts index 1c783829..550b6779 100644 --- a/packages/app/server/claude-proxy.ts +++ b/packages/app/server/claude-proxy.ts @@ -3,6 +3,7 @@ import { createServer } from 'http' import { readFileSync, existsSync } from 'fs' import { resolve, dirname } from 'path' import { fileURLToPath } from 'url' +import { validateDevAuth } from './dev-auth.js' const __dirname = dirname(fileURLToPath(import.meta.url)) @@ -294,6 +295,8 @@ const server = createServer((req, res) => { return } + if (!validateDevAuth(req, res)) return + let body = '' req.on('data', (chunk) => { body += chunk }) req.on('end', () => { diff --git a/packages/app/server/dev-auth.ts b/packages/app/server/dev-auth.ts new file mode 100644 index 00000000..b6eb3710 --- /dev/null +++ b/packages/app/server/dev-auth.ts @@ -0,0 +1,18 @@ +/** + * Shared dev server authentication middleware. + * Validates Bearer token on all /api/* requests. + * Token is auto-generated in scripts/dev.sh and injected via VITE_DEV_API_TOKEN. + */ +import type { IncomingMessage, ServerResponse } from 'http' + +const DEV_TOKEN = process.env.VITE_DEV_API_TOKEN ?? '' + +/** Validate Authorization header. Returns true if authorized, false if rejected (response already sent). */ +export function validateDevAuth(req: IncomingMessage, res: ServerResponse): boolean { + if (!DEV_TOKEN) return true // No token configured, skip auth + const auth = req.headers.authorization + if (auth === `Bearer ${DEV_TOKEN}`) return true + res.writeHead(401, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'Unauthorized' })) + return false +} diff --git a/packages/app/src/adapters/claude-adapter.ts b/packages/app/src/adapters/claude-adapter.ts index f25f60b6..a19d9725 100644 --- a/packages/app/src/adapters/claude-adapter.ts +++ b/packages/app/src/adapters/claude-adapter.ts @@ -1,5 +1,6 @@ import type { AIAdapter, ChatMessage, ChatOptions } from './types' import { getApiKey } from '@/utils/key-vault' +import { apiFetch } from '@/utils/api-fetch' const BASE = import.meta.env.BASE_URL || '/' const CLAUDE_PATH = `${BASE}api/claude/v1/messages` @@ -31,7 +32,7 @@ export const claudeAdapter: AIAdapter = { .filter(m => m.role !== 'system') .map(m => ({ role: m.role, content: m.content })) - const res = await fetch(CLAUDE_PATH, { + const res = await apiFetch(CLAUDE_PATH, { method: 'POST', headers, body: JSON.stringify({ diff --git a/packages/app/src/adapters/openrouter-adapter.ts b/packages/app/src/adapters/openrouter-adapter.ts index 23471888..78e6bf46 100644 --- a/packages/app/src/adapters/openrouter-adapter.ts +++ b/packages/app/src/adapters/openrouter-adapter.ts @@ -1,5 +1,6 @@ import type { AIAdapter, ChatOptions } from './types' import { getApiKey } from '@/utils/key-vault' +import { apiFetch } from '@/utils/api-fetch' const OPENROUTER_PATH = '/api/openrouter' @@ -41,7 +42,7 @@ export const openrouterAdapter: AIAdapter = { headers['Authorization'] = `Bearer ${vaultKey}` } - const res = await fetch(OPENROUTER_PATH, { + const res = await apiFetch(OPENROUTER_PATH, { method: 'POST', headers, body: JSON.stringify({ diff --git a/packages/app/src/composables/useAI.ts b/packages/app/src/composables/useAI.ts index 87351203..2212faf9 100644 --- a/packages/app/src/composables/useAI.ts +++ b/packages/app/src/composables/useAI.ts @@ -7,6 +7,7 @@ import { usePersonaStore } from '@/stores/personas' import { useMemoryStore } from '@/stores/memory' import { useArchy } from '@/composables/useArchy' import { useCodeContext } from '@/composables/useCodeContext' +import { apiFetch } from '@/utils/api-fetch' type Provider = 'claude' | 'openrouter' | 'mock' @@ -47,7 +48,7 @@ async function refreshWavlakeCatalog() { if (Date.now() - wavlakeFetchedAt < WAVLAKE_REFRESH_INTERVAL && wavlakeCatalog.value.length > 0) return try { const BASE = import.meta.env.BASE_URL || '/' - const res = await fetch(`${BASE}api/music/rankings?days=30&limit=40`) + const res = await apiFetch(`${BASE}api/music/rankings?days=30&limit=40`) if (!res.ok) return const data = await res.json() if (Array.isArray(data)) { @@ -236,7 +237,7 @@ async function streamClaude( if (params?.topP !== undefined) body.top_p = params.topP if (params?.stopSequences && params.stopSequences.length > 0) body.stop_sequences = params.stopSequences - const res = await fetch(CLAUDE_PATH, { + const res = await apiFetch(CLAUDE_PATH, { method: 'POST', headers, body: JSON.stringify(body), @@ -285,7 +286,7 @@ async function streamOpenRouter( headers['Authorization'] = `Bearer ${vaultKey}` } - const res = await fetch(OPENROUTER_PATH, { + const res = await apiFetch(OPENROUTER_PATH, { method: 'POST', headers, body: JSON.stringify({ @@ -460,7 +461,7 @@ async function generateAutoTitle(conversationId: string) { const vaultKey = await getApiKey('claude') if (vaultKey) headers['x-api-key'] = vaultKey - const res = await fetch(CLAUDE_PATH, { + const res = await apiFetch(CLAUDE_PATH, { method: 'POST', headers, body: JSON.stringify({ diff --git a/packages/app/src/composables/useCodeContext.ts b/packages/app/src/composables/useCodeContext.ts index ca9c461b..489856ef 100644 --- a/packages/app/src/composables/useCodeContext.ts +++ b/packages/app/src/composables/useCodeContext.ts @@ -1,4 +1,5 @@ import { ref, computed, shallowRef, readonly } from 'vue' +import { apiFetch } from '@/utils/api-fetch' export interface ProjectInfo { name: string @@ -65,7 +66,7 @@ export function useCodeContext() { // In dev/demo mode, scan the Projects folder // This would be replaced by Archy integration later try { - const response = await fetch(`/api/fs/list?path=${encodeURIComponent(PROJECTS_ROOT)}`) + const response = await apiFetch(`/api/fs/list?path=${encodeURIComponent(PROJECTS_ROOT)}`) if (response.ok) { const data = await response.json() projectList.value = data.projects ?? [] @@ -153,7 +154,7 @@ export function useCodeContext() { async function loadFileTree(projectPath: string): Promise { try { - const response = await fetch(`/api/fs/tree?path=${encodeURIComponent(projectPath)}`) + const response = await apiFetch(`/api/fs/tree?path=${encodeURIComponent(projectPath)}`) if (response.ok) { const data = await response.json() fileTree.value = data.files ?? [] @@ -188,7 +189,7 @@ export function useCodeContext() { const fullPath = activeProject.value ? `${activeProject.value.path}/${filePath}` : filePath - const response = await fetch(`/api/fs/read?path=${encodeURIComponent(fullPath)}`) + const response = await apiFetch(`/api/fs/read?path=${encodeURIComponent(fullPath)}`) if (response.status === 413) { fileError.value = 'File too large to preview (max 1MB)' activeFileContent.value = '' @@ -233,7 +234,7 @@ export function useCodeContext() { const projectPath = `${PROJECTS_ROOT}/${safeName}` try { - const res = await fetch('/api/fs/mkdir', { + const res = await apiFetch('/api/fs/mkdir', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: projectPath }), diff --git a/packages/app/src/composables/useImageFallback.ts b/packages/app/src/composables/useImageFallback.ts index 5ec6e03e..b66814ac 100644 --- a/packages/app/src/composables/useImageFallback.ts +++ b/packages/app/src/composables/useImageFallback.ts @@ -1,3 +1,5 @@ +import { apiFetch } from '@/utils/api-fetch' + type TmdbResult = { posterUrl: string | null; backdropUrl: string | null } const memoryCache = new Map() const SESSION_KEY = 'aiui-poster-cache' @@ -187,7 +189,7 @@ async function fetchTmdbGeneric( try { const params = new URLSearchParams({ q: title.trim() }) if (year && year > 0) params.set('y', String(year)) - const res = await fetch(`/api/tmdb/${endpoint}?${params}`) + const res = await apiFetch(`/api/tmdb/${endpoint}?${params}`) if (!res.ok) return empty const data = (await res.json()) as { posterUrl?: string | null; backdropUrl?: string | null } const result: TmdbResult = { @@ -391,7 +393,7 @@ export async function fetchMusicCover( try { const base = import.meta.env.BASE_URL || '/' const params = new URLSearchParams({ q: title, title, artist }) - const wlRes = await fetch(`${base}api/music/search?${params}`) + const wlRes = await apiFetch(`${base}api/music/search?${params}`) if (wlRes.ok) { const wlData = (await wlRes.json()) as { coverUrl?: string } if (wlData.coverUrl) { diff --git a/packages/app/src/composables/usePlayer.ts b/packages/app/src/composables/usePlayer.ts index 9ded1b0e..1e09040b 100644 --- a/packages/app/src/composables/usePlayer.ts +++ b/packages/app/src/composables/usePlayer.ts @@ -2,6 +2,7 @@ import { ref, shallowRef, computed, watch } from 'vue' import type { Song } from '@aiui/core/types/content' import Plyr from 'plyr' import 'plyr/dist/plyr.css' +import { apiFetch } from '@/utils/api-fetch' interface MusicSearchResult { source: 'wavlake' @@ -75,7 +76,7 @@ export function usePlayer() { if (title) params.set('title', title) if (artist) params.set('artist', artist) const base = import.meta.env.BASE_URL || '/' - const res = await fetch(`${base}api/music/search?${params}`, { + const res = await apiFetch(`${base}api/music/search?${params}`, { signal: controller.signal, }) if (!res.ok) { diff --git a/packages/app/src/composables/useRssFetch.ts b/packages/app/src/composables/useRssFetch.ts index 45737694..7debb0e4 100644 --- a/packages/app/src/composables/useRssFetch.ts +++ b/packages/app/src/composables/useRssFetch.ts @@ -1,4 +1,5 @@ import type { WebSearchResult } from '@aiui/core/types/message' +import { apiFetch } from '@/utils/api-fetch' export async function fetchRssFromUrls(urls: string[]): Promise { const safe = urls.filter((u) => typeof u === 'string' && /^https?:\/\//i.test(u.trim())).slice(0, 8) @@ -7,7 +8,7 @@ export async function fetchRssFromUrls(urls: string[]): Promise params.append('url', u)) - const res = await fetch(`/api/rss-articles?${params}`, { signal: AbortSignal.timeout(15000) }) + const res = await apiFetch(`/api/rss-articles?${params}`, { signal: AbortSignal.timeout(15000) }) if (!res.ok) return [] const data = (await res.json()) as { articles?: Array<{ title?: string; url?: string; content?: string; imgSrc?: string }> } const articles = data.articles ?? [] diff --git a/packages/app/src/composables/useSimilarContent.ts b/packages/app/src/composables/useSimilarContent.ts index 6e4cb53d..5c348e67 100644 --- a/packages/app/src/composables/useSimilarContent.ts +++ b/packages/app/src/composables/useSimilarContent.ts @@ -1,6 +1,7 @@ import { ref } from 'vue' import type { FavoriteType } from '@/stores/favorites' import { getApiKey } from '@/utils/key-vault' +import { apiFetch } from '@/utils/api-fetch' const CACHE_KEY = 'aiui-similar-content' const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000 // 7 days @@ -57,7 +58,7 @@ export function useSimilarContent() { const prompt = `List exactly 3 ${typeLabel}s similar to "${title}". For each, respond with ONLY a JSON array like: [{"title":"Name","reason":"one sentence why"}]. No other text.` const base = import.meta.env.BASE_URL || '/' - const res = await fetch(`${base}api/claude/v1/messages`, { + const res = await apiFetch(`${base}api/claude/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, body: JSON.stringify({ diff --git a/packages/app/src/composables/useWebSearch.ts b/packages/app/src/composables/useWebSearch.ts index 5f829861..4e36dd9b 100644 --- a/packages/app/src/composables/useWebSearch.ts +++ b/packages/app/src/composables/useWebSearch.ts @@ -1,3 +1,5 @@ +import { apiFetch } from '@/utils/api-fetch' + export interface WebSearchResult { title: string url: string @@ -9,7 +11,7 @@ export async function searchWeb(query: string): Promise { if (!query.trim()) return [] try { const params = new URLSearchParams({ q: query.trim() }) - const res = await fetch(`/api/web-search?${params}`, { signal: AbortSignal.timeout(10000) }) + const res = await apiFetch(`/api/web-search?${params}`, { signal: AbortSignal.timeout(10000) }) if (!res.ok) { const body = await res.text().catch(() => '') console.warn('[AIUI web-search]', res.status, body) diff --git a/packages/app/src/pages/BrowsePage.vue b/packages/app/src/pages/BrowsePage.vue index ae39414d..0e318da1 100644 --- a/packages/app/src/pages/BrowsePage.vue +++ b/packages/app/src/pages/BrowsePage.vue @@ -92,6 +92,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue' import FileTree from '@/components/browse/FileTree.vue' import FilePreview from '@/components/browse/FilePreview.vue' +import { apiFetch } from '@/utils/api-fetch' interface FileEntry { name: string @@ -127,7 +128,7 @@ async function loadProjects() { loading.value = true error.value = '' try { - const res = await fetch('/api/fs/list') + const res = await apiFetch('/api/fs/list') if (!res.ok) throw new Error(`Failed to load: ${res.status}`) const data = await res.json() projects.value = data.projects ?? [] @@ -144,7 +145,7 @@ async function openProject(project: Project) { loading.value = true error.value = '' try { - const res = await fetch(`/api/fs/tree?path=${encodeURIComponent(project.path)}`) + const res = await apiFetch(`/api/fs/tree?path=${encodeURIComponent(project.path)}`) if (!res.ok) throw new Error(`Failed to load: ${res.status}`) const data = await res.json() treeItems.value = data.files ?? [] @@ -165,7 +166,7 @@ async function openFile(entry: FileEntry) { if (!currentProject.value) return const absolutePath = currentProject.value.path + '/' + entry.path try { - const res = await fetch(`/api/fs/read?path=${encodeURIComponent(absolutePath)}`) + const res = await apiFetch(`/api/fs/read?path=${encodeURIComponent(absolutePath)}`) if (!res.ok) { if (res.status === 413) { error.value = 'File too large to preview (max 1MB)' diff --git a/packages/app/src/stores/chat.ts b/packages/app/src/stores/chat.ts index f5a40441..06352c69 100644 --- a/packages/app/src/stores/chat.ts +++ b/packages/app/src/stores/chat.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia' import { ref, computed, watch } from 'vue' import type { Message, Conversation, WebSearchResult } from '@aiui/core/types/message' +import { apiFetch } from '@/utils/api-fetch' function generateId(): string { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { @@ -32,7 +33,7 @@ async function loadServerChats(): Promise<{ conversations: Map(), activeId: null } if (!isDev) return empty try { - const res = await fetch('/api/dev-chats') + const res = await apiFetch('/api/dev-chats') if (!res.ok) return empty const data = await res.json() as { conversations?: Record @@ -54,7 +55,7 @@ function saveServerChats(conversations: Map, activeId: str if (devSaveTimer) clearTimeout(devSaveTimer) devSaveTimer = setTimeout(() => { const obj = Object.fromEntries(conversations) - fetch('/api/dev-chats', { + apiFetch('/api/dev-chats', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversations: obj, activeConversationId: activeId }), diff --git a/packages/app/src/utils/api-fetch.ts b/packages/app/src/utils/api-fetch.ts new file mode 100644 index 00000000..ff4bd802 --- /dev/null +++ b/packages/app/src/utils/api-fetch.ts @@ -0,0 +1,14 @@ +/** + * Authenticated fetch wrapper for dev API endpoints. + * Adds Authorization: Bearer header when VITE_DEV_API_TOKEN is set. + */ +const DEV_TOKEN = import.meta.env.VITE_DEV_API_TOKEN as string | undefined + +export function apiFetch(url: string, init?: RequestInit): Promise { + if (DEV_TOKEN) { + const headers = new Headers(init?.headers) + headers.set('Authorization', `Bearer ${DEV_TOKEN}`) + return fetch(url, { ...init, headers }) + } + return fetch(url, init) +} diff --git a/packages/app/vite-dev-chats.ts b/packages/app/vite-dev-chats.ts index 001a4a68..7cc0b247 100644 --- a/packages/app/vite-dev-chats.ts +++ b/packages/app/vite-dev-chats.ts @@ -1,6 +1,7 @@ import type { Plugin, Connect } from 'vite' import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs' import { resolve, dirname } from 'path' +import { validateDevAuth } from './server/dev-auth' const CHATS_DIR = '.dev' const CHATS_FILE = 'chats.json' @@ -37,6 +38,7 @@ export function devChatsPlugin(): Plugin { }, configureServer(server) { server.middlewares.use('/api/dev-chats', (req: Connect.IncomingMessage, res: any, next: () => void) => { + if (!validateDevAuth(req, res)) return if (req.method === 'GET') { const data = readChats(chatsPath) res.setHeader('Content-Type', 'application/json') diff --git a/packages/app/vite-fs.ts b/packages/app/vite-fs.ts index d5c10ca5..787bd1b3 100644 --- a/packages/app/vite-fs.ts +++ b/packages/app/vite-fs.ts @@ -2,6 +2,7 @@ import type { Plugin } from 'vite' import type { Connect } from 'vite' import { readdirSync, statSync, readFileSync, existsSync, mkdirSync } from 'fs' import { join, resolve, relative } from 'path' +import { validateDevAuth } from './server/dev-auth' const PROJECTS_ROOT = '/Users/dorian/Projects' @@ -187,18 +188,22 @@ export function fsPlugin(): Plugin { configureServer(server) { server.middlewares.use('/api/fs/list', (req, res, next) => { if (req.method !== 'GET') return next() + if (!validateDevAuth(req, res)) return handleList(req, res) }) server.middlewares.use('/api/fs/tree', (req, res, next) => { if (req.method !== 'GET') return next() + if (!validateDevAuth(req, res)) return handleTree(req, res) }) server.middlewares.use('/api/fs/read', (req, res, next) => { if (req.method !== 'GET') return next() + if (!validateDevAuth(req, res)) return handleRead(req, res) }) server.middlewares.use('/api/fs/mkdir', (req, res, next) => { if (req.method !== 'POST') return next() + if (!validateDevAuth(req, res)) return handleMkdir(req, res) }) }, diff --git a/packages/app/vite-music-search.ts b/packages/app/vite-music-search.ts index ab891efa..364467d0 100644 --- a/packages/app/vite-music-search.ts +++ b/packages/app/vite-music-search.ts @@ -1,5 +1,6 @@ import type { Plugin } from 'vite' import type { Connect } from 'vite' +import { validateDevAuth } from './server/dev-auth' export interface MusicSearchResult { source: 'wavlake' @@ -220,6 +221,7 @@ async function getWavlakeRankings( function createSearchMiddleware() { return async (req: Connect.IncomingMessage, res: any, next: () => void) => { if (req.method !== 'GET') return next() + if (!validateDevAuth(req, res)) return const url = new URL(req.url ?? '', `http://${req.headers?.host ?? 'localhost'}`) const q = url.searchParams.get('q')?.trim() const title = url.searchParams.get('title')?.trim() @@ -267,6 +269,7 @@ function createRankingsMiddleware() { return async (req: Connect.IncomingMessage, res: any, next: () => void) => { if (req.method !== 'GET') return next() + if (!validateDevAuth(req, res)) return const url = new URL(req.url ?? '', `http://${req.headers?.host ?? 'localhost'}`) const days = parseInt(url.searchParams.get('days') ?? '7', 10) const genre = url.searchParams.get('genre')?.trim() || undefined diff --git a/packages/app/vite-rss.ts b/packages/app/vite-rss.ts index 9fc1c9c1..491e2823 100644 --- a/packages/app/vite-rss.ts +++ b/packages/app/vite-rss.ts @@ -1,6 +1,7 @@ import type { Plugin } from 'vite' import type { Connect } from 'vite' import Parser from 'rss-parser' +import { validateDevAuth } from './server/dev-auth' export interface RssArticle { title: string @@ -117,6 +118,7 @@ async function fetchRssFromUrls(urls: string[]): Promise { function createRssMiddleware() { return async (req: Connect.IncomingMessage, res: any, next: () => void) => { if (req.method !== 'GET') return next() + if (!validateDevAuth(req, res)) return const requestUrl = new URL(req.url ?? '', `http://${req.headers?.host ?? 'localhost'}`) if (!requestUrl.pathname.startsWith('/api/rss-articles')) return next() diff --git a/packages/app/vite-tmdb.ts b/packages/app/vite-tmdb.ts index 4e781f8e..b95145e5 100644 --- a/packages/app/vite-tmdb.ts +++ b/packages/app/vite-tmdb.ts @@ -1,6 +1,7 @@ import type { Plugin } from 'vite' import type { Connect } from 'vite' import { loadEnv } from 'vite' +import { validateDevAuth } from './server/dev-auth' const TMDB_POSTER = 'https://image.tmdb.org/t/p/w342' const TMDB_BACKDROP = 'https://image.tmdb.org/t/p/w780' @@ -8,6 +9,7 @@ const TMDB_BACKDROP = 'https://image.tmdb.org/t/p/w780' function createTmdbSearchMiddleware(tmdbKey: string | undefined, type: 'movie' | 'tv') { return async (req: Connect.IncomingMessage, res: any, next: () => void) => { if (req.method !== 'GET') return next() + if (!validateDevAuth(req, res)) return const url = new URL(req.url ?? '', `http://${req.headers?.host ?? 'localhost'}`) const q = url.searchParams.get('q')?.trim() const y = url.searchParams.get('y') diff --git a/packages/app/vite-web-search.ts b/packages/app/vite-web-search.ts index ad6f059f..e248c4ad 100644 --- a/packages/app/vite-web-search.ts +++ b/packages/app/vite-web-search.ts @@ -2,6 +2,7 @@ import type { Plugin } from 'vite' import type { Connect } from 'vite' import { loadEnv } from 'vite' import { search as searchDuckDuckGo } from 'duck-duck-scrape' +import { validateDevAuth } from './server/dev-auth' export interface WebSearchResult { title: string @@ -83,6 +84,7 @@ async function fetchFromBrave( function createWebSearchMiddleware(searxUrl: string | undefined, braveApiKey: string | undefined) { return async (req: Connect.IncomingMessage, res: any, next: () => void) => { if (req.method !== 'GET') return next() + if (!validateDevAuth(req, res)) return const url = new URL(req.url ?? '', `http://${req.headers?.host ?? 'localhost'}`) const q = url.searchParams.get('q')?.trim() if (!q) {