replace launcher common settings with internal launcher settings
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-09 09:07:00 +09:00
4 changed files with 64 additions and 47 deletions
+9
View File
@@ -1523,6 +1523,15 @@ function App() {
return () => window.removeEventListener(authExpiredEventName, handleAuthExpired); return () => window.removeEventListener(authExpiredEventName, handleAuthExpired);
}, [navigate]); }, [navigate]);
useEffect(() => {
const handleAdminPermissionsChange = () => {
setCurrentAdmin(readCurrentAdmin());
};
window.addEventListener('studio-admin-app-permissions-change', handleAdminPermissionsChange);
return () => window.removeEventListener('studio-admin-app-permissions-change', handleAdminPermissionsChange);
}, []);
useEffect(() => { useEffect(() => {
if (location.pathname === '/') navigate('/dashboard', { replace: true }); if (location.pathname === '/') navigate('/dashboard', { replace: true });
}, [location.pathname, navigate]); }, [location.pathname, navigate]);
+5 -32
View File
@@ -1,14 +1,11 @@
export const adminAppPermissionsStorageKey = 'studio-admin-app-permissions';
export type CurrentAdminLike = { export type CurrentAdminLike = {
id?: string; id?: string;
email?: string; email?: string;
name?: string; name?: string;
role?: string; role?: string;
appPermissions?: string[];
}; };
type PermissionMap = Record<string, string[]>;
export function readCurrentAdmin(): CurrentAdminLike | null { export function readCurrentAdmin(): CurrentAdminLike | null {
try { try {
const raw = window.localStorage.getItem('studio-admin'); 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) { export function isOwnerAdmin(admin?: CurrentAdminLike | null) {
return admin?.role === 'OWNER'; 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) { export function getAdminAppPermissions(admin: CurrentAdminLike) {
const key = getAdminPermissionKey(admin); return Array.isArray(admin.appPermissions) ? admin.appPermissions : [];
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 [];
} }
export function setAdminAppPermissions(admin: CurrentAdminLike, appIds: string[]) { export function setAdminAppPermissions(admin: CurrentAdminLike, appIds: string[]) {
const key = getAdminPermissionKey(admin); const current = readCurrentAdmin();
if (!key) return; if (!current || !admin.id || current.id !== admin.id) return;
const nextMap = getAdminAppPermissionMap();
const nextAppIds = [...new Set(appIds.filter(Boolean))]; const nextAppIds = [...new Set(appIds.filter(Boolean))];
nextMap[key] = nextAppIds; window.localStorage.setItem('studio-admin', JSON.stringify({ ...current, appPermissions: nextAppIds }));
if (admin.id) nextMap[admin.id] = nextAppIds;
if (admin.email) nextMap[admin.email] = nextAppIds;
window.localStorage.setItem(adminAppPermissionsStorageKey, JSON.stringify(nextMap));
window.dispatchEvent(new CustomEvent('studio-admin-app-permissions-change')); window.dispatchEvent(new CustomEvent('studio-admin-app-permissions-change'));
} }
+26 -13
View File
@@ -4,16 +4,18 @@ import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../..
import { useToast } from '../../components/toast'; import { useToast } from '../../components/toast';
import { Button, Modal } from '../../components/ui'; import { Button, Modal } from '../../components/ui';
import { PageTitle } from '../../layout/AppLayout'; 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 { listApps } from '../applications/application-api';
import type { AppWithComponents } from '../../types/application'; import type { AppWithComponents } from '../../types/application';
import { import {
createAdmin, createAdmin,
createAdminSshKey, createAdminSshKey,
deleteAdminSshKey, deleteAdminSshKey,
loadAdminAppPermissions,
loadAdminRoles, loadAdminRoles,
loadAdmins, loadAdmins,
resetAdminPassword, resetAdminPassword,
updateAdminAppPermissions,
updateAdminRole, updateAdminRole,
updateAdminStatus, updateAdminStatus,
type AdminAccount, type AdminAccount,
@@ -68,6 +70,7 @@ export function AdminManagementPage() {
}); });
const [permissionApps, setPermissionApps] = useState<AppWithComponents[]>([]); const [permissionApps, setPermissionApps] = useState<AppWithComponents[]>([]);
const [permissionLoading, setPermissionLoading] = useState(false); const [permissionLoading, setPermissionLoading] = useState(false);
const [permissionSaving, setPermissionSaving] = useState(false);
const loadingVisible = useMinimumLoading(loading); const loadingVisible = useMinimumLoading(loading);
const refresh = () => { const refresh = () => {
@@ -123,14 +126,15 @@ export function AdminManagementPage() {
const openPermissionModal = (admin: AdminAccount) => { const openPermissionModal = (admin: AdminAccount) => {
setMenuAdmin(null); setMenuAdmin(null);
setPermissionTarget({ admin, appIds: isOwnerAdmin(admin) ? [] : getAdminAppPermissions(admin) }); setPermissionTarget({ admin, appIds: [] });
setPermissionLoading(true); setPermissionLoading(true);
listApps() Promise.all([
.then((apps) => { listApps(),
isOwnerAdmin(admin) ? Promise.resolve<string[]>([]) : loadAdminAppPermissions(admin.id)
])
.then(([apps, appIds]) => {
setPermissionApps(apps); setPermissionApps(apps);
if (isOwnerAdmin(admin)) { setPermissionTarget({ admin, appIds: isOwnerAdmin(admin) ? apps.map((app) => app.id) : appIds });
setPermissionTarget({ admin, appIds: apps.map((app) => app.id) });
}
}) })
.catch(() => { .catch(() => {
setPermissionApps([]); setPermissionApps([]);
@@ -152,9 +156,18 @@ export function AdminManagementPage() {
return; return;
} }
setAdminAppPermissions(permissionTarget.admin, permissionTarget.appIds); setPermissionSaving(true);
setPermissionTarget({ admin: null, appIds: [] }); updateAdminAppPermissions(permissionTarget.admin.id, permissionTarget.appIds)
toast({ type: 'success', title: '앱 권한 저장', message: '관리자 앱 권한이 저장되었습니다.' }); .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 = () => { const submitAdmin = () => {
@@ -486,9 +499,9 @@ export function AdminManagementPage() {
className="max-w-2xl" className="max-w-2xl"
footer={ footer={
<> <>
<Button variant="outline" onClick={() => setPermissionTarget({ admin: null, appIds: [] })}>취소</Button> <Button variant="outline" onClick={() => setPermissionTarget({ admin: null, appIds: [] })} disabled={permissionSaving}>취소</Button>
<Button onClick={saveAppPermissions} disabled={permissionLoading || isOwnerAdmin(permissionTarget.admin)}> <Button onClick={saveAppPermissions} disabled={permissionLoading || permissionSaving || isOwnerAdmin(permissionTarget.admin)}>
저장 {permissionSaving ? '저장 중' : '저장'}
</Button> </Button>
</> </>
} }
+24 -2
View File
@@ -20,6 +20,7 @@ export type AdminAccount = {
lastLoginAt?: string | null; lastLoginAt?: string | null;
sessions?: AdminSession[]; sessions?: AdminSession[];
sshKeys?: AdminSshKey[]; sshKeys?: AdminSshKey[];
appPermissions?: string[];
}; };
export type AdminRoleOption = { export type AdminRoleOption = {
@@ -81,8 +82,17 @@ export type LauncherNotice = {
}; };
export async function loadAdmins() { export async function loadAdmins() {
const response = await apiRequest<AdminAccount[] | { admins?: AdminAccount[] }>('/admins'); type RawAdminAccount = Omit<AdminAccount, 'appPermissions'> & {
return Array.isArray(response) ? response : response.admins ?? []; appPermissions?: Array<string | { appId: string }>;
};
const response = await apiRequest<RawAdminAccount[] | { admins?: RawAdminAccount[] }>('/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() { 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() { export async function loadUserSessions() {
const response = await apiRequest<{ sessions?: UserSession[] }>('/users/sessions'); const response = await apiRequest<{ sessions?: UserSession[] }>('/users/sessions');
return (response.sessions ?? []).map((session) => ({ return (response.sessions ?? []).map((session) => ({