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 - web-ui
- agent - agent
license: Apache-2.0 license: Apache-2.0
version: 1.2.7 version: 1.3.0-rc.1
date-released: "2026-02-16" date-released: "2026-02-23"
+2 -2
View File
@@ -33,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>
![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> </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", "name": "portabase",
"version": "1.2.7", "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"
} }
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; 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),
}); });
@@ -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");
@@ -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>
); );
+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, "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
} }
] ]
} }
-1
View File
@@ -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(),
+1 -1
View File
@@ -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"]);
+1 -1
View File
@@ -35,7 +35,7 @@ export const env = createEnv({
.string() .string()
.default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"), .default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"),
PRIVATE_PATH: z.string().optional(), PRIVATE_PATH: z.string(),
}, },
client: { client: {
+21 -2
View File
@@ -1,3 +1,4 @@
"use server"
import fs from "node:fs"; import fs from "node:fs";
import {env} from "@/env.mjs"; import {env} from "@/env.mjs";
import path from "path"; import path from "path";
@@ -6,7 +7,7 @@ import path from "path";
/** /**
* Get Public server key content * Get Public server key content
*/ */
export function getPublicServerKeyContent() { export async function getPublicServerKeyContent() {
try { try {
const keyPath = path.join(env.PRIVATE_PATH, '/keys/server_public.pem') const keyPath = path.join(env.PRIVATE_PATH, '/keys/server_public.pem')
return fs.readFileSync(keyPath, "utf8"); return fs.readFileSync(keyPath, "utf8");
@@ -23,7 +24,7 @@ export function getPublicServerKeyContent() {
/** /**
* Get Master server key * Get Master server key
*/ */
export function getMasterServerKeyContent() { export async function getMasterServerKeyContent() {
try { try {
const keyPath = path.join(env.PRIVATE_PATH, '/keys/master_key.bin') const keyPath = path.join(env.PRIVATE_PATH, '/keys/master_key.bin')
return fs.readFileSync(keyPath); 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.provider as StorageProviderKind,
channel.config, channel.config,
input, input,
); );
console.log(dispatchResult);
return dispatchResult;
} catch (err: any) { } catch (err: any) {
return { return {
+1 -1
View File
@@ -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;
+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 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 () => {
@@ -11,3 +12,12 @@ export const retentionJob = cron.schedule(env.RETENTION_CRON, async () => {
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);
}
});
+1 -2
View File
@@ -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
View File
@@ -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 = {