mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Merge branch 'main' into dev
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -55,3 +55,7 @@ certificates
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
/e2e/local-storage.json
|
||||
|
||||
.claude
|
||||
.codex
|
||||
/docs
|
||||
|
||||
+3
-1
@@ -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'
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
[](https://www.mongodb.com/)
|
||||
[](https://valkey.io/)
|
||||
[](https://firebirdsql.org/)
|
||||
[](https://www.microsoft.com/en-us/sql-server)
|
||||
|
||||
[](https://github.com/Portabase/portabase)
|
||||
[](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>
|
||||
);
|
||||
|
||||
@@ -9,8 +9,4 @@ export default async function Index() {
|
||||
redirect(`/dashboard/home`);
|
||||
}
|
||||
redirect("/login");
|
||||
//Do not delete
|
||||
// return (
|
||||
// <Home/>
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+2038
-1547
File diff suppressed because it is too large
Load Diff
@@ -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 |
+5
-2
@@ -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
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user