replace launcher common settings with internal launcher settings
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -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]);
|
||||||
|
|||||||
@@ -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'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user