From 82b0e45668f2ad915e34165d9f5500292ce33477 Mon Sep 17 00:00:00 2001 From: John Richmond <5629+jr@users.noreply.github.com> Date: Sat, 17 May 2025 16:47:51 -0700 Subject: [PATCH] Extension api and db (#16) Co-authored-by: cte --- src/actions/organizationSettings.ts | 36 +++++ src/app/api/organization-settings/route.ts | 39 +++++ .../0001_add_organization_settings.sql | 8 ++ src/db/migrations/meta/0001_snapshot.json | 135 ++++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + src/db/schema.ts | 23 +++ src/schemas/index.ts | 42 ++++++ 7 files changed, 290 insertions(+) create mode 100644 src/actions/organizationSettings.ts create mode 100644 src/app/api/organization-settings/route.ts create mode 100644 src/db/migrations/0001_add_organization_settings.sql create mode 100644 src/db/migrations/meta/0001_snapshot.json diff --git a/src/actions/organizationSettings.ts b/src/actions/organizationSettings.ts new file mode 100644 index 0000000000..831d8afbf2 --- /dev/null +++ b/src/actions/organizationSettings.ts @@ -0,0 +1,36 @@ +'use server'; + +import { auth } from '@clerk/nextjs/server'; +import { db } from '@/db'; +import { organizationSettings } from '@/db/schema'; +import { eq } from 'drizzle-orm'; +import { type OrganizationSettings } from '@/schemas'; + +/** + * Get organization settings for the current organization + */ +export async function getOrganizationSettings(): Promise< + OrganizationSettings | undefined +> { + const { userId, orgId } = await auth(); + + if (!userId) { + throw new Error('Unauthorized'); + } + + if (!orgId) { + throw new Error('Organization not found'); + } + + const settings = await db + .select() + .from(organizationSettings) + .where(eq(organizationSettings.organizationId, orgId)) + .limit(1); + + if (settings.length === 0) { + return; + } + + return settings[0]; +} diff --git a/src/app/api/organization-settings/route.ts b/src/app/api/organization-settings/route.ts new file mode 100644 index 0000000000..6bfaa43f63 --- /dev/null +++ b/src/app/api/organization-settings/route.ts @@ -0,0 +1,39 @@ +import { auth } from '@clerk/nextjs/server'; +import { NextResponse } from 'next/server'; +import { getOrganizationSettings } from '@/actions/organizationSettings'; +import { ORGANIZATION_ALLOW_ALL, type OrganizationSettings } from '@/schemas'; + +export async function GET() { + try { + const { userId, orgId } = await auth(); + + if (!userId) { + return NextResponse.json( + { error: 'Unauthorized request' }, + { status: 401 }, + ); + } + + if (!orgId) { + return NextResponse.json( + { error: 'Organization not found' }, + { status: 404 }, + ); + } + + const settings: OrganizationSettings = + (await getOrganizationSettings()) || { + defaultSettings: {}, + allowList: ORGANIZATION_ALLOW_ALL, + version: 0, + }; + + return NextResponse.json(settings); + } catch (error) { + console.error('Error fetching organization settings:', error); + return NextResponse.json( + { error: 'Failed to fetch organization settings' }, + { status: 500 }, + ); + } +} diff --git a/src/db/migrations/0001_add_organization_settings.sql b/src/db/migrations/0001_add_organization_settings.sql new file mode 100644 index 0000000000..7db27ae7d1 --- /dev/null +++ b/src/db/migrations/0001_add_organization_settings.sql @@ -0,0 +1,8 @@ +CREATE TABLE "organization_settings" ( + "organization_id" text PRIMARY KEY NOT NULL, + "version" integer NOT NULL DEFAULT 1, + "default_settings" jsonb NOT NULL DEFAULT '{}', + "allow_list" jsonb NOT NULL DEFAULT '{"allowAll": true, "providers": {}}'::jsonb, + "created_at" timestamp NOT NULL DEFAULT now(), + "updated_at" timestamp NOT NULL DEFAULT now() +); \ No newline at end of file diff --git a/src/db/migrations/meta/0001_snapshot.json b/src/db/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000000..1be76dd48c --- /dev/null +++ b/src/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,135 @@ +{ + "id": "a7b8c9d0-e1f2-3456-7890-a1b2c3d4e5f6", + "prevId": "6600a193-62ff-4475-9533-eb1dafc7339e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_value": { + "name": "new_value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_settings": { + "name": "organization_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": "1" + }, + "default_settings": { + "name": "default_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "allow_list": { + "name": "allow_list", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "{\"allowAll\": true, \"providers\": {}}" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 3107751770..a1ce77f058 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1747347845063, "tag": "0000_daffy_psylocke", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1747347900000, + "tag": "0001_add_organization_settings", + "breakpoints": true } ] } diff --git a/src/db/schema.ts b/src/db/schema.ts index c426c8fc0e..747caf3ca8 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,3 +1,8 @@ +import { + ORGANIZATION_ALLOW_ALL, + OrganizationDefaultSettings, + type OrganizationAllowList, +} from '@/schemas'; import { pgTable, text, @@ -29,3 +34,21 @@ export enum AuditLogTargetType { export type AuditLog = typeof auditLogs.$inferSelect & { targetType: AuditLogTargetType; }; + +export const organizationSettings = pgTable('organization_settings', { + // Organization ID (from Clerk) + organizationId: text('organization_id').notNull().primaryKey(), + + // Version number, incremented on updates + version: integer('version').notNull().default(1), + defaultSettings: jsonb('default_settings') + .notNull() + .$type() + .default({}), + allowList: jsonb('allow_list') + .notNull() + .$type() + .default(ORGANIZATION_ALLOW_ALL), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), +}); diff --git a/src/schemas/index.ts b/src/schemas/index.ts index b8de0f47b5..d7de12938c 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -2,6 +2,48 @@ * TimePeriod */ +import { z } from 'zod'; + export const timePeriods = [7, 30, 90] as const; export type TimePeriod = (typeof timePeriods)[number]; + +export const organizationAllowListSchema = z.object({ + allowAll: z.boolean(), + providers: z.record( + z.object({ + allowAll: z.boolean(), + models: z.array(z.string()).optional(), + }), + ), +}); + +// From Roo-Code-Internal + +export type OrganizationAllowList = z.infer; + +export const ORGANIZATION_ALLOW_ALL: OrganizationAllowList = { + allowAll: true, + providers: {}, +} as const; + +export const organizationDefaultSettingsSchema = z.object({ + enableCheckpoints: z.boolean().optional(), + maxOpenTabsContext: z.number().optional(), + maxWorkspaceFiles: z.number().optional(), + showRooIgnoredFiles: z.boolean().optional(), + maxReadFileLine: z.number().optional(), + fuzzyMatchThreshold: z.number().optional(), +}); + +export type OrganizationDefaultSettings = z.infer< + typeof organizationDefaultSettingsSchema +>; + +export const organizationSettingsSchema = z.object({ + version: z.number(), + defaultSettings: organizationDefaultSettingsSchema, + allowList: organizationAllowListSchema, +}); + +export type OrganizationSettings = z.infer;