diff --git a/.env.example b/.env.example index e84dcd89..ed6286c3 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,10 @@ AUTH_OIDC_JWKS_ENDPOINT="" AUTH_OIDC_PKCE=true ALLOWED_GROUP="admin" +# If true, the user's role will be updated from OIDC claims on every login. +# If false, the role is only set when the user is first created. +AUTH_SYNC_OIDC_ROLES_ON_LOGIN=true + AUTH_EMAIL_PASSWORD_ENABLED=true AUTH_SIGNUP_ENABLED=true AUTH_PASSKEY_ENABLED=true diff --git a/CITATION.cff b/CITATION.cff index ea9ad667..b15a7ccb 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -26,5 +26,5 @@ keywords: - web-ui - agent license: Apache-2.0 -version: 1.2.8-rc.1 -date-released: "2026-02-17" +version: 1.2.9 +date-released: "2026-02-19" diff --git a/README.md b/README.md index 75cf8dd3..6d955ede 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,14 @@ WebsiteDocumentation • - Demo • + DemoInstallationReport BugRequest Feature

-![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) 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 ( - - -
- System email -
-
- - - -
- ); -} 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 ( - - -
- System storage -
-
- - - -
- ); -} diff --git a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx index 4c0af23d..73cea61c 100644 --- a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx +++ b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx @@ -35,8 +35,6 @@ export default async function RoutePage( const edgeKey = await generateEdgeKey(getServerUrl(), agent.id); - console.log("edgeKey", edgeKey); - return (
diff --git a/docker-compose.yml b/docker-compose.yml index 4280f4bf..d81d77d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,7 @@ services: - "localhost:host-gateway" volumes: - ./private/uploads/tmp:/data/uploads/tmp + user: "1000:1000" keycloak: image: quay.io/keycloak/keycloak:latest diff --git a/package.json b/package.json index 85d503f4..e9dc5516 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "portabase", - "version": "1.2.8-rc.1", + "version": "1.2.9", "private": true, "scripts": { "dev": "next dev --turbopack -p 8887", diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index 5f25b739..6e2fbf99 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -208,12 +208,14 @@ type UseZodFormProps = Exclude< schema: Z; }; + const useZodForm = ({ schema, ...formProps }: UseZodFormProps) => useForm({ ...formProps, + // @ts-ignore resolver: zodResolver(schema), }); diff --git a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx index bb4c495d..5c4fdf00 100644 --- a/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx +++ b/src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-test-button.tsx @@ -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"); 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 + +
); diff --git a/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx index bf7eac2e..4c7f4c92 100644 --- a/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx +++ b/src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx @@ -1,14 +1,6 @@ "use client"; -import { Backup, BackupWith, Restoration } from "@/db/schema/07_database"; -import { Swiper, SwiperSlide } from "swiper/react"; -//@ts-ignore -import "swiper/css"; -import "swiper/css/pagination"; -import { Pagination, Mousewheel } from "swiper/modules"; -import { - DatabaseActionKind, - useBackupModal, -} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; import { Form, FormControl, @@ -18,33 +10,36 @@ import { FormMessage, useZodForm, } from "@/components/ui/form"; -import { - BackupActionsSchema, - BackupActionsType, -} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.schema"; -import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { BackupStorageWith } from "@/db/schema/14_storage-backup"; import { TooltipProvider } from "@/components/ui/tooltip"; +import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading"; import { getChannelIcon } from "@/components/wrappers/dashboard/admin/channels/helpers/common"; -import { Badge } from "@/components/ui/badge"; import { getStatusColor, getStatusIcon, } from "@/components/wrappers/dashboard/admin/notifications/logs/columns"; -import { useRouter } from "next/navigation"; import { createRestorationBackupAction, deleteBackupAction, deleteBackupStorageAction, downloadBackupAction, } from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.action"; -import { toast } from "sonner"; -import { SafeActionResult } from "next-safe-action"; +import { + BackupActionsSchema, + BackupActionsType, +} from "@/components/wrappers/dashboard/database/backup/actions/backup-actions.schema"; +import { + DatabaseActionKind, + useBackupModal, +} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; +import { Backup, BackupWith, Restoration } from "@/db/schema/07_database"; +import { BackupStorageWith } from "@/db/schema/14_storage-backup"; import { ServerActionResult } from "@/types/action-type"; -import { ZodString } from "zod"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { AlertCircleIcon } from "lucide-react"; +import { SafeActionResult } from "next-safe-action"; +import { useRouter } from "next/navigation"; +import { toast } from "sonner"; +import { ZodString } from "zod"; type BackupActionsFormProps = { backup: BackupWith; @@ -161,7 +156,7 @@ export const BackupActionsForm = ({
{ await mutation.mutateAsync(values); }} @@ -174,94 +169,82 @@ export const BackupActionsForm = ({ Choose a storage backup -
- - {filteredBackupStorages.map( - (storage: BackupStorageWith) => ( - - - - ), - ) ??

No storages available

} -
+ {storage.storageChannel.provider} + + )} + + + {getStatusIcon(storage.status === "success")} + + {storage.status.toUpperCase()} + + +
+ + ), + ) ??

