diff --git a/dashboard/src/lib/RoleChip.svelte b/dashboard/src/lib/RoleChip.svelte
index 606e08b..e827042 100644
--- a/dashboard/src/lib/RoleChip.svelte
+++ b/dashboard/src/lib/RoleChip.svelte
@@ -1,15 +1,24 @@
-{role}
+{label}
diff --git a/dashboard/src/lib/api.ts b/dashboard/src/lib/api.ts
index 64a1d75..8723ce6 100644
--- a/dashboard/src/lib/api.ts
+++ b/dashboard/src/lib/api.ts
@@ -296,6 +296,21 @@ export interface PatchAdminInput {
email?: string | null;
}
+export interface AppMemberDto {
+ user_id: string;
+ username: string;
+ email: string | null;
+ instance_role: InstanceRole;
+ is_active: boolean;
+ role: AppRole;
+ created_at: string;
+}
+
+export interface GrantAppMemberInput {
+ user_id: string;
+ role: AppRole;
+}
+
export interface ApiKeyDto {
id: string;
prefix: string;
@@ -479,6 +494,28 @@ export const api = {
)
},
+ appMembers: {
+ list: (idOrSlug: string) =>
+ adminRequest(
+ `/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/members`
+ ),
+ add: (idOrSlug: string, input: GrantAppMemberInput) =>
+ adminRequest(
+ `/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/members`,
+ { method: 'POST', body: JSON.stringify(input) }
+ ),
+ setRole: (idOrSlug: string, userId: string, role: AppRole) =>
+ adminRequest(
+ `/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/members/${userId}`,
+ { method: 'PATCH', body: JSON.stringify({ role }) }
+ ),
+ remove: (idOrSlug: string, userId: string) =>
+ adminRequest(
+ `/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/members/${userId}`,
+ { method: 'DELETE' }
+ )
+ },
+
execute: async (
id: string,
body: unknown,
diff --git a/dashboard/src/routes/apps/[slug]/+page.svelte b/dashboard/src/routes/apps/[slug]/+page.svelte
index c18edeb..3f6e583 100644
--- a/dashboard/src/routes/apps/[slug]/+page.svelte
+++ b/dashboard/src/routes/apps/[slug]/+page.svelte
@@ -5,26 +5,35 @@
import {
api,
ApiError,
+ type AdminDto,
type App,
type AppDomain,
+ type AppMemberDto,
+ type AppRole,
type Script
} from '$lib/api';
import CodeEditor from '$lib/CodeEditor.svelte';
import ConfirmModal from '$lib/ConfirmModal.svelte';
+ import ActionMenu from '$lib/ActionMenu.svelte';
+ import RoleChip from '$lib/RoleChip.svelte';
const SAMPLE_SOURCE =
'#{\n statusCode: 200,\n body: #{ ok: true, echo: ctx.request.body }\n}';
- type Tab = 'scripts' | 'domains' | 'settings';
+ type Tab = 'scripts' | 'domains' | 'members' | 'settings';
let slug = $derived(page.params.slug ?? '');
let app = $state(null);
+ let myRole = $state(null);
let loadError = $state(null);
let loading = $state(true);
let activeTab = $state('scripts');
let scripts = $state