import React from "react"; import { useSidebar } from "@/components/sidebar.tsx"; import { Separator } from "@/components/separator.tsx"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/tabs.tsx"; import { Shield, Users, Database, Clock, Key } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; import { getAdminOIDCConfig, getRegistrationAllowed, getPasswordLoginAllowed, getPasswordResetAllowed, getUserList, getUserInfo, isElectron, getSessions, unlinkOIDCFromPasswordAccount, } from "@/main-axios.ts"; import { SimpleLoader } from "@/lib/SimpleLoader.tsx"; import { RolesTab } from "@/admin/tabs/RolesTab.tsx"; import { GeneralSettingsTab } from "@/admin/tabs/GeneralSettingsTab.tsx"; import { OIDCSettingsTab } from "@/admin/tabs/OIDCSettingsTab.tsx"; import { UserManagementTab } from "@/admin/tabs/UserManagementTab.tsx"; import { SessionManagementTab } from "@/admin/tabs/SessionManagementTab.tsx"; import { DatabaseSecurityTab } from "@/admin/tabs/DatabaseSecurityTab.tsx"; import { ApiKeysTab } from "@/admin/tabs/ApiKeysTab.tsx"; import { CreateUserDialog } from "./dialogs/CreateUserDialog.tsx"; import { UserEditDialog } from "./dialogs/UserEditDialog.tsx"; import { LinkAccountDialog } from "./dialogs/LinkAccountDialog.tsx"; interface AdminSettingsProps { isTopbarOpen?: boolean; rightSidebarOpen?: boolean; rightSidebarWidth?: number; } export function AdminSettings({ isTopbarOpen = true, rightSidebarOpen = false, rightSidebarWidth = 400, }: AdminSettingsProps): React.ReactElement { const { t } = useTranslation(); const { confirmWithToast } = useConfirmation(); const { state: sidebarState } = useSidebar(); const [loading, setLoading] = React.useState(true); const [allowRegistration, setAllowRegistration] = React.useState(true); const [allowPasswordLogin, setAllowPasswordLogin] = React.useState(true); const [allowPasswordReset, setAllowPasswordReset] = React.useState(true); const [oidcConfig, setOidcConfig] = React.useState({ client_id: "", client_secret: "", issuer_url: "", authorization_url: "", token_url: "", identifier_path: "sub", name_path: "name", scopes: "openid email profile", userinfo_url: "", allowed_users: "", }); const [users, setUsers] = React.useState< Array<{ id: string; username: string; isAdmin: boolean; isOidc: boolean; passwordHash?: string; }> >([]); const [usersLoading, setUsersLoading] = React.useState(false); const [createUserDialogOpen, setCreateUserDialogOpen] = React.useState(false); const [userEditDialogOpen, setUserEditDialogOpen] = React.useState(false); const [selectedUserForEdit, setSelectedUserForEdit] = React.useState<{ id: string; username: string; isAdmin: boolean; isOidc: boolean; passwordHash?: string; } | null>(null); const [currentUser, setCurrentUser] = React.useState<{ id: string; username: string; is_admin: boolean; is_oidc: boolean; } | null>(null); const [sessions, setSessions] = React.useState< Array<{ id: string; userId: string; username?: string; deviceType: string; deviceInfo: string; createdAt: string; expiresAt: string; lastActiveAt: string; isRevoked?: boolean; isCurrentSession?: boolean; }> >([]); const [sessionsLoading, setSessionsLoading] = React.useState(false); const [linkAccountAlertOpen, setLinkAccountAlertOpen] = React.useState(false); const [linkOidcUser, setLinkOidcUser] = React.useState<{ id: string; username: string; } | null>(null); React.useEffect(() => { if (isElectron()) { const serverUrl = (window as { configuredServerUrl?: string }) .configuredServerUrl; if (!serverUrl) { setLoading(false); return; } } Promise.allSettled([ getAdminOIDCConfig() .then((res) => { if (res) setOidcConfig((prev) => ({ ...prev, ...res, identifier_path: (res.identifier_path as string) || "sub", name_path: (res.name_path as string) || "name", scopes: (res.scopes as string) || "openid email profile", })); }) .catch((err) => { if (!err.message?.includes("No server configured")) { toast.error(t("admin.failedToFetchOidcConfig")); } }), getUserInfo() .then((info) => { if (info) { setCurrentUser({ id: info.userId, username: info.username, is_admin: info.is_admin, is_oidc: info.is_oidc, }); } }) .catch((err) => { if (!err?.message?.includes("No server configured")) { console.warn("Failed to fetch current user info", err); } }), getSessions() .then((data) => setSessions(data.sessions || [])) .catch((err) => { if (!err?.message?.includes("No server configured")) { toast.error(t("admin.failedToFetchSessions")); } }), ]).finally(() => setLoading(false)); }, []); React.useEffect(() => { if (isElectron()) { const serverUrl = (window as { configuredServerUrl?: string }) .configuredServerUrl; if (!serverUrl) { return; } } getRegistrationAllowed() .then((res) => { if (typeof res?.allowed === "boolean") { setAllowRegistration(res.allowed); } }) .catch((err) => { if (!err.message?.includes("No server configured")) { toast.error(t("admin.failedToFetchRegistrationStatus")); } }); }, []); React.useEffect(() => { if (isElectron()) { const serverUrl = (window as { configuredServerUrl?: string }) .configuredServerUrl; if (!serverUrl) { return; } } getPasswordLoginAllowed() .then((res) => { if (typeof res?.allowed === "boolean") { setAllowPasswordLogin(res.allowed); } }) .catch((err) => { if (err.code !== "NO_SERVER_CONFIGURED") { toast.error(t("admin.failedToFetchPasswordLoginStatus")); } }); }, []); React.useEffect(() => { if (isElectron()) { const serverUrl = (window as { configuredServerUrl?: string }) .configuredServerUrl; if (!serverUrl) { return; } } getPasswordResetAllowed() .then((res) => { if (typeof res === "boolean") { setAllowPasswordReset(res); } }) .catch((err) => { if (err.code !== "NO_SERVER_CONFIGURED") { console.warn("Failed to fetch password reset status", err); } }); }, []); const fetchUsers = async () => { if (isElectron()) { const serverUrl = (window as { configuredServerUrl?: string }) .configuredServerUrl; if (!serverUrl) { return; } } setUsersLoading(true); try { const response = await getUserList(); setUsers(response.users); } catch (err) { if (!err.message?.includes("No server configured")) { toast.error(t("admin.failedToFetchUsers")); } } finally { setUsersLoading(false); } }; const handleEditUser = (user: (typeof users)[0]) => { setSelectedUserForEdit(user); setUserEditDialogOpen(true); }; const handleCreateUserSuccess = () => { fetchUsers(); setCreateUserDialogOpen(false); }; const handleEditUserSuccess = () => { fetchUsers(); setUserEditDialogOpen(false); setSelectedUserForEdit(null); }; const fetchSessions = async () => { if (isElectron()) { const serverUrl = (window as { configuredServerUrl?: string }) .configuredServerUrl; if (!serverUrl) { return; } } setSessionsLoading(true); try { const data = await getSessions(); setSessions(data.sessions || []); } catch (err) { if (!err?.message?.includes("No server configured")) { toast.error(t("admin.failedToFetchSessions")); } } finally { setSessionsLoading(false); } }; const handleLinkOIDCUser = (user: { id: string; username: string }) => { setLinkOidcUser(user); setLinkAccountAlertOpen(true); }; const handleLinkSuccess = () => { fetchUsers(); fetchSessions(); }; const handleUnlinkOIDC = async (userId: string, username: string) => { confirmWithToast( t("admin.unlinkOIDCDescription", { username }), async () => { try { const result = await unlinkOIDCFromPasswordAccount(userId); toast.success( result.message || t("admin.unlinkOIDCSuccess", { username }), ); fetchUsers(); fetchSessions(); } catch (error: unknown) { const err = error as { response?: { data?: { error?: string; code?: string } }; }; toast.error( err.response?.data?.error || t("admin.failedToUnlinkOIDC"), ); } }, "destructive", ); }; const topMarginPx = isTopbarOpen ? 74 : 26; const leftMarginPx = sidebarState === "collapsed" ? 26 : 8; const bottomMarginPx = 8; const wrapperStyle: React.CSSProperties = { marginLeft: leftMarginPx, marginRight: rightSidebarOpen ? `calc(var(--right-sidebar-width, ${rightSidebarWidth}px) + 8px)` : 17, marginTop: topMarginPx, marginBottom: bottomMarginPx, height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`, transition: "margin-left 200ms linear, margin-right 200ms linear, margin-top 200ms linear", }; return (