migration

This commit is contained in:
Théo LAGACHE
2025-05-16 15:54:17 +02:00
parent 01019fe1a3
commit 6f2523e524
285 changed files with 15492 additions and 46090 deletions
-36
View File
@@ -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("/")
}
}
+69 -62
View File
@@ -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>
)
);
},
},
]
];
+17 -29
View File
@@ -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" },
},
};
}
});
+12 -13
View File
@@ -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>
)
}
);
};
+8 -26
View File
@@ -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)]);
-14
View File
@@ -1,14 +0,0 @@
export const checkPermission = (role) => {
switch (role) {
case "admin":
break;
case "admin":
break;
case "admin":
break;
case "admin":
break;
}
}
-18
View File
@@ -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;
}
+9 -15
View File
@@ -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;
}
}
}
+66 -78
View File
@@ -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();
}