From ae766dab2fdbe5291e15f79c8faa76fc04016266 Mon Sep 17 00:00:00 2001 From: charlesgauthereau Date: Sat, 1 Nov 2025 14:50:08 +0100 Subject: [PATCH] Refactoring in roles. --- .../database/[databaseId]/page.tsx | 34 +- .../projects/[projectId]/page.tsx | 24 +- .../(organization)/projects/page.tsx | 25 +- .../(organization)/settings/page.tsx | 10 +- .../(organization)/statistics/page.tsx | 141 +- app/layout.tsx | 6 +- .../auth/login/login-form/login-form.tsx | 66 +- .../common/empty-state-placeholder.tsx | 31 +- .../wrappers/dashboard/admin/admin-tabs.tsx | 8 - .../admin/admin-user-tab/admin-user-table.tsx | 7 +- .../admin/admin-user-tab/columns-users.tsx | 28 +- .../dashboard/agent/agent-card/agent-card.tsx | 4 +- .../common/logged-in/logged-in-dropdown.tsx | 2 +- .../common/sidebar/menu-sidebar-main.tsx | 7 +- .../create-organisation-modal.tsx | 21 - .../organization/organization-combobox.tsx | 3 +- .../delete-project.action.ts | 1 + .../database/database-backup-list.tsx | 84 +- .../database/database-restore-list.tsx | 62 +- .../projects/database/database-tabs.tsx | 14 +- .../projects/project-card/project-card.tsx | 2 +- .../project-card/project-database-card.tsx | 2 +- .../project-form/project-form.action.ts | 27 +- .../settings/columns-organization-members.tsx | 73 +- .../settings-organization-members-table.tsx | 14 +- src/db/migrations/0005_old_swarm.sql | 3 + src/db/migrations/meta/0005_snapshot.json | 1355 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + src/db/schema/04_member.ts | 5 +- src/db/schema/06_project.ts | 17 +- src/env.mjs | 4 + src/features/dashboard/backup/columns.tsx | 13 +- src/features/dashboard/restore/columns.tsx | 72 +- src/fonts/fonts.ts | 5 + src/lib/auth/auth.ts | 4 +- src/lib/auth/permissions.ts | 2 + 36 files changed, 1800 insertions(+), 383 deletions(-) create mode 100644 src/db/migrations/0005_old_swarm.sql create mode 100644 src/db/migrations/meta/0005_snapshot.json create mode 100644 src/fonts/fonts.ts diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx index 952bc89e..2c9a8d6e 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx @@ -11,7 +11,7 @@ import {db} from "@/db"; import {eq, and, inArray} from "drizzle-orm"; import * as drizzleDb from "@/db"; import {getOrganizationProjectDatabases} from "@/lib/services"; -import {getOrganization} from "@/lib/auth/auth"; +import {getActiveMember, getOrganization} from "@/lib/auth/auth"; import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet"; import {capitalizeFirstLetter} from "@/utils/text"; @@ -22,8 +22,9 @@ export default async function RoutePage(props: PageParams<{ const {projectId, databaseId} = await props.params; const organization = await getOrganization({}); + const activeMember = await getActiveMember() - if (!organization) { + if (!organization || !activeMember) { notFound(); } @@ -83,6 +84,9 @@ export default async function RoutePage(props: PageParams<{ const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null; + const isMember = activeMember?.role === "member"; + + return (
@@ -90,17 +94,19 @@ export default async function RoutePage(props: PageParams<{
{capitalizeFirstLetter(dbItem.name)}
-
-
- {/* Do not delete*/} - {/**/} - - + {!isMember && ( +
+
+ {/* Do not delete*/} + {/**/} + + +
+
+ +
-
- -
-
+ )}
@@ -108,9 +114,9 @@ export default async function RoutePage(props: PageParams<{ {dbItem.description} )} - - diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx index 1f4ba8f5..4189bec0 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx @@ -12,7 +12,7 @@ import {notFound, redirect} from "next/navigation"; import {db} from "@/db"; import {eq} from "drizzle-orm"; -import {getOrganization} from "@/lib/auth/auth"; +import {getActiveMember, getOrganization} from "@/lib/auth/auth"; import * as drizzleDb from "@/db"; import {capitalizeFirstLetter} from "@/utils/text"; @@ -24,6 +24,8 @@ export default async function RoutePage(props: PageParams<{ } = await props.params; const organization = await getOrganization({}); + const activeMember = await getActiveMember() + if (!organization) { notFound(); } @@ -48,23 +50,27 @@ export default async function RoutePage(props: PageParams<{ redirect("/dashboard/projects"); } + const isMember = activeMember?.role === "member"; return (
{capitalizeFirstLetter(proj.name)} - - - + {!isMember && ( + + + + )} - - - + {!isMember && ( + + + + )}
- The list of associated databases - {proj.databases.length > 0 ? ( ) { +export default async function RoutePage(props: PageParams<{}>) { const organization = await getOrganization({}); + const activeMember = await getActiveMember() if (!organization) { notFound(); @@ -28,12 +29,14 @@ export default async function RoutePage(props: PageParams<{ }>) { databases: true, }, }); + const isMember = activeMember?.role === "member"; + return ( Projects - {projects.length > 0 && ( + {(projects.length > 0 && !isMember) && ( @@ -44,13 +47,17 @@ export default async function RoutePage(props: PageParams<{ }>) { {projects.length > 0 ? ( - - ) : ( - + ) : isMember ? ( + + ) : ( + )} diff --git a/app/(customer)/dashboard/(organization)/settings/page.tsx b/app/(customer)/dashboard/(organization)/settings/page.tsx index 0c2db7bb..a7836f4c 100644 --- a/app/(customer)/dashboard/(organization)/settings/page.tsx +++ b/app/(customer)/dashboard/(organization)/settings/page.tsx @@ -22,10 +22,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) { } const isMember = activeMember?.role === "member"; - - if (isMember) { - notFound(); - } + const isOwner = activeMember?.role === "owner"; return ( @@ -37,14 +34,11 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) { )} - {!isMember && organization.slug !== "default" && ( + {isOwner && organization.slug !== "default" && ( )} - {/**/} - {/* Manage your organization settings.*/} - {/**/} diff --git a/app/(customer)/dashboard/(organization)/statistics/page.tsx b/app/(customer)/dashboard/(organization)/statistics/page.tsx index 2576ae24..c7224563 100644 --- a/app/(customer)/dashboard/(organization)/statistics/page.tsx +++ b/app/(customer)/dashboard/(organization)/statistics/page.tsx @@ -8,7 +8,7 @@ import {db} from "@/db"; import {and, asc, count, eq, inArray} from "drizzle-orm"; import * as drizzleDb from "@/db"; import {getOrganization} from "@/lib/auth/auth"; -import {DatabaseBackup, Folder, RefreshCcw} from "lucide-react"; +import {Building2, DatabaseBackup, Folder, RefreshCcw} from "lucide-react"; export default async function RoutePage(props: PageParams<{}>) { const organization = await getOrganization({}); @@ -45,79 +45,6 @@ export default async function RoutePage(props: PageParams<{}>) { }); - - // const tomorrow = new Date(); - // tomorrow.setDate(tomorrow.getDate() + 1); - // - // const before = new Date(); - // before.setDate(before.getDate() - 1); - // - // const backupsEvolution = [ - // { - // id: '22e84aa4-228c-45b3-82ec-846a639cd509', - // createdAt: new Date() - // }, - // { - // id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331', - // createdAt: new Date() - // }, - // { - // id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331', - // createdAt: new Date() - // }, - // { - // id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331', - // createdAt: new Date() - // }, - // { - // id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331', - // createdAt: new Date(before) - // }, - // { - // id: '6a6106fe-7f45-48eb-a56f-0a1e734126a1', - // createdAt: new Date(before) - // }, - // { - // id: 'a529d790-502e-4609-ad37-9b1c00c73477', - // createdAt: new Date() - // }, - // { - // id: 'a8c105a8-3e29-423e-b7dd-d3218092cde1', - // createdAt: new Date() - // }, - // { - // id: 'd33aaf4f-8525-4490-addb-12e3c8650d6d', - // createdAt: new Date() - // }, - // { - // id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d', - // createdAt: new Date(tomorrow) - // }, - // { - // id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d', - // createdAt: new Date(tomorrow) - // }, - // { - // id: 'e88a588a-2353-4470-9976-8c3eb2ffc88d', - // createdAt: new Date() - // }, - // { - // id: 'ee11441e-4b41-4c1b-9d91-929565b4204a', - // createdAt: new Date() - // }, - // - // // Entries with tomorrow's date - // { - // id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d', - // createdAt: new Date(tomorrow) - // }, - // { - // id: 'c0a8323d-9241-4896-9e64-01e905c24e51', - // createdAt: new Date(tomorrow) - // } - // ]; - - const backupsRate = await db .select({ createdAt: drizzleDb.schemas.backup.createdAt, @@ -139,60 +66,84 @@ export default async function RoutePage(props: PageParams<{}>) { .where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds)); - const restorationsCount = restorationsCountResult[0]?.count ?? 0; const projectsCount = projects.length; const backupsEvolutionCount = backupsEvolution.length; - const sortedBackupsEvolution = backupsEvolution.sort( (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ); + const Placeholder = ({text}: {text: string}) => ( +
{text}
+ ); + return ( - Statistics + Statistics Overview + -
- - - - Projects +
+ + + Projects + - {projectsCount} + +
{projectsCount}
+

Active projects in this organization

+
- - - - Backups + + + + Backups + - {backupsEvolutionCount} + +
{backupsEvolutionCount}
+

Total backups executed across all databases

+
- - - - Restorations + + + + Restorations + - {restorationsCount} + +
{restorationsCount}
+

Total restoration operations performed

+
+
Evolution of the number of backups - + {sortedBackupsEvolution.length > 0 ? ( + + ) : ( + + )} + Success rate of backups - + {backupsRate.length > 0 ? ( + + ) : ( + + )}
diff --git a/app/layout.tsx b/app/layout.tsx index 452322fa..834c9064 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,15 +1,13 @@ import React from "react"; import type {Metadata} from "next"; -import {Inter} from "next/font/google"; import "./globals.css"; import {Providers} from "./providers"; import {cn} from "@/lib/utils"; import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer"; - -const inter = Inter({subsets: ["latin"]}); +import {inter} from "@/fonts/fonts"; export const metadata: Metadata = { - title: process.env.NEXT_PUBLIC_PROJECT_NAME ?? "App Title", + title: process.env.NEXT_PUBLIC_PROJECT_NAME ?? "Portabase", description: process.env.NEXT_PUBLIC_PROJECT_DESCRIPTION ?? undefined, }; diff --git a/src/components/wrappers/auth/login/login-form/login-form.tsx b/src/components/wrappers/auth/login/login-form/login-form.tsx index 80aee184..f1c1228c 100644 --- a/src/components/wrappers/auth/login/login-form/login-form.tsx +++ b/src/components/wrappers/auth/login/login-form/login-form.tsx @@ -1,20 +1,21 @@ "use client"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -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 { useMutation } from "@tanstack/react-query"; -import { TooltipProvider } from "@/components/ui/tooltip"; +import {Card, CardContent, CardHeader} from "@/components/ui/card"; +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 {useMutation} from "@tanstack/react-query"; +import {TooltipProvider} from "@/components/ui/tooltip"; import Link from "next/link"; -import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input"; -import { LoginSchema, LoginType } from "@/components/wrappers/auth/login/login-form/login-form.schema"; -import { SocialAuthButton, SocialProviderType } from "@/components/wrappers/auth/login/button-auth/social-auth-button"; -import { signIn } from "@/lib/auth/auth-client"; -import { useRouter } from "next/navigation"; -import { Icon } from "@iconify/react"; +import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input"; +import {LoginSchema, LoginType} from "@/components/wrappers/auth/login/login-form/login-form.schema"; +import {SocialAuthButton, SocialProviderType} from "@/components/wrappers/auth/login/button-auth/social-auth-button"; +import {signIn} from "@/lib/auth/auth-client"; +import {useRouter} from "next/navigation"; +import {Icon} from "@iconify/react"; +import {env} from "@/env.mjs"; export type loginFormProps = { defaultValues?: LoginType; @@ -29,7 +30,7 @@ export const LoginForm = (props: loginFormProps) => { const mutation = useMutation({ mutationFn: async (values: LoginType) => { - const { error } = await signIn.email(values, { + const {error} = await signIn.email(values, { onSuccess: () => { toast.success("Login success"); router.push("/dashboard/profile"); @@ -41,13 +42,18 @@ export const LoginForm = (props: loginFormProps) => { }, }); - const availableProviders: SocialProviderType[] = [ - { - id: "google", - name: "Google", - icon: , - }, - ]; + const availableProviders: SocialProviderType[] = []; + + if (env.NEXT_PUBLIC_GOOGLE_AUTH) { + availableProviders.push( + { + id: "google", + name: "Google", + icon: , + }, + ) + } + return ( @@ -70,13 +76,14 @@ export const LoginForm = (props: loginFormProps) => { control={form.control} name="email" defaultValue="" - render={({ field }) => ( + render={({field}) => ( Email - + - + )} /> @@ -84,7 +91,7 @@ export const LoginForm = (props: loginFormProps) => { control={form.control} name="password" defaultValue="" - render={({ field }) => ( + render={({field}) => (
Password @@ -93,9 +100,10 @@ export const LoginForm = (props: loginFormProps) => { */}
- + - +
)} /> @@ -107,7 +115,7 @@ export const LoginForm = (props: loginFormProps) => {
- + diff --git a/src/components/wrappers/common/empty-state-placeholder.tsx b/src/components/wrappers/common/empty-state-placeholder.tsx index 47b04b9e..9237d9ee 100644 --- a/src/components/wrappers/common/empty-state-placeholder.tsx +++ b/src/components/wrappers/common/empty-state-placeholder.tsx @@ -3,22 +3,29 @@ import {cn} from "@/lib/utils"; import {Plus} from "lucide-react"; type EmptyStatePlaceholderProps = { - url: string; + url?: string; text: string; } - export const EmptyStatePlaceholder = ({url, text}: EmptyStatePlaceholderProps) => { return ( - - - {text} - + <>{url ? + + + {text} + + : +
+

{text}

+
+ } + + ) } \ No newline at end of file diff --git a/src/components/wrappers/dashboard/admin/admin-tabs.tsx b/src/components/wrappers/dashboard/admin/admin-tabs.tsx index 6b670f42..0de9b75c 100644 --- a/src/components/wrappers/dashboard/admin/admin-tabs.tsx +++ b/src/components/wrappers/dashboard/admin/admin-tabs.tsx @@ -8,7 +8,6 @@ import {Setting} from "@/db/schema/01_setting"; import {useEffect, useState} from "react"; import {useRouter, useSearchParams} from "next/navigation"; import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table"; -import {AdminSettingsTab} from "@/components/wrappers/dashboard/admin/admin-settings-tab/admin-settings-tab"; export type AdminTabsProps = { users: UserWithAccounts[]; @@ -42,11 +41,7 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => { Storage - - Settings - - @@ -56,9 +51,6 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => { - - - ); }; diff --git a/src/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table.tsx b/src/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table.tsx index 02aa4bec..6cf77f4a 100644 --- a/src/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table.tsx +++ b/src/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table.tsx @@ -1,4 +1,4 @@ -import {User, UserWithAccounts} from "@/db/schema/02_user"; +import {UserWithAccounts} from "@/db/schema/02_user"; import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"; import {DataTable} from "@/components/wrappers/common/table/data-table"; import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/admin-user-tab/columns-users"; @@ -18,7 +18,10 @@ export const AdminUsersTable = (props: AdminUsersTableProps) => { Manage your users - +
diff --git a/src/components/wrappers/dashboard/admin/admin-user-tab/columns-users.tsx b/src/components/wrappers/dashboard/admin/admin-user-tab/columns-users.tsx index 4661a624..4dd786a1 100644 --- a/src/components/wrappers/dashboard/admin/admin-user-tab/columns-users.tsx +++ b/src/components/wrappers/dashboard/admin/admin-user-tab/columns-users.tsx @@ -72,7 +72,7 @@ export const usersColumnsAdmin: ColumnDef[] = [ accessorKey: "accounts", header: "Provider ID", cell: ({row}) => { - return( + return (
{row.original.accounts.map((item) => (
@@ -107,18 +107,20 @@ export const usersColumnsAdmin: ColumnDef[] = [ }); return ( -
- } - onClick={async () => { - await mutation.mutateAsync(); - }} - size="sm" - /> -
+ <> +
+ } + onClick={async () => { + await mutation.mutateAsync(); + }} + size="sm" + /> +
+ ); }, }, diff --git a/src/components/wrappers/dashboard/agent/agent-card/agent-card.tsx b/src/components/wrappers/dashboard/agent/agent-card/agent-card.tsx index 841084b1..2046eeb3 100644 --- a/src/components/wrappers/dashboard/agent/agent-card/agent-card.tsx +++ b/src/components/wrappers/dashboard/agent/agent-card/agent-card.tsx @@ -14,7 +14,9 @@ export const AgentCard = (props: agentCardProps) => { const { data: agent } = props; return ( - +
{agent.name} diff --git a/src/components/wrappers/dashboard/common/logged-in/logged-in-dropdown.tsx b/src/components/wrappers/dashboard/common/logged-in/logged-in-dropdown.tsx index 9f050051..5705895a 100644 --- a/src/components/wrappers/dashboard/common/logged-in/logged-in-dropdown.tsx +++ b/src/components/wrappers/dashboard/common/logged-in/logged-in-dropdown.tsx @@ -18,7 +18,7 @@ export const LoggedInDropdown = (props: LoggedInDropdownProps) => { return ( {props.children} - + { redirect("/dashboard/profile"); diff --git a/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx b/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx index 58170dc8..6e84fe42 100644 --- a/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx +++ b/src/components/wrappers/dashboard/common/sidebar/menu-sidebar-main.tsx @@ -29,11 +29,12 @@ export const SidebarMenuCustomMain = () => { const groupContent: SidebarGroupItem["group_content"] = [ { title: "Projects", url: "/projects", icon: Layers, details:true }, { title: "Statistics", url: "/statistics", icon: ChartArea }, + { title: "Settings", url: "/settings", icon: Settings, details:true } ]; - if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) { - groupContent.push({ title: "Settings", url: "/settings", icon: Settings, details:true }); - } + // if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) { + // groupContent.push({ title: "Settings", url: "/settings", icon: Settings, details:true }); + // } const items: SidebarGroupItem[] = [ { diff --git a/src/components/wrappers/dashboard/organization/create-organisation-modal.tsx b/src/components/wrappers/dashboard/organization/create-organisation-modal.tsx index facca6e2..7231ec04 100644 --- a/src/components/wrappers/dashboard/organization/create-organisation-modal.tsx +++ b/src/components/wrappers/dashboard/organization/create-organisation-modal.tsx @@ -41,7 +41,6 @@ export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createO await authClient.organization.setActive({organizationSlug: result.data.value.slug}); onSuccess?.(); toast.success(result.data.actionSuccess?.message || "Organization Created."); - // router.push("/"); router.replace(`/dashboard/home`); } else { // @ts-ignore @@ -82,26 +81,6 @@ export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createO )} /> - {/* (*/} - {/* */} - {/* Slug*/} - {/* */} - {/* {*/} - {/* const value = e.target.value.replaceAll(" ", "-").toLowerCase();*/} - {/* field.onChange(value);*/} - {/* }}*/} - {/* />*/} - {/* */} - {/* */} - {/* */} - {/* )}*/} - {/*/>*/}
diff --git a/src/components/wrappers/dashboard/organization/organization-combobox.tsx b/src/components/wrappers/dashboard/organization/organization-combobox.tsx index 59b5ec17..e601e15a 100644 --- a/src/components/wrappers/dashboard/organization/organization-combobox.tsx +++ b/src/components/wrappers/dashboard/organization/organization-combobox.tsx @@ -39,5 +39,6 @@ export function OrganizationCombobox() { return <>{state === "expanded" && }; + reload={handleReset}/>} + ; } diff --git a/src/components/wrappers/dashboard/projects/button-delete-project/delete-project.action.ts b/src/components/wrappers/dashboard/projects/button-delete-project/delete-project.action.ts index 176226bb..a8e96cf8 100644 --- a/src/components/wrappers/dashboard/projects/button-delete-project/delete-project.action.ts +++ b/src/components/wrappers/dashboard/projects/button-delete-project/delete-project.action.ts @@ -23,6 +23,7 @@ export const deleteProjectAction = userAction.schema(z.string()).action(async ({ .set({ isArchived: true, slug: uuid, + name: uuid, }) .where(eq(drizzleDb.schemas.project.id, parsedInput)) .returning(); diff --git a/src/components/wrappers/dashboard/projects/database/database-backup-list.tsx b/src/components/wrappers/dashboard/projects/database/database-backup-list.tsx index 3245e3d9..d0f2c5c4 100644 --- a/src/components/wrappers/dashboard/projects/database/database-backup-list.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-backup-list.tsx @@ -12,6 +12,7 @@ import {useMutation} from "@tanstack/react-query"; import {deleteBackupAction} from "@/features/dashboard/restore/restore.action"; import {toast} from "sonner"; import {useRouter} from "next/navigation"; +import {MemberWithUser} from "@/db/schema/03_organization"; type DatabaseBackupListProps = { @@ -19,6 +20,7 @@ type DatabaseBackupListProps = { settings: Setting; database: DatabaseWith; backups: Backup[]; + activeMember: MemberWithUser } @@ -69,7 +71,7 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => { backupId: backup.id, databaseId: backup.databaseId, status: backup.status, - file: backup.file!, + file: backup.file ?? "", projectSlug: props.database?.project?.slug! }); return { @@ -97,50 +99,58 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => { }, }); + const isMember = props.activeMember.role === "member"; return ( ( -
-
- - - { + <> - }} - disabled={rows.length === 0 || mutationDeleteBackups.isPending} - icon={} - isPending={mutationDeleteBackups.isPending} - size="sm" +
+
+ {!isMember && ( + + + { + + }} + disabled={rows.length === 0 || mutationDeleteBackups.isPending} + icon={} + isPending={mutationDeleteBackups.isPending} + size="sm" + /> + + + { + console.log("Deleting rows:", rows) + await mutationDeleteBackups.mutateAsync(rows) + }} + className="text-red-600 focus:text-red-700" + > + + Delete Selected + + + + )} + - - - { - console.log("Deleting rows:", rows) - await mutationDeleteBackups.mutateAsync(rows) - }} - className="text-red-600 focus:text-red-700" - > - - Delete Selected - - - - -
-
+
+
+ + )} /> ) diff --git a/src/components/wrappers/dashboard/projects/database/database-restore-list.tsx b/src/components/wrappers/dashboard/projects/database/database-restore-list.tsx index a9378dfa..16a80b8e 100644 --- a/src/components/wrappers/dashboard/projects/database/database-restore-list.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-restore-list.tsx @@ -9,11 +9,13 @@ import {useMutation} from "@tanstack/react-query"; import {deleteRestoreAction} from "@/features/dashboard/restore/restore.action"; import {toast} from "sonner"; import {useRouter} from "next/navigation"; +import {MemberWithUser} from "@/db/schema/03_organization"; type DatabaseRestoreListProps = { isAlreadyRestore: boolean; restorations: Restoration[]; + activeMember: MemberWithUser } export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => { @@ -47,40 +49,46 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => { router.refresh(); }, }); + const isMember = props.activeMember.role === "member"; return ( ( - - - { - }} - disabled={rows.length === 0 || mutationDeleteRestorations.isPending} - icon={} - isPending={mutationDeleteRestorations.isPending} - size="sm" - /> - - - { - await mutationDeleteRestorations.mutateAsync(rows) - }} - disabled={props.isAlreadyRestore} - className="text-red-600 focus:text-red-700" - > - - Delete Selected - - - + <> + {!isMember && ( + + + { + }} + disabled={rows.length === 0 || mutationDeleteRestorations.isPending} + icon={} + isPending={mutationDeleteRestorations.isPending} + size="sm" + /> + + + { + await mutationDeleteRestorations.mutateAsync(rows) + }} + disabled={props.isAlreadyRestore} + className="text-red-600 focus:text-red-700" + > + + Delete Selected + + + + )} + )} /> ) diff --git a/src/components/wrappers/dashboard/projects/database/database-tabs.tsx b/src/components/wrappers/dashboard/projects/database/database-tabs.tsx index f623d8d7..39848f0e 100644 --- a/src/components/wrappers/dashboard/projects/database/database-tabs.tsx +++ b/src/components/wrappers/dashboard/projects/database/database-tabs.tsx @@ -8,13 +8,15 @@ import {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/07_databa import {Setting} from "@/db/schema/01_setting"; import {DatabaseBackupList} from "@/components/wrappers/dashboard/projects/database/database-backup-list"; import {DatabaseRestoreList} from "@/components/wrappers/dashboard/projects/database/database-restore-list"; +import {MemberWithUser} from "@/db/schema/03_organization"; export type DatabaseTabsProps = { - settings: Setting - backups: Backup[]; - restorations: Restoration[]; - isAlreadyRestore: boolean; - database: DatabaseWith; + settings: Setting, + backups: Backup[], + restorations: Restoration[], + isAlreadyRestore: boolean, + database: DatabaseWith, + activeMember: MemberWithUser }; export const DatabaseTabs = (props: DatabaseTabsProps) => { @@ -58,12 +60,14 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => { settings={props.settings} database={props.database} backups={props.backups} + activeMember={props.activeMember} /> diff --git a/src/components/wrappers/dashboard/projects/project-card/project-card.tsx b/src/components/wrappers/dashboard/projects/project-card/project-card.tsx index 410e1716..49d78da6 100644 --- a/src/components/wrappers/dashboard/projects/project-card/project-card.tsx +++ b/src/components/wrappers/dashboard/projects/project-card/project-card.tsx @@ -15,7 +15,7 @@ export const ProjectCard = (props: projectCardProps) => { return (
diff --git a/src/components/wrappers/dashboard/projects/project-card/project-database-card.tsx b/src/components/wrappers/dashboard/projects/project-card/project-database-card.tsx index 00f244d4..daa217e2 100644 --- a/src/components/wrappers/dashboard/projects/project-card/project-database-card.tsx +++ b/src/components/wrappers/dashboard/projects/project-card/project-database-card.tsx @@ -18,7 +18,7 @@ export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => { const { organizationSlug, data: database, extendedProps: extendedProps } = props; return ( - + ); diff --git a/src/components/wrappers/dashboard/projects/project-form/project-form.action.ts b/src/components/wrappers/dashboard/projects/project-form/project-form.action.ts index ec4ff2f8..2f8ddca6 100644 --- a/src/components/wrappers/dashboard/projects/project-form/project-form.action.ts +++ b/src/components/wrappers/dashboard/projects/project-form/project-form.action.ts @@ -5,7 +5,7 @@ import { ProjectSchema } from "@/components/wrappers/dashboard/projects/project- import { z } from "zod"; import { ServerActionResult } from "@/types/action-type"; import { db } from "@/db"; -import { eq, inArray } from "drizzle-orm"; +import {and, eq, inArray} from "drizzle-orm"; import {Project} from "@/db/schema/06_project"; import * as drizzleDb from "@/db"; import {Database} from "@/db/schema/07_database"; @@ -21,6 +21,22 @@ export const createProjectAction = userAction .action(async ({ parsedInput }): Promise> => { try { const slug = slugify(parsedInput.data.name); + + const existingProject = await db.query.project.findFirst({ + where: and(eq(drizzleDb.schemas.project.name, parsedInput.data.name) ), + }) + + if (existingProject) { + return { + success: false, + actionError: { + message: "A project with this name already exists.", + status: 400, + messageParams: { projectName: parsedInput.data.name }, + }, + }; + } + const [createdProject] = await db .insert(drizzleDb.schemas.project) .values({ @@ -31,7 +47,10 @@ export const createProjectAction = userAction .returning(); if (parsedInput.data.databases.length > 0) { - await db.update(drizzleDb.schemas.database).set({ projectId: createdProject.id }).where(inArray(drizzleDb.schemas.database.id, parsedInput.data.databases)); + await db + .update(drizzleDb.schemas.database) + .set({ projectId: createdProject.id }) + .where(inArray(drizzleDb.schemas.database.id, parsedInput.data.databases)); } return { @@ -43,6 +62,7 @@ export const createProjectAction = userAction }, }; } catch (error) { + console.log(error); return { success: false, actionError: { @@ -55,6 +75,9 @@ export const createProjectAction = userAction } }); + + + export const updateProjectAction = userAction .schema( z.object({ diff --git a/src/components/wrappers/dashboard/settings/columns-organization-members.tsx b/src/components/wrappers/dashboard/settings/columns-organization-members.tsx index 9c014597..4f4e83c4 100644 --- a/src/components/wrappers/dashboard/settings/columns-organization-members.tsx +++ b/src/components/wrappers/dashboard/settings/columns-organization-members.tsx @@ -1,15 +1,20 @@ "use client"; -import {ColumnDef} from "@tanstack/react-table"; -import {MemberWithUser} from "@/db/schema/03_organization"; -import {useState} from "react"; -import {authClient, useSession} from "@/lib/auth/auth-client"; -import {useMutation} from "@tanstack/react-query"; -import {toast} from "sonner"; -import {Badge} from "@/components/ui/badge"; -import {updateMemberRoleAction} from "@/components/wrappers/dashboard/settings/update-member.action"; -import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema"; - +import { ColumnDef } from "@tanstack/react-table"; +import { MemberWithUser } from "@/db/schema/03_organization"; +import { useState } from "react"; +import { authClient, useSession } from "@/lib/auth/auth-client"; +import { useMutation } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { updateMemberRoleAction } from "@/components/wrappers/dashboard/settings/update-member.action"; +import { RoleSchemaMember } from "@/components/wrappers/dashboard/settings/member.schema"; export const organizationMemberColumns: ColumnDef[] = [ { @@ -18,7 +23,7 @@ export const organizationMemberColumns: ColumnDef[] = [ cell: ({ row }) => { const [role, setRole] = useState(row.getValue("role")); const { data: session } = useSession(); - + const activeOrgaMember = authClient.useActiveMember(); const updateMutation = useMutation({ mutationFn: () => @@ -28,36 +33,59 @@ export const organizationMemberColumns: ColumnDef[] = [ role: RoleSchemaMember.parse(role), }), onSuccess: () => { - toast.success(`User updated successfully.`); + toast.success("User updated successfully."); }, onError: () => { - toast.error(`An error occurred while updating user information.`); + toast.error("An error occurred while updating user information."); }, }); + // Only allow cycling between admin <-> member const handleUpdateRole = async () => { - const nextRole = - role === "owner" ? "admin" : role === "admin" ? "member" : "owner"; + const nextRole = role === "admin" ? "member" : "admin"; setRole(nextRole); await updateMutation.mutateAsync(); }; - const isCurrentUser = session?.user.email === row.original.user.email; - const isMember = session?.user.role === "member"; + const isMember = activeOrgaMember.data?.role === "member"; + const isRowRoleOwner = role === "owner"; + const isDisabled = isMember || isCurrentUser || isRowRoleOwner; - const isDisabled = isMember || isCurrentUser; + // Dynamic tooltip reason + const disabledReason = isCurrentUser + ? "You cannot change your own role" + : isRowRoleOwner + ? "Owner role cannot be modified" + : "Members cannot edit roles"; - return ( + const badge = ( {role} ); + + return isDisabled ? ( + + + {badge} + +

{disabledReason}

+
+
+
+ ) : ( + badge + ); }, }, { @@ -67,6 +95,5 @@ export const organizationMemberColumns: ColumnDef[] = [ { accessorKey: "user.email", header: "Email", - } - -]; + }, +]; \ No newline at end of file diff --git a/src/components/wrappers/dashboard/settings/settings-organization-members-table.tsx b/src/components/wrappers/dashboard/settings/settings-organization-members-table.tsx index ab878203..b2a75c97 100644 --- a/src/components/wrappers/dashboard/settings/settings-organization-members-table.tsx +++ b/src/components/wrappers/dashboard/settings/settings-organization-members-table.tsx @@ -1,7 +1,5 @@ import {DataTable} from "@/components/wrappers/common/table/data-table"; -import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/columns-users"; import {MemberWithUser, Organization, OrganizationWithMembers} from "@/db/schema/03_organization"; -import {OrganizationInvitation} from "@/db/schema/05_invitation"; import {organizationMemberColumns} from "@/components/wrappers/dashboard/settings/columns-organization-members"; @@ -11,12 +9,12 @@ interface SettingsOrganizationMembersTableProps { export const SettingsOrganizationMembersTable = ({organization}: SettingsOrganizationMembersTableProps) => { return ( -
-
-

List of Organization members

-
-
- +
+
+
) diff --git a/src/db/migrations/0005_old_swarm.sql b/src/db/migrations/0005_old_swarm.sql new file mode 100644 index 00000000..6c024b6a --- /dev/null +++ b/src/db/migrations/0005_old_swarm.sql @@ -0,0 +1,3 @@ +ALTER TABLE "projects" DROP CONSTRAINT "projects_organization_id_organization_id_fk"; +--> statement-breakpoint +ALTER TABLE "projects" ADD CONSTRAINT "projects_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/src/db/migrations/meta/0005_snapshot.json b/src/db/migrations/meta/0005_snapshot.json new file mode 100644 index 00000000..d8686f6a --- /dev/null +++ b/src/db/migrations/meta/0005_snapshot.json @@ -0,0 +1,1355 @@ +{ + "id": "6586f8b1-3b59-411b-8aef-1531e3ff2a9f", + "prevId": "d4bb4ba2-45ee-4e43-a340-80dd8fc4f8c7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "storage": { + "name": "storage", + "type": "type_storage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "s3_endpoint_url": { + "name": "s3_endpoint_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "s3_access_key_id": { + "name": "s3_access_key_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "s3_secret_access_key": { + "name": "s3_secret_access_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "s3_bucket_name": { + "name": "s3_bucket_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_password": { + "name": "smtp_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_from": { + "name": "smtp_from", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_host": { + "name": "smtp_host", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_port": { + "name": "smtp_port", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_user": { + "name": "smtp_user", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_name_unique": { + "name": "settings_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_slug_unique": { + "name": "projects_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backups": { + "name": "backups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "file": { + "name": "file", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backups_database_id_databases_id_fk": { + "name": "backups_database_id_databases_id_fk", + "tableFrom": "backups", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.databases": { + "name": "databases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_database_id": { + "name": "agent_database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dbms": { + "name": "dbms", + "type": "dbms_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backup_policy": { + "name": "backup_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_waiting_for_backup": { + "name": "is_waiting_for_backup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backup_to_restore": { + "name": "backup_to_restore", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_agent_id_agents_id_fk": { + "name": "databases_agent_id_agents_id_fk", + "tableFrom": "databases", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restorations": { + "name": "restorations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "restorations_backup_id_backups_id_fk": { + "name": "restorations_backup_id_backups_id_fk", + "tableFrom": "restorations", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_database_id_databases_id_fk": { + "name": "restorations_database_id_databases_id_fk", + "tableFrom": "restorations", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.retention_policies": { + "name": "retention_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "retention_policy_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "days": { + "name": "days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "gfs_daily": { + "name": "gfs_daily", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "gfs_weekly": { + "name": "gfs_weekly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 4 + }, + "gfs_monthly": { + "name": "gfs_monthly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 12 + }, + "gfs_yearly": { + "name": "gfs_yearly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "retention_policies_database_id_databases_id_fk": { + "name": "retention_policies_database_id_databases_id_fk", + "tableFrom": "retention_policies", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agents_slug_unique": { + "name": "agents_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.retention_policy_type": { + "name": "retention_policy_type", + "schema": "public", + "values": [ + "count", + "days", + "gfs" + ] + }, + "public.dbms_status": { + "name": "dbms_status", + "schema": "public", + "values": [ + "postgresql", + "mongodb" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "waiting", + "ongoing", + "failed", + "success" + ] + }, + "public.type_storage": { + "name": "type_storage", + "schema": "public", + "values": [ + "local", + "s3" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index d227402b..ad8c77c8 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1756473637441, "tag": "0004_dazzling_hawkeye", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1762004436821, + "tag": "0005_old_swarm", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/04_member.ts b/src/db/schema/04_member.ts index 36ff8d91..8b7f53c2 100644 --- a/src/db/schema/04_member.ts +++ b/src/db/schema/04_member.ts @@ -1,5 +1,5 @@ import {pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core"; -import {user} from "@/db/schema/02_user"; +import {User, user} from "@/db/schema/02_user"; import {organization} from "@/db/schema/03_organization"; import {relations} from "drizzle-orm"; import {createSelectSchema} from "drizzle-zod"; @@ -32,4 +32,5 @@ export const memberRelations = relations(member, ({ one }) => ({ export const organizationMemberSchema = createSelectSchema(member); -export type OrganizationMember = z.infer; \ No newline at end of file +export type OrganizationMember = z.infer; + diff --git a/src/db/schema/06_project.ts b/src/db/schema/06_project.ts index 6e1ccdb9..6f396f99 100644 --- a/src/db/schema/06_project.ts +++ b/src/db/schema/06_project.ts @@ -1,9 +1,9 @@ -import { pgTable, text, boolean, uuid, timestamp } from "drizzle-orm/pg-core"; -import { relations } from "drizzle-orm"; -import { Organization, organization } from "./03_organization"; -import { createSelectSchema } from "drizzle-zod"; -import { z } from "zod"; -import { Database, database } from "./07_database"; +import {pgTable, text, boolean, uuid} from "drizzle-orm/pg-core"; +import {relations} from "drizzle-orm"; +import {Organization, organization} from "./03_organization"; +import {createSelectSchema} from "drizzle-zod"; +import {z} from "zod"; +import {Database, database} from "./07_database"; import {timestamps} from "@/db/schema/00_common"; export const project = pgTable("projects", { @@ -13,12 +13,11 @@ export const project = pgTable("projects", { isArchived: boolean("is_archived").default(false), organizationId: uuid("organization_id") .notNull() - .references(() => organization.id), + .references(() => organization.id, {onDelete: "cascade"}), ...timestamps - }); -export const projectRelations = relations(project, ({ one, many }) => ({ +export const projectRelations = relations(project, ({one, many}) => ({ organization: one(organization, { fields: [project.organizationId], references: [organization.id], diff --git a/src/env.mjs b/src/env.mjs index 6ef049dc..58751d1c 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -22,6 +22,7 @@ export const env = createEnv({ AUTH_GOOGLE_ID: z.string().optional(), AUTH_GOOGLE_SECRET: z.string().optional(), + NEXT_PUBLIC_GOOGLE_AUTH: z.boolean().default(false).optional(), S3_ENDPOINT: z.string().optional(), S3_ACCESS_KEY: z.string().optional(), @@ -41,6 +42,8 @@ export const env = createEnv({ NEXT_PUBLIC_PROJECT_DESCRIPTION: z.string().optional(), NEXT_PUBLIC_PROJECT_URL: z.string().optional(), NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), + + NEXT_PUBLIC_GOOGLE_AUTH: z.boolean().default(false).optional(), }, runtimeEnv: { NEXT_PUBLIC_PROJECT_NAME: process.env.NEXT_PUBLIC_PROJECT_NAME, @@ -59,6 +62,7 @@ export const env = createEnv({ AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, + NEXT_PUBLIC_GOOGLE_AUTH: process.env.NEXT_PUBLIC_GOOGLE_AUTH === "true", S3_ENDPOINT: process.env.S3_ENDPOINT, S3_ACCESS_KEY: process.env.S3_ACCESS_KEY, diff --git a/src/features/dashboard/backup/columns.tsx b/src/features/dashboard/backup/columns.tsx index 1e0b700c..ea222b33 100644 --- a/src/features/dashboard/backup/columns.tsx +++ b/src/features/dashboard/backup/columns.tsx @@ -30,9 +30,15 @@ import {ZodString} from "zod"; import {ServerActionResult} from "@/types/action-type"; import {cn} from "@/lib/utils"; import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"; +import {MemberWithUser} from "@/db/schema/03_organization"; -export function backupColumns(isAlreadyRestore: boolean, settings: Setting, database: DatabaseWith): ColumnDef[] { +export function backupColumns( + isAlreadyRestore: boolean, + settings: Setting, + database: DatabaseWith, + activeMember: MemberWithUser +): ColumnDef[] { return [ { id: "availability", @@ -136,7 +142,7 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data }, readonly [], ServerActionResult, object> | undefined if (settings.storage == "local") { - data = await getFileUrlPresignedLocal({fileName:fileName!}) + data = await getFileUrlPresignedLocal({fileName: fileName!}) } else if (settings.storage == "s3") { data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`); } @@ -154,8 +160,7 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data return ( <> - {rowData.deletedAt == null && ( - + {(rowData.deletedAt == null && activeMember.role != "member") && ( - - - Actions + <> + {activeMember.role != "member" && ( + + + + + + Actions + + { + await handleRerunRestore(); + }} + > + Rerun + + + - - { - await handleRerunRestore(); - }} - > - Rerun - - - + { + await handleDelete(); + }} + > + Delete + + + + )} + - { - await handleDelete(); - }} - > - Delete - - - ); }, }, diff --git a/src/fonts/fonts.ts b/src/fonts/fonts.ts new file mode 100644 index 00000000..eb2f2566 --- /dev/null +++ b/src/fonts/fonts.ts @@ -0,0 +1,5 @@ +import {Inter} from "next/font/google"; + + + +export const inter = Inter({subsets: ["latin"]}); diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts index 062b1e16..0e95e382 100644 --- a/src/lib/auth/auth.ts +++ b/src/lib/auth/auth.ts @@ -8,7 +8,7 @@ import {admin as adminPlugin, openAPI, Organization, organization} from "better- import {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions"; import {headers} from "next/headers"; import {count, eq} from "drizzle-orm"; -import {OrganizationWithMembersAndUsers} from "@/db/schema/03_organization"; +import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization"; export const auth = betterAuth({ database: drizzleAdapter(db, { @@ -455,7 +455,7 @@ export const getActiveMember = async () => { }); console.log(member); - return member; + return member as MemberWithUser; } catch (e) { console.log("err", e); } diff --git a/src/lib/auth/permissions.ts b/src/lib/auth/permissions.ts index a08b5235..aadad5e3 100644 --- a/src/lib/auth/permissions.ts +++ b/src/lib/auth/permissions.ts @@ -12,6 +12,8 @@ const statement = { const ac = createAccessControl(statement); + + const superadmin = ac.newRole({ project: ["create", "list", "update", "delete"], database: ["create", "list", "update", "delete"],