diff --git a/README.md b/README.md index b95f10a37..1eb2f6bd5 100644 --- a/README.md +++ b/README.md @@ -81,39 +81,68 @@ Here is what you need to be able to run Comp AI. ## Development -To get the project working locally with all integrations, follow these extended development steps. +To get the project working locally with all integrations, follow these extended development steps ### Setup -1. Clone the repo: +## Add environment variables and fill them out with your credentials - ```sh - git clone https://github.com/trycompai/comp.git - ``` +```sh +cp apps/app/.env.example apps/app/.env +cp apps/portal/.env.example apps/portal/.env +cp packages/db/.env.example packages/db/.env +``` -2. Navigate to the project directory: +## Get code running locally - ```sh - cd comp - ``` +1. Clone the repo + +```sh +git clone https://github.com/trycompai/comp.git +``` -3. Install dependencies using Bun: +2. Navigate to the project directory ```sh - bun install +cd comp ``` -4. Install `concurrently` as a dev dependency: +3. Install dependencies using Bun ```sh - bun add -d concurrently +bun install +``` + +4. Get Database Running + +```sh +cd packages/db +bun run docker:up # Spin up docker container +bun run db:migrate # Run migrations +``` + +5. Generate Prisma Types for each app + +```sh +cd apps/app +bun run db:generate +cd ../portal +bun run db:generate +cd ../api +bun run db:generate +``` + +6. Run all apps in parallel from the root directory + +```sh +bun run dev ``` --- ### Environment Setup -Create the following `.env` files and fill them out with your credentials: +Create the following `.env` files and fill them out with your credentials - `comp/apps/app/.env` - `comp/apps/portal/.env` diff --git a/apps/app/.env.example b/apps/app/.env.example index 883e586e4..cf8f3fdf3 100644 --- a/apps/app/.env.example +++ b/apps/app/.env.example @@ -1,23 +1,54 @@ -# Required -AUTH_SECRET="" # openssl rand -base64 32 -DATABASE_URL="" # Format: "postgresql://postgres:pass@127.0.0.1:5432/comp" -RESEND_DOMAIN=" # Domain configured in Resend, e.g. mail.trycomp.ai -RESEND_API_KEY="" # API key from Resend for email authentication / invites -REVALIDATION_SECRET="" # openssl rand -base64 32 -NEXT_PUBLIC_PORTAL_URL="http://localhost:3002" # The employee portal uses port 3002 by default - -# Recommended -# Store attachments in any S3 compatible bucket, we use AWS -APP_AWS_ACCESS_KEY_ID="" # AWS Access Key ID -APP_AWS_SECRET_ACCESS_KEY="" # AWS Secret Access Key -APP_AWS_REGION="" # AWS Region -APP_AWS_BUCKET_NAME="" # AWS Bucket Name - -TRIGGER_SECRET_KEY="" # For background jobs. Self-host or use cloud-version @ https://trigger.dev -# TRIGGER_API_URL="" # Only set if you are self-hosting -TRIGGER_API_KEY="" # API key from Trigger.dev -TRIGGER_SECRET_KEY="" # Secret key from Trigger.dev - -OPENAI_API_KEY="" # AI Chat + Auto Generated Policies, Risks + Vendors -FIRECRAWL_API_KEY="" # For research, self-host or use cloud-version @ https://firecrawl.dev +# Authentication & Database +AUTH_GOOGLE_ID="" # Google login +AUTH_GOOGLE_SECRET="" # Google Login +AUTH_GITHUB_ID="" # Optional +AUTH_GITHUB_SECRET="" # Optional +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/comp" # Should be the default +AUTH_SECRET="" # Used for auth, use something random and strong +SECRET_KEY="" # Used for encrypting data, use something random and strong +NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000 # Must point to the domain hosting the app + +# Upstash +UPSTASH_REDIS_REST_URL="" # Optional, used for rate limiting +UPSTASH_REDIS_REST_TOKEN="" # Optional, used for rate limiting + +# OpenAI +OPENAI_API_KEY="" # Required for app to work + +# Resend +RESEND_API_KEY="" # For sending emails, app notifications, etc. + +# Trigger +TRIGGER_SECRET_KEY="" # Required, for all async jobs and tasks + +# Posthog +NEXT_PUBLIC_POSTHOG_HOST=/ingest # GTM Trackers +NEXT_PUBLIC_POSTHOG_KEY="" # GTM Trackers + +# Vercel +VERCEL_ACCESS_TOKEN="" # For trust portal domains +VERCEL_TEAM_ID="" # For trust portal domains +VERCEL_PROJECT_ID="" # For trust portal domains +NEXT_PUBLIC_VERCEL_URL="" # eg. trycomp.ai + +# AWS +APP_AWS_BUCKET_NAME="" # Required, for task attachments +APP_AWS_REGION="" # Required, for task attachments +APP_AWS_ACCESS_KEY_ID="" # Required, for task attachments +APP_AWS_SECRET_ACCESS_KEY="" # Required, for task attachments + +# TRIGGER REVAL +REVALIDATION_SECRET="" # Revalidate server side, generate something random + +GROQ_API_KEY="" # For the AI chat, on dashboard +NEXT_PUBLIC_PORTAL_URL="http://localhost:3001" +ANTHROPIC_API_KEY="" # Optional, For more options with models +BETTER_AUTH_URL=http://localhost:3000 # For auth + +NEXT_OUTPUT_STANDALONE=false # For deploying on AWS instead of Vercel + +FLEET_URL="" # If you want to enable MDM, your hosted url +FLEET_TOKEN="" # If you want to enable MDM + +FIRECRAWL_API_KEY="" # To research vendors, Required \ No newline at end of file diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx index a3d7a3431..be88095ca 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/components/Overview.tsx @@ -1,6 +1,6 @@ 'use client'; -import { FrameworkEditorFramework } from '@db'; +import { FrameworkEditorFramework, Policy, Task } from '@db'; import { FrameworkInstanceWithControls } from '../types'; import { ComplianceOverview } from './ComplianceOverview'; import { DraggableCards } from './DraggableCards'; @@ -11,31 +11,15 @@ import { FrameworkInstanceWithComplianceScore } from './types'; export interface PublishedPoliciesScore { totalPolicies: number; publishedPolicies: number; - draftPolicies: { - id: string; - status: 'draft' | 'published' | 'needs_review'; - name: string; - }[]; - policiesInReview: { - id: string; - status: 'draft' | 'published' | 'needs_review'; - name: string; - }[]; - unpublishedPolicies: { - id: string; - status: 'draft' | 'published' | 'needs_review'; - name: string; - }[]; + draftPolicies: Policy[]; + policiesInReview: Policy[]; + unpublishedPolicies: Policy[]; } export interface DoneTasksScore { totalTasks: number; doneTasks: number; - incompleteTasks: { - id: string; - title: string; - status: 'todo' | 'in_progress' | 'done' | 'not_relevant'; - }[]; + incompleteTasks: Task[]; } export interface OverviewProps { diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/ToDoOverview.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/ToDoOverview.tsx index a83c87365..70af41486 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/components/ToDoOverview.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/components/ToDoOverview.tsx @@ -5,6 +5,7 @@ import { Button } from '@comp/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card'; import { ScrollArea } from '@comp/ui/scroll-area'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@comp/ui/tabs'; +import { Policy, Task } from '@db'; import { ArrowRight, CheckCircle2, @@ -20,18 +21,6 @@ import { useMemo, useState } from 'react'; import { toast } from 'sonner'; import { ConfirmActionDialog } from './ConfirmActionDialog'; -interface Policy { - id: string; - name: string; - status: 'draft' | 'published' | 'needs_review'; -} - -interface Task { - id: string; - title: string; - status: 'todo' | 'in_progress' | 'done' | 'not_relevant'; -} - export function ToDoOverview({ totalPolicies, totalTasks, diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPolicies.ts b/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPolicies.ts index 8288acda2..eede592be 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPolicies.ts +++ b/apps/app/src/app/(app)/[orgId]/frameworks/lib/getPolicies.ts @@ -5,11 +5,6 @@ export async function getPublishedPoliciesScore(organizationId: string) { where: { organizationId, }, - select: { - id: true, - name: true, - status: true, - }, }); const publishedPolicies = allPolicies.filter((p) => p.status === 'published'); diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/lib/getTasks.ts b/apps/app/src/app/(app)/[orgId]/frameworks/lib/getTasks.ts index e02fbd21d..db924a8ca 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/lib/getTasks.ts +++ b/apps/app/src/app/(app)/[orgId]/frameworks/lib/getTasks.ts @@ -6,11 +6,6 @@ export const getDoneTasks = cache(async (organizationId: string) => { where: { organizationId, }, - select: { - id: true, - title: true, - status: true, - }, }); const doneTasks = tasks.filter((t) => t.status === 'done'); diff --git a/apps/app/src/app/(app)/[orgId]/settings/secrets/components/EditSecretDialog.tsx b/apps/app/src/app/(app)/[orgId]/settings/secrets/components/EditSecretDialog.tsx new file mode 100644 index 000000000..0c4d746dd --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/settings/secrets/components/EditSecretDialog.tsx @@ -0,0 +1,229 @@ +'use client'; + +import { Button } from '@comp/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@comp/ui/dialog'; +import { Input } from '@comp/ui/input'; +import { Label } from '@comp/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; +import { Textarea } from '@comp/ui/textarea'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Loader2 } from 'lucide-react'; +import { useEffect } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { z } from 'zod'; + +interface EditSecretDialogProps { + secret: { + id: string; + name: string; + description: string | null; + category: string | null; + }; + open: boolean; + onOpenChange: (open: boolean) => void; + onSecretUpdated?: () => void; +} + +const editSecretSchema = z.object({ + name: z + .string() + .min(1, 'Name is required') + .max(100, 'Name is too long') + .regex(/^[A-Z0-9_]+$/, 'Name must be uppercase letters, numbers, and underscores only'), + value: z.string().optional(), + description: z.string().optional(), + category: z.string().optional(), +}); + +type EditSecretFormValues = z.infer; + +export function EditSecretDialog({ + secret, + open, + onOpenChange, + onSecretUpdated, +}: EditSecretDialogProps) { + const { + handleSubmit, + control, + register, + reset, + setError, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(editSecretSchema), + defaultValues: { + name: secret.name, + value: '', + description: secret.description || '', + category: secret.category || '', + }, + mode: 'onChange', + }); + + // Reset form when secret changes + useEffect(() => { + reset({ + name: secret.name, + value: '', + description: secret.description || '', + category: secret.category || '', + }); + }, [secret, reset]); + + const onSubmit = handleSubmit(async (values) => { + // Get organizationId from the URL path + const pathSegments = window.location.pathname.split('/'); + const orgId = pathSegments[1]; + + try { + // Only send fields that have values + const updateData: Record = { + organizationId: orgId, + }; + if (values.name !== secret.name) updateData.name = values.name; + if (values.value) updateData.value = values.value; + if (values.description !== secret.description) + updateData.description = values.description || null; + if (values.category !== secret.category) updateData.category = values.category || null; + + const response = await fetch(`/api/secrets/${secret.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updateData), + }); + + if (!response.ok) { + const error = await response.json(); + // Map Zod errors to form fields + if (Array.isArray(error.details)) { + let handled = false; + for (const issue of error.details) { + const field = issue?.path?.[0] as keyof EditSecretFormValues | undefined; + if (field) { + setError(field, { type: 'server', message: issue.message }); + handled = true; + } + } + if (handled) return; + } + throw new Error(error.error || 'Failed to update secret'); + } + + toast.success('Secret updated successfully'); + onOpenChange(false); + reset(); + + if (onSecretUpdated) onSecretUpdated(); + else window.location.reload(); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'Failed to update secret'); + console.error('Error updating secret:', err); + } + }); + + return ( + + +
+ + Edit Secret + + Update the secret details. Leave value empty to keep the existing value. + + +
+
+ + + {errors.name?.message ? ( +

{errors.name.message}

+ ) : null} +

+ Use uppercase with underscores for naming convention +

+
+
+ + + {errors.value?.message ? ( +

{errors.value.message}

+ ) : null} +

+ Only provide a value if you want to update it +

+
+
+ + ( + + )} + /> + {errors.category?.message ? ( +

{errors.category.message}

+ ) : null} +
+
+ +