mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on S3 compatibility.
This commit is contained in:
@@ -6,11 +6,7 @@ import {requiredCurrentUser} from "@/auth/current-user";
|
|||||||
import {UserForm} from "@/components/wrappers/Dashboard/Profile/UserForm/UserForm";
|
import {UserForm} from "@/components/wrappers/Dashboard/Profile/UserForm/UserForm";
|
||||||
import {prisma} from "@/prisma";
|
import {prisma} from "@/prisma";
|
||||||
import {Badge} from "@/components/ui/badge";
|
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 {ButtonDeleteAccount} from "@/components/wrappers/Dashboard/Profile/ButtonDeleteAccount/ButtonDeleteAccount";
|
||||||
import {useIsMobile} from "@/hooks/use-mobile";
|
|
||||||
import {AvatarWithUpload} from "@/components/wrappers/Dashboard/Profile/Avatar/AvatarWithUpload";
|
import {AvatarWithUpload} from "@/components/wrappers/Dashboard/Profile/Avatar/AvatarWithUpload";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
where:{
|
where:{
|
||||||
id: {
|
id: {
|
||||||
not: user.id
|
not: user.id
|
||||||
}
|
},
|
||||||
|
deleted: { not: true },
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log(users)
|
||||||
|
|
||||||
const settings = await prisma.settings.findUnique({
|
const settings = await prisma.settings.findUnique({
|
||||||
where:{
|
where:{
|
||||||
name: "system"
|
name: "system"
|
||||||
|
|||||||
@@ -37,5 +37,21 @@ services:
|
|||||||
retries: 5
|
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:
|
volumes:
|
||||||
postgres-data:
|
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;
|
||||||
@@ -64,7 +64,7 @@ model User {
|
|||||||
authMethod String? @map("auth_method")
|
authMethod String? @map("auth_method")
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at")
|
updatedAt DateTime? @updatedAt @map("updated_at")
|
||||||
|
deleted Boolean? @default(false)
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {useState} from "react";
|
import {ButtonHTMLAttributes, useState} from "react";
|
||||||
import {Loader2} from "lucide-react";
|
import {Loader2} from "lucide-react";
|
||||||
|
|
||||||
export type VariantButton = {
|
export type VariantButton = {
|
||||||
@@ -21,19 +21,28 @@ export type ButtonWithConfirmProps = {
|
|||||||
isPending? : boolean
|
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(
|
return(
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onClick()
|
onClick()
|
||||||
}}
|
}}
|
||||||
variant={props.variant ? props.variant : "default"}
|
variant={variant ? variant : "default"}
|
||||||
className={props.className}
|
className={className}
|
||||||
|
{...props} // forward the remaining props to the Button component
|
||||||
>
|
>
|
||||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
{isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
||||||
{props.text}
|
{text}
|
||||||
<>
|
<>
|
||||||
{props.icon ? props.icon : null}
|
{icon ? icon : null}
|
||||||
</>
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const deleteUserAction = userAction
|
|||||||
data: {
|
data: {
|
||||||
email: `${uuid}@portabase.com`,
|
email: `${uuid}@portabase.com`,
|
||||||
name: `${uuid}`,
|
name: `${uuid}`,
|
||||||
|
deleted: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full py-4">
|
<div className="flex flex-col h-full py-4">
|
||||||
<div className="flex gap-4 h-fit justify-between">
|
<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 && (
|
{props.settings.smtpFrom && (
|
||||||
<ButtonWithLoading
|
<ButtonWithLoading
|
||||||
isPending={mutation.isPending}
|
isPending={mutation.isPending}
|
||||||
|
|||||||
+28
-2
@@ -1,10 +1,15 @@
|
|||||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
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 {Switch} from "@/components/ui/switch";
|
||||||
import {Label} from "@/components/ui/label";
|
import {Label} from "@/components/ui/label";
|
||||||
import {StorageS3Form} from "@/components/wrappers/Dashboard/Settings/SettingsStorageTab/StorageS3Form/StorageS3Form";
|
import {StorageS3Form} from "@/components/wrappers/Dashboard/Settings/SettingsStorageTab/StorageS3Form/StorageS3Form";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {Settings} from "@prisma/client";
|
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 = {
|
export type SettingsStorageTabProps = {
|
||||||
settings: Settings
|
settings: Settings
|
||||||
@@ -12,7 +17,15 @@ export type SettingsStorageTabProps = {
|
|||||||
|
|
||||||
export const SettingsStorageTab = (props: 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");
|
const [isSwitched, setIsSwitched] = useState<boolean>(props.settings.storage !== "local");
|
||||||
|
|
||||||
@@ -29,6 +42,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
<div className="flex flex-col h-full py-4 ">
|
<div className="flex flex-col h-full py-4 ">
|
||||||
|
<div className="flex items-center justify-between space-x-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Label htmlFor="storage-mode">Storage Mode (Local/s3 compatible)</Label>
|
<Label htmlFor="storage-mode">Storage Mode (Local/s3 compatible)</Label>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -38,6 +52,18 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
|||||||
}}
|
}}
|
||||||
id="storage-mode"/>
|
id="storage-mode"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<ButtonWithLoading
|
||||||
|
disabled={!isSwitched}
|
||||||
|
isPending={mutation.isPending}
|
||||||
|
onClick={async () => {
|
||||||
|
await mutation.mutateAsync()
|
||||||
|
}}
|
||||||
|
icon={<ShieldCheck />}
|
||||||
|
text="Test connexion"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{isSwitched && (
|
{isSwitched && (
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null}/>
|
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null}/>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {User, Settings} from "@prisma/client";
|
|||||||
import {backupColumns} from "@/features/backup/columns";
|
import {backupColumns} from "@/features/backup/columns";
|
||||||
import {SettingsEmailTab} from "@/components/wrappers/Dashboard/Settings/SettingsEmailTab/SettingsEmailTab";
|
import {SettingsEmailTab} from "@/components/wrappers/Dashboard/Settings/SettingsEmailTab/SettingsEmailTab";
|
||||||
import {SettingsStorageTab} from "@/components/wrappers/Dashboard/Settings/SettingsStorageTab/SettingsStorageTab";
|
import {SettingsStorageTab} from "@/components/wrappers/Dashboard/Settings/SettingsStorageTab/SettingsStorageTab";
|
||||||
|
import {SettingsUsersTab} from "@/components/wrappers/Dashboard/Settings/SettingsUsersTab/SettingsUsersTab";
|
||||||
|
|
||||||
|
|
||||||
export type SettingsTabsProps = {
|
export type SettingsTabsProps = {
|
||||||
@@ -34,7 +35,7 @@ export const SettingsTabs = (props: SettingsTabsProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="users" className="h-full justify-between">
|
<TabsContent value="users" className="h-full justify-between">
|
||||||
<DataTableWithPagination columns={usersColumns} data={props.users}/>
|
<SettingsUsersTab users={props.users}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="email">
|
<TabsContent value="email">
|
||||||
<SettingsEmailTab settings={props.settings}/>
|
<SettingsEmailTab settings={props.settings}/>
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ import {Badge} from "@/components/ui/badge";
|
|||||||
import {User} from "@prisma/client";
|
import {User} from "@prisma/client";
|
||||||
|
|
||||||
export const usersColumns: ColumnDef<User>[] = [
|
export const usersColumns: ColumnDef<User>[] = [
|
||||||
{
|
|
||||||
accessorKey: "id",
|
|
||||||
header: "Id",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
header: "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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import {v4 as uuidv4} from 'uuid';
|
|||||||
import { writeFile, access, mkdir } from "fs/promises";
|
import { writeFile, access, mkdir } from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {env} from "@/env.mjs";
|
import {env} from "@/env.mjs";
|
||||||
|
import {checkMinioAlive} from "@/utils/s3-file-management";
|
||||||
|
|
||||||
|
|
||||||
export const uploadImageAction = userAction
|
export const uploadImageAction = userAction
|
||||||
@@ -18,6 +19,32 @@ export const uploadImageAction = userAction
|
|||||||
const fileName = uuid + "." + fileFormat
|
const fileName = uuid + "." + fileFormat
|
||||||
const arrayBuffer = await file.arrayBuffer()
|
const arrayBuffer = await file.arrayBuffer()
|
||||||
const buffer = Buffer.from(arrayBuffer)
|
const buffer = Buffer.from(arrayBuffer)
|
||||||
|
|
||||||
|
|
||||||
|
// 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/"
|
const localDir = "public/uploads/"
|
||||||
try {
|
try {
|
||||||
await mkdir(path.join(process.cwd(), localDir), { recursive: true });
|
await mkdir(path.join(process.cwd(), localDir), { recursive: true });
|
||||||
@@ -39,8 +66,13 @@ export const uploadImageAction = userAction
|
|||||||
throw new Error('An error occured while importing image');
|
throw new Error('An error occured while importing image');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadS3Compatible() {
|
||||||
|
await checkMinioAlive()
|
||||||
|
}
|
||||||
|
|
||||||
});
|
export async function checkConnexionToS3(){
|
||||||
|
return await checkMinioAlive()
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -41,7 +41,7 @@ async function createSettingsIfNotExist() {
|
|||||||
console.log("====Init Setting : Create ====")
|
console.log("====Init Setting : Create ====")
|
||||||
await prisma.settings.create({
|
await prisma.settings.create({
|
||||||
data: {
|
data: {
|
||||||
name: "system",
|
...configSettings
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}else{
|
}else{
|
||||||
|
|||||||
@@ -1,19 +1,38 @@
|
|||||||
import * as Minio from 'minio'
|
import * as Minio from 'minio'
|
||||||
import {env} from "@/env.mjs";
|
import {env} from "@/env.mjs";
|
||||||
import internal from "node:stream";
|
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
|
// 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" ?
|
export const s3Client = env.NODE_ENV === "production" ?
|
||||||
new Minio.Client({
|
new Minio.Client({
|
||||||
endPoint: env.S3_ENDPOINT ?? "",
|
endPoint: settings.s3EndPointUrl ?? "",
|
||||||
accessKey: env.S3_ACCESS_KEY ?? "",
|
accessKey: settings.s3AccessKeyId ?? "",
|
||||||
secretKey: env.S3_SECRET_KEY ?? "",
|
secretKey: settings.s3SecretAccessKey ?? "",
|
||||||
}) : new Minio.Client({
|
}) : new Minio.Client({
|
||||||
endPoint: env.S3_ENDPOINT ?? "",
|
endPoint: settings.s3EndPointUrl ?? "",
|
||||||
port: Number(env.S3_PORT ?? 0),
|
port: Number(env.S3_PORT ?? 0),
|
||||||
accessKey: env.S3_ACCESS_KEY ?? "",
|
accessKey: settings.s3AccessKeyId ?? "",
|
||||||
secretKey: env.S3_SECRET_KEY ?? "",
|
secretKey: settings.s3SecretAccessKey ?? "",
|
||||||
useSSL: env.S3_USE_SSL === 'true'
|
useSSL: env.S3_USE_SSL === 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -22,8 +41,11 @@ export async function checkMinioAlive() {
|
|||||||
// Try to list buckets to check connectivity
|
// Try to list buckets to check connectivity
|
||||||
const buckets = await s3Client.listBuckets();
|
const buckets = await s3Client.listBuckets();
|
||||||
console.log('MinIO is up and running. Buckets:', buckets);
|
console.log('MinIO is up and running. Buckets:', buckets);
|
||||||
|
return {message: true}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error connecting to MinIO:', error);
|
console.error('Error connecting to MinIO:', error);
|
||||||
|
return {error: error}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user