diff --git a/src/App.tsx b/src/App.tsx index 02e17f0..e8e2e22 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1523,6 +1523,15 @@ function App() { return () => window.removeEventListener(authExpiredEventName, handleAuthExpired); }, [navigate]); + useEffect(() => { + const handleAdminPermissionsChange = () => { + setCurrentAdmin(readCurrentAdmin()); + }; + + window.addEventListener('studio-admin-app-permissions-change', handleAdminPermissionsChange); + return () => window.removeEventListener('studio-admin-app-permissions-change', handleAdminPermissionsChange); + }, []); + useEffect(() => { if (location.pathname === '/') navigate('/dashboard', { replace: true }); }, [location.pathname, navigate]); diff --git a/src/lib/admin-app-permissions.ts b/src/lib/admin-app-permissions.ts index 642afee..cf03698 100644 --- a/src/lib/admin-app-permissions.ts +++ b/src/lib/admin-app-permissions.ts @@ -1,14 +1,11 @@ -export const adminAppPermissionsStorageKey = 'studio-admin-app-permissions'; - export type CurrentAdminLike = { id?: string; email?: string; name?: string; role?: string; + appPermissions?: string[]; }; -type PermissionMap = Record; - export function readCurrentAdmin(): CurrentAdminLike | null { try { const raw = window.localStorage.getItem('studio-admin'); @@ -20,44 +17,20 @@ export function readCurrentAdmin(): CurrentAdminLike | null { } } -export function getAdminPermissionKey(admin: CurrentAdminLike) { - return admin.id || admin.email || ''; -} - export function isOwnerAdmin(admin?: CurrentAdminLike | null) { return admin?.role === 'OWNER'; } -export function getAdminAppPermissionMap(): PermissionMap { - try { - const raw = window.localStorage.getItem(adminAppPermissionsStorageKey); - if (!raw) return {}; - const parsed = JSON.parse(raw) as PermissionMap; - return parsed && typeof parsed === 'object' ? parsed : {}; - } catch { - return {}; - } -} - export function getAdminAppPermissions(admin: CurrentAdminLike) { - const key = getAdminPermissionKey(admin); - const map = getAdminAppPermissionMap(); - if (key && map[key]) return map[key]; - if (admin.id && map[admin.id]) return map[admin.id]; - if (admin.email && map[admin.email]) return map[admin.email]; - return []; + return Array.isArray(admin.appPermissions) ? admin.appPermissions : []; } export function setAdminAppPermissions(admin: CurrentAdminLike, appIds: string[]) { - const key = getAdminPermissionKey(admin); - if (!key) return; + const current = readCurrentAdmin(); + if (!current || !admin.id || current.id !== admin.id) return; - const nextMap = getAdminAppPermissionMap(); const nextAppIds = [...new Set(appIds.filter(Boolean))]; - nextMap[key] = nextAppIds; - if (admin.id) nextMap[admin.id] = nextAppIds; - if (admin.email) nextMap[admin.email] = nextAppIds; - window.localStorage.setItem(adminAppPermissionsStorageKey, JSON.stringify(nextMap)); + window.localStorage.setItem('studio-admin', JSON.stringify({ ...current, appPermissions: nextAppIds })); window.dispatchEvent(new CustomEvent('studio-admin-app-permissions-change')); } diff --git a/src/pages/operations/AdminManagementPage.tsx b/src/pages/operations/AdminManagementPage.tsx index 3e22283..1a958ed 100644 --- a/src/pages/operations/AdminManagementPage.tsx +++ b/src/pages/operations/AdminManagementPage.tsx @@ -4,16 +4,18 @@ import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../.. import { useToast } from '../../components/toast'; import { Button, Modal } from '../../components/ui'; import { PageTitle } from '../../layout/AppLayout'; -import { getAdminAppPermissions, isOwnerAdmin, setAdminAppPermissions } from '../../lib/admin-app-permissions'; +import { isOwnerAdmin, setAdminAppPermissions } from '../../lib/admin-app-permissions'; import { listApps } from '../applications/application-api'; import type { AppWithComponents } from '../../types/application'; import { createAdmin, createAdminSshKey, deleteAdminSshKey, + loadAdminAppPermissions, loadAdminRoles, loadAdmins, resetAdminPassword, + updateAdminAppPermissions, updateAdminRole, updateAdminStatus, type AdminAccount, @@ -68,6 +70,7 @@ export function AdminManagementPage() { }); const [permissionApps, setPermissionApps] = useState([]); const [permissionLoading, setPermissionLoading] = useState(false); + const [permissionSaving, setPermissionSaving] = useState(false); const loadingVisible = useMinimumLoading(loading); const refresh = () => { @@ -123,14 +126,15 @@ export function AdminManagementPage() { const openPermissionModal = (admin: AdminAccount) => { setMenuAdmin(null); - setPermissionTarget({ admin, appIds: isOwnerAdmin(admin) ? [] : getAdminAppPermissions(admin) }); + setPermissionTarget({ admin, appIds: [] }); setPermissionLoading(true); - listApps() - .then((apps) => { + Promise.all([ + listApps(), + isOwnerAdmin(admin) ? Promise.resolve([]) : loadAdminAppPermissions(admin.id) + ]) + .then(([apps, appIds]) => { setPermissionApps(apps); - if (isOwnerAdmin(admin)) { - setPermissionTarget({ admin, appIds: apps.map((app) => app.id) }); - } + setPermissionTarget({ admin, appIds: isOwnerAdmin(admin) ? apps.map((app) => app.id) : appIds }); }) .catch(() => { setPermissionApps([]); @@ -152,9 +156,18 @@ export function AdminManagementPage() { return; } - setAdminAppPermissions(permissionTarget.admin, permissionTarget.appIds); - setPermissionTarget({ admin: null, appIds: [] }); - toast({ type: 'success', title: '앱 권한 저장', message: '관리자 앱 권한이 저장되었습니다.' }); + setPermissionSaving(true); + updateAdminAppPermissions(permissionTarget.admin.id, permissionTarget.appIds) + .then((result) => { + setAdminAppPermissions(permissionTarget.admin as AdminAccount, result.appIds ?? permissionTarget.appIds); + setPermissionTarget({ admin: null, appIds: [] }); + toast({ type: 'success', title: '앱 권한 저장', message: '관리자 앱 권한이 저장되었습니다.' }); + refresh(); + }) + .catch((error) => { + toast({ type: 'error', title: '앱 권한 저장 실패', message: error instanceof Error ? error.message : '관리자 앱 권한 저장에 실패했습니다.' }); + }) + .finally(() => setPermissionSaving(false)); }; const submitAdmin = () => { @@ -486,9 +499,9 @@ export function AdminManagementPage() { className="max-w-2xl" footer={ <> - - + } diff --git a/src/pages/operations/operations-api.ts b/src/pages/operations/operations-api.ts index 1cb9063..aacd477 100644 --- a/src/pages/operations/operations-api.ts +++ b/src/pages/operations/operations-api.ts @@ -20,6 +20,7 @@ export type AdminAccount = { lastLoginAt?: string | null; sessions?: AdminSession[]; sshKeys?: AdminSshKey[]; + appPermissions?: string[]; }; export type AdminRoleOption = { @@ -81,8 +82,17 @@ export type LauncherNotice = { }; export async function loadAdmins() { - const response = await apiRequest('/admins'); - return Array.isArray(response) ? response : response.admins ?? []; + type RawAdminAccount = Omit & { + appPermissions?: Array; + }; + const response = await apiRequest('/admins'); + const admins = Array.isArray(response) ? response : response.admins ?? []; + return admins.map((admin) => ({ + ...admin, + appPermissions: (admin.appPermissions ?? []).map((permission) => ( + typeof permission === 'string' ? permission : permission.appId + )) + })); } export function loadAdminRoles() { @@ -138,6 +148,18 @@ export function deleteAdminSshKey(adminId: string, keyId: string) { }); } +export async function loadAdminAppPermissions(adminId: string) { + const response = await apiRequest<{ appIds?: string[] }>(`/admins/${adminId}/app-permissions`); + return response.appIds ?? []; +} + +export function updateAdminAppPermissions(adminId: string, appIds: string[]) { + return apiRequest<{ appIds?: string[] }>(`/admins/${adminId}/app-permissions`, { + method: 'PUT', + body: JSON.stringify({ appIds }) + }); +} + export async function loadUserSessions() { const response = await apiRequest<{ sessions?: UserSession[] }>('/users/sessions'); return (response.sessions ?? []).map((session) => ({