mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96be14e1ba | ||
|
|
eec3c2737b | ||
|
|
a6b960ffb2 | ||
|
|
d3f465b5d4 | ||
|
|
2139dfd55d |
@@ -18,8 +18,12 @@
|
|||||||
·
|
·
|
||||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||||
</p>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 📚 Table of Contents
|
## 📚 Table of Contents
|
||||||
|
|
||||||
- [About The Project](#about-the-project)
|
- [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 {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {db} from "@/db";
|
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 * as drizzleDb from "@/db";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
import {DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
import {DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
||||||
@@ -53,7 +53,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
_count: count(),
|
_count: count(),
|
||||||
})
|
})
|
||||||
.from(drizzleDb.schemas.backup)
|
.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)
|
.groupBy(drizzleDb.schemas.backup.createdAt, drizzleDb.schemas.backup.status)
|
||||||
.orderBy(drizzleDb.schemas.backup.createdAt);
|
.orderBy(drizzleDb.schemas.backup.createdAt);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,9 +2,17 @@ import path from "path";
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { NextResponse } from "next/server";
|
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 {
|
try {
|
||||||
const fileName = (await params).fileName;
|
const fileName = (await params).fileName;
|
||||||
|
|
||||||
|
|
||||||
|
console.log("fileName", fileName);
|
||||||
|
|
||||||
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
||||||
|
|
||||||
// Check if the file exists
|
// Check if the file exists
|
||||||
|
|||||||
+4
-1
@@ -23,7 +23,7 @@ services:
|
|||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5433:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres-data:/var/lib/postgresql/data
|
- postgres-data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
@@ -53,16 +53,19 @@ services:
|
|||||||
# - db
|
# - db
|
||||||
|
|
||||||
s3:
|
s3:
|
||||||
|
container_name: s3-portabase-dev
|
||||||
image: docker.io/bitnami/minio:latest
|
image: docker.io/bitnami/minio:latest
|
||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
- "9001:9001"
|
- "9001:9001"
|
||||||
volumes:
|
volumes:
|
||||||
- minio_data:/data
|
- minio_data:/data
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
|
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
|
||||||
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
|
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
|
||||||
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
|
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
|
||||||
|
- MINIO_BROWSER=on
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
|||||||
+3
-3
@@ -44,9 +44,9 @@ const nextConfig: NextConfig = {
|
|||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
// compiler: {
|
compiler: {
|
||||||
// removeConsole: process.env.NODE_ENV === "production",
|
removeConsole: process.env.NODE_ENV === "production",
|
||||||
// },
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
nodeMiddleware: true,
|
nodeMiddleware: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
import {checkConnexionToS3} from "@/features/upload/public/upload.action";
|
import {checkConnexionToS3} from "@/features/upload/public/upload.action";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import { updateStorageSettingsAction } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
|
import {
|
||||||
|
updateStorageSettingsAction
|
||||||
|
} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
|
||||||
import {Setting} from "@/db/schema/00_setting";
|
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 = {
|
export type SettingsStorageTabProps = {
|
||||||
settings: Setting;
|
settings: Setting;
|
||||||
@@ -48,6 +51,16 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
|||||||
await updateMutation.mutateAsync();
|
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 (
|
return (
|
||||||
<div className="flex flex-col h-full py-4">
|
<div className="flex flex-col h-full py-4">
|
||||||
<h1>Settings for Portabase storage</h1>
|
<h1>Settings for Portabase storage</h1>
|
||||||
@@ -55,7 +68,8 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
|||||||
<Info className="h-4 w-4"/>
|
<Info className="h-4 w-4"/>
|
||||||
<AlertTitle>Informations</AlertTitle>
|
<AlertTitle>Informations</AlertTitle>
|
||||||
<AlertDescription>
|
<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.
|
and .dump files in another.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -86,7 +100,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
|||||||
</div>
|
</div>
|
||||||
{isSwitched && (
|
{isSwitched && (
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null} />
|
<StorageS3Form defaultValues={extractS3FormValues(props.settings)}/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ export const SidebarMenuCustomMain = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const groupContentApplication: SidebarGroupItem["group_content"] = [
|
||||||
|
{ title: "Dashboard", url: "/home", icon: Home },
|
||||||
|
];
|
||||||
|
|
||||||
const groupContent: SidebarGroupItem["group_content"] = [
|
const groupContent: SidebarGroupItem["group_content"] = [
|
||||||
{ title: "Dashboard", url: "/home", icon: Home },
|
|
||||||
{ title: "Projects", url: "/projects", icon: Layers, details:true },
|
{ title: "Projects", url: "/projects", icon: Layers, details:true },
|
||||||
{ title: "Statistics", url: "/statistics", icon: ChartArea },
|
{ title: "Statistics", url: "/statistics", icon: ChartArea },
|
||||||
];
|
];
|
||||||
@@ -37,6 +39,11 @@ export const SidebarMenuCustomMain = () => {
|
|||||||
{
|
{
|
||||||
label: "Application",
|
label: "Application",
|
||||||
type: "list",
|
type: "list",
|
||||||
|
group_content: groupContentApplication,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Organization",
|
||||||
|
type: "list",
|
||||||
group_content: groupContent,
|
group_content: groupContent,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
|
|||||||
submitImage.mutate(file);
|
submitImage.mutate(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative ">
|
<div className="relative ">
|
||||||
<Avatar className="size-14 mr-3 ">
|
<Avatar className="size-14 mr-3 ">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as drizzleDb from "@/db";
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {env} from "@/env.mjs";
|
import {env} from "@/env.mjs";
|
||||||
import {nextCookies} from "better-auth/next-js";
|
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 {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions";
|
||||||
import {headers} from "next/headers";
|
import {headers} from "next/headers";
|
||||||
import {count, eq} from "drizzle-orm";
|
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 {
|
try {
|
||||||
return await auth.api.listOrganizations({
|
return await auth.api.listOrganizations({
|
||||||
headers: await headers(),
|
headers: await headers(),
|
||||||
});
|
}) as Organization[];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,37 +5,6 @@ import {db} from "@/db";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
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() {
|
async function getS3Client() {
|
||||||
const settings = await db
|
const settings = await db
|
||||||
|
|||||||
Reference in New Issue
Block a user