add launcher user app permission management

This commit is contained in:
2026-07-02 13:16:48 +09:00
parent 17f7413eef
commit 20ca8d24dc
2 changed files with 158 additions and 1 deletions
+129 -1
View File
@@ -1,4 +1,4 @@
import { MoreHorizontal, Pencil, Plus, Trash2 } from 'lucide-react';
import { MoreHorizontal, Pencil, Plus, ShieldCheck, Trash2 } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../../components/data-table';
import { Button, Card, Modal } from '../../components/ui';
@@ -10,9 +10,12 @@ import {
assignUserLicense,
loadLicensePlans,
loadLicenseUsers,
loadLauncherUserAppPermissions,
revokeUserLicense,
updateLauncherUserAppPermissions,
updateUserLicense,
type AuthLicenseUser,
type LauncherUserAppPermission,
type LicensePlan
} from './operations-api';
@@ -81,6 +84,9 @@ export function LicensesPage() {
const [assignOpen, setAssignOpen] = useState(false);
const [editingRow, setEditingRow] = useState<LicenseAssignmentRow | null>(null);
const [deleteTarget, setDeleteTarget] = useState<LicenseAssignmentRow | null>(null);
const [appPermissionTarget, setAppPermissionTarget] = useState<LicenseAssignmentRow | null>(null);
const [appPermissions, setAppPermissions] = useState<LauncherUserAppPermission[]>([]);
const [appPermissionLoading, setAppPermissionLoading] = useState(false);
const [menuRow, setMenuRow] = useState<{ id: string; x: number; y: number } | null>(null);
const [saving, setSaving] = useState(false);
const [assignForm, setAssignForm] = useState({
@@ -217,6 +223,59 @@ export function LicensesPage() {
}
};
const openAppPermissionModal = async (row: LicenseAssignmentRow) => {
setMenuRow(null);
setAppPermissionTarget(row);
setAppPermissions([]);
setAppPermissionLoading(true);
try {
setAppPermissions(await loadLauncherUserAppPermissions(row.userId));
} catch (error) {
toast({ type: 'error', message: error instanceof Error ? error.message : '앱 권한을 불러오지 못했습니다.' });
} finally {
setAppPermissionLoading(false);
}
};
const closeAppPermissionModal = () => {
setAppPermissionTarget(null);
setAppPermissions([]);
};
const toggleAppPermission = (appKey: string, field: 'canView' | 'canLaunch') => {
setAppPermissions((prev) => prev.map((permission) => {
if (permission.appKey !== appKey) return permission;
if (field === 'canView') {
const canView = !permission.canView;
return { ...permission, canView, canLaunch: canView ? permission.canLaunch : false };
}
const canLaunch = !permission.canLaunch;
return { ...permission, canView: canLaunch ? true : permission.canView, canLaunch };
}));
};
const saveAppPermissions = async () => {
if (!appPermissionTarget) return;
setSaving(true);
try {
const result = await updateLauncherUserAppPermissions(
appPermissionTarget.userId,
appPermissions.map((permission) => ({
appKey: permission.appKey,
canView: permission.canView,
canLaunch: permission.canView && permission.canLaunch
}))
);
setAppPermissions(result.permissions ?? []);
toast({ type: 'success', message: '앱 권한을 저장했습니다.' });
closeAppPermissionModal();
} catch (error) {
toast({ type: 'error', message: error instanceof Error ? error.message : '앱 권한 저장에 실패했습니다.' });
} finally {
setSaving(false);
}
};
const columns = useMemo<Array<DataTableColumn<LicenseAssignmentRow>>>(
() => [
{
@@ -343,6 +402,13 @@ export function LicensesPage() {
>
수정
</button>
<button
type="button"
className="block w-full px-3 py-2 text-left text-xs text-foreground hover:bg-muted"
onClick={() => void openAppPermissionModal(row)}
>
앱 권한 설정
</button>
<button
type="button"
className="block w-full px-3 py-2 text-left text-xs text-rose-300 hover:bg-rose-500/10"
@@ -499,6 +565,68 @@ export function LicensesPage() {
>
<p className="pb-6 text-sm text-foreground">라이센스를 삭제할까요?</p>
</Modal>
<Modal
open={Boolean(appPermissionTarget)}
title={
<span className="inline-flex items-center gap-2">
<ShieldCheck className="size-4 text-primary" />
앱 권한 설정
</span>
}
onClose={closeAppPermissionModal}
className="max-w-2xl"
footer={
<>
<Button variant="outline" onClick={closeAppPermissionModal} disabled={saving}>취소</Button>
<Button onClick={saveAppPermissions} disabled={saving || appPermissionLoading}>
{saving ? '저장 중' : '저장'}
</Button>
</>
}
>
<div className="space-y-4">
<div className="rounded-md border border-border bg-muted/20 px-4 py-3">
<p className="text-sm font-medium text-foreground">{appPermissionTarget?.user ?? '-'}</p>
<p className="mt-1 text-xs text-muted-foreground">{appPermissionTarget?.name || '-'}</p>
</div>
<div className="max-h-80 overflow-y-auto rounded-md border border-border">
{appPermissionLoading ? (
<div className="px-4 py-10 text-center text-sm text-muted-foreground">앱 권한을 불러오는 중입니다.</div>
) : appPermissions.length === 0 ? (
<div className="px-4 py-10 text-center text-sm text-muted-foreground">런처에 연결된 앱이 없습니다.</div>
) : (
appPermissions.map((permission) => (
<div key={permission.appKey} className="flex items-center justify-between gap-4 border-b border-border px-4 py-3 last:border-b-0 hover:bg-muted/20">
<div className="min-w-0">
<p className="truncate text-sm text-foreground">{permission.displayName}</p>
<p className="mt-1 truncate text-xs text-muted-foreground">{permission.appKey}</p>
</div>
<div className="flex shrink-0 items-center gap-4 text-xs text-muted-foreground">
<label className="inline-flex items-center gap-2">
<input
type="checkbox"
className="size-4 accent-primary"
checked={permission.canView}
onChange={() => toggleAppPermission(permission.appKey, 'canView')}
/>
보기
</label>
<label className="inline-flex items-center gap-2">
<input
type="checkbox"
className="size-4 accent-primary"
checked={permission.canLaunch}
onChange={() => toggleAppPermission(permission.appKey, 'canLaunch')}
/>
실행
</label>
</div>
</div>
))
)}
</div>
</div>
</Modal>
<LoadingModal open={loadingVisible} />
</>
);
+29
View File
@@ -269,3 +269,32 @@ export function revokeUserLicense(userLicenseId: string) {
method: 'DELETE'
});
}
export type LauncherUserAppPermission = {
appId: string;
appKey: string;
displayName: string;
description?: string | null;
sortOrder: number;
isEnabled: boolean;
permissionId?: string | null;
canView: boolean;
canLaunch: boolean;
assignedAt?: string | null;
};
export async function loadLauncherUserAppPermissions(userId: string) {
const response = await apiRequest<{ permissions?: LauncherUserAppPermission[] }>(`/launcher/users/${userId}/app-permissions`);
return response.permissions ?? [];
}
export function updateLauncherUserAppPermissions(userId: string, permissions: Array<{
appKey: string;
canView: boolean;
canLaunch: boolean;
}>) {
return apiRequest<{ permissions?: LauncherUserAppPermission[] }>(`/launcher/users/${userId}/app-permissions`, {
method: 'PUT',
body: JSON.stringify({ permissions })
});
}