Refactoring in roles.

This commit is contained in:
charlesgauthereau
2025-11-01 14:50:08 +01:00
parent 027928c29f
commit ae766dab2f
36 changed files with 1800 additions and 383 deletions
@@ -11,7 +11,7 @@ import {db} from "@/db";
import {eq, and, inArray} from "drizzle-orm"; import {eq, and, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {getOrganizationProjectDatabases} from "@/lib/services"; 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 {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
import {capitalizeFirstLetter} from "@/utils/text"; import {capitalizeFirstLetter} from "@/utils/text";
@@ -22,8 +22,9 @@ export default async function RoutePage(props: PageParams<{
const {projectId, databaseId} = await props.params; const {projectId, databaseId} = await props.params;
const organization = await getOrganization({}); const organization = await getOrganization({});
const activeMember = await getActiveMember()
if (!organization) { if (!organization || !activeMember) {
notFound(); notFound();
} }
@@ -83,6 +84,9 @@ export default async function RoutePage(props: PageParams<{
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null; const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
const isMember = activeMember?.role === "member";
return ( return (
<Page> <Page>
<div className="justify-between gap-2 sm:flex"> <div className="justify-between gap-2 sm:flex">
@@ -90,17 +94,19 @@ export default async function RoutePage(props: PageParams<{
<div className=" w-full md:w-fit"> <div className=" w-full md:w-fit">
{capitalizeFirstLetter(dbItem.name)} {capitalizeFirstLetter(dbItem.name)}
</div> </div>
<div className="flex items-center gap-2 md:justify-between w-full"> {!isMember && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 md:justify-between w-full">
{/* Do not delete*/} <div className="flex items-center gap-2">
{/*<EditButton/>*/} {/* Do not delete*/}
<RetentionPolicySheet database={dbItem}/> {/*<EditButton/>*/}
<CronButton database={dbItem}/> <RetentionPolicySheet database={dbItem}/>
<CronButton database={dbItem}/>
</div>
<div className="flex items-center gap-2">
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
</div>
</div> </div>
<div className="flex items-center gap-2"> )}
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
</div>
</div>
</PageTitle> </PageTitle>
</div> </div>
@@ -108,9 +114,9 @@ export default async function RoutePage(props: PageParams<{
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription> <PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
)} )}
<PageContent className="flex flex-col w-full h-full"> <PageContent className="flex flex-col w-full h-full">
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups} <DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
totalBackups={totalBackups}/> totalBackups={totalBackups}/>
<DatabaseTabs settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore} <DatabaseTabs activeMember={activeMember} settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
backups={backups} backups={backups}
restorations={restorations}/> restorations={restorations}/>
</PageContent> </PageContent>
@@ -12,7 +12,7 @@ import {notFound, redirect} from "next/navigation";
import {db} from "@/db"; import {db} from "@/db";
import {eq} from "drizzle-orm"; import {eq} from "drizzle-orm";
import {getOrganization} from "@/lib/auth/auth"; import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {capitalizeFirstLetter} from "@/utils/text"; import {capitalizeFirstLetter} from "@/utils/text";
@@ -24,6 +24,8 @@ export default async function RoutePage(props: PageParams<{
} = await props.params; } = await props.params;
const organization = await getOrganization({}); const organization = await getOrganization({});
const activeMember = await getActiveMember()
if (!organization) { if (!organization) {
notFound(); notFound();
} }
@@ -48,23 +50,27 @@ export default async function RoutePage(props: PageParams<{
redirect("/dashboard/projects"); redirect("/dashboard/projects");
} }
const isMember = activeMember?.role === "member";
return ( return (
<Page> <Page>
<div className="justify-between gap-2 sm:flex"> <div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center"> <PageTitle className="flex items-center">
{capitalizeFirstLetter(proj.name)} {capitalizeFirstLetter(proj.name)}
<Link className={buttonVariants({variant: "outline"})} href={`/dashboard/projects/${proj.id}/edit`}> {!isMember && (
<GearIcon className="w-7 h-7"/> <Link className={buttonVariants({variant: "outline"})}
</Link> href={`/dashboard/projects/${proj.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
)}
</PageTitle> </PageTitle>
<PageActions className="justify-between"> {!isMember && (
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/> <PageActions className="justify-between">
</PageActions> <ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
</PageActions>
)}
</div> </div>
<PageDescription>The list of associated databases</PageDescription> <PageDescription>The list of associated databases</PageDescription>
<PageContent className="flex flex-col w-full h-full"> <PageContent className="flex flex-col w-full h-full">
{proj.databases.length > 0 ? ( {proj.databases.length > 0 ? (
<CardsWithPagination <CardsWithPagination
@@ -7,12 +7,13 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
import {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card"; import {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card";
import {db} from "@/db"; import {db} from "@/db";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {getOrganization} from "@/lib/auth/auth"; import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder"; import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
export default async function RoutePage(props: PageParams<{ }>) { export default async function RoutePage(props: PageParams<{}>) {
const organization = await getOrganization({}); const organization = await getOrganization({});
const activeMember = await getActiveMember()
if (!organization) { if (!organization) {
notFound(); notFound();
@@ -28,12 +29,14 @@ export default async function RoutePage(props: PageParams<{ }>) {
databases: true, databases: true,
}, },
}); });
const isMember = activeMember?.role === "member";
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle>Projects</PageTitle> <PageTitle>Projects</PageTitle>
{projects.length > 0 && ( {(projects.length > 0 && !isMember) && (
<PageActions> <PageActions>
<Link href={`/dashboard/projects/new`}> <Link href={`/dashboard/projects/new`}>
<Button>+ Create Project</Button> <Button>+ Create Project</Button>
@@ -44,13 +47,17 @@ export default async function RoutePage(props: PageParams<{ }>) {
<PageContent> <PageContent>
{projects.length > 0 ? ( {projects.length > 0 ? (
<CardsWithPagination organizationSlug={organization.slug} data={projects} cardItem={ProjectCard} <CardsWithPagination
cardsPerPage={4} numberOfColumns={1}/> organizationSlug={organization.slug}
) : ( data={projects}
<EmptyStatePlaceholder cardItem={ProjectCard}
url={"/dashboard/projects/new"} cardsPerPage={4}
text={"Create new Project"} numberOfColumns={1}
/> />
) : isMember ? (
<EmptyStatePlaceholder text="No project available"/>
) : (
<EmptyStatePlaceholder url="/dashboard/projects/new" text="Create new Project"/>
)} )}
</PageContent> </PageContent>
</Page> </Page>
@@ -22,10 +22,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
} }
const isMember = activeMember?.role === "member"; const isMember = activeMember?.role === "member";
const isOwner = activeMember?.role === "owner";
if (isMember) {
notFound();
}
return ( return (
<Page> <Page>
@@ -37,14 +34,11 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
)} )}
</PageTitle> </PageTitle>
<PageActions> <PageActions>
{!isMember && organization.slug !== "default" && ( {isOwner && organization.slug !== "default" && (
<DeleteOrganizationButton organizationSlug={organization.slug}/> <DeleteOrganizationButton organizationSlug={organization.slug}/>
)} )}
</PageActions> </PageActions>
</PageHeader> </PageHeader>
{/*<PageDescription>*/}
{/* Manage your organization settings.*/}
{/*</PageDescription>*/}
<PageContent> <PageContent>
<SettingsOrganizationMembersTable organization={organization}/> <SettingsOrganizationMembersTable organization={organization}/>
</PageContent> </PageContent>
@@ -8,7 +8,7 @@ import {db} from "@/db";
import {and, asc, count, eq, inArray} from "drizzle-orm"; import {and, asc, count, eq, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {getOrganization} from "@/lib/auth/auth"; 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<{}>) { export default async function RoutePage(props: PageParams<{}>) {
const organization = await getOrganization({}); 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 const backupsRate = await db
.select({ .select({
createdAt: drizzleDb.schemas.backup.createdAt, createdAt: drizzleDb.schemas.backup.createdAt,
@@ -139,60 +66,84 @@ export default async function RoutePage(props: PageParams<{}>) {
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds)); .where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
const restorationsCount = restorationsCountResult[0]?.count ?? 0; const restorationsCount = restorationsCountResult[0]?.count ?? 0;
const projectsCount = projects.length; const projectsCount = projects.length;
const backupsEvolutionCount = backupsEvolution.length; const backupsEvolutionCount = backupsEvolution.length;
const sortedBackupsEvolution = backupsEvolution.sort( const sortedBackupsEvolution = backupsEvolution.sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
); );
const Placeholder = ({text}: {text: string}) => (
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">{text}</div>
);
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle>Statistics</PageTitle> <PageTitle>Statistics Overview</PageTitle>
</PageHeader> </PageHeader>
<PageContent className="flex flex-col gap-y-4"> <PageContent className="flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="w-full flex-1"> <Card className="w-full">
<CardHeader className="flex items-center gap-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
<Folder className="w-5 h-5 text-muted-foreground" /> <CardTitle className="text-sm font-medium">Projects</CardTitle>
<CardTitle>Projects</CardTitle> <Building2 className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent className="text-3xl font-bold">{projectsCount}</CardContent> <CardContent>
<div className="text-2xl font-bold">{projectsCount}</div>
<p className="text-xs text-muted-foreground">Active projects in this organization</p>
</CardContent>
</Card> </Card>
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2"> <Card className="w-full">
<DatabaseBackup className="w-5 h-5 text-muted-foreground" /> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle>Backups</CardTitle> <CardTitle className="text-sm font-medium">Backups</CardTitle>
<DatabaseBackup className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent className="text-3xl font-bold">{backupsEvolutionCount}</CardContent> <CardContent>
<div className="text-2xl font-bold">{backupsEvolutionCount}</div>
<p className="text-xs text-muted-foreground">Total backups executed across all databases</p>
</CardContent>
</Card> </Card>
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2"> <Card className="w-full">
<RefreshCcw className="w-5 h-5 text-muted-foreground" /> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle>Restorations</CardTitle> <CardTitle className="text-sm font-medium">Restorations</CardTitle>
<RefreshCcw className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent className="text-3xl font-bold">{restorationsCount}</CardContent> <CardContent>
<div className="text-2xl font-bold">{restorationsCount}</div>
<p className="text-xs text-muted-foreground">Total restoration operations performed</p>
</CardContent>
</Card> </Card>
</div> </div>
<div className="flex flex-col md:flex-row gap-4"> <div className="flex flex-col md:flex-row gap-4">
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle>Evolution of the number of backups</CardTitle> <CardTitle>Evolution of the number of backups</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<EvolutionLineChart data={sortedBackupsEvolution}/> {sortedBackupsEvolution.length > 0 ? (
<EvolutionLineChart data={sortedBackupsEvolution} />
) : (
<Placeholder text="No backup data available" />
)}
</CardContent> </CardContent>
</Card> </Card>
<Card className="w-full"> <Card className="w-full">
<CardHeader> <CardHeader>
<CardTitle>Success rate of backups</CardTitle> <CardTitle>Success rate of backups</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<PercentageLineChart data={backupsRate}/> {backupsRate.length > 0 ? (
<PercentageLineChart data={backupsRate} />
) : (
<Placeholder text="No backup rate data available" />
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
+2 -4
View File
@@ -1,15 +1,13 @@
import React from "react"; import React from "react";
import type {Metadata} from "next"; import type {Metadata} from "next";
import {Inter} from "next/font/google";
import "./globals.css"; import "./globals.css";
import {Providers} from "./providers"; import {Providers} from "./providers";
import {cn} from "@/lib/utils"; import {cn} from "@/lib/utils";
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer"; import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
import {inter} from "@/fonts/fonts";
const inter = Inter({subsets: ["latin"]});
export const metadata: Metadata = { 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, description: process.env.NEXT_PUBLIC_PROJECT_DESCRIPTION ?? undefined,
}; };
@@ -1,20 +1,21 @@
"use client"; "use client";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; 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 {Input} from "@/components/ui/input";
import { Form } from "@/components/ui/form"; import {Form} from "@/components/ui/form";
import { Button } from "@/components/ui/button"; import {Button} from "@/components/ui/button";
import { toast } from "sonner"; import {toast} from "sonner";
import { useMutation } from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import { TooltipProvider } from "@/components/ui/tooltip"; import {TooltipProvider} from "@/components/ui/tooltip";
import Link from "next/link"; import Link from "next/link";
import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input"; import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
import { LoginSchema, LoginType } from "@/components/wrappers/auth/login/login-form/login-form.schema"; 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 {SocialAuthButton, SocialProviderType} from "@/components/wrappers/auth/login/button-auth/social-auth-button";
import { signIn } from "@/lib/auth/auth-client"; import {signIn} from "@/lib/auth/auth-client";
import { useRouter } from "next/navigation"; import {useRouter} from "next/navigation";
import { Icon } from "@iconify/react"; import {Icon} from "@iconify/react";
import {env} from "@/env.mjs";
export type loginFormProps = { export type loginFormProps = {
defaultValues?: LoginType; defaultValues?: LoginType;
@@ -29,7 +30,7 @@ export const LoginForm = (props: loginFormProps) => {
const mutation = useMutation({ const mutation = useMutation({
mutationFn: async (values: LoginType) => { mutationFn: async (values: LoginType) => {
const { error } = await signIn.email(values, { const {error} = await signIn.email(values, {
onSuccess: () => { onSuccess: () => {
toast.success("Login success"); toast.success("Login success");
router.push("/dashboard/profile"); router.push("/dashboard/profile");
@@ -41,13 +42,18 @@ export const LoginForm = (props: loginFormProps) => {
}, },
}); });
const availableProviders: SocialProviderType[] = [ const availableProviders: SocialProviderType[] = [];
{
id: "google", if (env.NEXT_PUBLIC_GOOGLE_AUTH) {
name: "Google", availableProviders.push(
icon: <Icon icon={"logos:google-icon"} width="25" height="25" />, {
}, id: "google",
]; name: "Google",
icon: <Icon icon={"logos:google-icon"} width="25" height="25"/>,
},
)
}
return ( return (
<TooltipProvider> <TooltipProvider>
@@ -70,13 +76,14 @@ export const LoginForm = (props: loginFormProps) => {
control={form.control} control={form.control}
name="email" name="email"
defaultValue="" defaultValue=""
render={({ field }) => ( render={({field}) => (
<FormItem> <FormItem>
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
<FormControl> <FormControl>
<Input autoComplete="email webauthn" placeholder="exemple@portabase.io" {...field} /> <Input autoComplete="email webauthn"
placeholder="exemple@portabase.io" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage/>
</FormItem> </FormItem>
)} )}
/> />
@@ -84,7 +91,7 @@ export const LoginForm = (props: loginFormProps) => {
control={form.control} control={form.control}
name="password" name="password"
defaultValue="" defaultValue=""
render={({ field }) => ( render={({field}) => (
<FormItem> <FormItem>
<div className="flex items-center"> <div className="flex items-center">
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
@@ -93,9 +100,10 @@ export const LoginForm = (props: loginFormProps) => {
</Link>*/} </Link>*/}
</div> </div>
<FormControl> <FormControl>
<PasswordInput autoComplete="current-password webauthn" placeholder="Your password" {...field} /> <PasswordInput autoComplete="current-password webauthn"
placeholder="Your password" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage/>
</FormItem> </FormItem>
)} )}
/> />
@@ -107,7 +115,7 @@ export const LoginForm = (props: loginFormProps) => {
</Link> </Link>
</div> </div>
</Form> </Form>
<SocialAuthButton providers={availableProviders} /> <SocialAuthButton providers={availableProviders}/>
</CardContent> </CardContent>
</Card> </Card>
</TooltipProvider> </TooltipProvider>
@@ -3,22 +3,29 @@ import {cn} from "@/lib/utils";
import {Plus} from "lucide-react"; import {Plus} from "lucide-react";
type EmptyStatePlaceholderProps = { type EmptyStatePlaceholderProps = {
url: string; url?: string;
text: string; text: string;
} }
export const EmptyStatePlaceholder = ({url, text}: EmptyStatePlaceholderProps) => { export const EmptyStatePlaceholder = ({url, text}: EmptyStatePlaceholderProps) => {
return ( return (
<Link <>{url ?
href={url} <Link
className={cn( href={url}
"flex flex-col items-center justify-center w-full rounded-2xl border border-dashed border-muted p-6 lg:p-10", className={cn(
"hover:bg-muted/50 transition-colors text-muted-foreground hover:text-primary text-center space-y-2" "flex flex-col items-center justify-center w-full rounded-2xl border border-dashed border-muted p-6 lg:p-10",
)} "hover:bg-muted/50 transition-colors text-muted-foreground hover:text-primary text-center space-y-2"
> )}
<Plus className="w-5 h-5 lg:w-6 lg:h-6"/> >
<span className="text-sm lg:text-base font-medium">{text}</span> <Plus className="w-5 h-5 lg:w-6 lg:h-6"/>
</Link> <span className="text-sm lg:text-base font-medium">{text}</span>
</Link>
:
<div className="flex flex-col items-center justify-center py-12 text-center">
<p className="text-lg text-muted-foreground">{text}</p>
</div>
}
</>
) )
} }
@@ -8,7 +8,6 @@ import {Setting} from "@/db/schema/01_setting";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {useRouter, useSearchParams} from "next/navigation"; import {useRouter, useSearchParams} from "next/navigation";
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table"; 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 = { export type AdminTabsProps = {
users: UserWithAccounts[]; users: UserWithAccounts[];
@@ -42,11 +41,7 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => {
<TabsTrigger className="w-full" value="storage"> <TabsTrigger className="w-full" value="storage">
Storage Storage
</TabsTrigger> </TabsTrigger>
<TabsTrigger className="w-full" value="settings">
Settings
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="users"> <TabsContent value="users">
<AdminUsersTable users={users}/> <AdminUsersTable users={users}/>
</TabsContent> </TabsContent>
@@ -56,9 +51,6 @@ export const AdminTabs = ({users, settings}: AdminTabsProps) => {
<TabsContent value="storage"> <TabsContent value="storage">
<SettingsStorageTab settings={settings}/> <SettingsStorageTab settings={settings}/>
</TabsContent> </TabsContent>
<TabsContent value="settings">
<AdminSettingsTab/>
</TabsContent>
</Tabs> </Tabs>
); );
}; };
@@ -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 {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
import {DataTable} from "@/components/wrappers/common/table/data-table"; import {DataTable} from "@/components/wrappers/common/table/data-table";
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/admin-user-tab/columns-users"; import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/admin-user-tab/columns-users";
@@ -18,7 +18,10 @@ export const AdminUsersTable = (props: AdminUsersTableProps) => {
<CardDescription>Manage your users</CardDescription> <CardDescription>Manage your users</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<DataTable columns={usersColumnsAdmin} data={users}/> <DataTable
enableSelect={false}
columns={usersColumnsAdmin}
data={users}/>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -72,7 +72,7 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
accessorKey: "accounts", accessorKey: "accounts",
header: "Provider ID", header: "Provider ID",
cell: ({row}) => { cell: ({row}) => {
return( return (
<div> <div>
{row.original.accounts.map((item) => ( {row.original.accounts.map((item) => (
<div key={item.id}> <div key={item.id}>
@@ -107,18 +107,20 @@ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
}); });
return ( return (
<div className="flex items-center gap-2"> <>
<ButtonWithLoading <div className="flex items-center gap-2">
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email} <ButtonWithLoading
variant="outline" disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
text="" variant="outline"
icon={<Trash2 color="red" size={15}/>} text=""
onClick={async () => { icon={<Trash2 color="red" size={15}/>}
await mutation.mutateAsync(); onClick={async () => {
}} await mutation.mutateAsync();
size="sm" }}
/> size="sm"
</div> />
</div>
</>
); );
}, },
}, },
@@ -14,7 +14,9 @@ export const AgentCard = (props: agentCardProps) => {
const { data: agent } = props; const { data: agent } = props;
return ( return (
<Link href={`/dashboard/agents/${agent.id}`} className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md"> <Link href={`/dashboard/agents/${agent.id}`}
className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
>
<Card className="flex flex-row justify-between"> <Card className="flex flex-row justify-between">
<div className="flex-1 text-left"> <div className="flex-1 text-left">
<CardHeader className="text-2xl font-bold">{agent.name}</CardHeader> <CardHeader className="text-2xl font-bold">{agent.name}</CardHeader>
@@ -18,7 +18,7 @@ export const LoggedInDropdown = (props: LoggedInDropdownProps) => {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild>{props.children}</DropdownMenuTrigger> <DropdownMenuTrigger asChild>{props.children}</DropdownMenuTrigger>
<DropdownMenuContent side="top" className="w-[--radix-popper-anchor-width]"> <DropdownMenuContent side="top" className="min-w-[var(--radix-popper-anchor-width)]">
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
redirect("/dashboard/profile"); redirect("/dashboard/profile");
@@ -29,11 +29,12 @@ export const SidebarMenuCustomMain = () => {
const groupContent: SidebarGroupItem["group_content"] = [ const groupContent: SidebarGroupItem["group_content"] = [
{ title: "Projects", url: "/projects", icon: Layers, details:true }, { title: "Projects", url: "/projects", icon: Layers, details:true },
{ title: "Statistics", url: "/statistics", icon: ChartArea }, { title: "Statistics", url: "/statistics", icon: ChartArea },
{ title: "Settings", url: "/settings", icon: Settings, details:true }
]; ];
if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) { // if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) {
groupContent.push({ title: "Settings", url: "/settings", icon: Settings, details:true }); // groupContent.push({ title: "Settings", url: "/settings", icon: Settings, details:true });
} // }
const items: SidebarGroupItem[] = [ const items: SidebarGroupItem[] = [
{ {
@@ -41,7 +41,6 @@ export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createO
await authClient.organization.setActive({organizationSlug: result.data.value.slug}); await authClient.organization.setActive({organizationSlug: result.data.value.slug});
onSuccess?.(); onSuccess?.();
toast.success(result.data.actionSuccess?.message || "Organization Created."); toast.success(result.data.actionSuccess?.message || "Organization Created.");
// router.push("/");
router.replace(`/dashboard/home`); router.replace(`/dashboard/home`);
} else { } else {
// @ts-ignore // @ts-ignore
@@ -82,26 +81,6 @@ export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createO
</FormItem> </FormItem>
)} )}
/> />
{/*<FormField*/}
{/* control={form.control}*/}
{/* name="slug"*/}
{/* defaultValue=""*/}
{/* render={({field}) => (*/}
{/* <FormItem>*/}
{/* <FormLabel>Slug</FormLabel>*/}
{/* <FormControl>*/}
{/* <Input*/}
{/* {...field}*/}
{/* onChange={(e) => {*/}
{/* const value = e.target.value.replaceAll(" ", "-").toLowerCase();*/}
{/* field.onChange(value);*/}
{/* }}*/}
{/* />*/}
{/* </FormControl>*/}
{/* <FormMessage/>*/}
{/* </FormItem>*/}
{/* )}*/}
{/*/>*/}
<DialogFooter> <DialogFooter>
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<Button type="submit">Create</Button> <Button type="submit">Create</Button>
@@ -39,5 +39,6 @@ export function OrganizationCombobox() {
return <>{state === "expanded" && return <>{state === "expanded" &&
<ComboBox sideBar values={values} defaultValue={activeOrganization?.slug} onValueChange={onValueChange} <ComboBox sideBar values={values} defaultValue={activeOrganization?.slug} onValueChange={onValueChange}
reload={handleReset}/>}</>; reload={handleReset}/>}
</>;
} }
@@ -23,6 +23,7 @@ export const deleteProjectAction = userAction.schema(z.string()).action(async ({
.set({ .set({
isArchived: true, isArchived: true,
slug: uuid, slug: uuid,
name: uuid,
}) })
.where(eq(drizzleDb.schemas.project.id, parsedInput)) .where(eq(drizzleDb.schemas.project.id, parsedInput))
.returning(); .returning();
@@ -12,6 +12,7 @@ import {useMutation} from "@tanstack/react-query";
import {deleteBackupAction} from "@/features/dashboard/restore/restore.action"; import {deleteBackupAction} from "@/features/dashboard/restore/restore.action";
import {toast} from "sonner"; import {toast} from "sonner";
import {useRouter} from "next/navigation"; import {useRouter} from "next/navigation";
import {MemberWithUser} from "@/db/schema/03_organization";
type DatabaseBackupListProps = { type DatabaseBackupListProps = {
@@ -19,6 +20,7 @@ type DatabaseBackupListProps = {
settings: Setting; settings: Setting;
database: DatabaseWith; database: DatabaseWith;
backups: Backup[]; backups: Backup[];
activeMember: MemberWithUser
} }
@@ -69,7 +71,7 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
backupId: backup.id, backupId: backup.id,
databaseId: backup.databaseId, databaseId: backup.databaseId,
status: backup.status, status: backup.status,
file: backup.file!, file: backup.file ?? "",
projectSlug: props.database?.project?.slug! projectSlug: props.database?.project?.slug!
}); });
return { return {
@@ -97,50 +99,58 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
}, },
}); });
const isMember = props.activeMember.role === "member";
return ( return (
<DataTable <DataTable
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database)} enableSelect={!isMember}
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database, props.activeMember)}
data={filteredBackups} data={filteredBackups}
enablePagination enablePagination
selectedActions={(rows) => ( selectedActions={(rows) => (
<div className="flex justify-start md:justify-between gap-3 md:gap-0 items-center w-full ml-0"> <>
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonWithLoading
variant="outline"
text="Actions"
onClick={() => {
}} <div className="flex justify-start md:justify-between gap-3 md:gap-0 items-center w-full ml-0">
disabled={rows.length === 0 || mutationDeleteBackups.isPending} <div className="flex gap-2">
icon={<MoreHorizontal/>} {!isMember && (
isPending={mutationDeleteBackups.isPending} <DropdownMenu>
size="sm" <DropdownMenuTrigger asChild>
<ButtonWithLoading
variant="outline"
text="Actions"
onClick={() => {
}}
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
icon={<MoreHorizontal/>}
isPending={mutationDeleteBackups.isPending}
size="sm"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={async () => {
console.log("Deleting rows:", rows)
await mutationDeleteBackups.mutateAsync(rows)
}}
className="text-red-600 focus:text-red-700"
>
<Trash2 className="w-4 h-4 mr-2"/>
Delete Selected
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
<FiltersDropdown
items={items}
selectedItems={selectedFilters}
onSelect={handleSelectFilter}
clearFilters={clearFilters}
/> />
</DropdownMenuTrigger> </div>
<DropdownMenuContent align="start"> </div>
<DropdownMenuItem
onClick={async () => { </>
console.log("Deleting rows:", rows)
await mutationDeleteBackups.mutateAsync(rows)
}}
className="text-red-600 focus:text-red-700"
>
<Trash2 className="w-4 h-4 mr-2"/>
Delete Selected
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<FiltersDropdown
items={items}
selectedItems={selectedFilters}
onSelect={handleSelectFilter}
clearFilters={clearFilters}
/>
</div>
</div>
)} )}
/> />
) )
@@ -9,11 +9,13 @@ import {useMutation} from "@tanstack/react-query";
import {deleteRestoreAction} from "@/features/dashboard/restore/restore.action"; import {deleteRestoreAction} from "@/features/dashboard/restore/restore.action";
import {toast} from "sonner"; import {toast} from "sonner";
import {useRouter} from "next/navigation"; import {useRouter} from "next/navigation";
import {MemberWithUser} from "@/db/schema/03_organization";
type DatabaseRestoreListProps = { type DatabaseRestoreListProps = {
isAlreadyRestore: boolean; isAlreadyRestore: boolean;
restorations: Restoration[]; restorations: Restoration[];
activeMember: MemberWithUser
} }
export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => { export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
@@ -47,40 +49,46 @@ export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
router.refresh(); router.refresh();
}, },
}); });
const isMember = props.activeMember.role === "member";
return ( return (
<DataTable <DataTable
columns={restoreColumns(props.isAlreadyRestore)} enableSelect={!isMember}
columns={restoreColumns(props.isAlreadyRestore, props.activeMember)}
data={props.restorations} data={props.restorations}
enablePagination enablePagination
selectedActions={(rows) => ( selectedActions={(rows) => (
<DropdownMenu> <>
<DropdownMenuTrigger asChild> {!isMember && (
<ButtonWithLoading <DropdownMenu>
variant="outline" <DropdownMenuTrigger asChild>
text="Actions" <ButtonWithLoading
onClick={() => { variant="outline"
}} text="Actions"
disabled={rows.length === 0 || mutationDeleteRestorations.isPending} onClick={() => {
icon={<MoreHorizontal/>} }}
isPending={mutationDeleteRestorations.isPending} disabled={rows.length === 0 || mutationDeleteRestorations.isPending}
size="sm" icon={<MoreHorizontal/>}
/> isPending={mutationDeleteRestorations.isPending}
</DropdownMenuTrigger> size="sm"
<DropdownMenuContent align="start"> />
<DropdownMenuItem </DropdownMenuTrigger>
onClick={async () => { <DropdownMenuContent align="start">
await mutationDeleteRestorations.mutateAsync(rows) <DropdownMenuItem
}} onClick={async () => {
disabled={props.isAlreadyRestore} await mutationDeleteRestorations.mutateAsync(rows)
className="text-red-600 focus:text-red-700" }}
> disabled={props.isAlreadyRestore}
<Trash2 className="w-4 h-4 mr-2"/> className="text-red-600 focus:text-red-700"
Delete Selected >
</DropdownMenuItem> <Trash2 className="w-4 h-4 mr-2"/>
</DropdownMenuContent> Delete Selected
</DropdownMenu> </DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</>
)} )}
/> />
) )
@@ -8,13 +8,15 @@ import {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/07_databa
import {Setting} from "@/db/schema/01_setting"; import {Setting} from "@/db/schema/01_setting";
import {DatabaseBackupList} from "@/components/wrappers/dashboard/projects/database/database-backup-list"; import {DatabaseBackupList} from "@/components/wrappers/dashboard/projects/database/database-backup-list";
import {DatabaseRestoreList} from "@/components/wrappers/dashboard/projects/database/database-restore-list"; import {DatabaseRestoreList} from "@/components/wrappers/dashboard/projects/database/database-restore-list";
import {MemberWithUser} from "@/db/schema/03_organization";
export type DatabaseTabsProps = { export type DatabaseTabsProps = {
settings: Setting settings: Setting,
backups: Backup[]; backups: Backup[],
restorations: Restoration[]; restorations: Restoration[],
isAlreadyRestore: boolean; isAlreadyRestore: boolean,
database: DatabaseWith; database: DatabaseWith,
activeMember: MemberWithUser
}; };
export const DatabaseTabs = (props: DatabaseTabsProps) => { export const DatabaseTabs = (props: DatabaseTabsProps) => {
@@ -58,12 +60,14 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
settings={props.settings} settings={props.settings}
database={props.database} database={props.database}
backups={props.backups} backups={props.backups}
activeMember={props.activeMember}
/> />
</TabsContent> </TabsContent>
<TabsContent className="h-full justify-between" value="restore"> <TabsContent className="h-full justify-between" value="restore">
<DatabaseRestoreList <DatabaseRestoreList
isAlreadyRestore={props.isAlreadyRestore} isAlreadyRestore={props.isAlreadyRestore}
restorations={props.restorations} restorations={props.restorations}
activeMember={props.activeMember}
/> />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
@@ -15,7 +15,7 @@ export const ProjectCard = (props: projectCardProps) => {
return ( return (
<Link <Link
href={`/dashboard/projects/${project.id}`} href={`/dashboard/projects/${project.id}`}
className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md" className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl"
> >
<Card className="flex flex-row justify-between"> <Card className="flex flex-row justify-between">
<div className="flex-1 text-left"> <div className="flex-1 text-left">
@@ -18,7 +18,7 @@ export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
const { organizationSlug, data: database, extendedProps: extendedProps } = props; const { organizationSlug, data: database, extendedProps: extendedProps } = props;
return ( return (
<Link className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md" href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}> <Link className="block transition-all duration-200 hover:scale-[1.01] hover:shadow-md rounded-xl" href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
<DatabaseCard data={database} /> <DatabaseCard data={database} />
</Link> </Link>
); );
@@ -5,7 +5,7 @@ import { ProjectSchema } from "@/components/wrappers/dashboard/projects/project-
import { z } from "zod"; import { z } from "zod";
import { ServerActionResult } from "@/types/action-type"; import { ServerActionResult } from "@/types/action-type";
import { db } from "@/db"; 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 {Project} from "@/db/schema/06_project";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {Database} from "@/db/schema/07_database"; import {Database} from "@/db/schema/07_database";
@@ -21,6 +21,22 @@ export const createProjectAction = userAction
.action(async ({ parsedInput }): Promise<ServerActionResult<Project>> => { .action(async ({ parsedInput }): Promise<ServerActionResult<Project>> => {
try { try {
const slug = slugify(parsedInput.data.name); 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 const [createdProject] = await db
.insert(drizzleDb.schemas.project) .insert(drizzleDb.schemas.project)
.values({ .values({
@@ -31,7 +47,10 @@ export const createProjectAction = userAction
.returning(); .returning();
if (parsedInput.data.databases.length > 0) { 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 { return {
@@ -43,6 +62,7 @@ export const createProjectAction = userAction
}, },
}; };
} catch (error) { } catch (error) {
console.log(error);
return { return {
success: false, success: false,
actionError: { actionError: {
@@ -55,6 +75,9 @@ export const createProjectAction = userAction
} }
}); });
export const updateProjectAction = userAction export const updateProjectAction = userAction
.schema( .schema(
z.object({ z.object({
@@ -1,15 +1,20 @@
"use client"; "use client";
import {ColumnDef} from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import {MemberWithUser} from "@/db/schema/03_organization"; import { MemberWithUser } from "@/db/schema/03_organization";
import {useState} from "react"; import { useState } from "react";
import {authClient, useSession} from "@/lib/auth/auth-client"; import { authClient, useSession } from "@/lib/auth/auth-client";
import {useMutation} from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import {toast} from "sonner"; import { toast } from "sonner";
import {Badge} from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import {updateMemberRoleAction} from "@/components/wrappers/dashboard/settings/update-member.action"; import {
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema"; 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<MemberWithUser>[] = [ export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
{ {
@@ -18,7 +23,7 @@ export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
cell: ({ row }) => { cell: ({ row }) => {
const [role, setRole] = useState<string>(row.getValue("role")); const [role, setRole] = useState<string>(row.getValue("role"));
const { data: session } = useSession(); const { data: session } = useSession();
const activeOrgaMember = authClient.useActiveMember();
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: () => mutationFn: () =>
@@ -28,36 +33,59 @@ export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
role: RoleSchemaMember.parse(role), role: RoleSchemaMember.parse(role),
}), }),
onSuccess: () => { onSuccess: () => {
toast.success(`User updated successfully.`); toast.success("User updated successfully.");
}, },
onError: () => { 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 handleUpdateRole = async () => {
const nextRole = const nextRole = role === "admin" ? "member" : "admin";
role === "owner" ? "admin" : role === "admin" ? "member" : "owner";
setRole(nextRole); setRole(nextRole);
await updateMutation.mutateAsync(); await updateMutation.mutateAsync();
}; };
const isCurrentUser = session?.user.email === row.original.user.email; 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 = (
<Badge <Badge
className={isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"} className={
isDisabled
? "cursor-not-allowed opacity-50"
: "cursor-pointer hover:bg-accent"
}
onClick={isDisabled ? undefined : handleUpdateRole} onClick={isDisabled ? undefined : handleUpdateRole}
variant="outline" variant="outline"
> >
{role} {role}
</Badge> </Badge>
); );
return isDisabled ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{badge}</TooltipTrigger>
<TooltipContent>
<p>{disabledReason}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
badge
);
}, },
}, },
{ {
@@ -67,6 +95,5 @@ export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
{ {
accessorKey: "user.email", accessorKey: "user.email",
header: "Email", header: "Email",
} },
]; ];
@@ -1,7 +1,5 @@
import {DataTable} from "@/components/wrappers/common/table/data-table"; 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 {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"; import {organizationMemberColumns} from "@/components/wrappers/dashboard/settings/columns-organization-members";
@@ -11,12 +9,12 @@ interface SettingsOrganizationMembersTableProps {
export const SettingsOrganizationMembersTable = ({organization}: SettingsOrganizationMembersTableProps) => { export const SettingsOrganizationMembersTable = ({organization}: SettingsOrganizationMembersTableProps) => {
return ( return (
<div className="flex flex-col h-full py-4"> <div className="flex flex-col h-full ">
<div className="flex gap-4 h-fit justify-between"> <div className=" h-full">
<h1>List of Organization members</h1> <DataTable
</div> columns={organizationMemberColumns}
<div className="mt-5 h-full"> enableSelect={false}
<DataTable columns={organizationMemberColumns} data={organization.members as MemberWithUser[]}/> data={organization.members as MemberWithUser[]}/>
</div> </div>
</div> </div>
) )
+3
View File
@@ -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;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -36,6 +36,13 @@
"when": 1756473637441, "when": 1756473637441,
"tag": "0004_dazzling_hawkeye", "tag": "0004_dazzling_hawkeye",
"breakpoints": true "breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1762004436821,
"tag": "0005_old_swarm",
"breakpoints": true
} }
] ]
} }
+2 -1
View File
@@ -1,5 +1,5 @@
import {pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core"; 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 {organization} from "@/db/schema/03_organization";
import {relations} from "drizzle-orm"; import {relations} from "drizzle-orm";
import {createSelectSchema} from "drizzle-zod"; import {createSelectSchema} from "drizzle-zod";
@@ -33,3 +33,4 @@ export const memberRelations = relations(member, ({ one }) => ({
export const organizationMemberSchema = createSelectSchema(member); export const organizationMemberSchema = createSelectSchema(member);
export type OrganizationMember = z.infer<typeof organizationMemberSchema>; export type OrganizationMember = z.infer<typeof organizationMemberSchema>;
+8 -9
View File
@@ -1,9 +1,9 @@
import { pgTable, text, boolean, uuid, timestamp } from "drizzle-orm/pg-core"; import {pgTable, text, boolean, uuid} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm"; import {relations} from "drizzle-orm";
import { Organization, organization } from "./03_organization"; import {Organization, organization} from "./03_organization";
import { createSelectSchema } from "drizzle-zod"; import {createSelectSchema} from "drizzle-zod";
import { z } from "zod"; import {z} from "zod";
import { Database, database } from "./07_database"; import {Database, database} from "./07_database";
import {timestamps} from "@/db/schema/00_common"; import {timestamps} from "@/db/schema/00_common";
export const project = pgTable("projects", { export const project = pgTable("projects", {
@@ -13,12 +13,11 @@ export const project = pgTable("projects", {
isArchived: boolean("is_archived").default(false), isArchived: boolean("is_archived").default(false),
organizationId: uuid("organization_id") organizationId: uuid("organization_id")
.notNull() .notNull()
.references(() => organization.id), .references(() => organization.id, {onDelete: "cascade"}),
...timestamps ...timestamps
}); });
export const projectRelations = relations(project, ({ one, many }) => ({ export const projectRelations = relations(project, ({one, many}) => ({
organization: one(organization, { organization: one(organization, {
fields: [project.organizationId], fields: [project.organizationId],
references: [organization.id], references: [organization.id],
+4
View File
@@ -22,6 +22,7 @@ export const env = createEnv({
AUTH_GOOGLE_ID: z.string().optional(), AUTH_GOOGLE_ID: z.string().optional(),
AUTH_GOOGLE_SECRET: z.string().optional(), AUTH_GOOGLE_SECRET: z.string().optional(),
NEXT_PUBLIC_GOOGLE_AUTH: z.boolean().default(false).optional(),
S3_ENDPOINT: z.string().optional(), S3_ENDPOINT: z.string().optional(),
S3_ACCESS_KEY: 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_DESCRIPTION: z.string().optional(),
NEXT_PUBLIC_PROJECT_URL: z.string().optional(), NEXT_PUBLIC_PROJECT_URL: z.string().optional(),
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
NEXT_PUBLIC_GOOGLE_AUTH: z.boolean().default(false).optional(),
}, },
runtimeEnv: { runtimeEnv: {
NEXT_PUBLIC_PROJECT_NAME: process.env.NEXT_PUBLIC_PROJECT_NAME, 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_ID: process.env.AUTH_GOOGLE_ID,
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, 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_ENDPOINT: process.env.S3_ENDPOINT,
S3_ACCESS_KEY: process.env.S3_ACCESS_KEY, S3_ACCESS_KEY: process.env.S3_ACCESS_KEY,
+9 -4
View File
@@ -30,9 +30,15 @@ import {ZodString} from "zod";
import {ServerActionResult} from "@/types/action-type"; import {ServerActionResult} from "@/types/action-type";
import {cn} from "@/lib/utils"; import {cn} from "@/lib/utils";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"; 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<Backup>[] { export function backupColumns(
isAlreadyRestore: boolean,
settings: Setting,
database: DatabaseWith,
activeMember: MemberWithUser
): ColumnDef<Backup>[] {
return [ return [
{ {
id: "availability", id: "availability",
@@ -136,7 +142,7 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data
}, readonly [], ServerActionResult<string>, object> | undefined }, readonly [], ServerActionResult<string>, object> | undefined
if (settings.storage == "local") { if (settings.storage == "local") {
data = await getFileUrlPresignedLocal({fileName:fileName!}) data = await getFileUrlPresignedLocal({fileName: fileName!})
} else if (settings.storage == "s3") { } else if (settings.storage == "s3") {
data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`); data = await getFileUrlPreSignedS3Action(`backups/${database.project?.slug}/${fileName}`);
} }
@@ -154,8 +160,7 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data
return ( return (
<> <>
{rowData.deletedAt == null && ( {(rowData.deletedAt == null && activeMember.role != "member") && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0"> <Button variant="ghost" className="h-8 w-8 p-0">
+40 -32
View File
@@ -24,9 +24,13 @@ import {
import {toast} from "sonner"; import {toast} from "sonner";
import {useRouter} from "next/navigation"; import {useRouter} from "next/navigation";
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom"; import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
import {MemberWithUser} from "@/db/schema/03_organization";
export function restoreColumns(isAlreadyRestore: boolean): ColumnDef<Restoration>[] { export function restoreColumns(
isAlreadyRestore: boolean,
activeMember: MemberWithUser
): ColumnDef<Restoration>[] {
return[ return[
{ {
accessorKey: "id", accessorKey: "id",
@@ -102,39 +106,43 @@ export function restoreColumns(isAlreadyRestore: boolean): ColumnDef<Restoration
return ( return (
<DropdownMenu> <>
<DropdownMenuTrigger asChild> {activeMember.role != "member" && (
<Button variant="ghost" className="h-8 w-8 p-0"> <DropdownMenu>
<span className="sr-only">Open menu</span> <DropdownMenuTrigger asChild>
<MoreHorizontal className="h-4 w-4" /> <Button variant="ghost" className="h-8 w-8 p-0">
</Button> <span className="sr-only">Open menu</span>
</DropdownMenuTrigger> <MoreHorizontal className="h-4 w-4"/>
<DropdownMenuContent align="end"> </Button>
<DropdownMenuLabel>Actions</DropdownMenuLabel> </DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<TooltipCustom disabled={isAlreadyRestore} text="Already a restoration waiting">
<DropdownMenuItem
disabled={mutationRerunRestore.isPending || isAlreadyRestore}
onClick={async () => {
await handleRerunRestore();
}}
>
<ReloadIcon/> Rerun
</DropdownMenuItem>
</TooltipCustom>
<DropdownMenuSeparator/>
<TooltipCustom disabled={isAlreadyRestore} text="Already a restoration waiting"> <DropdownMenuItem
<DropdownMenuItem disabled={status == "waiting"}
disabled={mutationRerunRestore.isPending || isAlreadyRestore} className="text-red-600"
onClick={async () => { onClick={async () => {
await handleRerunRestore(); await handleDelete();
}} }}
> >
<ReloadIcon/> Rerun <Trash2/> Delete
</DropdownMenuItem> </DropdownMenuItem>
</TooltipCustom> </DropdownMenuContent>
<DropdownMenuSeparator/> </DropdownMenu>
)}
</>
<DropdownMenuItem
disabled={status == "waiting"}
className="text-red-600"
onClick={async () => {
await handleDelete();
}}
>
<Trash2/> Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
); );
}, },
}, },
+5
View File
@@ -0,0 +1,5 @@
import {Inter} from "next/font/google";
export const inter = Inter({subsets: ["latin"]});
+2 -2
View File
@@ -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 {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions";
import {headers} from "next/headers"; import {headers} from "next/headers";
import {count, eq} from "drizzle-orm"; 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({ export const auth = betterAuth({
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
@@ -455,7 +455,7 @@ export const getActiveMember = async () => {
}); });
console.log(member); console.log(member);
return member; return member as MemberWithUser;
} catch (e) { } catch (e) {
console.log("err", e); console.log("err", e);
} }
+2
View File
@@ -12,6 +12,8 @@ const statement = {
const ac = createAccessControl(statement); const ac = createAccessControl(statement);
const superadmin = ac.newRole({ const superadmin = ac.newRole({
project: ["create", "list", "update", "delete"], project: ["create", "list", "update", "delete"],
database: ["create", "list", "update", "delete"], database: ["create", "list", "update", "delete"],