Compare commits

..
Author SHA1 Message Date
charlesgauthereau 96be14e1ba Working on the s3 feature. 2025-07-31 17:58:40 +02:00
charlesgauthereau eec3c2737b Working on the s3 feature. 2025-07-31 17:32:03 +02:00
charlesgauthereau a6b960ffb2 Working on the s3 feature. 2025-07-29 09:53:38 +02:00
charlesgauthereau d3f465b5d4 Working on the home page. 2025-07-29 08:51:50 +02:00
charlesgauthereau 2139dfd55d For release. 2025-07-28 18:55:49 +02:00
12 changed files with 157 additions and 80 deletions
+4
View File
@@ -18,8 +18,12 @@
·
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
</p>
<iframe width="560" height="315" src="https://www.youtube.com/watch?v=D9uFrGxLc4s" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
## 📚 Table of Contents
- [About The Project](#about-the-project)
@@ -1,21 +0,0 @@
import { PageParams } from "@/types/next";
import {Page, PageHeader, PageTitle} from "@/features/layout/page";
export default async function RoutePage(props: PageParams<{}>) {
return (
<Page>
<PageHeader>
<PageTitle>Dashboard</PageTitle>
</PageHeader>
<div className="flex flex-1 flex-col gap-4">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
</div>
</Page>
);
}
@@ -5,7 +5,7 @@ import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/cha
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
import {notFound} from "next/navigation";
import {db} from "@/db";
import {asc, count, eq, inArray} from "drizzle-orm";
import {and, asc, count, eq, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {getOrganization} from "@/lib/auth/auth";
import {DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
@@ -53,7 +53,7 @@ export default async function RoutePage(props: PageParams<{}>) {
_count: count(),
})
.from(drizzleDb.schemas.backup)
.where(inArray(drizzleDb.schemas.backup.status, ["success", "failed"]))
.where(and(inArray(drizzleDb.schemas.backup.status, ["success", "failed"]), inArray(drizzleDb.schemas.backup.databaseId, databaseIds)))
.groupBy(drizzleDb.schemas.backup.createdAt, drizzleDb.schemas.backup.status)
.orderBy(drizzleDb.schemas.backup.createdAt);
+90
View File
@@ -0,0 +1,90 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {Building2, DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
import {currentUser} from "@/lib/auth/current-user";
import {notFound} from "next/navigation";
import {db} from "@/db";
import {asc, eq, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {auth, listOrganizations} from "@/lib/auth/auth";
import {authClient} from "@/lib/auth/auth-client";
export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser();
const organizations = await listOrganizations()
if (!user || !organizations) notFound();
const organizationIds = organizations.map(project => project.id);
const projects = await db.query.project.findMany({
where: inArray(drizzleDb.schemas.project.organizationId, organizationIds),
});
const projectIds = projects.map(project => project.id);
const databasesOfAllProjects = await db.query.database.findMany({
where: inArray(drizzleDb.schemas.database.projectId, projectIds),
})
const databaseIds = databasesOfAllProjects.map((database) => database.id);
const backupsEvolution = await db.query.backup.findMany({
columns: {
id: true,
createdAt: true,
},
orderBy: [asc(drizzleDb.schemas.backup.id)],
where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
});
return (
<Page>
<PageHeader>
<PageTitle>Dashboard</PageTitle>
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2">
<Building2 className="w-5 h-5 text-muted-foreground"/>
<CardTitle>Organizations</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold">{organizations.length}</CardContent>
</Card>
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2">
<Folder className="w-5 h-5 text-muted-foreground"/>
<CardTitle>Projects</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold">{projects.length}</CardContent>
</Card>
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2">
<DatabaseBackup className="w-5 h-5 text-muted-foreground"/>
<CardTitle>Backups</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold">{backupsEvolution.length}</CardContent>
</Card>
</div>
<div className="flex flex-1 flex-col gap-4">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50"/>
<div className="aspect-video rounded-xl bg-muted/50"/>
<div className="aspect-video rounded-xl bg-muted/50"/>
</div>
</div>
</PageContent>
</Page>
);
}
+9 -1
View File
@@ -2,9 +2,17 @@ import path from "path";
import fs from "fs/promises";
import { NextResponse } from "next/server";
export async function GET({ params }: { params: Promise<{ fileName: string }> }) {
export async function GET(
request: Request,
{params}: { params: Promise<{ fileName: string }> }
) {
try {
const fileName = (await params).fileName;
console.log("fileName", fileName);
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
// Check if the file exists
+4 -1
View File
@@ -23,7 +23,7 @@ services:
db:
image: postgres:16-alpine
ports:
- "5432:5432"
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
@@ -53,16 +53,19 @@ services:
# - db
s3:
container_name: s3-portabase-dev
image: docker.io/bitnami/minio:latest
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
- MINIO_BROWSER=on
volumes:
postgres-data:
+3 -3
View File
@@ -44,9 +44,9 @@ const nextConfig: NextConfig = {
eslint: {
ignoreDuringBuilds: true,
},
// compiler: {
// removeConsole: process.env.NODE_ENV === "production",
// },
compiler: {
removeConsole: process.env.NODE_ENV === "production",
},
experimental: {
nodeMiddleware: true,
},
@@ -1,16 +1,19 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Info, ShieldCheck } from "lucide-react";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { StorageS3Form } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/storage-s3-form";
import { useState } from "react";
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
import { useMutation } from "@tanstack/react-query";
import { checkConnexionToS3 } from "@/features/upload/public/upload.action";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { updateStorageSettingsAction } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {Info, ShieldCheck} from "lucide-react";
import {Switch} from "@/components/ui/switch";
import {Label} from "@/components/ui/label";
import {StorageS3Form} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/storage-s3-form";
import {useState} from "react";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {useMutation} from "@tanstack/react-query";
import {checkConnexionToS3} from "@/features/upload/public/upload.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {
updateStorageSettingsAction
} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
import {Setting} from "@/db/schema/00_setting";
import {S3FormType} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
export type SettingsStorageTabProps = {
settings: Setting;
@@ -33,7 +36,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
const [isSwitched, setIsSwitched] = useState<boolean>(props.settings.storage !== "local");
const updateMutation = useMutation({
mutationFn: () => updateStorageSettingsAction({ name: "system", data: { storage: isSwitched ? "s3" : "local" } }),
mutationFn: () => updateStorageSettingsAction({name: "system", data: {storage: isSwitched ? "s3" : "local"}}),
onSuccess: () => {
toast.success(`Settings updated successfully.`);
router.refresh();
@@ -48,14 +51,25 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
await updateMutation.mutateAsync();
};
const extractS3FormValues = (settings: Setting): S3FormType | undefined => {
if (!settings.s3EndPointUrl) return undefined;
return {
s3EndPointUrl: settings.s3EndPointUrl,
s3AccessKeyId: settings.s3AccessKeyId!,
s3SecretAccessKey: settings.s3SecretAccessKey!,
S3BucketName: settings.S3BucketName!,
};
};
return (
<div className="flex flex-col h-full py-4">
<h1>Settings for Portabase storage</h1>
<Alert className="mt-3">
<Info className="h-4 w-4" />
<Info className="h-4 w-4"/>
<AlertTitle>Informations</AlertTitle>
<AlertDescription>
Actually you can only store you data in one place : s3 compatible or in local. For exemple you cannot choose to store images in one place
Actually you can only store you data in one place : s3 compatible or in local. For exemple you
cannot choose to store images in one place
and .dump files in another.
</AlertDescription>
</Alert>
@@ -79,14 +93,14 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
onClick={async () => {
await mutation.mutateAsync();
}}
icon={<ShieldCheck />}
icon={<ShieldCheck/>}
text="Test connexion"
/>
</div>
</div>
{isSwitched && (
<div className="mt-5">
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null} />
<StorageS3Form defaultValues={extractS3FormValues(props.settings)}/>
</div>
)}
</div>
@@ -22,9 +22,11 @@ export const SidebarMenuCustomMain = () => {
return null;
}
const groupContentApplication: SidebarGroupItem["group_content"] = [
{ title: "Dashboard", url: "/home", icon: Home },
];
const groupContent: SidebarGroupItem["group_content"] = [
{ title: "Dashboard", url: "/home", icon: Home },
{ title: "Projects", url: "/projects", icon: Layers, details:true },
{ title: "Statistics", url: "/statistics", icon: ChartArea },
];
@@ -37,6 +39,11 @@ export const SidebarMenuCustomMain = () => {
{
label: "Application",
type: "list",
group_content: groupContentApplication,
},
{
label: "Organization",
type: "list",
group_content: groupContent,
},
];
@@ -53,6 +53,8 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
submitImage.mutate(file);
};
return (
<div className="relative ">
<Avatar className="size-14 mr-3 ">
+4 -3
View File
@@ -4,7 +4,7 @@ import * as drizzleDb from "@/db";
import {db} from "@/db";
import {env} from "@/env.mjs";
import {nextCookies} from "better-auth/next-js";
import {admin as adminPlugin, openAPI, organization} from "better-auth/plugins";
import {admin as adminPlugin, openAPI, Organization, organization} from "better-auth/plugins";
import {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions";
import {headers} from "next/headers";
import {count, eq} from "drizzle-orm";
@@ -353,12 +353,13 @@ export const getOrganization = async ({
}
};
export const listOrganizations = async () => {
export const listOrganizations = async (): Promise<Organization[] | null> => {
try {
return await auth.api.listOrganizations({
headers: await headers(),
});
}) as Organization[];
} catch (e) {
return null;
}
};
-31
View File
@@ -5,37 +5,6 @@ import {db} from "@/db";
import * as drizzleDb from "@/db";
import {eq} from "drizzle-orm";
// 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: settings.s3EndPointUrl ?? "",
// accessKey: settings.s3AccessKeyId ?? "",
// secretKey: settings.s3SecretAccessKey ?? "",
// }) : new Minio.Client({
// endPoint: settings.s3EndPointUrl ?? "",
// port: Number(env.S3_PORT ?? 0),
// accessKey: settings.s3AccessKeyId ?? "",
// secretKey: settings.s3SecretAccessKey ?? "",
// useSSL: env.S3_USE_SSL === 'true'
// })
async function getS3Client() {
const settings = await db