diff --git a/src/actions/mcps.ts b/src/actions/mcps.ts index a35c7dcf..27b90e44 100644 --- a/src/actions/mcps.ts +++ b/src/actions/mcps.ts @@ -1,5 +1,6 @@ -import { ActionError, defineAction } from "astro:actions"; +import { defineAction } from "astro:actions"; import { z } from "astro/zod"; +import { requireUser } from "@/server/auth/requireUser"; import { addMcpServer, listMcpServers, @@ -7,19 +8,10 @@ import { toggleMcpServer, } from "@/server/mcps/mcps.service"; -function ensureAuthenticated(context: { locals: { user: unknown } }): void { - if (!context.locals.user) { - throw new ActionError({ - code: "UNAUTHORIZED", - message: "You must be logged in", - }); - } -} - export const mcps = { list: defineAction({ handler: async (_input, context) => { - ensureAuthenticated(context); + requireUser(context); return listMcpServers(); }, }), @@ -36,7 +28,7 @@ export const mcps = { headers: z.record(z.string(), z.string()).optional(), }), handler: async (input, context) => { - ensureAuthenticated(context); + requireUser(context); const { name, ...config } = input; await addMcpServer(name, config); return { success: true }; @@ -47,7 +39,7 @@ export const mcps = { accept: "json", input: z.object({ name: z.string().min(1) }), handler: async (input, context) => { - ensureAuthenticated(context); + requireUser(context); await removeMcpServer(input.name); return { success: true }; }, @@ -60,7 +52,7 @@ export const mcps = { enabled: z.boolean(), }), handler: async (input, context) => { - ensureAuthenticated(context); + requireUser(context); await toggleMcpServer(input.name, input.enabled); return { success: true }; }, diff --git a/src/actions/providers.ts b/src/actions/providers.ts index ad955f06..a6ddf610 100644 --- a/src/actions/providers.ts +++ b/src/actions/providers.ts @@ -1,5 +1,6 @@ import { ActionError, defineAction } from "astro:actions"; import { z } from "astro/zod"; +import { requireUser } from "@/server/auth/requireUser"; import { cachedAction } from "@/server/cache/actionCache"; import { invalidatePrefix } from "@/server/cache/memory"; import { logger } from "@/server/logger"; @@ -27,6 +28,26 @@ interface AvailableModel { supportsAttachments: boolean; } +const cachedProvidersList = cachedAction( + "providers.list", + { ttlMs: PROVIDERS_LIST_TTL_MS }, + async () => ({ providers: await getSettingsProviders() }), +); + +const cachedAvailableModelsForUser = cachedAction( + "providers.getAvailableModelsForUser", + { ttlMs: AVAILABLE_MODELS_TTL_MS }, + async (): Promise<{ models: AvailableModel[] }> => { + try { + const availableModels = await getAvailableModels(); + return { models: availableModels }; + } catch (error) { + logger.error({ error }, "Failed to get available models for user"); + return { models: [] }; + } + }, +); + async function syncOpencodeCredentials( client: OpencodeClient, providerId: string, @@ -65,11 +86,10 @@ async function reloadRuntimeAfterAuthChange( export const providers = { list: defineAction({ - handler: cachedAction( - "providers.list", - { ttlMs: PROVIDERS_LIST_TTL_MS }, - async () => ({ providers: await getSettingsProviders() }), - ), + handler: async (input, context) => { + requireUser(context); + return cachedProvidersList(input, context); + }, }), connect: defineAction({ @@ -77,7 +97,8 @@ export const providers = { providerId: z.string().min(1, "Provider ID is required"), apiKey: z.string().min(1, "API key is required"), }), - handler: async (input) => { + handler: async (input, context) => { + requireUser(context); const client = createOpencodeClient(); const authPayload = { type: "api" as const, key: input.apiKey }; @@ -106,7 +127,8 @@ export const providers = { input: z.object({ providerId: z.string().min(1, "Provider ID is required"), }), - handler: async (input) => { + handler: async (input, context) => { + requireUser(context); const client = createOpencodeClient(); const result = await client.auth.remove({ providerID: input.providerId }); @@ -131,7 +153,8 @@ export const providers = { providerId: z.string().min(1, "Provider ID is required"), methodIndex: z.number().int().min(0, "Method index is required"), }), - handler: async (input) => { + handler: async (input, context) => { + requireUser(context); const client = createOpencodeClient(); const result = await client.provider.oauth.authorize({ providerID: input.providerId, @@ -155,7 +178,8 @@ export const providers = { methodIndex: z.number().int().min(0, "Method index is required"), code: z.string().optional(), }), - handler: async (input) => { + handler: async (input, context) => { + requireUser(context); const client = createOpencodeClient(); const result = await client.provider.oauth.callback({ providerID: input.providerId, @@ -179,18 +203,9 @@ export const providers = { }), getAvailableModelsForUser: defineAction({ - handler: cachedAction( - "providers.getAvailableModelsForUser", - { ttlMs: AVAILABLE_MODELS_TTL_MS }, - async (): Promise<{ models: AvailableModel[] }> => { - try { - const availableModels = await getAvailableModels(); - return { models: availableModels }; - } catch (error) { - logger.error({ error }, "Failed to get available models for user"); - return { models: [] }; - } - }, - ), + handler: async (input, context) => { + requireUser(context); + return cachedAvailableModelsForUser(input, context); + }, }), }; diff --git a/src/actions/update.ts b/src/actions/update.ts index 0df7c40e..5fd3565e 100644 --- a/src/actions/update.ts +++ b/src/actions/update.ts @@ -1,5 +1,6 @@ import { defineAction } from "astro:actions"; import { z } from "astro/zod"; +import { requireUser } from "@/server/auth/requireUser"; import { logger } from "@/server/logger"; import { spawnCommand } from "@/server/utils/execAsync"; import { VERSION } from "@/server/version"; @@ -232,7 +233,8 @@ export const update = { checkForUpdate: defineAction({ accept: "json", input: z.object({}), - handler: async () => { + handler: async (_input, context) => { + requireUser(context); try { if (VERSION === "unknown" || VERSION === "dev") { logger.warn("No VERSION env var set in container"); @@ -308,7 +310,8 @@ export const update = { pull: defineAction({ accept: "json", input: z.object({}), - handler: async () => { + handler: async (_input, context) => { + requireUser(context); try { const containerName = await getContainerName(); logger.info({ containerName }, "Pulling latest image"); @@ -344,7 +347,8 @@ export const update = { restart: defineAction({ accept: "json", input: z.object({}), - handler: async () => { + handler: async (_input, context) => { + requireUser(context); try { const containerName = await getContainerName(); logger.info({ containerName }, "Restarting container"); diff --git a/src/pages/api/system/health.ts b/src/pages/api/system/health.ts index a10a3a17..48102c97 100644 --- a/src/pages/api/system/health.ts +++ b/src/pages/api/system/health.ts @@ -1,10 +1,14 @@ import type { APIRoute } from "astro"; import { count, desc, isNull } from "drizzle-orm"; +import { requireAuth } from "@/server/auth/requireAuth"; import { db } from "@/server/db/client"; import { projects, queueJobs, systemHealthSnapshots } from "@/server/db/schema"; import { isGlobalOpencodeHealthy } from "@/server/opencode/runtime"; -export const GET: APIRoute = async () => { +export const GET: APIRoute = async ({ cookies }) => { + const auth = await requireAuth(cookies); + if (!auth.ok) return auth.response; + // Queue stats const queueStats = await db .select({ diff --git a/src/server/auth/requireUser.ts b/src/server/auth/requireUser.ts new file mode 100644 index 00000000..065f42fd --- /dev/null +++ b/src/server/auth/requireUser.ts @@ -0,0 +1,20 @@ +import { ActionError } from "astro:actions"; +import type { User } from "@/server/db/schema"; + +interface UserContext { + locals: { + user: User | null; + }; +} + +export function requireUser(context: UserContext): User { + const { user } = context.locals; + if (!user) { + throw new ActionError({ + code: "UNAUTHORIZED", + message: "You must be logged in", + }); + } + + return user; +}