From 496be692e70aac489cfaa6975d16292b183036cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20LAGACHE?= Date: Mon, 23 Feb 2026 12:04:48 +0100 Subject: [PATCH] fix --- .../(admin)/agents/[agentId]/page.tsx | 116 +- docker-compose.yml | 98 +- .../common/profile/profile-modal.tsx | 3 +- .../backup/actions/backup-actions-form.tsx | 516 ++-- .../dashboard/profile/profile-providers.tsx | 12 +- .../dashboard/profile/profile-security.tsx | 744 ++--- src/db/migrations/0036_left_longshot.sql | 1 + src/db/migrations/meta/0036_snapshot.json | 2429 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + src/db/schema/02_user.ts | 2 +- src/env.mjs | 166 +- src/lib/auth/auth.ts | 1166 ++++---- src/lib/auth/config.ts | 127 +- src/lib/auth/oidc.ts | 93 + test-dev/.env | 6 - test-dev/databases.json | 14 - test-dev/docker-compose.yml | 42 - 17 files changed, 4196 insertions(+), 1346 deletions(-) create mode 100644 src/db/migrations/0036_left_longshot.sql create mode 100644 src/db/migrations/meta/0036_snapshot.json create mode 100644 src/lib/auth/oidc.ts delete mode 100644 test-dev/.env delete mode 100644 test-dev/databases.json delete mode 100644 test-dev/docker-compose.yml diff --git a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx index 29dcd6e2..4c0af23d 100644 --- a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx +++ b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx @@ -1,61 +1,71 @@ -import {PageParams} from "@/types/next"; -import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page"; -import {db} from "@/db"; +import { PageParams } from "@/types/next"; +import { + Page, + PageContent, + PageDescription, + PageTitle, +} from "@/features/layout/page"; +import { db } from "@/db"; import * as drizzleDb from "@/db"; -import {eq} from "drizzle-orm"; -import {notFound} from "next/navigation"; -import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent"; -import {capitalizeFirstLetter} from "@/utils/text"; -import {generateEdgeKey} from "@/utils/edge_key"; -import {getServerUrl} from "@/utils/get-server-url"; -import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content"; -import {AgentDialog} from "@/features/agents/components/agent.dialog"; -import {AgentType} from "@/features/agents/agents.schema"; +import { eq } from "drizzle-orm"; +import { notFound } from "next/navigation"; +import { ButtonDeleteAgent } from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent"; +import { capitalizeFirstLetter } from "@/utils/text"; +import { generateEdgeKey } from "@/utils/edge_key"; +import { getServerUrl } from "@/utils/get-server-url"; +import { AgentContentPage } from "@/components/wrappers/dashboard/agent/agent-content"; +import { AgentDialog } from "@/features/agents/components/agent.dialog"; +import { AgentType } from "@/features/agents/agents.schema"; -export default async function RoutePage(props: PageParams<{ agentId: string }>) { +export default async function RoutePage( + props: PageParams<{ agentId: string }>, +) { + const { agentId } = await props.params; - const {agentId} = await props.params + const agent = await db.query.agent.findFirst({ + where: eq(drizzleDb.schemas.agent.id, agentId), + with: { + databases: true, + }, + }); - const agent = await db.query.agent.findFirst({ - where: eq(drizzleDb.schemas.agent.id, agentId), - with: { - databases: true - } - }) + if (!agent) { + notFound(); + } - if (!agent) { - notFound() - } + const edgeKey = await generateEdgeKey(getServerUrl(), agent.id); - const edgeKey = await generateEdgeKey(getServerUrl(), agent.id); - - return ( - -
- -
- {capitalizeFirstLetter(agent.name)} -
-
-
- -
-
- -
-
-
+ console.log("edgeKey", edgeKey); + + return ( + +
+ +
+ {capitalizeFirstLetter(agent.name)} +
+
+
+
+
+ +
+
+
+
- {agent.description && ( - {agent.description} - )} - - - -
- ) -} \ No newline at end of file + {agent.description && ( + + {agent.description} + + )} + + + + + ); +} diff --git a/docker-compose.yml b/docker-compose.yml index e762cbf7..4280f4bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,47 +1,63 @@ name: portabase-dev services: - db: - image: postgres:17-alpine - ports: - - "5433:5432" - volumes: - - postgres-data:/var/lib/postgresql/data - environment: - - POSTGRES_DB=devdb - - POSTGRES_USER=devuser - - POSTGRES_PASSWORD=changeme - healthcheck: - test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"] - interval: 10s - timeout: 5s - retries: 5 + db: + image: postgres:17-alpine + ports: + - "5433:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=devdb + - POSTGRES_USER=devuser + - POSTGRES_PASSWORD=changeme + healthcheck: + test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"] + interval: 10s + timeout: 5s + retries: 5 - tusd: - image: tusproject/tusd:v2.8.0 - ports: - - "1080:8080" - command: > - -upload-dir /data/uploads/tmp - -hooks-http http://localhost:8887/api/tus/hooks - -max-size 21474836480 - -base-path /tus/files/ - extra_hosts: - - "localhost:host-gateway" - volumes: - - ./private/uploads/tmp:/data/uploads/tmp - - keycloak: - image: quay.io/keycloak/keycloak:latest - command: start-dev - environment: - KC_BOOTSTRAP_ADMIN_USERNAME: admin - KC_BOOTSTRAP_ADMIN_PASSWORD: admin - ports: - - "8080:8080" - volumes: - - keycloak-data:/opt/keycloak/data + tusd: + image: tusproject/tusd:v2.8.0 + ports: + - "1080:8080" + command: > + -upload-dir /data/uploads/tmp + -hooks-http http://localhost:8887/api/tus/hooks + -max-size 21474836480 + -base-path /tus/files/ + extra_hosts: + - "localhost:host-gateway" + volumes: + - ./private/uploads/tmp:/data/uploads/tmp + keycloak: + image: quay.io/keycloak/keycloak:latest + command: start-dev + environment: + KC_BOOTSTRAP_ADMIN_USERNAME: admin + KC_BOOTSTRAP_ADMIN_PASSWORD: admin + ports: + - "8080:8080" + volumes: + - keycloak-data:/opt/keycloak/data + pocket-id: + image: ghcr.io/pocket-id/pocket-id + restart: unless-stopped + environment: + - APP_URL=http://localhost:3055 + - ENCRYPTION_KEY=QwHyjbZvSsDUAcjpdmSPsuYxaH6vET6OeBaeLwXccCb43L6Om3W1AoU5pKIJTzYr + ports: + - 3055:1411 + volumes: + - pocket-id-data:/app/data + healthcheck: + test: "curl -f http://localhost:1411/healthz" + interval: 1m30s + timeout: 5s + retries: 2 + start_period: 10s volumes: - postgres-data: - keycloak-data: + postgres-data: + keycloak-data: + pocket-id-data: diff --git a/src/components/wrappers/dashboard/common/profile/profile-modal.tsx b/src/components/wrappers/dashboard/common/profile/profile-modal.tsx index 27684d22..b5470b4e 100644 --- a/src/components/wrappers/dashboard/common/profile/profile-modal.tsx +++ b/src/components/wrappers/dashboard/common/profile/profile-modal.tsx @@ -3,7 +3,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import { ProfileSidebar } from "./profile-sidebar"; -import { AuthProviderConfig } from "@/lib/auth/config"; +import type { AuthProviderConfig } from "@/lib/auth/config"; import { User, Session, Account } from "@/db/schema/02_user"; import { ProfileGeneral } from "../../profile/profile-general"; import { ProfileSecurity } from "../../profile/profile-security"; @@ -45,6 +45,7 @@ export const ProfileModal = ({ user, sessions, currentSession, accounts, open, o credentialAccount={accounts.find((acc) => acc.providerId === "credential")!} isPasswordEnabled={providers.some((p) => p.id === "credential")} isPasskeyEnabled={providers.some((p) => p.id === "passkey")} + providers={providers} /> 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 c4d0a340..bf7eac2e 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,253 +1,311 @@ -"use client" -import {Backup, BackupWith, Restoration} from "@/db/schema/07_database"; -import {Swiper, SwiperSlide} from "swiper/react"; +"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 {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form"; +import { Pagination, Mousewheel } from "swiper/modules"; import { - BackupActionsSchema, - BackupActionsType + DatabaseActionKind, + useBackupModal, +} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + 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 {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 { 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 { getChannelIcon } from "@/components/wrappers/dashboard/admin/channels/helpers/common"; +import { Badge } from "@/components/ui/badge"; import { - createRestorationBackupAction, deleteBackupAction, deleteBackupStorageAction, - downloadBackupAction + 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 {ServerActionResult} from "@/types/action-type"; -import {ZodString} from "zod"; -import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert"; -import {AlertCircleIcon} from "lucide-react"; +import { toast } from "sonner"; +import { SafeActionResult } from "next-safe-action"; +import { ServerActionResult } from "@/types/action-type"; +import { ZodString } from "zod"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { AlertCircleIcon } from "lucide-react"; type BackupActionsFormProps = { - backup: BackupWith; - action: DatabaseActionKind; -} + backup: BackupWith; + action: DatabaseActionKind; +}; -export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => { +export const BackupActionsForm = ({ + backup, + action, +}: BackupActionsFormProps) => { + const filteredBackupStorages = + backup.storages?.filter((storage) => storage.deletedAt === null) ?? []; + const { closeModal } = useBackupModal(); + const queryClient = useQueryClient(); + const router = useRouter(); - const filteredBackupStorages = backup.storages?.filter((storage) => storage.deletedAt === null) ?? [] - const {closeModal} = useBackupModal(); - const queryClient = useQueryClient(); - const router = useRouter(); + const form = useZodForm({ + schema: BackupActionsSchema, + }); - const form = useZodForm({ - schema: BackupActionsSchema, - }); + const mutation = useMutation({ + mutationFn: async (values: BackupActionsType) => { + let result: + | SafeActionResult< + string, + ZodString, + readonly [], + { + _errors?: string[] | undefined; + }, + readonly [], + ServerActionResult, + object + > + | undefined; - const mutation = useMutation({ - mutationFn: async (values: BackupActionsType) => { + if (action === "download") { + result = await downloadBackupAction({ + backupStorageId: values.backupStorageId, + }); + } else if (action === "restore") { + result = await createRestorationBackupAction({ + databaseId: backup.databaseId, + backupStorageId: values.backupStorageId, + backupId: backup.id, + }); + } else if (action === "delete") { + result = await deleteBackupStorageAction({ + databaseId: backup.databaseId, + backupStorageId: values.backupStorageId, + backupId: backup.id, + }); + } - let result: SafeActionResult, object> | undefined + const inner = result?.data; - if (action === "download") { - result = await downloadBackupAction({backupStorageId: values.backupStorageId}) - } else if (action === "restore") { - result = await createRestorationBackupAction({ - databaseId: backup.databaseId, - backupStorageId: values.backupStorageId, - backupId: backup.id - }) - } else if (action === "delete") { - result = await deleteBackupStorageAction({ - databaseId: backup.databaseId, - backupStorageId: values.backupStorageId, - backupId: backup.id, - }) - } + if (inner?.success) { + toast.success(inner.actionSuccess?.message); + queryClient.invalidateQueries({ + queryKey: ["database-data", backup.databaseId], + }); + router.refresh(); + if (action === "download") { + const url = inner.value; + if (typeof url === "string") { + window.open(url, "_self"); + } + closeModal(); + } else if (action === "restore") { + closeModal(); + } else if (action === "delete") { + closeModal(); + } else { + closeModal(); + } + } else { + if (action === "delete") { + toast.success("Backup deleted successfully."); + queryClient.invalidateQueries({ + queryKey: ["database-data", backup.databaseId], + }); + router.refresh(); + closeModal(); + } else { + toast.error(inner?.actionError?.message ?? "An error occurred."); + } + } + }, + }); - const inner = result?.data; + const mutationDeleteEntireBackup = useMutation({ + mutationFn: async () => { + const result = await deleteBackupAction({ + databaseId: backup.databaseId, + backupId: backup.id, + }); - if (inner?.success) { - toast.success(inner.actionSuccess?.message); - queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]}); - router.refresh(); - if (action === "download") { - const url = inner.value - if (typeof url === "string") { - window.open(url, "_self"); - } - closeModal() - } else if (action === "restore") { - closeModal() - } else if (action === "delete") { - closeModal() - } else { - closeModal() - } - } else { - if (action === "delete") { - toast.success("Backup deleted successfully.") - queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]}); - router.refresh(); - closeModal() - } else { - toast.error(inner?.actionError?.message ?? "An error occurred."); - } - } - }, - }); + const inner = result?.data; - const mutationDeleteEntireBackup = useMutation({ - mutationFn: async () => { + if (inner?.success) { + toast.success(inner.actionSuccess?.message); + queryClient.invalidateQueries({ + queryKey: ["database-data", backup.databaseId], + }); + router.refresh(); + closeModal(); + } else { + toast.error(inner?.actionError?.message); + } + }, + }); - const result = await deleteBackupAction({ - databaseId: backup.databaseId, - backupId: backup.id, - }) - - const inner = result?.data; - - if (inner?.success) { - toast.success(inner.actionSuccess?.message); - queryClient.invalidateQueries({queryKey: ["database-data", backup.databaseId]}); - router.refresh(); - closeModal() - } else { - toast.error(inner?.actionError?.message); - } - }, - }); - - return ( - - - -
{ - await mutation.mutateAsync(values); - }} - > - - {filteredBackupStorages.length > 0 ? - ( - - Choose a storage backup - - -
- - {filteredBackupStorages.map((storage: BackupStorageWith) => ( - - - - )) ??

No storages available

} -
+ > +
+ {field.value === storage.id && ( +
+ )} +
+
+
+
+
+
+
+ {getChannelIcon( + storage.storageChannel?.provider || + "", + )} +
+

+ {storage.storageChannel?.name} +

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

No storages available

} + +
+ + + + )} + /> + ) : ( + + + Backup does not have files + +

+ You can safely delete the entire backup; no files seem to be + related. Maybe an error occurred. +

+
+
+ )} - : - - - Backup does not have files - -

