Bernt Christian Egeland hace 7 meses
padre
commit
3038090e42

+ 27 - 0
src/app/(authenticated)/layout.tsx

@@ -7,6 +7,8 @@ import { getLayoutData } from "@/lib/get-layout-data";
 import { getFeatures, isCloudMode } from "@/lib/features";
 import { WhiteLabelCtaProvider } from "@/components/white-label-cta-context";
 import { DateSettingsProvider } from "@/components/date-settings-context";
+import { getCachedMembership } from "@/lib/cached-session";
+import { hasPermission, PermissionAction, PermissionSubject } from "@/lib/permissions";
 
 export default async function DashboardLayout({
   children,
@@ -21,6 +23,29 @@ export default async function DashboardLayout({
   const features = await getFeatures(data.organizationId);
   const showWhiteLabelCta = !isCloudMode() && !features.brandingRemoved;
 
+  // Determine if user can access settings and reports
+  const isOwnerOrAdmin = data.role === "owner" || data.role === "admin" || data.role === "super_admin";
+  let canAccessSettings = isOwnerOrAdmin;
+  let canAccessReports = isOwnerOrAdmin;
+  if (!isOwnerOrAdmin) {
+    const membership = await getCachedMembership(data.userId);
+    // Members without a custom role have full access
+    if (!membership?.roleId) {
+      canAccessSettings = true;
+      canAccessReports = true;
+    } else {
+      const userPermissions = membership?.customRole?.permissions ?? [];
+      canAccessSettings = hasPermission(userPermissions, {
+        action: PermissionAction.READ,
+        subject: PermissionSubject.SETTINGS,
+      });
+      canAccessReports = hasPermission(userPermissions, {
+        action: PermissionAction.READ,
+        subject: PermissionSubject.REPORTS,
+      });
+    }
+  }
+
   return (
     <WhiteLabelCtaProvider show={showWhiteLabelCta}>
     <SidebarProvider
@@ -42,6 +67,8 @@ export default async function DashboardLayout({
           activeOrgId={data.organizationId}
           isSuperAdmin={data.isSuperAdmin}
           features={features}
+          canAccessSettings={canAccessSettings}
+          canAccessReports={canAccessReports}
         />
         <SidebarInset>{children}</SidebarInset>
         <SearchCommand />

+ 17 - 0
src/app/(authenticated)/reports/page.tsx

@@ -5,6 +5,8 @@ import { UpgradePrompt } from "@/components/upgrade-prompt";
 import { getLayoutData } from "@/lib/get-layout-data";
 import { getFeatures } from "@/lib/features";
 import { redirect } from "next/navigation";
+import { getCachedMembership } from "@/lib/cached-session";
+import { hasPermission, PermissionAction, PermissionSubject } from "@/lib/permissions";
 import ReportsClient from "./reports-client";
 
 export default async function ReportsPage() {
@@ -13,6 +15,21 @@ export default async function ReportsPage() {
   if (data.status === "unauthenticated") redirect("/auth/sign-in");
   if (data.status === "no-organization") redirect("/onboarding");
 
+  // Check if user has reports access
+  const isOwnerOrAdmin = data.role === "owner" || data.role === "admin" || data.role === "super_admin";
+  if (!isOwnerOrAdmin) {
+    const membership = await getCachedMembership(data.userId);
+    // Members without a custom role have full access; only restrict if a custom role is assigned
+    if (membership?.roleId) {
+      const userPermissions = membership?.customRole?.permissions ?? [];
+      const canReadReports = hasPermission(userPermissions, {
+        action: PermissionAction.READ,
+        subject: PermissionSubject.REPORTS,
+      });
+      if (!canReadReports) redirect("/");
+    }
+  }
+
   const features = await getFeatures(data.organizationId);
 
   if (!features.reports) {

+ 28 - 17
src/app/(authenticated)/settings/company/company-settings.tsx

@@ -21,6 +21,7 @@ import {
 } from "@/components/ui/dialog";
 import { Building2, ImageIcon, Loader2, Plus, Save, Trash2, Upload } from "lucide-react";
 import { createNewOrganization } from "@/features/team/Actions/createNewOrganization";
+import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 
 export function CompanySettings({ settings, organizationName }: { settings: Record<string, string>; organizationName: string }) {
   const router = useRouter();
@@ -40,7 +41,7 @@ export function CompanySettings({ settings, organizationName }: { settings: Reco
 
   const handleSave = async () => {
     setSaving(true);
-    await Promise.all([
+    const [renameResult, settingsResult] = await Promise.all([
       renameOrganization({ name: workshopName }),
       setSettings({
         [SETTING_KEYS.WORKSHOP_ADDRESS]: workshopAddress,
@@ -50,6 +51,12 @@ export function CompanySettings({ settings, organizationName }: { settings: Reco
       }),
     ]);
     setSaving(false);
+
+    if (!renameResult.success || !settingsResult.success) {
+      toast.error(renameResult.error || settingsResult.error || "Failed to save settings");
+      return;
+    }
+
     router.refresh();
     toast.success("Company settings saved");
   };
@@ -99,6 +106,8 @@ export function CompanySettings({ settings, organizationName }: { settings: Reco
 
   return (
     <div className="space-y-4">
+      <ReadOnlyBanner />
+      <ReadOnlyWrapper>
       <Card className="border-0 shadow-sm">
         <CardHeader className="pb-2">
           <CardTitle className="flex items-center gap-2 text-sm text-muted-foreground">
@@ -218,24 +227,26 @@ export function CompanySettings({ settings, organizationName }: { settings: Reco
             />
           </div>
 
-          <Separator />
-
-          <div className="flex items-center justify-between">
-            <Button size="sm" onClick={handleSave} disabled={saving}>
-              {saving ? (
-                <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
-              ) : (
-                <Save className="mr-1.5 h-3.5 w-3.5" />
-              )}
-              Save Company Details
-            </Button>
-            <Button variant="outline" size="sm" onClick={() => setShowCreateOrg(true)}>
-              <Plus className="mr-1.5 h-3.5 w-3.5" />
-              Add New Company
-            </Button>
-          </div>
+          <SaveButton>
+            <Separator />
+            <div className="flex items-center justify-between">
+              <Button size="sm" onClick={handleSave} disabled={saving}>
+                {saving ? (
+                  <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
+                ) : (
+                  <Save className="mr-1.5 h-3.5 w-3.5" />
+                )}
+                Save Company Details
+              </Button>
+              <Button variant="outline" size="sm" onClick={() => setShowCreateOrg(true)}>
+                <Plus className="mr-1.5 h-3.5 w-3.5" />
+                Add New Company
+              </Button>
+            </div>
+          </SaveButton>
         </CardContent>
       </Card>
+      </ReadOnlyWrapper>
 
       <Dialog open={showCreateOrg} onOpenChange={setShowCreateOrg}>
         <DialogContent className="sm:max-w-md">

+ 19 - 11
src/app/(authenticated)/settings/currency/currency-settings.tsx

@@ -20,6 +20,7 @@ import {
 import { Switch } from "@/components/ui/switch";
 import { Coins, Loader2, Ruler, Save } from "lucide-react";
 import { formatCurrency } from "@/lib/format";
+import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 
 const CURRENCIES = [
   { code: "USD", name: "US Dollar" },
@@ -79,6 +80,7 @@ export function CurrencySettings({ settings }: { settings: Record<string, string
 
   return (
     <div className="space-y-6">
+      <ReadOnlyBanner />
       <Card className="border-0 shadow-sm">
         <CardHeader className="flex flex-row items-center gap-3 pb-4">
           <Coins className="h-5 w-5 text-muted-foreground" />
@@ -89,6 +91,8 @@ export function CurrencySettings({ settings }: { settings: Record<string, string
             Default currency and tax rate used across invoices and service records.
           </p>
 
+          <ReadOnlyWrapper>
+          <div className="space-y-6">
           <div className="space-y-2">
             <Label>Currency</Label>
             <Select value={currencyCode} onValueChange={setCurrencyCode}>
@@ -178,18 +182,22 @@ export function CurrencySettings({ settings }: { settings: Record<string, string
             </div>
           </div>
 
-          <Separator />
-
-          <div className="flex items-center gap-3">
-            <Button onClick={handleSave} disabled={saving}>
-              {saving ? (
-                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
-              ) : (
-                <Save className="mr-2 h-4 w-4" />
-              )}
-              Save Settings
-            </Button>
           </div>
+          </ReadOnlyWrapper>
+
+          <SaveButton>
+            <Separator />
+            <div className="flex items-center gap-3">
+              <Button onClick={handleSave} disabled={saving}>
+                {saving ? (
+                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                ) : (
+                  <Save className="mr-2 h-4 w-4" />
+                )}
+                Save Settings
+              </Button>
+            </div>
+          </SaveButton>
         </CardContent>
       </Card>
     </div>

+ 4 - 0
src/app/(authenticated)/settings/data/data-settings.tsx

@@ -20,6 +20,7 @@ import {
   Loader2,
   Upload,
 } from 'lucide-react'
+import { ReadOnlyBanner, ReadOnlyWrapper } from '../read-only-guard'
 
 interface ExportOptions {
   settings: boolean
@@ -232,6 +233,7 @@ export function DataSettings() {
 
   return (
     <div className="space-y-6">
+      <ReadOnlyBanner />
       <div>
         <h2 className="text-lg font-semibold">Data Management</h2>
         <p className="text-sm text-muted-foreground">
@@ -239,6 +241,7 @@ export function DataSettings() {
         </p>
       </div>
 
+      <ReadOnlyWrapper>
       <div className="grid gap-6 lg:grid-cols-2">
         {/* Export Card */}
         <Card className="border-0 shadow-sm">
@@ -383,6 +386,7 @@ export function DataSettings() {
           </div>
         </CardContent>
       </Card>
+      </ReadOnlyWrapper>
 
       {/* LubeLog Import Dialog */}
       <Dialog open={lubelogOpen} onOpenChange={(open) => {

+ 13 - 6
src/app/(authenticated)/settings/invoice-template/template-settings.tsx

@@ -16,6 +16,7 @@ import { toast } from "sonner";
 import { setSetting } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { Loader2, Palette } from "lucide-react";
+import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 
 interface TemplateValues {
   primaryColor: string;
@@ -65,6 +66,7 @@ export function TemplateSettings({
 
   return (
     <div className="space-y-6">
+      <ReadOnlyBanner />
       <div>
         <h2 className="text-lg font-semibold">Invoice Template</h2>
         <p className="text-sm text-muted-foreground">
@@ -72,6 +74,7 @@ export function TemplateSettings({
         </p>
       </div>
 
+      <ReadOnlyWrapper>
       <div className="grid gap-6 lg:grid-cols-2">
         {/* Color Settings */}
         <Card className="border-0 shadow-sm">
@@ -286,12 +289,16 @@ export function TemplateSettings({
         </CardContent>
       </Card>
 
-      <div className="flex justify-end">
-        <Button onClick={handleSave} disabled={saving}>
-          {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
-          Save Template Settings
-        </Button>
-      </div>
+      </ReadOnlyWrapper>
+
+      <SaveButton>
+        <div className="flex justify-end">
+          <Button onClick={handleSave} disabled={saving}>
+            {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+            Save Template Settings
+          </Button>
+        </div>
+      </SaveButton>
     </div>
   );
 }

+ 17 - 12
src/app/(authenticated)/settings/invoice/invoice-settings.tsx

@@ -13,6 +13,7 @@ import { toast } from "sonner";
 import { setSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { FileText, Loader2, Save } from "lucide-react";
+import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 
 export function InvoiceSettings({ settings }: { settings: Record<string, string> }) {
   const router = useRouter();
@@ -46,6 +47,8 @@ export function InvoiceSettings({ settings }: { settings: Record<string, string>
 
   return (
     <div className="space-y-6">
+      <ReadOnlyBanner />
+      <ReadOnlyWrapper>
       <Card className="border-0 shadow-sm">
         <CardHeader className="flex flex-row items-center gap-3 pb-4">
           <FileText className="h-5 w-5 text-muted-foreground" />
@@ -172,20 +175,22 @@ export function InvoiceSettings({ settings }: { settings: Record<string, string>
             </p>
           </div>
 
-          <Separator />
-
-          <div className="flex items-center gap-3">
-            <Button onClick={handleSave} disabled={saving}>
-              {saving ? (
-                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
-              ) : (
-                <Save className="mr-2 h-4 w-4" />
-              )}
-              Save Invoice Settings
-            </Button>
-          </div>
+          <SaveButton>
+            <Separator />
+            <div className="flex items-center gap-3">
+              <Button onClick={handleSave} disabled={saving}>
+                {saving ? (
+                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                ) : (
+                  <Save className="mr-2 h-4 w-4" />
+                )}
+                Save Invoice Settings
+              </Button>
+            </div>
+          </SaveButton>
         </CardContent>
       </Card>
+      </ReadOnlyWrapper>
     </div>
   );
 }

+ 44 - 15
src/app/(authenticated)/settings/layout.tsx

@@ -1,8 +1,11 @@
 import { PageHeader } from "@/components/page-header";
 import { SettingsNav } from "./settings-nav";
+import { SettingsPermissionProvider } from "./settings-permission-context";
 import { getLayoutData } from "@/lib/get-layout-data";
 import { getFeatures, isCloudMode } from "@/lib/features";
 import { redirect } from "next/navigation";
+import { getCachedMembership } from "@/lib/cached-session";
+import { hasPermission, PermissionAction, PermissionSubject } from "@/lib/permissions";
 
 export default async function SettingsLayout({
   children,
@@ -14,25 +17,51 @@ export default async function SettingsLayout({
   if (data.status === "unauthenticated") redirect("/auth/sign-in");
   if (data.status === "no-organization") redirect("/onboarding");
 
+  // Check if user has settings access and edit permission
+  const isOwnerOrAdmin = data.role === "owner" || data.role === "admin" || data.role === "super_admin";
+  let canReadSettings = isOwnerOrAdmin;
+  let canEditSettings = isOwnerOrAdmin;
+  if (!isOwnerOrAdmin) {
+    const membership = await getCachedMembership(data.userId);
+    // Members without a custom role have full access
+    if (!membership?.roleId) {
+      canReadSettings = true;
+      canEditSettings = true;
+    } else {
+      const userPermissions = membership?.customRole?.permissions ?? [];
+      canReadSettings = hasPermission(userPermissions, {
+        action: PermissionAction.READ,
+        subject: PermissionSubject.SETTINGS,
+      });
+      canEditSettings = hasPermission(userPermissions, {
+        action: PermissionAction.UPDATE,
+        subject: PermissionSubject.SETTINGS,
+      });
+      if (!canReadSettings) redirect("/");
+    }
+  }
+
   const features = await getFeatures(data.organizationId);
 
   return (
-    <div className="flex h-svh flex-col">
-      <PageHeader />
-      <div className="flex min-h-0 flex-1 flex-col gap-4 p-4 pt-0">
-        <div className="shrink-0">
-          <h1 className="text-3xl font-bold tracking-tight">Settings</h1>
-          <p className="mt-1 text-muted-foreground">
-            Manage your workshop, invoices, and preferences
-          </p>
-        </div>
-        <div className="flex min-h-0 flex-1 flex-col gap-6 md:flex-row">
-          <aside className="w-full shrink-0 overflow-y-auto md:w-56 lg:w-64">
-            <SettingsNav features={features} isCloud={isCloudMode()} />
-          </aside>
-          <div className="min-w-0 flex-1 overflow-y-auto pb-8">{children}</div>
+    <SettingsPermissionProvider canEdit={canEditSettings}>
+      <div className="flex h-svh flex-col">
+        <PageHeader />
+        <div className="flex min-h-0 flex-1 flex-col gap-4 p-4 pt-0">
+          <div className="shrink-0">
+            <h1 className="text-3xl font-bold tracking-tight">Settings</h1>
+            <p className="mt-1 text-muted-foreground">
+              Manage your workshop, invoices, and preferences
+            </p>
+          </div>
+          <div className="flex min-h-0 flex-1 flex-col gap-6 md:flex-row">
+            <aside className="w-full shrink-0 overflow-y-auto md:w-56 lg:w-64">
+              <SettingsNav features={features} isCloud={isCloudMode()} />
+            </aside>
+            <div className="min-w-0 flex-1 overflow-y-auto pb-8">{children}</div>
+          </div>
         </div>
       </div>
-    </div>
+    </SettingsPermissionProvider>
   );
 }

+ 17 - 12
src/app/(authenticated)/settings/payment/payment-settings.tsx

@@ -12,6 +12,7 @@ import { toast } from "sonner";
 import { setSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { Banknote, CreditCard, Loader2, Save, Copy, Check } from "lucide-react";
+import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 
 export function PaymentSettings({ settings }: { settings: Record<string, string> }) {
   const router = useRouter();
@@ -78,6 +79,8 @@ export function PaymentSettings({ settings }: { settings: Record<string, string>
 
   return (
     <div className="space-y-6">
+      <ReadOnlyBanner />
+      <ReadOnlyWrapper>
       <Card className="border-0 shadow-sm">
         <CardHeader className="flex flex-row items-center gap-3 pb-4">
           <Banknote className="h-5 w-5 text-muted-foreground" />
@@ -272,20 +275,22 @@ export function PaymentSettings({ settings }: { settings: Record<string, string>
             )}
           </div>
 
-          <Separator />
-
-          <div className="flex items-center gap-3">
-            <Button onClick={handleSave} disabled={saving}>
-              {saving ? (
-                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
-              ) : (
-                <Save className="mr-2 h-4 w-4" />
-              )}
-              Save Payment Settings
-            </Button>
-          </div>
+          <SaveButton>
+            <Separator />
+            <div className="flex items-center gap-3">
+              <Button onClick={handleSave} disabled={saving}>
+                {saving ? (
+                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                ) : (
+                  <Save className="mr-2 h-4 w-4" />
+                )}
+                Save Payment Settings
+              </Button>
+            </div>
+          </SaveButton>
         </CardContent>
       </Card>
+      </ReadOnlyWrapper>
     </div>
   );
 }

+ 37 - 0
src/app/(authenticated)/settings/read-only-guard.tsx

@@ -0,0 +1,37 @@
+"use client";
+
+import { useSettingsPermission } from "./settings-permission-context";
+import { Badge } from "@/components/ui/badge";
+import { Lock } from "lucide-react";
+
+export function ReadOnlyBanner() {
+  const { canEdit } = useSettingsPermission();
+  if (canEdit) return null;
+
+  return (
+    <div className="mb-4 flex items-center gap-2 rounded-lg border border-amber-500/20 bg-amber-500/5 p-3">
+      <Lock className="h-4 w-4 text-amber-600 shrink-0" />
+      <p className="text-sm text-amber-600">
+        You have view-only access to settings. Contact an admin to make changes.
+      </p>
+    </div>
+  );
+}
+
+export function SaveButton({ children }: { children: React.ReactNode }) {
+  const { canEdit } = useSettingsPermission();
+  if (!canEdit) return null;
+  return <>{children}</>;
+}
+
+export function ReadOnlyWrapper({ children }: { children: React.ReactNode }) {
+  const { canEdit } = useSettingsPermission();
+
+  if (canEdit) return <>{children}</>;
+
+  return (
+    <div className="pointer-events-none opacity-60 select-none">
+      {children}
+    </div>
+  );
+}

+ 23 - 0
src/app/(authenticated)/settings/settings-permission-context.tsx

@@ -0,0 +1,23 @@
+"use client";
+
+import { createContext, useContext } from "react";
+
+const SettingsPermissionContext = createContext({ canEdit: true });
+
+export function SettingsPermissionProvider({
+  canEdit,
+  children,
+}: {
+  canEdit: boolean;
+  children: React.ReactNode;
+}) {
+  return (
+    <SettingsPermissionContext.Provider value={{ canEdit }}>
+      {children}
+    </SettingsPermissionContext.Provider>
+  );
+}
+
+export function useSettingsPermission() {
+  return useContext(SettingsPermissionContext);
+}

+ 17 - 12
src/app/(authenticated)/settings/workshop/workshop-settings.tsx

@@ -11,6 +11,7 @@ import { toast } from "sonner";
 import { setSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { Loader2, Save, Wrench } from "lucide-react";
+import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 
 export function WorkshopSettings({ settings }: { settings: Record<string, string> }) {
   const router = useRouter();
@@ -40,6 +41,8 @@ export function WorkshopSettings({ settings }: { settings: Record<string, string
 
   return (
     <div className="space-y-6">
+      <ReadOnlyBanner />
+      <ReadOnlyWrapper>
       <Card className="border-0 shadow-sm">
         <CardHeader className="flex flex-row items-center gap-3 pb-4">
           <Wrench className="h-5 w-5 text-muted-foreground" />
@@ -82,20 +85,22 @@ export function WorkshopSettings({ settings }: { settings: Record<string, string
             />
           </div>
 
-          <Separator />
-
-          <div className="flex items-center gap-3">
-            <Button onClick={handleSave} disabled={saving}>
-              {saving ? (
-                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
-              ) : (
-                <Save className="mr-2 h-4 w-4" />
-              )}
-              Save Workshop Settings
-            </Button>
-          </div>
+          <SaveButton>
+            <Separator />
+            <div className="flex items-center gap-3">
+              <Button onClick={handleSave} disabled={saving}>
+                {saving ? (
+                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                ) : (
+                  <Save className="mr-2 h-4 w-4" />
+                )}
+                Save Workshop Settings
+              </Button>
+            </div>
+          </SaveButton>
         </CardContent>
       </Card>
+      </ReadOnlyWrapper>
     </div>
   );
 }

+ 14 - 8
src/components/app-sidebar.tsx

@@ -77,6 +77,8 @@ export function AppSidebar({
   activeOrgId,
   isSuperAdmin,
   features,
+  canAccessSettings = true,
+  canAccessReports = true,
   ...props
 }: React.ComponentProps<typeof Sidebar> & {
   companyLogo?: string
@@ -84,6 +86,8 @@ export function AppSidebar({
   activeOrgId?: string
   isSuperAdmin?: boolean
   features?: PlanFeatures
+  canAccessSettings?: boolean
+  canAccessReports?: boolean
 }) {
   const pathname = usePathname()
   const router = useRouter()
@@ -95,10 +99,10 @@ export function AppSidebar({
 
   const navItems = [
     ...baseNavItems.filter((item) => {
-      if (item.title === 'Reports' && features?.reports === false) return false
+      if (item.title === 'Reports' && (features?.reports === false || !canAccessReports)) return false
       return true
     }),
-    { title: 'Settings', url: '/settings', icon: Settings },
+    ...(canAccessSettings ? [{ title: 'Settings', url: '/settings', icon: Settings }] : []),
   ]
 
   const activeOrg = organizations.find((o) => o.id === activeOrgId) || organizations[0]
@@ -264,12 +268,14 @@ export function AppSidebar({
                 align="end"
                 sideOffset={4}
               >
-                <DropdownMenuItem asChild>
-                  <Link href="/settings">
-                    <Settings className="mr-2 size-4" />
-                    Settings
-                  </Link>
-                </DropdownMenuItem>
+                {canAccessSettings && (
+                  <DropdownMenuItem asChild>
+                    <Link href="/settings">
+                      <Settings className="mr-2 size-4" />
+                      Settings
+                    </Link>
+                  </DropdownMenuItem>
+                )}
                 <DropdownMenuItem onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
                   {theme === 'dark' ? (
                     <Sun className="mr-2 size-4" />

+ 12 - 6
src/features/email/Components/EmailSettingsForm.tsx

@@ -20,6 +20,7 @@ import {
   setEmailSettings,
   testOrgEmailConnection,
 } from "../Actions/emailSettingsActions";
+import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "@/app/(authenticated)/settings/read-only-guard";
 
 type EmailProviderType =
   | "smtp"
@@ -234,6 +235,8 @@ export function EmailSettingsForm({
 
   return (
     <div className="space-y-6">
+      <ReadOnlyBanner />
+      <ReadOnlyWrapper>
       <Card>
         <CardHeader>
           <CardTitle>Email Provider</CardTitle>
@@ -769,12 +772,15 @@ export function EmailSettingsForm({
         </CardContent>
       </Card>
 
-      <div className="flex justify-end">
-        <Button onClick={handleSave} disabled={isPending}>
-          {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
-          Save Settings
-        </Button>
-      </div>
+      <SaveButton>
+        <div className="flex justify-end">
+          <Button onClick={handleSave} disabled={isPending}>
+            {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+            Save Settings
+          </Button>
+        </div>
+      </SaveButton>
+      </ReadOnlyWrapper>
     </div>
   );
 }

+ 3 - 1
src/lib/with-auth.ts

@@ -50,8 +50,10 @@ export async function withAuth<T>(
     if (!isSuperAdmin && options.requiredPermissions && options.requiredPermissions.length > 0) {
       const isOwnerOrAdmin = membership?.role === "owner" || membership?.role === "admin";
       const roleIsAdmin = membership?.customRole?.isAdmin === true;
+      // Members without a custom role have full access (no restrictions)
+      const hasNoCustomRole = !membership?.roleId;
 
-      if (!isOwnerOrAdmin && !roleIsAdmin) {
+      if (!isOwnerOrAdmin && !roleIsAdmin && !hasNoCustomRole) {
         const userPermissions = membership?.customRole?.permissions ?? [];
         if (!hasAllPermissions(userPermissions, options.requiredPermissions)) {
           return { success: false, error: "Insufficient permissions" };