diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx
index 445d87a2a..cba2295c6 100644
--- a/surfsense_web/components/settings/image-model-manager.tsx
+++ b/surfsense_web/components/settings/image-model-manager.tsx
@@ -19,7 +19,7 @@ import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
-import { membersAtom } from "@/atoms/members/members-query.atoms";
+import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import {
createImageGenConfigMutationAtom,
deleteImageGenConfigMutationAtom,
@@ -148,6 +148,22 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
return map;
}, [members]);
+ // Permissions
+ const { data: access } = useAtomValue(myAccessAtom);
+ const canCreate = useMemo(() => {
+ if (!access) return false;
+ if (access.is_owner) return true;
+ return access.permissions?.includes("image_generations:create") ?? false;
+ }, [access]);
+ const canDelete = useMemo(() => {
+ if (!access) return false;
+ if (access.is_owner) return true;
+ return access.permissions?.includes("image_generations:delete") ?? false;
+ }, [access]);
+ // Backend uses image_generations:create for update as well
+ const canUpdate = canCreate;
+ const isReadOnly = !canCreate && !canDelete;
+
// Local state
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingConfig, setEditingConfig] = useState(null);
@@ -344,6 +360,34 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
))}
+ {/* Read-only / Limited permissions notice */}
+ {access && !isLoading && isReadOnly && (
+
+
+
+
+ You have read-only access to image generation
+ configurations. Contact a space owner to request additional permissions.
+
+
+
+ )}
+ {access && !isLoading && !isReadOnly && (!canCreate || !canDelete) && (
+
+
+
+
+ You can{" "}
+ {[canCreate && "create and edit", canDelete && "delete"]
+ .filter(Boolean)
+ .join(" and ")}{" "}
+ image model configurations
+ {!canDelete && ", but cannot delete them"}.
+
+
+
+ )}
+
{/* Global info */}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (
@@ -530,12 +574,14 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
Your Image Models
-
- Add Image Model
-
+ {canCreate && (
+
+ Add Image Model
+
+ )}
{(userConfigs?.length ?? 0) === 0 ? (
@@ -546,12 +592,16 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
No Image Models Yet
- Add your own image generation model (DALL-E 3, GPT Image 1, etc.)
+ {canCreate
+ ? "Add your own image generation model (DALL-E 3, GPT Image 1, etc.)"
+ : "No image models have been added to this space yet. Contact a space owner to add one."}
-
-
- Add First Image Model
-
+ {canCreate && (
+
+
+ Add First Image Model
+
+ )}
) : (
@@ -586,38 +636,44 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)}
+ {(canUpdate || canDelete) && (
-
-
-
- openEditDialog(config)}
- className="h-7 w-7 text-muted-foreground hover:text-foreground"
- >
-
-
-
- Edit
-
-
-
-
-
- setConfigToDelete(config)}
- className="h-7 w-7 text-muted-foreground hover:text-destructive"
- >
-
-
-
- Delete
-
-
+ {canUpdate && (
+
+
+
+ openEditDialog(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-foreground"
+ >
+
+
+
+ Edit
+
+
+ )}
+ {canDelete && (
+
+
+
+ setConfigToDelete(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
+ >
+
+
+
+ Delete
+
+
+ )}
+ )}
{/* Provider + Model */}
diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx
index 238286a96..1666fae20 100644
--- a/surfsense_web/components/settings/model-config-manager.tsx
+++ b/surfsense_web/components/settings/model-config-manager.tsx
@@ -15,7 +15,7 @@ import {
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import { useCallback, useMemo, useState } from "react";
-import { membersAtom } from "@/atoms/members/members-query.atoms";
+import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms";
import {
createNewLLMConfigMutationAtom,
deleteNewLLMConfigMutationAtom,
@@ -120,6 +120,25 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
return map;
}, [members]);
+ // Permissions
+ const { data: access } = useAtomValue(myAccessAtom);
+ const canCreate = useMemo(() => {
+ if (!access) return false;
+ if (access.is_owner) return true;
+ return access.permissions?.includes("llm_configs:create") ?? false;
+ }, [access]);
+ const canUpdate = useMemo(() => {
+ if (!access) return false;
+ if (access.is_owner) return true;
+ return access.permissions?.includes("llm_configs:update") ?? false;
+ }, [access]);
+ const canDelete = useMemo(() => {
+ if (!access) return false;
+ if (access.is_owner) return true;
+ return access.permissions?.includes("llm_configs:delete") ?? false;
+ }, [access]);
+ const isReadOnly = !canCreate && !canUpdate && !canDelete;
+
// Local state
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingConfig, setEditingConfig] = useState(null);
@@ -187,13 +206,15 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
Refresh
-
- Add Configuration
-
+ {canCreate && (
+
+ Add Configuration
+
+ )}
{/* Fetch Error Alert */}
@@ -215,6 +236,34 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
)}
+ {/* Read-only / Limited permissions notice */}
+ {access && !isLoading && isReadOnly && (
+
+
+
+
+ You have read-only access to LLM configurations.
+ Contact a space owner to request additional permissions.
+
+
+
+ )}
+ {access && !isLoading && !isReadOnly && (!canCreate || !canUpdate || !canDelete) && (
+
+
+
+
+ You can{" "}
+ {[canCreate && "create", canUpdate && "edit", canDelete && "delete"]
+ .filter(Boolean)
+ .join(" and ")}{" "}
+ configurations
+ {!canDelete && ", but cannot delete them"}.
+
+
+
+ )}
+
{/* Global Configs Info */}
{globalConfigs.length > 0 && (
@@ -279,17 +328,21 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
No Configurations Yet
- Create your first AI configuration to customize how your agent responds
+ {canCreate
+ ? "Create your first AI configuration to customize how your agent responds"
+ : "No AI configurations have been added to this space yet. Contact a space owner to add one."}
-
-
- Create First Configuration
-
+ {canCreate && (
+
+
+ Create First Configuration
+
+ )}
@@ -325,38 +378,44 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
)}
+ {(canUpdate || canDelete) && (
-
-
-
- openEditDialog(config)}
- className="h-7 w-7 text-muted-foreground hover:text-foreground"
- >
-
-
-
- Edit
-
-
-
-
-
- setConfigToDelete(config)}
- className="h-7 w-7 text-muted-foreground hover:text-destructive"
- >
-
-
-
- Delete
-
-
+ {canUpdate && (
+
+
+
+ openEditDialog(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-foreground"
+ >
+
+
+
+ Edit
+
+
+ )}
+ {canDelete && (
+
+
+
+ setConfigToDelete(config)}
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
+ >
+
+
+
+ Delete
+
+
+ )}
+ )}
{/* Provider + Model */}