diff --git a/apps/app/languine.lock b/apps/app/languine.lock index 071cf56b79..f1c929755b 100644 --- a/apps/app/languine.lock +++ b/apps/app/languine.lock @@ -569,6 +569,11 @@ files: settings.team.invite.button.sent: f98552d4b87007da82818937e16becb1 settings.team.invite.toast.error: 4e7e65d5a93d3037b3dda6f2f485f809 settings.team.invite.toast.unexpected: 48640555cac27f997e813e57b2567719 + settings.team.invite.error.title: b3ae1177e33927d873811dab64b96b5f + settings.team.invite.error.description: 3fe295d3d0645eb12d2540f51ee87322 + settings.team.invite.error.default: 05fc634188165901fca91a73958314be + settings.team.invite.error.home: d78de1a1ae35d774b5f97ccc434d0697 + settings.team.invite.error.signin: f2fdea440d768b85591e936a803c3631 settings.team.member_actions.actions: 06df33001c1d7187fdd81ea1f5b277aa settings.team.member_actions.change_role: 092c25021695408aaa0a250ca5c6489c settings.team.member_actions.remove_member: 5f76745afd9e5ee8676f8949544bfb1a diff --git a/apps/app/src/actions/organization/accept-invitation.ts b/apps/app/src/actions/organization/accept-invitation.ts index bb10c35fef..3828e0936b 100644 --- a/apps/app/src/actions/organization/accept-invitation.ts +++ b/apps/app/src/actions/organization/accept-invitation.ts @@ -1,178 +1,138 @@ "use server"; import { db } from "@bubba/db"; -import { authActionClient } from "../safe-action"; +import { revalidatePath, revalidateTag } from "next/cache"; import { z } from "zod"; +import { authActionClient } from "../safe-action"; import type { ActionResponse } from "../types"; -import { createSafeActionClient } from "next-safe-action"; -import { revalidatePath, revalidateTag } from "next/cache"; -const inviteCodeSchema = z.object({ - inviteCode: z.string(), -}); - -async function findMemberByInviteCode(inviteCode: string) { - const pendingInvitation = await db.organizationMember.findFirst({ - where: { - accepted: false, - invitedEmail: { not: null }, - inviteCode: inviteCode, - }, - include: { - organization: { - select: { - id: true, - name: true, - }, - }, - }, - }); - - return pendingInvitation; +async function validateInviteCode(inviteCode: string, invitedEmail: string) { + const pendingInvitation = await db.organizationMember.findFirst({ + where: { + accepted: false, + invitedEmail, + inviteCode, + }, + include: { + organization: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + return pendingInvitation; } -const publicActionClient = createSafeActionClient(); - -export const acceptInvitation = publicActionClient - .schema(inviteCodeSchema) - .action( - async ({ - parsedInput, - }): Promise< - ActionResponse<{ accepted: boolean; organizationId: string | null }> - > => { - const { inviteCode } = parsedInput; - - try { - const invitation = await findMemberByInviteCode(inviteCode); - - if (!invitation) { - return { - success: false, - error: "Invitation not found or has already been accepted", - }; - } - - return { - success: true, - data: { - accepted: false, - organizationId: invitation.organization.id, - }, - }; - } catch (error) { - console.error("Error validating invitation:", error); - return { - success: false, - error: "Failed to validate invitation", - }; - } - }, - ); - const completeInvitationSchema = z.object({ - inviteCode: z.string(), - userId: z.string(), + inviteCode: z.string(), }); export const completeInvitation = authActionClient - .metadata({ - name: "complete-invitation", - track: { - event: "complete_invitation", - channel: "organization", - }, - }) - .schema(completeInvitationSchema) - .action( - async ({ - parsedInput, - ctx, - }): Promise< - ActionResponse<{ accepted: boolean; organizationId: string }> - > => { - const { inviteCode, userId } = parsedInput; - - try { - const invitation = await findMemberByInviteCode(inviteCode); - - if (!invitation) { - return { - success: false, - error: "Invitation not found or has already been accepted", - }; - } - - const existingMembership = await db.organizationMember.findFirst({ - where: { - userId, - organizationId: invitation.organizationId, - }, - }); - - if (existingMembership) { - const user = await db.user.findUnique({ where: { id: userId } }); - if (!user?.organizationId) { - await db.user.update({ - where: { id: userId }, - data: { - organizationId: invitation.organizationId, - }, - }); - - await db.organizationMember.update({ - where: { id: existingMembership.id }, - data: { - accepted: true, - }, - }); - } - - return { - success: true, - data: { - accepted: true, - organizationId: invitation.organizationId, - }, - }; - } - - await db.organizationMember.update({ - where: { - id: invitation.id, - }, - data: { - accepted: true, - userId, - invitedEmail: null, - }, - }); - - await db.user.update({ - where: { - id: userId, - }, - data: { - organizationId: invitation.organizationId, - }, - }); - - revalidatePath("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/settings/members"); - revalidateTag(`user_${userId}`); - - return { - success: true, - data: { - accepted: true, - organizationId: invitation.organizationId, - }, - }; - } catch (error) { - console.error("Error accepting invitation:", error); - return { - success: false, - error: "Failed to accept invitation", - }; - } - }, - ); + .metadata({ + name: "complete-invitation", + track: { + event: "complete_invitation", + channel: "organization", + }, + }) + .schema(completeInvitationSchema) + .action( + async ({ + parsedInput, + ctx, + }): Promise< + ActionResponse<{ + accepted: boolean; + organizationId: string; + }> + > => { + const { inviteCode } = parsedInput; + const user = ctx.user; + + if (!user || !user.email) { + throw new Error("Unauthorized"); + } + + try { + const invitation = await validateInviteCode(inviteCode, user.email); + + if (!invitation) { + throw new Error("Invitation either used or expired"); + } + + const existingMembership = await db.organizationMember.findFirst({ + where: { + userId: user.id, + organizationId: invitation.organizationId, + }, + }); + + if (existingMembership) { + if (user.organizationId !== invitation.organizationId) { + await db.user.update({ + where: { id: user.id }, + data: { + organizationId: invitation.organizationId, + }, + }); + + await db.organizationMember.update({ + where: { id: existingMembership.id }, + data: { + accepted: true, + invitedEmail: null, + inviteCode: null, + }, + }); + } + + return { + success: true, + data: { + accepted: true, + organizationId: invitation.organizationId, + }, + }; + } + + await db.organizationMember.update({ + where: { + id: invitation.id, + }, + data: { + accepted: true, + userId: user.id, + invitedEmail: null, + inviteCode: null, + }, + }); + + await db.user.update({ + where: { + id: user.id, + }, + data: { + organizationId: invitation.organizationId, + }, + }); + + revalidatePath(`/${invitation.organization.id}`); + revalidatePath(`/${invitation.organization.id}/settings/members`); + revalidateTag(`user_${user.id}`); + + return { + success: true, + data: { + accepted: true, + organizationId: invitation.organizationId, + }, + }; + } catch (error) { + console.error("Error accepting invitation:", error); + throw new Error(error as string); + } + } + ); diff --git a/apps/app/src/actions/organization/remove-member.ts b/apps/app/src/actions/organization/remove-member.ts index 1efb57337d..cc2ba2fcb3 100644 --- a/apps/app/src/actions/organization/remove-member.ts +++ b/apps/app/src/actions/organization/remove-member.ts @@ -8,103 +8,121 @@ import type { ActionResponse } from "../types"; import { revalidatePath, revalidateTag } from "next/cache"; const removeMemberSchema = z.object({ - memberId: z.string(), + memberId: z.string(), }); export const removeMember = authActionClient - .metadata({ - name: "remove-member", - track: { - event: "remove_member", - channel: "organization", - }, - }) - .schema(removeMemberSchema) - .action( - async ({ - parsedInput, - ctx, - }): Promise> => { - if (!ctx.user.organizationId) { - return { - success: false, - error: "User does not have an organization", - }; - } + .metadata({ + name: "remove-member", + track: { + event: "remove_member", + channel: "organization", + }, + }) + .schema(removeMemberSchema) + .action( + async ({ + parsedInput, + ctx, + }): Promise> => { + if (!ctx.user.organizationId) { + return { + success: false, + error: "User does not have an organization", + }; + } - const { memberId } = parsedInput; + const { memberId } = parsedInput; - try { - // Check if user has admin permissions - const currentUserMember = await db.organizationMember.findFirst({ - where: { - organizationId: ctx.user.organizationId, - userId: ctx.user.id, - }, - }); + try { + // Check if user has admin permissions + const currentUserMember = await db.organizationMember.findFirst({ + where: { + organizationId: ctx.user.organizationId, + userId: ctx.user.id, + }, + }); - if ( - !currentUserMember || - (currentUserMember.role !== MembershipRole.admin && - currentUserMember.role !== MembershipRole.owner) - ) { - return { - success: false, - error: "You don't have permission to remove members", - }; - } + if ( + !currentUserMember || + (currentUserMember.role !== MembershipRole.admin && + currentUserMember.role !== MembershipRole.owner) + ) { + return { + success: false, + error: "You don't have permission to remove members", + }; + } - // Check if the target member exists in the organization - const targetMember = await db.organizationMember.findFirst({ - where: { - id: memberId, - organizationId: ctx.user.organizationId, - }, - }); + // Check if the target member exists in the organization + const targetMember = await db.organizationMember.findFirst({ + where: { + id: memberId, + organizationId: ctx.user.organizationId, + }, + }); - if (!targetMember) { - return { - success: false, - error: "Member not found in this organization", - }; - } + if (!targetMember) { + return { + success: false, + error: "Member not found in this organization", + }; + } - // Prevent removing the owner - if (targetMember.role === MembershipRole.owner) { - return { - success: false, - error: "Cannot remove the organization owner", - }; - } + // Prevent removing the owner + if (targetMember.role === MembershipRole.owner) { + return { + success: false, + error: "Cannot remove the organization owner", + }; + } - // Prevent self-removal - if (targetMember.userId === ctx.user.id) { - return { - success: false, - error: "You cannot remove yourself from the organization", - }; - } + // Prevent self-removal + if (targetMember.userId === ctx.user.id) { + return { + success: false, + error: "You cannot remove yourself from the organization", + }; + } - // Remove the member - await db.organizationMember.delete({ - where: { - id: memberId, - }, - }); + // Remove the member + await db.organizationMember.delete({ + where: { + id: memberId, + }, + select: { + organizationId: true, + }, + }); - revalidatePath("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/settings/members"); - revalidateTag(`user_${ctx.user.id}`); + await db.user.update({ + where: { + id: targetMember.userId, + }, + data: { + organizationId: null, + }, + }); - return { - success: true, - data: { removed: true }, - }; - } catch (error) { - console.error("Error removing member:", error); - return { - success: false, - error: "Failed to remove member", - }; - } - }, - ); + await db.session.deleteMany({ + where: { + userId: targetMember.userId, + }, + }); + + revalidatePath(`/${ctx.user.organizationId}`); + revalidateTag(`user_${ctx.user.id}`); + + return { + success: true, + data: { removed: true }, + }; + } catch (error) { + console.error("Error removing member:", error); + return { + success: false, + error: "Failed to remove member", + }; + } + } + ); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/integrations/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/integrations/layout.tsx new file mode 100644 index 0000000000..d6a18431bf --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/integrations/layout.tsx @@ -0,0 +1,17 @@ +import { auth } from "@/auth"; +import { redirect } from "next/navigation"; + +export default async function IntegrationsLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await auth(); + const user = session?.user; + + if (!user?.isAdmin) { + redirect(`/${user?.organizationId}`); + } + + return children; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/lib/utils.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/lib/utils.ts new file mode 100644 index 0000000000..908fe79e79 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/lib/utils.ts @@ -0,0 +1 @@ +"use server"; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/people/components/table/types.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/people/components/table/types.ts index 0849d28369..d4afdbdbfd 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/people/components/table/types.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/people/components/table/types.ts @@ -1,4 +1,3 @@ - export interface EmployeesTableProps { columnHeaders: { name: string; @@ -12,7 +11,6 @@ export interface EmployeesTableProps { full_name: string | null; email: string | null; role: string; - onboarded: boolean; emailVerified: Date | null; image: string | null; lastLogin: Date | null; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/settings/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/settings/layout.tsx index 56b4226399..828cbf3114 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/settings/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/settings/layout.tsx @@ -1,6 +1,7 @@ import { auth } from "@/auth"; import { getI18n } from "@/locales/server"; import { SecondaryMenu } from "@bubba/ui/secondary-menu"; +import { redirect } from "next/navigation"; import { Suspense } from "react"; export default async function Layout({ @@ -13,6 +14,14 @@ export default async function Layout({ const user = session?.user; const orgId = user?.organizationId; + if (!session) { + return redirect("/"); + } + + if (!session.user.isAdmin) { + return redirect(`/${orgId}`); + } + return (
Loading...
}> diff --git a/apps/app/src/app/[locale]/(public)/auth/invite/[code]/route.ts b/apps/app/src/app/[locale]/(public)/auth/invite/[code]/route.ts index 18f2b6e48a..6090ac6559 100644 --- a/apps/app/src/app/[locale]/(public)/auth/invite/[code]/route.ts +++ b/apps/app/src/app/[locale]/(public)/auth/invite/[code]/route.ts @@ -1,46 +1,46 @@ -import { acceptInvitation } from "@/actions/organization/accept-invitation"; +import { completeInvitation } from "@/actions/organization/accept-invitation"; import { db } from "@bubba/db"; import { redirect } from "next/navigation"; export async function GET(request: Request) { - const { searchParams } = new URL(request.url); - const inviteCode = searchParams.get("code"); - - if (!inviteCode) { - return redirect("/"); - } - - try { - const invitationResult = await acceptInvitation({ - inviteCode, - }); - - if (!invitationResult || !invitationResult.data?.success) { - throw new Error("Invalid invitation"); - } - - const organizationId = invitationResult.data?.data?.organizationId; - - if (!organizationId) { - throw new Error("Organization not found"); - } - - const organization = await db.organization.findUnique({ - where: { - id: organizationId, - }, - select: { - name: true, - }, - }); - - if (!organization) { - throw new Error("Organization not found"); - } - - redirect(`/auth?inviteCode=${inviteCode}`); - } catch (error) { - console.error("Error accepting invitation:", error); - redirect("/auth?error=invalid-invitation"); - } + const { searchParams } = new URL(request.url); + const inviteCode = searchParams.get("code"); + + if (!inviteCode) { + return redirect("/"); + } + + try { + const invitationResult = await completeInvitation({ + inviteCode, + }); + + if (!invitationResult || !invitationResult.data?.success) { + throw new Error("Invalid invitation"); + } + + const organizationId = invitationResult.data?.data?.organizationId; + + if (!organizationId) { + throw new Error("Organization not found"); + } + + const organization = await db.organization.findUnique({ + where: { + id: organizationId, + }, + select: { + name: true, + }, + }); + + if (!organization) { + throw new Error("Organization not found"); + } + + redirect(`/auth?inviteCode=${inviteCode}`); + } catch (error) { + console.error("Error accepting invitation:", error); + redirect("/auth?error=invalid-invitation"); + } } diff --git a/apps/app/src/app/[locale]/(public)/auth/invite/error/page.tsx b/apps/app/src/app/[locale]/(public)/auth/invite/error/page.tsx new file mode 100644 index 0000000000..8ed23688e0 --- /dev/null +++ b/apps/app/src/app/[locale]/(public)/auth/invite/error/page.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useSearchParams, useRouter } from "next/navigation"; +import { Button } from "@bubba/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@bubba/ui/card"; +import { AlertCircle } from "lucide-react"; +import Link from "next/link"; +import { useI18n } from "@/locales/client"; +import { SignOut } from "@/components/sign-out"; +import { useEffect } from "react"; + +export default function InviteErrorPage() { + const t = useI18n(); + const searchParams = useSearchParams(); + const router = useRouter(); + const errorMessage = searchParams.get("message"); + + useEffect(() => { + if (!errorMessage) { + router.push("/"); + } + }, [errorMessage, router]); + + if (!errorMessage) { + return null; + } + + return ( +
+ + +
+ + {t("settings.team.invite.error.title")} +
+ + {t("settings.team.invite.error.description")} + +
+ +
+ {errorMessage} +
+
+ + + + +
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(public)/auth/page.tsx b/apps/app/src/app/[locale]/(public)/auth/page.tsx index d300433509..59f7f1ecd8 100644 --- a/apps/app/src/app/[locale]/(public)/auth/page.tsx +++ b/apps/app/src/app/[locale]/(public)/auth/page.tsx @@ -3,89 +3,94 @@ import { MagicLinkSignIn } from "@/components/magic-link"; import { getI18n } from "@/locales/server"; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@bubba/ui/accordion"; import type { Metadata } from "next"; import Link from "next/link"; import { auth } from "@/auth"; import { redirect } from "next/navigation"; - export const metadata: Metadata = { - title: "Login | Comp AI", + title: "Login | Comp AI", }; export default async function Page({ - searchParams, + searchParams, }: { - searchParams: Promise<{ inviteCode?: string }>; + searchParams: Promise<{ inviteCode?: string }>; }) { - const t = await getI18n(); - const session = await auth(); + const t = await getI18n(); + const session = await auth(); + + const { inviteCode } = await searchParams; - if (session?.user) { - redirect("/"); - } + // If user is already logged in and there is no invite code, redirect to home + if (session?.user && !inviteCode) { + redirect("/"); + } - const { inviteCode } = await searchParams; + // If user is already logged in and there is an invite code, complete the invitation + if (session?.user && inviteCode) { + redirect(`/api/auth/invitation?code=${inviteCode}`); + } - const defaultSignInOptions = ( -
- -
- ); + const defaultSignInOptions = ( +
+ +
+ ); - const moreSignInOptions = ( -
- -
- ); + const moreSignInOptions = ( +
+ +
+ ); - return ( - <> -
-
-
-
-
- -

Comp AI

- -
-

{t("auth.title")}

-
- - {t("auth.description")} - -
-
+ return ( + <> +
+
+
+
+
+ +

Comp AI

+ +
+

{t("auth.title")}

+
+ + {t("auth.description")} + +
+
-
- {defaultSignInOptions} - - - - {t("auth.options")} - - -
- {moreSignInOptions} -
-
-
-
-
+
+ {defaultSignInOptions} + + + + {t("auth.options")} + + +
+ {moreSignInOptions} +
+
+
+
+
-

{t("auth.terms")}

-
-
-
- - ); +

{t("auth.terms")}

+
+
+
+ + ); } diff --git a/apps/app/src/app/api/auth/invitation/route.ts b/apps/app/src/app/api/auth/invitation/route.ts index 016f42da04..3381f467f3 100644 --- a/apps/app/src/app/api/auth/invitation/route.ts +++ b/apps/app/src/app/api/auth/invitation/route.ts @@ -1,38 +1,60 @@ import { completeInvitation } from "@/actions/organization/accept-invitation"; import { auth } from "@/auth"; +import { db } from "@bubba/db"; import { redirect } from "next/navigation"; import { NextResponse } from "next/server"; export async function GET(request: Request) { - const { searchParams } = new URL(request.url); - const inviteCode = searchParams.get("code"); - - if (!inviteCode) { - return redirect("/"); - } - - const session = await auth(); - - if (!session?.user?.id) { - return redirect(`/auth?inviteCode=${encodeURIComponent(inviteCode)}`); - } - - try { - const result = await completeInvitation({ - inviteCode, - userId: session.user.id, - }); - - if (!result || !result.data?.success) { - throw new Error("Failed to accept invitation"); - } - - return NextResponse.redirect(new URL("/", request.url)); - } catch (error) { - console.error("Error accepting invitation:", error); - - return redirect( - "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/auth/invite-error?message=Failed%20to%20accept%20invitation", - ); - } + const { searchParams } = new URL(request.url); + const inviteCode = searchParams.get("code"); + + if (!inviteCode) { + return redirect("/"); + } + + const session = await auth(); + + if (!session?.user?.id) { + return redirect(`/auth?inviteCode=${encodeURIComponent(inviteCode)}`); + } + + const signedInUserEmail = session.user.email; + const orgMember = await db.organizationMember.findFirst({ + where: { + userId: session.user.id, + inviteCode, + }, + }); + + if (!orgMember) { + return redirect( + `/auth/invite/error?message=${encodeURIComponent("You are not a member of this organization")}` + ); + } + + if (orgMember.invitedEmail !== signedInUserEmail) { + return redirect( + `/auth/invite/error?message=${encodeURIComponent(`Incorrect email. Please sign out and sign in with the email you were invited with, you used the email: ${signedInUserEmail}`)}` + ); + } + + try { + const result = await completeInvitation({ + inviteCode, + }); + + if (result?.serverError) { + throw new Error(result?.serverError); + } + + return NextResponse.redirect(new URL("/", request.url)); + } catch (error) { + console.error("Error accepting invitation:", error); + + return redirect( + `/auth/invite/error?message=${encodeURIComponent( + (error as Error).message + )}` + ); + } } diff --git a/apps/app/src/auth/config.ts b/apps/app/src/auth/config.ts index 3dae1e5048..a35ba384a3 100644 --- a/apps/app/src/auth/config.ts +++ b/apps/app/src/auth/config.ts @@ -1,5 +1,6 @@ import { PrismaAdapter } from "@auth/prisma-adapter"; import { db } from "@bubba/db"; +import type { OrganizationMember } from "@bubba/db/types"; import { sendMagicLinkEmail } from "@bubba/email/lib/magic-link"; import type { DefaultSession, NextAuthConfig } from "next-auth"; import GoogleProvider from "next-auth/providers/google"; @@ -8,18 +9,20 @@ import Resend from "next-auth/providers/resend"; declare module "next-auth" { interface User { organizationId?: string; - onboarded?: boolean; full_name?: string; avatar_url?: string; + role?: OrganizationMember["role"]; + isAdmin?: boolean; } interface Session extends DefaultSession { user: { id: string; organizationId?: string; - onboarded?: boolean; full_name?: string; avatar_url?: string; + role?: OrganizationMember["role"]; + isAdmin?: boolean; } & DefaultSession["user"]; } } @@ -29,6 +32,7 @@ export const authConfig: NextAuthConfig = { GoogleProvider({ clientId: process.env.AUTH_GOOGLE_ID!, clientSecret: process.env.AUTH_GOOGLE_SECRET!, + allowDangerousEmailAccountLinking: true, authorization: { params: { prompt: "select_account", @@ -52,15 +56,26 @@ export const authConfig: NextAuthConfig = { // Logged in users are authenticated, otherwise redirect to login page return !!auth; }, - session: ({ session, user }) => ({ - ...session, - user: { - ...session.user, - id: user.id, - organizationId: user.organizationId, - onboarded: user.onboarded, - }, - }), + session: async ({ session, user }) => { + const organizationMemberCount = await db.organizationMember.count({ + where: { + userId: user.id, + organizationId: user.organizationId, + OR: [{ role: "admin" }, { role: "owner" }], + }, + }); + + return { + ...session, + user: { + ...session.user, + id: user.id, + organizationId: user.organizationId, + role: user.role, + isAdmin: organizationMemberCount > 0, + }, + }; + }, }, events: { signIn: async ({ user }) => { diff --git a/apps/app/src/auth/org.ts b/apps/app/src/auth/org.ts index ef9092be33..b1080260fc 100644 --- a/apps/app/src/auth/org.ts +++ b/apps/app/src/auth/org.ts @@ -45,13 +45,6 @@ export async function createOrganizationAndConnectUser(input: { name: true, }, }), - db.user.update({ - where: { id: input.userId }, - data: { role: "admin" }, - select: { - id: true, - }, - }), ]); const stripeCustomerId = await createStripeCustomer({ diff --git a/apps/app/src/components/header.tsx b/apps/app/src/components/header.tsx index 3da815780e..25abf862f8 100644 --- a/apps/app/src/components/header.tsx +++ b/apps/app/src/components/header.tsx @@ -8,50 +8,52 @@ import { Suspense } from "react"; import { FeedbackForm } from "./feedback-form"; import { MobileMenu } from "./mobile-menu"; import { NotificationCenter } from "./notification-center"; -import { OrganizationSwitcher } from "./organization-switcher"; -import { getOrganizations } from "@/data/getOrganizations"; import { auth } from "@/auth"; import { redirect } from "next/navigation"; export async function Header() { - const t = await getI18n(); - const session = await auth(); - const user = session?.user; - const currentOrganizationId = user?.organizationId; + const t = await getI18n(); + const session = await auth(); + const user = session?.user; + const currentOrganizationId = user?.organizationId; + const hasAccess = user?.isAdmin; - if (!currentOrganizationId) { - redirect("/"); - } + if (!currentOrganizationId) { + redirect("/"); + } - return ( -
- + return ( +
+ -
-
- - -
+
+
+ + +
- + - }> - - -
-
- ); + }> + + + +
+ ); } diff --git a/apps/app/src/components/main-menu.tsx b/apps/app/src/components/main-menu.tsx index 18ba505087..eb8f84f1fd 100644 --- a/apps/app/src/components/main-menu.tsx +++ b/apps/app/src/components/main-menu.tsx @@ -27,6 +27,7 @@ type MenuItem = { name: string; disabled: boolean; icon: React.FC<{ size?: number }>; + protected: boolean; }; // Map of menu item IDs to their icon components @@ -175,9 +176,15 @@ type Props = { initialItems?: MenuItem[]; onSelect?: () => void; organizationId: string; + userIsAdmin: boolean; }; -export function MainMenu({ initialItems, onSelect, organizationId }: Props) { +export function MainMenu({ + initialItems, + onSelect, + organizationId, + userIsAdmin, +}: Props) { const t = useI18n(); const defaultItems: MenuItem[] = [ @@ -187,6 +194,7 @@ export function MainMenu({ initialItems, onSelect, organizationId }: Props) { name: t("sidebar.overview"), disabled: false, icon: Icons.Overview, + protected: false, }, { id: "evidence", @@ -194,6 +202,7 @@ export function MainMenu({ initialItems, onSelect, organizationId }: Props) { name: t("sidebar.evidence"), disabled: false, icon: Icons.Evidence, + protected: false, }, { id: "tests", @@ -201,6 +210,7 @@ export function MainMenu({ initialItems, onSelect, organizationId }: Props) { name: t("sidebar.tests"), disabled: false, icon: Icons.CloudSync, + protected: false, }, { id: "policies", @@ -208,6 +218,7 @@ export function MainMenu({ initialItems, onSelect, organizationId }: Props) { name: t("sidebar.policies"), disabled: false, icon: Icons.Policies, + protected: false, }, { id: "people", @@ -215,6 +226,7 @@ export function MainMenu({ initialItems, onSelect, organizationId }: Props) { name: t("sidebar.people"), disabled: false, icon: Icons.Peolple, + protected: false, }, { id: "risk", @@ -222,6 +234,7 @@ export function MainMenu({ initialItems, onSelect, organizationId }: Props) { name: t("sidebar.risk"), disabled: false, icon: Icons.Risk, + protected: false, }, { id: "vendors", @@ -229,6 +242,7 @@ export function MainMenu({ initialItems, onSelect, organizationId }: Props) { name: t("sidebar.vendors"), disabled: false, icon: Icons.Vendors, + protected: false, }, { id: "integrations", @@ -236,6 +250,7 @@ export function MainMenu({ initialItems, onSelect, organizationId }: Props) { name: t("sidebar.integrations"), disabled: false, icon: Icons.Apps, + protected: true, }, { id: "settings", @@ -243,6 +258,7 @@ export function MainMenu({ initialItems, onSelect, organizationId }: Props) { name: t("sidebar.settings"), disabled: false, icon: Icons.Settings, + protected: true, }, ]; @@ -338,6 +354,10 @@ export function MainMenu({ initialItems, onSelect, organizationId }: Props) { .map((item) => { const isActive = isPathActive(item.path); + if (item.protected && !userIsAdmin) { + return null; + } + return ( setOpen(false)} organizationId={organizationId} + userIsAdmin={isAdmin} /> diff --git a/apps/app/src/components/settings/team/members-list.tsx b/apps/app/src/components/settings/team/members-list.tsx index de6a831f06..6d928aff17 100644 --- a/apps/app/src/components/settings/team/members-list.tsx +++ b/apps/app/src/components/settings/team/members-list.tsx @@ -123,14 +123,16 @@ export function MembersList({ {getMemberRoleIcon(member.role)} {t(`settings.team.members.role.${member.role}`)} - - {member.accepted - ? t("settings.team.members.status.accepted") - : t("settings.team.members.status.pending")} - + {member.role !== "owner" && ( + + {member.accepted + ? t("settings.team.members.status.accepted") + : t("settings.team.members.status.pending")} + + )} diff --git a/apps/app/src/components/settings/team/team-members.tsx b/apps/app/src/components/settings/team/team-members.tsx index 3f37a68b62..9092725ef8 100644 --- a/apps/app/src/components/settings/team/team-members.tsx +++ b/apps/app/src/components/settings/team/team-members.tsx @@ -2,7 +2,6 @@ import { auth } from "@/auth"; import { getI18n } from "@/locales/server"; import { db } from "@bubba/db"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@bubba/ui/tabs"; -import { cache } from "react"; import { InviteMemberForm } from "./invite-member-form"; import { MembersList } from "./members-list"; import { PendingInvitations } from "./pending-invitations"; @@ -56,7 +55,7 @@ export async function TeamMembers() { ); } -const getOrganizationMembers = cache(async (organizationId: string) => { +const getOrganizationMembers = async (organizationId: string) => { return db.organizationMember.findMany({ where: { organizationId, @@ -68,9 +67,9 @@ const getOrganizationMembers = cache(async (organizationId: string) => { joinedAt: "desc", }, }); -}); +}; -const getPendingInvitations = cache(async (organizationId: string) => { +const getPendingInvitations = async (organizationId: string) => { return db.organizationMember.findMany({ where: { organizationId, @@ -81,4 +80,4 @@ const getPendingInvitations = cache(async (organizationId: string) => { joinedAt: "desc", }, }); -}); +}; diff --git a/apps/app/src/components/sidebar.tsx b/apps/app/src/components/sidebar.tsx index e4d443208e..2dd7761f30 100644 --- a/apps/app/src/components/sidebar.tsx +++ b/apps/app/src/components/sidebar.tsx @@ -9,36 +9,40 @@ import { Suspense } from "react"; import { OrgMenu } from "./org-menu"; export async function Sidebar() { - const session = await auth(); - const user = session?.user; - const organizationId = user?.organizationId; + const session = await auth(); + const user = session?.user; + const organizationId = user?.organizationId; - if (!organizationId) { - redirect("/"); - } + if (!organizationId) { + redirect("/"); + } - const cookieStore = await cookies(); + const cookieStore = await cookies(); - const initialItems = cookieStore.has(Cookies.MenuConfig) - ? JSON.parse(cookieStore.get(Cookies.MenuConfig)?.value ?? "") - : null; + const initialItems = cookieStore.has(Cookies.MenuConfig) + ? JSON.parse(cookieStore.get(Cookies.MenuConfig)?.value ?? "") + : null; - return ( - + ); } diff --git a/apps/app/src/components/sign-out.tsx b/apps/app/src/components/sign-out.tsx index ffdb79e852..cbf4b51b5b 100644 --- a/apps/app/src/components/sign-out.tsx +++ b/apps/app/src/components/sign-out.tsx @@ -1,22 +1,34 @@ "use client"; import { useI18n } from "@/locales/client"; +import { Button } from "@bubba/ui/button"; import { DropdownMenuItem } from "@bubba/ui/dropdown-menu"; import { signOut } from "next-auth/react"; +import { useRouter } from "next/navigation"; import { useState } from "react"; -export function SignOut() { - const t = useI18n(); - const [isLoading, setLoading] = useState(false); +export function SignOut({ asButton = false }: { asButton?: boolean }) { + const t = useI18n(); + const router = useRouter(); + const [isLoading, setLoading] = useState(false); - const handleSignOut = async () => { - setLoading(true); - await signOut(); - }; + const handleSignOut = async () => { + setLoading(true); + await signOut({ redirect: false }); + router.push("/auth"); + }; - return ( - - {isLoading ? "Loading..." : t("user_menu.sign_out")} - - ); + if (asButton) { + return ( + + ); + } + + return ( + + {isLoading ? "Loading..." : t("user_menu.sign_out")} + + ); } diff --git a/apps/app/src/data/getOrganizations.ts b/apps/app/src/data/getOrganizations.ts index 7ffb58696f..daae34a409 100644 --- a/apps/app/src/data/getOrganizations.ts +++ b/apps/app/src/data/getOrganizations.ts @@ -12,6 +12,14 @@ export async function getOrganizations() { const memberOrganizations = await db.organizationMember.findMany({ where: { userId: user.id, + OR: [ + { + accepted: true, + }, + { + role: "owner", + }, + ], }, include: { organization: true, diff --git a/apps/app/src/jobs/tasks/organization/create-organization.ts b/apps/app/src/jobs/tasks/organization/create-organization.ts index 9510d5347d..a0d0ff0150 100644 --- a/apps/app/src/jobs/tasks/organization/create-organization.ts +++ b/apps/app/src/jobs/tasks/organization/create-organization.ts @@ -5,385 +5,376 @@ import { logger, schemaTask } from "@trigger.dev/sdk/v3"; import { z } from "zod"; 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, userId); - - 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; - } - }, + 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 createOrganizationCategories(organizationId, frameworkIds); + + const organizationFrameworks = await Promise.all( + frameworkIds.map((frameworkId) => + createOrganizationFramework(organizationId, frameworkId) + ) + ); + + await createOrganizationPolicy(organizationId, frameworkIds, userId); + + 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, + 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; + // 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[], - userId: string, + organizationId: string, + frameworkIds: string[], + userId: 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, - ownerId: userId, - status: "draft", - content: policy.content as InputJsonValue[], - frequency: policy.frequency, - })), - }); - - return organizationPolicies; + 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, + ownerId: userId, + status: "draft", + content: policy.content as InputJsonValue[], + frequency: policy.frequency, + })), + }); + + return organizationPolicies; }; const createOrganizationCategories = async ( - organizationId: string, - frameworkIds: string[], + 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; + 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[], + 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; + 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, + 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, - department: evidence.department, - })), - }); - - return organizationEvidence; + 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, + department: evidence.department, + })), + }); + + return organizationEvidence; }; diff --git a/apps/app/src/locales/en.ts b/apps/app/src/locales/en.ts index 754c528833..31b2e41318 100644 --- a/apps/app/src/locales/en.ts +++ b/apps/app/src/locales/en.ts @@ -867,6 +867,13 @@ export default { unexpected: "An unexpected error occurred while sending the invitation", }, + error: { + title: "Invitation Error", + description: "There was an issue with your invitation:", + default: "An unknown error occurred with your invitation.", + home: "Go to Home", + signin: "Sign In", + }, }, member_actions: { actions: "Actions", diff --git a/apps/app/src/locales/es.ts b/apps/app/src/locales/es.ts index b4bf7b614e..b3722fa36b 100644 --- a/apps/app/src/locales/es.ts +++ b/apps/app/src/locales/es.ts @@ -738,6 +738,13 @@ export default { toast: { error: "Error al enviar la invitación", unexpected: "Ocurrió un error inesperado al enviar la invitación" + }, + error: { + title: "Error de invitación", + description: "Hubo un problema con tu invitación:", + "default": "Ocurrió un error desconocido con tu invitación.", + home: "Ir a Inicio", + signin: "Iniciar sesión" } }, member_actions: { diff --git a/apps/app/src/locales/fr.ts b/apps/app/src/locales/fr.ts index 056a41c713..1816d50886 100644 --- a/apps/app/src/locales/fr.ts +++ b/apps/app/src/locales/fr.ts @@ -738,6 +738,13 @@ export default { toast: { error: "Échec de l'envoi de l'invitation", unexpected: "Une erreur inattendue s'est produite lors de l'envoi de l'invitation" + }, + error: { + title: "Erreur d'invitation", + description: "Il y a eu un problème avec votre invitation :", + "default": "Une erreur inconnue est survenue avec votre invitation.", + home: "Aller à l'accueil", + signin: "Se connecter" } }, member_actions: { diff --git a/apps/app/src/locales/no.ts b/apps/app/src/locales/no.ts index eddd3ffc6e..cbde466579 100644 --- a/apps/app/src/locales/no.ts +++ b/apps/app/src/locales/no.ts @@ -738,6 +738,13 @@ export default { toast: { error: "Feil ved sending av invitasjon", unexpected: "En uventet feil oppstod under sending av invitasjonen" + }, + error: { + title: "Invitasjonsfeil", + description: "Det oppstod et problem med invitasjonen din:", + "default": "En ukjent feil oppstod med invitasjonen din.", + home: "Gå til Hjem", + signin: "Logg inn" } }, member_actions: { diff --git a/apps/app/src/locales/pt.ts b/apps/app/src/locales/pt.ts index 6b90f1f2dc..eacbb62fcb 100644 --- a/apps/app/src/locales/pt.ts +++ b/apps/app/src/locales/pt.ts @@ -738,6 +738,13 @@ export default { toast: { error: "Falha ao enviar convite", unexpected: "Ocorreu um erro inesperado ao enviar o convite" + }, + error: { + title: "Erro de Convite", + description: "Houve um problema com seu convite:", + "default": "Ocorreu um erro desconhecido com seu convite.", + home: "Ir para a Página Inicial", + signin: "Entrar" } }, member_actions: { diff --git a/apps/app/src/store/user/store.ts b/apps/app/src/store/user/store.ts index 99cd8fc492..ae4f96ab8a 100644 --- a/apps/app/src/store/user/store.ts +++ b/apps/app/src/store/user/store.ts @@ -5,7 +5,6 @@ import { createStore } from "zustand"; type User = { id: string; organizationId?: string; - onboarded?: boolean; full_name?: string; avatar_url?: string; } & NextAuthUser; diff --git a/apps/portal/src/app/components/header.tsx b/apps/portal/src/app/components/header.tsx index 140f836106..de80f499a7 100644 --- a/apps/portal/src/app/components/header.tsx +++ b/apps/portal/src/app/components/header.tsx @@ -1,21 +1,18 @@ import { UserMenu } from "@/app/components/user-menu"; -import { getI18n } from "@/app/locales/server"; import { Skeleton } from "@bubba/ui/skeleton"; import { Suspense } from "react"; import { MobileMenu } from "./mobile-menu"; export async function Header() { - const t = await getI18n(); + return ( +
+ - return ( -
- - -
- }> - - -
-
- ); +
+ }> + + +
+
+ ); } diff --git a/packages/db/prisma/migrations/20250321155359_remove_unwanted_columns_in_user_since_theyre_in_orgmember/migration.sql b/packages/db/prisma/migrations/20250321155359_remove_unwanted_columns_in_user_since_theyre_in_orgmember/migration.sql new file mode 100644 index 0000000000..f476b432e1 --- /dev/null +++ b/packages/db/prisma/migrations/20250321155359_remove_unwanted_columns_in_user_since_theyre_in_orgmember/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `onboarded` on the `User` table. All the data in the column will be lost. + - You are about to drop the column `role` on the `User` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "User" DROP COLUMN "onboarded", +DROP COLUMN "role"; diff --git a/packages/db/prisma/schema/schema.prisma b/packages/db/prisma/schema/schema.prisma index eeea8e2986..042a90a683 100644 --- a/packages/db/prisma/schema/schema.prisma +++ b/packages/db/prisma/schema/schema.prisma @@ -40,12 +40,10 @@ model Session { } model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? full_name String? - email String? @unique - role Role @default(member) - onboarded Boolean @default(false) + email String? @unique emailVerified DateTime? image String? lastLogin DateTime? @@ -64,7 +62,7 @@ model User { TaskAttachment TaskAttachment[] TaskComments TaskComments[] PolicyComments PolicyComments[] - organization Organization? @relation("CurrentOrganization", fields: [organizationId], references: [id]) + organization Organization? @relation("CurrentOrganization", fields: [organizationId], references: [id]) OrganizationPolicy OrganizationPolicy[] OrganizationIntegrationResults OrganizationIntegrationResults[] IntegrationResultsComments OrganizationIntegrationResultsComments[] @@ -80,18 +78,18 @@ model User { } model Organization { - id String @id @default(cuid()) + id String @id @default(cuid()) stripeCustomerId String? name String - setup Boolean @default(false) + setup Boolean @default(false) website String - tier Tier @default(free) - policiesCreated Boolean @default(false) + tier Tier @default(free) + policiesCreated Boolean @default(false) frameworkId String? Artifact Artifact[] AuditLog AuditLog[] employees Employee[] - Framework Framework? @relation(fields: [frameworkId], references: [id], onDelete: Cascade) + Framework Framework? @relation(fields: [frameworkId], references: [id], onDelete: Cascade) OrganizationCategory OrganizationCategory[] OrganizationControl OrganizationControl[] OrganizationEvidence OrganizationEvidence[] @@ -106,7 +104,7 @@ model Organization { TaskAttachment TaskAttachment[] TaskComments TaskComments[] PolicyComments PolicyComments[] - users User[] @relation("CurrentOrganization") + users User[] @relation("CurrentOrganization") PortalUser PortalUser[] apiKeys OrganizationApiKey[] OrganizationIntegrationResults OrganizationIntegrationResults[] @@ -376,4 +374,3 @@ enum VendorAttachmentType { document other } -