mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
47
Commits
1.2.5
...
1.3.0-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9deb3cdf0 | ||
|
|
5dbf293e1e | ||
|
|
7232f17cb7 | ||
|
|
5b1c3d0606 | ||
|
|
8e6ca64fa2 | ||
|
|
9116b4d126 | ||
|
|
4ce38d9857 | ||
|
|
8811c48384 | ||
|
|
078d0d1ee2 | ||
|
|
56d755afa6 | ||
|
|
9e3abb366f | ||
|
|
4ebac1c46f | ||
|
|
1476df7a1f | ||
|
|
833872603a | ||
|
|
ebe5c63a7b | ||
|
|
afd98ae652 | ||
|
|
8f905c8128 | ||
|
|
f43f30c9b9 | ||
|
|
6b1bea9b17 | ||
|
|
cdaed9766e | ||
|
|
d31a9ab2d2 | ||
|
|
07d49749fb | ||
|
|
a1f1da8a07 | ||
|
|
1b791fd287 | ||
|
|
2a4af58a51 | ||
|
|
b12f8ca044 | ||
|
|
519b3abd1d | ||
|
|
e08a5bd6b6 | ||
|
|
f6c597b42d | ||
|
|
f3b9032174 | ||
|
|
44b4ee2953 | ||
|
|
b77f79f42d | ||
|
|
8017bf7c8a | ||
|
|
85f4cda752 | ||
|
|
1fd2f553ed | ||
|
|
d68170eda8 | ||
|
|
51f27226e7 | ||
|
|
d54c366323 | ||
|
|
0e7cb9fbab | ||
|
|
63e83032f1 | ||
|
|
b4da6fdd9f | ||
|
|
12535a59e8 | ||
|
|
b345eda7aa | ||
|
|
660c1dcadf | ||
|
|
be66856034 | ||
|
|
80e3027107 | ||
|
|
362ca53efb |
+1
-3
@@ -2,9 +2,7 @@ title = "Custom gitleaks config"
|
|||||||
|
|
||||||
[allowlist]
|
[allowlist]
|
||||||
# Global allowlist patterns (won’t be flagged)
|
# Global allowlist patterns (won’t be flagged)
|
||||||
regexes = [
|
regexes = []
|
||||||
"s3SecretAccessKey: env.S3_SECRET_KEY ?? null",
|
|
||||||
]
|
|
||||||
paths = [
|
paths = [
|
||||||
"src/utils/init.ts"
|
"src/utils/init.ts"
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-2
@@ -26,5 +26,5 @@ keywords:
|
|||||||
- web-ui
|
- web-ui
|
||||||
- agent
|
- agent
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
version: 1.2.5
|
version: 1.3.0-rc.1
|
||||||
date-released: "2026-02-13"
|
date-released: "2026-02-23"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<h3 align="center">Portabase</h3>
|
<h3 align="center">Portabase</h3>
|
||||||
|
|
||||||
<p align="center" style="margin-top: 20px; font-style: italic;">
|
<p align="center" style="margin-top: 20px; font-style: italic;">
|
||||||
<i>Portabase is a tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with <a href="https://github.com/Portabase/agent">Portabase agents</a> for managing operations securely and efficiently.</i>
|
<i>Portabase is a tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with <a href="https://github.com/Portabase/agent-rust">Portabase agents</a> for managing operations securely and efficiently.</i>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
[](https://github.com/Portabase/portabase)
|
[](https://github.com/Portabase/portabase)
|
||||||
[](https://github.com/Portabase/portabase)
|
[](https://github.com/Portabase/portabase)
|
||||||
|
|
||||||
|
|
||||||
[![NextJS][NextJS]][NextJS-url]
|
[![NextJS][NextJS]][NextJS-url]
|
||||||
[![BetterAuth][BetterAuth]][BetterAuth-url]
|
[![BetterAuth][BetterAuth]][BetterAuth-url]
|
||||||
[![Drizzle][Drizzle]][Drizzle-url]
|
[![Drizzle][Drizzle]][Drizzle-url]
|
||||||
@@ -34,14 +33,14 @@
|
|||||||
<strong>
|
<strong>
|
||||||
<a href="https://portabase.io">Website</a> •
|
<a href="https://portabase.io">Website</a> •
|
||||||
<a href="https://portabase.io/docs">Documentation</a> •
|
<a href="https://portabase.io/docs">Documentation</a> •
|
||||||
<a href="https://www.youtube.com/watch?v=hvLbX5LN1UE">Demo</a> •
|
<a href="https://www.youtube.com/watch?v=nSTzT27GgAg">Demo</a> •
|
||||||
<a href="https://portabase.io/docs/dashboard/setup">Installation</a> •
|
<a href="https://portabase.io/docs/dashboard/setup">Installation</a> •
|
||||||
<a href="https://github.com/Portabase/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> •
|
<a href="https://github.com/Portabase/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> •
|
||||||
<a href="https://github.com/Portabase/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
<a href="https://github.com/Portabase/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -51,14 +50,13 @@
|
|||||||
You have 4 ways to install Portabase:
|
You have 4 ways to install Portabase:
|
||||||
|
|
||||||
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
|
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
|
||||||
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker)
|
- Docker Run - [details](https://portabase.io/docs/dashboard/setup#docker)
|
||||||
|
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker-compose)
|
||||||
- Kubernetes with Helm (soon)
|
- Kubernetes with Helm (soon)
|
||||||
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
|
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
|
||||||
|
|
||||||
**Ensure Docker is installed on your machine before getting started.**
|
**Ensure Docker is installed on your machine before getting started.**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
[](https://github.com/Portabase/portabase/graphs/contributors)
|
[](https://github.com/Portabase/portabase/graphs/contributors)
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {SettingsEmailSection} from "@/components/wrappers/dashboard/admin/settings/email/settings-email-section";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
|
||||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!settings) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader className="flex flex-col">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<PageTitle className="mb-3">System email</PageTitle>
|
|
||||||
</div>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent className="flex flex-col gap-5">
|
|
||||||
<SettingsEmailSection settings={settings}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {SettingsStorageSection} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage-section";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
|
||||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!settings) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader className="flex flex-col">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<PageTitle className="mb-3">System storage</PageTitle>
|
|
||||||
</div>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent className="flex flex-col gap-5">
|
|
||||||
<SettingsStorageSection settings={settings}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -35,12 +35,10 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>Notification channels</PageTitle>
|
<PageTitle>Notification channels</PageTitle>
|
||||||
<PageActions>
|
<PageActions>
|
||||||
{/*<NotifierAddEditModal adminView={false}/>*/}
|
|
||||||
<ChannelAddEditModal kind="notification" adminView={false}/>
|
<ChannelAddEditModal kind="notification" adminView={false}/>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
{/*<NotificationChannelsSection organizations={organizations} notificationChannels={notificationChannels}/>*/}
|
|
||||||
<ChannelsSection kind="notification" organizations={organizations}
|
<ChannelsSection kind="notification" organizations={organizations}
|
||||||
channels={notificationChannels}/>
|
channels={notificationChannels}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
@@ -29,7 +30,8 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const uploadDir = path.join(process.cwd(), "/private/uploads/");
|
// const uploadDir = path.join(process.cwd(), "/private/uploads/");
|
||||||
|
const uploadDir = path.join(env.PRIVATE_PATH, "/uploads/");
|
||||||
|
|
||||||
const oldFilePath = path.join(uploadDir, "tmp", id);
|
const oldFilePath = path.join(uploadDir, "tmp", id);
|
||||||
const newFilePath = path.join(uploadDir, filePath);
|
const newFilePath = path.join(uploadDir, filePath);
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
name: portabase-prod
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build:
|
# build:
|
||||||
context: .
|
# context: .
|
||||||
dockerfile: docker/dockerfile/Dockerfile
|
# dockerfile: docker/dockerfile/Dockerfile
|
||||||
target: prod
|
# target: prod
|
||||||
image: portabase/portabase:1.2.5-rc.9
|
image: portabase/portabase:1.2.7-rc.1
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
environment:
|
environment:
|
||||||
TZ: "Europe/Paris"
|
TZ: "Europe/Paris"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
volumes:
|
||||||
|
- portabase-data:/data
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -36,4 +35,5 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
portabase-data:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
|||||||
@@ -18,6 +18,21 @@ RUN apt-get update && apt-get install -y \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
RUN mkdir -p /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
|
||||||
|
| gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" \
|
||||||
|
> /etc/apt/sources.list.d/pgdg.list
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
postgresql-18 \
|
||||||
|
postgresql-client-18 \
|
||||||
|
postgresql-contrib-18 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV PATH="/usr/lib/postgresql/18/bin:${PATH}"
|
||||||
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.23-bookworm AS tusd-base
|
FROM --platform=$BUILDPLATFORM golang:1.23-bookworm AS tusd-base
|
||||||
|
|
||||||
ARG TUSD_VERSION=2.8.0
|
ARG TUSD_VERSION=2.8.0
|
||||||
@@ -76,6 +91,9 @@ ENV NODE_ENV=production
|
|||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
ENV PORT=80
|
ENV PORT=80
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
ENV PGDATA=/data/postgres
|
||||||
|
ENV PRIVATE_PATH=/data/private
|
||||||
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
@@ -94,14 +112,16 @@ RUN chmod +x /usr/local/bin/tusd
|
|||||||
|
|
||||||
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
#RUN mkdir -p .next /app/private/uploads \
|
||||||
|
# && chown -R nextjs:nodejs .next /app/private /app/public
|
||||||
|
|
||||||
|
RUN mkdir -p .next /data/private/uploads \
|
||||||
|
&& chown -R nextjs:nodejs .next /data/private /app/public
|
||||||
|
|
||||||
RUN mkdir -p .next /app/private/uploads \
|
|
||||||
&& chown -R nextjs:nodejs .next /app/private /app/public
|
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
|
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
|
||||||
RUN chmod +x /app/app-prod-entrypoint.sh
|
RUN chmod +x /app/app-prod-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
#USER nextjs
|
|
||||||
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
||||||
|
|||||||
@@ -7,9 +7,72 @@ else
|
|||||||
echo "[WARN] No TZ provided, using default container timezone"
|
echo "[WARN] No TZ provided, using default container timezone"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p /app/private/uploads/tmp
|
POSTGRES_BIN=$(ls -d /usr/lib/postgresql/*/bin | head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$POSTGRES_BIN" ]; then
|
||||||
|
echo "PostgreSQL binaries not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PATH="$POSTGRES_BIN:$PATH"
|
||||||
|
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "[INFO] No DATABASE_URL provided, starting internal Postgres..."
|
||||||
|
|
||||||
|
mkdir -p "$PGDATA"
|
||||||
|
chown -R postgres:postgres "$PGDATA"
|
||||||
|
|
||||||
|
if [ ! -f "$PGDATA/PG_VERSION" ]; then
|
||||||
|
echo "[INFO] Initializing database cluster..."
|
||||||
|
if ! su postgres -c "initdb -D '$PGDATA'" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] initdb failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! su postgres -c "pg_ctl -D '$PGDATA' \
|
||||||
|
-o \"-c listen_addresses='localhost' -c logging_collector=on\" \
|
||||||
|
-l $PGDATA/postgres.log -w start" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] PostgreSQL failed to start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
until su postgres -c "pg_isready -h 127.0.0.1 -p 5432" > /dev/null 2>&1; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[INFO] PostgreSQL server is up and accepting connections"
|
||||||
|
|
||||||
|
DB_USER="${POSTGRES_USER:-portabase_user}"
|
||||||
|
DB_PASS="${POSTGRES_PASSWORD:-JaB6b1SUtIWYvt7srnOt}"
|
||||||
|
DB_NAME="${POSTGRES_DB:-portabase_db}"
|
||||||
|
|
||||||
|
USER_EXISTS=$(su postgres -c "psql -tAc \"SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'\"" 2>/dev/null)
|
||||||
|
if [ "$USER_EXISTS" != "1" ]; then
|
||||||
|
if ! su postgres -c "psql -c \"CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';\"" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] Failed creating user"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
DB_EXISTS=$(su postgres -c "psql -tAc \"SELECT 1 FROM pg_database WHERE datname='$DB_NAME'\"" 2>/dev/null)
|
||||||
|
if [ "$DB_EXISTS" != "1" ]; then
|
||||||
|
if ! su postgres -c "psql -c \"CREATE DATABASE $DB_NAME OWNER $DB_USER;\"" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] Failed creating database"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DATABASE_URL="postgres://$DB_USER:$DB_PASS@127.0.0.1:5432/$DB_NAME"
|
||||||
|
|
||||||
|
echo "[SUCCESS] Internal PostgreSQL started successfully"
|
||||||
|
echo "[SUCCESS] Database: $DB_NAME | User: $DB_USER | Host: 127.0.0.1:5432"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
mkdir -p /data/private/uploads/tmp
|
||||||
echo "▶ Starting tusd server..."
|
echo "▶ Starting tusd server..."
|
||||||
tusd --base-path /tus/files/ --upload-dir /app/private/uploads/tmp --hooks-http http://127.0.0.1:3000/api/tus/hooks --port 1080 --max-size 21474836480 &
|
tusd --base-path /tus/files/ --upload-dir /data/private/uploads/tmp --hooks-http http://127.0.0.1:3000/api/tus/hooks --port 1080 --max-size 21474836480 &
|
||||||
|
|
||||||
echo "▶ Starting Next.js server..."
|
echo "▶ Starting Next.js server..."
|
||||||
PORT=3000 node server.js &
|
PORT=3000 node server.js &
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.2.5",
|
"version": "1.3.0-rc.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
@@ -127,5 +127,5 @@
|
|||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"zenstack": "2.14.2"
|
"zenstack": "2.14.2"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8"
|
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
export interface AuthProviderConfig {
|
export interface AuthProviderConfig {
|
||||||
id: "google" | "github" | "credential";
|
id: "google" | "github" | "credential";
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -64,7 +62,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
|
|||||||
FORM_ACTION: ["'self'"],
|
FORM_ACTION: ["'self'"],
|
||||||
FRAME_ANCESTORS: ["'none'"],
|
FRAME_ANCESTORS: ["'none'"],
|
||||||
BLOCK_ALL_MIXED_CONTENT: false,
|
BLOCK_ALL_MIXED_CONTENT: false,
|
||||||
UPGRADE_INSECURE_REQUESTS: true,
|
UPGRADE_INSECURE_REQUESTS: process.env.PROJECT_URL?.startsWith("https://") || false,
|
||||||
},
|
},
|
||||||
PERMISSIONS_POLICY: {
|
PERMISSIONS_POLICY: {
|
||||||
CAMERA: ["()"],
|
CAMERA: ["()"],
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -4,7 +4,7 @@ set -e
|
|||||||
|
|
||||||
if [ -z "$1" ]; then
|
if [ -z "$1" ]; then
|
||||||
echo "Usage: ./release <version>"
|
echo "Usage: ./release <version>"
|
||||||
echo "Example: ./release v1.0.0"
|
echo "Example: ./release 1.0.0"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -208,12 +208,14 @@ type UseZodFormProps<Z extends ZodSchema> = Exclude<
|
|||||||
schema: Z;
|
schema: Z;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const useZodForm = <Z extends ZodSchema>({
|
const useZodForm = <Z extends ZodSchema>({
|
||||||
schema,
|
schema,
|
||||||
...formProps
|
...formProps
|
||||||
}: UseZodFormProps<Z>) =>
|
}: UseZodFormProps<Z>) =>
|
||||||
useForm({
|
useForm({
|
||||||
...formProps,
|
...formProps,
|
||||||
|
// @ts-ignore
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -224,7 +224,7 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
|||||||
|
|
||||||
<div className="flex justify-between mt-4">
|
<div className="flex justify-between mt-4">
|
||||||
<div>
|
<div>
|
||||||
{kind == "storage" && (
|
{(!isCreate || kind == "storage") && (
|
||||||
<ChannelTestButton
|
<ChannelTestButton
|
||||||
kind={kind}
|
kind={kind}
|
||||||
organizationId={organization?.id}
|
organizationId={organization?.id}
|
||||||
|
|||||||
+3
-2
@@ -34,7 +34,7 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
|
|||||||
message: `We are testing channel ${channel.name}`,
|
message: `We are testing channel ${channel.name}`,
|
||||||
level: 'info',
|
level: 'info',
|
||||||
};
|
};
|
||||||
const result = await dispatchNotification(payload, undefined, channel, organizationId);
|
const result = await dispatchNotification(payload, undefined, channel.id, organizationId);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message);
|
toast.success(result.message);
|
||||||
@@ -51,7 +51,8 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
|
|||||||
toast.success("Successfully connected to storage channel");
|
toast.success("Successfully connected to storage channel");
|
||||||
} else {
|
} else {
|
||||||
console.error(result);
|
console.error(result);
|
||||||
toast.error("An error occurred while testing the storage channel, check your configuration");
|
const responseText = result.response ? `(${result.response})` : "";
|
||||||
|
toast.error(`An error occurred while testing the storage channel, check your configuration ${responseText}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error("Not yet supported");
|
toast.error("Not yet supported");
|
||||||
|
|||||||
+51
-4
@@ -4,7 +4,15 @@ import {Info} from "lucide-react";
|
|||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
useZodForm
|
||||||
|
} from "@/components/ui/form";
|
||||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
@@ -18,6 +26,7 @@ import {
|
|||||||
} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.action";
|
} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.action";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {Switch} from "@/components/ui/switch";
|
import {Switch} from "@/components/ui/switch";
|
||||||
|
import {downloadMasterKeyAction} from "@/features/keys/keys.action";
|
||||||
|
|
||||||
export type SettingsStorageSectionProps = {
|
export type SettingsStorageSectionProps = {
|
||||||
settings: Setting;
|
settings: Setting;
|
||||||
@@ -50,6 +59,34 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
const result = await downloadMasterKeyAction();
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
toast.error(result?.message ?? "Download failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteCharacters = atob(result.data as string);
|
||||||
|
const byteNumbers = new Array(byteCharacters.length)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, i) => byteCharacters.charCodeAt(i));
|
||||||
|
|
||||||
|
const blob = new Blob([new Uint8Array(byteNumbers)], {
|
||||||
|
type: "application/octet-stream",
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "master_key.bin";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<Alert className="mt-3">
|
<Alert className="mt-3">
|
||||||
@@ -59,7 +96,7 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
|||||||
The default storage channel will be used by default to store your backups if no storage policy is
|
The default storage channel will be used by default to store your backups if no storage policy is
|
||||||
configured at the database level. </AlertDescription>
|
configured at the database level. </AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
<div className="flex flex-col h-full py-4 ">
|
<div className="flex flex-col h-full py-4 gap-3">
|
||||||
<Form
|
<Form
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
form={form}
|
form={form}
|
||||||
@@ -96,8 +133,6 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="encryption"
|
name="encryption"
|
||||||
@@ -120,6 +155,18 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
|||||||
Confirm
|
Confirm
|
||||||
</ButtonWithLoading>
|
</ButtonWithLoading>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-sm font-medium ">
|
||||||
|
Download instance <button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="text-sm hover:text-muted-foreground underline underline-offset-4 transition-colors w-fit"
|
||||||
|
>
|
||||||
|
Master Key
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {v4 as uuidv4} from "uuid";
|
|||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {storeBackupFiles} from "@/features/storages/helpers";
|
import {storeBackupFiles} from "@/features/storages/helpers";
|
||||||
import {getFileExtension} from "@/features/api/upload/helpers/file";
|
import {getFileExtension} from "@/utils/common";
|
||||||
|
|
||||||
|
|
||||||
export const uploadBackupAction = userAction
|
export const uploadBackupAction = userAction
|
||||||
@@ -46,18 +46,6 @@ export const uploadBackupAction = userAction
|
|||||||
const fileName = `${uuid}${fileExtension}`;
|
const fileName = `${uuid}${fileExtension}`;
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
// const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
|
||||||
//
|
|
||||||
// if (!settings) {
|
|
||||||
// return {
|
|
||||||
// success: false,
|
|
||||||
// actionError: {
|
|
||||||
// message: "Settings not set",
|
|
||||||
// status: 500,
|
|
||||||
// cause: "Unknown error",
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
const [backup] = await db
|
const [backup] = await db
|
||||||
.insert(drizzleDb.schemas.backup)
|
.insert(drizzleDb.schemas.backup)
|
||||||
@@ -68,30 +56,8 @@ export const uploadBackupAction = userAction
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
await storeBackupFiles(backup, database, buffer, fileName)
|
await storeBackupFiles(backup, database, buffer, fileName)
|
||||||
|
|
||||||
|
|
||||||
// let success: boolean, message: string, filePath: string;
|
|
||||||
//
|
|
||||||
// const result =
|
|
||||||
// settings.storage === "local"
|
|
||||||
// ? await uploadLocalPrivate(fileName, buffer)
|
|
||||||
// : await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
|
|
||||||
//
|
|
||||||
// ({success, message, filePath} = result);
|
|
||||||
//
|
|
||||||
// if (!success) {
|
|
||||||
// return {
|
|
||||||
// success: false,
|
|
||||||
// actionError: {
|
|
||||||
// message: "An error has occurred while uploading file",
|
|
||||||
// status: 500,
|
|
||||||
// cause: "Unknown error",
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
value: backup,
|
value: backup,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {useMutation, useQueryClient} from "@tanstack/react-query";
|
|||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||||
import {deleteBackupAction} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.action";
|
import {deleteBackupAction} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.action";
|
||||||
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
|
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||||
|
|
||||||
|
|
||||||
type DatabaseBackupListProps = {
|
type DatabaseBackupListProps = {
|
||||||
@@ -69,19 +69,12 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
|||||||
mutationFn: async (backups: Backup[]) => {
|
mutationFn: async (backups: Backup[]) => {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
backups.map(async (backup) => {
|
backups.map(async (backup) => {
|
||||||
if (backup.deletedAt == null) {
|
if (backup.deletedAt == null || backup.status == "ongoing") {
|
||||||
|
|
||||||
const backupDeleted = await deleteBackupAction({
|
const backupDeleted = await deleteBackupAction({
|
||||||
databaseId: backup.databaseId,
|
databaseId: backup.databaseId,
|
||||||
backupId: backup.id,
|
backupId: backup.id,
|
||||||
})
|
})
|
||||||
// const backupDeleted = await deleteBackupAction({
|
|
||||||
// backupId: backup.id,
|
|
||||||
// databaseId: backup.databaseId,
|
|
||||||
// status: backup.status,
|
|
||||||
// file: backup.file ?? "",
|
|
||||||
// projectSlug: props.database?.project?.slug!
|
|
||||||
// });
|
|
||||||
return {
|
return {
|
||||||
success: backupDeleted?.data?.success,
|
success: backupDeleted?.data?.success,
|
||||||
message: backupDeleted?.data?.success
|
message: backupDeleted?.data?.success
|
||||||
@@ -136,15 +129,9 @@ export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
|
|||||||
>Actions</ButtonWithLoading>
|
>Actions</ButtonWithLoading>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<ButtonWithConfirm
|
<ButtonWithConfirm
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
const backupsToDelete = rows.map(row => row
|
mutationDeleteBackups.mutate(rows);
|
||||||
).filter(backup => backup.deletedAt == null)
|
|
||||||
if (backupsToDelete.length === 0) {
|
|
||||||
toast.error("No available backup selected for deletion.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mutationDeleteBackups.mutate(backupsToDelete);
|
|
||||||
setIsActionsOpen(false);
|
setIsActionsOpen(false);
|
||||||
}}
|
}}
|
||||||
onCancel={() => setIsActionsOpen(false)}
|
onCancel={() => setIsActionsOpen(false)}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE "public"."dbms_status" ADD VALUE 'sqlite';
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -239,6 +239,13 @@
|
|||||||
"when": 1770923665015,
|
"when": 1770923665015,
|
||||||
"tag": "0033_handy_valeria_richards",
|
"tag": "0033_handy_valeria_richards",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 34,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1771790675240,
|
||||||
|
"tag": "0034_lush_speed",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,6 @@ import {timestamps} from "@/db/schema/00_common";
|
|||||||
import {AlertPolicy, alertPolicy} from "@/db/schema/10_alert-policy";
|
import {AlertPolicy, alertPolicy} from "@/db/schema/10_alert-policy";
|
||||||
import {StoragePolicy, storagePolicy} from "@/db/schema/13_storage-policy";
|
import {StoragePolicy, storagePolicy} from "@/db/schema/13_storage-policy";
|
||||||
import {BackupStorage, backupStorage} from "@/db/schema/14_storage-backup";
|
import {BackupStorage, backupStorage} from "@/db/schema/14_storage-backup";
|
||||||
import {storageChannel} from "@/db/schema/12_storage-channel";
|
|
||||||
|
|
||||||
export const database = pgTable("databases", {
|
export const database = pgTable("databases", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {pgEnum} from "drizzle-orm/pg-core";
|
|||||||
import {createSelectSchema} from "drizzle-zod";
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
|
|
||||||
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mongodb"]);
|
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mongodb", "sqlite"]);
|
||||||
export const statusEnum = pgEnum("status", ["waiting", "ongoing", "failed", "success"]);
|
export const statusEnum = pgEnum("status", ["waiting", "ongoing", "failed", "success"]);
|
||||||
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
|
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
|
||||||
|
|
||||||
|
|||||||
+6
-16
@@ -1,6 +1,7 @@
|
|||||||
import {createEnv} from "@t3-oss/env-nextjs";
|
import {createEnv} from "@t3-oss/env-nextjs";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import packageJson from "../package.json" with {type: "json"};
|
import packageJson from "../package.json" with {type: "json"};
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const {version} = packageJson;
|
const {version} = packageJson;
|
||||||
|
|
||||||
@@ -14,8 +15,8 @@ export const env = createEnv({
|
|||||||
|
|
||||||
PROJECT_NAME: z.string().optional(),
|
PROJECT_NAME: z.string().optional(),
|
||||||
PROJECT_DESCRIPTION: z.string().optional(),
|
PROJECT_DESCRIPTION: z.string().optional(),
|
||||||
PROJECT_URL: z.string().optional(),
|
PROJECT_URL: z.string().regex(/^https?:\/\//, "URL must start with http:// or https://"),
|
||||||
PROJECT_SECRET: z.string().optional(),
|
PROJECT_SECRET: z.string(),
|
||||||
|
|
||||||
SMTP_PASSWORD: z.string().optional(),
|
SMTP_PASSWORD: z.string().optional(),
|
||||||
SMTP_FROM: z.string().optional(),
|
SMTP_FROM: z.string().optional(),
|
||||||
@@ -28,19 +29,14 @@ export const env = createEnv({
|
|||||||
AUTH_GOOGLE_SECRET: z.string().optional(),
|
AUTH_GOOGLE_SECRET: z.string().optional(),
|
||||||
AUTH_GOOGLE_METHOD: z.boolean().default(false),
|
AUTH_GOOGLE_METHOD: z.boolean().default(false),
|
||||||
|
|
||||||
S3_ENDPOINT: z.string().optional(),
|
|
||||||
S3_ACCESS_KEY: z.string().optional(),
|
|
||||||
S3_SECRET_KEY: z.string().optional(),
|
|
||||||
S3_BUCKET_NAME: z.string().optional(),
|
|
||||||
S3_PORT: z.string().optional(),
|
|
||||||
S3_USE_SSL: z.string().optional(),
|
|
||||||
|
|
||||||
STORAGE_TYPE: z.enum(["local", "s3"]).optional(),
|
STORAGE_TYPE: z.enum(["local", "s3"]).optional(),
|
||||||
|
|
||||||
RETENTION_CRON: z
|
RETENTION_CRON: z
|
||||||
.string()
|
.string()
|
||||||
.default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"),
|
.default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"),
|
||||||
|
|
||||||
|
PRIVATE_PATH: z.string(),
|
||||||
|
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
|
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
|
||||||
@@ -66,16 +62,10 @@ export const env = createEnv({
|
|||||||
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
|
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
|
||||||
AUTH_GOOGLE_METHOD: process.env.AUTH_GOOGLE_METHOD === "true",
|
AUTH_GOOGLE_METHOD: process.env.AUTH_GOOGLE_METHOD === "true",
|
||||||
|
|
||||||
S3_ENDPOINT: process.env.S3_ENDPOINT,
|
|
||||||
S3_ACCESS_KEY: process.env.S3_ACCESS_KEY,
|
|
||||||
S3_SECRET_KEY: process.env.S3_SECRET_KEY,
|
|
||||||
S3_BUCKET_NAME: process.env.S3_BUCKET_NAME,
|
|
||||||
S3_PORT: process.env.S3_PORT,
|
|
||||||
S3_USE_SSL: process.env.S3_USE_SSL,
|
|
||||||
|
|
||||||
STORAGE_TYPE: process.env.STORAGE_TYPE,
|
STORAGE_TYPE: process.env.STORAGE_TYPE,
|
||||||
|
|
||||||
RETENTION_CRON: process.env.RETENTION_CRON,
|
RETENTION_CRON: process.env.RETENTION_CRON,
|
||||||
|
|
||||||
|
PRIVATE_PATH: process.env.PRIVATE_PATH || path.join(process.cwd(), 'private')
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import {Request, Response, NextFunction} from "express";
|
|
||||||
|
|
||||||
export function loggingMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
||||||
console.log(`[API - SERVICES - V1] Received ${req.method} request : ${req.url} at ${new Date().toISOString()}`);
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import express, { Router } from "express";
|
|
||||||
import uploadRouter from "./upload";
|
|
||||||
import { loggingMiddleware } from "@/features/api/middleware";
|
|
||||||
|
|
||||||
const router: Router = express.Router();
|
|
||||||
|
|
||||||
router.use(loggingMiddleware);
|
|
||||||
router.use("/upload", uploadRouter);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import {Backup, DatabaseWith} from "@/db/schema/07_database";
|
|
||||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
|
||||||
import type {StorageInput, StorageResult} from "@/features/storages/types";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
import {eq} from "drizzle-orm";
|
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
|
||||||
import forge from "node-forge";
|
|
||||||
import crypto from "node:crypto";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save stream to a temporary file and upload to all storage providers in parallel.
|
|
||||||
*/
|
|
||||||
export default async function uploadTempFileToProviders(
|
|
||||||
backup: Backup,
|
|
||||||
database: DatabaseWith,
|
|
||||||
// inputStream: NodeJS.ReadableStream,
|
|
||||||
tmpPath: string,
|
|
||||||
fileName: string
|
|
||||||
): Promise<StorageResult[]> {
|
|
||||||
|
|
||||||
const stats = fs.statSync(tmpPath);
|
|
||||||
const fileSize = stats.size;
|
|
||||||
console.log(tmpPath);
|
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.setting.name, "system"),
|
|
||||||
with: {storageChannel: true},
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultPolicy = settings?.storageChannel
|
|
||||||
? [{
|
|
||||||
id: null,
|
|
||||||
storageChannelId: settings.storageChannel.id,
|
|
||||||
enabled: settings.storageChannel.enabled,
|
|
||||||
}]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const enabledPolicies = database.storagePolicies?.filter(p => p.enabled) ?? [];
|
|
||||||
const policies = enabledPolicies.length ? enabledPolicies : defaultPolicy;
|
|
||||||
|
|
||||||
if (!policies.length) {
|
|
||||||
await db.update(drizzleDb.schemas.backup)
|
|
||||||
.set(withUpdatedAt({status: "failed"}))
|
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
|
||||||
fs.existsSync(tmpPath) && fs.unlinkSync(tmpPath);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const storagePath = `backups/${database.project?.slug}/${fileName}`;
|
|
||||||
|
|
||||||
const results = await Promise.all(
|
|
||||||
policies.map(async (policy) => {
|
|
||||||
const fileStream = fs.createReadStream(tmpPath);
|
|
||||||
|
|
||||||
const [backupStorage] = await db.insert(drizzleDb.schemas.backupStorage)
|
|
||||||
.values({
|
|
||||||
backupId: backup.id,
|
|
||||||
storageChannelId: policy.storageChannelId,
|
|
||||||
status: "pending",
|
|
||||||
path: storagePath,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
const input: StorageInput = {
|
|
||||||
action: "upload",
|
|
||||||
data: {path: storagePath, file: fileStream, size: fileSize},
|
|
||||||
};
|
|
||||||
|
|
||||||
let result: StorageResult;
|
|
||||||
|
|
||||||
try {
|
|
||||||
result = policy.id
|
|
||||||
? await dispatchStorage(input, policy.id)
|
|
||||||
: await dispatchStorage(input, undefined, policy.storageChannelId);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err);
|
|
||||||
result = {success: false, provider: null, error: err.message};
|
|
||||||
}
|
|
||||||
await db.update(drizzleDb.schemas.backupStorage)
|
|
||||||
.set(withUpdatedAt({status: result.success ? "success" : "failed"}))
|
|
||||||
.where(eq(drizzleDb.schemas.backupStorage.id, backupStorage.id));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const backupStatus = results.some(r => r.success) ? "success" : "failed";
|
|
||||||
await db.update(drizzleDb.schemas.backup)
|
|
||||||
.set(withUpdatedAt({
|
|
||||||
fileSize: fileSize,
|
|
||||||
status: backupStatus
|
|
||||||
}))
|
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
|
||||||
|
|
||||||
fs.existsSync(tmpPath) && fs.unlinkSync(tmpPath);
|
|
||||||
|
|
||||||
console.log(results);
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
|
||||||
|
|
||||||
if (!backupStatus) {
|
|
||||||
await sendNotificationsBackupRestore(database, "error_backup");
|
|
||||||
}
|
|
||||||
await sendNotificationsBackupRestore(database, "success_backup");
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function createDecryptionStream(
|
|
||||||
encryptedAesKeyHex: string,
|
|
||||||
ivHex: string
|
|
||||||
) {
|
|
||||||
const privateKeyPem = fs.readFileSync(
|
|
||||||
"private/keys/server_private.pem",
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
|
|
||||||
|
|
||||||
const encryptedBytes = forge.util.hexToBytes(encryptedAesKeyHex);
|
|
||||||
const aesKeyBytes = privateKey.decrypt(encryptedBytes, "RSA-OAEP", {
|
|
||||||
md: forge.md.sha256.create(),
|
|
||||||
mgf1: {md: forge.md.sha256.create()},
|
|
||||||
});
|
|
||||||
|
|
||||||
const aesKey = Buffer.from(aesKeyBytes, "binary");
|
|
||||||
const iv = Buffer.from(ivHex, "hex");
|
|
||||||
|
|
||||||
return crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import {promisify} from "util";
|
|
||||||
import {pipeline} from "stream";
|
|
||||||
|
|
||||||
const pipelineAsync = promisify(pipeline);
|
|
||||||
const TMP_DIR = path.join(process.cwd(), "private/uploads/tmp");
|
|
||||||
fs.mkdirSync(TMP_DIR, { recursive: true });
|
|
||||||
|
|
||||||
export async function saveStreamToTempFile(stream: NodeJS.ReadableStream, fileName: string): Promise<string> {
|
|
||||||
const tmpPath = path.join(TMP_DIR, fileName);
|
|
||||||
const writeStream = fs.createWriteStream(tmpPath);
|
|
||||||
await pipelineAsync(stream, writeStream);
|
|
||||||
return tmpPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function getFileExtension(dbType: string) {
|
|
||||||
switch (dbType) {
|
|
||||||
case "postgresql":
|
|
||||||
return ".dump";
|
|
||||||
case "mysql":
|
|
||||||
return ".sql";
|
|
||||||
default:
|
|
||||||
return ".dump";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
import express, {Request, Response, Router} from "express";
|
|
||||||
import {v4 as uuidv4} from "uuid";
|
|
||||||
import {and, eq} from "drizzle-orm";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
|
||||||
import uploadTempFileToProviders, {createDecryptionStream} from "@/features/api/upload/helpers/common";
|
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
|
||||||
import {Backup} from "@/db/schema/07_database";
|
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
|
||||||
import {getFileExtension, saveStreamToTempFile} from "@/features/api/upload/helpers/file";
|
|
||||||
|
|
||||||
const router: Router = express.Router();
|
|
||||||
|
|
||||||
// router.post("/:agentId", async (req: Request, res: Response) => {
|
|
||||||
// try {
|
|
||||||
// const agentId = req.params.agentId as string | undefined;
|
|
||||||
// const generatedId = req.headers["x-generated-id"] as string | undefined;
|
|
||||||
// const status = req.headers["x-status"] as string | undefined;
|
|
||||||
// const encryptedAesKeyHex = req.headers["x-aes-key"] as string | undefined;
|
|
||||||
// const ivHex = req.headers["x-iv"] as string | undefined;
|
|
||||||
// const method = (req.headers["x-method"] as string) ?? "manual";
|
|
||||||
// const extension = req.headers["x-extension"] as string | undefined;
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// if (!generatedId || !encryptedAesKeyHex || !ivHex || !agentId || !status) {
|
|
||||||
// return res.status(400).json({error: "Missing required headers/params"});
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (!isUuidv4(generatedId)) {
|
|
||||||
// return res.status(400).json({error: "generatedId is not a valid UUID"});
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const agent = await db.query.agent.findFirst({
|
|
||||||
// where: eq(drizzleDb.schemas.agent.id, agentId),
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// if (!agent) {
|
|
||||||
// return res.status(404).json({error: "Agent not found"});
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// const database = await db.query.database.findFirst({
|
|
||||||
// where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
|
||||||
// with: {
|
|
||||||
// project: true,
|
|
||||||
// storagePolicies: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// if (!database) {
|
|
||||||
// return res.status(404).json({error: "Database not found"});
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let backup: Backup | null | undefined = null;
|
|
||||||
//
|
|
||||||
// if (method === "automatic") {
|
|
||||||
// [backup] = await db
|
|
||||||
// .insert(drizzleDb.schemas.backup)
|
|
||||||
// .values({
|
|
||||||
// status: "ongoing",
|
|
||||||
// databaseId: database.id,
|
|
||||||
// })
|
|
||||||
// .returning();
|
|
||||||
// } else {
|
|
||||||
// backup = await db.query.backup.findFirst({
|
|
||||||
// where: and(
|
|
||||||
// eq(drizzleDb.schemas.backup.databaseId, database.id),
|
|
||||||
// eq(drizzleDb.schemas.backup.status, "ongoing")
|
|
||||||
// ),
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (!backup) {
|
|
||||||
// return res.status(404).json({error: "Backup not found"});
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// if (status === "success") {
|
|
||||||
//
|
|
||||||
// const decipher = createDecryptionStream(encryptedAesKeyHex, ivHex);
|
|
||||||
//
|
|
||||||
// const fileExt = extension || getFileExtension(database.dbms);
|
|
||||||
// const fileName = `${uuidv4()}${fileExt}`;
|
|
||||||
//
|
|
||||||
// const decryptedStream = req.pipe(decipher);
|
|
||||||
//
|
|
||||||
// const tmpPath = await saveStreamToTempFile(decryptedStream, fileName);
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// if (!tmpPath) {
|
|
||||||
// return res.status(500).json({
|
|
||||||
// error: "Unable to save tmp backup file",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// uploadTempFileToProviders(backup, database, tmpPath, fileName);
|
|
||||||
//
|
|
||||||
// return res.json({success: true});
|
|
||||||
// } else {
|
|
||||||
// await db
|
|
||||||
// .update(drizzleDb.schemas.backup)
|
|
||||||
// .set(withUpdatedAt({status: 'failed'}))
|
|
||||||
// .where(eq(drizzleDb.schemas.backup.id, backup.id));
|
|
||||||
//
|
|
||||||
// eventEmitter.emit('modification', {update: true});
|
|
||||||
//
|
|
||||||
// await sendNotificationsBackupRestore(database, "error_backup");
|
|
||||||
//
|
|
||||||
// return res.status(200).json({
|
|
||||||
// error: "Backup successfully updated with status failed",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// } catch (err: any) {
|
|
||||||
// console.error("Upload error:", err);
|
|
||||||
// return res.status(500).json({
|
|
||||||
// error: "Upload failed",
|
|
||||||
// detail: err.message,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
router.post("/:agentId", async (req: Request, res: Response) => {
|
|
||||||
const agentId = req.params.agentId as string | undefined;
|
|
||||||
const generatedId = req.headers["x-generated-id"] as string | undefined;
|
|
||||||
const status = req.headers["x-status"] as string | undefined;
|
|
||||||
const encryptedAesKeyHex = req.headers["x-aes-key"] as string | undefined;
|
|
||||||
const ivHex = req.headers["x-iv"] as string | undefined;
|
|
||||||
const method = (req.headers["x-method"] as string) ?? "manual";
|
|
||||||
const extension = req.headers["x-extension"] as string | undefined;
|
|
||||||
|
|
||||||
if (!generatedId || !encryptedAesKeyHex || !ivHex || !agentId || !status) {
|
|
||||||
return res.status(400).json({error: "Missing required headers/params"});
|
|
||||||
}
|
|
||||||
if (!isUuidv4(generatedId)) {
|
|
||||||
return res.status(400).json({error: "generatedId is not a valid UUID"});
|
|
||||||
}
|
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({ where: eq(drizzleDb.schemas.agent.id, agentId) });
|
|
||||||
if (!agent) return res.status(404).json({error: "Agent not found"});
|
|
||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
|
||||||
with: { project: true, storagePolicies: true },
|
|
||||||
});
|
|
||||||
if (!database) return res.status(404).json({error: "Database not found"});
|
|
||||||
|
|
||||||
let backup: Backup | null = null;
|
|
||||||
if (method === "automatic") {
|
|
||||||
[backup] = await db.insert(drizzleDb.schemas.backup).values({ status: "ongoing", databaseId: database.id }).returning();
|
|
||||||
} else {
|
|
||||||
backup = await db.query.backup.findFirst({
|
|
||||||
where: and(
|
|
||||||
eq(drizzleDb.schemas.backup.databaseId, database.id),
|
|
||||||
eq(drizzleDb.schemas.backup.status, "ongoing")
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!backup) return res.status(404).json({error: "Backup not found"});
|
|
||||||
|
|
||||||
res.status(202).json({success: true, message: "Backup received, processing in background"});
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
if (status === "success") {
|
|
||||||
const decipher = createDecryptionStream(encryptedAesKeyHex!, ivHex!);
|
|
||||||
const fileExt = extension || getFileExtension(database.dbms);
|
|
||||||
const fileName = `${uuidv4()}${fileExt}`;
|
|
||||||
const decryptedStream = req.pipe(decipher);
|
|
||||||
const tmpPath = await saveStreamToTempFile(decryptedStream, fileName);
|
|
||||||
|
|
||||||
if (!tmpPath) throw new Error("Unable to save tmp backup file");
|
|
||||||
|
|
||||||
await uploadTempFileToProviders(backup!, database, tmpPath, fileName);
|
|
||||||
} else {
|
|
||||||
await db.update(drizzleDb.schemas.backup)
|
|
||||||
.set(withUpdatedAt({status: 'failed'}))
|
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup!.id));
|
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
|
||||||
await sendNotificationsBackupRestore(database, "error_backup");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Background backup processing failed:", err);
|
|
||||||
try {
|
|
||||||
await db.update(drizzleDb.schemas.backup)
|
|
||||||
.set(withUpdatedAt({status: 'failed'}))
|
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup!.id));
|
|
||||||
} catch (_) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
|
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import {cn} from "@/lib/utils";
|
|||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
import {MemberWithUser} from "@/db/schema/03_organization";
|
import {MemberWithUser} from "@/db/schema/03_organization";
|
||||||
import {formatLocalizedDate} from "@/utils/date-formatting";
|
import {formatLocalizedDate} from "@/utils/date-formatting";
|
||||||
import {formatBytes, isImportedFilename} from "@/utils/text";
|
import {formatBytes} from "@/utils/text";
|
||||||
import {DatabaseActionsCell} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions-cell";
|
import {DatabaseActionsCell} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions-cell";
|
||||||
import { Badge as BadgeC } from "@/components/ui/badge";
|
import { Badge as BadgeC } from "@/components/ui/badge";
|
||||||
|
|
||||||
@@ -22,7 +22,17 @@ export function backupColumns(
|
|||||||
{
|
{
|
||||||
id: "availability",
|
id: "availability",
|
||||||
cell: ({row}) => {
|
cell: ({row}) => {
|
||||||
const colorStatus = row.original.deletedAt != null ? "bg-red-400 border-red-600" : "bg-green-400 border-green-600";
|
const statusColors: Record<string, string> = {
|
||||||
|
waiting: "bg-gray-400 border-gray-600",
|
||||||
|
ongoing: "bg-orange-400 border-orange-600",
|
||||||
|
success: "bg-green-400 border-green-600",
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorStatus =
|
||||||
|
row.original.deletedAt != null
|
||||||
|
? "bg-red-400 border-red-600"
|
||||||
|
: statusColors[row.original.status] ?? "bg-gray-400 border-gray-600";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@@ -7,12 +7,6 @@ import * as drizzleDb from "@/db";
|
|||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import {Backup, Restoration} from "@/db/schema/07_database";
|
import {Backup, Restoration} from "@/db/schema/07_database";
|
||||||
import {
|
|
||||||
deleteFileS3Private,
|
|
||||||
deleteLocalPrivate,
|
|
||||||
} from "@/features/upload/private/upload.action";
|
|
||||||
import {env} from "@/env.mjs";
|
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
|
||||||
|
|
||||||
export const deleteRestoreAction = userAction
|
export const deleteRestoreAction = userAction
|
||||||
.schema(
|
.schema(
|
||||||
@@ -48,84 +42,6 @@ export const deleteRestoreAction = userAction
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const deleteBackupAction = userAction
|
|
||||||
.schema(
|
|
||||||
z.object({
|
|
||||||
backupId: z.string(),
|
|
||||||
databaseId: z.string(),
|
|
||||||
projectSlug: z.string(),
|
|
||||||
status: z.enum(["ongoing", "failed", "success", "waiting"]),
|
|
||||||
file: z.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
|
|
||||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
|
||||||
if (!settings) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
actionError: {
|
|
||||||
message: "No settings found.",
|
|
||||||
status: 404,
|
|
||||||
cause: "No settings found.",
|
|
||||||
messageParams: {message: "Error deleting the backup"},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(drizzleDb.schemas.backup)
|
|
||||||
.set(withUpdatedAt({
|
|
||||||
deletedAt: new Date(),
|
|
||||||
status: parsedInput.status == "ongoing" ? "failed" : parsedInput.status
|
|
||||||
}))
|
|
||||||
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
|
||||||
|
|
||||||
let success: boolean, message: string;
|
|
||||||
|
|
||||||
if (parsedInput.file) {
|
|
||||||
const result =
|
|
||||||
settings.storage === "local"
|
|
||||||
? await deleteLocalPrivate(parsedInput.file)
|
|
||||||
: await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
|
|
||||||
|
|
||||||
({success, message} = result);
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
actionError: {
|
|
||||||
message: message,
|
|
||||||
status: 404,
|
|
||||||
cause: "Unable to delete backup from storage",
|
|
||||||
messageParams: {message: "Error deleting the backup"},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
actionSuccess: {
|
|
||||||
message: `Backup deleted successfully (ref: ${parsedInput.backupId}).`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (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"},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export const rerunRestorationAction = userAction
|
export const rerunRestorationAction = userAction
|
||||||
.schema(
|
.schema(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -175,46 +91,3 @@ export const rerunRestorationAction = userAction
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const createRestorationAction = userAction
|
|
||||||
.schema(
|
|
||||||
z.object({
|
|
||||||
backupId: z.string(),
|
|
||||||
databaseId: z.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
|
|
||||||
try {
|
|
||||||
const restorationData = await db
|
|
||||||
.insert(drizzleDb.schemas.restoration)
|
|
||||||
.values({
|
|
||||||
databaseId: parsedInput.databaseId,
|
|
||||||
backupId: parsedInput.backupId,
|
|
||||||
status: "waiting",
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
const createdRestoration = restorationData[0];
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: createdRestoration,
|
|
||||||
actionSuccess: {
|
|
||||||
message: "Restoration has been successfully created.",
|
|
||||||
messageParams: {restorationId: createdRestoration.id},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
actionError: {
|
|
||||||
message: "Failed to create restoration.",
|
|
||||||
status: 500,
|
|
||||||
cause: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
messageParams: {message: "Error creating the restoration"},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
"use server"
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Public server key content
|
* Get Public server key content
|
||||||
*/
|
*/
|
||||||
export function getPublicServerKeyContent() {
|
export async function getPublicServerKeyContent() {
|
||||||
try {
|
try {
|
||||||
return fs.readFileSync("private/keys/server_public.pem", "utf8");
|
const keyPath = path.join(env.PRIVATE_PATH, '/keys/server_public.pem')
|
||||||
|
return fs.readFileSync(keyPath, "utf8");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error :", error);
|
console.error("Error :", error);
|
||||||
return {
|
return {
|
||||||
@@ -20,9 +24,10 @@ export function getPublicServerKeyContent() {
|
|||||||
/**
|
/**
|
||||||
* Get Master server key
|
* Get Master server key
|
||||||
*/
|
*/
|
||||||
export function getMasterServerKeyContent() {
|
export async function getMasterServerKeyContent() {
|
||||||
try {
|
try {
|
||||||
return fs.readFileSync("private/keys/master_key.bin");
|
const keyPath = path.join(env.PRIVATE_PATH, '/keys/master_key.bin')
|
||||||
|
return fs.readFileSync(keyPath);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error :", error);
|
console.error("Error :", error);
|
||||||
return {
|
return {
|
||||||
@@ -31,3 +36,21 @@ export function getMasterServerKeyContent() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function downloadMasterKeyAction() {
|
||||||
|
try {
|
||||||
|
const keyPath = path.join(env.PRIVATE_PATH, "keys/master_key.bin");
|
||||||
|
const fileBuffer = fs.readFileSync(keyPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: fileBuffer.toString("base64"),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Unable to download master key",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import {Organization} from "@/db/schema/03_organization";
|
|||||||
import {ProjectWith} from "@/db/schema/06_project";
|
import {ProjectWith} from "@/db/schema/06_project";
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
|
import {useRouter} from "next/navigation";
|
||||||
|
|
||||||
type ProjectDialogProps = {
|
type ProjectDialogProps = {
|
||||||
databases: DatabaseWith[];
|
databases: DatabaseWith[];
|
||||||
@@ -33,6 +34,7 @@ export const ProjectDialog = ({
|
|||||||
isEmpty = false
|
isEmpty = false
|
||||||
}: ProjectDialogProps) => {
|
}: ProjectDialogProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@@ -60,7 +62,10 @@ export const ProjectDialog = ({
|
|||||||
<DialogTitle>{isEdit ? `Edit ${project?.name}` : "Create new project"}</DialogTitle>
|
<DialogTitle>{isEdit ? `Edit ${project?.name}` : "Create new project"}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<ProjectForm
|
<ProjectForm
|
||||||
onSuccess={() => setOpen(false)}
|
onSuccess={() => {
|
||||||
|
setOpen(false)
|
||||||
|
router.refresh()
|
||||||
|
}}
|
||||||
databases={databases}
|
databases={databases}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
defaultValues={project ? {
|
defaultValues={project ? {
|
||||||
|
|||||||
@@ -22,11 +22,6 @@ export type projectFormProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const ProjectForm = (props: projectFormProps) => {
|
export const ProjectForm = (props: projectFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ export async function dispatchStorage(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channelData) channel = {...channelData, config: channelData.config as Json};
|
if (channelData) {
|
||||||
|
// @ts-ignore
|
||||||
|
channel = {...channelData, config: channelData.config as Json};
|
||||||
|
}
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
return {
|
return {
|
||||||
@@ -94,12 +97,14 @@ export async function dispatchStorage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return await dispatchViaProvider(
|
const dispatchResult = await dispatchViaProvider(
|
||||||
channel.provider as StorageProviderKind,
|
channel.provider as StorageProviderKind,
|
||||||
channel.config,
|
channel.config,
|
||||||
input,
|
input,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(dispatchResult);
|
||||||
|
return dispatchResult;
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, Sto
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import {generateFileUrl} from "@/features/storages/helpers";
|
import {generateFileUrl} from "@/features/storages/helpers";
|
||||||
import {Readable} from "node:stream";
|
import {Readable} from "node:stream";
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
|
||||||
const BASE_DIR = "/private/uploads/";
|
const BASE_DIR = path.join(env.PRIVATE_PATH, '/uploads')
|
||||||
|
|
||||||
export async function uploadLocal(
|
export async function uploadLocal(
|
||||||
config: { baseDir?: string },
|
config: { baseDir?: string },
|
||||||
input: { data: StorageUploadInput; metadata?: StorageMetaData }
|
input: { data: StorageUploadInput; metadata?: StorageMetaData }
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const base = config.baseDir || BASE_DIR;
|
const base = config.baseDir ? path.join(process.cwd(), config.baseDir ?? "") : BASE_DIR;
|
||||||
const fullPath = path.join(process.cwd(), base, input.data.path);
|
|
||||||
|
const fullPath = path.join(base, input.data.path);
|
||||||
const dir = path.dirname(fullPath);
|
const dir = path.dirname(fullPath);
|
||||||
|
|
||||||
await mkdir(dir, { recursive: true });
|
await mkdir(dir, { recursive: true });
|
||||||
@@ -52,8 +54,9 @@ export async function getLocal(
|
|||||||
config: { baseDir?: string },
|
config: { baseDir?: string },
|
||||||
input: { data: StorageGetInput; metadata: StorageMetaData }
|
input: { data: StorageGetInput; metadata: StorageMetaData }
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const base = config.baseDir || BASE_DIR;
|
|
||||||
const filePath = path.join(process.cwd(), base, input.data.path);
|
const base = config.baseDir ? path.join(process.cwd(), config.baseDir ?? "") : BASE_DIR;
|
||||||
|
const filePath = path.join(base, input.data.path);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
console.error("File not found at:", filePath);
|
console.error("File not found at:", filePath);
|
||||||
@@ -108,8 +111,10 @@ export async function deleteLocal(
|
|||||||
config: { baseDir?: string },
|
config: { baseDir?: string },
|
||||||
input: { data: StorageDeleteInput, metadata?: StorageMetaData }
|
input: { data: StorageDeleteInput, metadata?: StorageMetaData }
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const base = config.baseDir || BASE_DIR;
|
|
||||||
const fullPath = path.join(process.cwd(), base, input.data.path);
|
const base = config.baseDir ? path.join(process.cwd(), config.baseDir ?? "") : BASE_DIR;
|
||||||
|
const fullPath = path.join(base, input.data.path);
|
||||||
|
|
||||||
await unlink(fullPath);
|
await unlink(fullPath);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -121,8 +126,8 @@ export async function pingLocal(
|
|||||||
config: { baseDir?: string }
|
config: { baseDir?: string }
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
|
|
||||||
const base = config.baseDir || BASE_DIR;
|
const base = path.join(process.cwd(), config.baseDir ?? "") || BASE_DIR;
|
||||||
const fullPath = path.join(process.cwd(), base, "ping.txt");
|
const fullPath = path.join(base, "ping.txt");
|
||||||
|
|
||||||
await fs.promises.writeFile(fullPath, "ping");
|
await fs.promises.writeFile(fullPath, "ping");
|
||||||
await fs.promises.readFile(fullPath);
|
await fs.promises.readFile(fullPath);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ async function getS3Client(config: S3Config) {
|
|||||||
region: config.region ?? "us-east-1",
|
region: config.region ?? "us-east-1",
|
||||||
accessKey: config.accessKey,
|
accessKey: config.accessKey,
|
||||||
secretKey: config.secretKey,
|
secretKey: config.secretKey,
|
||||||
port: config.port ?? 443,
|
port: config.port ? Number(config.port) : 443,
|
||||||
useSSL: config.ssl ?? true,
|
useSSL: config.ssl ?? true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ export const useUpdateCheck = () => {
|
|||||||
staleTime: 1000 * 60 * 60,
|
staleTime: 1000 * 60 * 60,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("newRelease", newRelease);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!newRelease) return;
|
if (!newRelease) return;
|
||||||
|
|||||||
@@ -1,215 +0,0 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
import {mkdir, writeFile} from "fs/promises";
|
|
||||||
import path from "path";
|
|
||||||
import * as fs from "node:fs";
|
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
|
||||||
import {createPresignedUrlToDownload, deleteFileFromBucket, saveFileInBucket} from "@/utils/s3-file-management";
|
|
||||||
import {env} from "@/env.mjs";
|
|
||||||
import {z} from "zod";
|
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
|
||||||
import {unlink} from "fs/promises";
|
|
||||||
import {action} from "@/lib/safe-actions/actions";
|
|
||||||
|
|
||||||
const privateLocalDir = "private/uploads/files/";
|
|
||||||
const privateS3Dir = "backups/";
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadS3Private(fileName: string, buffer: any, bucketName: string) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
await saveFileInBucket({
|
|
||||||
bucketName,
|
|
||||||
fileName: `${privateS3Dir}${fileName}`,
|
|
||||||
file: buffer,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
filePath: `${privateS3Dir}${fileName}`,
|
|
||||||
message: "File uploaded successfully",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error occurred:", error);
|
|
||||||
throw new Error("An error occurred while importing the private file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export async function deleteFileS3Private(fileName: string, bucketName: string) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
await deleteFileFromBucket({
|
|
||||||
bucketName,
|
|
||||||
fileName: `${privateS3Dir}${fileName}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "File deleted successfully",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error occurred:", error);
|
|
||||||
throw new Error("An error occurred while deleting the private file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a file from local private storage
|
|
||||||
*/
|
|
||||||
export async function deleteLocalPrivate(fileName: string) {
|
|
||||||
try {
|
|
||||||
const filePath = path.join(process.cwd(), privateLocalDir, fileName);
|
|
||||||
await unlink(filePath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `File '${fileName}' deleted successfully`,
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code === "ENOENT") {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: `File '${fileName}' not found`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("Error occurred while deleting file:", error);
|
|
||||||
throw new Error("An error occurred while deleting the file");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// export async function getFileUrlPresignedLocal(fileName: string) {
|
|
||||||
// try {
|
|
||||||
// const filePath = path.join(privateLocalDir, fileName);
|
|
||||||
// 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}`;
|
|
||||||
// }
|
|
||||||
// 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");
|
|
||||||
// return `${baseUrl}/api/files/${fileName}?token=${token}&expires=${expiresAt}`;
|
|
||||||
// } catch (error) {
|
|
||||||
// throw error;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
export async function getFileUrlPresignedS3(fileName: string) {
|
|
||||||
try {
|
|
||||||
return await createPresignedUrlToDownload({
|
|
||||||
bucketName: env.S3_BUCKET_NAME!,
|
|
||||||
fileName: fileName,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const getFileUrlPresignedLocal = action
|
|
||||||
.schema(z.object({
|
|
||||||
dir: z.string().optional(),
|
|
||||||
fileName: z.string()
|
|
||||||
}))
|
|
||||||
.action(async ({parsedInput}): Promise<ServerActionResult<string>> => {
|
|
||||||
try {
|
|
||||||
const filePath = path.join(parsedInput.dir ? parsedInput.dir : privateLocalDir, parsedInput.fileName);
|
|
||||||
await mkdir(path.join(process.cwd(), privateLocalDir), {recursive: true});
|
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
console.error("File not found at:", filePath);
|
|
||||||
throw new Error(`File not found at: ${filePath}`);
|
|
||||||
}
|
|
||||||
const crypto = require("crypto");
|
|
||||||
const baseUrl = getServerUrl();
|
|
||||||
|
|
||||||
const expiresAt = Date.now() + 60 * 1000; // expires in 1 minute
|
|
||||||
const token = crypto.createHash("sha256").update(`${parsedInput.fileName}${expiresAt}`).digest("hex");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: `${baseUrl}/api/files/${parsedInput.fileName}?token=${token}&expires=${expiresAt}`,
|
|
||||||
actionSuccess: {
|
|
||||||
message: "Successfully retrieved presigned URL Local",
|
|
||||||
messageParams: {fileName: parsedInput.fileName},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
actionError: {
|
|
||||||
message: "Failed to generate presigned URL",
|
|
||||||
status: 500,
|
|
||||||
cause: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
messageParams: {fileName: parsedInput.fileName},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export const getFileUrlPreSignedS3Action = action
|
|
||||||
.schema(z.string())
|
|
||||||
.action(async ({parsedInput}): Promise<ServerActionResult<string>> => {
|
|
||||||
try {
|
|
||||||
const data = await createPresignedUrlToDownload({
|
|
||||||
bucketName: env.S3_BUCKET_NAME!,
|
|
||||||
fileName: parsedInput,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
value: data.url,
|
|
||||||
actionSuccess: {
|
|
||||||
message: "Successfully retrieved presigned URL",
|
|
||||||
messageParams: {fileName: parsedInput},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const isNotFound = error instanceof Error && error.message.includes("File does not exist");
|
|
||||||
|
|
||||||
const logContext = {
|
|
||||||
file: parsedInput,
|
|
||||||
reason: error instanceof Error ? error.message : "Unknown",
|
|
||||||
};
|
|
||||||
|
|
||||||
console.error("Presigned URL generation failed:", logContext);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
actionError: {
|
|
||||||
message: isNotFound
|
|
||||||
? "File not found in S3 bucket"
|
|
||||||
: "Failed to generate presigned URL",
|
|
||||||
status: isNotFound ? 404 : 500,
|
|
||||||
cause: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
messageParams: {fileName: parsedInput},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
+15
-2
@@ -192,6 +192,8 @@ export const auth = betterAuth({
|
|||||||
session: {
|
session: {
|
||||||
create: {
|
create: {
|
||||||
before: async (session, context) => {
|
before: async (session, context) => {
|
||||||
|
|
||||||
|
|
||||||
const userId = session.userId;
|
const userId = session.userId;
|
||||||
|
|
||||||
let memberships = await db.query.member.findMany({
|
let memberships = await db.query.member.findMany({
|
||||||
@@ -231,6 +233,9 @@ export const auth = betterAuth({
|
|||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
after: async (session) => {
|
after: async (session) => {
|
||||||
|
|
||||||
|
console.log("session", session);
|
||||||
|
|
||||||
const user = await db.query.user.findFirst({
|
const user = await db.query.user.findFirst({
|
||||||
where: eq(drizzleDb.schemas.user.id, session.userId),
|
where: eq(drizzleDb.schemas.user.id, session.userId),
|
||||||
});
|
});
|
||||||
@@ -244,6 +249,12 @@ export const auth = betterAuth({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lastDiff = user.lastConnectedAt
|
||||||
|
? new Date(session.createdAt).getTime() - new Date(user.lastConnectedAt).getTime()
|
||||||
|
: Infinity;
|
||||||
|
|
||||||
|
if (lastDiff < 30000) return;
|
||||||
|
|
||||||
if (user.role === "pending") return;
|
if (user.role === "pending") return;
|
||||||
|
|
||||||
const deviceInfo = getDeviceDetails(session.userAgent);
|
const deviceInfo = getDeviceDetails(session.userAgent);
|
||||||
@@ -509,7 +520,8 @@ export const checkSlugOrganization = async (slug: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
} catch {}
|
} catch {
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getActiveMember = async () => {
|
export const getActiveMember = async () => {
|
||||||
@@ -532,5 +544,6 @@ export const setActiveOrganization = async (slug: string) => {
|
|||||||
organizationSlug: slug,
|
organizationSlug: slug,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {db} from "@/db";
|
||||||
|
import {and, eq, isNotNull, isNull} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
|
||||||
|
export const backupCleanTask = async () => {
|
||||||
|
try {
|
||||||
|
const backups = await db.query.backup.findMany({
|
||||||
|
where: and(
|
||||||
|
isNotNull(drizzleDb.schemas.backup.deletedAt),
|
||||||
|
eq(drizzleDb.schemas.backup.status, "ongoing")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Backups to clean: ${backups.length}`);
|
||||||
|
|
||||||
|
for (const backup of backups) {
|
||||||
|
await db.update(drizzleDb.schemas.backup).set(withUpdatedAt({
|
||||||
|
status: "failed",
|
||||||
|
}))
|
||||||
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Backup cleanup failed:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -70,6 +70,8 @@ export const deleteBackupCronAction = action
|
|||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set(withUpdatedAt({
|
.set(withUpdatedAt({
|
||||||
deletedAt: new Date(),
|
deletedAt: new Date(),
|
||||||
|
status: backup.status == "ongoing" ? "failed" : backup.status
|
||||||
|
|
||||||
}))
|
}))
|
||||||
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import cron from "node-cron";
|
import cron from "node-cron";
|
||||||
import {retentionCleanTask} from "@/lib/tasks/database";
|
import {retentionCleanTask} from "@/lib/tasks/database";
|
||||||
import {env} from "@/env.mjs";
|
import {env} from "@/env.mjs";
|
||||||
|
import {backupCleanTask} from "@/lib/tasks/cleaning";
|
||||||
|
|
||||||
|
|
||||||
export const retentionJob = cron.schedule(env.RETENTION_CRON, async () => {
|
export const retentionJob = cron.schedule(env.RETENTION_CRON, async () => {
|
||||||
@@ -10,4 +11,13 @@ export const retentionJob = cron.schedule(env.RETENTION_CRON, async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[CRON] Error:`, err);
|
console.error(`[CRON] Error:`, err);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cleaningJob = cron.schedule("* * * * *", async () => {
|
||||||
|
try {
|
||||||
|
console.log("Cleaning Job : Starting task");
|
||||||
|
await backupCleanTask();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[CRON] Error:`, err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@@ -39,3 +39,13 @@ export function buildOrganizationWithMembers(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function getFileExtension(dbType: string) {
|
||||||
|
switch (dbType) {
|
||||||
|
case "postgresql":
|
||||||
|
return ".dump";
|
||||||
|
case "mysql":
|
||||||
|
return ".sql";
|
||||||
|
default:
|
||||||
|
return ".dump";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
import {getMasterServerKeyContent} from "@/features/keys/keys.action";
|
import {getMasterServerKeyContent} from "@/features/keys/keys.action";
|
||||||
|
|
||||||
export async function generateEdgeKey(serverUrl: string, agentId: string): Promise<string> {
|
export async function generateEdgeKey(serverUrl: string, agentId: string): Promise<string> {
|
||||||
const masterKey = getMasterServerKeyContent()
|
const masterKey = await getMasterServerKeyContent()
|
||||||
console.log("Master server key: ", masterKey)
|
|
||||||
const edgeKeyData = {
|
const edgeKeyData = {
|
||||||
serverUrl,
|
serverUrl,
|
||||||
agentId,
|
agentId,
|
||||||
|
|||||||
+8
-1
@@ -2,7 +2,7 @@ import {env} from "@/env.mjs";
|
|||||||
import {db, makeMigration} from "@/db";
|
import {db, makeMigration} from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {retentionJob} from "@/lib/tasks";
|
import {cleaningJob, retentionJob} from "@/lib/tasks";
|
||||||
import {generateRSAKeys, getOrCreateMasterKey} from "@/utils/rsa-keys";
|
import {generateRSAKeys, getOrCreateMasterKey} from "@/utils/rsa-keys";
|
||||||
import {StorageProviderKind} from "@/features/storages/types";
|
import {StorageProviderKind} from "@/features/storages/types";
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ export async function init() {
|
|||||||
await createSettingsIfNotExist()
|
await createSettingsIfNotExist()
|
||||||
console.log("====Initialization completed====");
|
console.log("====Initialization completed====");
|
||||||
await setupCronJobs()
|
await setupCronJobs()
|
||||||
|
await setupCleaningJobs()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupCronJobs() {
|
async function setupCronJobs() {
|
||||||
@@ -25,6 +26,12 @@ async function setupCronJobs() {
|
|||||||
console.log("==== Cron job started ====");
|
console.log("==== Cron job started ====");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setupCleaningJobs() {
|
||||||
|
console.log("==== Setting up Cleaning Jobs ====");
|
||||||
|
cleaningJob.start();
|
||||||
|
console.log("==== Cleaning job started ====");
|
||||||
|
}
|
||||||
|
|
||||||
async function createSettingsIfNotExist() {
|
async function createSettingsIfNotExist() {
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
const systemSettingsValues = {
|
const systemSettingsValues = {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import path from 'path';
|
|||||||
import {generateKeyPair} from 'crypto';
|
import {generateKeyPair} from 'crypto';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import {randomBytes} from 'crypto';
|
import {randomBytes} from 'crypto';
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
|
||||||
const generateKeyPairAsync = promisify(generateKeyPair);
|
const generateKeyPairAsync = promisify(generateKeyPair);
|
||||||
|
|
||||||
@@ -13,9 +14,10 @@ const generateKeyPairAsync = promisify(generateKeyPair);
|
|||||||
* @param {string} [dir] path to directory
|
* @param {string} [dir] path to directory
|
||||||
* @returns {Promise<{privateKeyPath:string, publicKeyPath:string}>}
|
* @returns {Promise<{privateKeyPath:string, publicKeyPath:string}>}
|
||||||
*/
|
*/
|
||||||
export async function generateRSAKeys(dir = path.join(process.cwd(), 'private/keys')) {
|
export async function generateRSAKeys(dir = path.join(env.PRIVATE_PATH, '/keys')) {
|
||||||
await fs.mkdir(dir, {recursive: true});
|
await fs.mkdir(dir, {recursive: true});
|
||||||
|
|
||||||
|
|
||||||
const privateKeyPath = path.join(dir, 'server_private.pem');
|
const privateKeyPath = path.join(dir, 'server_private.pem');
|
||||||
const publicKeyPath = path.join(dir, 'server_public.pem');
|
const publicKeyPath = path.join(dir, 'server_public.pem');
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ export async function generateRSAKeys(dir = path.join(process.cwd(), 'private/ke
|
|||||||
* @param {string} [filePath] Path to store the key
|
* @param {string} [filePath] Path to store the key
|
||||||
* @returns {Promise<Buffer>} The master key
|
* @returns {Promise<Buffer>} The master key
|
||||||
*/
|
*/
|
||||||
export async function getOrCreateMasterKey(filePath = path.join(process.cwd(), 'private/keys', 'master_key.bin')) {
|
export async function getOrCreateMasterKey(filePath = path.join(env.PRIVATE_PATH, '/keys', 'master_key.bin')) {
|
||||||
|
|
||||||
await fs.mkdir(path.dirname(filePath), {recursive: true});
|
await fs.mkdir(path.dirname(filePath), {recursive: true});
|
||||||
|
|
||||||
@@ -61,8 +63,6 @@ export async function getOrCreateMasterKey(filePath = path.join(process.cwd(), '
|
|||||||
|
|
||||||
const key = randomBytes(32); // 256-bit key
|
const key = randomBytes(32); // 256-bit key
|
||||||
|
|
||||||
console.log(key)
|
|
||||||
|
|
||||||
await fs.writeFile(filePath, key, {mode: 0o600});
|
await fs.writeFile(filePath, key, {mode: 0o600});
|
||||||
console.log(`Master key generated at ${filePath}`);
|
console.log(`Master key generated at ${filePath}`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,243 +0,0 @@
|
|||||||
import * as Minio from "minio";
|
|
||||||
import {env} from "@/env.mjs";
|
|
||||||
import internal from "node:stream";
|
|
||||||
import stream from "node:stream";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {eq} from "drizzle-orm";
|
|
||||||
|
|
||||||
|
|
||||||
async function getS3Client() {
|
|
||||||
const settings = await db
|
|
||||||
.select()
|
|
||||||
.from(drizzleDb.schemas.setting)
|
|
||||||
.where(eq(drizzleDb.schemas.setting.name, "system"))
|
|
||||||
.then((res) => res[0]);
|
|
||||||
|
|
||||||
if (!settings) {
|
|
||||||
throw new Error("S3 settings not found in database.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseConfig = {
|
|
||||||
endPoint: settings?.s3EndPointUrl ?? "",
|
|
||||||
accessKey: settings?.s3AccessKeyId ?? "",
|
|
||||||
secretKey: settings?.s3SecretAccessKey ?? "",
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Minio.Client({
|
|
||||||
...baseConfig,
|
|
||||||
port: Number(env.S3_PORT ?? 0),
|
|
||||||
useSSL: env.S3_USE_SSL === "true",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkMinioAlive() {
|
|
||||||
try {
|
|
||||||
console.log("Check MinioAlive");
|
|
||||||
const s3Client = await getS3Client();
|
|
||||||
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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createBucketIfNotExists(bucketName: string) {
|
|
||||||
const s3Client = await getS3Client();
|
|
||||||
|
|
||||||
const bucketExists = await s3Client.bucketExists(bucketName);
|
|
||||||
if (!bucketExists) {
|
|
||||||
console.log(`Creating bucket ${bucketName}`);
|
|
||||||
await s3Client.makeBucket(bucketName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a file from a bucket
|
|
||||||
* @param bucketName name of the bucket
|
|
||||||
* @param fileName name of the file
|
|
||||||
* @returns true if deleted, false if not
|
|
||||||
*/
|
|
||||||
export async function deleteFileFromBucket({
|
|
||||||
bucketName,
|
|
||||||
fileName,
|
|
||||||
}: {
|
|
||||||
bucketName: string;
|
|
||||||
fileName: string;
|
|
||||||
}): Promise<boolean> {
|
|
||||||
const s3Client = await getS3Client();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fileExists = await checkFileExistsInBucket({ bucketName, fileName });
|
|
||||||
|
|
||||||
if (!fileExists) {
|
|
||||||
console.warn(`File not found: ${bucketName}/${fileName}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await s3Client.removeObject(bucketName, fileName);
|
|
||||||
console.log(`Deleted file: ${bucketName}/${fileName}`);
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error deleting file from bucket:", {
|
|
||||||
bucketName,
|
|
||||||
fileName,
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save file in S3 bucket
|
|
||||||
* @param bucketName name of the bucket
|
|
||||||
* @param fileName name of the file
|
|
||||||
* @param file file to save
|
|
||||||
*/
|
|
||||||
export async function saveFileInBucket({bucketName, fileName, file}: {
|
|
||||||
bucketName: string;
|
|
||||||
fileName: string;
|
|
||||||
file: Buffer | internal.Readable
|
|
||||||
}) {
|
|
||||||
await checkMinioAlive();
|
|
||||||
await createBucketIfNotExists(bucketName);
|
|
||||||
const fileExists = await checkFileExistsInBucket({
|
|
||||||
bucketName,
|
|
||||||
fileName,
|
|
||||||
});
|
|
||||||
console.log("File exists:", fileExists);
|
|
||||||
if (fileExists) {
|
|
||||||
throw new Error("File already exists");
|
|
||||||
}
|
|
||||||
const s3Client = await getS3Client();
|
|
||||||
|
|
||||||
return await s3Client.putObject(bucketName, fileName, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export async function getObjectFromClient({
|
|
||||||
bucketName,
|
|
||||||
fileName,
|
|
||||||
}: {
|
|
||||||
bucketName: string;
|
|
||||||
fileName: string;
|
|
||||||
}): Promise<stream.Readable> {
|
|
||||||
const s3 = await getS3Client();
|
|
||||||
return await s3.getObject(bucketName, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function checkFileExistsInBucket({
|
|
||||||
bucketName,
|
|
||||||
fileName,
|
|
||||||
}: {
|
|
||||||
bucketName: string;
|
|
||||||
fileName: string;
|
|
||||||
}): Promise<boolean> {
|
|
||||||
const s3 = await getS3Client();
|
|
||||||
try {
|
|
||||||
const stat = await s3.statObject(bucketName, fileName);
|
|
||||||
return !!stat;
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code === 'NoSuchKey' || error.message?.includes('not found')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate presigned urls for uploading files to S3
|
|
||||||
* @param files files to upload
|
|
||||||
* @returns promise with array of presigned urls
|
|
||||||
*/
|
|
||||||
export async function createPresignedUrlToUpload({
|
|
||||||
bucketName,
|
|
||||||
fileName,
|
|
||||||
expiry = 60 * 60, // 1 hour
|
|
||||||
}: {
|
|
||||||
bucketName: string;
|
|
||||||
fileName: string;
|
|
||||||
expiry?: number;
|
|
||||||
}) {
|
|
||||||
// Create bucket if it doesn't exist
|
|
||||||
await createBucketIfNotExists(bucketName);
|
|
||||||
const s3Client = await getS3Client();
|
|
||||||
|
|
||||||
return await s3Client.presignedPutObject(bucketName, fileName, expiry);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createPublicBucket({bucketName}: { bucketName: string }) {
|
|
||||||
const s3Client = await getS3Client();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const exists = await s3Client.bucketExists(bucketName);
|
|
||||||
if (!exists) {
|
|
||||||
await s3Client.makeBucket(bucketName);
|
|
||||||
console.log(`Bucket ${bucketName} created successfully.`);
|
|
||||||
} else {
|
|
||||||
console.log(`Bucket ${bucketName} already exists.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const policy = {
|
|
||||||
Version: "2012-10-17",
|
|
||||||
Statement: [
|
|
||||||
{
|
|
||||||
Effect: "Allow",
|
|
||||||
Principal: "*",
|
|
||||||
Action: "s3:GetObject",
|
|
||||||
Resource: `arn:aws:s3:::${bucketName}/*`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
await s3Client.setBucketPolicy(bucketName, JSON.stringify(policy));
|
|
||||||
console.log(`Bucket ${bucketName} is now public.`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating bucket:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a presigned URL for downloading a file from a private S3 bucket
|
|
||||||
* @param bucketName name of the bucket
|
|
||||||
* @param fileName name of the file
|
|
||||||
* @param expiry expiry time in seconds (default 1 hour)
|
|
||||||
* @returns presigned download URL
|
|
||||||
*/
|
|
||||||
export async function createPresignedUrlToDownload({
|
|
||||||
bucketName,
|
|
||||||
fileName,
|
|
||||||
expiry = 60 * 60,
|
|
||||||
}: {
|
|
||||||
bucketName: string;
|
|
||||||
fileName: string;
|
|
||||||
expiry?: number;
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
const s3Client = await getS3Client();
|
|
||||||
|
|
||||||
console.debug("Checking if file exists in bucket:", {bucketName, fileName});
|
|
||||||
|
|
||||||
const fileExists = await checkFileExistsInBucket({bucketName, fileName});
|
|
||||||
|
|
||||||
if (!fileExists) {
|
|
||||||
console.warn("File does not exist:", {bucketName, fileName});
|
|
||||||
throw new Error("File does not exist in the bucket.");
|
|
||||||
}
|
|
||||||
const presignedUrl = await s3Client.presignedGetObject(bucketName, fileName, expiry);
|
|
||||||
console.debug("Generated pre signed URL:", presignedUrl);
|
|
||||||
return {url: presignedUrl};
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error("Error in createPreSignedUrlToDownload:", {
|
|
||||||
bucketName,
|
|
||||||
fileName,
|
|
||||||
errorMessage: err?.message,
|
|
||||||
});
|
|
||||||
throw {error: err.message ?? "Unknown error"};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user