Compare commits

...
17 Commits
Author SHA1 Message Date
charlesgauthereau b9deb3cdf0 chore(release): 1.3.0-rc.1 2026-02-23 21:56:38 +01:00
Charles GTEandGitHub 5dbf293e1e Merge pull request #94 from Portabase/feat/sqlite
feat: add sqlite support
2026-02-23 21:50:37 +01:00
charlesgauthereau 7232f17cb7 feat: add sqlite support, first working version. 2026-02-22 21:46:54 +01:00
charlesgauthereau 5b1c3d0606 chore(release): 1.2.9 2026-02-19 21:13:33 +01:00
Charles GTEandGitHub 8e6ca64fa2 Merge pull request #93 from Portabase/fix/s3-port
fix: type error when user add port for s3 storages.
2026-02-19 21:12:09 +01:00
charlesgauthereau 9116b4d126 fix: type error when user add port for s3 storages. 2026-02-19 21:09:56 +01:00
charlesgauthereau 4ce38d9857 chore(release): 1.2.8 2026-02-19 13:49:16 +01:00
charlesgauthereau 8811c48384 Merge branch 'dev'
# Conflicts:
#	README.md
2026-02-19 13:48:20 +01:00
Killian LarcherandGitHub 078d0d1ee2 Update demo video link and dashboard image 2026-02-19 11:51:08 +01:00
Killian LarcherandGitHub 56d755afa6 Change demo video link and update image
Updated demo link and image in README.
2026-02-19 11:35:41 +01:00
Killian LarcherandGitHub 9e3abb366f Change dashboard image to video link
Updated the dashboard image link to a video.
2026-02-19 11:34:10 +01:00
charlesgauthereau 4ebac1c46f fix: ts-ignore 2026-02-18 19:09:20 +01:00
Charles GTEandGitHub 1476df7a1f Merge pull request #91 from Portabase/feat/manage-master-key
feat(master_key): download master key file
2026-02-18 19:08:17 +01:00
charlesgauthereau 833872603a feat(master_key): download master key file 2026-02-18 19:07:14 +01:00
Charles GTEandGitHub ebe5c63a7b Merge pull request #90 from Portabase/fix/add-backup-cleanup
fix/add-backup-cleanup
2026-02-18 17:49:27 +01:00
charlesgauthereau afd98ae652 chore(release): 1.2.8-rc.1 2026-02-17 08:48:52 +01:00
charlesgauthereau 8f905c8128 fix: stuck backups with ongoing status but deleted, for legacy. 2026-02-17 08:47:57 +01:00
23 changed files with 2477 additions and 82 deletions
+2 -2
View File
@@ -26,5 +26,5 @@ keywords:
- web-ui
- agent
license: Apache-2.0
version: 1.2.7
date-released: "2026-02-16"
version: 1.3.0-rc.1
date-released: "2026-02-23"
+2 -2
View File
@@ -33,14 +33,14 @@
<strong>
<a href="https://portabase.io">Website</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://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>
</strong>
</p>
![portabase-dashboard](https://github.com/user-attachments/assets/f5ec7204-79c0-43ac-ba17-f2ed682c39d6)
![portabase-dashboard](https://github.com/user-attachments/assets/8f2c69d6-f1f9-4b80-b51c-01f6f13b9b62)
</div>
@@ -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>
);
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "portabase",
"version": "1.2.7",
"version": "1.3.0-rc.1",
"private": true,
"scripts": {
"dev": "next dev --turbopack -p 8887",
@@ -127,5 +127,5 @@
"typescript": "^5.9.3",
"zenstack": "2.14.2"
},
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8"
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+2
View File
@@ -208,12 +208,14 @@ type UseZodFormProps<Z extends ZodSchema> = Exclude<
schema: Z;
};
const useZodForm = <Z extends ZodSchema>({
schema,
...formProps
}: UseZodFormProps<Z>) =>
useForm({
...formProps,
// @ts-ignore
resolver: zodResolver(schema),
});
@@ -51,7 +51,8 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
toast.success("Successfully connected to storage channel");
} else {
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 {
toast.error("Not yet supported");
@@ -4,7 +4,15 @@ import {Info} from "lucide-react";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {useRouter} from "next/navigation";
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 {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
import {useMutation} from "@tanstack/react-query";
@@ -18,6 +26,7 @@ import {
} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.action";
import {toast} from "sonner";
import {Switch} from "@/components/ui/switch";
import {downloadMasterKeyAction} from "@/features/keys/keys.action";
export type SettingsStorageSectionProps = {
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 (
<div className="flex flex-col h-full">
<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
configured at the database level. </AlertDescription>
</Alert>
<div className="flex flex-col h-full py-4 ">
<div className="flex flex-col h-full py-4 gap-3">
<Form
className="space-y-4"
form={form}
@@ -96,8 +133,6 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
</FormItem>
)}
/>
<FormField
control={form.control}
name="encryption"
@@ -120,6 +155,18 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
Confirm
</ButtonWithLoading>
</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>
);
+1
View File
@@ -0,0 +1 @@
ALTER TYPE "public"."dbms_status" ADD VALUE 'sqlite';
File diff suppressed because it is too large Load Diff
+7
View File
@@ -239,6 +239,13 @@
"when": 1770923665015,
"tag": "0033_handy_valeria_richards",
"breakpoints": true
},
{
"idx": 34,
"version": "7",
"when": 1771790675240,
"tag": "0034_lush_speed",
"breakpoints": true
}
]
}
-1
View File
@@ -9,7 +9,6 @@ import {timestamps} from "@/db/schema/00_common";
import {AlertPolicy, alertPolicy} from "@/db/schema/10_alert-policy";
import {StoragePolicy, storagePolicy} from "@/db/schema/13_storage-policy";
import {BackupStorage, backupStorage} from "@/db/schema/14_storage-backup";
import {storageChannel} from "@/db/schema/12_storage-channel";
export const database = pgTable("databases", {
id: uuid("id").primaryKey().defaultRandom(),
+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", "mongodb"]);
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mongodb", "sqlite"]);
export const statusEnum = pgEnum("status", ["waiting", "ongoing", "failed", "success"]);
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
+1 -1
View File
@@ -35,7 +35,7 @@ export const env = createEnv({
.string()
.default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"),
PRIVATE_PATH: z.string().optional(),
PRIVATE_PATH: z.string(),
},
client: {
+21 -2
View File
@@ -1,3 +1,4 @@
"use server"
import fs from "node:fs";
import {env} from "@/env.mjs";
import path from "path";
@@ -6,7 +7,7 @@ import path from "path";
/**
* Get Public server key content
*/
export function getPublicServerKeyContent() {
export async function getPublicServerKeyContent() {
try {
const keyPath = path.join(env.PRIVATE_PATH, '/keys/server_public.pem')
return fs.readFileSync(keyPath, "utf8");
@@ -23,7 +24,7 @@ export function getPublicServerKeyContent() {
/**
* Get Master server key
*/
export function getMasterServerKeyContent() {
export async function getMasterServerKeyContent() {
try {
const keyPath = path.join(env.PRIVATE_PATH, '/keys/master_key.bin')
return fs.readFileSync(keyPath);
@@ -35,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",
};
}
}
+3 -1
View File
@@ -97,12 +97,14 @@ export async function dispatchStorage(
}
return await dispatchViaProvider(
const dispatchResult = await dispatchViaProvider(
channel.provider as StorageProviderKind,
channel.config,
input,
);
console.log(dispatchResult);
return dispatchResult;
} catch (err: any) {
return {
+1 -1
View File
@@ -18,7 +18,7 @@ async function getS3Client(config: S3Config) {
region: config.region ?? "us-east-1",
accessKey: config.accessKey,
secretKey: config.secretKey,
port: config.port ?? 443,
port: config.port ? Number(config.port) : 443,
useSSL: config.ssl ?? true,
});
}
@@ -19,8 +19,6 @@ export const useUpdateCheck = () => {
staleTime: 1000 * 60 * 60,
});
console.log("newRelease", newRelease);
useEffect(() => {
if (!newRelease) return;
+28
View File
@@ -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;
}
};
+10
View File
@@ -1,6 +1,7 @@
import cron from "node-cron";
import {retentionCleanTask} from "@/lib/tasks/database";
import {env} from "@/env.mjs";
import {backupCleanTask} from "@/lib/tasks/cleaning";
export const retentionJob = cron.schedule(env.RETENTION_CRON, async () => {
@@ -10,4 +11,13 @@ export const retentionJob = cron.schedule(env.RETENTION_CRON, async () => {
} catch (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);
}
});
+1 -2
View File
@@ -2,8 +2,7 @@
import {getMasterServerKeyContent} from "@/features/keys/keys.action";
export async function generateEdgeKey(serverUrl: string, agentId: string): Promise<string> {
const masterKey = getMasterServerKeyContent()
console.log("Master server key: ", masterKey)
const masterKey = await getMasterServerKeyContent()
const edgeKeyData = {
serverUrl,
agentId,
+8 -1
View File
@@ -2,7 +2,7 @@ import {env} from "@/env.mjs";
import {db, makeMigration} from "@/db";
import {eq} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {retentionJob} from "@/lib/tasks";
import {cleaningJob, retentionJob} from "@/lib/tasks";
import {generateRSAKeys, getOrCreateMasterKey} from "@/utils/rsa-keys";
import {StorageProviderKind} from "@/features/storages/types";
@@ -17,6 +17,7 @@ export async function init() {
await createSettingsIfNotExist()
console.log("====Initialization completed====");
await setupCronJobs()
await setupCleaningJobs()
}
async function setupCronJobs() {
@@ -25,6 +26,12 @@ async function setupCronJobs() {
console.log("==== Cron job started ====");
}
async function setupCleaningJobs() {
console.log("==== Setting up Cleaning Jobs ====");
cleaningJob.start();
console.log("==== Cleaning job started ====");
}
async function createSettingsIfNotExist() {
await db.transaction(async (tx) => {
const systemSettingsValues = {