fix: wire orphaned admin policy gates and surface OAuth scope settings

This commit is contained in:
Linus Rath
2026-05-18 16:47:20 +02:00
parent b2d24670ff
commit dcd2f4b079
5 changed files with 22 additions and 5 deletions
+15 -1
View File
@@ -2,7 +2,7 @@
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { useTranslations } from "next-intl";
import { ArrowLeft, Users } from "lucide-react";
import { ArrowLeft, Users, AlertTriangle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { useConfirmDialog } from "@/hooks/use-confirm-dialog";
@@ -19,6 +19,7 @@ import { AppTopBannerSlot } from "@/components/plugins/app-top-banner-slot";
import { useContactStore, getContactDisplayName } from "@/stores/contact-store";
import { useAuthStore, redirectToLogin } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store";
import { usePolicyStore } from "@/stores/policy-store";
import { toast } from "@/stores/toast-store";
import { cn, generateUUID } from "@/lib/utils";
import { NavigationRail } from "@/components/layout/navigation-rail";
@@ -43,6 +44,7 @@ type View =
export default function ContactsPage() {
const t = useTranslations("contacts");
const contactsEnabled = usePolicyStore((s) => s.isFeatureEnabled('contactsEnabled'));
const { client, isAuthenticated, logout, checkAuth, isLoading: authLoading } = useAuthStore();
const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps();
const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client);
@@ -641,6 +643,18 @@ export default function ContactsPage() {
}
};
if (!contactsEnabled) {
return (
<div className="flex h-dvh items-center justify-center bg-background p-6">
<div className="max-w-lg text-center space-y-3">
<AlertTriangle className="w-10 h-10 text-yellow-500 mx-auto" />
<p className="text-sm font-medium">Contacts feature is disabled by your administrator</p>
<p className="text-xs text-muted-foreground">Please contact your administrator if you need access.</p>
</div>
</div>
);
}
const showListPanel = !isMobile || view === "list";
const showRightPanel = !isMobile || view !== "list";
+1 -1
View File
@@ -590,7 +590,7 @@ export default function SettingsPage() {
// Apps
...(supportsCalendar ? [{ id: 'calendar' as Tab, label: t('tabs.calendar'), icon: tabIcons.calendar, group: 'apps' as TabGroup }] : []),
{ id: 'contacts', label: t('tabs.contacts'), icon: tabIcons.contacts, group: 'apps' },
...(isFeatureEnabled('contactsEnabled') ? [{ id: 'contacts' as Tab, label: t('tabs.contacts'), icon: tabIcons.contacts, group: 'apps' as TabGroup }] : []),
...(supportsFiles && isFeatureEnabled('filesEnabled') ? [{ id: 'files' as Tab, label: t('tabs.files'), icon: tabIcons.files, group: 'apps' as TabGroup }] : []),
...(isFeatureEnabled('sidebarAppsEnabled') ? [{ id: 'sidebar_apps' as Tab, label: t('tabs.sidebar_apps'), icon: tabIcons.sidebar_apps, group: 'apps' as TabGroup }] : []),
+2
View File
@@ -273,6 +273,8 @@ export function AuthTab() {
<Text label="OAuth Client ID" configKey="oauthClientId" value={currentValue('oauthClientId') as string} source={config.oauthClientId?.source} onChange={handleChange} onRevert={handleRevert} />
<Text label="OAuth Client Secret" configKey="oauthClientSecret" value={currentValue('oauthClientSecret') as string} source={config.oauthClientSecret?.source} onChange={handleChange} onRevert={handleRevert} type="password" placeholder={config.oauthClientSecret?.hasValue ? '•••••••• (saved — type to replace)' : undefined} />
<Text label="OAuth Issuer URL" configKey="oauthIssuerUrl" value={currentValue('oauthIssuerUrl') as string} source={config.oauthIssuerUrl?.source} onChange={handleChange} onRevert={handleRevert} placeholder="https://auth.example.com" />
<Text label="OAuth Scopes" description="Space-separated scopes that replace the defaults. Leave blank to use the built-in scope list." configKey="oauthScopes" value={currentValue('oauthScopes') as string} source={config.oauthScopes?.source} onChange={handleChange} onRevert={handleRevert} placeholder="openid email offline_access" />
<Text label="OAuth Extra Scopes" description="Additional space-separated scopes appended to the defaults." configKey="oauthExtraScopes" value={currentValue('oauthExtraScopes') as string} source={config.oauthExtraScopes?.source} onChange={handleChange} onRevert={handleRevert} placeholder="urn:ietf:params:oauth:..." />
</Section>
<Section title="Single Sign-On">
+2 -1
View File
@@ -175,6 +175,7 @@ export function NavigationRail({
const showRailAccountList = useSettingsStore((s) => s.showRailAccountList);
const sidebarAppsEnabled = usePolicyStore((s) => s.isFeatureEnabled('sidebarAppsEnabled'));
const filesEnabled = usePolicyStore((s) => s.isFeatureEnabled('filesEnabled'));
const contactsEnabled = usePolicyStore((s) => s.isFeatureEnabled('contactsEnabled'));
const visibleSidebarApps = sidebarAppsEnabled ? sidebarApps : [];
const inboxUnread = mailboxes.find(m => m.role === "inbox")?.unreadEmails || 0;
const [isStalwartAdmin, setIsStalwartAdmin] = useState(false);
@@ -254,7 +255,7 @@ export function NavigationRail({
const navItems: NavItem[] = [
{ id: "mail", icon: Mail, labelKey: "mail", href: "/", badge: inboxUnread },
{ id: "calendar", icon: Calendar, labelKey: "calendar", href: "/calendar", hidden: !supportsCalendar },
{ id: "contacts", icon: BookUser, labelKey: "contacts", href: "/contacts", hidden: !supportsContacts },
{ id: "contacts", icon: BookUser, labelKey: "contacts", href: "/contacts", hidden: !supportsContacts || !contactsEnabled },
{ id: "files", icon: HardDrive, labelKey: "files", href: "/files", hidden: !supportsFiles || !filesEnabled },
];
@@ -12,7 +12,7 @@ import { useContactStore } from '@/stores/contact-store';
export function ContentSendersSettings() {
const t = useTranslations('settings.email_behavior');
const [showTrustedModal, setShowTrustedModal] = useState(false);
const { isSettingLocked, isSettingHidden } = usePolicyStore();
const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore();
const {
externalContentPolicy,
@@ -32,7 +32,7 @@ export function ContentSendersSettings() {
return (
<SettingsSection title={t('title')} description={t('description')}>
{!isSettingHidden('externalContentPolicy') && (
{isFeatureEnabled('externalContentEnabled') && !isSettingHidden('externalContentPolicy') && (
<SettingItem label={t('external_content.label')} description={t('external_content.description')} locked={isSettingLocked('externalContentPolicy')}>
<Select
value={externalContentPolicy}