Working on S3 compatibility.

This commit is contained in:
charles-gauthereau
2024-11-17 21:01:05 +01:00
parent de4242f4ca
commit 9b38d68d51
16 changed files with 179 additions and 51 deletions
@@ -6,11 +6,7 @@ import {requiredCurrentUser} from "@/auth/current-user";
import {UserForm} from "@/components/wrappers/Dashboard/Profile/UserForm/UserForm";
import {prisma} from "@/prisma";
import {Badge} from "@/components/ui/badge";
import Link from "next/link";
import {Button} from "@/components/ui/button";
import {ButtonWithConfirm} from "@/components/wrappers/Button/ButtonWithConfirm/ButtonWithConfirm";
import {ButtonDeleteAccount} from "@/components/wrappers/Dashboard/Profile/ButtonDeleteAccount/ButtonDeleteAccount";
import {useIsMobile} from "@/hooks/use-mobile";
import {AvatarWithUpload} from "@/components/wrappers/Dashboard/Profile/Avatar/AvatarWithUpload";
export default async function RoutePage(props: PageParams<{}>) {
+5 -1
View File
@@ -12,10 +12,14 @@ export default async function RoutePage(props: PageParams<{}>) {
where:{
id: {
not: user.id
}
},
deleted: { not: true },
}
})
console.log(users)
const settings = await prisma.settings.findUnique({
where:{
name: "system"
+16
View File
@@ -37,5 +37,21 @@ services:
retries: 5
s3:
image: docker.io/bitnami/minio:latest
ports:
- '9000:9000'
- '9001:9001'
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=SoluceTechnologies
- MINIO_ROOT_PASSWORD=p18avkwHPDh9mk
- MINIO_DEFAULT_BUCKETS=statics
volumes:
postgres-data:
minio_data:
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "deleted" BOOLEAN;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ALTER COLUMN "deleted" SET DEFAULT false;
+3 -3
View File
@@ -64,9 +64,9 @@ model User {
authMethod String? @map("auth_method")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt @map("updated_at")
accounts Account[]
sessions Session[]
deleted Boolean? @default(false)
accounts Account[]
sessions Session[]
@@map("users")
}
@@ -1,6 +1,6 @@
"use client"
import {Button} from "@/components/ui/button";
import {useState} from "react";
import {ButtonHTMLAttributes, useState} from "react";
import {Loader2} from "lucide-react";
export type VariantButton = {
@@ -21,19 +21,28 @@ export type ButtonWithConfirmProps = {
isPending? : boolean
};
export const ButtonWithLoading = (props: ButtonWithConfirmProps) => {
export const ButtonWithLoading = ({
icon,
text,
variant,
className,
onClick,
isPending,
...props // catch all remaining props
}: ButtonWithConfirmProps & ButtonHTMLAttributes<HTMLButtonElement>) => {
return(
<Button
onClick={() => {
props.onClick()
onClick()
}}
variant={props.variant ? props.variant : "default"}
className={props.className}
variant={variant ? variant : "default"}
className={className}
{...props} // forward the remaining props to the Button component
>
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
{props.text}
{isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
{text}
<>
{props.icon ? props.icon : null}
{icon ? icon : null}
</>
</Button>
)
@@ -18,6 +18,7 @@ export const deleteUserAction = userAction
data: {
email: `${uuid}@portabase.com`,
name: `${uuid}`,
deleted: true
}
})
@@ -39,7 +39,7 @@ export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
return (
<div className="flex flex-col h-full py-4">
<div className="flex gap-4 h-fit justify-between">
<h1>Settings for Portabase storage</h1>
<h1>Settings for Portabase email setup</h1>
{props.settings.smtpFrom && (
<ButtonWithLoading
isPending={mutation.isPending}
@@ -1,10 +1,15 @@
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {Info} from "lucide-react";
import {Info, Send, ShieldCheck} from "lucide-react";
import {Switch} from "@/components/ui/switch";
import {Label} from "@/components/ui/label";
import {StorageS3Form} from "@/components/wrappers/Dashboard/Settings/SettingsStorageTab/StorageS3Form/StorageS3Form";
import {useState} from "react";
import {Settings} from "@prisma/client";
import {ButtonWithLoading} from "@/components/wrappers/Button/ButtonWithLoading/ButtonWithLoading";
import {useMutation} from "@tanstack/react-query";
import {checkConnexionToS3} from "@/features/upload/upload.action";
import {toast} from "sonner";
export type SettingsStorageTabProps = {
settings: Settings
@@ -12,7 +17,15 @@ export type SettingsStorageTabProps = {
export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
const mutation = useMutation({
mutationFn: async () => {
const result = await checkConnexionToS3()
if(result.error){
toast.error("An error occured during the connexion !")
}
toast.success("Connexion succeed!")
}
})
const [isSwitched, setIsSwitched] = useState<boolean>(props.settings.storage !== "local");
@@ -29,7 +42,8 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
</AlertDescription>
</Alert>
<div className="flex flex-col h-full py-4 ">
<div className="flex items-center space-x-2">
<div className="flex items-center justify-between space-x-2">
<div className="flex items-center space-x-2">
<Label htmlFor="storage-mode">Storage Mode (Local/s3 compatible)</Label>
<Switch
checked={isSwitched}
@@ -37,6 +51,18 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
setIsSwitched(!isSwitched);
}}
id="storage-mode"/>
</div>
<div>
<ButtonWithLoading
disabled={!isSwitched}
isPending={mutation.isPending}
onClick={async () => {
await mutation.mutateAsync()
}}
icon={<ShieldCheck />}
text="Test connexion"
/>
</div>
</div>
{isSwitched && (
<div className="mt-5">
@@ -6,6 +6,7 @@ import {User, Settings} from "@prisma/client";
import {backupColumns} from "@/features/backup/columns";
import {SettingsEmailTab} from "@/components/wrappers/Dashboard/Settings/SettingsEmailTab/SettingsEmailTab";
import {SettingsStorageTab} from "@/components/wrappers/Dashboard/Settings/SettingsStorageTab/SettingsStorageTab";
import {SettingsUsersTab} from "@/components/wrappers/Dashboard/Settings/SettingsUsersTab/SettingsUsersTab";
export type SettingsTabsProps = {
@@ -34,7 +35,7 @@ export const SettingsTabs = (props: SettingsTabsProps) => {
</div>
</TabsContent>
<TabsContent value="users" className="h-full justify-between">
<DataTableWithPagination columns={usersColumns} data={props.users}/>
<SettingsUsersTab users={props.users}/>
</TabsContent>
<TabsContent value="email">
<SettingsEmailTab settings={props.settings}/>
@@ -5,10 +5,6 @@ import {Badge} from "@/components/ui/badge";
import {User} from "@prisma/client";
export const usersColumns: ColumnDef<User>[] = [
{
accessorKey: "id",
header: "Id",
},
{
accessorKey: "name",
header: "Name"
@@ -0,0 +1,21 @@
import {DataTableWithPagination} from "@/components/wrappers/table/data-table-with-pagination";
import {usersColumns} from "@/components/wrappers/Dashboard/Settings/SettingsTabs/columns-users";
import {User} from "@prisma/client";
export type SettingsUsersTabProps = {
users: User[]
}
export const SettingsUsersTab = (props: SettingsUsersTabProps) => {
return (
<div className="flex flex-col h-full py-4">
<div className="flex gap-4 h-fit justify-between">
<h1>List of Portabase users</h1>
</div>
<div className="mt-5">
<DataTableWithPagination columns={usersColumns} data={props.users}/>
</div>
</div>
)
}
+51 -19
View File
@@ -5,6 +5,7 @@ import {v4 as uuidv4} from 'uuid';
import { writeFile, access, mkdir } from "fs/promises";
import path from "path";
import {env} from "@/env.mjs";
import {checkMinioAlive} from "@/utils/s3-file-management";
export const uploadImageAction = userAction
@@ -18,29 +19,60 @@ export const uploadImageAction = userAction
const fileName = uuid + "." + fileFormat
const arrayBuffer = await file.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const localDir = "public/uploads/"
try {
await mkdir(path.join(process.cwd(), localDir), { recursive: true });
const result = await writeFile(
path.join(process.cwd(), localDir + fileName),
buffer
);
let url: string = "";
if (env.NODE_ENV === "production") {
// url = `https://${env.S3_ENDPOINT}/${bucketName}/${fileName}`
} else {
url = `http://localhost:8887/uploads/${fileName}`
}
return {
data: {result: result, url: url},
}
} catch (error) {
console.log("Error occured ", error);
throw new Error('An error occured while importing image');
// await uploadS3Compatible()
const result = await uploadLocal(fileName,buffer)
const url = getUrl(fileName)
return {
data: {result: result, url: url},
}
});
function getUrl(fileName:string):string {
let url: string = "";
if (env.NODE_ENV === "production") {
// url = `https://${env.S3_ENDPOINT}/${bucketName}/${fileName}`
return url
} else {
url = `http://localhost:8887/uploads/${fileName}`
return url
}
}
async function uploadLocal(fileName: string, buffer: any): Promise<{ data: { result: void; url: string } }> {
const localDir = "public/uploads/"
try {
await mkdir(path.join(process.cwd(), localDir), { recursive: true });
const result = await writeFile(
path.join(process.cwd(), localDir + fileName),
buffer
);
let url: string = "";
if (env.NODE_ENV === "production") {
// url = `https://${env.S3_ENDPOINT}/${bucketName}/${fileName}`
} else {
url = `http://localhost:8887/uploads/${fileName}`
}
return {
data: {result: result, url: url},
}
} catch (error) {
console.log("Error occured ", error);
throw new Error('An error occured while importing image');
}
}
async function uploadS3Compatible() {
await checkMinioAlive()
}
export async function checkConnexionToS3(){
return await checkMinioAlive()
}
+1 -1
View File
@@ -41,7 +41,7 @@ async function createSettingsIfNotExist() {
console.log("====Init Setting : Create ====")
await prisma.settings.create({
data: {
name: "system",
...configSettings
}
})
}else{
+28 -6
View File
@@ -1,19 +1,38 @@
import * as Minio from 'minio'
import {env} from "@/env.mjs";
import internal from "node:stream";
import {prisma} from "@/prisma";
const settings = await prisma.settings.findUnique({
where:{
name: "system"
}
})
// Create a new Minio client with the S3 endpoint, access key, and secret key
// export const s3Client = env.NODE_ENV === "production" ?
// new Minio.Client({
// endPoint: env.S3_ENDPOINT ?? "",
// accessKey: env.S3_ACCESS_KEY ?? "",
// secretKey: env.S3_SECRET_KEY ?? "",
// }) : new Minio.Client({
// endPoint: env.S3_ENDPOINT ?? "",
// port: Number(env.S3_PORT ?? 0),
// accessKey: env.S3_ACCESS_KEY ?? "",
// secretKey: env.S3_SECRET_KEY ?? "",
// useSSL: env.S3_USE_SSL === 'true'
// })
export const s3Client = env.NODE_ENV === "production" ?
new Minio.Client({
endPoint: env.S3_ENDPOINT ?? "",
accessKey: env.S3_ACCESS_KEY ?? "",
secretKey: env.S3_SECRET_KEY ?? "",
endPoint: settings.s3EndPointUrl ?? "",
accessKey: settings.s3AccessKeyId ?? "",
secretKey: settings.s3SecretAccessKey ?? "",
}) : new Minio.Client({
endPoint: env.S3_ENDPOINT ?? "",
endPoint: settings.s3EndPointUrl ?? "",
port: Number(env.S3_PORT ?? 0),
accessKey: env.S3_ACCESS_KEY ?? "",
secretKey: env.S3_SECRET_KEY ?? "",
accessKey: settings.s3AccessKeyId ?? "",
secretKey: settings.s3SecretAccessKey ?? "",
useSSL: env.S3_USE_SSL === 'true'
})
@@ -22,8 +41,11 @@ export async function checkMinioAlive() {
// Try to list buckets to check connectivity
const buckets = await s3Client.listBuckets();
console.log('MinIO is up and running. Buckets:', buckets);
return {message: true}
} catch (error) {
console.error('Error connecting to MinIO:', error);
return {error: error}
}
}