You can safely delete the entire backup; no files seem to be related. Maybe an error - occurred.

-
-
- } +
+ {action === "delete" && ( + mutationDeleteEntireBackup.mutateAsync()} + isPending={mutationDeleteEntireBackup.isPending} + disabled={mutationDeleteEntireBackup.isPending} + > + Delete entire backup + + )} -
- {action === "delete" && ( - mutationDeleteEntireBackup.mutateAsync()} - isPending={mutationDeleteEntireBackup.isPending} - disabled={mutationDeleteEntireBackup.isPending} - > - Delete entire backup - - )} - - {filteredBackupStorages.length > 0 && ( - - Confirm - - - )} -
- - - ); -} + {filteredBackupStorages.length > 0 && ( + + Confirm + + )} +
+ + + ); +}; diff --git a/src/components/wrappers/dashboard/profile/profile-providers.tsx b/src/components/wrappers/dashboard/profile/profile-providers.tsx index 5d7e1034..52bf03af 100644 --- a/src/components/wrappers/dashboard/profile/profile-providers.tsx +++ b/src/components/wrappers/dashboard/profile/profile-providers.tsx @@ -13,7 +13,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { SetPasswordProfileProviderModal } from "./modal/set-password-modal"; import { Icon } from "@iconify/react"; import Image from "next/image"; -import { AuthProviderConfig } from "@/lib/auth/config"; +import type { AuthProviderConfig } from "@/lib/auth/config"; import { Account } from "@/db/schema/02_user"; interface ProfileProviderProps { @@ -122,16 +122,16 @@ export function ProfileProviders({ accounts, providers }: ProfileProviderProps) variant="outline" size="sm" onClick={() => unlinkAccount(provider.id)} - disabled={!canUnlink || isLoading || provider.isManual} - className={!canUnlink ? "opacity-50 cursor-not-allowed" : ""} + disabled={!canUnlink || isLoading || provider.isManual || provider.allowUnlinking === false} + className={!canUnlink || provider.allowUnlinking === false ? "opacity-50 cursor-not-allowed" : ""} > {isLoading ? : "Unlink"} - {!canUnlink && ( + {(!canUnlink || provider.allowUnlinking === false) && ( -

You cannot unlink your last authentication provider.

+

{provider.allowUnlinking === false ? "Unlinking is disabled for this provider." : "You cannot unlink your last authentication provider."}

)} @@ -141,7 +141,7 @@ export function ProfileProviders({ accounts, providers }: ProfileProviderProps) {provider.id === "credential" ? ( ) : ( - )} diff --git a/src/components/wrappers/dashboard/profile/profile-security.tsx b/src/components/wrappers/dashboard/profile/profile-security.tsx index a3639246..de535d54 100644 --- a/src/components/wrappers/dashboard/profile/profile-security.tsx +++ b/src/components/wrappers/dashboard/profile/profile-security.tsx @@ -4,10 +4,22 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { Badge } from "@/components/ui/badge"; -import { Globe, LogOut, Loader2, Fingerprint, Trash2, Plus } from "lucide-react"; +import { + Globe, + LogOut, + Loader2, + Fingerprint, + Trash2, + Plus, +} from "lucide-react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { toast } from "sonner"; -import { revokeAllSessionsAction, revokeSessionAction, getPasskeysAction, revokePasskeyAction } from "./actions/security.action"; +import { + revokeAllSessionsAction, + revokeSessionAction, + getPasskeysAction, + revokePasskeyAction, +} from "./actions/security.action"; import { useRouter } from "next/navigation"; import { ResetPasswordProfileProviderModal } from "./modal/reset-password-modal"; import { SetPasswordProfileProviderModal } from "./modal/set-password-modal"; @@ -17,335 +29,459 @@ import { ViewBackupCodesModal } from "./modal/view-backup-codes-modal"; import { getDeviceDetails } from "@/utils/detection"; import { timeAgo } from "@/utils/date-formatting"; import { authClient } from "@/lib/auth/auth-client"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Account, Session, User } from "@/db/schema/02_user"; +import { Icon } from "@iconify/react"; +import Image from "next/image"; +import type { AuthProviderConfig } from "@/lib/auth/config"; interface ProfileSecurityProps { - user: User; - sessions: Session[]; - credentialAccount: Account; - currentSession: Session; - isPasswordEnabled?: boolean; - isPasskeyEnabled?: boolean; + user: User; + sessions: Session[]; + credentialAccount: Account; + currentSession: Session; + isPasswordEnabled?: boolean; + isPasskeyEnabled?: boolean; + providers: AuthProviderConfig[]; } -export function ProfileSecurity({ user, sessions, credentialAccount, currentSession, isPasswordEnabled = false, isPasskeyEnabled = false }: ProfileSecurityProps) { - const router = useRouter(); +export function ProfileSecurity({ + user, + sessions, + credentialAccount, + currentSession, + isPasswordEnabled = false, + isPasskeyEnabled = false, + providers, +}: ProfileSecurityProps) { + const router = useRouter(); - const [isBackupCodesDialogOpen, setIsBackupCodesDialogOpen] = useState(false); - const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false); - const [isSetup2FADialogOpen, setIsSetup2FADialogOpen] = useState(false); - const [isDisable2FADialogOpen, setIsDisable2FADialogOpen] = useState(false); - const [isAddPasskeyOpen, setIsAddPasskeyOpen] = useState(false); - const [passkeyName, setPasskeyName] = useState(""); + const [isBackupCodesDialogOpen, setIsBackupCodesDialogOpen] = useState(false); + const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false); + const [isSetup2FADialogOpen, setIsSetup2FADialogOpen] = useState(false); + const [isDisable2FADialogOpen, setIsDisable2FADialogOpen] = useState(false); + const [isAddPasskeyOpen, setIsAddPasskeyOpen] = useState(false); + const [passkeyName, setPasskeyName] = useState(""); - const { mutate: revokeSession, isPending: isRevoking } = useMutation({ - mutationFn: async (token: string) => { - const result = await revokeSessionAction({ token }); - const inner = result?.data; - if (inner?.success) { - toast.success("Session successfully revoked"); - router.refresh(); - } else { - toast.error("An error occurred while revoking session"); - } - }, - }); + const { mutate: revokeSession, isPending: isRevoking } = useMutation({ + mutationFn: async (token: string) => { + const result = await revokeSessionAction({ token }); + const inner = result?.data; + if (inner?.success) { + toast.success("Session successfully revoked"); + router.refresh(); + } else { + toast.error("An error occurred while revoking session"); + } + }, + }); - const { mutate: revokeOthers, isPending: isRevokingOthers } = useMutation({ - mutationFn: async () => { - const result = await revokeAllSessionsAction(); - const inner = result?.data; - if (inner?.success) { - toast.success("Revoking all sessions successfully done."); - router.refresh(); - } else { - toast.error("An error occurred while revoking all sessions"); - } - }, - }); + const { mutate: revokeOthers, isPending: isRevokingOthers } = useMutation({ + mutationFn: async () => { + const result = await revokeAllSessionsAction(); + const inner = result?.data; + if (inner?.success) { + toast.success("Revoking all sessions successfully done."); + router.refresh(); + } else { + toast.error("An error occurred while revoking all sessions"); + } + }, + }); - const { - data: passkeys, - isLoading: isLoadingPasskeys, - refetch: refetchPasskeys, - } = useQuery({ - queryKey: ["passkeys"], - queryFn: async () => { - const result = await getPasskeysAction(); - if (result?.data?.success) { - return result.data.value; - } - throw new Error("Failed to fetch passkeys"); - }, - }); + const { + data: passkeys, + isLoading: isLoadingPasskeys, + refetch: refetchPasskeys, + } = useQuery({ + queryKey: ["passkeys"], + queryFn: async () => { + const result = await getPasskeysAction(); + if (result?.data?.success) { + return result.data.value; + } + throw new Error("Failed to fetch passkeys"); + }, + }); - const { mutate: revokePasskey, isPending: isRevokingPasskey } = useMutation({ - mutationFn: async (id: string) => { - const result = await revokePasskeyAction({ id }); - if (!result?.data?.success) { - throw new Error("Failed to revoke passkey"); - } - }, - onSuccess: () => { - toast.success("Passkey revoked successfully"); - refetchPasskeys(); - }, - onError: () => { - toast.error("Failed to revoke passkey"); - }, - }); + const { mutate: revokePasskey, isPending: isRevokingPasskey } = useMutation({ + mutationFn: async (id: string) => { + const result = await revokePasskeyAction({ id }); + if (!result?.data?.success) { + throw new Error("Failed to revoke passkey"); + } + }, + onSuccess: () => { + toast.success("Passkey revoked successfully"); + refetchPasskeys(); + }, + onError: () => { + toast.error("Failed to revoke passkey"); + }, + }); - const { mutate: addPasskey, isPending: isAddingPasskey } = useMutation({ - mutationFn: async () => { - const result = await authClient.passkey.addPasskey({ - name: passkeyName || "My Passkey", - }); - if (result?.error) { - throw result.error; - } - return result; - }, - onSuccess: () => { - toast.success("Passkey added successfully"); - setIsAddPasskeyOpen(false); - setPasskeyName(""); - refetchPasskeys(); - }, - onError: (error: any) => { - toast.error(error.message || "Failed to add passkey"); - }, - }); + const { mutate: addPasskey, isPending: isAddingPasskey } = useMutation({ + mutationFn: async () => { + const result = await authClient.passkey.addPasskey({ + name: passkeyName || "My Passkey", + }); + if (result?.error) { + throw result.error; + } + return result; + }, + onSuccess: () => { + toast.success("Passkey added successfully"); + setIsAddPasskeyOpen(false); + setPasskeyName(""); + refetchPasskeys(); + }, + onError: (error: any) => { + toast.error(error.message || "Failed to add passkey"); + }, + }); - return ( -
-
-

Security Settings

-

Manage your password, two-factor authentication and sessions.

+ return ( +
+
+

+ Security Settings +

+

+ Manage your password, two-factor authentication and sessions. +

+
+ +
+

Authentication

+
+ {isPasswordEnabled && ( + <> +
+
+
Password
+
+ {user.lastChangedPasswordAt + ? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}` + : "Never changed"} +
+
+ {credentialAccount ? ( + + ) : ( + + )} +
+ + + + )} + +
+
+
+
Two-Factor Authentication
+ {user.twoFactorEnabled && ( + + Active + + )} +
+
+ Enhance the security of your account by requiring a second form + of verification during login. +
-
-

Authentication

-
- {isPasswordEnabled && ( - <> -
-
-
Password
-
- {user.lastChangedPasswordAt ? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}` : "Never changed"} -
-
- {credentialAccount ? ( - - ) : ( - - )} -
- - - - )} - -
-
-
-
Two-Factor Authentication
- {user.twoFactorEnabled && ( - - Active - - )} -
-
- Enhance the security of your account by requiring a second form of verification during login. -
-
- - {user.twoFactorEnabled ? ( -
- - -
- ) : ( - - )} -
-
-
- - {isPasskeyEnabled && ( -
-
-
-

Passkeys

-
Login securely with your fingerprint, face recognition, or hardware key.
-
- - - - - - - - Add New Passkey - Create a name for your passkey to identify it later. - -
-
- - setPasskeyName(e.target.value)} - /> -
-
- - - - -
-
-
- -
- {isLoadingPasskeys ? ( -
- -
- ) : passkeys && passkeys.length > 0 ? ( - passkeys.map((pk: any) => revokePasskey(id)} isRevoking={isRevokingPasskey} />) - ) : ( -
No passkeys found.
- )} -
-
+ {user.twoFactorEnabled ? ( +
+ + +
+ ) : ( + )} - -
-
-

Active Sessions

- {sessions && sessions.length > 1 && ( - - )} -
-
- {sessions && sessions.length > 0 ? ( - sessions?.map((session) => ( - revokeSession(token)} - isRevoking={isRevoking} - currentSession={currentSession} - /> - )) - ) : ( -
No active sessions found.
- )} -
-
+
- ); +
+ + {isPasskeyEnabled && ( +
+
+
+

Passkeys

+
+ Login securely with your fingerprint, face recognition, or + hardware key. +
+
+ + + + + + + + Add New Passkey + + Create a name for your passkey to identify it later. + + +
+
+ + setPasskeyName(e.target.value)} + /> +
+
+ + + + +
+
+
+ +
+ {isLoadingPasskeys ? ( +
+ +
+ ) : passkeys && passkeys.length > 0 ? ( + passkeys.map((pk: any) => ( + revokePasskey(id)} + isRevoking={isRevokingPasskey} + /> + )) + ) : ( +
+ No passkeys found. +
+ )} +
+
+ )} + +
+
+

