Working on statistics

This commit is contained in:
charlesgauthereau
2025-08-02 11:14:51 +02:00
parent 13241811d0
commit d13e4b7d40
26 changed files with 641 additions and 119 deletions
@@ -1,18 +1,21 @@
import { PageParams } from "@/types/next"; import {PageParams} from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import { AdminTabs } from "@/components/wrappers/dashboard/admin/admin-tabs"; import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
import { db } from "@/db"; import {db} from "@/db";
import {isNull} from "drizzle-orm"; import {isNull} from "drizzle-orm";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
const users = await db.query.user.findMany({ const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt) where: (fields) => isNull(fields.deletedAt),
with: {
accounts: true
}
}); });
const settings = await db.query.setting.findFirst({ const settings = await db.query.setting.findFirst({
where: (fields, { eq }) => eq(fields.name, "system"), where: (fields, {eq}) => eq(fields.name, "system"),
}); });
return ( return (
@@ -21,7 +24,7 @@ export default async function RoutePage(props: PageParams<{}>) {
<PageTitle>Administration Panel</PageTitle> <PageTitle>Administration Panel</PageTitle>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<AdminTabs settings={settings!} users={users} /> <AdminTabs settings={settings!} users={users}/>
</PageContent> </PageContent>
</Page> </Page>
); );
@@ -19,6 +19,7 @@ export default async function RoutePage(props: PageParams<{ databaseId: string }
notFound(); notFound();
} }
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
@@ -27,6 +28,7 @@ export default async function RoutePage(props: PageParams<{ databaseId: string }
<PageContent> <PageContent>
<DatabaseForm <DatabaseForm
databaseId={databaseId} databaseId={databaseId}
// @ts-ignore
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }} defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
/> />
</PageContent> </PageContent>
@@ -42,8 +42,8 @@ export default async function RoutePage(props: PageParams<{ projectId: string }>
}, },
orderBy: (db, {desc}) => [desc(db.createdAt)], orderBy: (db, {desc}) => [desc(db.createdAt)],
}) })
); )
// .filter((db): db is DatabaseWith => db.project !== null); // .filter((db) => db.project !== null) as DatabaseWith[];
console.log("ici34",availableDatabases); console.log("ici34",availableDatabases);
@@ -57,7 +57,7 @@ export default async function RoutePage(props: PageParams<{ projectId: string }>
<ProjectForm <ProjectForm
organization={org} organization={org}
databases={availableDatabases as DatabaseWith[]} databases={availableDatabases as DatabaseWith[]}
defaultValues={{name: proj.name, slug: proj.slug, databases: proj.databases.map((db) => db.id)}} defaultValues={{...proj, databases: proj.databases.map((db) => db.id)}}
projectId={proj.id} projectId={proj.id}
/> />
</PageContent> </PageContent>
@@ -27,7 +27,7 @@ export default async function RoutePage(props: PageParams<{ }>) {
}, },
orderBy: (db, {desc}) => [desc(db.createdAt)], orderBy: (db, {desc}) => [desc(db.createdAt)],
}) })
).filter((db) => db.project !== null) as DatabaseWith[]; ).filter((db) => db.project == null) as DatabaseWith[];
const org = await db.query.organization.findFirst({ const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug), where: eq(drizzleDb.schemas.organization.slug, organization.slug),
@@ -35,6 +35,8 @@ export default async function RoutePage(props: PageParams<{ }>) {
if (!org) notFound(); if (!org) notFound();
console.log("ici",availableDatabases)
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
@@ -45,6 +45,56 @@ export default async function RoutePage(props: PageParams<{}>) {
}); });
console.log("evolution",backupsEvolution);
// const tomorrow = new Date();
// tomorrow.setDate(tomorrow.getDate() + 1);
//
// const backupsEvolution = [
// {
// id: '22e84aa4-228c-45b3-82ec-846a639cd509',
// createdAt: new Date()
// },
// {
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
// createdAt: new Date()
// },
// {
// id: '6a6106fe-7f45-48eb-a56f-0a1e734126a1',
// createdAt: new Date()
// },
// {
// 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: '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({
+8 -6
View File
@@ -6,6 +6,7 @@ import {Badge} from "@/components/ui/badge";
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/button-delete-account/button-delete-account"; import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/button-delete-account/button-delete-account";
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/avatar/avatar-with-upload"; import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/avatar/avatar-with-upload";
import {currentUser} from "@/lib/auth/current-user"; import {currentUser} from "@/lib/auth/current-user";
import {getAccounts, getSessions} from "@/lib/auth/auth";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser(); const user = await currentUser();
@@ -18,8 +19,8 @@ export default async function RoutePage(props: PageParams<{}>) {
return notFound(); return notFound();
} }
// const sessions = await getSessions(); const sessions = await getSessions();
// const accounts = await getAccounts(); const accounts = await getAccounts();
return ( return (
<Page> <Page>
@@ -43,18 +44,19 @@ export default async function RoutePage(props: PageParams<{}>) {
{/* <ButtonDeleteAccount text="Delete my account"/>*/} {/* <ButtonDeleteAccount text="Delete my account"/>*/}
{/*</PageActions>*/} {/*</PageActions>*/}
</div> </div>
<PageContent> <PageContent >
<UserForm <UserForm
userId={user.id} userId={user.id}
sessions={sessions} accounts={accounts}
defaultValues={{ defaultValues={{
name: user.name, name: user.name,
email: user.email, email: user.email,
role: user.role ?? undefined, role: user.role ?? undefined,
}} }}
/> />
<div className="mt-4 sm:hidden"> {/*<div className="mt-4 sm:hidden ">*/}
<ButtonDeleteAccount text="Delete my account"/> {/* <ButtonDeleteAccount text="Delete my account"/>*/}
</div> {/*</div>*/}
</PageContent> </PageContent>
</Page> </Page>
); );
+1 -1
View File
@@ -6,7 +6,7 @@ echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ "
echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ " echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ " echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
echo " " echo " "
echo " Community Edition v1.0.0 " echo " Community Edition v1.1.1 "
echo " " echo " "
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "portabase", "name": "portabase",
"version": "1.1.0", "version": "1.1.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack -p 8887", "dev": "next dev --turbopack -p 8887",
@@ -0,0 +1,28 @@
import {Icon} from "@iconify/react";
import {CircleHelp, KeyRound} from "lucide-react";
export const providerSwitch = (provider: string) => {
switch (provider) {
case "google":
return (
<div className="p-4">
<Icon icon={"logos:google"} height="24" />
</div>
);
case "credential":
return (
<div className="flex flex-row gap-x-2 items-center p-4">
<KeyRound height="24" />
<span>Email and Password</span>
</div>
);
default:
return (
<div className="flex flex-row gap-x-2 items-center p-4">
<CircleHelp height="24" />
<span>No credentials</span>
</div>
);
}
}
@@ -1,20 +1,20 @@
"use client"; "use client";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import { SettingsEmailTab } from "@/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab"; import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab";
import { SettingsStorageTab } from "@/components/wrappers/dashboard/admin/admin-storage-tab/settings-storage-tab"; import {SettingsStorageTab} from "@/components/wrappers/dashboard/admin/admin-storage-tab/settings-storage-tab";
import { AdminUsersTable } from "@/components/wrappers/dashboard/admin/admin-user-table"; import {User, UserWithAccounts} from "@/db/schema/01_user";
import { User } from "@/db/schema/01_user"; import {Setting} from "@/db/schema/00_setting";
import { Setting } from "@/db/schema/00_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";
export type AdminTabsProps = { export type AdminTabsProps = {
users: User[]; users: UserWithAccounts[];
settings: Setting; settings: Setting;
}; };
export const AdminTabs = ({ users, settings }: AdminTabsProps) => { export const AdminTabs = ({users, settings}: AdminTabsProps) => {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@@ -44,13 +44,13 @@ export const AdminTabs = ({ users, settings }: AdminTabsProps) => {
</TabsList> </TabsList>
<TabsContent value="users"> <TabsContent value="users">
<AdminUsersTable users={users} /> <AdminUsersTable users={users}/>
</TabsContent> </TabsContent>
<TabsContent value="email"> <TabsContent value="email">
<SettingsEmailTab settings={settings} /> <SettingsEmailTab settings={settings}/>
</TabsContent> </TabsContent>
<TabsContent value="storage"> <TabsContent value="storage">
<SettingsStorageTab settings={settings} /> <SettingsStorageTab settings={settings}/>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
); );
@@ -0,0 +1,81 @@
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {useMutation} from "@tanstack/react-query";
import {ColumnDef} from "@tanstack/react-table";
import {Unlink} from "lucide-react";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {unlinkUserProviderAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
export const accountsColumns: ColumnDef<{
id: string;
provider: string;
createdAt: Date;
updatedAt: Date;
accountId: string;
scopes: string[];
}>[] = [
{
id: "provider",
header: "Provider",
cell: ({row}) => {
return (
<div>
{providerSwitch(row.original.provider)}
</div>
)
},
},
{
header: "Action",
id: "actions",
cell: ({row, table}) => {
const router = useRouter();
const mutation = useMutation({
mutationFn: async () => {
if (row.original.provider === "credential") {
toast.error(`This provider cannot be unlinked.`);
router.refresh();
return;
}
if (table.getRowModel().rows.length <= 1) {
toast.error(`You only have one provider linked to your account. Please add more one to unlink this`);
router.refresh();
return;
}
const status = await unlinkUserProviderAction({
provider: row.original.provider,
account: row.original.accountId,
});
if (status?.serverError || !status) {
toast.error(status?.serverError);
return;
}
toast.success(`Provider unlinked successfully.`);
router.refresh();
},
});
return (
<div className="flex items-center gap-2">
<ButtonWithLoading
variant="outline"
text=""
disabled={row.original.provider === "credential" || table.getRowModel().rows.length <= 1}
icon={<Unlink color="red" size={15}/>}
onClick={async () => {
await mutation.mutateAsync();
}}
size="icon"
/>
</div>
);
},
},
];
@@ -0,0 +1,26 @@
import {User, UserWithAccounts} from "@/db/schema/01_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";
export type AdminUsersTableProps = {
users: UserWithAccounts[];
};
export const AdminUsersTable = (props: AdminUsersTableProps) => {
const {users} = props;
return (
<div className="flex flex-col gap-y-4 h-full py-4">
<Card>
<CardHeader>
<CardTitle>Active users</CardTitle>
<CardDescription>Manage your users</CardDescription>
</CardHeader>
<CardContent>
<DataTable columns={usersColumnsAdmin} data={users}/>
</CardContent>
</Card>
</div>
);
};
@@ -9,22 +9,20 @@ import {useState} from "react";
import {Trash2} from "lucide-react"; import {Trash2} from "lucide-react";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action"; import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading"; import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {User} from "@/db/schema/01_user"; import {UserWithAccounts} from "@/db/schema/01_user";
import {authClient, useSession} from "@/lib/auth/auth-client"; import {authClient, useSession} from "@/lib/auth/auth-client";
import {formatFrenchDate} from "@/utils/date-formatting"; import {formatFrenchDate} from "@/utils/date-formatting";
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
export const usersColumnsAdmin: ColumnDef<User>[] = [ export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
{ {
accessorKey: "role", accessorKey: "role",
header: "Role", header: "Role",
cell: ({row}) => { cell: ({row}) => {
const [role, setRole] = useState<string>(row.getValue("role")); const [role, setRole] = useState<string>(row.getValue("role"));
const {data: session, isPending, error} = authClient.useSession(); const {data: session, isPending, error} = authClient.useSession();
const isSuperAdmin = session?.user.role == "superadmin";
const isCurrentUser = session?.user.email === row.original.email; const isCurrentUser = session?.user.email === row.original.email;
const updateMutation = useMutation({ const updateMutation = useMutation({
@@ -51,11 +49,10 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
} }
return ( return (
<Badge <Badge
className={isCurrentUser ? "cursor-not-allowed opacity-50" : "cursor-pointer"} className={isCurrentUser || !isSuperAdmin ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
onClick={isCurrentUser ? undefined : () => handleUpdateRole()} onClick={isCurrentUser || !isSuperAdmin ? undefined : () => handleUpdateRole()}
variant="outline" variant="outline"
> >
{role} {role}
@@ -71,6 +68,21 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
accessorKey: "email", accessorKey: "email",
header: "Email", header: "Email",
}, },
{
accessorKey: "accounts",
header: "Provider ID",
cell: ({row}) => {
return(
<div>
{row.original.accounts.map((item) => (
<div key={item.id}>
{providerSwitch(item.providerId)}
</div>
))}
</div>
)
}
},
{ {
accessorKey: "updatedAt", accessorKey: "updatedAt",
header: "Updated At", header: "Updated At",
@@ -84,6 +96,7 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
cell: ({row}) => { cell: ({row}) => {
const router = useRouter(); const router = useRouter();
const {data: session, isPending} = useSession(); const {data: session, isPending} = useSession();
const isSuperAdmin = session?.user.role == "superadmin";
const mutation = useMutation({ const mutation = useMutation({
mutationFn: () => deleteUserAction(row.original.id), mutationFn: () => deleteUserAction(row.original.id),
@@ -95,16 +108,16 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ButtonWithLoading <ButtonWithLoading
disabled={!session || session?.user.email === row.original.email} disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
variant="outline" variant="outline"
text="" text=""
icon={<Trash2 color="red" size={15}/>} icon={<Trash2 color="red" size={15}/>}
onClick={async () => { onClick={async () => {
await mutation.mutateAsync(); await mutation.mutateAsync();
}} }}
size="sm" size="sm"
/> />
</div> </div>
); );
}, },
@@ -0,0 +1,87 @@
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
import { useMutation } from "@tanstack/react-query";
import { ColumnDef } from "@tanstack/react-table";
import { Session } from "better-auth";
import { Unlink } from "lucide-react";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import detectOSWithUA from "@/utils/os-parser";
import { Icon } from "@iconify/react";
import { authClient } from "@/lib/auth/auth-client";
import { timeAgo } from "@/utils/date-formatting";
import {deleteUserSessionAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
export const sessionsColumns: ColumnDef<Session>[] = [
{
accessorKey: "expiresAt",
header: "Expires At",
cell: ({ row }) => {
return timeAgo(row.original.expiresAt);
},
},
{
accessorKey: "ipAddress",
header: "IP Address",
},
{
id: "device",
header: "Device",
cell: ({ row }) => {
const os = detectOSWithUA(row.original.userAgent!);
return (
<div className="flex flex-row gap-x-2 items-center pt-4 pb-4">
{os.icon && <Icon icon={`logos:${os.icon.name}`} height={os.icon.size.height} width={os.icon.size.width} />}
{os.showText && <span>{os.name}</span>}
</div>
);
},
},
{
accessorKey: "userAgent",
header: "User Agent",
},
{
header: "Action",
id: "actions",
cell: ({ row }) => {
const router = useRouter();
const { data: session } = authClient.useSession();
const mutation = useMutation({
mutationFn: async () => {
if (session?.session.id === row.original.id) {
toast.error(`Unable to unlink active session.`);
router.refresh();
return;
}
const status = await deleteUserSessionAction(row.original.token);
if (status?.serverError || !status) {
toast.error(status?.serverError);
return;
}
toast.success(`Session deleted successfully.`);
router.refresh();
},
});
return (
<div className="flex items-center gap-2">
<ButtonWithLoading
variant="outline"
disabled={session?.session.id === row.original.id}
text=""
icon={<Unlink color="red" size={15} />}
onClick={async () => {
await mutation.mutateAsync();
}}
size="icon"
/>
</div>
);
},
},
];
@@ -1,21 +0,0 @@
import { usersColumnsAdmin } from "@/components/wrappers/dashboard/admin/columns-users";
import { User } from "@/db/schema/01_user";
import { DataTable } from "../../common/table/data-table";
export type AdminUsersTableProps = {
users: User[];
};
export const AdminUsersTable = (props: AdminUsersTableProps) => {
const { users } = props;
return (
<div className="flex flex-col h-full py-4">
<div className="flex gap-4 h-fit justify-between">
<h1>List of Portabase's users</h1>
</div>
<div className="mt-5 h-full">
<DataTable columns={usersColumnsAdmin} data={users} />
</div>
</div>
);
};
@@ -20,9 +20,11 @@ export type CronButtonProps = {
export const CronButton = (props: CronButtonProps) => { export const CronButton = (props: CronButtonProps) => {
const router = useRouter(); const router = useRouter();
const [isSwitched, setIsSwitched] = useState(props.database.backupPolicy !== null); const [isSwitched, setIsSwitched] = useState(props.database.backupPolicy !== null);
const [open, setOpen] = useState(false);
const updateDatabaseBackupPolicy = useMutation({ const updateDatabaseBackupPolicy = useMutation({
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }), mutationFn: (value: string) =>
updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }),
onSuccess: () => { onSuccess: () => {
toast.success(`Method updated successfully.`); toast.success(`Method updated successfully.`);
router.refresh(); router.refresh();
@@ -34,15 +36,15 @@ export const CronButton = (props: CronButtonProps) => {
const handleTypeChange = async (state: boolean) => { const handleTypeChange = async (state: boolean) => {
setIsSwitched(state); setIsSwitched(state);
if (state == false) { if (!state) {
await updateDatabaseBackupPolicy.mutateAsync(""); await updateDatabaseBackupPolicy.mutateAsync("");
} }
}; };
return ( return (
<Dialog> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" {...props}> <Button variant="outline" {...props} onClick={() => setOpen(true)}>
<Clock9 /> <Clock9 />
</Button> </Button>
</DialogTrigger> </DialogTrigger>
@@ -64,7 +66,14 @@ export const CronButton = (props: CronButtonProps) => {
id="type-mode" id="type-mode"
/> />
</div> </div>
{isSwitched ? <CronInput database={props.database} /> : null} {isSwitched ? (
<CronInput
database={props.database}
onSuccess={() => {
setOpen(false);
}}
/>
) : null}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
@@ -1,25 +1,27 @@
import { AdvancedCronSelect } from "./advanced-cron-select"; import {AdvancedCronSelect} from "./advanced-cron-select";
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/cron-button/cron.action"; import {updateDatabaseBackupPolicyAction} from "@/components/wrappers/dashboard/database/cron-button/cron.action";
import { useMutation } from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import { useState } from "react"; import {useState} from "react";
import { useRouter } from "next/navigation"; import {useRouter} from "next/navigation";
import { toast } from "sonner"; import {toast} from "sonner";
import { Button } from "@/components/ui/button"; import {Button} from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import {Separator} from "@/components/ui/separator";
import {Database} from "@/db/schema/06_database"; import {Database} from "@/db/schema/06_database";
export type CronInputProps = { export type CronInputProps = {
database: Database; database: Database;
onSuccess?: () => void;
}; };
export const CronInput = ({ database }: CronInputProps) => { export const CronInput = ({database, onSuccess}: CronInputProps) => {
const [cron, setCron] = useState<string>(database.backupPolicy ?? "* * * * *"); const [cron, setCron] = useState<string>(database.backupPolicy ?? "* * * * *");
const router = useRouter(); const router = useRouter();
const updateBackupPolicy = useMutation({ const updateBackupPolicy = useMutation({
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({ databaseId: database.id, backupPolicy: value }), mutationFn: (value: string) => updateDatabaseBackupPolicyAction({databaseId: database.id, backupPolicy: value}),
onSuccess: () => { onSuccess: () => {
toast.success(`Cron updated successfully.`); toast.success(`Cron updated successfully.`);
onSuccess?.()
router.refresh(); router.refresh();
}, },
onError: () => { onError: () => {
@@ -29,7 +31,7 @@ export const CronInput = ({ database }: CronInputProps) => {
const handleChangeCron = (type: "minute" | "hour" | "day-of-month" | "month" | "day-of-week", value: string) => { const handleChangeCron = (type: "minute" | "hour" | "day-of-month" | "month" | "day-of-week", value: string) => {
const cronParts = cron.split(" "); const cronParts = cron.split(" ");
const indexMap = { minute: 0, hour: 1, "day-of-month": 2, month: 3, "day-of-week": 4 }; const indexMap = {minute: 0, hour: 1, "day-of-month": 2, month: 3, "day-of-week": 4};
cronParts[indexMap[type]] = value; cronParts[indexMap[type]] = value;
setCron(cronParts.join(" ")); setCron(cronParts.join(" "));
}; };
@@ -44,7 +46,7 @@ export const CronInput = ({ database }: CronInputProps) => {
<AdvancedCronSelect <AdvancedCronSelect
id="minute" id="minute"
label="Minute" label="Minute"
options={Array.from({ length: 60 }, (_, i) => String(i).padStart(2, "0"))} options={Array.from({length: 60}, (_, i) => String(i).padStart(2, "0"))}
type="minute" type="minute"
value={cron.split(" ")[0]} value={cron.split(" ")[0]}
defaultValue={cron.split(" ")[0]} defaultValue={cron.split(" ")[0]}
@@ -53,7 +55,7 @@ export const CronInput = ({ database }: CronInputProps) => {
<AdvancedCronSelect <AdvancedCronSelect
id="hour" id="hour"
label="Hour" label="Hour"
options={Array.from({ length: 24 }, (_, i) => String(i).padStart(2, "0"))} options={Array.from({length: 24}, (_, i) => String(i).padStart(2, "0"))}
type="hour" type="hour"
value={cron.split(" ")[1]} value={cron.split(" ")[1]}
defaultValue={cron.split(" ")[1]} defaultValue={cron.split(" ")[1]}
@@ -62,7 +64,7 @@ export const CronInput = ({ database }: CronInputProps) => {
<AdvancedCronSelect <AdvancedCronSelect
id="day-of-month" id="day-of-month"
label="Day of Month" label="Day of Month"
options={Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, "0"))} options={Array.from({length: 31}, (_, i) => String(i + 1).padStart(2, "0"))}
type="day-of-month" type="day-of-month"
value={cron.split(" ")[2]} value={cron.split(" ")[2]}
defaultValue={cron.split(" ")[2]} defaultValue={cron.split(" ")[2]}
@@ -86,13 +88,14 @@ export const CronInput = ({ database }: CronInputProps) => {
defaultValue={cron.split(" ")[4]} defaultValue={cron.split(" ")[4]}
onValueChange={(value) => handleChangeCron("day-of-week", value)} onValueChange={(value) => handleChangeCron("day-of-week", value)}
/> />
<Separator /> <Separator/>
<div className="grid gap-2"> <div className="grid gap-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="font-semibold">Cron Expression</div> <div className="font-semibold">Cron Expression</div>
<div className="font-mono text-muted-foreground">{cron}</div> <div className="font-mono text-muted-foreground">{cron}</div>
</div> </div>
<div className="text-sm text-muted-foreground">This cron expression determines when the job will run.</div> <div className="text-sm text-muted-foreground">This cron expression determines when the job will run.
</div>
</div> </div>
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<Button <Button
@@ -5,6 +5,7 @@ import { UserSchema } from "@/components/wrappers/dashboard/profile/user-form/us
import { db } from "@/db"; import { db } from "@/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {revokeSession, unlinkAccount} from "@/lib/auth/auth";
export const updateUserAction = userAction export const updateUserAction = userAction
.schema( .schema(
@@ -19,3 +20,23 @@ export const updateUserAction = userAction
data: updatedUser, data: updatedUser,
}; };
}); });
export const deleteUserSessionAction = userAction.schema(z.string()).action(async ({ parsedInput }) => {
const status = await revokeSession(parsedInput);
return status;
});
export const unlinkUserProviderAction = userAction
.schema(
z.object({
provider: z.string(),
account: z.string(),
})
)
.action(async ({ parsedInput }) => {
const status = await unlinkAccount(parsedInput.provider, parsedInput.account);
return status;
});
@@ -11,13 +11,26 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/user-form/user-form.schema"; import { UserSchema, UserType } from "@/components/wrappers/dashboard/profile/user-form/user-form.schema";
import { toast } from "sonner"; import { toast } from "sonner";
import { updateUserAction } from "@/components/wrappers/dashboard/profile/user-form/user-form.action"; import { updateUserAction } from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {sessionsColumns} from "@/components/wrappers/dashboard/admin/admin-user-tab/sessions/table-columns";
import {accountsColumns} from "@/components/wrappers/dashboard/admin/admin-user-tab/accounts/table-columns";
import {Session} from "better-auth";
export type userFormProps = { export type UserFormProps = {
defaultValues?: UserType; defaultValues?: UserType;
userId?: string; userId?: string;
sessions: Session[];
accounts: {
id: string;
provider: string;
createdAt: Date;
updatedAt: Date;
accountId: string;
scopes: string[];
}[];
}; };
export const UserForm = (props: userFormProps) => { export const UserForm = (props: UserFormProps) => {
const isCreate = !Boolean(props.defaultValues); const isCreate = !Boolean(props.defaultValues);
const form = useZodForm({ const form = useZodForm({
@@ -49,6 +62,7 @@ export const UserForm = (props: userFormProps) => {
}); });
return ( return (
<div className="flex flex-col gap-y-4 h-full py-4">
<TooltipProvider> <TooltipProvider>
<Card> <Card>
<CardHeader> <CardHeader>
@@ -93,6 +107,25 @@ export const UserForm = (props: userFormProps) => {
</Form> </Form>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle>Active sessions</CardTitle>
<CardDescription>Manage your active sessions</CardDescription>
</CardHeader>
<CardContent>
<DataTable columns={sessionsColumns} data={props.sessions} enableSelect={false}/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Auth providers</CardTitle>
<CardDescription>Manage your active auth providers</CardDescription>
</CardHeader>
<CardContent>
<DataTable columns={accountsColumns} data={props.accounts} enableSelect={false}/>
</CardContent>
</Card>
</TooltipProvider> </TooltipProvider>
</div>
); );
}; };
@@ -4,13 +4,19 @@ import {userAction} from "@/safe-actions";
import {z} from "zod"; import {z} from "zod";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import {ServerActionResult} from "@/types/action-type"; import {ServerActionResult} from "@/types/action-type";
import {eq} from "drizzle-orm"; import {and, eq} from "drizzle-orm";
import {db} from "@/db"; import {db} from "@/db";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
export const deleteProjectAction = userAction.schema(z.string()).action(async ({parsedInput}): Promise<ServerActionResult<typeof drizzleDb.schemas.project.$inferSelect>> => { export const deleteProjectAction = userAction.schema(z.string()).action(async ({parsedInput}): Promise<ServerActionResult<typeof drizzleDb.schemas.project.$inferSelect>> => {
try { try {
const uuid = uuidv4(); const uuid = uuidv4();
await db
.update(drizzleDb.schemas.database)
.set({
projectId: null,
})
.where(eq(drizzleDb.schemas.database.projectId, parsedInput));
const updatedProjects = await db const updatedProjects = await db
.update(drizzleDb.schemas.project) .update(drizzleDb.schemas.project)
@@ -27,6 +33,7 @@ export const deleteProjectAction = userAction.schema(z.string()).action(async ({
throw new Error("Project not found or update failed"); throw new Error("Project not found or update failed");
} }
return { return {
success: true, success: true,
value: updatedProject, value: updatedProject,
@@ -36,6 +43,7 @@ export const deleteProjectAction = userAction.schema(z.string()).action(async ({
}, },
}; };
} catch (error) { } catch (error) {
console.log(error);
return { return {
success: false, success: false,
actionError: { actionError: {
@@ -27,7 +27,7 @@ export const ProjectForm = (props: projectFormProps) => {
const formatDatabasesList = (databases: DatabaseWith[]) => { const formatDatabasesList = (databases: DatabaseWith[]) => {
return databases.map((database) => ({ return databases.map((database) => ({
value: database.id, value: database.id,
label: `${database.name} (${database.id}) | ${database.agent.name}`, label: `${database.name} (${database.agentDatabaseId}) | ${database.agent.name}`,
})); }));
}; };
@@ -89,15 +89,15 @@ export const ProjectForm = (props: projectFormProps) => {
await mutation.mutateAsync(values); await mutation.mutateAsync(values);
}} }}
> >
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
defaultValue=""
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Project 1" {...field} /> <Input placeholder="Project 1" {...field} value={field.value ?? ""} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -15,25 +15,38 @@ export type evolutionLineChartProps = {
export function EvolutionLineChart(props: evolutionLineChartProps) { export function EvolutionLineChart(props: evolutionLineChartProps) {
const { data } = props; const { data } = props;
// Process data to calculate cumulative count // const cumulativeData = data.reduce(
const cumulativeData = data.reduce( // (acc, backup) => {
(acc, backup) => { // const date = backup.createdAt.toISOString().split("T")[0]; // Format as YYYY-MM-DD
const date = backup.createdAt.toISOString().split("T")[0]; // Format as YYYY-MM-DD //
// // Increment count for the current date or initialize it
// if (acc.length && acc[acc.length - 1].date === date) {
// acc[acc.length - 1].count += 1;
// } else {
// const lastCount = acc.length ? acc[acc.length - 1].count : 0;
// acc.push({ date, count: lastCount + 1 });
// }
//
// return acc;
// },
// [] as { date: string; count: number }[]
// );
const dailyData = data
.reduce((acc, backup) => {
const date = backup.createdAt.toISOString().split("T")[0]; // Format: YYYY-MM-DD
// Increment count for the current date or initialize it // Find if the date already exists in the accumulator
if (acc.length && acc[acc.length - 1].date === date) { const existing = acc.find(item => item.date === date);
acc[acc.length - 1].count += 1;
if (existing) {
existing.count += 1;
} else { } else {
const lastCount = acc.length ? acc[acc.length - 1].count : 0; acc.push({ date, count: 1 });
acc.push({ date, count: lastCount + 1 });
} }
return acc; return acc;
}, }, [] as { date: string; count: number }[]);
[] as { date: string; count: number }[]
);
console.log(cumulativeData);
const chartConfig = { const chartConfig = {
date: { date: {
@@ -50,7 +63,7 @@ export function EvolutionLineChart(props: evolutionLineChartProps) {
<ChartContainer config={chartConfig}> <ChartContainer config={chartConfig}>
<LineChart <LineChart
accessibilityLayer accessibilityLayer
data={cumulativeData} data={dailyData}
margin={{ margin={{
left: 12, left: 12,
right: 12, right: 12,
@@ -68,7 +81,9 @@ export function EvolutionLineChart(props: evolutionLineChartProps) {
/> />
<YAxis /> <YAxis />
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} /> <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
<Line dataKey="count" type="linear" stroke="var(--color-desktop)" strokeWidth={2} dot={false} /> <Line dataKey="count" type="linear" stroke="#60a5fa" strokeWidth={2} dot={false} />
{/*<Line dataKey="count" type="linear" stroke="var(--color-desktop)" strokeWidth={2} dot={false} />*/}
</LineChart> </LineChart>
</ChartContainer> </ChartContainer>
); );
+10 -1
View File
@@ -3,9 +3,10 @@ import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createSelectSchema } from "drizzle-zod"; import { createSelectSchema } from "drizzle-zod";
import { z } from "zod"; import { z } from "zod";
import { project } from "./05_project"; import { project } from "./05_project";
import {member} from "@/db/schema/03_member"; import {member, OrganizationMember} from "@/db/schema/03_member";
import {invitation} from "@/db/schema/04_invitation"; import {invitation} from "@/db/schema/04_invitation";
import {organization} from "@/db/schema/02_organization"; import {organization} from "@/db/schema/02_organization";
import {Account} from "better-auth";
export const user = pgTable("user", { export const user = pgTable("user", {
id: uuid("id").defaultRandom().primaryKey(), id: uuid("id").defaultRandom().primaryKey(),
@@ -94,3 +95,11 @@ export const projectRelations = relations(project, ({ one }) => ({
export const userSchema = createSelectSchema(user); export const userSchema = createSelectSchema(user);
export type User = z.infer<typeof userSchema>; export type User = z.infer<typeof userSchema>;
type FixedAccount = Omit<Account, 'updatedAt'> & {
updatedAt: Date | null;
};
export type UserWithAccounts = User & {
accounts: FixedAccount[];
};
+4 -1
View File
@@ -1,8 +1,9 @@
import {createAuthClient} from "better-auth/react"; import {createAuthClient} from "better-auth/react";
import {adminClient, organizationClient} from "better-auth/client/plugins"; import {adminClient, inferAdditionalFields, organizationClient} from "better-auth/client/plugins";
import {env} from "@/env.mjs"; import {env} from "@/env.mjs";
import {ac, user, admin as adminRole, pending, superadmin, orgAdmin, orgMember, orgOwner} from "./permissions"; import {ac, user, admin as adminRole, pending, superadmin, orgAdmin, orgMember, orgOwner} from "./permissions";
import {auth} from "@/lib/auth/auth";
export const authClient = createAuthClient({ export const authClient = createAuthClient({
baseURL: env.NEXT_PUBLIC_PROJECT_URL, baseURL: env.NEXT_PUBLIC_PROJECT_URL,
@@ -24,7 +25,9 @@ export const authClient = createAuthClient({
superadmin, superadmin,
}, },
}), }),
inferAdditionalFields<typeof auth>(),
], ],
}); });
export const {signIn, signOut, signUp, useSession, listAccounts, admin} = authClient; export const {signIn, signOut, signUp, useSession, listAccounts, admin} = authClient;
+3 -3
View File
@@ -1,12 +1,12 @@
import {format} from "date-fns"; import {format, formatDistanceToNow} from "date-fns";
export function humanReadableDate(rawDate: string | number | Date) { export function humanReadableDate(rawDate: string | number | Date) {
return formatFrenchDate(rawDate); return formatFrenchDate(rawDate);
} }
export function timeAgo(rawDate: string | number | Date) { export function timeAgo(rawDate: string | number | Date) {
const date = new Date(rawDate) const date = new Date(rawDate);
return "Not implemented" return formatDistanceToNow(date, { addSuffix: true });
} }
export function formatDateLastContact(lastContact: string | number | Date | null) { export function formatDateLastContact(lastContact: string | number | Date | null) {
+148
View File
@@ -0,0 +1,148 @@
export default function detectOSWithUA(userAgent: string) {
const osList = [
{
name: "Windows",
keywords: ["Win", "NT", "Windows"],
icon: {
name: "microsoft-windows-icon",
size: {
width: 24,
height: 0,
},
},
showText: true,
},
{
name: "Ubuntu",
keywords: ["Ubuntu"],
icon: {
name: "ubuntu",
size: {
width: 24,
height: 0,
},
},
showText: true,
},
{
name: "iOS",
keywords: ["iOS"],
icon: {
name: "ios",
size: {
width: 24,
height: 0,
},
},
showText: false,
},
{
name: "iPadOS",
keywords: ["iPadOS", "iPad"],
icon: {
name: "ios",
size: {
width: 24,
height: 0,
},
},
showText: true,
},
{
name: "MacOS",
keywords: ["MacOS", "Macintosh", "Mac OS", "Mac OS X"],
icon: {
name: "macos",
size: {
width: 0,
height: 16,
},
},
showText: false,
},
{
name: "Android",
keywords: ["Android"],
icon: {
name: "android-icon",
size: {
width: 24,
height: 0,
},
},
showText: true,
},
{
name: "Linux",
keywords: ["X11", "Linux"],
icon: {
name: "linux-tux",
size: {
width: 24,
height: 0,
},
},
showText: true,
},
{
name: "Playstation 4",
keywords: ["PlayStation 4"],
showText: true,
},
{
name: "Playstation 5",
keywords: ["PlayStation 5"],
showText: true,
},
{
name: "Xbox Series X",
keywords: ["Xbox Series X"],
showText: true,
},
{
name: "Xbox One S",
keywords: ["XBOX_ONE_ED"],
showText: true,
},
{
name: "Xbox One",
keywords: ["Xbox One"],
showText: true,
},
{
name: "Nintendo Switch",
keywords: ["Nintendo Switch"],
showText: true,
},
{
name: "AppleTV",
keywords: ["AppleTV"],
icon: {
name: "apple",
size: {
width: 24,
height: 0,
},
},
showText: true,
},
];
for (let os of osList) {
if (os.keywords.some((keyword) => userAgent.includes(keyword))) {
return os;
}
}
return {
name: "Unknown OS",
icon: {
name: "unknown",
size: {
width: 24,
height: 0,
},
},
showText: true,
};
}