mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
migration
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
"use server"
|
||||
|
||||
import {redirect} from "next/navigation";
|
||||
import {signIn, signOut} from "@/auth/auth";
|
||||
import {setCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
||||
|
||||
export const signOutAction = async () => {
|
||||
const deleteCookieResponse = await setCurrentOrganizationSlug("");
|
||||
await signOut({ redirectTo: '/', redirect: true });
|
||||
|
||||
// if (typeof window !== 'undefined') {
|
||||
// window.location.reload();
|
||||
// }
|
||||
|
||||
return deleteCookieResponse;
|
||||
};
|
||||
|
||||
export const signInAction = async (type: string, formData?: any) => {
|
||||
if (type === "google") {
|
||||
await setCurrentOrganizationSlug("default");
|
||||
await signIn(type, {redirectTo: '/dashboard'})
|
||||
} else {
|
||||
try {
|
||||
await setCurrentOrganizationSlug("default");
|
||||
|
||||
await signIn(type, {
|
||||
redirect: false,
|
||||
password: formData.password,
|
||||
email: formData.email
|
||||
});
|
||||
} catch (error) {
|
||||
return {error: "Error loging in, please try again or check your credentials !"};
|
||||
}
|
||||
redirect("/")
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {ColumnDef} from "@tanstack/react-table"
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel, DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Download, MoreHorizontal, Trash2} from "lucide-react";
|
||||
import {ReloadIcon} from "@radix-ui/react-icons";
|
||||
import {Backup} from "@prisma/client";
|
||||
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {createRestorationAction, deleteBackupAction} from "@/features/dashboard/restore/restore.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {StatusBadge} from "@/components/wrappers/common/status-badge";
|
||||
import {TooltipCustom} from "@/components/wrappers/common/tooltipCustom/TooltipCustom";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Download, MoreHorizontal, Trash2 } from "lucide-react";
|
||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
import { Backup } from "@prisma/client";
|
||||
import { getFileUrlPresignedLocal } from "@/features/upload/private/upload.action";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { createRestorationAction, deleteBackupAction } from "@/features/dashboard/restore/restore.action";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { StatusBadge } from "@/components/wrappers/common/status-badge";
|
||||
import { TooltipCustom } from "@/components/wrappers/common/tooltipCustom/TooltipCustom";
|
||||
|
||||
export const backupColumns: ColumnDef<Backup>[] = [
|
||||
{
|
||||
@@ -29,107 +29,114 @@ export const backupColumns: ColumnDef<Backup>[] = [
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: "Created At",
|
||||
cell: ({row}) => {
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.getValue("createdAt")).toLocaleString("fr-FR");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({row}) => {
|
||||
return <StatusBadge status={row.getValue("status")}/>
|
||||
cell: ({ row }) => {
|
||||
return <StatusBadge status={row.getValue("status")} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({row, table}) => {
|
||||
const status = row.getValue("status")
|
||||
const rowData: Backup = row.original
|
||||
const fileName = rowData.file
|
||||
cell: ({ row, table }) => {
|
||||
const status = row.getValue("status");
|
||||
const rowData: Backup = row.original;
|
||||
const fileName = rowData.file;
|
||||
// @ts-ignore
|
||||
const {extendedProps} = table.options.meta;
|
||||
|
||||
const router = useRouter()
|
||||
const { extendedProps } = table.options.meta;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const mutationRestore = useMutation({
|
||||
mutationFn: async () => {
|
||||
const restoration = await createRestorationAction({
|
||||
backupId: rowData.id,
|
||||
databaseId: rowData.databaseId
|
||||
})
|
||||
databaseId: rowData.databaseId,
|
||||
});
|
||||
if (restoration.data.success) {
|
||||
toast.success(restoration.data.actionSuccess?.message || "Restoration created successfully!");
|
||||
router.refresh()
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(restoration.serverError || "Failed to create restoration.");
|
||||
}
|
||||
}})
|
||||
},
|
||||
});
|
||||
|
||||
const mutationDeleteBackup = useMutation({
|
||||
const mutationDeleteBackup = useMutation({
|
||||
mutationFn: async () => {
|
||||
const restoration = await deleteBackupAction({
|
||||
backupId: rowData.id,
|
||||
databaseId: rowData.databaseId
|
||||
})
|
||||
databaseId: rowData.databaseId,
|
||||
});
|
||||
if (restoration.data.success) {
|
||||
toast.success(restoration.data.actionSuccess.message);
|
||||
router.refresh()
|
||||
router.refresh();
|
||||
} else {
|
||||
toast.error(restoration.data.actionError.message);
|
||||
}
|
||||
}})
|
||||
},
|
||||
});
|
||||
|
||||
const handleRestore = async () => {
|
||||
await mutationRestore.mutateAsync()
|
||||
}
|
||||
await mutationRestore.mutateAsync();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
await mutationDeleteBackup.mutateAsync()
|
||||
}
|
||||
await mutationDeleteBackup.mutateAsync();
|
||||
};
|
||||
|
||||
const handleDownload = async (fileName: string) => {
|
||||
const url = await getFileUrlPresignedLocal(fileName)
|
||||
window.open(url, '_self');
|
||||
}
|
||||
|
||||
const url = await getFileUrlPresignedLocal(fileName);
|
||||
window.open(url, "_self");
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4"/>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
{status == "success" ?
|
||||
{status == "success" ? (
|
||||
<>
|
||||
<TooltipCustom disabled={extendedProps} text="Already a restoration waiting">
|
||||
<DropdownMenuItem disabled={mutationRestore.isPending || extendedProps}
|
||||
onClick={async () => {
|
||||
await handleRestore()
|
||||
}}>
|
||||
<ReloadIcon/> Restore
|
||||
<DropdownMenuItem
|
||||
disabled={mutationRestore.isPending || extendedProps}
|
||||
onClick={async () => {
|
||||
await handleRestore();
|
||||
}}
|
||||
>
|
||||
<ReloadIcon /> Restore
|
||||
</DropdownMenuItem>
|
||||
</TooltipCustom>
|
||||
<DropdownMenuItem onClick={async () => {
|
||||
await handleDownload(fileName)
|
||||
}}>
|
||||
<Download/> Download
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
await handleDownload(fileName);
|
||||
}}
|
||||
>
|
||||
<Download /> Download
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
: null}
|
||||
<DropdownMenuSeparator/>
|
||||
<DropdownMenuItem className="text-red-600" onClick={async () => {
|
||||
await handleDelete()
|
||||
}}>
|
||||
<Trash2/> Delete
|
||||
) : null}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-red-600"
|
||||
onClick={async () => {
|
||||
await handleDelete();
|
||||
}}
|
||||
>
|
||||
<Trash2 /> Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import {ColumnDef} from "@tanstack/react-table"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {MoreHorizontal} from "lucide-react";
|
||||
import {ReloadIcon} from "@radix-ui/react-icons";
|
||||
import {Restoration} from "@prisma/client";
|
||||
import {StatusBadge} from "@/components/wrappers/common/status-badge";
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
import { StatusBadge } from "@/components/wrappers/common/status-badge";
|
||||
import { Restoration } from "@/db/schema";
|
||||
|
||||
export const restoreColumns: ColumnDef<Restoration>[] = [
|
||||
{
|
||||
@@ -23,42 +16,37 @@ export const restoreColumns: ColumnDef<Restoration>[] = [
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: "Created At",
|
||||
cell: ({row}) => {
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.getValue("createdAt")).toLocaleString("fr-FR");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({row}) => {
|
||||
return <StatusBadge status={row.getValue("status")}/>
|
||||
cell: ({ row }) => {
|
||||
return <StatusBadge status={row.getValue("status")} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({row}) => {
|
||||
const payment = row.original
|
||||
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4"/>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuItem onClick={() => {
|
||||
}}>
|
||||
<ReloadIcon/> Rerun
|
||||
<DropdownMenuItem onClick={() => {}}>
|
||||
<ReloadIcon /> Rerun
|
||||
</DropdownMenuItem>
|
||||
|
||||
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
@@ -1,88 +1,101 @@
|
||||
"use server"
|
||||
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {z} from "zod";
|
||||
import {prisma} from "@/prisma";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {Backup, Restoration} from "@prisma/client";
|
||||
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { z } from "zod";
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import { Backup, backup, Restoration, restoration } from "@/db/schema";
|
||||
import { db } from "@/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
export const deleteBackupAction = userAction
|
||||
.schema(z.object({
|
||||
backupId: z.string(),
|
||||
databaseId: z.string(),
|
||||
}))
|
||||
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Backup>> => {
|
||||
|
||||
.schema(
|
||||
z.object({
|
||||
backupId: z.string(),
|
||||
databaseId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Backup>> => {
|
||||
try {
|
||||
const backupDeleted = await prisma.backup.delete({
|
||||
where:{
|
||||
databaseId: parsedInput.databaseId,
|
||||
id: parsedInput.backupId
|
||||
}
|
||||
});
|
||||
await db
|
||||
.delete(backup)
|
||||
.where(and(eq(backup.id, parsedInput.backupId), eq(backup.databaseId, parsedInput.databaseId)))
|
||||
.execute();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: backupDeleted,
|
||||
actionSuccess: {
|
||||
message: "Backup has been successfully deleted.",
|
||||
messageParams: {message: "success"},
|
||||
},
|
||||
};
|
||||
const backupExists = await db
|
||||
.select()
|
||||
.from(backup)
|
||||
.where(and(eq(backup.id, parsedInput.backupId), eq(backup.databaseId, parsedInput.databaseId)))
|
||||
.execute();
|
||||
|
||||
if (backupExists.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
actionSuccess: {
|
||||
message: "Backup deleted successfully.",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Backup not found or already deleted.",
|
||||
status: 404,
|
||||
cause: "Backup could not be deleted.",
|
||||
messageParams: { message: "Error deleting the backup" },
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting backup:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to delete backup.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {message: "Error deleting the backup"},
|
||||
messageParams: { message: "Error deleting the backup" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Create Restoration Action (Drizzle version)
|
||||
export const createRestorationAction = userAction
|
||||
.schema(z.object({
|
||||
backupId: z.string(),
|
||||
databaseId: z.string(),
|
||||
}))
|
||||
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Restoration>> => {
|
||||
|
||||
.schema(
|
||||
z.object({
|
||||
backupId: z.string(),
|
||||
databaseId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<Restoration>> => {
|
||||
try {
|
||||
const restoration = await prisma.restoration.create({
|
||||
data: {
|
||||
// Insert new restoration into the database
|
||||
const restorationData = await db
|
||||
.insert(restoration)
|
||||
.values({
|
||||
databaseId: parsedInput.databaseId,
|
||||
backupId: parsedInput.backupId,
|
||||
status: "waiting",
|
||||
},
|
||||
});
|
||||
})
|
||||
.returning()
|
||||
.execute();
|
||||
|
||||
const createdRestoration = restorationData[0];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: restoration,
|
||||
value: createdRestoration,
|
||||
actionSuccess: {
|
||||
message: "Restoration has been successfully created.",
|
||||
messageParams: {restorationId: restoration.id},
|
||||
messageParams: { restorationId: createdRestoration.id },
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating restoration:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create backup.",
|
||||
message: "Failed to create restoration.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {message: "Error creating the restoration"},
|
||||
messageParams: { message: "Error creating the restoration" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import {notFound} from "next/navigation";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import {currentUser} from "@/auth/current-user";
|
||||
import {SidebarTrigger} from "@/components/ui/sidebar";
|
||||
import {ModeToggle} from "@/features/theme/ModeToggle";
|
||||
import {LoggedInButton} from "@/components/wrappers/dashboard/loggedInButton/LoggedInButton";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { ModeToggle } from "@/features/theme/ModeToggle";
|
||||
import { LoggedInButton } from "@/components/wrappers/dashboard/loggedInButton/LoggedInButton";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export const Header = async () => {
|
||||
const user = await currentUser()
|
||||
const user = await currentUser();
|
||||
if (!user) {
|
||||
return notFound()
|
||||
return notFound();
|
||||
}
|
||||
return (
|
||||
<header className="flex h-16 shrink-0 items-center justify-between border-b px-4">
|
||||
<SidebarTrigger className="-ml-1"/>
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<div className="flex items-center gap-2">
|
||||
<ModeToggle/>
|
||||
<LoggedInButton/>
|
||||
<ModeToggle />
|
||||
<LoggedInButton />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
import {twx} from "@/lib/twx";
|
||||
import {cn} from "@/lib/utils";
|
||||
import { twx } from "@/lib/twx";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Page = twx.div((props) => [cn(`flex flex-col h-full px-10 py-6`, props.className)]);
|
||||
|
||||
export const Page = twx.div((props) => [
|
||||
cn(`flex flex-col h-full px-10 py-6`, props.className)
|
||||
])
|
||||
export const PageHeader = twx.div((props) => [cn(`flex justify-between`, props.className)]);
|
||||
|
||||
export const PageTitle = twx.h1((props) => [cn(`text-3xl font-bold mb-6 flex gap-4 items-center`, props.className)]);
|
||||
|
||||
export const PageHeader = twx.div((props) => [
|
||||
cn(`flex justify-between`, props.className),
|
||||
])
|
||||
export const PageDescription = twx.h2((props) => [cn(`text-s mb-6 `, props.className)]);
|
||||
|
||||
export const PageActions = twx.h1((props) => [cn(`flex gap-4 h-fit`, props.className)]);
|
||||
|
||||
export const PageTitle = twx.h1((props) => [
|
||||
cn(`text-3xl font-bold mb-6 flex gap-4 items-center`, props.className),
|
||||
])
|
||||
|
||||
|
||||
export const PageDescription = twx.h2((props) => [
|
||||
cn(`text-s mb-6 `, props.className),
|
||||
])
|
||||
|
||||
|
||||
export const PageActions = twx.h1((props) => [
|
||||
cn(`flex gap-4 h-fit`, props.className),
|
||||
]
|
||||
)
|
||||
|
||||
export const PageContent = twx.div((props) => [
|
||||
cn(`h-full`, props.className)
|
||||
])
|
||||
export const PageContent = twx.div((props) => [cn(`h-full`, props.className)]);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export const checkPermission = (role) => {
|
||||
|
||||
switch (role) {
|
||||
case "admin":
|
||||
break;
|
||||
case "admin":
|
||||
break;
|
||||
case "admin":
|
||||
break;
|
||||
case "admin":
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
|
||||
|
||||
export async function checkAllPermissions(db: any, model: string) {
|
||||
const operations = ['read', 'create', 'update', 'delete'];
|
||||
const results: Record<string, boolean> = {};
|
||||
|
||||
for (const operation of operations) {
|
||||
// Dynamically access the model and check permissions
|
||||
results[operation] = await db[model].check({ operation });
|
||||
}
|
||||
|
||||
const hasAllPermissions = Object.values(results).every(permission => permission === true);
|
||||
|
||||
console.log('Permissions:', results);
|
||||
console.log('Has all permissions:', hasAllPermissions);
|
||||
|
||||
return hasAllPermissions;
|
||||
}
|
||||
@@ -1,28 +1,23 @@
|
||||
"use server"
|
||||
"use server";
|
||||
|
||||
import {mkdir, writeFile} from "fs/promises";
|
||||
import { mkdir, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import * as fs from "node:fs";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import { getServerUrl } from "@/utils/get-server-url";
|
||||
|
||||
const privateLocalDir = "private/uploads/files/";
|
||||
|
||||
|
||||
export async function uploadLocalPrivate(fileName: string, buffer: any) {
|
||||
try {
|
||||
await mkdir(path.join(process.cwd(), privateLocalDir), { recursive: true });
|
||||
|
||||
await writeFile(
|
||||
path.join(process.cwd(), privateLocalDir, fileName),
|
||||
buffer
|
||||
);
|
||||
await writeFile(path.join(process.cwd(), privateLocalDir, fileName), buffer);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "File uploaded successfully",
|
||||
filePath: path.join(privateLocalDir, fileName),
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error occurred:", error);
|
||||
throw new Error("An error occurred while importing the private file");
|
||||
@@ -35,17 +30,16 @@ export async function getFileUrlPresignedLocal(fileName: string) {
|
||||
await mkdir(path.join(process.cwd(), privateLocalDir), { recursive: true });
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error('File not found at:', filePath);
|
||||
return `File not found at: ${filePath}`
|
||||
console.error("File not found at:", filePath);
|
||||
return `File not found at: ${filePath}`;
|
||||
}
|
||||
const crypto = require('crypto');
|
||||
const crypto = require("crypto");
|
||||
const baseUrl = getServerUrl();
|
||||
|
||||
const expiresAt = Date.now() + 60 * 1000; // expires in 1 minute
|
||||
const token = crypto.createHash('sha256').update(`${fileName}${expiresAt}`).digest('hex');
|
||||
const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex");
|
||||
return `${baseUrl}/api/files/${fileName}?token=${token}&expires=${expiresAt}`;
|
||||
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +1,87 @@
|
||||
"use server"
|
||||
|
||||
import {userAction} from "@/safe-actions";
|
||||
import {z} from "zod";
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
import {mkdir, writeFile} from "fs/promises";
|
||||
"use server";
|
||||
import { userAction } from "@/safe-actions";
|
||||
import { z } from "zod";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { mkdir, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import {env} from "@/env.mjs";
|
||||
import {checkMinioAlive, createPublicBucket, saveFileInBucket} from "@/utils/s3-file-management";
|
||||
import {prisma} from "@/prisma";
|
||||
import {UploadedObjectInfo} from "minio/src/internal/type";
|
||||
import {Settings} from "@prisma/client";
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
import { env } from "@/env.mjs";
|
||||
import { checkMinioAlive, createPublicBucket, saveFileInBucket } from "@/utils/s3-file-management";
|
||||
//@ts-ignore
|
||||
import { UploadedObjectInfo } from "minio/src/internal/type";
|
||||
import { getServerUrl } from "@/utils/get-server-url";
|
||||
import { setting as drizzleSetting, Setting } from "@/db/schema";
|
||||
import { db } from "@/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const uploadImageAction = userAction.schema(z.instanceof(FormData)).action(async ({ parsedInput: formData, ctx }) => {
|
||||
const file = formData.get("file") as File;
|
||||
const uuid = uuidv4();
|
||||
const fileFormat = file.name.split(".").slice(-1)[0];
|
||||
const fileName = uuid + "." + fileFormat;
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
export const uploadImageAction = userAction
|
||||
.schema(z.instanceof(FormData))
|
||||
.action(async ({parsedInput: formData, ctx}) => {
|
||||
const [settings] = await db.select().from(drizzleSetting).where(eq(drizzleSetting.name, "system")).limit(1);
|
||||
|
||||
const file = formData.get("file") as File
|
||||
const uuid = uuidv4()
|
||||
const fileFormat = file.name.split(".").slice(-1)[0]
|
||||
const fileName = uuid + "." + fileFormat
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
|
||||
const settings = await prisma.settings.findUnique({
|
||||
where:{
|
||||
name: "system"
|
||||
}
|
||||
})
|
||||
|
||||
let result: void | UploadedObjectInfo;
|
||||
const bucketName = 'public-image-bucket';
|
||||
|
||||
if(settings.storage === "local") {
|
||||
result = await uploadLocal(fileName,buffer)
|
||||
}else if (settings.storage === "s3"){
|
||||
result = await uploadS3Compatible(bucketName,fileName, buffer)
|
||||
}
|
||||
|
||||
const url = getUrl(fileName, settings, bucketName)
|
||||
console.log(url)
|
||||
return {
|
||||
data: {result: result, url: url},
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
function getUrl(fileName:string, settings: Settings, bucketName: string):string {
|
||||
if (env.NODE_ENV === "production") {
|
||||
if(settings.storage === "s3"){
|
||||
return `https://${env.S3_ENDPOINT}/${bucketName}/${fileName}`
|
||||
}else if(settings.storage === "local"){
|
||||
const url = getServerUrl()
|
||||
return `${url}/api/images/${fileName}`
|
||||
}
|
||||
|
||||
} else {
|
||||
if(settings.storage === "s3"){
|
||||
return `http://localhost:${env.S3_PORT}/${bucketName}/${fileName}`
|
||||
}else if(settings.storage === "local"){
|
||||
const url = getServerUrl()
|
||||
return `${url}/api/images/${fileName}`
|
||||
}
|
||||
if (!settings) {
|
||||
throw new Error("System settings not found.");
|
||||
}
|
||||
}
|
||||
|
||||
let result: void | UploadedObjectInfo;
|
||||
const bucketName = "public-image-bucket";
|
||||
|
||||
if (settings.storage === "local") {
|
||||
result = await uploadLocal(fileName, buffer);
|
||||
} else if (settings.storage === "s3") {
|
||||
result = await uploadS3Compatible(bucketName, fileName, buffer);
|
||||
}
|
||||
|
||||
const url = getUrl(fileName, settings, bucketName);
|
||||
console.log(url);
|
||||
return {
|
||||
data: { result: result, url: url },
|
||||
};
|
||||
});
|
||||
|
||||
async function uploadLocal(fileName: string, buffer: any) {
|
||||
const localDir = "private/uploads/images/"
|
||||
const localDir = "private/uploads/images/";
|
||||
try {
|
||||
await mkdir(path.join(process.cwd(), localDir), { recursive: true });
|
||||
return await writeFile(
|
||||
path.join(process.cwd(), localDir + fileName),
|
||||
buffer
|
||||
)
|
||||
|
||||
return await writeFile(path.join(process.cwd(), localDir + fileName), buffer);
|
||||
} catch (error) {
|
||||
console.log("Error occured ", error);
|
||||
throw new Error('An error occured while importing image');
|
||||
|
||||
throw new Error("An error occured while importing image");
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadS3Compatible(bucketName: string, fileName: string, buffer: any){
|
||||
await createPublicBucket({bucketName});
|
||||
async function uploadS3Compatible(bucketName: string, fileName: string, buffer: any) {
|
||||
await createPublicBucket({ bucketName });
|
||||
return await saveFileInBucket({
|
||||
bucketName,
|
||||
fileName,
|
||||
file: buffer,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function checkConnexionToS3(){
|
||||
return await checkMinioAlive()
|
||||
function getUrl(fileName: string, settings: Setting, bucketName: string): string {
|
||||
if (env.NODE_ENV === "production") {
|
||||
if (settings.storage === "s3") {
|
||||
return `https://${env.S3_ENDPOINT}/${bucketName}/${fileName}`;
|
||||
} else if (settings.storage === "local") {
|
||||
const url = getServerUrl();
|
||||
return `${url}/api/images/${fileName}`;
|
||||
}
|
||||
} else {
|
||||
if (settings.storage === "s3") {
|
||||
return `http://localhost:${env.S3_PORT}/${bucketName}/${fileName}`;
|
||||
} else if (settings.storage === "local") {
|
||||
const url = getServerUrl();
|
||||
return `${url}/api/images/${fileName}`;
|
||||
}
|
||||
}
|
||||
throw new Error("Invalid storage configuration");
|
||||
}
|
||||
|
||||
}
|
||||
export async function checkConnexionToS3() {
|
||||
return await checkMinioAlive();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user