Active Sessions

+ {sessions && sessions.length > 1 && ( + + )} +
+
+ {sessions && sessions.length > 0 ? ( + sessions?.map((session) => ( + revokeSession(token)} + isRevoking={isRevoking} + currentSession={currentSession} + providers={providers} + /> + )) + ) : ( +
+ No active sessions found. +
+ )} +
+
+
+ ); } function SessionRow({ - session, - onRevoke, - isRevoking, - currentSession, + session, + onRevoke, + isRevoking, + currentSession, + providers, }: { - session: Session; - onRevoke: (token: string) => void; - isRevoking: boolean; - currentSession: Session; + session: Session; + onRevoke: (token: string) => void; + isRevoking: boolean; + currentSession: Session; + providers: AuthProviderConfig[]; }) { - const deviceInfo = getDeviceDetails(session.userAgent); + const deviceInfo = getDeviceDetails(session.userAgent); + const provider = providers.find((p) => p.id === (session as any).providerId); - return ( -
-
-
- -
-
-
- {deviceInfo.os} • {deviceInfo.browser} - {session.id === currentSession.id && ( - - This device - - )} -
-
- {session.ipAddress} • - {session.id === currentSession.id ? "Active now" : `Last active ${timeAgo(new Date(session.createdAt))}`} -
-
-
- - {session.id !== currentSession.id && ( - + return ( +
+
+
+ + {provider && ( +
+ {provider.icon.startsWith("/") || provider.icon.startsWith("http") ? ( + {provider.id} + ) : ( + + )} +
+ )} +
+
+
+ {deviceInfo.os}{" "} + + • {deviceInfo.browser} + + {provider && ( + + • {provider.title || provider.name} + )} + {session.id === currentSession.id && ( + + This device + + )} +
+
+ {session.ipAddress} • + + {session.id === currentSession.id + ? "Active now" + : `Last active ${timeAgo(new Date(session.createdAt))}`} + +
- ); +
+ + {session.id !== currentSession.id && ( + + )} +
+ ); } -function PasskeyRow({ passkey, onRevoke, isRevoking }: { passkey: any; onRevoke: (id: string) => void; isRevoking: boolean }) { - return ( -
-
-
- -
-
-
{passkey.name || "Unnamed Passkey"}
-
Created {timeAgo(new Date(passkey.createdAt))}
-
-
- +function PasskeyRow({ + passkey, + onRevoke, + isRevoking, +}: { + passkey: any; + onRevoke: (id: string) => void; + isRevoking: boolean; +}) { + return ( +
+
+
+
- ); +
+
+ {passkey.name || "Unnamed Passkey"} +
+
+ Created {timeAgo(new Date(passkey.createdAt))} +
+
+
+ +
+ ); } diff --git a/src/db/migrations/0036_left_longshot.sql b/src/db/migrations/0036_left_longshot.sql new file mode 100644 index 00000000..cd14752d --- /dev/null +++ b/src/db/migrations/0036_left_longshot.sql @@ -0,0 +1 @@ +ALTER TABLE "session" ADD COLUMN "provider_id" text; \ No newline at end of file diff --git a/src/db/migrations/meta/0036_snapshot.json b/src/db/migrations/meta/0036_snapshot.json new file mode 100644 index 00000000..3c1ac9cb --- /dev/null +++ b/src/db/migrations/meta/0036_snapshot.json @@ -0,0 +1,2429 @@ +{ + "id": "2b6fc923-dae8-44a4-8cd5-116cd0fa7244", + "prevId": "fc5d3d01-2097-4498-b5b0-525eb5f8417c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "smtp_password": { + "name": "smtp_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_from": { + "name": "smtp_from", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_host": { + "name": "smtp_host", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_port": { + "name": "smtp_port", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_user": { + "name": "smtp_user", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_secure": { + "name": "smtp_secure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "default_storage_channel_id": { + "name": "default_storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "encryption": { + "name": "encryption", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "settings_default_storage_channel_id_storage_channel_id_fk": { + "name": "settings_default_storage_channel_id_storage_channel_id_fk", + "tableFrom": "settings", + "tableTo": "storage_channel", + "columnsFrom": [ + "default_storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_name_unique": { + "name": "settings_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_i_d": { + "name": "credential_i_d", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "user_themes", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'light'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastConnectedAt": { + "name": "lastConnectedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastChangedPasswordAt": { + "name": "lastChangedPasswordAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_slug_unique": { + "name": "projects_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backups": { + "name": "backups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "file": { + "name": "file", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imported": { + "name": "imported", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backups_database_id_databases_id_fk": { + "name": "backups_database_id_databases_id_fk", + "tableFrom": "backups", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.databases": { + "name": "databases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_database_id": { + "name": "agent_database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dbms": { + "name": "dbms", + "type": "dbms_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backup_policy": { + "name": "backup_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_waiting_for_backup": { + "name": "is_waiting_for_backup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backup_to_restore": { + "name": "backup_to_restore", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_agent_id_agents_id_fk": { + "name": "databases_agent_id_agents_id_fk", + "tableFrom": "databases", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restorations": { + "name": "restorations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "backup_storage_id": { + "name": "backup_storage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "restorations_backup_storage_id_backup_storage_id_fk": { + "name": "restorations_backup_storage_id_backup_storage_id_fk", + "tableFrom": "restorations", + "tableTo": "backup_storage", + "columnsFrom": [ + "backup_storage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_backup_id_backups_id_fk": { + "name": "restorations_backup_id_backups_id_fk", + "tableFrom": "restorations", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_database_id_databases_id_fk": { + "name": "restorations_database_id_databases_id_fk", + "tableFrom": "restorations", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.retention_policies": { + "name": "retention_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "retention_policy_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "days": { + "name": "days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "gfs_daily": { + "name": "gfs_daily", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "gfs_weekly": { + "name": "gfs_weekly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 4 + }, + "gfs_monthly": { + "name": "gfs_monthly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 12 + }, + "gfs_yearly": { + "name": "gfs_yearly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "retention_policies_database_id_databases_id_fk": { + "name": "retention_policies_database_id_databases_id_fk", + "tableFrom": "retention_policies", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agents_slug_unique": { + "name": "agents_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channel": { + "name": "notification_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "provider_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channel_organization_id_organization_id_fk": { + "name": "notification_channel_organization_id_organization_id_fk", + "tableFrom": "notification_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_notification_channels": { + "name": "organization_notification_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_notification_channels_organization_id_organization_id_fk": { + "name": "organization_notification_channels_organization_id_organization_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_notification_channels_notification_channel_id_notification_channel_id_fk": { + "name": "organization_notification_channels_notification_channel_id_notification_channel_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_notification_channels_organization_id_notification_channel_id_unique": { + "name": "organization_notification_channels_organization_id_notification_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "notification_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_policy": { + "name": "alert_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_kind": { + "name": "event_kind", + "type": "event_kind[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_policy_notification_channel_id_notification_channel_id_fk": { + "name": "alert_policy_notification_channel_id_notification_channel_id_fk", + "tableFrom": "alert_policy", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_policy_database_id_databases_id_fk": { + "name": "alert_policy_database_id_databases_id_fk", + "tableFrom": "alert_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_log": { + "name": "notification_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel_id": { + "name": "channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_name": { + "name": "provider_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_response": { + "name": "provider_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_storage_channels": { + "name": "organization_storage_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_storage_channels_organization_id_organization_id_fk": { + "name": "organization_storage_channels_organization_id_organization_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_storage_channels_storage_channel_id_storage_channel_id_fk": { + "name": "organization_storage_channels_storage_channel_id_storage_channel_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_storage_channels_organization_id_storage_channel_id_unique": { + "name": "organization_storage_channels_organization_id_storage_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "storage_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_channel": { + "name": "storage_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "provider_storage_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "storage_channel_organization_id_organization_id_fk": { + "name": "storage_channel_organization_id_organization_id_fk", + "tableFrom": "storage_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_policy": { + "name": "storage_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "storage_policy_storage_channel_id_storage_channel_id_fk": { + "name": "storage_policy_storage_channel_id_storage_channel_id_fk", + "tableFrom": "storage_policy", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "storage_policy_database_id_databases_id_fk": { + "name": "storage_policy_database_id_databases_id_fk", + "tableFrom": "storage_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup_storage": { + "name": "backup_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "backup_storage_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_storage_backup_id_backups_id_fk": { + "name": "backup_storage_backup_id_backups_id_fk", + "tableFrom": "backup_storage", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_storage_storage_channel_id_storage_channel_id_fk": { + "name": "backup_storage_storage_channel_id_storage_channel_id_fk", + "tableFrom": "backup_storage", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.user_themes": { + "name": "user_themes", + "schema": "public", + "values": [ + "light", + "dark", + "system" + ] + }, + "public.retention_policy_type": { + "name": "retention_policy_type", + "schema": "public", + "values": [ + "count", + "days", + "gfs" + ] + }, + "public.provider_kind": { + "name": "provider_kind", + "schema": "public", + "values": [ + "slack", + "smtp", + "discord", + "telegram", + "gotify", + "ntfy", + "webhook" + ] + }, + "public.event_kind": { + "name": "event_kind", + "schema": "public", + "values": [ + "error_backup", + "error_restore", + "success_restore", + "success_backup", + "weekly_report" + ] + }, + "public.level": { + "name": "level", + "schema": "public", + "values": [ + "critical", + "warning", + "info" + ] + }, + "public.provider_storage_kind": { + "name": "provider_storage_kind", + "schema": "public", + "values": [ + "local", + "s3", + "google-drive" + ] + }, + "public.backup_storage_status": { + "name": "backup_storage_status", + "schema": "public", + "values": [ + "pending", + "success", + "failed" + ] + }, + "public.dbms_status": { + "name": "dbms_status", + "schema": "public", + "values": [ + "postgresql", + "mysql", + "mongodb" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "waiting", + "ongoing", + "failed", + "success" + ] + }, + "public.type_storage": { + "name": "type_storage", + "schema": "public", + "values": [ + "local", + "s3" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index d8a843c4..dfca0234 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -253,6 +253,13 @@ "when": 1770993283219, "tag": "0035_windy_shockwave", "breakpoints": true + }, + { + "idx": 36, + "version": "7", + "when": 1771842940506, + "tag": "0036_left_longshot", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/02_user.ts b/src/db/schema/02_user.ts index f16f300a..b012eb94 100644 --- a/src/db/schema/02_user.ts +++ b/src/db/schema/02_user.ts @@ -38,10 +38,10 @@ export const session = pgTable("session", { userId: uuid("user_id") .notNull() .references(() => user.id, {onDelete: "cascade"}), + providerId: text("provider_id"), impersonatedBy: text("impersonated_by"), //id or name ???? activeOrganizationId: text("active_organization_id"), ...timestamps - }); export const account = pgTable("account", { diff --git a/src/env.mjs b/src/env.mjs index 7cd1bef3..dd11dd15 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -1,107 +1,113 @@ import { createEnv } from "@t3-oss/env-nextjs"; +import path from "path"; import { z } from "zod"; import packageJson from "../package.json" with { type: "json" }; -import path from "path"; const { version } = packageJson; export const env = createEnv({ - server: { - NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), + server: { + NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), - NODE_ENV: z.enum(["development", "production"]).optional(), + NODE_ENV: z.enum(["development", "production"]).optional(), - DATABASE_URL: z.string().url().optional(), + DATABASE_URL: z.string().url().optional(), - PROJECT_NAME: z.string().optional(), - PROJECT_DESCRIPTION: z.string().optional(), - PROJECT_URL: z.string().regex(/^https?:\/\//, "URL must start with http:// or https://"), - PROJECT_SECRET: z.string(), + PROJECT_NAME: z.string().optional(), + PROJECT_DESCRIPTION: z.string().optional(), + PROJECT_URL: z + .string() + .regex(/^https?:\/\//, "URL must start with http:// or https://"), + PROJECT_SECRET: z.string(), - SMTP_PASSWORD: z.string().optional(), - SMTP_FROM: z.string().optional(), - SMTP_HOST: z.string().optional(), - SMTP_PORT: z.string().optional(), - SMTP_USER: z.string().optional(), - SMTP_SECURE: z.coerce.boolean().default(true), + SMTP_PASSWORD: z.string().optional(), + SMTP_FROM: z.string().optional(), + SMTP_HOST: z.string().optional(), + SMTP_PORT: z.string().optional(), + SMTP_USER: z.string().optional(), + SMTP_SECURE: z.coerce.boolean().default(true), - AUTH_GOOGLE_ID: z.string().optional(), - AUTH_GOOGLE_SECRET: z.string().optional(), - AUTH_GOOGLE_METHOD: z.boolean().default(false), + AUTH_GOOGLE_ID: z.string().optional(), + AUTH_GOOGLE_SECRET: z.string().optional(), + AUTH_GOOGLE_METHOD: z.boolean().default(false), - AUTH_GITHUB_ID: z.string().optional(), - AUTH_GITHUB_SECRET: z.string().optional(), + AUTH_GITHUB_ID: z.string().optional(), + AUTH_GITHUB_SECRET: z.string().optional(), - RETENTION_CRON: z.string().default(process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *"), + RETENTION_CRON: z + .string() + .default( + process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *", + ), - AUTH_OIDC_ID: z.string().optional().default("oidc"), - AUTH_OIDC_TITLE: z.string().optional(), - AUTH_OIDC_DESC: z.string().optional(), - AUTH_OIDC_ICON: z.string().optional(), - AUTH_OIDC_CLIENT: z.string().optional(), - AUTH_OIDC_SECRET: z.string().optional(), - AUTH_OIDC_ISSUER_URL: z.string().optional(), - AUTH_OIDC_HOST: z.string().optional(), - AUTH_OIDC_SCOPES: z.string().optional(), - AUTH_OIDC_DISCOVERY_ENDPOINT: z.string().optional(), - AUTH_OIDC_JWKS_ENDPOINT: z.string().optional(), - AUTH_OIDC_PKCE: z.string().optional(), - ALLOWED_GROUP: z.string().optional(), + AUTH_OIDC_ID: z.string().optional().default("oidc"), + AUTH_OIDC_TITLE: z.string().optional(), + AUTH_OIDC_DESC: z.string().optional(), + AUTH_OIDC_ICON: z.string().optional(), + AUTH_OIDC_CLIENT: z.string().optional(), + AUTH_OIDC_SECRET: z.string().optional(), + AUTH_OIDC_ISSUER_URL: z.string().optional(), + AUTH_OIDC_HOST: z.string().optional(), + AUTH_OIDC_SCOPES: z.string().optional(), + AUTH_OIDC_DISCOVERY_ENDPOINT: z.string().optional(), + AUTH_OIDC_JWKS_ENDPOINT: z.string().optional(), + AUTH_OIDC_PKCE: z.string().optional(), + ALLOWED_GROUP: z.string().optional(), - AUTH_EMAIL_PASSWORD_ENABLED: z.string().optional().default("true"), - AUTH_SIGNUP_ENABLED: z.string().optional().default("true"), - AUTH_PASSKEY_ENABLED: z.string().optional().default("true"), + AUTH_EMAIL_PASSWORD_ENABLED: z.string().optional().default("true"), + AUTH_SIGNUP_ENABLED: z.string().optional().default("true"), + AUTH_PASSKEY_ENABLED: z.string().optional().default("true"), - PRIVATE_PATH: z.string().optional(), + PRIVATE_PATH: z.string().optional(), + }, + client: { + NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), + }, + runtimeEnv: { + NEXT_PUBLIC_PROJECT_VERSION: version || "Unknown Version", - }, - client: { - NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), - }, - runtimeEnv: { - NEXT_PUBLIC_PROJECT_VERSION: version || "Unknown Version", + PROJECT_NAME: process.env.PROJECT_NAME, + PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION, + PROJECT_URL: process.env.PROJECT_URL, + PROJECT_SECRET: process.env.PROJECT_SECRET, - PROJECT_NAME: process.env.PROJECT_NAME, - PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION, - PROJECT_URL: process.env.PROJECT_URL, - PROJECT_SECRET: process.env.PROJECT_SECRET, + DATABASE_URL: process.env.DATABASE_URL, - DATABASE_URL: process.env.DATABASE_URL, + SMTP_PASSWORD: process.env.SMTP_PASSWORD, + SMTP_FROM: process.env.SMTP_FROM, + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: process.env.SMTP_PORT, + SMTP_USER: process.env.SMTP_USER, + SMTP_SECURE: process.env.SMTP_SECURE, - SMTP_PASSWORD: process.env.SMTP_PASSWORD, - SMTP_FROM: process.env.SMTP_FROM, - SMTP_HOST: process.env.SMTP_HOST, - SMTP_PORT: process.env.SMTP_PORT, - SMTP_USER: process.env.SMTP_USER, - SMTP_SECURE: process.env.SMTP_SECURE, + AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID, + AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, + AUTH_GOOGLE_METHOD: process.env.AUTH_GOOGLE_METHOD === "true", - AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID, - AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET, - AUTH_GOOGLE_METHOD: process.env.AUTH_GOOGLE_METHOD === "true", + AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID, + AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET, - AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID, - AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET, + RETENTION_CRON: process.env.RETENTION_CRON, - RETENTION_CRON: process.env.RETENTION_CRON, + AUTH_OIDC_ID: process.env.AUTH_OIDC_ID, + AUTH_OIDC_TITLE: process.env.AUTH_OIDC_TITLE, + AUTH_OIDC_DESC: process.env.AUTH_OIDC_DESC, + AUTH_OIDC_ICON: process.env.AUTH_OIDC_ICON, + AUTH_OIDC_CLIENT: process.env.AUTH_OIDC_CLIENT, + AUTH_OIDC_SECRET: process.env.AUTH_OIDC_SECRET, + AUTH_OIDC_ISSUER_URL: process.env.AUTH_OIDC_ISSUER_URL, + AUTH_OIDC_HOST: process.env.AUTH_OIDC_HOST, + AUTH_OIDC_SCOPES: process.env.AUTH_OIDC_SCOPES, + AUTH_OIDC_DISCOVERY_ENDPOINT: process.env.AUTH_OIDC_DISCOVERY_ENDPOINT, + AUTH_OIDC_JWKS_ENDPOINT: process.env.AUTH_OIDC_JWKS_ENDPOINT, + AUTH_OIDC_PKCE: process.env.AUTH_OIDC_PKCE, + ALLOWED_GROUP: process.env.ALLOWED_GROUP, - AUTH_OIDC_ID: process.env.AUTH_OIDC_ID, - AUTH_OIDC_TITLE: process.env.AUTH_OIDC_TITLE, - AUTH_OIDC_DESC: process.env.AUTH_OIDC_DESC, - AUTH_OIDC_ICON: process.env.AUTH_OIDC_ICON, - AUTH_OIDC_CLIENT: process.env.AUTH_OIDC_CLIENT, - AUTH_OIDC_SECRET: process.env.AUTH_OIDC_SECRET, - AUTH_OIDC_ISSUER_URL: process.env.AUTH_OIDC_ISSUER_URL, - AUTH_OIDC_HOST: process.env.AUTH_OIDC_HOST, - AUTH_OIDC_SCOPES: process.env.AUTH_OIDC_SCOPES, - AUTH_OIDC_DISCOVERY_ENDPOINT: process.env.AUTH_OIDC_DISCOVERY_ENDPOINT, - AUTH_OIDC_JWKS_ENDPOINT: process.env.AUTH_OIDC_JWKS_ENDPOINT, - AUTH_OIDC_PKCE: process.env.AUTH_OIDC_PKCE, - ALLOWED_GROUP: process.env.ALLOWED_GROUP, + AUTH_EMAIL_PASSWORD_ENABLED: process.env.AUTH_EMAIL_PASSWORD_ENABLED, + AUTH_SIGNUP_ENABLED: process.env.AUTH_SIGNUP_ENABLED, + AUTH_PASSKEY_ENABLED: process.env.AUTH_PASSKEY_ENABLED, - AUTH_EMAIL_PASSWORD_ENABLED: process.env.AUTH_EMAIL_PASSWORD_ENABLED, - AUTH_SIGNUP_ENABLED: process.env.AUTH_SIGNUP_ENABLED, - AUTH_PASSKEY_ENABLED: process.env.AUTH_PASSKEY_ENABLED, - - PRIVATE_PATH: process.env.PRIVATE_PATH || path.join(process.cwd(), 'private') - }, + PRIVATE_PATH: + process.env.PRIVATE_PATH || path.join(process.cwd(), "private"), + }, }); diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts index 21a9ecf1..a45cda34 100644 --- a/src/lib/auth/auth.ts +++ b/src/lib/auth/auth.ts @@ -1,372 +1,512 @@ -import {betterAuth} from "better-auth"; -import {drizzleAdapter} from "better-auth/adapters/drizzle"; +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; import * as drizzleDb from "@/db"; -import {db} from "@/db"; -import {env} from "@/env.mjs"; -import {nextCookies} from "better-auth/next-js"; -import {admin as adminPlugin, openAPI, Organization, organization, twoFactor} from "better-auth/plugins"; -import {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions"; -import {headers} from "next/headers"; -import {count, eq} from "drizzle-orm"; -import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization"; -import {sendEmail} from "@/lib/email"; -import {render} from "@react-email/render"; -import {withUpdatedAt} from "@/db/utils"; +import { db } from "@/db"; +import { env } from "@/env.mjs"; +import { nextCookies } from "better-auth/next-js"; +import { + admin as adminPlugin, + openAPI, + Organization, + organization, + twoFactor, +} from "better-auth/plugins"; +import { + ac, + admin, + orgAdmin, + orgMember, + orgOwner, + pending, + superadmin, + user, +} from "@/lib/auth/permissions"; +import { headers } from "next/headers"; +import { count, eq } from "drizzle-orm"; +import { + MemberWithUser, + OrganizationWithMembersAndUsers, +} from "@/db/schema/03_organization"; +import { sendEmail } from "@/lib/email"; +import { render } from "@react-email/render"; +import { withUpdatedAt } from "@/db/utils"; import EmailVerification from "@/components/emails/auth/email-verification"; import EmailForgotPassword from "@/components/emails/auth/email-forgot-password"; -import {getDeviceDetails} from "@/utils/detection"; +import { getDeviceDetails } from "@/utils/detection"; import EmailNewLogin from "@/components/emails/auth/email-new-login"; import { sso } from "@better-auth/sso"; import { AuthProviderConfig, SUPPORTED_PROVIDERS } from "@/lib/auth/config"; import { passkey } from "@better-auth/passkey"; +import { getOidcProviders } from "./oidc"; +import { APIError } from "better-auth/api"; + +const oidcProviders = getOidcProviders(); export const auth = betterAuth({ - database: drizzleAdapter(db, { - provider: "pg", - schema: drizzleDb.schemas, + database: drizzleAdapter(db, { + provider: "pg", + schema: drizzleDb.schemas, + }), + appName: env.PROJECT_NAME!, + baseURL: env.PROJECT_URL, + secret: env.PROJECT_SECRET, + emailAndPassword: { + enabled: env.AUTH_EMAIL_PASSWORD_ENABLED === "true", + requireEmailVerification: false, + sendResetPassword: async ({ user, token }, request) => { + await db + .update(drizzleDb.schemas.user) + .set( + withUpdatedAt({ + emailVerified: true, + }), + ) + .where(eq(drizzleDb.schemas.user.id, user.id)) + .returning(); + + await sendEmail({ + to: user.email, + subject: "Reset your password", + html: await render( + EmailForgotPassword({ + firstname: user.name!, + token, + }), + {}, + ), + }); + }, + }, + emailVerification: { + async sendVerificationEmail({ user, token, url }) { + await sendEmail({ + to: user.email, + subject: "Portabase Email Verification", + html: await render( + EmailVerification({ + firstname: user.name, + url: url, + }), + ), + }); + + await ( + await auth.$context + ).internalAdapter.updateUser(user.id, { + emailVerified: false, + }); + }, + async afterEmailVerification(user) { + await ( + await auth.$context + ).internalAdapter.updateUser(user.id, { + emailVerified: true, + }); + }, + }, + socialProviders: SUPPORTED_PROVIDERS.reduce( + (acc: any, provider: AuthProviderConfig) => { + if (!provider.isActive) return acc; + if (provider.id === "credential") return acc; + if (provider.type === "sso") return acc; + if (provider.id === "google") { + acc.google = { + clientId: env.AUTH_GOOGLE_ID! as string, + clientSecret: env.AUTH_GOOGLE_SECRET! as string, + }; + } + if (provider.id === "github") { + acc.github = { + // clientId: provider.credentials?.clientId, + // clientSecret: provider.credentials?.clientSecret, + }; + } + return acc; + }, + {}, + ), + account: { + accountLinking: { + enabled: true, + trustedProviders: [ + "google", + "github", + "credential", + ...oidcProviders.map((p) => p.id), + ], + allowDifferentEmails: false, + }, + }, + + plugins: [ + sso({ + defaultSSO: oidcProviders.map((p) => ({ + oidcConfig: { + issuer: p.issuerUrl, + discoveryEndpoint: p.discoveryEndpoint, + jwksEndpoint: p.jwksEndpoint, + clientId: p.client, + clientSecret: p.secret, + scopes: p.scopes?.split(" ") ?? ["openid", "profile", "email"], + pkce: p.pkce, + mapping: { + extraFields: { + groups: "groups", + }, + }, + }, + providerId: p.id, + domain: p.host, + //@ts-ignore + issuer: p.issuerUrl, + })), + provisionUser: async ({ user: usr, userInfo, provider }) => { + const providerId = provider.providerId; + const oidcProvider = oidcProviders.find((p) => p.id === providerId); + const allowedGroup = oidcProvider?.allowedGroup || env.ALLOWED_GROUP; + const roleMapStr = oidcProvider?.roleMap; + + const rawGroups = userInfo?.groups || userInfo?.roles || []; + const userGroups: string[] = Array.isArray(rawGroups) + ? rawGroups + : [rawGroups]; + + let roleToAssign: string | undefined; + + if (roleMapStr) { + const mappings = roleMapStr.split(",").map((m) => m.split(":")); + for (const [group, role] of mappings) { + if (userGroups.includes(group.trim())) { + roleToAssign = role.trim(); + break; + } + } + } + + if (!roleToAssign && allowedGroup) { + const hasAccess = userGroups.includes(allowedGroup); + + if (hasAccess) { + const userCount = ( + await db.select({ count: count() }).from(drizzleDb.schemas.user) + )[0].count; + const isSuperadmin = userCount === 0; + + roleToAssign = + allowedGroup.includes("admin") || + allowedGroup.includes("superadmin") + ? isSuperadmin + ? "superadmin" + : "admin" + : "pending"; + } + } + + if (!roleToAssign && oidcProvider?.defaultRole) { + roleToAssign = oidcProvider.defaultRole; + } + + if (!roleToAssign) { + console.warn( + `Access Denied for user ${usr.email}: No matching group/role found in ${providerId} config.`, + ); + throw new APIError("FORBIDDEN", { + message: `Access Denied: No matching roles found (${roleToAssign})`, + }); + } + + const userCount = ( + await db.select({ count: count() }).from(drizzleDb.schemas.user) + )[0].count; + if ( + userCount === 0 && + (roleToAssign === "admin" || roleToAssign === "superadmin") + ) { + 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) + .set({ role: roleToAssign, emailVerified: true }) + .where(eq(drizzleDb.schemas.user.id, existingUser.id)); + } else { + return { + ...usr, + role: roleToAssign, + emailVerified: true, + }; + } + }, }), - appName: env.PROJECT_NAME!, - baseURL: env.PROJECT_URL, - secret: env.PROJECT_SECRET, - emailAndPassword: { - enabled: env.AUTH_EMAIL_PASSWORD_ENABLED === "true", - requireEmailVerification: false, - sendResetPassword: async ({user, token}, request) => { - - await db.update(drizzleDb.schemas.user).set(withUpdatedAt({ - emailVerified: true, - })).where(eq(drizzleDb.schemas.user.id, user.id)).returning(); - - await sendEmail({ - to: user.email, - subject: "Reset your password", - html: await render( - EmailForgotPassword({ - firstname: user.name!, - token, - }), - {} - ), - }); - } - }, - emailVerification: { - async sendVerificationEmail({user, token, url}) { - - - await sendEmail({ - to: user.email, - subject: "Portabase Email Verification", - html: await render(EmailVerification({ - firstname: user.name, - url: url - })), - }); - - await ( - await auth.$context - ).internalAdapter.updateUser(user.id, { - emailVerified: false, - }); - }, - async afterEmailVerification(user) { - await ( - await auth.$context - ).internalAdapter.updateUser(user.id, { - emailVerified: true, - }); - }, - }, - socialProviders: SUPPORTED_PROVIDERS.reduce((acc: any, provider: AuthProviderConfig) => { - if (!provider.isActive) return acc; - if (provider.id === "credential") return acc; - if (provider.id === env.AUTH_OIDC_ID!) return acc; - if (provider.id === "google") { - acc.google = { - clientId: env.AUTH_GOOGLE_ID! as string, - clientSecret: env.AUTH_GOOGLE_SECRET! as string, - }; - } - if (provider.id === "github") { - acc.github = { - // clientId: provider.credentials?.clientId, - // clientSecret: provider.credentials?.clientSecret, - }; - } - return acc; - }, {}), - account: { - accountLinking: { - enabled: true, - trustedProviders: ["google", "github", "credential",env.AUTH_OIDC_ID!], - allowDifferentEmails: false - }, - }, - - plugins: [ - sso({ - defaultSSO: [{ - oidcConfig: { - issuer: env.AUTH_OIDC_ISSUER_URL!, - discoveryEndpoint: env.AUTH_OIDC_DISCOVERY_ENDPOINT!, - jwksEndpoint: env.AUTH_OIDC_JWKS_ENDPOINT!, - clientId: env.AUTH_OIDC_CLIENT!, - clientSecret: env.AUTH_OIDC_SECRET!, - scopes: env.AUTH_OIDC_SCOPES?.split(" ") ?? ["openid", "profile", "email"], - pkce: env.AUTH_OIDC_PKCE === "true", - mapping: { - extraFields: { - groups: "groups" - } - } - }, - providerId: env.AUTH_OIDC_ID!, - domain: env.AUTH_OIDC_HOST!, - //@ts-ignore - issuer: env.AUTH_OIDC_ISSUER_URL! - }], - provisionUser: async ({ user: usr, userInfo }) => { - const allowedGroup = env.ALLOWED_GROUP; - - if (!allowedGroup) return; - - const rawGroups = (userInfo as any).groups || (userInfo as any).roles || []; - - const userGroups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups]; - - const hasAccess = userGroups.includes(allowedGroup); - - if (!hasAccess) { - throw new Error("Access Denied"); - } - - const userCount = (await db.select({ count: count() }).from(drizzleDb.schemas.user))[0].count; - const isSuperadmin = userCount === 0 ? "superadmin" : undefined; - - const roleToAssign = allowedGroup.includes('admin') || allowedGroup.includes('superadmin') ? - isSuperadmin ? 'superadmin' : "admin" : 'pending'; - - const existingUser = await db.query.user.findFirst({ - where: eq(drizzleDb.schemas.user.email, usr.email) - }); - - if (existingUser) { - await db.update(drizzleDb.schemas.user) - .set({ role: roleToAssign, emailVerified: true }) - .where(eq(drizzleDb.schemas.user.id, existingUser.id)); - } - }, - }), - ...(env.AUTH_PASSKEY_ENABLED === "true" ? [passkey({ + ...(env.AUTH_PASSKEY_ENABLED === "true" + ? [ + passkey({ rpName: env.PROJECT_NAME || "Portabase", - rpID: env.PROJECT_URL ? new URL(env.PROJECT_URL).hostname : "localhost" - })] : []), - openAPI(), - nextCookies(), - twoFactor(), - organization({ - ac, - roles: { - owner: orgOwner, - admin: orgAdmin, - member: orgMember, - }, - }), - adminPlugin({ - adminRoles: ["admin", "superadmin"], - defaultRole: "pending", - ac, - roles: { - admin, - user, - pending, - superadmin, - }, - }), - ], - advanced: { - database: { - generateId: false, + rpID: env.PROJECT_URL + ? new URL(env.PROJECT_URL).hostname + : "localhost", + }), + ] + : []), + openAPI(), + nextCookies(), + twoFactor(), + organization({ + ac, + roles: { + owner: orgOwner, + admin: orgAdmin, + member: orgMember, + }, + }), + adminPlugin({ + adminRoles: ["admin", "superadmin"], + defaultRole: "pending", + ac, + roles: { + admin, + user, + pending, + superadmin, + }, + }), + ], + advanced: { + database: { + generateId: false, + }, + }, + user: { + deleteUser: { + enabled: true, + }, + changeEmail: { + enabled: true, + }, + additionalFields: { + deletedAt: { + type: "number", + nullable: true, + required: false, + }, + theme: { + type: "string", + }, + lastConnectedAt: { + type: "date", + }, + lastChangedPasswordAt: { + type: "date", + }, + }, + }, + databaseHooks: { + account: { + create: { + before: async (account) => { + const provider = SUPPORTED_PROVIDERS.find( + (p) => p.id === account.providerId, + ); + if (provider && provider.allowLinking === false) { + throw new APIError("FORBIDDEN", { + message: "Linking is disabled for this provider.", + }); + } }, + }, + delete: { + before: async (account) => { + const provider = SUPPORTED_PROVIDERS.find( + (p) => p.id === account.providerId, + ); + if (provider && provider.allowUnlinking === false) { + throw new APIError("FORBIDDEN", { + message: "Unlinking is disabled for this provider.", + }); + } + }, + }, }, user: { - deleteUser: { - enabled: true, + update: { + async before(user, context) { + if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") { + if (user.password || user.lastChangedPasswordAt) { + throw new APIError("FORBIDDEN", { + message: "Password updates are disabled", + }); + } + } + return { + data: user, + }; }, - changeEmail: { - enabled: true, + }, + create: { + async before(user, context) { + const userCount = ( + await db.select({ count: count() }).from(drizzleDb.schemas.user) + )[0].count; + + if (env.AUTH_SIGNUP_ENABLED !== "true" && userCount > 0) { + throw new APIError("FORBIDDEN", { + message: "Sign up is disabled", + }); + } + + const role = userCount === 0 ? "superadmin" : "pending"; + + return { + data: { + ...user, + role, + }, + }; }, - additionalFields: { - deletedAt: { - type: "number", - nullable: true, - required: false, - }, - theme: { - type: "string", - }, - lastConnectedAt: { - type: "date", - }, - lastChangedPasswordAt: { - type: "date", - }, + async after(user, context) { + const userCount = ( + await db.select({ count: count() }).from(drizzleDb.schemas.user) + )[0].count; + const role = userCount === 0 ? "owner" : "admin"; + + const defaultOrgSlug = "default"; + const defaultOrg = await db.query.organization.findFirst({ + where: eq(drizzleDb.schemas.organization.slug, defaultOrgSlug), + }); + + if (defaultOrg) { + await db.insert(drizzleDb.schemas.member).values({ + userId: user.id, + organizationId: defaultOrg.id, + role: role, + }); + } else { + console.warn( + "Default organization not found. Cannot assign member.", + ); + } }, - }, - databaseHooks: { - user: { - update: { - async before(user, context) { - if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") { - if (user.password || user.lastChangedPasswordAt) { - throw new Error("Password updates are disabled"); - } - } - return { - data: user, - }; - }, - }, - create: { - async before(user, context) { - const userCount = (await db.select({count: count()}).from(drizzleDb.schemas.user))[0].count; - - if (env.AUTH_SIGNUP_ENABLED !== "true" && userCount > 0) { - throw new Error("Sign up is disabled"); - } - - const role = userCount === 0 ? "superadmin" : "pending"; - - return { - data: { - ...user, - role, - }, - }; - }, - async after(user, context) { - const userCount = (await db.select({count: count()}).from(drizzleDb.schemas.user))[0].count; - const role = userCount === 0 ? "owner" : "admin"; - - - const defaultOrgSlug = "default"; - const defaultOrg = await db.query.organization.findFirst({ - where: eq(drizzleDb.schemas.organization.slug, defaultOrgSlug), - }); - - if (defaultOrg) { - await db.insert(drizzleDb.schemas.member).values({ - userId: user.id, - organizationId: defaultOrg.id, - role: role, - }); - } else { - console.warn("Default organization not found. Cannot assign member."); - } - }, - }, - }, - session: { - create: { - before: async (session, context) => { - - - const userId = session.userId; - - let memberships = await db.query.member.findMany({ - where: eq(drizzleDb.schemas.member.userId, userId), - }); - - return { - data: { - activeOrganizationId: memberships[0].organizationId, - }, - }; - }, - // after: async (session) => { - // const user = await db.query.user.findFirst({ - // where: eq(drizzleDb.schemas.user.id, session.userId), - // }); - // - // if (user && user.role != "pending") { - // const deviceInfo = getDeviceDetails(session.userAgent); - // await sendEmail({ - // to: user.email, - // subject: "New login to your account", - // html: await render( - // EmailNewLogin({ - // firstname: user.name!, - // os: deviceInfo.os, - // browser: deviceInfo.browser, - // ipAddress: session.ipAddress!, - // }), - // {} - // ), - // }); - // - // (await auth.$context).internalAdapter.updateUser(user.id, { - // lastConnectedAt: new Date(), - // }); - // } - // }, - after: async (session) => { - - console.log("session", session); - - const user = await db.query.user.findFirst({ - where: eq(drizzleDb.schemas.user.id, session.userId), - }); - - if (!user) return; - - const createdAtDiff = new Date(session.createdAt).getTime() - new Date(user.createdAt).getTime(); - - if (createdAtDiff < 5000) { - console.log(`Skipping new login email for freshly created user ${user.email}`); - 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; - - const deviceInfo = getDeviceDetails(session.userAgent); - - await sendEmail({ - to: user.email, - subject: "New login to your account", - html: await render( - EmailNewLogin({ - firstname: user.name!, - os: deviceInfo.os, - browser: deviceInfo.browser, - ipAddress: session.ipAddress!, - }), - {} - ), - }); - - (await auth.$context).internalAdapter.updateUser(user.id, { - lastConnectedAt: new Date(), - }); - }, - }, - }, - + }, }, session: { - additionalFields: { - activeOrganizationId: { - type: "string", - required: false, + create: { + before: async (session, context) => { + const userId = session.userId; + + const memberships = await db.query.member.findMany({ + where: eq(drizzleDb.schemas.member.userId, userId), + }); + + const url = + context?.request?.url || context?.headers?.get("referer") || ""; + + let providerId: string; + + if (url.includes("/sso/callback")) { + const urlObj = new URL(url, "http://localhost"); + providerId = urlObj.searchParams.get("providerId") || "sso"; + console.log(`Found provider: ${providerId}`); + } + + return { + data: { + activeOrganizationId: memberships[0].organizationId, + providerId: providerId, }, + }; }, + // after: async (session) => { + // const user = await db.query.user.findFirst({ + // where: eq(drizzleDb.schemas.user.id, session.userId), + // }); + // + // if (user && user.role != "pending") { + // const deviceInfo = getDeviceDetails(session.userAgent); + // await sendEmail({ + // to: user.email, + // subject: "New login to your account", + // html: await render( + // EmailNewLogin({ + // firstname: user.name!, + // os: deviceInfo.os, + // browser: deviceInfo.browser, + // ipAddress: session.ipAddress!, + // }), + // {} + // ), + // }); + // + // (await auth.$context).internalAdapter.updateUser(user.id, { + // lastConnectedAt: new Date(), + // }); + // } + // }, + after: async (session) => { + console.log("session", session); + + const user = await db.query.user.findFirst({ + where: eq(drizzleDb.schemas.user.id, session.userId), + }); + + if (!user) return; + + const createdAtDiff = + new Date(session.createdAt).getTime() - + new Date(user.createdAt).getTime(); + + if (createdAtDiff < 5000) { + console.log( + `Skipping new login email for freshly created user ${user.email}`, + ); + 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; + + const deviceInfo = getDeviceDetails(session.userAgent); + + await sendEmail({ + to: user.email, + subject: "New login to your account", + html: await render( + EmailNewLogin({ + firstname: user.name!, + os: deviceInfo.os, + browser: deviceInfo.browser, + ipAddress: session.ipAddress!, + }), + {}, + ), + }); + + (await auth.$context).internalAdapter.updateUser(user.id, { + lastConnectedAt: new Date(), + }); + }, + }, }, - /* databaseHooks: { + }, + session: { + additionalFields: { + activeOrganizationId: { + type: "string", + required: false, + }, + }, + }, + /* databaseHooks: { session: { create: { before: async (session) => { @@ -394,7 +534,7 @@ export const auth = betterAuth({ }, }, },*/ - trustedOrigins: [env.PROJECT_URL!, "http://app"], + trustedOrigins: [env.PROJECT_URL!, "http://app"], }); /*export const signUpUser = async (email: string, password: string, name: string) => { @@ -420,226 +560,228 @@ export const signInUser = async (email: string, password: string) => { return user; };*/ -export const createUser = async (name: string, email: string, password: string, role: "user" | "pending" | "admin" | "superadmin" = "pending") => { - return await auth.api.createUser({ - headers: await headers(), - body: { - name, - email, - password, - role, - }, - }); +export const createUser = async ( + name: string, + email: string, + password: string, + role: "user" | "pending" | "admin" | "superadmin" = "pending", +) => { + return await auth.api.createUser({ + headers: await headers(), + body: { + name, + email, + password, + role, + }, + }); }; export const getSessions = async () => { - return await auth.api.listSessions({ - headers: await headers(), - }); + return await auth.api.listSessions({ + headers: await headers(), + }); }; export const getSession = async () => { - return await auth.api.getSession({ - headers: await headers(), - }); + return await auth.api.getSession({ + headers: await headers(), + }); }; export const revokeSession = async (e: string) => { - try { - const {status} = await auth.api.revokeSession({ - body: { - token: e, - }, - headers: await headers(), - }); - return status; - } catch (e) { - } + try { + const { status } = await auth.api.revokeSession({ + body: { + token: e, + }, + headers: await headers(), + }); + return status; + } catch (e) {} }; export const getAccounts = async () => { - return await auth.api.listUserAccounts({ - headers: await headers(), - }); + return await auth.api.listUserAccounts({ + headers: await headers(), + }); }; export const unlinkAccount = async (provider: string, account: string) => { - try { - const {status} = await auth.api.unlinkAccount({ - body: { - providerId: provider, - accountId: account, - }, - headers: await headers(), - }); + try { + const { status } = await auth.api.unlinkAccount({ + body: { + providerId: provider, + accountId: account, + }, + headers: await headers(), + }); - return status; - } catch (e) { - } + return status; + } catch (e) {} }; export const getOrganization = async ({ - organizationId, - organizationSlug, - }: { - organizationId?: string; - organizationSlug?: string; + organizationId, + organizationSlug, +}: { + organizationId?: string; + organizationSlug?: string; } = {}): Promise => { - const query = - organizationId != null - ? {organizationId} - : organizationSlug != null - ? {organizationSlug} - : undefined; + const query = + organizationId != null + ? { organizationId } + : organizationSlug != null + ? { organizationSlug } + : undefined; - try { - const response = await auth.api.getFullOrganization({ - headers: await headers(), - ...(query ? {query} : {}), - }); + try { + const response = await auth.api.getFullOrganization({ + headers: await headers(), + ...(query ? { query } : {}), + }); - return response as OrganizationWithMembersAndUsers; - } catch (e) { - console.error(e); - return null; - } + return response as OrganizationWithMembersAndUsers; + } catch (e) { + console.error(e); + return null; + } }; export const getPasskeys = async () => { - if (env.AUTH_PASSKEY_ENABLED !== "true") return []; - const passkeys = await auth.api.listPasskeys({ - headers: await headers(), - }); + if (env.AUTH_PASSKEY_ENABLED !== "true") return []; + const passkeys = await auth.api.listPasskeys({ + headers: await headers(), + }); - return passkeys; + return passkeys; }; export const revokePasskey = async (e: string) => { - if (env.AUTH_PASSKEY_ENABLED !== "true") return; - await auth.api.deletePasskey({ - body: { - id: e, - }, - headers: await headers(), - }); + if (env.AUTH_PASSKEY_ENABLED !== "true") return; + await auth.api.deletePasskey({ + body: { + id: e, + }, + headers: await headers(), + }); }; export const listOrganizations = async (): Promise => { - try { - return await auth.api.listOrganizations({ - headers: await headers(), - }) as Organization[]; - } catch (e) { - return null; - } + try { + return (await auth.api.listOrganizations({ + headers: await headers(), + })) as Organization[]; + } catch (e) { + return null; + } }; export const getLastOrganizationOrFirst = async (userId: string) => { - try { - const organizations = await db.query.organization.findMany({ - where: eq(drizzleDb.schemas.member.userId, userId), - }); + try { + const organizations = await db.query.organization.findMany({ + where: eq(drizzleDb.schemas.member.userId, userId), + }); - if (organizations.length > 0) { - return organizations[0].id; - } - - return null; - } catch (e) { - return null; + if (organizations.length > 0) { + return organizations[0].id; } + + return null; + } catch (e) { + return null; + } }; export const createOrganization = async (name: string, slug: string) => { - try { - return await auth.api.createOrganization({ - headers: await headers(), - body: { - name, - slug, - }, - }); - } catch (e: any) { - const errorMessage = e?.response?.data?.message || e?.message || "Unknown auth error"; - const status = e?.response?.status || 500; + try { + return await auth.api.createOrganization({ + headers: await headers(), + body: { + name, + slug, + }, + }); + } catch (e: any) { + const errorMessage = + e?.response?.data?.message || e?.message || "Unknown auth error"; + const status = e?.response?.status || 500; - console.error("Auth API createOrganization error:", { - message: errorMessage, - status, - raw: e, - }); + console.error("Auth API createOrganization error:", { + message: errorMessage, + status, + raw: e, + }); - throw { - name: "AuthCreateOrganizationError", - message: errorMessage, - status, - cause: e, - }; - } + throw { + name: "AuthCreateOrganizationError", + message: errorMessage, + status, + cause: e, + }; + } }; export const deleteOrganization = async (organizationId: string) => { - try { - return await auth.api.deleteOrganization({ - body: { - organizationId, - }, - headers: await headers(), - }); - } catch (e: any) { - const errorMessage = e?.response?.data?.message || e?.message || "Unknown auth error"; - const status = e?.response?.status || 500; + try { + return await auth.api.deleteOrganization({ + body: { + organizationId, + }, + headers: await headers(), + }); + } catch (e: any) { + const errorMessage = + e?.response?.data?.message || e?.message || "Unknown auth error"; + const status = e?.response?.status || 500; - console.error("Auth API deleteOrganization error:", { - message: errorMessage, - status, - raw: e, - }); + console.error("Auth API deleteOrganization error:", { + message: errorMessage, + status, + raw: e, + }); - throw { - name: "AuthDeleteOrganizationError", - message: errorMessage, - status, - cause: e, - }; - } + throw { + name: "AuthDeleteOrganizationError", + message: errorMessage, + status, + cause: e, + }; + } }; - export const checkSlugOrganization = async (slug: string) => { - try { - const {status} = await auth.api.checkOrganizationSlug({ - headers: await headers(), - body: { - slug, - }, - }); + try { + const { status } = await auth.api.checkOrganizationSlug({ + headers: await headers(), + body: { + slug, + }, + }); - return status; - } catch { - } + return status; + } catch {} }; export const getActiveMember = async () => { - try { - const member = await auth.api.getActiveMember({ - headers: await headers(), - }); + try { + const member = await auth.api.getActiveMember({ + headers: await headers(), + }); - return member as MemberWithUser; - } catch (e) { - console.log("err", e); - } + return member as MemberWithUser; + } catch (e) { + console.log("err", e); + } }; export const setActiveOrganization = async (slug: string) => { - try { - return await auth.api.setActiveOrganization({ - headers: await headers(), - body: { - organizationSlug: slug, - }, - }); - } catch { - } + try { + return await auth.api.setActiveOrganization({ + headers: await headers(), + body: { + organizationSlug: slug, + }, + }); + } catch {} }; diff --git a/src/lib/auth/config.ts b/src/lib/auth/config.ts index 78972ebb..a1499388 100644 --- a/src/lib/auth/config.ts +++ b/src/lib/auth/config.ts @@ -1,63 +1,76 @@ import { env } from "@/env.mjs"; +import { getOidcProviders } from "./oidc"; export interface AuthProviderConfig { - id: string; - isActive: boolean; - name?: string; - icon: string; - isManual?: boolean; - title?: string; - description?: string; - type: "social" | "sso" | "credential" | "passkey"; + id: string; + isActive: boolean; + name?: string; + icon: string; + isManual?: boolean; + title?: string; + description?: string; + type: "social" | "sso" | "credential" | "passkey"; + allowLinking?: boolean; + allowUnlinking?: boolean; } +const oidcProviders = getOidcProviders(); + export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [ - { - id: "credential", - isActive: env.AUTH_EMAIL_PASSWORD_ENABLED === "true", - name: "Password", - icon: "lucide:lock", - title: "Password", - description: "Standard email and password login.", - isManual: true, - type: "credential" - }, - { - id: "google", - isActive: !!env.AUTH_GOOGLE_ID, - name: "Google", - icon: "logos:google-icon", - title: "Google", - description: "Sign in with your Google account.", - type: "social" - }, - { - id: "github", - isActive: !!env.AUTH_GITHUB_ID, - name: "GitHub", - icon: "logos:github-icon", - title: "GitHub", - description: "Sign in with your GitHub account.", - type: "social" - }, - { - id: env.AUTH_OIDC_ID || "oidc", - isActive: !!env.AUTH_OIDC_CLIENT, - name: env.AUTH_OIDC_TITLE || "SSO", - icon: env.AUTH_OIDC_ICON || "lucide:building", - title: env.AUTH_OIDC_TITLE || "SSO", - description: env.AUTH_OIDC_DESC || "Sign in with your SSO account.", - isManual: true, - type: "sso" - }, - { - id: "passkey", - isActive: env.AUTH_PASSKEY_ENABLED === "true", - name: "Passkey", - icon: "lucide:fingerprint", - title: "Passkey", - description: "Sign in with your passkey.", - isManual: false, - type: "passkey" - } -]; \ No newline at end of file + { + id: "credential", + isActive: env.AUTH_EMAIL_PASSWORD_ENABLED === "true", + name: "Password", + icon: "lucide:lock", + title: "Password", + description: "Standard email and password login.", + isManual: true, + type: "credential", + allowLinking: true, + allowUnlinking: true, + }, + { + id: "google", + isActive: !!env.AUTH_GOOGLE_ID, + name: "Google", + icon: "logos:google-icon", + title: "Google", + description: "Sign in with your Google account.", + type: "social", + allowLinking: true, + allowUnlinking: true, + }, + { + id: "github", + isActive: !!env.AUTH_GITHUB_ID, + name: "GitHub", + icon: "logos:github-icon", + title: "GitHub", + description: "Sign in with your GitHub account.", + type: "social", + allowLinking: true, + allowUnlinking: true, + }, + ...oidcProviders.map((p) => ({ + id: p.id, + isActive: true, + name: p.title, + icon: p.icon, + title: p.title, + description: p.description, + isManual: true, + type: "sso" as const, + allowLinking: p.allowLinking, + allowUnlinking: p.allowUnlinking, + })), + { + id: "passkey", + isActive: env.AUTH_PASSKEY_ENABLED === "true", + name: "Passkey", + icon: "lucide:fingerprint", + title: "Passkey", + description: "Sign in with your passkey.", + isManual: false, + type: "passkey", + }, +]; diff --git a/src/lib/auth/oidc.ts b/src/lib/auth/oidc.ts new file mode 100644 index 00000000..dca46a43 --- /dev/null +++ b/src/lib/auth/oidc.ts @@ -0,0 +1,93 @@ +import { env } from "@/env.mjs"; + +export interface OIDCProvider { + id: string; + title: string; + description: string; + icon: string; + client: string; + secret: string; + issuerUrl: string; + host: string; + scopes?: string; + discoveryEndpoint?: string; + jwksEndpoint?: string; + pkce: boolean; + allowedGroup?: string; + roleMap?: string; + defaultRole?: string; + allowLinking: boolean; + allowUnlinking: boolean; +} + +export function getOidcProviders(): OIDCProvider[] { + const providers: OIDCProvider[] = []; + + if ( + env.AUTH_OIDC_CLIENT && + (env.AUTH_OIDC_ISSUER_URL || env.AUTH_OIDC_DISCOVERY_ENDPOINT) + ) { + providers.push({ + id: env.AUTH_OIDC_ID || "oidc", + title: env.AUTH_OIDC_TITLE || "SSO", + description: env.AUTH_OIDC_DESC || "Sign in with your SSO account.", + icon: env.AUTH_OIDC_ICON || "lucide:building", + client: env.AUTH_OIDC_CLIENT, + secret: env.AUTH_OIDC_SECRET || "", + issuerUrl: env.AUTH_OIDC_ISSUER_URL || "", + host: env.AUTH_OIDC_HOST || "", + scopes: env.AUTH_OIDC_SCOPES, + discoveryEndpoint: env.AUTH_OIDC_DISCOVERY_ENDPOINT, + jwksEndpoint: env.AUTH_OIDC_JWKS_ENDPOINT, + pkce: env.AUTH_OIDC_PKCE === "true", + allowedGroup: env.ALLOWED_GROUP, + roleMap: process.env.AUTH_OIDC_ROLE_MAP, + defaultRole: process.env.AUTH_OIDC_DEFAULT_ROLE, + allowLinking: process.env.AUTH_OIDC_ALLOW_LINKING !== "false", + allowUnlinking: process.env.AUTH_OIDC_ALLOW_UNLINKING !== "false", + }); + } + + const prefixes = new Set(); + Object.keys(process.env).forEach((key) => { + const match = key.match(/^AUTH_OIDC_(.+)_CLIENT$/); + if (match) { + prefixes.add(match[1]); + } + }); + + prefixes.forEach((prefix) => { + const client = process.env[`AUTH_OIDC_${prefix}_CLIENT`]; + const issuer = process.env[`AUTH_OIDC_${prefix}_ISSUER_URL`]; + const discovery = process.env[`AUTH_OIDC_${prefix}_DISCOVERY_ENDPOINT`]; + + if (!client || (!issuer && !discovery)) return; + + providers.push({ + id: process.env[`AUTH_OIDC_${prefix}_ID`] || prefix.toLowerCase(), + title: process.env[`AUTH_OIDC_${prefix}_TITLE`] || prefix, + description: + process.env[`AUTH_OIDC_${prefix}_DESC`] || `Sign in with ${prefix}`, + icon: process.env[`AUTH_OIDC_${prefix}_ICON`] || "lucide:building", + client: client, + secret: process.env[`AUTH_OIDC_${prefix}_SECRET`] || "", + issuerUrl: issuer || "", + host: process.env[`AUTH_OIDC_${prefix}_HOST`] || "", + scopes: process.env[`AUTH_OIDC_${prefix}_SCOPES`], + discoveryEndpoint: discovery, + jwksEndpoint: process.env[`AUTH_OIDC_${prefix}_JWKS_ENDPOINT`], + pkce: process.env[`AUTH_OIDC_${prefix}_PKCE`] === "true", + allowedGroup: + process.env[`AUTH_OIDC_${prefix}_ALLOWED_GROUP`] || + process.env.ALLOWED_GROUP, + roleMap: process.env[`AUTH_OIDC_${prefix}_ROLE_MAP`], + defaultRole: process.env[`AUTH_OIDC_${prefix}_DEFAULT_ROLE`], + allowLinking: + process.env[`AUTH_OIDC_${prefix}_ALLOW_LINKING`] !== "false", + allowUnlinking: + process.env[`AUTH_OIDC_${prefix}_ALLOW_UNLINKING`] !== "false", + }); + }); + + return providers; +} diff --git a/test-dev/.env b/test-dev/.env deleted file mode 100644 index 02c14814..00000000 --- a/test-dev/.env +++ /dev/null @@ -1,6 +0,0 @@ -EDGE_KEY="eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODcvIiwiYWdlbnRJZCI6IjAwNWVjODdiLWY1MzAtNDU5YS04MjM4LTgzZjdiNzZiYjEzOSIsIm1hc3RlcktleUI2NCI6Imt0cC9ZbXk3TE1lU2hkNEJHZ3I5RDBIeE5sYnozRExVUnRoVlYwQW8vek09In0=" -PROJECT_NAME="test-dev" -DB_PG_2CD9_PORT="42597" -DB_PG_2CD9_DB="pg_d7699b61" -DB_PG_2CD9_USER="admin" -DB_PG_2CD9_PASS="9e20bcd7e4e21cef" diff --git a/test-dev/databases.json b/test-dev/databases.json deleted file mode 100644 index 6a0d8c5b..00000000 --- a/test-dev/databases.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "databases": [ - { - "name": "pg_d7699b61", - "database": "pg_d7699b61", - "type": "postgresql", - "username": "admin", - "password": "9e20bcd7e4e21cef", - "port": 42597, - "host": "localhost", - "generated_id": "b8edfd1e-00a6-412f-988d-62b477966578" - } - ] -} \ No newline at end of file diff --git a/test-dev/docker-compose.yml b/test-dev/docker-compose.yml deleted file mode 100644 index b43f569b..00000000 --- a/test-dev/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: test-dev -services: - app: - container_name: test-dev-app - restart: always - image: portabase/agent:latest - volumes: - - ./databases.json:/config/config.json - extra_hosts: - - "localhost:host-gateway" - environment: - TZ: "Europe/Paris" - EDGE_KEY: "${EDGE_KEY}" - LOG: info - networks: - - portabase - - - db-pg-2cd9: - container_name: test-dev-db-pg-2cd9 - image: postgres:17-alpine - networks: - - portabase - - default - ports: - - "${DB_PG_2CD9_PORT}:5432" - volumes: - - db-pg-2cd9-data:/var/lib/postgresql/data - environment: - - POSTGRES_DB=${DB_PG_2CD9_DB} - - POSTGRES_USER=${DB_PG_2CD9_USER} - - POSTGRES_PASSWORD=${DB_PG_2CD9_PASS} - - -volumes: - db-pg-2cd9-data: - - -networks: - portabase: - name: portabase_network - external: true \ No newline at end of file