From afb936bc5bdc64e2bcb7b10a07e720d2b0dc6d1f Mon Sep 17 00:00:00 2001 From: killian-larcher Date: Thu, 12 Dec 2024 13:31:11 +0100 Subject: [PATCH] adding organization feature. --- Makefile | 2 + .../dashboard/agents/[agentId]/page.tsx | 3 +- app/(customer)/dashboard/agents/page.tsx | 2 + app/(customer)/dashboard/layout.tsx | 27 +++++---- .../projects/[projectId]/edit/page.tsx | 10 +++- .../dashboard/projects/new/page.tsx | 3 + app/(customer)/dashboard/projects/page.tsx | 13 +++- app/middleware.ts | 11 ++++ docker/dockerfile/Dockerfile | 2 +- package.json | 4 +- .../20241212103214_2024_12_12/migration.sql | 17 ++++++ .../20241212103831_2024_12_12/migration.sql | 32 ++++++++++ .../20241212110141_2024_12_12/migration.sql | 32 ++++++++++ prisma/schema.prisma | 27 ++++++--- src/auth/auth.ts | 29 ++++++--- .../wrappers/Agent/AgentCard/AgentCard.tsx | 3 +- .../Register/RegisterForm/RegisterForm.tsx | 16 ++--- .../RegisterForm/register-form.action.ts | 22 +++++-- .../Projects/ProjectsForm/ProjectForm.tsx | 60 +++++++++++-------- .../ProjectsForm/project-form.action.ts | 4 +- .../Dashboard/SideBar/app-sidebar.tsx | 13 +++- .../Organization/OrganizationCombobox.tsx | 27 +++++---- src/components/wrappers/combobox.tsx | 9 ++- src/features/backup/columns.tsx | 2 +- src/features/restore/restore.action.ts | 4 +- src/state-management/HydratationZustand.tsx | 16 +++++ src/state-management/provider.tsx | 31 ++++++++++ src/state-management/store.ts | 30 ++++++++++ yarn.lock | 5 ++ 29 files changed, 363 insertions(+), 93 deletions(-) create mode 100644 Makefile create mode 100644 app/middleware.ts create mode 100644 prisma/migrations/20241212103214_2024_12_12/migration.sql create mode 100644 prisma/migrations/20241212103831_2024_12_12/migration.sql create mode 100644 prisma/migrations/20241212110141_2024_12_12/migration.sql create mode 100644 src/state-management/HydratationZustand.tsx create mode 100644 src/state-management/provider.tsx create mode 100644 src/state-management/store.ts diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..429121c3 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +migrate: + docker compose run --rm app sh -c "npx prisma migrate dev" \ No newline at end of file diff --git a/app/(customer)/dashboard/agents/[agentId]/page.tsx b/app/(customer)/dashboard/agents/[agentId]/page.tsx index 0724d784..df4694e9 100644 --- a/app/(customer)/dashboard/agents/[agentId]/page.tsx +++ b/app/(customer)/dashboard/agents/[agentId]/page.tsx @@ -13,6 +13,7 @@ import {AgentModalKey} from "@/components/wrappers/Agent/AgentModalKey/AgentModa import {KeyRound} from "lucide-react"; import {BackupButton} from "@/components/wrappers/BackupButton/BackupButton"; import {backups, restaurations} from "@/utils/mock-data"; +import {formatDateLastContact} from "@/utils/date-formatting"; export default async function RoutePage(props: PageParams<{ agentId: string }>) { @@ -85,7 +86,7 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>) Last contact - {agent.lastContact?.toDateString() ?? "Never connected."} + {formatDateLastContact(agent.lastContact)} diff --git a/app/(customer)/dashboard/agents/page.tsx b/app/(customer)/dashboard/agents/page.tsx index 258dc5d7..58a7c133 100644 --- a/app/(customer)/dashboard/agents/page.tsx +++ b/app/(customer)/dashboard/agents/page.tsx @@ -6,9 +6,11 @@ import {Button} from "@/components/ui/button"; import Link from 'next/link' import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; + export default async function RoutePage(props: PageParams<{}>) { const agents = await prisma.agent.findMany({}) + return ( diff --git a/app/(customer)/dashboard/layout.tsx b/app/(customer)/dashboard/layout.tsx index 05a98994..f23fddb7 100644 --- a/app/(customer)/dashboard/layout.tsx +++ b/app/(customer)/dashboard/layout.tsx @@ -6,6 +6,7 @@ import {AppSidebar} from "@/components/wrappers/Dashboard/SideBar/app-sidebar"; import {currentUser} from "@/auth/current-user"; import {Header} from "@/features/layout/Header"; import {prisma} from "@/prisma"; +import {GlobalStoreProvider} from "@/state-management/provider"; export default async function Layout({children}: { children: React.ReactNode }) { @@ -22,16 +23,20 @@ export default async function Layout({children}: { children: React.ReactNode }) if (!user) redirect('/login') return ( - -
- - -
-
- {children} -
- -
-
+ // + + +
+ + +
+
+ {children} +
+ +
+
+
+ //
) } \ No newline at end of file diff --git a/app/(customer)/dashboard/projects/[projectId]/edit/page.tsx b/app/(customer)/dashboard/projects/[projectId]/edit/page.tsx index b835010f..ed55ed63 100644 --- a/app/(customer)/dashboard/projects/[projectId]/edit/page.tsx +++ b/app/(customer)/dashboard/projects/[projectId]/edit/page.tsx @@ -32,10 +32,13 @@ export default async function RoutePage(props: PageParams<{ const availableDatabases = await prisma.database.findMany({ where: { OR: [ - { projectId: null }, - { projectId: project.id }, + {projectId: null}, + {projectId: project.id}, ], }, + include: { + agent: {} + }, orderBy: { createdAt: 'desc', }, @@ -50,7 +53,8 @@ export default async function RoutePage(props: PageParams<{
- +
) diff --git a/app/(customer)/dashboard/projects/new/page.tsx b/app/(customer)/dashboard/projects/new/page.tsx index 17e1a304..33ef8647 100644 --- a/app/(customer)/dashboard/projects/new/page.tsx +++ b/app/(customer)/dashboard/projects/new/page.tsx @@ -10,6 +10,9 @@ export default async function RoutePage(props: PageParams<{}>) { where: { projectId: null }, + include: { + agent: {} + }, orderBy: { createdAt: 'desc', }, diff --git a/app/(customer)/dashboard/projects/page.tsx b/app/(customer)/dashboard/projects/page.tsx index 518cc654..51999067 100644 --- a/app/(customer)/dashboard/projects/page.tsx +++ b/app/(customer)/dashboard/projects/page.tsx @@ -5,17 +5,28 @@ import {Button} from "@/components/ui/button"; import Link from 'next/link' import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {ProjectCard} from "@/components/wrappers/Dashboard/Projects/ProjectCard/ProjectCard"; +import {requiredCurrentUser} from "@/auth/current-user"; export default async function RoutePage(props: PageParams<{}>) { + // const {searchParams} = props + // const organizationId = searchParams?.organizationId || "default"; + + const user = await requiredCurrentUser() const projects = await prisma.project.findMany({ where: { organization: { slug: "default", + // id: organizationId, + users: { + some: { + userId: user.id + }, + }, } }, - include:{ + include: { databases: {} } }) diff --git a/app/middleware.ts b/app/middleware.ts new file mode 100644 index 00000000..7e54a7c4 --- /dev/null +++ b/app/middleware.ts @@ -0,0 +1,11 @@ +// import {NextResponse} from "next/server"; +// import {useStore} from "@/state-management/store"; +// +// export function middleware(req) { +// // const organizationId = req.cookies.get("organizationId") || "default"; // Retrieve from cookies +// const url = req.nextUrl.clone(); +// const {organizationId, moveToAnotherOrganization} = useStore((state) => state); +// console.log("middlewaressss", organizationId); +// // url.searchParams.set("organizationId", organizationId); // Append to query string +// return NextResponse.rewrite(url); +// } \ No newline at end of file diff --git a/docker/dockerfile/Dockerfile b/docker/dockerfile/Dockerfile index f08aa4dd..96518981 100644 --- a/docker/dockerfile/Dockerfile +++ b/docker/dockerfile/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine AS base +FROM node:20.1.0-alpine AS base ENV YARN_VERSION=4.2.2 diff --git a/package.json b/package.json index 2c59cd6e..bf7defe1 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "start": "next start", "lint": "next lint", "email": "email dev" - }, "dependencies": { "@auth/prisma-adapter": "^2.4.1", @@ -76,7 +75,8 @@ "tailwindcss-animate": "^1.0.7", "uuid": "^11.0.3", "vaul": "^1.1.1", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zustand": "^5.0.2" }, "devDependencies": { "@types/node": "^20", diff --git a/prisma/migrations/20241212103214_2024_12_12/migration.sql b/prisma/migrations/20241212103214_2024_12_12/migration.sql new file mode 100644 index 00000000..ddea0021 --- /dev/null +++ b/prisma/migrations/20241212103214_2024_12_12/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "_OrganizationToUser" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_OrganizationToUser_AB_unique" ON "_OrganizationToUser"("A", "B"); + +-- CreateIndex +CREATE INDEX "_OrganizationToUser_B_index" ON "_OrganizationToUser"("B"); + +-- AddForeignKey +ALTER TABLE "_OrganizationToUser" ADD CONSTRAINT "_OrganizationToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_OrganizationToUser" ADD CONSTRAINT "_OrganizationToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20241212103831_2024_12_12/migration.sql b/prisma/migrations/20241212103831_2024_12_12/migration.sql new file mode 100644 index 00000000..d0912c61 --- /dev/null +++ b/prisma/migrations/20241212103831_2024_12_12/migration.sql @@ -0,0 +1,32 @@ +/* + Warnings: + + - You are about to drop the `_OrganizationToUser` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "_OrganizationToUser" DROP CONSTRAINT "_OrganizationToUser_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_OrganizationToUser" DROP CONSTRAINT "_OrganizationToUser_B_fkey"; + +-- DropTable +DROP TABLE "_OrganizationToUser"; + +-- CreateTable +CREATE TABLE "UserOrganization" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "UserOrganization_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserOrganization_userId_organizationId_key" ON "UserOrganization"("userId", "organizationId"); + +-- AddForeignKey +ALTER TABLE "UserOrganization" ADD CONSTRAINT "UserOrganization_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserOrganization" ADD CONSTRAINT "UserOrganization_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20241212110141_2024_12_12/migration.sql b/prisma/migrations/20241212110141_2024_12_12/migration.sql new file mode 100644 index 00000000..4521b67f --- /dev/null +++ b/prisma/migrations/20241212110141_2024_12_12/migration.sql @@ -0,0 +1,32 @@ +/* + Warnings: + + - You are about to drop the `UserOrganization` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "UserOrganization" DROP CONSTRAINT "UserOrganization_organizationId_fkey"; + +-- DropForeignKey +ALTER TABLE "UserOrganization" DROP CONSTRAINT "UserOrganization_userId_fkey"; + +-- DropTable +DROP TABLE "UserOrganization"; + +-- CreateTable +CREATE TABLE "users_organisations" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "users_organisations_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_organisations_userId_organizationId_key" ON "users_organisations"("userId", "organizationId"); + +-- AddForeignKey +ALTER TABLE "users_organisations" ADD CONSTRAINT "users_organisations_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "users_organisations" ADD CONSTRAINT "users_organisations_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c771cdae..1c69c23d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -54,19 +54,20 @@ model VerificationToken { } model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - email String? @unique - emailVerified DateTime? @map("email_verified") + email String? @unique + emailVerified DateTime? @map("email_verified") image String? role Role password String? - authMethod String? @map("auth_method") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt @map("updated_at") - deleted Boolean? @default(false) + authMethod String? @map("auth_method") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime? @updatedAt @map("updated_at") + deleted Boolean? @default(false) accounts Account[] sessions Session[] + organizations UserOrganization[] @@map("users") } @@ -84,10 +85,22 @@ model Organization { createdAt DateTime @default(now()) @map("created_at") projects Project[] + users UserOrganization[] @@map("organizations") } +model UserOrganization { + id String @id @default(cuid()) + userId String + organizationId String + user User @relation(fields: [userId], references: [id]) + organization Organization @relation(fields: [organizationId], references: [id]) + + @@unique([userId, organizationId]) + @@map("users_organisations") +} + model Project { id String @id @default(cuid()) slug String @unique diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 3f225040..879cbc1a 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -6,7 +6,6 @@ import {env} from "@/env.mjs"; import GoogleProvider from "next-auth/providers/google"; - export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({ adapter: PrismaAdapter(prisma), theme: { @@ -79,20 +78,20 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({ // // session.user.authMethod = user.authMethod; // return session // }, - async jwt({ token, trigger, session, user }) { + async jwt({token, trigger, session, user}) { if (trigger === "update" && session) { - return { ...token, ...session?.user }; + return {...token, ...session?.user}; } - return { ...token, ...user }; + return {...token, ...user}; }, - async session({ session, token, user }) { + async session({session, token, user}) { session.user = token; return session; }, - async signIn({ account, user, profile }) { + async signIn({account, user, profile}) { const existingUser = await prisma.user.findFirst({ - where: { email: user.email }, + where: {email: user.email}, }); if (!existingUser) { @@ -106,7 +105,7 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({ }) const role = users.length > 0 ? "pending" : "admin" - await prisma.user.create({ + const newUser = await prisma.user.create({ data: { email: user.email, name: user.name, @@ -115,6 +114,18 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({ authMethod: account.provider, }, }); + + const defaultOrganization = await prisma.organization.findUnique({ + where: {slug: "default"}, + }); + + await prisma.userOrganization.create({ + data: { + userId: newUser.id, + organizationId: defaultOrganization.id + }, + }); + return role !== "pending"; } @@ -125,7 +136,7 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({ // Update auth method if user exists await prisma.user.update({ - where: { id: existingUser.id }, + where: {id: existingUser.id}, data: { authMethod: account.provider, }, diff --git a/src/components/wrappers/Agent/AgentCard/AgentCard.tsx b/src/components/wrappers/Agent/AgentCard/AgentCard.tsx index f29c5224..b97b9c5e 100644 --- a/src/components/wrappers/Agent/AgentCard/AgentCard.tsx +++ b/src/components/wrappers/Agent/AgentCard/AgentCard.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import {ValueIcon} from "@radix-ui/react-icons"; import {Circle} from "lucide-react"; import {ConnectionCircle} from "@/components/wrappers/connection-circle"; +import {formatDateLastContact} from "@/utils/date-formatting"; export type agentCardProps = { data: any @@ -20,7 +21,7 @@ export const AgentCard = (props: agentCardProps) => {
{agent.name} - Last contact : {agent.lastContact?.toDateString() ?? "Never connected."} + Last contact : {formatDateLastContact(agent.lastContact)}
diff --git a/src/components/wrappers/Auth/Register/RegisterForm/RegisterForm.tsx b/src/components/wrappers/Auth/Register/RegisterForm/RegisterForm.tsx index 1758aabf..ffdcc861 100644 --- a/src/components/wrappers/Auth/Register/RegisterForm/RegisterForm.tsx +++ b/src/components/wrappers/Auth/Register/RegisterForm/RegisterForm.tsx @@ -1,19 +1,18 @@ "use client"; +import {useRouter} from "next/navigation"; +import {Info} from "lucide-react"; +import {useMutation} from "@tanstack/react-query"; +import {toast} from "sonner"; + import {Card, CardContent, CardHeader} from "@/components/ui/card"; -import { - FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm -} from "@/components/ui/form"; +import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form"; import {Input} from "@/components/ui/input"; import {Form} from "@/components/ui/form" import {Button} from "@/components/ui/button"; -import {toast} from "sonner"; -import {useRouter} from "next/navigation"; -import {useMutation} from "@tanstack/react-query"; import {TooltipProvider, TooltipTrigger, Tooltip, TooltipContent} from "@/components/ui/tooltip"; import {RegisterSchema, RegisterType} from "@/components/wrappers/Auth/Register/RegisterForm/register-form.schema"; import {registerUserAction} from "@/components/wrappers/Auth/Register/RegisterForm/register-form.action"; -import {Info} from "lucide-react"; import {PasswordInput} from "@/components/wrappers/Auth/PaswordInput/password-input"; export type registerFormProps = { @@ -104,7 +103,8 @@ export const RegisterForm = (props: registerFormProps) => { -

Min. 8 characters, 1 uppercase (A-Z), 1 lowercase (a-z), 1 number (0-9), 1 special character (!, @, etc.)

+

Min. 8 characters, 1 uppercase (A-Z), 1 lowercase (a-z), 1 + number (0-9), 1 special character (!, @, etc.)

diff --git a/src/components/wrappers/Auth/Register/RegisterForm/register-form.action.ts b/src/components/wrappers/Auth/Register/RegisterForm/register-form.action.ts index a7e18805..6a0db2d8 100644 --- a/src/components/wrappers/Auth/Register/RegisterForm/register-form.action.ts +++ b/src/components/wrappers/Auth/Register/RegisterForm/register-form.action.ts @@ -8,18 +8,18 @@ import {hashPassword} from "@/utils/password"; export const registerUserAction = action .schema(RegisterSchema) .action(async ({parsedInput, ctx}) => { - const user = await prisma.user.findUnique({ where: { email: parsedInput.email } }); + const user = await prisma.user.findUnique({where: {email: parsedInput.email}}); console.log(user); if (!user && parsedInput.password === parsedInput.confirmPassword) { const users = await prisma.user.findMany({ - where:{ - deleted: { not: true }, + where: { + deleted: {not: true}, } }) const role = users.length > 0 ? "pending" : "admin" - const new_user = await prisma.user.create({ + const newUser = await prisma.user.create({ data: { name: parsedInput.name, email: parsedInput.email, @@ -27,8 +27,20 @@ export const registerUserAction = action role: role }, }); + + const defaultOrganization = await prisma.organization.findUnique({ + where: {slug: "default"}, + }); + + await prisma.userOrganization.create({ + data: { + userId: newUser.id, + organizationId: defaultOrganization.id + }, + }); + return { - data: new_user, + data: newUser, } } throw new Error('An error occured while creating user'); diff --git a/src/components/wrappers/Dashboard/Projects/ProjectsForm/ProjectForm.tsx b/src/components/wrappers/Dashboard/Projects/ProjectsForm/ProjectForm.tsx index c0699ccd..703c2081 100644 --- a/src/components/wrappers/Dashboard/Projects/ProjectsForm/ProjectForm.tsx +++ b/src/components/wrappers/Dashboard/Projects/ProjectsForm/ProjectForm.tsx @@ -15,13 +15,14 @@ import {Form} from "@/components/ui/form" import {Button} from "@/components/ui/button"; import {useMutation} from "@tanstack/react-query"; import {ProjectSchema, ProjectType} from "@/components/wrappers/Dashboard/Projects/ProjectsForm/ProjectForm.schema"; -import {createProjectAction, updateProjectAction} from "@/components/wrappers/Dashboard/Projects/ProjectsForm/project-form.action"; +import { + createProjectAction, + updateProjectAction +} from "@/components/wrappers/Dashboard/Projects/ProjectsForm/project-form.action"; import {useRouter} from "next/navigation"; import {Database, Organization, Projects} from "@prisma/client" import {MultiSelect} from "@/components/wrappers/MultiSelect/MultiSelect"; -import {ZodString} from "zod"; import {toast} from "sonner"; -import {ServerActionResult} from "@/types/action-type"; export type projectFormProps = { @@ -41,7 +42,7 @@ export const ProjectForm = (props: projectFormProps) => { const formatDatabasesList = (databases: Database[]) => { return databases.map(database => ({ value: database.id, - label: `${database.name} | ${database.generatedId}`, + label: `${database.name} (${database.generatedId}) | ${database.agent.name}`, })); }; @@ -64,14 +65,21 @@ export const ProjectForm = (props: projectFormProps) => { const mutation = useMutation({ mutationFn: async (values: ProjectType) => { console.log(values) - const project: Projects = isCreate ? await createProjectAction({data: values, organizationId: props.organization.id}) : await updateProjectAction({data: values, organizationId: props.organization.id, projectId: props.projectId}); + const project: Projects = isCreate ? await createProjectAction({ + data: values, + organizationId: props.organization.id + }) : await updateProjectAction({ + data: values, + organizationId: props.organization.id, + projectId: props.projectId + }); console.log(project) if (project.data.success) { toast.success(project.data.actionSuccess.message); router.push(`/dashboard/projects/${project.data.value.id}`); router.refresh() - }else{ + } else { toast.success(project.data.actionError.message); } @@ -115,7 +123,11 @@ export const ProjectForm = (props: projectFormProps) => { Slug + placeholder="project-1" {...field} + onChange={(e) => { + const value = e.target.value.replaceAll(" ", "-").toLowerCase() + field.onChange(value) + }}/> @@ -125,24 +137,24 @@ export const ProjectForm = (props: projectFormProps) => { control={form.control} name="databases" render={({field}) => ( - - Databases - + + Databases + - - - Select databases you want to add to this project - - - ) + + + Select databases you want to add to this project + + + ) } />