diff --git a/app/(customer)/dashboard/(admin)/admin/settings/_email/page.tsx b/app/(customer)/dashboard/(admin)/admin/settings/_email/page.tsx
deleted file mode 100644
index a714d815..00000000
--- a/app/(customer)/dashboard/(admin)/admin/settings/_email/page.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
-
- );
-}
diff --git a/app/(customer)/dashboard/(admin)/admin/settings/_storage/page.tsx b/app/(customer)/dashboard/(admin)/admin/settings/_storage/page.tsx
deleted file mode 100644
index 9ab5f5d5..00000000
--- a/app/(customer)/dashboard/(admin)/admin/settings/_storage/page.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/wrappers/dashboard/admin/settings/storage/settings-storage-section.tsx b/src/components/wrappers/dashboard/admin/settings/storage/settings-storage-section.tsx
index c480cdd5..eed23989 100644
--- a/src/components/wrappers/dashboard/admin/settings/storage/settings-storage-section.tsx
+++ b/src/components/wrappers/dashboard/admin/settings/storage/settings-storage-section.tsx
@@ -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 (
@@ -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.
-
+
+
+
+
+ Download instance
+ Master Key
+
+
+
);
diff --git a/src/env.mjs b/src/env.mjs
index 2c00598b..18ee380d 100644
--- a/src/env.mjs
+++ b/src/env.mjs
@@ -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: {
diff --git a/src/features/keys/keys.action.ts b/src/features/keys/keys.action.ts
index 5f935554..d7c047a5 100644
--- a/src/features/keys/keys.action.ts
+++ b/src/features/keys/keys.action.ts
@@ -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",
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/utils/edge_key.ts b/src/utils/edge_key.ts
index fee5ef21..2d9944d6 100644
--- a/src/utils/edge_key.ts
+++ b/src/utils/edge_key.ts
@@ -2,7 +2,7 @@
import {getMasterServerKeyContent} from "@/features/keys/keys.action";
export async function generateEdgeKey(serverUrl: string, agentId: string): Promise
{
- const masterKey = getMasterServerKeyContent()
+ const masterKey = await getMasterServerKeyContent()
const edgeKeyData = {
serverUrl,
agentId,