diff --git a/apps/app/languine.lock b/apps/app/languine.lock index 9109e6f58f..386b7d7d77 100644 --- a/apps/app/languine.lock +++ b/apps/app/languine.lock @@ -199,8 +199,14 @@ files: auth.email.error: ed239af1d84d25fd9bb99251114b488c auth.terms: bb73614638d9a468c878278692a71a5e onboarding.title: deecac09e6560d6f0f98401d9852d514 - onboarding.setup: ad2376beebecdcf7846ba973fa1a005b - onboarding.description: 7cd2a7692cfacbbea2045f2e6416b805 + onboarding.submit: 85a18a0474d135c1f4c6da6c383a2d81 + onboarding.setup: 54efd9605180a9a74e6d1a53529f858e + onboarding.description: fcfd2a71d9095421e290f1b1229d1c6e + onboarding.trigger.title: c0a683b95c32208c79a08309225647ee + onboarding.trigger.creating: 5c1f94a8b72c6f33012732e6b8ba44e5 + onboarding.trigger.completed: 435e0545f0bbfbbbe640e9e12e54c663 + onboarding.trigger.continue: 01739cdc86e75ee2fea0aabcdeb2e557 + onboarding.trigger.error: 9e78ff392dee43a6c78a8c4e29ff4dc2 onboarding.fields.fullName.label: 614cffa523202658a898e34a5d94d05e onboarding.fields.fullName.placeholder: df1fc96e5401396fe01e24363a9ec40d onboarding.fields.name.label: c1ca926603dc454ba981aa514db8402b @@ -214,6 +220,7 @@ files: onboarding.check_availability: 118740af1c4521b917e29791d661db82 onboarding.available: 78945de8de090e90045d299651a68a9b onboarding.unavailable: 453e6aa38d87b28ccae545967c53004f + onboarding.creating: 3ab00f2a7303dc9e1aed05d535677b7e overview.title: 3b878279a04dc47d60932cb294d96259 overview.framework_chart.title: 9c53d5a3379ee2c9bc7c1c55f54724b9 overview.requirement_chart.title: 69d9304854b767112e32573d7eeddc24 diff --git a/apps/app/package.json b/apps/app/package.json index 51fcfa5c5a..ff597e779b 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -3,7 +3,8 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "NODE_OPTIONS='--inspect' next dev --turbopack --turbo -p 3001", + "dev": "npx concurrently --kill-others --names \"next,trigger\" --prefix-colors \"yellow,blue\" \"next dev --turbopack --turbo -p 3001\" \"npm run trigger:dev\"", + "trigger:dev": "npx trigger.dev@latest dev", "build": "next build", "start": "next start", "lint": "biome lint ./src", diff --git a/apps/app/src/actions/organization/check-subdomain-availability.ts b/apps/app/src/actions/organization/check-subdomain-availability.ts deleted file mode 100644 index 8672e51859..0000000000 --- a/apps/app/src/actions/organization/check-subdomain-availability.ts +++ /dev/null @@ -1,40 +0,0 @@ -// check-subdomain-availability.ts - -"use server"; - -import { db } from "@bubba/db"; -import { authActionClient } from "../safe-action"; -import { subdomainAvailabilitySchema } from "../schema"; -import type { ActionResponse } from "../types"; - -export const checkSubdomainAvailability = authActionClient - .schema(subdomainAvailabilitySchema) - .metadata({ - name: "check-subdomain-availability", - }) - .action(async ({ parsedInput }): Promise => { - const { subdomain } = parsedInput; - - try { - const subdomainExists = await db.organization.findFirst({ - where: { - subdomain: { - equals: subdomain, - mode: "insensitive", - }, - }, - select: { id: true }, - }); - - return { - success: true, - data: !subdomainExists, - }; - } catch (error) { - console.error("Prisma error:", error); - return { - success: false, - error: "Failed to check subdomain availability", - }; - } - }); diff --git a/apps/app/src/actions/organization/create-organization-action.ts b/apps/app/src/actions/organization/create-organization-action.ts index 2a37ceaf3d..21432c933f 100644 --- a/apps/app/src/actions/organization/create-organization-action.ts +++ b/apps/app/src/actions/organization/create-organization-action.ts @@ -3,133 +3,71 @@ "use server"; import { createOrganizationAndConnectUser } from "@/auth/org"; -import type { createDefaultPoliciesTask } from "@/jobs/tasks/organization/create-default-policies"; -import { - addDomainToVercel, - removeDomainFromVercelProject, -} from "@/lib/domains"; import { db } from "@bubba/db"; -import { tasks } from "@trigger.dev/sdk/v3"; -import { revalidateTag } from "next/cache"; import { authActionClient } from "../safe-action"; import { organizationSchema } from "../schema"; +import { tasks } from "@trigger.dev/sdk/v3"; +import type { createOrganizationTask } from "@/jobs/tasks/organization/create-organization"; export const createOrganizationAction = authActionClient - .schema(organizationSchema) - .metadata({ - name: "create-organization", - track: { - event: "create-organization", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - const { name, website, subdomain } = parsedInput; - const { id: userId, organizationId } = ctx.user; - - if (!name || !website) { - console.log("Invalid input detected:", { name, website }); - throw new Error("Invalid user input"); - } - - const hasVercelConfig = Boolean( - process.env.NEXT_PUBLIC_VERCEL_URL && - process.env.VERCEL_ACCESS_TOKEN && - process.env.VERCEL_TEAM_ID && - process.env.VERCEL_PROJECT_ID, - ); - - if (hasVercelConfig && subdomain) { - try { - await addDomainToVercel( - `${subdomain}.${process.env.NEXT_PUBLIC_VERCEL_URL}`, - ); - } catch (error) { - console.error("Failed to add domain to Vercel:", error); - throw new Error("Failed to set up subdomain"); - } - } - - if (!organizationId) { - await createOrganizationAndConnectUser({ - userId, - normalizedEmail: ctx.user.email!, - subdomain: hasVercelConfig ? subdomain || "" : "", - }); - } - - const organization = await db.organization.findFirst({ - where: { - users: { - some: { - id: userId, - }, - }, - }, - }); + .schema(organizationSchema) + .metadata({ + name: "create-organization", + track: { + event: "create-organization", + channel: "server", + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { name, website, frameworks } = parsedInput; + const { id: userId, organizationId } = ctx.user; - if (!organization) { - throw new Error("Organization not found"); - } + if (!name || !website) { + console.log("Invalid input detected:", { name, website }); + throw new Error("Invalid user input"); + } - try { - await db.$transaction(async () => { - await db.organization.upsert({ - where: { - id: organization.id, - }, - update: { - name, - website, - subdomain: hasVercelConfig ? subdomain || "" : "", - }, - create: { - name, - website, - subdomain: hasVercelConfig ? subdomain || "" : "", - }, - }); + if (!organizationId) { + await createOrganizationAndConnectUser({ + userId, + normalizedEmail: ctx.user.email!, + }); + } - await db.user.update({ - where: { - id: userId, - }, - data: { - onboarded: true, - }, - }); - }); + const organization = await db.organization.findFirst({ + where: { + users: { + some: { + id: userId, + }, + }, + }, + }); - // await tasks.trigger( - // "create-default-policies", - // { - // ownerId: userId, - // organizationId: organization.id, - // organizationName: name, - // } - // ); + if (!organization) { + throw new Error("Organization not found"); + } - revalidateTag(`user_${userId}`); - revalidateTag(`organization_${organizationId}`); + try { + const handle = await tasks.trigger( + "create-organization", + { + userId, + fullName: name, + website, + frameworkIds: frameworks, + organizationId: organization.id, + }, + ); - return { - success: true, - }; - } catch (error) { - if (hasVercelConfig && subdomain) { - try { - await removeDomainFromVercelProject( - `${subdomain}.${process.env.NEXT_PUBLIC_VERCEL_URL}`, - ); - } catch (cleanupError) { - console.error( - "Failed to clean up subdomain after error:", - cleanupError, - ); - } - } + return { + success: true, + runId: handle.id, + publicAccessToken: handle.publicAccessToken, + }; + } catch (error) { + console.error("Error during organization update:", error); - console.error("Error during organization update:", error); - throw new Error("Failed to update organization"); - } - }); + throw new Error("Failed to update organization"); + } + }); diff --git a/apps/app/src/actions/schema.ts b/apps/app/src/actions/schema.ts index 62b7f5fb2d..788d53cc7c 100644 --- a/apps/app/src/actions/schema.ts +++ b/apps/app/src/actions/schema.ts @@ -11,12 +11,16 @@ import { import { z } from "zod"; export const organizationSchema = z.object({ - fullName: z.string().min(1, "Full name is required"), name: z.string().min(1, "Name is required"), - website: z.string().url("Must be a valid URL"), - subdomain: z.string().min(1, "Subdomain is required").optional(), + fullName: z.string().min(1, "Full name is required"), + website: z.string().url("Must be a valid URL").optional().or(z.literal("")), + frameworks: z + .array(z.string()) + .min(1, "Please select at least one framework to get started with"), }); +export type OrganizationSchema = z.infer; + export const organizationNameSchema = z.object({ name: z .string() diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/layout.tsx index 32a1e55265..c5be7799bc 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/layout.tsx @@ -1,8 +1,10 @@ import { auth } from "@/auth"; import { Header } from "@/components/header"; import { Sidebar } from "@/components/sidebar"; +import { db } from "@bubba/db"; import dynamic from "next/dynamic"; import { redirect } from "next/navigation"; +import { cache } from "react"; const HotKeys = dynamic( () => import("@/components/hot-keys").then((mod) => mod.HotKeys), @@ -18,10 +20,16 @@ export default async function Layout({ }) { const session = await auth(); - if (!session?.user) { + if (!session?.user || !session.user.organizationId) { redirect("/auth"); } + const isSetup = await isOrganizationSetup(session.user.organizationId); + + if (!isSetup) { + redirect("/setup"); + } + return (
@@ -35,3 +43,16 @@ export default async function Layout({
); } + +const isOrganizationSetup = cache(async (organizationId: string) => { + const organization = await db.organization.findUnique({ + where: { + id: organizationId, + }, + select: { + setup: true, + }, + }); + + return organization?.setup; +}); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx index 63836d9aa2..a618b73fe0 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx @@ -7,6 +7,7 @@ import { db } from "@bubba/db"; import type { Metadata } from "next"; import { setStaticParamsLocale } from "next-international/server"; import { redirect } from "next/navigation"; +import { cache } from "react"; export default async function OrganizationSettings({ params, @@ -18,28 +19,17 @@ export default async function OrganizationSettings({ const session = await auth(); - const [organization] = await Promise.all([ - db.organization.findUnique({ - where: { - id: session?.user.organizationId, - }, - select: { - name: true, - website: true, - id: true, - }, - }), - ]); - - if (!organization) { + if (!session?.user.organizationId) { return redirect("/"); } + const organization = await organizationDetails(session.user.organizationId); + return (
- - - + + +
); } @@ -57,3 +47,16 @@ export async function generateMetadata({ title: t("sidebar.settings"), }; } + +const organizationDetails = cache(async (organizationId: string) => { + const organization = await db.organization.findUnique({ + where: { id: organizationId }, + select: { + name: true, + website: true, + id: true, + }, + }); + + return organization; +}); diff --git a/apps/app/src/app/[locale]/(app)/setup/page.tsx b/apps/app/src/app/[locale]/(app)/setup/page.tsx index f458766d8f..9397d81300 100644 --- a/apps/app/src/app/[locale]/(app)/setup/page.tsx +++ b/apps/app/src/app/[locale]/(app)/setup/page.tsx @@ -1,9 +1,9 @@ import { auth } from "@/auth"; import { Onboarding } from "@/components/forms/create-organization-form"; -import { Icons } from "@bubba/ui/icons"; +import { db } from "@bubba/db"; import type { Metadata } from "next"; -import Link from "next/link"; import { redirect } from "next/navigation"; +import { cache } from "react"; export const metadata: Metadata = { title: "Organization Setup | Comp AI", @@ -16,19 +16,35 @@ export default async function Page() { return redirect("/"); } - if (session.user.onboarded && session.user.organizationId) { + if (!session.user.organizationId) { return redirect("/"); } - return ( -
-
- - - -
- - -
- ); + const isSetup = await isOrganizationSetup(session.user.organizationId); + const frameworks = await getFrameworks(); + + if (isSetup) { + return redirect("/"); + } + + return ; } + +const getFrameworks = cache(async () => { + return await db.framework.findMany({ + orderBy: { + name: "asc", + }, + }); +}); + +const isOrganizationSetup = cache(async (organizationId: string) => { + const organization = await db.organization.findUnique({ + where: { id: organizationId }, + select: { + setup: true, + }, + }); + + return organization?.setup; +}); diff --git a/apps/app/src/app/api/revalidate/path/route.ts b/apps/app/src/app/api/revalidate/path/route.ts new file mode 100644 index 0000000000..2b15f9e30c --- /dev/null +++ b/apps/app/src/app/api/revalidate/path/route.ts @@ -0,0 +1,30 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { revalidatePath } from "next/cache"; +import { env } from "@/env.mjs"; + +export async function POST(request: NextRequest) { + try { + const { path, type, secret } = await request.json(); + + if (secret !== env.REVALIDATION_SECRET) { + return NextResponse.json({ message: "Invalid secret" }, { status: 401 }); + } + + if (!path) { + return NextResponse.json( + { message: "Path is required" }, + { status: 400 }, + ); + } + + revalidatePath(path, type); + + return NextResponse.json({ revalidated: true }); + } catch (err) { + console.error("Error revalidating path:", err); + return NextResponse.json( + { message: "Error revalidating path" }, + { status: 500 }, + ); + } +} diff --git a/apps/app/src/auth/org.ts b/apps/app/src/auth/org.ts index 0e08985d4e..8471dc2ffa 100644 --- a/apps/app/src/auth/org.ts +++ b/apps/app/src/auth/org.ts @@ -25,7 +25,6 @@ async function createStripeCustomer(input: { export async function createOrganizationAndConnectUser(input: { userId: string; normalizedEmail: string; - subdomain?: string; }): Promise { const initialName = "New Organization"; @@ -35,11 +34,10 @@ export async function createOrganizationAndConnectUser(input: { name: initialName, tier: "free", website: "", - subdomain: input.subdomain || "", members: { create: { userId: input.userId, - role: "admin", + role: "owner", }, }, }, diff --git a/apps/app/src/components/forms/create-organization-form.tsx b/apps/app/src/components/forms/create-organization-form.tsx index 49f286de67..2ccc26e4fd 100644 --- a/apps/app/src/components/forms/create-organization-form.tsx +++ b/apps/app/src/components/forms/create-organization-form.tsx @@ -1,15 +1,9 @@ "use client"; -import { checkSubdomainAvailability } from "@/actions/organization/check-subdomain-availability"; import { createOrganizationAction } from "@/actions/organization/create-organization-action"; -import { - organizationSchema, - subdomainAvailabilitySchema, -} from "@/actions/schema"; -import { env } from "@/env.mjs"; +import { organizationSchema } from "@/actions/schema"; import { useI18n } from "@/locales/client"; import { Button } from "@bubba/ui/button"; -import { Checkbox } from "@bubba/ui/checkbox"; import { Form, FormControl, @@ -19,155 +13,78 @@ import { FormMessage, } from "@bubba/ui/form"; import { Input } from "@bubba/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@bubba/ui/select"; import { zodResolver } from "@hookform/resolvers/zod"; -import { - ArrowLeftIcon, - ArrowRightIcon, - CheckIcon, - Loader2, - XIcon, -} from "lucide-react"; +import { ArrowRight, Loader2 } from "lucide-react"; import { useSession } from "next-auth/react"; import { useAction } from "next-safe-action/hooks"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; -import { useDebouncedCallback } from "use-debounce"; import type { z } from "zod"; +import type { Framework } from "@bubba/db/types"; +import { Icons } from "@bubba/ui/icons"; +import Link from "next/link"; +import { Checkbox } from "@bubba/ui/checkbox"; +import { cn } from "@bubba/ui/cn"; +import { useRealtimeRun } from "@trigger.dev/react-hooks"; -interface BaseFieldConfig { - name: string; - label: string; - placeholder: string; -} - -interface TextFieldConfig extends BaseFieldConfig { - type?: "text"; -} - -interface CheckboxFieldConfig extends BaseFieldConfig { - type: "checkbox"; - options?: string[]; -} - -interface SelectFieldConfig extends BaseFieldConfig { - type: "select"; - options: string[]; -} - -type FieldConfig = TextFieldConfig | CheckboxFieldConfig | SelectFieldConfig; - -export function Onboarding() { +function RealtimeStatus({ runId, publicAccessToken }: { runId: string; publicAccessToken: string }) { const t = useI18n(); - const { data: session } = useSession(); - const [isCheckingName, setIsCheckingName] = useState(false); - const [isNameAvailable, setIsNameAvailable] = useState(null); - - const hasVercelConfig = Boolean(env.NEXT_PUBLIC_VERCEL_URL); - - const steps: Array<{ - title: string; - description: string; - fields: FieldConfig[]; - }> = [ - { - title: t("onboarding.title"), - description: t("onboarding.description"), - fields: [ - { - name: "fullName", - label: t("onboarding.fields.fullName.label"), - placeholder: t("onboarding.fields.fullName.placeholder"), - }, - { - name: "name", - label: t("onboarding.fields.name.label"), - placeholder: t("onboarding.fields.name.placeholder"), - }, - { - name: "website", - label: t("onboarding.fields.website.label"), - placeholder: t("onboarding.fields.website.placeholder"), - }, - ...(hasVercelConfig - ? [ - { - name: "subdomain", - label: t("onboarding.fields.subdomain.label"), - placeholder: t("onboarding.fields.subdomain.placeholder"), - }, - ] - : []), - ], - }, - ]; - - const [currentStep, setCurrentStep] = useState(0); - - const checkAvailability = useAction(checkSubdomainAvailability, { - onSuccess: (result) => { - if (result.data?.data === false) { - setIsCheckingName(false); - setIsNameAvailable(false); - } else { - setIsCheckingName(false); - setIsNameAvailable(true); + const { run, error } = useRealtimeRun(runId, { + accessToken: publicAccessToken, + onComplete: (run, error) => { + if (error) { + toast.error(t("common.actions.error"), { duration: 5000 }); + return; } }, - onError: () => { - setIsCheckingName(false); - setIsNameAvailable(false); - }, }); - const debouncedCheck = useDebouncedCallback(async (subdomain: string) => { - setIsNameAvailable(null); - - const result = subdomainAvailabilitySchema.safeParse({ subdomain }); - - if (!result.success) { - setIsNameAvailable(false); - return; - } - - setIsCheckingName(true); - checkAvailability.execute({ subdomain }); - }, 500); - - const handleNext = async () => { - if (currentStep < steps.length - 1) { - const currentFields = steps[currentStep].fields.map( - (field) => field.name, - ) as Array>; - - const result = await form.trigger(currentFields); + return ( +
+ {run?.status !== "FAILED" && run?.status !== "COMPLETED" && ( +
+ +

+ {t("onboarding.trigger.title")} +

+

+ {t("onboarding.trigger.creating")} +

+
+ )} - if (result) { - setCurrentStep(currentStep + 1); - } - } - }; + {run?.status === "COMPLETED" && ( +
+

+ {t("onboarding.trigger.completed")} +

+
+ +
+
+ )} +
+ ); +} - const handlePrevious = () => { - if (currentStep > 0) { - setCurrentStep(currentStep - 1); - } - }; +function OnboardingClient({ frameworks }: { frameworks: Framework[] }) { + const t = useI18n(); + const { data: session } = useSession(); + const [runId, setRunId] = useState(null); + const [publicAccessToken, setPublicAccessToken] = useState(null); const createOrganization = useAction(createOrganizationAction, { - onSuccess: () => { - toast.success(t("onboarding.success")); + onSuccess: async (data) => { + setRunId(data.data?.runId ?? null); + setPublicAccessToken(data.data?.publicAccessToken ?? null); }, onError: () => { - toast.error(t("common.actions.error")); + toast.error(t("common.actions.error"), { duration: 5000 }); }, }); @@ -177,208 +94,175 @@ export function Onboarding() { fullName: session?.user?.name ?? "", name: "", website: "", - ...(hasVercelConfig ? { subdomain: "" } : {}), + frameworks: [], }, mode: "onChange", }); - const onSubmit = (data: z.infer) => { - createOrganization.execute(data); + const onSubmit = async (data: z.infer) => { + await createOrganization.execute(data); }; + if (runId && publicAccessToken) { + return ( +
+
+
+ + + +
+ +
+
+ ); + } + return (
+
+ + + +
+

{t("onboarding.setup")}

- {steps[currentStep].description} + {t("onboarding.description")}

- {steps[currentStep].fields.map((fieldConfig) => ( - - } - key={fieldConfig.name} - render={({ field }) => ( - - - {fieldConfig.label} - - - {fieldConfig.name === "subdomain" ? ( -
- { - field.onChange(e); - debouncedCheck(e.target.value); - }} - /> - {env.NEXT_PUBLIC_VERCEL_URL && ( -
- .{env.NEXT_PUBLIC_VERCEL_URL} -
- )} -
- ) : (fieldConfig as CheckboxFieldConfig).type === - "checkbox" ? ( -
- {(fieldConfig as CheckboxFieldConfig).options ? ( - (fieldConfig as CheckboxFieldConfig).options?.map( - (option) => ( -
- { - const currentValue = Array.isArray( - field.value, - ) - ? field.value - : []; - const newValue = checked - ? [...currentValue, option] - : currentValue.filter( - (v: string) => v !== option, - ); - field.onChange(newValue); - }} - /> - {option} -
- ), - ) - ) : ( -
- - - {fieldConfig.label} - -
+ ( + + + {t("onboarding.fields.fullName.label")} + + + + + + + )} + /> + + ( + + + {t("onboarding.fields.name.label")} + + + + + + + )} + /> + + ( + + + {t("onboarding.fields.website.label")} + + + + + + + )} + /> + + ( + + + {t("frameworks.overview.grid.title")} + + +
+ {frameworks.map((framework) => ( +
- ) : (fieldConfig as SelectFieldConfig).type === - "select" ? ( - - ) : ( - - )} - - - {fieldConfig.name === "subdomain" && - field.value && - isCheckingName && ( -
- - - {t("onboarding.check_availability")} - -
- )} - {fieldConfig.name === "subdomain" && isNameAvailable && ( -
- - - {t("onboarding.available")} - -
- )} - {fieldConfig.name === "subdomain" && - !form.formState.errors.subdomain && - !isNameAvailable && - !isCheckingName && - field.value && ( -
- - - {t("onboarding.unavailable")} - +
- )} - - )} - /> - ))} + ))} +
+
+ +
+ )} + /> -
- {currentStep > 0 && ( - + -
+ {t("onboarding.submit")} +
); } + +export function Onboarding({ frameworks }: { frameworks: Framework[] }) { + return ; +} diff --git a/apps/app/src/components/settings/team/team-members.tsx b/apps/app/src/components/settings/team/team-members.tsx index fd6ed4c4d8..f06f306ed9 100644 --- a/apps/app/src/components/settings/team/team-members.tsx +++ b/apps/app/src/components/settings/team/team-members.tsx @@ -1,14 +1,12 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@bubba/ui/tabs"; -import { Suspense } from "react"; import { InviteMemberForm } from "./invite-member-form"; import { MembersList } from "./members-list"; import { PendingInvitations } from "./pending-invitations"; import { db } from "@bubba/db"; import { auth } from "@/auth"; import { unstable_cache } from "next/cache"; -import type { Metadata } from "next"; -import { setStaticParamsLocale } from "next-international/server"; import { getI18n } from "@/locales/server"; +import { cache } from "react"; export async function TeamMembers() { const session = await auth(); @@ -53,39 +51,35 @@ export async function TeamMembers() { ); } - -const getOrganizationMembers = unstable_cache( - async (organizationId: string) => { - return db.organizationMember.findMany({ - where: { - organizationId, - accepted: true, - }, - include: { - user: true, - }, - orderBy: { - joinedAt: "desc", - }, - }); - }, - ["organization-members"], - { tags: ["organization-members"], revalidate: 60 } +const getOrganizationMembers = cache(async (organizationId: string) => { + return db.organizationMember.findMany({ + where: { + organizationId, + OR: [ + { accepted: true }, + { invitedEmail: null } + ] + }, + include: { + user: true, + }, + orderBy: { + joinedAt: "desc", + }, + }); +}, ); -const getPendingInvitations = unstable_cache( - async (organizationId: string) => { - return db.organizationMember.findMany({ - where: { - organizationId, - accepted: false, - invitedEmail: { not: null }, - }, - orderBy: { - joinedAt: "desc", - }, - }); - }, - ["pending-invitations"], - { tags: ["pending-invitations"], revalidate: 60 } +const getPendingInvitations = cache(async (organizationId: string) => { + return db.organizationMember.findMany({ + where: { + organizationId, + accepted: false, + invitedEmail: { not: null }, + }, + orderBy: { + joinedAt: "desc", + }, + }); +}, ); \ No newline at end of file diff --git a/apps/app/src/env.mjs b/apps/app/src/env.mjs index afd08c240b..7a8f2848a5 100644 --- a/apps/app/src/env.mjs +++ b/apps/app/src/env.mjs @@ -2,66 +2,68 @@ import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; export const env = createEnv({ - server: { - AUTH_GOOGLE_ID: z.string(), - AUTH_GOOGLE_SECRET: z.string(), - AUTH_SECRET: z.string(), - DATABASE_URL: z.string().min(1), - OPENAI_API_KEY: z.string(), - RESEND_API_KEY: z.string(), - UPSTASH_REDIS_REST_URL: z.string(), - UPSTASH_REDIS_REST_TOKEN: z.string(), - STRIPE_SECRET_KEY: z.string(), - STRIPE_WEBHOOK_SECRET: z.string(), - DISCORD_WEBHOOK_URL: z.string(), - TRIGGER_SECRET_KEY: z.string(), - TRIGGER_API_KEY: z.string().optional(), - TRIGGER_API_URL: z.string().optional(), - VERCEL_ACCESS_TOKEN: z.string().optional(), - VERCEL_TEAM_ID: z.string().optional(), - VERCEL_PROJECT_ID: z.string().optional(), - NODE_ENV: z.string().optional(), - AWS_ACCESS_KEY_ID: z.string(), - AWS_SECRET_ACCESS_KEY: z.string(), - AWS_REGION: z.string(), - AWS_BUCKET_NAME: z.string(), - }, + server: { + AUTH_GOOGLE_ID: z.string(), + AUTH_GOOGLE_SECRET: z.string(), + AUTH_SECRET: z.string(), + DATABASE_URL: z.string().min(1), + OPENAI_API_KEY: z.string(), + RESEND_API_KEY: z.string(), + UPSTASH_REDIS_REST_URL: z.string(), + UPSTASH_REDIS_REST_TOKEN: z.string(), + STRIPE_SECRET_KEY: z.string(), + STRIPE_WEBHOOK_SECRET: z.string(), + DISCORD_WEBHOOK_URL: z.string(), + TRIGGER_SECRET_KEY: z.string(), + TRIGGER_API_KEY: z.string().optional(), + TRIGGER_API_URL: z.string().optional(), + REVALIDATION_SECRET: z.string(), + VERCEL_ACCESS_TOKEN: z.string().optional(), + VERCEL_TEAM_ID: z.string().optional(), + VERCEL_PROJECT_ID: z.string().optional(), + NODE_ENV: z.string().optional(), + AWS_ACCESS_KEY_ID: z.string(), + AWS_SECRET_ACCESS_KEY: z.string(), + AWS_REGION: z.string(), + AWS_BUCKET_NAME: z.string(), + }, - client: { - NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), - NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(), - NEXT_PUBLIC_VERCEL_URL: z.string().optional(), - NEXT_PUBLIC_NOVU_IDENTIFIER: z.string().optional(), - }, + client: { + NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), + NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(), + NEXT_PUBLIC_VERCEL_URL: z.string().optional(), + NEXT_PUBLIC_NOVU_IDENTIFIER: z.string().optional(), + }, - runtimeEnv: { - AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID, - AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, - AUTH_SECRET: process.env.AUTH_SECRET, - DATABASE_URL: process.env.DATABASE_URL, - OPENAI_API_KEY: process.env.OPENAI_API_KEY, - RESEND_API_KEY: process.env.RESEND_API_KEY, - UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL, - UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN, - STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, - STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, - DISCORD_WEBHOOK_URL: process.env.DISCORD_WEBHOOK_URL, - TRIGGER_SECRET_KEY: process.env.TRIGGER_SECRET_KEY, - TRIGGER_API_KEY: process.env.TRIGGER_API_KEY, - TRIGGER_API_URL: process.env.TRIGGER_API_URL, - NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, - NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, - VERCEL_ACCESS_TOKEN: process.env.VERCEL_ACCESS_TOKEN, - VERCEL_TEAM_ID: process.env.VERCEL_TEAM_ID, - VERCEL_PROJECT_ID: process.env.VERCEL_PROJECT_ID, - NEXT_PUBLIC_VERCEL_URL: process.env.NEXT_PUBLIC_VERCEL_URL, - NEXT_PUBLIC_NOVU_IDENTIFIER: process.env.NEXT_PUBLIC_NOVU_IDENTIFIER, - NODE_ENV: process.env.NODE_ENV, - AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID, - AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, - AWS_REGION: process.env.AWS_REGION, - AWS_BUCKET_NAME: process.env.AWS_BUCKET_NAME, - }, + runtimeEnv: { + AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID, + AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, + AUTH_SECRET: process.env.AUTH_SECRET, + DATABASE_URL: process.env.DATABASE_URL, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, + RESEND_API_KEY: process.env.RESEND_API_KEY, + UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL, + UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN, + STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, + STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, + DISCORD_WEBHOOK_URL: process.env.DISCORD_WEBHOOK_URL, + TRIGGER_SECRET_KEY: process.env.TRIGGER_SECRET_KEY, + TRIGGER_API_KEY: process.env.TRIGGER_API_KEY, + TRIGGER_API_URL: process.env.TRIGGER_API_URL, + REVALIDATION_SECRET: process.env.REVALIDATION_SECRET, + NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, + NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, + VERCEL_ACCESS_TOKEN: process.env.VERCEL_ACCESS_TOKEN, + VERCEL_TEAM_ID: process.env.VERCEL_TEAM_ID, + VERCEL_PROJECT_ID: process.env.VERCEL_PROJECT_ID, + NEXT_PUBLIC_VERCEL_URL: process.env.NEXT_PUBLIC_VERCEL_URL, + NEXT_PUBLIC_NOVU_IDENTIFIER: process.env.NEXT_PUBLIC_NOVU_IDENTIFIER, + NODE_ENV: process.env.NODE_ENV, + AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, + AWS_REGION: process.env.AWS_REGION, + AWS_BUCKET_NAME: process.env.AWS_BUCKET_NAME, + }, - skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION, + skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION, }); diff --git a/apps/app/src/jobs/tasks/organization/create-default-policies.ts b/apps/app/src/jobs/tasks/organization/create-default-policies.ts deleted file mode 100644 index d136b3ed8e..0000000000 --- a/apps/app/src/jobs/tasks/organization/create-default-policies.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { db } from "@bubba/db"; -import { ArtifactType } from "@bubba/db/types"; -import type { JSONContent } from "@tiptap/react"; -import { logger, schemaTask } from "@trigger.dev/sdk/v3"; -import { parseStringPromise } from "xml2js"; -import { z } from "zod"; - -const S3_BUCKET_URL = "https://compai-policies.s3.eu-central-1.amazonaws.com"; - -async function listPolicyFiles(): Promise { - try { - const response = await fetch(`${S3_BUCKET_URL}`); - const text = await response.text(); - - const result = await parseStringPromise(text); - - const keys: string[] = []; - const contents = result.ListBucketResult?.Contents || []; - - for (const content of contents) { - const key = content.Key?.[0]; - if (key?.endsWith(".json")) { - keys.push(key); - } - } - - return keys; - } catch (error) { - logger.error("Error listing policy files from S3:", { error }); - throw error; - } -} - -// Helper to fetch a single policy file -async function fetchPolicyFile(fileName: string) { - const response = await fetch(`${S3_BUCKET_URL}/${fileName}`); - return response.json(); -} - -export const createDefaultPoliciesTask = schemaTask({ - id: "create-default-policies", - maxDuration: 1000 * 60 * 10, // 10 minutes - schema: z.object({ - organizationId: z.string(), - organizationName: z.string(), - ownerId: z.string(), - }), - run: async (payload) => { - const { organizationId, organizationName, ownerId } = payload; - - try { - const policyFiles = await listPolicyFiles(); - - for (const fileName of policyFiles) { - const policyData = (await fetchPolicyFile(fileName)) as { - type: string; - metadata: { controls: string[] }; - content: JSONContent[]; - }; - - const currentDate = new Date().toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - - // Replace placeholders in content while preserving node structure - const replaceInContent = (node: any): any => { - if (!node) return node; - - if (typeof node === "string") { - return node - .replace(/\{\{organization\}\}/g, organizationName) - .replace(/\{\{date\}\}/g, currentDate); - } - - if (Array.isArray(node)) { - return node.map((item) => replaceInContent(item)); - } - - if (typeof node === "object") { - const result: any = {}; - for (const [key, value] of Object.entries(node)) { - if (key === "type" || key === "attrs") { - result[key] = value; - } else { - result[key] = replaceInContent(value); - } - } - return result; - } - - return node; - }; - - const processedPolicy = { - type: policyData.type, - metadata: policyData.metadata, - content: replaceInContent(policyData.content), - }; - - const policyName = - processedPolicy.content?.find( - (node: any) => node.type === "heading" && node.attrs?.level === 1 - )?.content?.[0]?.text || fileName.replace(".json", ""); - - const artifact = await db.artifact.create({ - data: { - name: policyName, - content: processedPolicy, - organizationId, - type: ArtifactType.policy, - published: false, - needsReview: true, - version: 1, - ownerId, - }, - }); - - const controls = processedPolicy.metadata?.controls; - if (controls && Array.isArray(controls)) { - const dbControls = await db.control.findMany({ - where: { - code: { - in: controls, - }, - }, - }); - - for (const control of dbControls) { - const orgControl = await db.organizationControl.upsert({ - where: { - id: `${organizationId}-${control.id}`, - }, - create: { - id: `${organizationId}-${control.id}`, - organizationId, - controlId: control.id, - }, - update: {}, - }); - - await db.controlArtifact.create({ - data: { - organizationControlId: orgControl.id, - artifactId: artifact.id, - }, - }); - } - } - - logger.info(`Created policy: ${policyName}`); - } - - return { - success: true, - message: `Successfully copied and customized ${policyFiles.length} policies for organization ${organizationId}`, - }; - } catch (error) { - logger.error("Error creating default policies:", { error }); - throw error; - } - }, -}); - -export default createDefaultPoliciesTask; diff --git a/apps/app/src/jobs/tasks/organization/create-organization.ts b/apps/app/src/jobs/tasks/organization/create-organization.ts new file mode 100644 index 0000000000..f6fc4de4af --- /dev/null +++ b/apps/app/src/jobs/tasks/organization/create-organization.ts @@ -0,0 +1,387 @@ +import { db } from "@bubba/db"; +import { logger, schemaTask } from "@trigger.dev/sdk/v3"; +import { z } from "zod"; +import { setupOrgFrameworksTask } from "./setup-org-frameworks"; +import { RequirementType, type Policy, type User } from "@bubba/db/types"; +import type { InputJsonValue } from "@prisma/client/runtime/library"; + +export const createOrganizationTask = schemaTask({ + id: "create-organization", + maxDuration: 1000 * 60 * 3, // 3 minutes + schema: z.object({ + organizationId: z.string(), + userId: z.string(), + fullName: z.string(), + website: z.string(), + frameworkIds: z.array(z.string()), + }), + run: async ({ organizationId, userId, fullName, website, frameworkIds }) => { + logger.info("Creating organization", { + organizationId, + userId, + fullName, + website, + frameworkIds, + }); + + const organization = await db.organization.findFirst({ + where: { + id: organizationId, + users: { + some: { + id: userId, + }, + }, + }, + }); + + if (!organization) { + throw new Error("Organization not found"); + } + + try { + await db.$transaction(async () => { + await db.organization.upsert({ + where: { + id: organization.id, + }, + update: { + name: fullName, + website, + }, + create: { + name: fullName, + website, + }, + }); + + await db.user.update({ + where: { + id: userId, + }, + data: { + onboarded: true, + }, + }); + }); + + await createOrganizationCategories(organizationId, frameworkIds); + + const organizationFrameworks = await Promise.all( + frameworkIds.map((frameworkId) => + createOrganizationFramework(organizationId, frameworkId), + ), + ); + + await createOrganizationPolicy(organizationId, frameworkIds); + + await createOrganizationControlRequirements( + organizationId, + organizationFrameworks.map((framework) => framework.id), + ); + + await createOrganizationEvidence(organizationId, frameworkIds, userId); + + await db.organization.update({ + where: { + id: organizationId, + }, + data: { + setup: true, + }, + }); + + return { + success: true, + }; + } catch (error) { + logger.error("Error creating organization", { + error, + }); + + throw error; + } + }, +}); + +const createOrganizationFramework = async ( + organizationId: string, + frameworkId: string, +) => { + // First verify the organization exists + const organization = await db.organization.findUnique({ + where: { id: organizationId }, + }); + + if (!organization) { + logger.error("Organization not found when creating framework", { + organizationId, + frameworkId, + }); + throw new Error(`Organization with ID ${organizationId} not found`); + } + + // Verify the framework exists + const framework = await db.framework.findUnique({ + where: { id: frameworkId }, + }); + + if (!framework) { + logger.error("Framework not found when creating organization framework", { + organizationId, + frameworkId, + }); + throw new Error(`Framework with ID ${frameworkId} not found`); + } + + const organizationFramework = await db.organizationFramework.create({ + data: { + organizationId, + frameworkId, + status: "not_started", + }, + select: { + id: true, + }, + }); + + logger.info("Created organization framework", { + organizationId, + frameworkId, + organizationFrameworkId: organizationFramework.id, + }); + + const frameworkCategories = await db.frameworkCategory.findMany({ + where: { frameworkId }, + include: { + controls: true, + }, + }); + + const organizationCategories = await db.organizationCategory.findMany({ + where: { + organizationId, + frameworkId, + }, + }); + + for (const frameworkCategory of frameworkCategories) { + const organizationCategory = organizationCategories.find( + (oc) => oc.name === frameworkCategory.name, + ); + + if (!organizationCategory) continue; + + await db.organizationControl.createMany({ + data: frameworkCategory.controls.map((control) => ({ + organizationFrameworkId: organizationFramework.id, + controlId: control.id, + organizationId, + status: "not_started", + organizationCategoryId: organizationCategory.id, + })), + }); + } + + return organizationFramework; +}; + +const createOrganizationPolicy = async ( + organizationId: string, + frameworkIds: string[], +) => { + if (!organizationId) { + throw new Error("Not authorized - no organization found"); + } + + const policies = await db.policy.findMany(); + const policiesForFrameworks: Policy[] = []; + + for (const policy of policies) { + const usedBy = policy.usedBy; + + if (!usedBy) { + continue; + } + + const usedByFrameworkIds = Object.keys(usedBy); + + if ( + usedByFrameworkIds.some((frameworkId) => + frameworkIds.includes(frameworkId), + ) + ) { + policiesForFrameworks.push(policy); + } + } + + const organizationPolicies = await db.organizationPolicy.createMany({ + data: policiesForFrameworks.map((policy) => ({ + organizationId, + policyId: policy.id, + status: "draft", + content: policy.content as InputJsonValue[], + frequency: policy.frequency, + })), + }); + + return organizationPolicies; +}; + +const createOrganizationCategories = async ( + organizationId: string, + frameworkIds: string[], +) => { + if (!organizationId) { + throw new Error("Not authorized - no organization found"); + } + + // For each frameworkCategory we need to get the controls. + const frameworkCategories = await db.frameworkCategory.findMany({ + where: { + frameworkId: { in: frameworkIds }, + }, + }); + + // Create the organization categories. + const organizationCategories = await db.organizationCategory.createMany({ + data: frameworkCategories.map((category) => ({ + name: category.name, + description: category.description, + organizationId, + frameworkId: category.frameworkId, + })), + }); + + return organizationCategories; +}; + +const createOrganizationControlRequirements = async ( + organizationId: string, + organizationFrameworkIds: string[], +) => { + if (!organizationId) { + throw new Error("Not authorized - no organization found"); + } + + const controls = await db.organizationControl.findMany({ + where: { + organizationId, + organizationFrameworkId: { + in: organizationFrameworkIds, + }, + }, + include: { + control: true, + }, + }); + + // Create control requirements for each control + const controlRequirements = await db.controlRequirement.findMany({ + where: { + controlId: { in: controls.map((control) => control.controlId) }, + }, + include: { + policy: true, // Include the policy to get its ID + evidence: true, // Include the evidence to get its ID + }, + }); + + // Get all organization policies for this organization + const organizationPolicies = await db.organizationPolicy.findMany({ + where: { + organizationId, + }, + }); + + // Get all organization evidences for this organization + const organizationEvidences = await db.organizationEvidence.findMany({ + where: { + organizationId, + }, + }); + + for (const control of controls) { + const requirements = controlRequirements.filter( + (req) => req.controlId === control.controlId, + ); + + await db.organizationControlRequirement.createMany({ + data: requirements.map((requirement) => { + // Find the corresponding organization policy if this is a policy requirement + const policyId = + requirement.type === "policy" ? requirement.policy?.id : null; + const organizationPolicy = policyId + ? organizationPolicies.find((op) => op.policyId === policyId) + : null; + + const evidenceId = + requirement.type === "evidence" ? requirement.evidenceId : null; + + console.log({ + evidenceId, + }); + + const organizationEvidence = evidenceId + ? organizationEvidences.find((e) => e.evidenceId === evidenceId) + : null; + + console.log({ + organizationEvidence, + }); + + return { + organizationControlId: control.id, + controlRequirementId: requirement.id, + type: requirement.type, + description: requirement.description, + organizationPolicyId: organizationPolicy?.id || null, + organizationEvidenceId: organizationEvidence?.id || null, + }; + }), + }); + } + + return controlRequirements; +}; + +const createOrganizationEvidence = async ( + organizationId: string, + frameworkIds: string[], + userId: string, +) => { + if (!organizationId) { + throw new Error("Not authorized - no organization found"); + } + + const evidence = await db.controlRequirement.findMany({ + where: { + type: RequirementType.evidence, + }, + include: { + control: { + include: { + frameworkCategory: { + include: { + framework: true, + }, + }, + }, + }, + }, + }); + + const organizationEvidence = await db.organizationEvidence.createMany({ + data: evidence.map((evidence) => ({ + organizationId, + evidenceId: evidence.id, + name: evidence.name, + description: evidence.description, + frequency: evidence.frequency, + frameworkId: evidence.control.frameworkCategory?.framework.id || "", + assigneeId: userId, + })), + }); + + return organizationEvidence; +}; diff --git a/apps/app/src/jobs/tasks/organization/new-organization.ts b/apps/app/src/jobs/tasks/organization/new-organization.ts deleted file mode 100644 index 367b49cb2a..0000000000 --- a/apps/app/src/jobs/tasks/organization/new-organization.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { logger, schemaTask } from "@trigger.dev/sdk/v3"; -import { z } from "zod"; - -export const newOrganizationTask = schemaTask({ - id: "new-organization", - maxDuration: 1000 * 60 * 10, // 10 minutes - schema: z.object({ - organizationId: z.string(), - userId: z.string(), - }), - run: async ({ organizationId, userId }) => { - logger.info("New organization task started", { - organizationId, - userId, - }); - - logger.info("New organization task completed", { - organizationId, - userId, - }); - }, -}); diff --git a/apps/app/src/jobs/tasks/organization/setup-org-frameworks.ts b/apps/app/src/jobs/tasks/organization/setup-org-frameworks.ts new file mode 100644 index 0000000000..84d3d91024 --- /dev/null +++ b/apps/app/src/jobs/tasks/organization/setup-org-frameworks.ts @@ -0,0 +1,342 @@ +import { RequirementType, type Policy, type User } from "@bubba/db/types"; +import type { InputJsonValue } from "@prisma/client/runtime/library"; +import { db } from "@bubba/db"; +import { logger, schemaTask } from "@trigger.dev/sdk/v3"; +import { z } from "zod"; + +export const setupOrgFrameworksTask = schemaTask({ + id: "setup-org-frameworks", + maxDuration: 1000 * 60 * 3, // 3 minutes + schema: z.object({ + userId: z.string(), + organizationId: z.string(), + frameworkIds: z.array(z.string()), + }), + run: async ({ organizationId, frameworkIds, userId }) => { + logger.info("Setting up organization frameworks", { + organizationId, + frameworkIds, + }); + + const organization = await db.organization.findFirst({ + where: { + id: organizationId, + }, + }); + + if (!organization) { + throw new Error("Organization not found"); + } + + try { + await createOrganizationCategories(organizationId, frameworkIds); + + const organizationFrameworks = await Promise.all( + frameworkIds.map((frameworkId) => + createOrganizationFramework(organizationId, frameworkId), + ), + ); + + await createOrganizationPolicy(organizationId, frameworkIds); + + await createOrganizationControlRequirements( + organizationId, + organizationFrameworks.map((framework) => framework.id), + ); + + await createOrganizationEvidence(organizationId, frameworkIds, userId); + + return { + success: true, + }; + } catch (error) { + logger.error("Error setting up organization frameworks", { + error, + }); + + throw error; + } + }, +}); + +const createOrganizationFramework = async ( + organizationId: string, + frameworkId: string, +) => { + // First verify the organization exists + const organization = await db.organization.findUnique({ + where: { id: organizationId }, + }); + + if (!organization) { + logger.error("Organization not found when creating framework", { + organizationId, + frameworkId, + }); + throw new Error(`Organization with ID ${organizationId} not found`); + } + + // Verify the framework exists + const framework = await db.framework.findUnique({ + where: { id: frameworkId }, + }); + + if (!framework) { + logger.error("Framework not found when creating organization framework", { + organizationId, + frameworkId, + }); + throw new Error(`Framework with ID ${frameworkId} not found`); + } + + const organizationFramework = await db.organizationFramework.create({ + data: { + organizationId, + frameworkId, + status: "not_started", + }, + select: { + id: true, + }, + }); + + logger.info("Created organization framework", { + organizationId, + frameworkId, + organizationFrameworkId: organizationFramework.id, + }); + + const frameworkCategories = await db.frameworkCategory.findMany({ + where: { frameworkId }, + include: { + controls: true, + }, + }); + + const organizationCategories = await db.organizationCategory.findMany({ + where: { + organizationId, + frameworkId, + }, + }); + + for (const frameworkCategory of frameworkCategories) { + const organizationCategory = organizationCategories.find( + (oc) => oc.name === frameworkCategory.name, + ); + + if (!organizationCategory) continue; + + await db.organizationControl.createMany({ + data: frameworkCategory.controls.map((control) => ({ + organizationFrameworkId: organizationFramework.id, + controlId: control.id, + organizationId, + status: "not_started", + organizationCategoryId: organizationCategory.id, + })), + }); + } + + return organizationFramework; +}; + +const createOrganizationPolicy = async ( + organizationId: string, + frameworkIds: string[], +) => { + if (!organizationId) { + throw new Error("Not authorized - no organization found"); + } + + const policies = await db.policy.findMany(); + const policiesForFrameworks: Policy[] = []; + + for (const policy of policies) { + const usedBy = policy.usedBy; + + if (!usedBy) { + continue; + } + + const usedByFrameworkIds = Object.keys(usedBy); + + if ( + usedByFrameworkIds.some((frameworkId) => + frameworkIds.includes(frameworkId), + ) + ) { + policiesForFrameworks.push(policy); + } + } + + const organizationPolicies = await db.organizationPolicy.createMany({ + data: policiesForFrameworks.map((policy) => ({ + organizationId, + policyId: policy.id, + status: "draft", + content: policy.content as InputJsonValue[], + frequency: policy.frequency, + })), + }); + + return organizationPolicies; +}; + +const createOrganizationCategories = async ( + organizationId: string, + frameworkIds: string[], +) => { + if (!organizationId) { + throw new Error("Not authorized - no organization found"); + } + + // For each frameworkCategory we need to get the controls. + const frameworkCategories = await db.frameworkCategory.findMany({ + where: { + frameworkId: { in: frameworkIds }, + }, + }); + + // Create the organization categories. + const organizationCategories = await db.organizationCategory.createMany({ + data: frameworkCategories.map((category) => ({ + name: category.name, + description: category.description, + organizationId, + frameworkId: category.frameworkId, + })), + }); + + return organizationCategories; +}; + +const createOrganizationControlRequirements = async ( + organizationId: string, + organizationFrameworkIds: string[], +) => { + if (!organizationId) { + throw new Error("Not authorized - no organization found"); + } + + const controls = await db.organizationControl.findMany({ + where: { + organizationId, + organizationFrameworkId: { + in: organizationFrameworkIds, + }, + }, + include: { + control: true, + }, + }); + + // Create control requirements for each control + const controlRequirements = await db.controlRequirement.findMany({ + where: { + controlId: { in: controls.map((control) => control.controlId) }, + }, + include: { + policy: true, // Include the policy to get its ID + evidence: true, // Include the evidence to get its ID + }, + }); + + // Get all organization policies for this organization + const organizationPolicies = await db.organizationPolicy.findMany({ + where: { + organizationId, + }, + }); + + // Get all organization evidences for this organization + const organizationEvidences = await db.organizationEvidence.findMany({ + where: { + organizationId, + }, + }); + + for (const control of controls) { + const requirements = controlRequirements.filter( + (req) => req.controlId === control.controlId, + ); + + await db.organizationControlRequirement.createMany({ + data: requirements.map((requirement) => { + // Find the corresponding organization policy if this is a policy requirement + const policyId = + requirement.type === "policy" ? requirement.policy?.id : null; + const organizationPolicy = policyId + ? organizationPolicies.find((op) => op.policyId === policyId) + : null; + + const evidenceId = + requirement.type === "evidence" ? requirement.evidenceId : null; + + console.log({ + evidenceId, + }); + + const organizationEvidence = evidenceId + ? organizationEvidences.find((e) => e.evidenceId === evidenceId) + : null; + + console.log({ + organizationEvidence, + }); + + return { + organizationControlId: control.id, + controlRequirementId: requirement.id, + type: requirement.type, + description: requirement.description, + organizationPolicyId: organizationPolicy?.id || null, + organizationEvidenceId: organizationEvidence?.id || null, + }; + }), + }); + } + + return controlRequirements; +}; + +const createOrganizationEvidence = async ( + organizationId: string, + frameworkIds: string[], + userId: string, +) => { + if (!organizationId) { + throw new Error("Not authorized - no organization found"); + } + + const evidence = await db.controlRequirement.findMany({ + where: { + type: RequirementType.evidence, + }, + include: { + control: { + include: { + frameworkCategory: { + include: { + framework: true, + }, + }, + }, + }, + }, + }); + + const organizationEvidence = await db.organizationEvidence.createMany({ + data: evidence.map((evidence) => ({ + organizationId, + evidenceId: evidence.id, + name: evidence.name, + description: evidence.description, + frequency: evidence.frequency, + frameworkId: evidence.control.frameworkCategory?.framework.id || "", + assigneeId: userId, + })), + }); + + return organizationEvidence; +}; diff --git a/apps/app/src/locales/en.ts b/apps/app/src/locales/en.ts index 82d00b69a8..88b4d0f9dd 100644 --- a/apps/app/src/locales/en.ts +++ b/apps/app/src/locales/en.ts @@ -1,1094 +1,1104 @@ export default { - language: { - title: "Languages", - description: "Change the language used in the user interface.", - placeholder: "Select language", - }, - languages: { - en: "English", - es: "Spanish", - fr: "French", - no: "Norwegian", - pt: "Portuguese", - }, - common: { - frequency: { - daily: "Daily", - weekly: "Weekly", - monthly: "Monthly", - quarterly: "Quarterly", - yearly: "Yearly", - }, - notifications: { - inbox: "Inbox", - archive: "Archive", - archive_all: "Archive all", - no_notifications: "No new notifications", - }, - actions: { - save: "Save", - edit: "Edit", - delete: "Delete", - cancel: "Cancel", - clear: "Clear", - create: "Create", - addNew: "Add New", - send: "Send", - return: "Return", - success: "Success", - error: "Error", - next: "Next", - complete: "Complete", - }, - assignee: { - label: "Assignee", - placeholder: "Select assignee", - }, - date: { - pick: "Pick a date", - due_date: "Due Date", - }, - status: { - open: "Open", - pending: "Pending", - closed: "Closed", - archived: "Archived", - compliant: "Compliant", - non_compliant: "Non Compliant", - not_started: "Not Started", - in_progress: "In Progress", - published: "Published", - needs_review: "Needs Review", - draft: "Draft", - not_assessed: "Not Assessed", - assessed: "Assessed", - active: "Active", - inactive: "Inactive", - title: "Status", - }, - filters: { - clear: "Clear filters", - search: "Search...", - status: "Status", - department: "Department", - owner: { - label: "Assignee", - placeholder: "Filter by assignee", - }, - }, - table: { - title: "Title", - status: "Status", - assigned_to: "Assigned To", - due_date: "Due Date", - last_updated: "Last Updated", - no_results: "No results found", - }, - empty_states: { - no_results: { - title: "No results found", - title_tasks: "No tasks found", - title_risks: "No risks found", - description: "Try another search, or adjusting the filters", - description_filters: "Try another search, or adjusting the filters", - description_no_tasks: "Create a task to get started", - description_no_risks: "Create a risk to get started", - }, - no_items: { - title: "No items found", - description: "Try adjusting your search or filters", - }, - }, - pagination: { - of: "of", - items_per_page: "Items per page", - rows_per_page: "Rows per page", - page_x_of_y: "Page {{current}} of {{total}}", - go_to_first_page: "Go to first page", - go_to_previous_page: "Go to previous page", - go_to_next_page: "Go to next page", - go_to_last_page: "Go to last page", - }, - comments: { - title: "Comments", - description: "Add a comment using the form below.", - add: "New Comment", - new: "New Comment", - save: "Save Comment", - success: "Comment added successfully", - error: "Failed to add comment", - placeholder: "Write your comment here...", - empty: { - title: "No comments yet", - description: "Be the first to add a comment", - }, - }, - upload: { - fileUpload: { - uploadingText: "Uploading...", - uploadingFile: "Uploading file...", - dropFileHere: "Drop file here", - dropFileHereAlt: "Drop file here", - releaseToUpload: "Release to upload", - addFiles: "Add Files", - uploadAdditionalEvidence: "Upload a file or document", - dragDropOrClick: "Drag and drop or click to browse", - dragDropOrClickToSelect: "Drag and drop or click to select file", - maxFileSize: "Max file size: {size}MB", - }, - fileUrl: { - additionalLinks: "Additional Links", - add: "Add", - linksAdded: "{count} link{s} added", - enterUrl: "Enter URL", - addAnotherLink: "Add Another Link", - saveLinks: "Save Links", - urlBadge: "URL", - copyLink: "Copy Link", - openLink: "Open Link", - deleteLink: "Delete Link", - }, - fileCard: { - preview: "Preview", - filePreview: "File Preview: {fileName}", - previewNotAvailable: "Preview not available for this file type", - openFile: "Open File", - deleteFile: "Delete File", - deleteFileConfirmTitle: "Delete File", - deleteFileConfirmDescription: - "This action cannot be undone. The file will be permanently deleted.", - }, - fileSection: { - filesUploaded: "{count} files uploaded", - }, - }, - attachments: { - title: "Attachments", - description: "Add a file by clicking 'Add Attachment'.", - upload: "Upload attachment", - upload_description: - "Upload an attachment or add a link to an external resource.", - drop: "Drop the files here", - drop_description: - "Drop files here or click to choose files from your device.", - drop_files_description: "Files can be up to ", - empty: { - title: "No attachments", - description: "Add a file by clicking 'Add Attachment'.", - }, - toasts: { - error: "Something went wrong, please try again.", - error_uploading_files: "Cannot upload more than 1 file at a time", - error_uploading_files_multiple: "Cannot upload more than 10 files", - error_no_files_selected: "No files selected", - error_file_rejected: "File {file} was rejected", - error_failed_to_upload_files: "Failed to upload files", - error_failed_to_upload_files_multiple: "Failed to upload files", - error_failed_to_upload_files_single: "Failed to upload file", - success_uploading_files: "Files uploaded successfully", - success_uploading_files_multiple: "Files uploaded successfully", - success_uploading_files_single: "File uploaded successfully", - success_uploading_files_target: "Files uploaded", - uploading_files: "Uploading {target}...", - remove_file: "Remove file", - }, - }, - edit: "Edit", - errors: { - unexpected_error: "An unexpected error occurred", - }, - description: "Description", - last_updated: "Last Updated", - }, - header: { - discord: { - button: "Join us on Discord", - }, - feedback: { - button: "Feedback", - title: "Thank you for your feedback!", - description: "We will be back with you as soon as possible", - placeholder: "Ideas to improve this page or issues you are experiencing.", - success: "Thank you for your feedback!", - error: "Error sending feedback - try again?", - send: "Send Feedback", - }, - }, - not_found: { - title: "404 - Page not found", - description: "The page you are looking for does not exist.", - return: "Return to dashboard", - }, - theme: { - options: { - light: "Light", - dark: "Dark", - system: "System", - }, - }, - sidebar: { - overview: "Overview", - policies: "Policies", - risk: "Risk Management", - vendors: "Vendors", - integrations: "Integrations", - settings: "Settings", - evidence: "Evidence Tasks", - people: "People", - tests: "Cloud Tests", - }, - sub_pages: { - evidence: { - title: "Evidence", - list: "Evidence List", - overview: "Evidence Overview", - }, - risk: { - overview: "Risk Management", - register: "Risk Register", - risk_overview: "Risk Overview", - risk_comments: "Risk Comments", - tasks: { - task_overview: "Task Overview", - }, - }, - policies: { - all: "All Policies", - editor: "Policy Editor", - policy_details: "Policy Details", - }, - people: { - all: "People", - employee_details: "Employee Details", - }, - settings: { - members: "Team Members", - }, - frameworks: { - overview: "Frameworks", - }, - tests: { - overview: "Cloud Tests", - test_details: "Test Details", - }, - }, - auth: { - title: "Automate SOC 2, ISO 27001 and GDPR compliance with AI.", - description: - "Create a free account or log in with an existing account to continue.", - options: "More options", - google: "Continue with Google", - email: { - description: "Enter your email address to continue.", - placeholder: "Enter email address", - button: "Continue with email", - magic_link_sent: "Magic link sent", - magic_link_description: "Check your inbox for a magic link.", - magic_link_try_again: "Try again.", - success: "Email sent - check your inbox!", - error: "Error sending email - try again?", - }, - terms: - "By clicking continue, you acknowledge that you have read and agree to the Terms of Service and Privacy Policy.", - }, - onboarding: { - title: "Create an organization", - setup: "Setup", - description: "Tell us a bit about your organization.", - fields: { - fullName: { - label: "Your Name", - placeholder: "Your full name", - }, - name: { - label: "Organization Name", - placeholder: "Your organization name", - }, - subdomain: { - label: "Subdomain", - placeholder: "example", - }, - website: { - label: "Website", - placeholder: "Your organization website", - }, - }, - success: "Thanks, you're all set!", - error: "Something went wrong, please try again.", - check_availability: "Checking availability", - available: "Available", - unavailable: "Unavailable", - }, - overview: { - title: "Overview", - framework_chart: { - title: "Framework Progress", - }, - requirement_chart: { - title: "Compliance Status", - }, - }, - policies: { - dashboard: { - title: "Dashboard", - all: "All Policies", - policy_status: "Policy by Status", - policies_by_assignee: "Policies by Assignee", - policies_by_framework: "Policies by Framework", - sub_pages: { - overview: "Overview", - edit_policy: "Edit Policy", - }, - }, - overview: { - title: "Policy Overview", - form: { - update_policy: "Update Policy", - update_policy_description: "Update the policy title or description.", - update_policy_success: "Policy updated successfully", - update_policy_error: "Failed to update policy", - update_policy_title: "Policy Name", - review_frequency: "Review Frequency", - review_frequency_placeholder: "Select a review frequency", - review_date: "Review Date", - review_date_placeholder: "Select a review date", - required_to_sign: "Required to be signed by employees", - signature_required: "Require employees signature", - signature_not_required: "Do not ask employees to sign", - signature_requirement: "Signature Requirement", - signature_requirement_placeholder: "Select signature requirement", - }, - }, - new: { - success: "Policy successfully created", - error: "Failed to create policy", - details: "Policy Details", - title: "Enter a title for the policy", - description: "Enter a description for the policy", - }, - table: { - name: "Policy Name", - statuses: { - draft: "Draft", - published: "Published", - archived: "Archived", - }, - filters: { - owner: { - label: "Assignee", - placeholder: "Filter by assignee", - }, - }, - }, - filters: { - search: "Search policies...", - all: "All Policies", - }, - status: { - draft: "Draft", - published: "Published", - needs_review: "Needs Review", - archived: "Archived", - }, - policies: "policies", - title: "Policies", - create_new: "Create New Policy", - search_placeholder: "Search policies...", - status_filter: "Filter by status", - all_statuses: "All statuses", - no_policies_title: "No policies yet", - no_policies_description: "Get started by creating your first policy", - create_first: "Create first policy", - no_description: "No description provided", - last_updated: "Last updated: {{date}}", - save: "Save", - saving: "Saving...", - saved_success: "Policy saved successfully", - saved_error: "Failed to save policy", - }, - evidence_tasks: { - evidence_tasks: "Evidence Tasks", - overview: "Overview", - }, - risk: { - risks: "risks", - overview: "Overview", - create: "Create New Risk", - vendor: { - title: "Vendor Management", - dashboard: { - title: "Vendor Dashboard", - overview: "Vendor Overview", - vendor_status: "Vendor Status", - vendor_category: "Vendor Categories", - vendors_by_assignee: "Vendors by Assignee", - inherent_risk_description: - "Initial risk level before any controls are applied", - residual_risk_description: - "Remaining risk level after controls are applied", - }, - register: { - title: "Vendor Register", - table: { - name: "Name", - category: "Category", - status: "Status", - owner: "Owner", - }, - }, - assessment: { - title: "Vendor Assessment", - update_success: "Vendor risk assessment updated successfully", - update_error: "Failed to update vendor risk assessment", - inherent_risk: "Inherent Risk", - residual_risk: "Residual Risk", - }, - form: { - vendor_details: "Vendor Details", - vendor_name: "Name", - vendor_name_placeholder: "Enter vendor name", - vendor_website: "Website", - vendor_website_placeholder: "Enter vendor website", - vendor_description: "Description", - vendor_description_placeholder: "Enter vendor description", - vendor_category: "Category", - vendor_category_placeholder: "Select category", - vendor_status: "Status", - vendor_status_placeholder: "Select status", - create_vendor_success: "Vendor created successfully", - create_vendor_error: "Failed to create vendor", - update_vendor: "Update Vendor", - update_vendor_success: "Vendor updated successfully", - update_vendor_error: "Failed to update vendor", - add_comment: "Add Comment", - }, - table: { - name: "Name", - category: "Category", - status: "Status", - owner: "Owner", - }, - filters: { - search_placeholder: "Search vendors...", - status_placeholder: "Filter by status", - category_placeholder: "Filter by category", - owner_placeholder: "Filter by owner", - }, - empty_states: { - no_vendors: { - title: "No vendors yet", - description: "Get started by creating your first vendor", - }, - no_results: { - title: "No results found", - description: "No vendors match your search", - description_with_filters: "Try adjusting your filters", - }, - }, - actions: { - create: "Create Vendor", - }, - status: { - not_assessed: "Not Assessed", - in_progress: "In Progress", - assessed: "Assessed", - }, - category: { - cloud: "Cloud", - infrastructure: "Infrastructure", - software_as_a_service: "Software as a Service", - finance: "Finance", - marketing: "Marketing", - sales: "Sales", - hr: "HR", - other: "Other", - }, - risk_level: { - low: "Low Risk", - medium: "Medium Risk", - high: "High Risk", - unknown: "Unknown Risk", - }, - }, - dashboard: { - title: "Dashboard", - overview: "Risk Overview", - risk_status: "Risk Status", - risks_by_department: "Risks by Department", - risks_by_assignee: "Risks by Assignee", - inherent_risk_description: - "Inherent risk is calculated as likelihood * impact. Calculated before any controls are applied.", - residual_risk_description: - "Residual risk is calculated as likelihood * impact. This is the risk level after controls are applied.", - risk_assessment_description: "Compare inherent and residual risk levels", - }, - register: { - title: "Risk Register", - table: { - risk: "Risk", - }, - empty: { - no_risks: { - title: "Create a risk to get started", - description: - "Track and score risks, create and assign mitigation tasks for your team, and manage your risk register all in one simple interface.", - }, - create_risk: "Create a risk", - }, - }, - metrics: { - probability: "Probability", - impact: "Impact", - inherentRisk: "Inherent Risk", - residualRisk: "Residual Risk", - }, - form: { - update_inherent_risk: "Save Inherent Risk", - update_inherent_risk_description: - "Update the inherent risk of the risk. This is the risk level before any controls are applied.", - update_inherent_risk_success: "Inherent risk updated successfully", - update_inherent_risk_error: "Failed to update inherent risk", - update_residual_risk: "Save Residual Risk", - update_residual_risk_description: - "Update the residual risk of the risk. This is the risk level after controls are applied.", - update_residual_risk_success: "Residual risk updated successfully", - update_residual_risk_error: "Failed to update residual risk", - update_risk: "Update Risk", - update_risk_description: "Update the risk title or description.", - update_risk_success: "Risk updated successfully", - update_risk_error: "Failed to update risk", - create_risk_success: "Risk created successfully", - create_risk_error: "Failed to create risk", - risk_details: "Risk Details", - risk_title: "Risk Title", - risk_title_description: "Enter a name for the risk", - risk_description: "Description", - risk_description_description: "Enter a description for the risk", - risk_category: "Category", - risk_category_placeholder: "Select a category", - risk_department: "Department", - risk_department_placeholder: "Select a department", - risk_status: "Risk Status", - risk_status_placeholder: "Select a risk status", - }, - tasks: { - title: "Tasks", - attachments: "Attachments", - overview: "Task Overview", - form: { - title: "Task Details", - task_title: "Task Title", - status: "Task Status", - status_placeholder: "Select a task status", - task_title_description: "Enter a name for the task", - description: "Description", - description_description: "Enter a description for the task", - due_date: "Due Date", - due_date_description: "Select the due date for the task", - success: "Task created successfully", - error: "Failed to create task", - }, - sheet: { - title: "Create Task", - update: "Update Task", - update_description: "Update the task title or description.", - }, - empty: { - description_create: - "Create a mitigation task for this risk, add a treatment plan, and assign it to a team member.", - }, - }, - }, - people: { - title: "People", - details: { - taskProgress: "Task Progress", - tasks: "Tasks", - noTasks: "No tasks assigned yet", - }, - description: "Manage your team members and their roles.", - filters: { - search: "Search people...", - role: "Filter by role", - }, - actions: { - invite: "Add Employee", - clear: "Clear filters", - }, - table: { - name: "Name", - email: "Email", - department: "Department", - externalId: "External ID", - status: "Status", - }, - empty: { - no_employees: { - title: "No employees yet", - description: "Get started by inviting your first team member.", - }, - no_results: { - title: "No results found", - description: "No employees match your search", - description_with_filters: "Try adjusting your filters", - }, - }, - invite: { - title: "Add Employee", - description: "Add an employee to your organization.", - email: { - label: "Email address", - placeholder: "Enter email address", - }, - role: { - label: "Role", - placeholder: "Select a role", - }, - name: { - label: "Name", - placeholder: "Enter name", - }, - department: { - label: "Department", - placeholder: "Select a department", - }, - submit: "Add Employee", - success: "Employee added successfully", - error: "Failed to add employee", - }, - }, - settings: { - general: { - title: "General", - org_name: "Organization name", - org_name_description: - "This is your organizations visible name. You should use the legal name of your organization.", - org_name_tip: "Please use 32 characters at maximum.", - org_website: "Organization Website", - org_website_description: - "This is your organization's official website URL. Make sure to include the full URL with https://.", - org_website_tip: "Please enter a valid URL including https://", - org_website_error: "Error updating organization website", - org_website_updated: "Organization website updated", - org_delete: "Delete organization", - org_delete_description: - "Permanently remove your organization and all of its contents from the Comp AI platform. This action is not reversible - please continue with caution.", - org_delete_alert_title: "Are you absolutely sure?", - org_delete_alert_description: - "This action cannot be undone. This will permanently delete your organization and remove your data from our servers.", - org_delete_error: "Error deleting organization", - org_delete_success: "Organization deleted", - org_name_updated: "Organization name updated", - org_name_error: "Error updating organization name", - save_button: "Save", - delete_button: "Delete", - delete_confirm: "DELETE", - delete_confirm_tip: "Type DELETE to confirm.", - cancel_button: "Cancel", - }, - members: { - title: "Members", - }, - api_keys: { - title: "API Keys", - description: - "Manage API keys for programmatic access to your organization's data.", - list_title: "API Keys", - list_description: - "API keys allow secure access to your organization's data via our API.", - create: "New API Key", - create_title: "New API Key", - create_description: - "Create a new API key for programmatic access to your organization's data.", - created_title: "API Key Created", - created_description: - "Your API key has been created. Make sure to copy it now as you won't be able to see it again.", - name: "Name", - name_label: "Name", - name_placeholder: "Enter a name for this API key", - expiration: "Expiration", - expiration_placeholder: "Select expiration", - expires_label: "Expires", - expires_placeholder: "Select expiration", - expires_30days: "30 days", - expires_90days: "90 days", - expires_1year: "1 year", - expires_never: "Never", - thirty_days: "30 days", - ninety_days: "90 days", - one_year: "1 year", - your_key: "Your API Key", - api_key: "API Key", - save_warning: - "This key will only be shown once. Make sure to copy it now.", - copied: "API key copied to clipboard", - close_confirm: - "Are you sure you want to close? You won't be able to see this API key again.", - revoke_confirm: - "Are you sure you want to revoke this API key? This action cannot be undone.", - revoke_title: "Revoke API Key", - revoke: "Revoke", - created: "Created", - expires: "Expires", - last_used: "Last Used", - actions: "Actions", - never: "Never", - never_used: "Never used", - no_keys: "No API keys found. Create one to get started.", - security_note: - "API keys provide full access to your organization's data. Keep them secure and rotate them regularly.", - fetch_error: "Failed to fetch API keys", - create_error: "Failed to create API key", - revoked_success: "API key revoked successfully", - revoked_error: "Failed to revoke API key", - done: "Done", - }, - billing: { - title: "Billing", - }, - team: { - tabs: { - members: "Team Members", - invite: "Invite Members", - }, - members: { - title: "Team Members", - empty: { - no_organization: { - title: "No Organization", - description: "You are not part of any organization", - }, - no_members: { - title: "No Members", - description: "There are no active members in your organization", - }, - }, - role: { - owner: "Owner", - admin: "Admin", - member: "Member", - viewer: "Viewer", - }, - }, - invitations: { - title: "Pending Invitations", - description: "Users who have been invited but haven't accepted yet", - empty: { - no_organization: { - title: "No Organization", - description: "You are not part of any organization", - }, - no_invitations: { - title: "No Pending Invitations", - description: "There are no pending invitations", - }, - }, - invitation_sent: "Invitation sent", - actions: { - resend: "Resend Invite", - sending: "Sending Invite", - revoke: "Revoke", - revoke_title: "Revoke Invitation", - revoke_description_prefix: - "Are you sure you want to revoke the invitation for", - revoke_description_suffix: "This action cannot be undone.", - }, - toast: { - resend_success_prefix: "An invitation email has been sent to", - resend_error: "Failed to send invitation", - resend_unexpected: - "An unexpected error occurred while sending the invitation", - revoke_success_prefix: "Invitation to", - revoke_success_suffix: "has been revoked", - revoke_error: "Failed to revoke invitation", - revoke_unexpected: - "An unexpected error occurred while revoking the invitation", - }, - }, - invite: { - title: "Invite Team Member", - description: - "Send an invitation to a new team member to join your organization", - form: { - email: { - label: "Email", - placeholder: "member@example.com", - error: "Please enter a valid email address", - }, - role: { - label: "Role", - placeholder: "Select a role", - error: "Please select a role", - }, - department: { - label: "Department", - placeholder: "Select a department", - error: "Please select a department", - }, - departments: { - none: "None", - it: "IT", - hr: "HR", - admin: "Admin", - gov: "Government", - itsm: "ITSM", - qms: "QMS", - }, - }, - button: { - send: "Send Invitation", - sending: "Sending invitation...", - sent: "Invitation Sent", - }, - toast: { - error: "Failed to send invitation", - unexpected: - "An unexpected error occurred while sending the invitation", - }, - }, - member_actions: { - actions: "Actions", - change_role: "Change Role", - remove_member: "Remove Member", - remove_confirm: { - title: "Remove Team Member", - description_prefix: "Are you sure you want to remove", - description_suffix: "This action cannot be undone.", - }, - role_dialog: { - title: "Change Role", - description_prefix: "Update the role for", - role_label: "Role", - role_placeholder: "Select a role", - role_descriptions: { - admin: "Admins can manage team members and settings.", - member: - "Members can use all features but cannot manage team members.", - viewer: "Viewers can only view content without making changes.", - }, - cancel: "Cancel", - update: "Update Role", - }, - toast: { - remove_success: "has been removed from the organization", - remove_error: "Failed to remove member", - remove_unexpected: - "An unexpected error occurred while removing the member", - update_role_success: "has had their role updated to", - update_role_error: "Failed to update member role", - update_role_unexpected: - "An unexpected error occurred while updating the member's role", - }, - }, - }, - }, - tests: { - dashboard: { - overview: "Overview", - all: "All Tests", - tests_by_assignee: "Tests by Assignee", - passed: "Passed", - failed: "Failed", - severity_distribution: "Test Severity Distribution", - }, - severity: { - low: "Low", - medium: "Medium", - high: "High", - critical: "Critical", - }, - name: "Cloud Tests", - title: "Cloud Tests", - actions: { - create: "Add Cloud Test", - clear: "Clear filters", - refresh: "Refresh", - refresh_success: "Tests refreshed successfully", - refresh_error: "Failed to refresh tests", - }, - empty: { - no_tests: { - title: "No cloud tests yet", - description: "Get started by creating your first cloud test.", - }, - no_results: { - title: "No results found", - description: "No tests match your search", - description_with_filters: "Try adjusting your filters", - }, - }, - filters: { - search: "Search tests...", - role: "Filter by vendor", - }, - register: { - title: "Add Cloud Test", - description: "Configure a new cloud compliance test.", - submit: "Create Test", - success: "Test created successfully", - invalid_json: "Invalid JSON configuration provided", + language: { + title: "Languages", + description: "Change the language used in the user interface.", + placeholder: "Select language", + }, + languages: { + en: "English", + es: "Spanish", + fr: "French", + no: "Norwegian", + pt: "Portuguese", + }, + common: { + frequency: { + daily: "Daily", + weekly: "Weekly", + monthly: "Monthly", + quarterly: "Quarterly", + yearly: "Yearly", + }, + notifications: { + inbox: "Inbox", + archive: "Archive", + archive_all: "Archive all", + no_notifications: "No new notifications", + }, + actions: { + save: "Save", + edit: "Edit", + delete: "Delete", + cancel: "Cancel", + clear: "Clear", + create: "Create", + addNew: "Add New", + send: "Send", + return: "Return", + success: "Success", + error: "Error", + next: "Next", + complete: "Complete", + }, + assignee: { + label: "Assignee", + placeholder: "Select assignee", + }, + date: { + pick: "Pick a date", + due_date: "Due Date", + }, + status: { + open: "Open", + pending: "Pending", + closed: "Closed", + archived: "Archived", + compliant: "Compliant", + non_compliant: "Non Compliant", + not_started: "Not Started", + in_progress: "In Progress", + published: "Published", + needs_review: "Needs Review", + draft: "Draft", + not_assessed: "Not Assessed", + assessed: "Assessed", + active: "Active", + inactive: "Inactive", + title: "Status", + }, + filters: { + clear: "Clear filters", + search: "Search...", + status: "Status", + department: "Department", + owner: { + label: "Assignee", + placeholder: "Filter by assignee", + }, + }, + table: { + title: "Title", + status: "Status", + assigned_to: "Assigned To", + due_date: "Due Date", + last_updated: "Last Updated", + no_results: "No results found", + }, + empty_states: { + no_results: { + title: "No results found", + title_tasks: "No tasks found", + title_risks: "No risks found", + description: "Try another search, or adjusting the filters", + description_filters: "Try another search, or adjusting the filters", + description_no_tasks: "Create a task to get started", + description_no_risks: "Create a risk to get started", + }, + no_items: { + title: "No items found", + description: "Try adjusting your search or filters", + }, + }, + pagination: { + of: "of", + items_per_page: "Items per page", + rows_per_page: "Rows per page", + page_x_of_y: "Page {{current}} of {{total}}", + go_to_first_page: "Go to first page", + go_to_previous_page: "Go to previous page", + go_to_next_page: "Go to next page", + go_to_last_page: "Go to last page", + }, + comments: { + title: "Comments", + description: "Add a comment using the form below.", + add: "New Comment", + new: "New Comment", + save: "Save Comment", + success: "Comment added successfully", + error: "Failed to add comment", + placeholder: "Write your comment here...", + empty: { + title: "No comments yet", + description: "Be the first to add a comment", + }, + }, + upload: { + fileUpload: { + uploadingText: "Uploading...", + uploadingFile: "Uploading file...", + dropFileHere: "Drop file here", + dropFileHereAlt: "Drop file here", + releaseToUpload: "Release to upload", + addFiles: "Add Files", + uploadAdditionalEvidence: "Upload a file or document", + dragDropOrClick: "Drag and drop or click to browse", + dragDropOrClickToSelect: "Drag and drop or click to select file", + maxFileSize: "Max file size: {size}MB", + }, + fileUrl: { + additionalLinks: "Additional Links", + add: "Add", + linksAdded: "{count} link{s} added", + enterUrl: "Enter URL", + addAnotherLink: "Add Another Link", + saveLinks: "Save Links", + urlBadge: "URL", + copyLink: "Copy Link", + openLink: "Open Link", + deleteLink: "Delete Link", + }, + fileCard: { + preview: "Preview", + filePreview: "File Preview: {fileName}", + previewNotAvailable: "Preview not available for this file type", + openFile: "Open File", + deleteFile: "Delete File", + deleteFileConfirmTitle: "Delete File", + deleteFileConfirmDescription: + "This action cannot be undone. The file will be permanently deleted.", + }, + fileSection: { + filesUploaded: "{count} files uploaded", + }, + }, + attachments: { + title: "Attachments", + description: "Add a file by clicking 'Add Attachment'.", + upload: "Upload attachment", + upload_description: + "Upload an attachment or add a link to an external resource.", + drop: "Drop the files here", + drop_description: + "Drop files here or click to choose files from your device.", + drop_files_description: "Files can be up to ", + empty: { + title: "No attachments", + description: "Add a file by clicking 'Add Attachment'.", + }, + toasts: { + error: "Something went wrong, please try again.", + error_uploading_files: "Cannot upload more than 1 file at a time", + error_uploading_files_multiple: "Cannot upload more than 10 files", + error_no_files_selected: "No files selected", + error_file_rejected: "File {file} was rejected", + error_failed_to_upload_files: "Failed to upload files", + error_failed_to_upload_files_multiple: "Failed to upload files", + error_failed_to_upload_files_single: "Failed to upload file", + success_uploading_files: "Files uploaded successfully", + success_uploading_files_multiple: "Files uploaded successfully", + success_uploading_files_single: "File uploaded successfully", + success_uploading_files_target: "Files uploaded", + uploading_files: "Uploading {target}...", + remove_file: "Remove file", + }, + }, + edit: "Edit", + errors: { + unexpected_error: "An unexpected error occurred", + }, + description: "Description", + last_updated: "Last Updated", + }, + header: { + discord: { + button: "Join us on Discord", + }, + feedback: { + button: "Feedback", + title: "Thank you for your feedback!", + description: "We will be back with you as soon as possible", + placeholder: "Ideas to improve this page or issues you are experiencing.", + success: "Thank you for your feedback!", + error: "Error sending feedback - try again?", + send: "Send Feedback", + }, + }, + not_found: { + title: "404 - Page not found", + description: "The page you are looking for does not exist.", + return: "Return to dashboard", + }, + theme: { + options: { + light: "Light", + dark: "Dark", + system: "System", + }, + }, + sidebar: { + overview: "Overview", + policies: "Policies", + risk: "Risk Management", + vendors: "Vendors", + integrations: "Integrations", + settings: "Settings", + evidence: "Evidence Tasks", + people: "People", + tests: "Cloud Tests", + }, + sub_pages: { + evidence: { + title: "Evidence", + list: "Evidence List", + overview: "Evidence Overview", + }, + risk: { + overview: "Risk Management", + register: "Risk Register", + risk_overview: "Risk Overview", + risk_comments: "Risk Comments", + tasks: { + task_overview: "Task Overview", + }, + }, + policies: { + all: "All Policies", + editor: "Policy Editor", + policy_details: "Policy Details", + }, + people: { + all: "People", + employee_details: "Employee Details", + }, + settings: { + members: "Team Members", + }, + frameworks: { + overview: "Frameworks", + }, + tests: { + overview: "Cloud Tests", + test_details: "Test Details", + }, + }, + auth: { + title: "Automate SOC 2, ISO 27001 and GDPR compliance with AI.", + description: + "Create a free account or log in with an existing account to continue.", + options: "More options", + google: "Continue with Google", + email: { + description: "Enter your email address to continue.", + placeholder: "Enter email address", + button: "Continue with email", + magic_link_sent: "Magic link sent", + magic_link_description: "Check your inbox for a magic link.", + magic_link_try_again: "Try again.", + success: "Email sent - check your inbox!", + error: "Error sending email - try again?", + }, + terms: + "By clicking continue, you acknowledge that you have read and agree to the Terms of Service and Privacy Policy.", + }, + onboarding: { + title: "Create an organization", + submit: "Finish setup", + setup: "Welcome to Comp AI", + description: + "Tell us a bit about your organization and what framework(s) you want to get started with.", + trigger: { + title: "Hold tight, we're creating your organization", + creating: "This may take a minute or two...", + completed: "Organization created successfully", + continue: "Continue to dashboard", + error: "Something went wrong, please try again.", + }, + fields: { + fullName: { + label: "Your Name", + placeholder: "Your full name", + }, + name: { + label: "Organization Name", + placeholder: "Your organization name", + }, + subdomain: { + label: "Subdomain", + placeholder: "example", + }, + website: { + label: "Website", + placeholder: "Your organization website", + }, + }, + success: "Thanks, you're all set!", + error: "Something went wrong, please try again.", + check_availability: "Checking availability", + available: "Available", + unavailable: "Unavailable", + creating: "Creating your organization...", + }, + overview: { + title: "Overview", + framework_chart: { + title: "Framework Progress", + }, + requirement_chart: { + title: "Compliance Status", + }, + }, + policies: { + dashboard: { + title: "Dashboard", + all: "All Policies", + policy_status: "Policy by Status", + policies_by_assignee: "Policies by Assignee", + policies_by_framework: "Policies by Framework", + sub_pages: { + overview: "Overview", + edit_policy: "Edit Policy", + }, + }, + overview: { + title: "Policy Overview", + form: { + update_policy: "Update Policy", + update_policy_description: "Update the policy title or description.", + update_policy_success: "Policy updated successfully", + update_policy_error: "Failed to update policy", + update_policy_title: "Policy Name", + review_frequency: "Review Frequency", + review_frequency_placeholder: "Select a review frequency", + review_date: "Review Date", + review_date_placeholder: "Select a review date", + required_to_sign: "Required to be signed by employees", + signature_required: "Require employees signature", + signature_not_required: "Do not ask employees to sign", + signature_requirement: "Signature Requirement", + signature_requirement_placeholder: "Select signature requirement", + }, + }, + new: { + success: "Policy successfully created", + error: "Failed to create policy", + details: "Policy Details", + title: "Enter a title for the policy", + description: "Enter a description for the policy", + }, + table: { + name: "Policy Name", + statuses: { + draft: "Draft", + published: "Published", + archived: "Archived", + }, + filters: { + owner: { + label: "Assignee", + placeholder: "Filter by assignee", + }, + }, + }, + filters: { + search: "Search policies...", + all: "All Policies", + }, + status: { + draft: "Draft", + published: "Published", + needs_review: "Needs Review", + archived: "Archived", + }, + policies: "policies", + title: "Policies", + create_new: "Create New Policy", + search_placeholder: "Search policies...", + status_filter: "Filter by status", + all_statuses: "All statuses", + no_policies_title: "No policies yet", + no_policies_description: "Get started by creating your first policy", + create_first: "Create first policy", + no_description: "No description provided", + last_updated: "Last updated: {{date}}", + save: "Save", + saving: "Saving...", + saved_success: "Policy saved successfully", + saved_error: "Failed to save policy", + }, + evidence_tasks: { + evidence_tasks: "Evidence Tasks", + overview: "Overview", + }, + risk: { + risks: "risks", + overview: "Overview", + create: "Create New Risk", + vendor: { + title: "Vendor Management", + dashboard: { + title: "Vendor Dashboard", + overview: "Vendor Overview", + vendor_status: "Vendor Status", + vendor_category: "Vendor Categories", + vendors_by_assignee: "Vendors by Assignee", + inherent_risk_description: + "Initial risk level before any controls are applied", + residual_risk_description: + "Remaining risk level after controls are applied", + }, + register: { + title: "Vendor Register", + table: { + name: "Name", + category: "Category", + status: "Status", + owner: "Owner", + }, + }, + assessment: { + title: "Vendor Assessment", + update_success: "Vendor risk assessment updated successfully", + update_error: "Failed to update vendor risk assessment", + inherent_risk: "Inherent Risk", + residual_risk: "Residual Risk", + }, + form: { + vendor_details: "Vendor Details", + vendor_name: "Name", + vendor_name_placeholder: "Enter vendor name", + vendor_website: "Website", + vendor_website_placeholder: "Enter vendor website", + vendor_description: "Description", + vendor_description_placeholder: "Enter vendor description", + vendor_category: "Category", + vendor_category_placeholder: "Select category", + vendor_status: "Status", + vendor_status_placeholder: "Select status", + create_vendor_success: "Vendor created successfully", + create_vendor_error: "Failed to create vendor", + update_vendor: "Update Vendor", + update_vendor_success: "Vendor updated successfully", + update_vendor_error: "Failed to update vendor", + add_comment: "Add Comment", + }, + table: { + name: "Name", + category: "Category", + status: "Status", + owner: "Owner", + }, + filters: { + search_placeholder: "Search vendors...", + status_placeholder: "Filter by status", + category_placeholder: "Filter by category", + owner_placeholder: "Filter by owner", + }, + empty_states: { + no_vendors: { + title: "No vendors yet", + description: "Get started by creating your first vendor", + }, + no_results: { + title: "No results found", + description: "No vendors match your search", + description_with_filters: "Try adjusting your filters", + }, + }, + actions: { + create: "Create Vendor", + }, + status: { + not_assessed: "Not Assessed", + in_progress: "In Progress", + assessed: "Assessed", + }, + category: { + cloud: "Cloud", + infrastructure: "Infrastructure", + software_as_a_service: "Software as a Service", + finance: "Finance", + marketing: "Marketing", + sales: "Sales", + hr: "HR", + other: "Other", + }, + risk_level: { + low: "Low Risk", + medium: "Medium Risk", + high: "High Risk", + unknown: "Unknown Risk", + }, + }, + dashboard: { + title: "Dashboard", + overview: "Risk Overview", + risk_status: "Risk Status", + risks_by_department: "Risks by Department", + risks_by_assignee: "Risks by Assignee", + inherent_risk_description: + "Inherent risk is calculated as likelihood * impact. Calculated before any controls are applied.", + residual_risk_description: + "Residual risk is calculated as likelihood * impact. This is the risk level after controls are applied.", + risk_assessment_description: "Compare inherent and residual risk levels", + }, + register: { + title: "Risk Register", + table: { + risk: "Risk", + }, + empty: { + no_risks: { + title: "Create a risk to get started", + description: + "Track and score risks, create and assign mitigation tasks for your team, and manage your risk register all in one simple interface.", + }, + create_risk: "Create a risk", + }, + }, + metrics: { + probability: "Probability", + impact: "Impact", + inherentRisk: "Inherent Risk", + residualRisk: "Residual Risk", + }, + form: { + update_inherent_risk: "Save Inherent Risk", + update_inherent_risk_description: + "Update the inherent risk of the risk. This is the risk level before any controls are applied.", + update_inherent_risk_success: "Inherent risk updated successfully", + update_inherent_risk_error: "Failed to update inherent risk", + update_residual_risk: "Save Residual Risk", + update_residual_risk_description: + "Update the residual risk of the risk. This is the risk level after controls are applied.", + update_residual_risk_success: "Residual risk updated successfully", + update_residual_risk_error: "Failed to update residual risk", + update_risk: "Update Risk", + update_risk_description: "Update the risk title or description.", + update_risk_success: "Risk updated successfully", + update_risk_error: "Failed to update risk", + create_risk_success: "Risk created successfully", + create_risk_error: "Failed to create risk", + risk_details: "Risk Details", + risk_title: "Risk Title", + risk_title_description: "Enter a name for the risk", + risk_description: "Description", + risk_description_description: "Enter a description for the risk", + risk_category: "Category", + risk_category_placeholder: "Select a category", + risk_department: "Department", + risk_department_placeholder: "Select a department", + risk_status: "Risk Status", + risk_status_placeholder: "Select a risk status", + }, + tasks: { + title: "Tasks", + attachments: "Attachments", + overview: "Task Overview", + form: { + title: "Task Details", + task_title: "Task Title", + status: "Task Status", + status_placeholder: "Select a task status", + task_title_description: "Enter a name for the task", + description: "Description", + description_description: "Enter a description for the task", + due_date: "Due Date", + due_date_description: "Select the due date for the task", + success: "Task created successfully", + error: "Failed to create task", + }, + sheet: { + title: "Create Task", + update: "Update Task", + update_description: "Update the task title or description.", + }, + empty: { + description_create: + "Create a mitigation task for this risk, add a treatment plan, and assign it to a team member.", + }, + }, + }, + people: { + title: "People", + details: { + taskProgress: "Task Progress", + tasks: "Tasks", + noTasks: "No tasks assigned yet", + }, + description: "Manage your team members and their roles.", + filters: { + search: "Search people...", + role: "Filter by role", + }, + actions: { + invite: "Add Employee", + clear: "Clear filters", + }, + table: { + name: "Name", + email: "Email", + department: "Department", + externalId: "External ID", + status: "Status", + }, + empty: { + no_employees: { + title: "No employees yet", + description: "Get started by inviting your first team member.", + }, + no_results: { + title: "No results found", + description: "No employees match your search", + description_with_filters: "Try adjusting your filters", + }, + }, + invite: { + title: "Add Employee", + description: "Add an employee to your organization.", + email: { + label: "Email address", + placeholder: "Enter email address", + }, + role: { + label: "Role", + placeholder: "Select a role", + }, + name: { + label: "Name", + placeholder: "Enter name", + }, + department: { + label: "Department", + placeholder: "Select a department", + }, + submit: "Add Employee", + success: "Employee added successfully", + error: "Failed to add employee", + }, + }, + settings: { + general: { + title: "General", + org_name: "Organization name", + org_name_description: + "This is your organizations visible name. You should use the legal name of your organization.", + org_name_tip: "Please use 32 characters at maximum.", + org_website: "Organization Website", + org_website_description: + "This is your organization's official website URL. Make sure to include the full URL with https://.", + org_website_tip: "Please enter a valid URL including https://", + org_website_error: "Error updating organization website", + org_website_updated: "Organization website updated", + org_delete: "Delete organization", + org_delete_description: + "Permanently remove your organization and all of its contents from the Comp AI platform. This action is not reversible - please continue with caution.", + org_delete_alert_title: "Are you absolutely sure?", + org_delete_alert_description: + "This action cannot be undone. This will permanently delete your organization and remove your data from our servers.", + org_delete_error: "Error deleting organization", + org_delete_success: "Organization deleted", + org_name_updated: "Organization name updated", + org_name_error: "Error updating organization name", + save_button: "Save", + delete_button: "Delete", + delete_confirm: "DELETE", + delete_confirm_tip: "Type DELETE to confirm.", + cancel_button: "Cancel", + }, + members: { + title: "Members", + }, + api_keys: { + title: "API Keys", + description: + "Manage API keys for programmatic access to your organization's data.", + list_title: "API Keys", + list_description: + "API keys allow secure access to your organization's data via our API.", + create: "New API Key", + create_title: "New API Key", + create_description: + "Create a new API key for programmatic access to your organization's data.", + created_title: "API Key Created", + created_description: + "Your API key has been created. Make sure to copy it now as you won't be able to see it again.", + name: "Name", + name_label: "Name", + name_placeholder: "Enter a name for this API key", + expiration: "Expiration", + expiration_placeholder: "Select expiration", + expires_label: "Expires", + expires_placeholder: "Select expiration", + expires_30days: "30 days", + expires_90days: "90 days", + expires_1year: "1 year", + expires_never: "Never", + thirty_days: "30 days", + ninety_days: "90 days", + one_year: "1 year", + your_key: "Your API Key", + api_key: "API Key", + save_warning: + "This key will only be shown once. Make sure to copy it now.", + copied: "API key copied to clipboard", + close_confirm: + "Are you sure you want to close? You won't be able to see this API key again.", + revoke_confirm: + "Are you sure you want to revoke this API key? This action cannot be undone.", + revoke_title: "Revoke API Key", + revoke: "Revoke", + created: "Created", + expires: "Expires", + last_used: "Last Used", + actions: "Actions", + never: "Never", + never_used: "Never used", + no_keys: "No API keys found. Create one to get started.", + security_note: + "API keys provide full access to your organization's data. Keep them secure and rotate them regularly.", + fetch_error: "Failed to fetch API keys", + create_error: "Failed to create API key", + revoked_success: "API key revoked successfully", + revoked_error: "Failed to revoke API key", + done: "Done", + }, + billing: { + title: "Billing", + }, + team: { + tabs: { + members: "Team Members", + invite: "Invite Members", + }, + members: { + title: "Team Members", + empty: { + no_organization: { + title: "No Organization", + description: "You are not part of any organization", + }, + no_members: { + title: "No Members", + description: "There are no active members in your organization", + }, + }, + role: { + owner: "Owner", + admin: "Admin", + member: "Member", + viewer: "Viewer", + }, + }, + invitations: { + title: "Pending Invitations", + description: "Users who have been invited but haven't accepted yet", + empty: { + no_organization: { + title: "No Organization", + description: "You are not part of any organization", + }, + no_invitations: { + title: "No Pending Invitations", + description: "There are no pending invitations", + }, + }, + invitation_sent: "Invitation sent", + actions: { + resend: "Resend Invite", + sending: "Sending Invite", + revoke: "Revoke", + revoke_title: "Revoke Invitation", + revoke_description_prefix: + "Are you sure you want to revoke the invitation for", + revoke_description_suffix: "This action cannot be undone.", + }, + toast: { + resend_success_prefix: "An invitation email has been sent to", + resend_error: "Failed to send invitation", + resend_unexpected: + "An unexpected error occurred while sending the invitation", + revoke_success_prefix: "Invitation to", + revoke_success_suffix: "has been revoked", + revoke_error: "Failed to revoke invitation", + revoke_unexpected: + "An unexpected error occurred while revoking the invitation", + }, + }, + invite: { + title: "Invite Team Member", + description: + "Send an invitation to a new team member to join your organization", + form: { + email: { + label: "Email", + placeholder: "member@example.com", + error: "Please enter a valid email address", + }, + role: { + label: "Role", + placeholder: "Select a role", + error: "Please select a role", + }, + department: { + label: "Department", + placeholder: "Select a department", + error: "Please select a department", + }, + departments: { + none: "None", + it: "IT", + hr: "HR", + admin: "Admin", + gov: "Government", + itsm: "ITSM", + qms: "QMS", + }, + }, + button: { + send: "Send Invitation", + sending: "Sending invitation...", + sent: "Invitation Sent", + }, + toast: { + error: "Failed to send invitation", + unexpected: + "An unexpected error occurred while sending the invitation", + }, + }, + member_actions: { + actions: "Actions", + change_role: "Change Role", + remove_member: "Remove Member", + remove_confirm: { + title: "Remove Team Member", + description_prefix: "Are you sure you want to remove", + description_suffix: "This action cannot be undone.", + }, + role_dialog: { + title: "Change Role", + description_prefix: "Update the role for", + role_label: "Role", + role_placeholder: "Select a role", + role_descriptions: { + admin: "Admins can manage team members and settings.", + member: + "Members can use all features but cannot manage team members.", + viewer: "Viewers can only view content without making changes.", + }, + cancel: "Cancel", + update: "Update Role", + }, + toast: { + remove_success: "has been removed from the organization", + remove_error: "Failed to remove member", + remove_unexpected: + "An unexpected error occurred while removing the member", + update_role_success: "has had their role updated to", + update_role_error: "Failed to update member role", + update_role_unexpected: + "An unexpected error occurred while updating the member's role", + }, + }, + }, + }, + tests: { + dashboard: { + overview: "Overview", + all: "All Tests", + tests_by_assignee: "Tests by Assignee", + passed: "Passed", + failed: "Failed", + severity_distribution: "Test Severity Distribution", + }, + severity: { + low: "Low", + medium: "Medium", + high: "High", + critical: "Critical", + }, + name: "Cloud Tests", + title: "Cloud Tests", + actions: { + create: "Add Cloud Test", + clear: "Clear filters", + refresh: "Refresh", + refresh_success: "Tests refreshed successfully", + refresh_error: "Failed to refresh tests", + }, + empty: { + no_tests: { + title: "No cloud tests yet", + description: "Get started by creating your first cloud test.", + }, + no_results: { + title: "No results found", + description: "No tests match your search", + description_with_filters: "Try adjusting your filters", + }, + }, + filters: { + search: "Search tests...", + role: "Filter by vendor", + }, + register: { + title: "Add Cloud Test", + description: "Configure a new cloud compliance test.", + submit: "Create Test", + success: "Test created successfully", + invalid_json: "Invalid JSON configuration provided", - title_field: { - label: "Test Title", - placeholder: "Enter test title", - }, - description_field: { - label: "Description", - placeholder: "Enter test description", - }, - provider: { - label: "Cloud Provider", - placeholder: "Select cloud provider", - }, - config: { - label: "Test Configuration", - placeholder: "Enter JSON configuration for the test", - }, - auth_config: { - label: "Authentication Configuration", - placeholder: "Enter JSON authentication configuration", - }, - }, - table: { - title: "Title", - provider: "Provider", - status: "Status", - severity: "Severity", - result: "Result", - createdAt: "Created At", - assignedUser: "Assigned User", - assignedUserEmpty: "Not Assigned", - no_results: "No results found", - }, - }, - user_menu: { - theme: "Theme", - language: "Language", - sign_out: "Sign out", - account: "Account", - support: "Support", - settings: "Settings", - teams: "Teams", - }, - frameworks: { - title: "Frameworks", - overview: { - error: "Failed to load frameworks", - loading: "Loading frameworks...", - empty: { - title: "No frameworks selected", - description: - "Select frameworks to get started with your compliance journey", - }, - progress: { - title: "Framework Progress", - empty: { - title: "No frameworks yet", - description: - "Get started by adding a compliance framework to track your progress", - action: "Add Framework", - }, - }, - grid: { - welcome: { - title: "Welcome to Comp AI", - description: - "Get started by selecting the compliance frameworks you would like to implement. We'll help you manage and track your compliance journey across multiple standards.", - action: "Get Started", - }, - title: "Select Frameworks", - version: "Version", - actions: { - clear: "Clear", - confirm: "Confirm Selection", - }, - }, - }, - controls: { - title: "Controls", - description: "Review and manage compliance controls", - table: { - status: "Status", - control: "Control", - artifacts: "Artifacts", - actions: "Actions", - }, - statuses: { - completed: "Completed", - in_progress: "In Progress", - not_started: "Not Started", - }, - }, - }, - errors: { - unexpected: "Something went wrong, please try again", - }, - editor: { - ai: { - thinking: "AI is thinking", - thinking_spinner: "AI is thinking", - edit_or_generate: "Edit or generate...", - tell_ai_what_to_do_next: "Tell AI what to do next", - request_limit_reached: "You have reached your request limit for the day.", - }, - ai_selector: { - improve: "Improve writing", - fix: "Fix grammar", - shorter: "Make shorter", - longer: "Make longer", - continue: "Continue writing", - replace: "Replace selection", - insert: "Insert below", - discard: "Discard", - }, - }, - evidence: { - title: "Evidence", - list: "All Evidence", - overview: "Evidence Overview", - edit: "Uploaded Evidence", - dashboard: { - layout: "Dashboard", - layout_back_button: "Back", - title: "Evidence Dashboard", - by_department: "By Department", - by_assignee: "By Assignee", - by_framework: "By Framework", - }, - items: "items", - status: { - up_to_date: "Up to Date", - needs_review: "Needs Review", - draft: "Draft", - empty: "Empty", - }, - departments: { - none: "Uncategorized", - admin: "Administration", - gov: "Governance", - hr: "Human Resources", - it: "Information Technology", - itsm: "IT Service Management", - qms: "Quality Management", - }, - details: { - review_section: "Review Information", - content: "Evidence Content", - }, - }, - vendors: { - title: "Vendors", - register: { - title: "Vendor Register", - }, - dashboard: { - title: "Vendor Overview", - }, - }, - dashboard: { - risk_status: "Risk Status", - risks_by_department: "Risks by Department", - vendor_status: "Vendor Status", - vendors_by_category: "Vendors by Category", - }, + title_field: { + label: "Test Title", + placeholder: "Enter test title", + }, + description_field: { + label: "Description", + placeholder: "Enter test description", + }, + provider: { + label: "Cloud Provider", + placeholder: "Select cloud provider", + }, + config: { + label: "Test Configuration", + placeholder: "Enter JSON configuration for the test", + }, + auth_config: { + label: "Authentication Configuration", + placeholder: "Enter JSON authentication configuration", + }, + }, + table: { + title: "Title", + provider: "Provider", + status: "Status", + severity: "Severity", + result: "Result", + createdAt: "Created At", + assignedUser: "Assigned User", + assignedUserEmpty: "Not Assigned", + no_results: "No results found", + }, + }, + user_menu: { + theme: "Theme", + language: "Language", + sign_out: "Sign out", + account: "Account", + support: "Support", + settings: "Settings", + teams: "Teams", + }, + frameworks: { + title: "Frameworks", + overview: { + error: "Failed to load frameworks", + loading: "Loading frameworks...", + empty: { + title: "No frameworks selected", + description: + "Select frameworks to get started with your compliance journey", + }, + progress: { + title: "Framework Progress", + empty: { + title: "No frameworks yet", + description: + "Get started by adding a compliance framework to track your progress", + action: "Add Framework", + }, + }, + grid: { + welcome: { + title: "Welcome to Comp AI", + description: + "Get started by selecting the compliance frameworks you would like to implement. We'll help you manage and track your compliance journey across multiple standards.", + action: "Get Started", + }, + title: "Select Frameworks", + version: "Version", + actions: { + clear: "Clear", + confirm: "Confirm Selection", + }, + }, + }, + controls: { + title: "Controls", + description: "Review and manage compliance controls", + table: { + status: "Status", + control: "Control", + artifacts: "Artifacts", + actions: "Actions", + }, + statuses: { + completed: "Completed", + in_progress: "In Progress", + not_started: "Not Started", + }, + }, + }, + errors: { + unexpected: "Something went wrong, please try again", + }, + editor: { + ai: { + thinking: "AI is thinking", + thinking_spinner: "AI is thinking", + edit_or_generate: "Edit or generate...", + tell_ai_what_to_do_next: "Tell AI what to do next", + request_limit_reached: "You have reached your request limit for the day.", + }, + ai_selector: { + improve: "Improve writing", + fix: "Fix grammar", + shorter: "Make shorter", + longer: "Make longer", + continue: "Continue writing", + replace: "Replace selection", + insert: "Insert below", + discard: "Discard", + }, + }, + evidence: { + title: "Evidence", + list: "All Evidence", + overview: "Evidence Overview", + edit: "Uploaded Evidence", + dashboard: { + layout: "Dashboard", + layout_back_button: "Back", + title: "Evidence Dashboard", + by_department: "By Department", + by_assignee: "By Assignee", + by_framework: "By Framework", + }, + items: "items", + status: { + up_to_date: "Up to Date", + needs_review: "Needs Review", + draft: "Draft", + empty: "Empty", + }, + departments: { + none: "Uncategorized", + admin: "Administration", + gov: "Governance", + hr: "Human Resources", + it: "Information Technology", + itsm: "IT Service Management", + qms: "Quality Management", + }, + details: { + review_section: "Review Information", + content: "Evidence Content", + }, + }, + vendors: { + title: "Vendors", + register: { + title: "Vendor Register", + }, + dashboard: { + title: "Vendor Overview", + }, + }, + dashboard: { + risk_status: "Risk Status", + risks_by_department: "Risks by Department", + vendor_status: "Vendor Status", + vendors_by_category: "Vendors by Category", + }, } as const; diff --git a/apps/app/src/locales/es.ts b/apps/app/src/locales/es.ts index 98e161c3bc..9b3041ef7f 100644 --- a/apps/app/src/locales/es.ts +++ b/apps/app/src/locales/es.ts @@ -253,8 +253,8 @@ export default { }, onboarding: { title: "Crear una organización", - setup: "Configuración", - description: "Cuéntanos un poco sobre tu organización.", + setup: "Bienvenido a Comp AI", + description: "Cuéntanos un poco sobre tu organización y con qué marco(s) deseas comenzar.", fields: { name: { label: "Nombre de la Organización", @@ -277,7 +277,16 @@ export default { error: "Algo salió mal, por favor intenta de nuevo.", unavailable: "No disponible", check_availability: "Verificando disponibilidad", - available: "Disponible" + available: "Disponible", + submit: "Finalizar configuración", + trigger: { + title: "Espera un momento, estamos creando tu organización", + creating: "Esto puede tardar uno o dos minutos...", + completed: "Organización creada con éxito", + "continue": "Continuar al panel de control", + error: "Algo salió mal, por favor intenta de nuevo." + }, + creating: "Creando tu organización..." }, overview: { title: "Resumen", diff --git a/apps/app/src/locales/fr.ts b/apps/app/src/locales/fr.ts index 599a795c32..3b363aa3d5 100644 --- a/apps/app/src/locales/fr.ts +++ b/apps/app/src/locales/fr.ts @@ -253,8 +253,8 @@ export default { }, onboarding: { title: "Créer une organisation", - setup: "Configuration", - description: "Parlez-nous un peu de votre organisation.", + setup: "Bienvenue dans Comp AI", + description: "Parlez-nous un peu de votre organisation et des cadres avec lesquels vous souhaitez commencer.", fields: { name: { label: "Nom de l'organisation", @@ -277,7 +277,16 @@ export default { error: "Quelque chose s'est mal passé, veuillez réessayer.", unavailable: "Indisponible", check_availability: "Vérification de la disponibilité", - available: "Disponible" + available: "Disponible", + submit: "Terminez la configuration", + trigger: { + title: "Patientez, nous créons votre organisation", + creating: "Cela peut prendre une minute ou deux...", + completed: "Organisation créée avec succès", + "continue": "Continuer vers le tableau de bord", + error: "Une erreur s'est produite, veuillez réessayer." + }, + creating: "Création de votre organisation..." }, overview: { title: "Aperçu", diff --git a/apps/app/src/locales/no.ts b/apps/app/src/locales/no.ts index 20f310afb6..ac1e7a041f 100644 --- a/apps/app/src/locales/no.ts +++ b/apps/app/src/locales/no.ts @@ -253,8 +253,8 @@ export default { }, onboarding: { title: "Opprett en organisasjon", - setup: "Oppsett", - description: "Fortell oss litt om organisasjonen din.", + setup: "Velkommen til Comp AI", + description: "Fortell oss litt om organisasjonen din og hvilke rammeverk du ønsker å begynne med.", fields: { name: { label: "Organisasjonsnavn", @@ -277,7 +277,16 @@ export default { error: "Noe gikk galt, vennligst prøv igjen.", unavailable: "Ikke tilgjengelig", check_availability: "Sjekker tilgjengelighet", - available: "Tilgjengelig" + available: "Tilgjengelig", + submit: "Fullfør oppsett", + trigger: { + title: "Hold deg fast, vi oppretter organisasjonen din", + creating: "Dette kan ta et minutt eller to...", + completed: "Organisasjonen ble opprettet med suksess", + "continue": "Fortsett til dashbordet", + error: "Noe gikk galt, vennligst prøv igjen." + }, + creating: "Oppretter organisasjonen din..." }, overview: { title: "Oversikt", diff --git a/apps/app/src/locales/pt.ts b/apps/app/src/locales/pt.ts index f5285495b4..cdc69d5547 100644 --- a/apps/app/src/locales/pt.ts +++ b/apps/app/src/locales/pt.ts @@ -253,8 +253,8 @@ export default { }, onboarding: { title: "Criar uma organização", - setup: "Configuração", - description: "Conte-nos um pouco sobre sua organização.", + setup: "Bem-vindo ao Comp AI", + description: "Conte-nos um pouco sobre sua organização e quais framework(s) você deseja começar a usar.", fields: { name: { label: "Nome da Organização", @@ -277,7 +277,16 @@ export default { error: "Algo deu errado, por favor tente novamente.", unavailable: "Indisponível", check_availability: "Verificando disponibilidade", - available: "Disponível" + available: "Disponível", + submit: "Finalizar configuração", + trigger: { + title: "Aguarde, estamos criando sua organização", + creating: "Isso pode levar um ou dois minutos...", + completed: "Organização criada com sucesso", + "continue": "Continuar para o painel", + error: "Algo deu errado, por favor, tente novamente." + }, + creating: "Criando sua organização..." }, overview: { title: "Visão Geral", diff --git a/apps/app/src/types/actions.ts b/apps/app/src/types/actions.ts new file mode 100644 index 0000000000..54357c0112 --- /dev/null +++ b/apps/app/src/types/actions.ts @@ -0,0 +1,5 @@ +export interface ActionResponse { + success: boolean; + data?: T; + error?: string; +} diff --git a/packages/db/prisma/migrations/20250318161548_drop_subdomain_add_setup/migration.sql b/packages/db/prisma/migrations/20250318161548_drop_subdomain_add_setup/migration.sql new file mode 100644 index 0000000000..5f15bbd109 --- /dev/null +++ b/packages/db/prisma/migrations/20250318161548_drop_subdomain_add_setup/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to drop the column `subdomain` on the `Organization` table. All the data in the column will be lost. + +*/ +-- DropIndex +DROP INDEX "Organization_subdomain_key"; + +-- AlterTable +ALTER TABLE "Organization" DROP COLUMN "subdomain", +ADD COLUMN "setup" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/db/prisma/schema/schema.prisma b/packages/db/prisma/schema/schema.prisma index abb9430371..987f1131a7 100644 --- a/packages/db/prisma/schema/schema.prisma +++ b/packages/db/prisma/schema/schema.prisma @@ -77,7 +77,7 @@ model Organization { id String @id @default(cuid()) stripeCustomerId String? name String - subdomain String @unique + setup Boolean @default(false) website String tier Tier @default(free) policiesCreated Boolean @default(false)