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
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
+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 { 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[];
};
+4 -1
View File
@@ -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;
+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) {
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) {
+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,
};
}