fix: Upload with new storage backend system.

This commit is contained in:
charlesgauthereau
2026-01-17 20:56:57 +01:00
parent 424fa581b9
commit 53f82792d8
16 changed files with 2395 additions and 555 deletions
@@ -1,75 +0,0 @@
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/profile2/user-form/user-form.action";
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
import {Account} from "better-auth";
export const accountsColumns: ColumnDef<Account>[] = [
{
id: "provider",
header: "Provider",
cell: ({row}) => {
return (
<div>
{providerSwitch(row.original.providerId)}
</div>
)
},
},
{
header: "Action",
id: "actions",
cell: ({row, table}) => {
const router = useRouter();
const mutation = useMutation({
mutationFn: async () => {
if (row.original.providerId === "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.providerId,
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"
disabled={row.original.providerId === "credential" || table.getRowModel().rows.length <= 1}
icon={<Unlink color="red" size={15}/>}
onClick={async () => {
await mutation.mutateAsync();
}}
size="icon"
/>
</div>
);
},
},
];
@@ -1,29 +0,0 @@
import {UserWithAccounts} from "@/db/schema/02_user";
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/users/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 className="h-full ">
<CardHeader>
<CardTitle>Active users</CardTitle>
<CardDescription>Manage your users</CardDescription>
</CardHeader>
<CardContent className="h-full">
<DataTable
enableSelect={false}
columns={usersColumnsAdmin}
data={users}/>
</CardContent>
</Card>
</div>
);
};
@@ -1,61 +0,0 @@
"use client";
import {Trash2} from "lucide-react";
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
import {useMutation} from "@tanstack/react-query";
import {useRouter} from "next/navigation";
import {toast} from "sonner";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile2/button-delete-account/delete-account.action";
export type ButtonDeleteUserProps = {
userId: string;
disabled?: boolean;
};
export const ButtonDeleteUser = (props: ButtonDeleteUserProps) => {
const router = useRouter();
const mutation = useMutation({
mutationFn: () => deleteUserAction(props.userId),
onSuccess: async () => {
toast.success("User deleted successfully.");
router.refresh();
},
});
return (
<div className="flex items-center gap-2">
<ButtonWithConfirm
title={""}
description="Are you sure you want to remove this user? This action cannot be undone."
button={{
main: {
disabled: !!props.disabled,
text: "",
variant: "outline",
size: "sm",
icon: <Trash2 color="red" size={15}/>,
},
confirm: {
className: "w-full",
text: "Delete",
icon: <Trash2/>,
variant: "destructive",
onClick: () => {
mutation.mutate()
},
},
cancel: {
className: "w-full",
text: "Cancel",
icon: <Trash2/>,
variant: "outline",
},
}}
isPending={mutation.isPending}
/>
</div>
);
};
@@ -1,108 +0,0 @@
"use client"
import {ColumnDef} from "@tanstack/react-table";
import {Badge} from "@/components/ui/badge";
import {updateUserAction} from "@/components/wrappers/dashboard/profile2/user-form/user-form.action";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {useState} from "react";
import {UserWithAccounts} from "@/db/schema/02_user";
import {authClient, useSession} from "@/lib/auth/auth-client";
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/users/button-delete-use";
import {formatLocalizedDate} from "@/utils/date-formatting";
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({
mutationFn: () => updateUserAction({id: row.original.id, data: {role: role}}),
onSuccess: () => {
toast.success(`User updated successfully.`);
},
onError: () => {
toast.error(`An error occurred while updating user information.`);
},
});
const handleUpdateRole = async () => {
const nextRole = role === "admin" ? "pending" : role === "pending" ? "user" : "admin";
setRole(nextRole);
await updateMutation.mutateAsync();
};
if (isPending) return null;
if (error || !session) {
return null;
}
return (
<Badge
className={isCurrentUser || !isSuperAdmin ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
onClick={isCurrentUser || !isSuperAdmin ? undefined : () => handleUpdateRole()}
variant="outline"
>
{role}
</Badge>
);
},
},
{
accessorKey: "name",
header: "Name",
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "accounts",
header: "Provider(s)",
cell: ({row}) => {
return (
<div className="flex flex-row items-center gap-x-2">
{row.original.accounts.map((item) => (
<div key={item.id}>
{providerSwitch(item.providerId, true)}
</div>
))}
</div>
)
}
},
{
accessorKey: "updatedAt",
header: "Updated At",
cell: ({row}) => {
return formatLocalizedDate(row.getValue("updatedAt"))
},
},
{
header: "Action",
id: "actions",
cell: ({row}) => {
const router = useRouter();
const {data: session, isPending, error} = useSession();
const isSuperAdmin = session?.user.role == "superadmin";
if (isPending || error) return null;
return (
<ButtonDeleteUser
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
userId={row.original.id}/>
);
},
},
];
@@ -1,89 +0,0 @@
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/profile2/user-form/user-form.action";
export const sessionsColumns: ColumnDef<Session>[] = [
{
accessorKey: "expiresAt",
header: "Expires At",
cell: ({row}) => {
return timeAgo(row.original.expiresAt);
},
},
{
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, isPending, error} = 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();
},
});
if (isPending || error) return null;
return (
<div className="flex items-center gap-2">
<ButtonWithLoading
isPending={mutation.isPending}
variant="outline"
disabled={session?.session.id === row.original.id}
icon={<Unlink color="red" size={15}/>}
onClick={async () => {
await mutation.mutateAsync();
}}
size="icon"
/>
</div>
);
},
},
];
@@ -10,6 +10,7 @@ import {eq} from "drizzle-orm";
import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
import {z} from "zod";
import {env} from "@/env.mjs";
import {storeBackupFiles} from "@/features/storages/helpers";
export const uploadBackupAction = userAction
@@ -23,7 +24,8 @@ export const uploadBackupAction = userAction
where: eq(drizzleDb.schemas.database.id, databaseId),
with: {
project: true,
alertPolicies: true
alertPolicies: true,
storagePolicies: true
}
});
@@ -42,55 +44,56 @@ export const uploadBackupAction = userAction
const arrayBuffer = await file.arrayBuffer();
const fileSize = file.size;
const uuid = uuidv4();
const fileName = `imported_${uuid}${fileExtension}`;
const fileName = `${uuid}${fileExtension}`;
const buffer = Buffer.from(arrayBuffer);
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) {
return {
success: false,
actionError: {
message: "Settings not set",
status: 500,
cause: "Unknown error",
},
};
}
let success: boolean, message: string, filePath: string;
const result =
settings.storage === "local"
? await uploadLocalPrivate(fileName, buffer)
: await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
({success, message, filePath} = result);
if (!success) {
return {
success: false,
actionError: {
message: "An error has occurred while uploading file",
status: 500,
cause: "Unknown error",
},
};
}
// const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
//
// if (!settings) {
// return {
// success: false,
// actionError: {
// message: "Settings not set",
// status: 500,
// cause: "Unknown error",
// },
// };
// }
const [backup] = await db
.insert(drizzleDb.schemas.backup)
.values({
status: 'success',
imported: true,
status: 'ongoing',
databaseId: database.id,
file: fileName,
fileSize: fileSize,
})
.returning();
await storeBackupFiles(backup, database, buffer, fileName)
// let success: boolean, message: string, filePath: string;
//
// const result =
// settings.storage === "local"
// ? await uploadLocalPrivate(fileName, buffer)
// : await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
//
// ({success, message, filePath} = result);
//
// if (!success) {
// return {
// success: false,
// actionError: {
// message: "An error has occurred while uploading file",
// status: 500,
// cause: "Unknown error",
// },
// };
// }
return {
success: true,
value: backup,
@@ -1,61 +0,0 @@
"use client";
import {useMutation} from "@tanstack/react-query";
import {Trash2} from "lucide-react";
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
import {signOut} from "@/lib/auth/auth-client";
import {useRouter} from "next/navigation";
import {deleteUserAction} from "./delete-account.action";
export type ButtonDeleteAccountProps = {
text?: string;
};
export const ButtonDeleteAccount = (props: ButtonDeleteAccountProps) => {
const router = useRouter();
const mutation = useMutation({
mutationFn: () => deleteUserAction(""),
onSuccess: async () => {
await signOut({
fetchOptions: {
onSuccess: () => {
router.push("/login");
},
},
});
},
});
return (
<ButtonWithConfirm
title={props.text ? props.text : ""}
description="Are you sure you want to delete your account ? This action cannot be undone."
button={{
main: {
text: props.text ? props.text : "",
variant: "outline",
icon: <Trash2 color="red"/>,
},
confirm: {
className: "w-full",
text: "Delete",
icon: <Trash2/>,
variant: "destructive",
onClick: () => {
mutation.mutate();
},
},
cancel: {
className: "w-full",
text: "Cancel",
icon: <Trash2/>,
variant: "outline",
},
}}
isPending={mutation.isPending}
/>
);
};
@@ -1,36 +0,0 @@
"use server";
import {userAction} from "@/lib/safe-actions/actions";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import * as drizzleDb from "@/db";
import {authClient} from "@/lib/auth/auth-client";
import {auth} from "@/lib/auth/auth";
export const deleteUserAction = userAction.schema(z.string()).action(async ({ parsedInput, ctx }) => {
const userId = parsedInput.length > 0 ? parsedInput : ctx.user.id;
const uuid = uuidv4();
// const [updatedUser] = await db
// .update(drizzleDb.schemas.user)
// .set({
// email: `${uuid}@portabase.com`,
// name: `${uuid}`,
// //deleted: true,
// //todo: add deleted
// })
// .where(eq(drizzleDb.schemas.user.id, userId))
// .returning();
const [deletedUser] = await db
.delete(drizzleDb.schemas.user)
.where(eq(drizzleDb.schemas.user.id, userId))
.returning();
return {
data: deletedUser,
};
});
@@ -1,43 +0,0 @@
"use server";
import {userAction} from "@/lib/safe-actions/actions";
import { z } from "zod";
import { UserSchema } from "@/components/wrappers/dashboard/profile2/user-form/user-form.schema";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import * as drizzleDb from "@/db";
import {revokeSession, unlinkAccount} from "@/lib/auth/auth";
import {withUpdatedAt} from "@/db/utils";
export const updateUserAction = userAction
.schema(
z.object({
id: z.string(),
data: UserSchema,
})
)
.action(async ({ parsedInput }) => {
const [updatedUser] = await db.update(drizzleDb.schemas.user).set(withUpdatedAt(parsedInput.data)).where(eq(drizzleDb.schemas.user.id, parsedInput.id)).returning();
return {
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;
});
@@ -1,9 +0,0 @@
import {z} from "zod";
export const UserSchema = z.object({
name: z.string().optional(),
email: z.string().optional(),
role: z.string().optional(),
});
export type UserType = z.infer<typeof UserSchema>;
@@ -0,0 +1 @@
ALTER TABLE "backups" ADD COLUMN "imported" boolean DEFAULT false;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -190,6 +190,13 @@
"when": 1768665081232,
"tag": "0026_demonic_santa_claus",
"breakpoints": true
},
{
"idx": 27,
"version": "7",
"when": 1768676733859,
"tag": "0027_special_the_santerians",
"breakpoints": true
}
]
}
+1
View File
@@ -41,6 +41,7 @@ export const backup = pgTable(
databaseId: uuid("database_id")
.notNull()
.references(() => database.id, {onDelete: "cascade"}),
imported: boolean('imported').default(false),
...timestamps
},
);
+1 -2
View File
@@ -66,9 +66,8 @@ export function backupColumns(
accessorKey: "id",
header: "Reference",
cell: ({row}) => {
const fileName = row.original.file
const reference = row.original.id
const isImported = isImportedFilename(`${fileName}`)
const isImported = row.original.imported
return isImported ? `${reference} (imported)` : `${reference}`
},
},