mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
34
Commits
1.2.6
...
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 |
+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.6
|
version: 1.3.0-rc.1
|
||||||
date-released: "2026-02-15"
|
date-released: "2026-02-23"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ services:
|
|||||||
# context: .
|
# context: .
|
||||||
# dockerfile: docker/dockerfile/Dockerfile
|
# dockerfile: docker/dockerfile/Dockerfile
|
||||||
# target: prod
|
# target: prod
|
||||||
image: portabase/portabase:latest
|
image: portabase/portabase:1.2.7-rc.1
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.2.6",
|
"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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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"]);
|
||||||
|
|
||||||
|
|||||||
+2
-16
@@ -15,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(),
|
||||||
@@ -29,13 +29,6 @@ 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
|
||||||
@@ -69,13 +62,6 @@ 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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user