Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 6 additions & 14 deletions src/actions/mcps.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
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,
removeMcpServer,
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();
},
}),
Expand All @@ -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 };
Expand All @@ -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 };
},
Expand All @@ -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 };
},
Expand Down
59 changes: 37 additions & 22 deletions src/actions/providers.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -65,19 +86,19 @@ 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({
input: z.object({
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 };

Expand Down Expand Up @@ -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 });

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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);
},
}),
};
10 changes: 7 additions & 3 deletions src/actions/update.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down
6 changes: 5 additions & 1 deletion src/pages/api/system/health.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down
20 changes: 20 additions & 0 deletions src/server/auth/requireUser.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading