mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on statistics
This commit is contained in:
@@ -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";
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
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 { AdminUsersTable } from "@/components/wrappers/dashboard/admin/admin-user-table";
|
||||
import { User } from "@/db/schema/01_user";
|
||||
import { Setting } from "@/db/schema/00_setting";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
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 {User, UserWithAccounts} from "@/db/schema/01_user";
|
||||
import {Setting} from "@/db/schema/00_setting";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useRouter, useSearchParams} from "next/navigation";
|
||||
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table";
|
||||
|
||||
export type AdminTabsProps = {
|
||||
users: User[];
|
||||
users: UserWithAccounts[];
|
||||
settings: Setting;
|
||||
};
|
||||
|
||||
export const AdminTabs = ({ users, settings }: AdminTabsProps) => {
|
||||
export const AdminTabs = ({users, settings}: AdminTabsProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -44,13 +44,13 @@ export const AdminTabs = ({ users, settings }: AdminTabsProps) => {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users">
|
||||
<AdminUsersTable users={users} />
|
||||
<AdminUsersTable users={users}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="email">
|
||||
<SettingsEmailTab settings={settings} />
|
||||
<SettingsEmailTab settings={settings}/>
|
||||
</TabsContent>
|
||||
<TabsContent value="storage">
|
||||
<SettingsStorageTab settings={settings} />
|
||||
<SettingsStorageTab settings={settings}/>
|
||||
</TabsContent>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
+32
-19
@@ -9,22 +9,20 @@ import {useState} from "react";
|
||||
import {Trash2} from "lucide-react";
|
||||
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
|
||||
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 {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",
|
||||
header: "Role",
|
||||
cell: ({row}) => {
|
||||
const [role, setRole] = useState<string>(row.getValue("role"));
|
||||
|
||||
|
||||
const {data: session, isPending, error} = authClient.useSession();
|
||||
|
||||
|
||||
|
||||
const isSuperAdmin = session?.user.role == "superadmin";
|
||||
const isCurrentUser = session?.user.email === row.original.email;
|
||||
|
||||
const updateMutation = useMutation({
|
||||
@@ -51,11 +49,10 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={isCurrentUser ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
|
||||
onClick={isCurrentUser ? undefined : () => handleUpdateRole()}
|
||||
className={isCurrentUser || !isSuperAdmin ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
|
||||
onClick={isCurrentUser || !isSuperAdmin ? undefined : () => handleUpdateRole()}
|
||||
variant="outline"
|
||||
>
|
||||
{role}
|
||||
@@ -71,6 +68,21 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
||||
accessorKey: "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",
|
||||
header: "Updated At",
|
||||
@@ -84,6 +96,7 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
||||
cell: ({row}) => {
|
||||
const router = useRouter();
|
||||
const {data: session, isPending} = useSession();
|
||||
const isSuperAdmin = session?.user.role == "superadmin";
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteUserAction(row.original.id),
|
||||
@@ -95,16 +108,16 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonWithLoading
|
||||
disabled={!session || session?.user.email === row.original.email}
|
||||
variant="outline"
|
||||
text=""
|
||||
icon={<Trash2 color="red" size={15}/>}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
<ButtonWithLoading
|
||||
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||
variant="outline"
|
||||
text=""
|
||||
icon={<Trash2 color="red" size={15}/>}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
</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) => {
|
||||
const router = useRouter();
|
||||
const [isSwitched, setIsSwitched] = useState(props.database.backupPolicy !== null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const updateDatabaseBackupPolicy = useMutation({
|
||||
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }),
|
||||
mutationFn: (value: string) =>
|
||||
updateDatabaseBackupPolicyAction({ databaseId: props.database.id, backupPolicy: value }),
|
||||
onSuccess: () => {
|
||||
toast.success(`Method updated successfully.`);
|
||||
router.refresh();
|
||||
@@ -34,15 +36,15 @@ export const CronButton = (props: CronButtonProps) => {
|
||||
|
||||
const handleTypeChange = async (state: boolean) => {
|
||||
setIsSwitched(state);
|
||||
if (state == false) {
|
||||
if (!state) {
|
||||
await updateDatabaseBackupPolicy.mutateAsync("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" {...props}>
|
||||
<Button variant="outline" {...props} onClick={() => setOpen(true)}>
|
||||
<Clock9 />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -64,7 +66,14 @@ export const CronButton = (props: CronButtonProps) => {
|
||||
id="type-mode"
|
||||
/>
|
||||
</div>
|
||||
{isSwitched ? <CronInput database={props.database} /> : null}
|
||||
{isSwitched ? (
|
||||
<CronInput
|
||||
database={props.database}
|
||||
onSuccess={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { AdvancedCronSelect } from "./advanced-cron-select";
|
||||
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/cron-button/cron.action";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {AdvancedCronSelect} from "./advanced-cron-select";
|
||||
import {updateDatabaseBackupPolicyAction} from "@/components/wrappers/dashboard/database/cron-button/cron.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useState} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {Database} from "@/db/schema/06_database";
|
||||
|
||||
export type CronInputProps = {
|
||||
database: Database;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export const CronInput = ({ database }: CronInputProps) => {
|
||||
export const CronInput = ({database, onSuccess}: CronInputProps) => {
|
||||
const [cron, setCron] = useState<string>(database.backupPolicy ?? "* * * * *");
|
||||
const router = useRouter();
|
||||
|
||||
const updateBackupPolicy = useMutation({
|
||||
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({ databaseId: database.id, backupPolicy: value }),
|
||||
mutationFn: (value: string) => updateDatabaseBackupPolicyAction({databaseId: database.id, backupPolicy: value}),
|
||||
onSuccess: () => {
|
||||
toast.success(`Cron updated successfully.`);
|
||||
onSuccess?.()
|
||||
router.refresh();
|
||||
},
|
||||
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 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;
|
||||
setCron(cronParts.join(" "));
|
||||
};
|
||||
@@ -44,7 +46,7 @@ export const CronInput = ({ database }: CronInputProps) => {
|
||||
<AdvancedCronSelect
|
||||
id="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"
|
||||
value={cron.split(" ")[0]}
|
||||
defaultValue={cron.split(" ")[0]}
|
||||
@@ -53,7 +55,7 @@ export const CronInput = ({ database }: CronInputProps) => {
|
||||
<AdvancedCronSelect
|
||||
id="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"
|
||||
value={cron.split(" ")[1]}
|
||||
defaultValue={cron.split(" ")[1]}
|
||||
@@ -62,7 +64,7 @@ export const CronInput = ({ database }: CronInputProps) => {
|
||||
<AdvancedCronSelect
|
||||
id="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"
|
||||
value={cron.split(" ")[2]}
|
||||
defaultValue={cron.split(" ")[2]}
|
||||
@@ -86,13 +88,14 @@ export const CronInput = ({ database }: CronInputProps) => {
|
||||
defaultValue={cron.split(" ")[4]}
|
||||
onValueChange={(value) => handleChangeCron("day-of-week", value)}
|
||||
/>
|
||||
<Separator />
|
||||
<Separator/>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-semibold">Cron Expression</div>
|
||||
<div className="font-mono text-muted-foreground">{cron}</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 className="flex justify-between gap-2">
|
||||
<Button
|
||||
|
||||
@@ -5,6 +5,7 @@ import { UserSchema } from "@/components/wrappers/dashboard/profile/user-form/us
|
||||
import { db } from "@/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {revokeSession, unlinkAccount} from "@/lib/auth/auth";
|
||||
|
||||
export const updateUserAction = userAction
|
||||
.schema(
|
||||
@@ -19,3 +20,23 @@ export const updateUserAction = userAction
|
||||
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 { toast } from "sonner";
|
||||
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;
|
||||
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 form = useZodForm({
|
||||
@@ -49,6 +62,7 @@ export const UserForm = (props: userFormProps) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4 h-full py-4">
|
||||
<TooltipProvider>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -93,6 +107,25 @@ export const UserForm = (props: userFormProps) => {
|
||||
</Form>
|
||||
</CardContent>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+9
-1
@@ -4,13 +4,19 @@ import {userAction} from "@/safe-actions";
|
||||
import {z} from "zod";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
export const deleteProjectAction = userAction.schema(z.string()).action(async ({parsedInput}): Promise<ServerActionResult<typeof drizzleDb.schemas.project.$inferSelect>> => {
|
||||
try {
|
||||
const uuid = uuidv4();
|
||||
await db
|
||||
.update(drizzleDb.schemas.database)
|
||||
.set({
|
||||
projectId: null,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.database.projectId, parsedInput));
|
||||
|
||||
const updatedProjects = await db
|
||||
.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");
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: updatedProject,
|
||||
@@ -36,6 +43,7 @@ export const deleteProjectAction = userAction.schema(z.string()).action(async ({
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
|
||||
@@ -27,7 +27,7 @@ export const ProjectForm = (props: projectFormProps) => {
|
||||
const formatDatabasesList = (databases: DatabaseWith[]) => {
|
||||
return databases.map((database) => ({
|
||||
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);
|
||||
}}
|
||||
>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Project 1" {...field} />
|
||||
<Input placeholder="Project 1" {...field} value={field.value ?? ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -15,25 +15,38 @@ export type evolutionLineChartProps = {
|
||||
export function EvolutionLineChart(props: evolutionLineChartProps) {
|
||||
const { data } = props;
|
||||
|
||||
// Process data to calculate cumulative count
|
||||
const cumulativeData = data.reduce(
|
||||
(acc, backup) => {
|
||||
const date = backup.createdAt.toISOString().split("T")[0]; // Format as YYYY-MM-DD
|
||||
// const cumulativeData = data.reduce(
|
||||
// (acc, backup) => {
|
||||
// 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
|
||||
if (acc.length && acc[acc.length - 1].date === date) {
|
||||
acc[acc.length - 1].count += 1;
|
||||
// Find if the date already exists in the accumulator
|
||||
const existing = acc.find(item => item.date === date);
|
||||
|
||||
if (existing) {
|
||||
existing.count += 1;
|
||||
} else {
|
||||
const lastCount = acc.length ? acc[acc.length - 1].count : 0;
|
||||
acc.push({ date, count: lastCount + 1 });
|
||||
acc.push({ date, count: 1 });
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[] as { date: string; count: number }[]
|
||||
);
|
||||
}, [] as { date: string; count: number }[]);
|
||||
|
||||
console.log(cumulativeData);
|
||||
|
||||
const chartConfig = {
|
||||
date: {
|
||||
@@ -50,7 +63,7 @@ export function EvolutionLineChart(props: evolutionLineChartProps) {
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={cumulativeData}
|
||||
data={dailyData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
@@ -68,7 +81,9 @@ export function EvolutionLineChart(props: evolutionLineChartProps) {
|
||||
/>
|
||||
<YAxis />
|
||||
<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>
|
||||
</ChartContainer>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,10 @@ import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
||||
import { createSelectSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
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 {organization} from "@/db/schema/02_organization";
|
||||
import {Account} from "better-auth";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
@@ -94,3 +95,11 @@ export const projectRelations = relations(project, ({ one }) => ({
|
||||
|
||||
export const userSchema = createSelectSchema(user);
|
||||
export type User = z.infer<typeof userSchema>;
|
||||
|
||||
type FixedAccount = Omit<Account, 'updatedAt'> & {
|
||||
updatedAt: Date | null;
|
||||
};
|
||||
|
||||
export type UserWithAccounts = User & {
|
||||
accounts: FixedAccount[];
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
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 {ac, user, admin as adminRole, pending, superadmin, orgAdmin, orgMember, orgOwner} from "./permissions";
|
||||
import {auth} from "@/lib/auth/auth";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: env.NEXT_PUBLIC_PROJECT_URL,
|
||||
@@ -24,7 +25,9 @@ export const authClient = createAuthClient({
|
||||
superadmin,
|
||||
},
|
||||
}),
|
||||
inferAdditionalFields<typeof auth>(),
|
||||
],
|
||||
|
||||
});
|
||||
|
||||
export const {signIn, signOut, signUp, useSession, listAccounts, admin} = authClient;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {format} from "date-fns";
|
||||
import {format, formatDistanceToNow} from "date-fns";
|
||||
|
||||
export function humanReadableDate(rawDate: string | number | Date) {
|
||||
return formatFrenchDate(rawDate);
|
||||
}
|
||||
|
||||
export function timeAgo(rawDate: string | number | Date) {
|
||||
const date = new Date(rawDate)
|
||||
return "Not implemented"
|
||||
const date = new Date(rawDate);
|
||||
return formatDistanceToNow(date, { addSuffix: true });
|
||||
}
|
||||
|
||||
export function formatDateLastContact(lastContact: string | number | Date | null) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user