No storages available

}
diff --git a/src/env.mjs b/src/env.mjs index dd11dd15..b95aa26e 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -58,6 +58,8 @@ export const env = createEnv({ AUTH_SIGNUP_ENABLED: z.string().optional().default("true"), AUTH_PASSKEY_ENABLED: z.string().optional().default("true"), + AUTH_SYNC_OIDC_ROLES_ON_LOGIN: z.enum(["true", "false"]).default("true"), + PRIVATE_PATH: z.string().optional(), }, client: { @@ -107,6 +109,8 @@ export const env = createEnv({ AUTH_SIGNUP_ENABLED: process.env.AUTH_SIGNUP_ENABLED, AUTH_PASSKEY_ENABLED: process.env.AUTH_PASSKEY_ENABLED, + AUTH_SYNC_OIDC_ROLES_ON_LOGIN: process.env.AUTH_SYNC_OIDC_ROLES_ON_LOGIN, + PRIVATE_PATH: process.env.PRIVATE_PATH || path.join(process.cwd(), "private"), }, 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/features/storages/dispatch.ts b/src/features/storages/dispatch.ts index 32b87fc8..d13fef6c 100644 --- a/src/features/storages/dispatch.ts +++ b/src/features/storages/dispatch.ts @@ -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 { diff --git a/src/features/storages/providers/s3.ts b/src/features/storages/providers/s3.ts index f03f3372..6bdc8b29 100644 --- a/src/features/storages/providers/s3.ts +++ b/src/features/storages/providers/s3.ts @@ -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, }); } diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts index a45cda34..339afd8d 100644 --- a/src/lib/auth/auth.ts +++ b/src/lib/auth/auth.ts @@ -161,6 +161,18 @@ export const auth = betterAuth({ issuer: p.issuerUrl, })), provisionUser: async ({ user: usr, userInfo, provider }) => { + const existingUser = await db.query.user.findFirst({ + where: eq(drizzleDb.schemas.user.email, usr.email), + }); + + if (existingUser && existingUser.role === "superadmin") { + return; + } + + if (existingUser && env.AUTH_SYNC_OIDC_ROLES_ON_LOGIN === "false") { + return; + } + const providerId = provider.providerId; const oidcProvider = oidcProviders.find((p) => p.id === providerId); const allowedGroup = oidcProvider?.allowedGroup || env.ALLOWED_GROUP; @@ -225,10 +237,6 @@ export const auth = betterAuth({ roleToAssign = "superadmin"; } - const existingUser = await db.query.user.findFirst({ - where: eq(drizzleDb.schemas.user.email, usr.email), - }); - if (existingUser) { await db .update(drizzleDb.schemas.user) 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,