Merge branch 'main' into dev

This commit is contained in:
Charles GTE
2026-05-15 23:03:57 +02:00
26 changed files with 4819 additions and 1592 deletions
+6
View File
@@ -1,6 +1,12 @@
# Environment
NODE_ENV=development
# Logging
# Controls minimum log level. Options: debug | info | warn | error
# Set to "warn" or "error" in production to suppress health-check ping noise.
# Use "debug" to see all request logs including agent status pings.
LOG_LEVEL=info
# Database
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
+4
View File
@@ -55,3 +55,7 @@ certificates
/playwright/.cache/
/playwright/.auth/
/e2e/local-storage.json
.claude
.codex
/docs
+3 -1
View File
@@ -21,6 +21,8 @@ keywords:
- backups
- postgresql
- mysql
- firebird
- mssql
- mariadb
- devops
- database
@@ -31,5 +33,5 @@ keywords:
- web-ui
- agent
license: Apache-2.0
version: 1.13.0
version: 1.14.1
date-released: '2026-03-02'
+12 -11
View File
@@ -25,6 +25,7 @@
[![MongoDB](https://img.shields.io/badge/-MongoDB-13aa52?logo=mongodb&logoColor=white)](https://www.mongodb.com/)
[![Valkey](https://img.shields.io/badge/Valkey-6284fc?style=flat&logo=Valkey&logoColor=white)](https://valkey.io/)
[![Firebird](https://img.shields.io/badge/Firebird-f55b14?style=flat&logo=Firebird&logoColor=white)](https://firebirdsql.org/)
[![Microsoft SQL Server](https://img.shields.io/badge/Microsoft%20SQL%20Server-CC2927?style=flat&logo=microsoftsqlserver&logoColor=white)](https://www.microsoft.com/en-us/sql-server)
[![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/Portabase/portabase)
[![Open Source](https://img.shields.io/badge/open%20source-❤️-red)](https://github.com/Portabase/portabase)
@@ -65,17 +66,17 @@ You have 4 ways to install Portabase:
## Supported databases
| Engine | Support | Supported Versions | Restore |
|:-------------------|:-----------|:------------------------------|:--------|
| **PostgreSQL** | ✅ Stable | 12, 13, 14, 15, 16, 17 and 18 | Yes |
| **MySQL** | ✅ Stable | 5.7, 8 and 9 | Yes |
| **MariaDB** | ✅ Stable | 10 and 11 | Yes |
| **MongoDB** | ✅ Stable | 4, 5, 6, 7 and 8 | Yes |
| **SQLite** | ✅ Stable | 3.x | Yes |
| **Redis** | ✅ Stable | 2.8+ | No |
| **Valkey** | ✅ Stable | 7.2+ | No |
| **Firebird** | ✅ Stable | 3.0, 4.0, 5.0 | Yes |
| **MSSQL Server** | ❌ Ongoing | - | Yes |
| Engine | Support | Supported Versions | Restore |
|:-------------------|:----------|:------------------------------|:--------|
| **PostgreSQL** | ✅ Stable | 12, 13, 14, 15, 16, 17 and 18 | Yes |
| **MySQL** | ✅ Stable | 5.7, 8 and 9 | Yes |
| **MariaDB** | ✅ Stable | 10 and 11 | Yes |
| **MongoDB** | ✅ Stable | 4, 5, 6, 7 and 8 | Yes |
| **SQLite** | ✅ Stable | 3.x | Yes |
| **Redis** | ✅ Stable | 2.8+ | No |
| **Valkey** | ✅ Stable | 7.2+ | No |
| **Firebird** | ✅ Stable | 3.0, 4.0, 5.0 | Yes |
| **MSSQL Server** | ✅ Stable | 2017, 2019, 2022, Azure SQL | Yes |
See the [Database Servers documentation](https://portabase.io/docs/agent/db) for version-specific backup and restore details.
@@ -1,6 +1,7 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {db} from "@/db";
import {logger} from "@/lib/logger";
import {notFound} from "next/navigation";
import {SettingsTabs} from "@/components/wrappers/dashboard/admin/settings/settings-tabs";
import {desc, isNull} from "drizzle-orm";
@@ -8,13 +9,15 @@ import * as drizzleDb from "@/db";
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
const log = logger.child({module: "dashboard/admin/settings"});
export default async function RoutePage(props: PageParams<{}>) {
const settings = await db.query.setting.findFirst({
where: (fields, {eq}) => eq(fields.name, "system"),
});
console.log(settings)
log.debug({settings}, "Settings loaded");
const storageChannels = await db.query.storageChannel.findMany({
with: {
@@ -33,7 +36,7 @@ export default async function RoutePage(props: PageParams<{}>) {
}) as NotificationChannelWith[]
if (!settings || !storageChannels || !notificationChannels ) {
if (!settings || !storageChannels || !notificationChannels) {
notFound()
}
@@ -45,7 +48,8 @@ export default async function RoutePage(props: PageParams<{}>) {
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
<SettingsTabs storageChannels={storageChannels} notificationChannels={notificationChannels} settings={settings} />
<SettingsTabs storageChannels={storageChannels} notificationChannels={notificationChannels}
settings={settings}/>
</PageContent>
</Page>
);
-4
View File
@@ -9,8 +9,4 @@ export default async function Index() {
redirect(`/dashboard/home`);
}
redirect("/login");
//Do not delete
// return (
// <Home/>
// );
}
+1 -1
View File
@@ -31,7 +31,7 @@ export async function POST(
) {
try {
const agentId = (await params).agentId
log.info(`Agent ID: ${agentId}`)
log.debug(`Agent ID: ${agentId}`)
const body: Body = await request.json();
const lastContact = new Date();
let message: string
+10 -1
View File
@@ -12,7 +12,7 @@ services:
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
interval: 10s
timeout: 5s
retries: 5
@@ -32,5 +32,14 @@ services:
- ./private/uploads/tmp:/data/uploads/tmp
user: "1000:1000"
mailpit:
image: axllent/mailpit:latest
container_name: portabase-mailpit-dev
hostname: mailpit
restart: unless-stopped
ports:
- "8025:8025"
- "1025:1025"
volumes:
postgres-data:
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "portabase",
"version": "1.13.0",
"version": "1.14.1",
"private": true,
"scripts": {
"dev": "next dev --turbopack -p 8887",
@@ -16,8 +16,8 @@
},
"dependencies": {
"@better-auth/core": "1.6.2",
"@better-auth/passkey": "^1.6.2",
"@better-auth/sso": "^1.6.2",
"@better-auth/passkey": "1.6.2",
"@better-auth/sso": "1.6.2",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
@@ -128,7 +128,7 @@
"eslint-plugin-tailwindcss": "^3.18.2",
"framer-motion": "^12.38.0",
"node-pty": "^1.1.0",
"postcss": "^8.5.9",
"postcss": "8.5.10",
"release-it": "^19.2.4",
"tailwindcss": "^4.2.2",
"tsx": "^4.21.0",
@@ -136,5 +136,5 @@
"typescript": "^5.9.3",
"zenstack": "2.14.2"
},
"packageManager": "pnpm@10.33.0"
"packageManager": "pnpm@11.1.2+sha512.415a1cc25974731e75455c1468371be74c5aa5fb7621b50d4056d222451609f11412f23fd602e6169f1e060466641f798597e1be961a10688836a67b16569499"
}
+2038 -1547
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -1,3 +1,17 @@
allowBuilds:
'@prisma/client': true
'@prisma/engines': true
argon2: true
bcrypt: true
cpu-features: true
esbuild: true
node-pty: true
prisma: true
protobufjs: true
sharp: true
ssh2: true
unrs-resolver: true
zenstack: true
ignoredBuiltDependencies:
- node-pty
onlyBuiltDependencies:
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

@@ -1,13 +1,16 @@
"use server";
import {userAction} from "@/lib/safe-actions/actions";
import { logger } from "@/lib/logger";
import {z} from "zod";
import {v4 as uuidv4} from "uuid";
import {ServerActionResult} from "@/types/action-type";
import {and, eq, inArray} from "drizzle-orm";
import {eq, inArray} from "drizzle-orm";
import {db} from "@/db";
import * as drizzleDb from "@/db";
const log = logger.child({ module: "dashboard/delete-project.action" });
export const deleteProjectAction = userAction.schema(z.string()).action(async ({parsedInput}): Promise<ServerActionResult<typeof drizzleDb.schemas.project.$inferSelect>> => {
try {
const uuid = uuidv4();
@@ -51,7 +54,7 @@ export const deleteProjectAction = userAction.schema(z.string()).action(async ({
},
};
} catch (error) {
console.log(error);
log.error({ error }, "Failed to archive project");
return {
success: false,
actionError: {
@@ -0,0 +1 @@
ALTER TYPE "public"."dbms_status" ADD VALUE 'mssql';
File diff suppressed because it is too large Load Diff
+7
View File
@@ -379,6 +379,13 @@
"when": 1777204042208,
"tag": "0053_lyrical_union_jack",
"breakpoints": true
},
{
"idx": 54,
"version": "7",
"when": 1778523056918,
"tag": "0054_hesitant_darkhawk",
"breakpoints": true
}
]
}
+1 -1
View File
@@ -2,7 +2,7 @@ import {pgEnum} from "drizzle-orm/pg-core";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mariadb", "mongodb", "sqlite", "redis", "valkey", "firebird"]);
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mariadb", "mongodb", "sqlite", "redis", "valkey", "firebird", "mssql"]);
export const statusEnum = pgEnum("status", ["waiting", "ongoing", "failed", "success"]);
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
+2
View File
@@ -10,6 +10,7 @@ export const env = createEnv({
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
NODE_ENV: z.enum(["development", "production"]).optional(),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
DATABASE_URL: z.string().url().optional(),
@@ -100,6 +101,7 @@ export const env = createEnv({
},
runtimeEnv: {
NEXT_PUBLIC_PROJECT_VERSION: version || "Unknown Version",
LOG_LEVEL: process.env.LOG_LEVEL,
PROJECT_NAME: process.env.PROJECT_NAME,
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
+4 -1
View File
@@ -11,6 +11,9 @@ import {StorageChannel} from "@/db/schema/12_storage-channel";
import {
StorageChannelFormType
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.schema";
import { logger } from "@/lib/logger";
const log = logger.child({ module: "features/storages/dispatch" });
export async function dispatchStorage(
@@ -103,7 +106,7 @@ export async function dispatchStorage(
input,
);
console.log(dispatchResult);
log.debug({ result: dispatchResult }, "Storage dispatch result");
return dispatchResult;
} catch (err: any) {
+4 -3
View File
@@ -1,5 +1,6 @@
"use server";
import {userAction} from "@/lib/safe-actions/actions";
import {logger} from "@/lib/logger";
import {z} from "zod";
import {v4 as uuidv4} from "uuid";
import {db} from "@/db";
@@ -10,6 +11,7 @@ import {dispatchStorage} from "@/features/storages/dispatch";
import {StorageInput} from "@/features/storages/types";
import sharp from "sharp";
const log = logger.child({module: "features/upload/upload.action"});
export const uploadUserImageAction = userAction.schema(
z.instanceof(FormData)
@@ -27,7 +29,7 @@ export const uploadUserImageAction = userAction.schema(
const isWebp = fileFormat === "webp";
const compressedBuffer = await sharp(buffer)
.resize({ width: 1024 })
.resize({width: 1024})
.toFormat(isPng ? "png" : isWebp ? "webp" : "jpeg", {
quality: 80,
compressionLevel: isPng ? 9 : undefined
@@ -35,7 +37,6 @@ export const uploadUserImageAction = userAction.schema(
.toBuffer();
const settings = await db.query.setting.findFirst({
where: eq(drizzleDb.schemas.setting.name, "system"),
with: {
@@ -69,7 +70,7 @@ export const uploadUserImageAction = userAction.schema(
}
const result = await dispatchStorage(input, undefined, settings.storageChannel.id);
console.log(result);
log.debug({result}, "Upload dispatch result");
if (!result.success) {
return {
success: false,
+6 -6
View File
@@ -22,7 +22,9 @@ import {passkey} from "@better-auth/passkey";
import {getOidcProviders} from "./oidc";
import {APIError} from "better-auth/api";
import {getOAuthProviders} from "./oauth";
import { logger } from "@/lib/logger";
const log = logger.child({ module: "lib/auth" });
const oidcProviders = getOidcProviders();
@@ -438,9 +440,7 @@ export const auth = betterAuth({
new Date(user.createdAt).getTime();
if (createdAtDiff < 5000) {
console.log(
`Skipping new login email for freshly created user ${user.email}`,
);
log.debug(`Skipping new login email for freshly created user ${user.email}`);
return;
}
@@ -471,12 +471,12 @@ export const auth = betterAuth({
),
});
} catch (error) {
console.log("sendEmail failed", {
log.error({
userId: user.id,
details: "Please Check your env variables or system config",
email: user.email,
error,
});
}, "sendEmail failed");
}
(await auth.$context).internalAdapter.updateUser(user.id, {
@@ -784,7 +784,7 @@ export const getActiveMember = async () => {
return member as MemberWithUser;
} catch (e) {
console.log("err", e);
log.error({ error: e }, "Auth error");
}
};
+5 -2
View File
@@ -1,6 +1,9 @@
import pino, { type Logger } from "pino";
import {env} from "@/env.mjs";
const isProd = process.env.NODE_ENV === "production";
const isProd = env.NODE_ENV === "production";
const defaultLevel = isProd ? "info" : "debug";
const level = (env.LOG_LEVEL ?? defaultLevel) as pino.Level;
function getLocalTimestamp() {
const date = new Date();
@@ -19,7 +22,7 @@ function getLocalTimestamp() {
}
export const logger: Logger = pino({
level: isProd ? "info" : "debug",
level,
base: null,
...(isProd
+2 -2
View File
@@ -14,7 +14,7 @@ export const backupCleanTask = async () => {
eq(drizzleDb.schemas.backup.status, "ongoing")
)
});
log.info(`Backups to clean: ${backups.length}`);
log.debug(`Backups to clean: ${backups.length}`);
for (const backup of backups) {
await db.update(drizzleDb.schemas.backup).set(withUpdatedAt({
@@ -24,7 +24,7 @@ export const backupCleanTask = async () => {
}
} catch (e: any) {
log.info({name: "backupCleanTask", error: e},`Backup cleanup failed`);
log.error({name: "backupCleanTask", error: e},`Backup cleanup failed`);
throw e;
}
};
+1 -1
View File
@@ -22,7 +22,7 @@ export const retentionJob = cron.schedule(env.RETENTION_CRON, async () => {
export const cleaningJob = cron.schedule("* * * * *", async () => {
try {
log.info({ job: "cron", action: "start", name: "cleaningJob" }, "Cleaning Job started");
log.debug({ job: "cron", action: "start", name: "cleaningJob" }, "Cleaning Job started");
await backupCleanTask();
} catch (err) {
log.error({ job: "cron", name: "cleaningJob", error: err }, "Cleaning Job Error");
+15 -1
View File
@@ -1,8 +1,22 @@
import { NextRequest, NextResponse } from 'next/server';
const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
type LogLevel = typeof LOG_LEVELS[number];
const raw = (process.env.LOG_LEVEL ?? "info") as LogLevel;
const _idx = LOG_LEVELS.indexOf(raw);
const LEVEL_INDEX = _idx === -1 ? 1 : _idx;
const STATUS_PATH_RE = /\/api\/agent\/[^/]+\/status\/?$/;
export function loggingMiddleware(request: NextRequest) {
if (request.url.includes('/api')) {
console.log(`[API] Received ${request.method} request : ${request.url} at ${new Date()}`);
const isStatusPing = STATUS_PATH_RE.test(request.nextUrl.pathname);
// Status pings are debug-level; all other API calls are info-level.
const requiredIndex = isStatusPing ? 0 : 1;
if (LEVEL_INDEX <= requiredIndex) {
console.log(`[API] Received ${request.method} request : ${request.url} at ${new Date()}`);
}
}
return NextResponse.next();
}
+4
View File
@@ -78,6 +78,10 @@ export function getFileHeadersBasedOnDbms(dbType: string): Record<string, string
return {
"application/octet-stream": [".backup"],
};
case "mssql":
return {
"application/octet-stream": [".bacpac"],
};
default:
throw new Error(`Unsupported database type: ${dbType}`);