add launcher user app permission management
This commit is contained in:
@@ -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 { useEffect, useMemo, useState } from 'react';
|
||||||
import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../../components/data-table';
|
import { DataTableCard, type DataTableColumn, type DataTableFilter } from '../../components/data-table';
|
||||||
import { Button, Card, Modal } from '../../components/ui';
|
import { Button, Card, Modal } from '../../components/ui';
|
||||||
@@ -10,9 +10,12 @@ import {
|
|||||||
assignUserLicense,
|
assignUserLicense,
|
||||||
loadLicensePlans,
|
loadLicensePlans,
|
||||||
loadLicenseUsers,
|
loadLicenseUsers,
|
||||||
|
loadLauncherUserAppPermissions,
|
||||||
revokeUserLicense,
|
revokeUserLicense,
|
||||||
|
updateLauncherUserAppPermissions,
|
||||||
updateUserLicense,
|
updateUserLicense,
|
||||||
type AuthLicenseUser,
|
type AuthLicenseUser,
|
||||||
|
type LauncherUserAppPermission,
|
||||||
type LicensePlan
|
type LicensePlan
|
||||||
} from './operations-api';
|
} from './operations-api';
|
||||||
|
|
||||||
@@ -81,6 +84,9 @@ export function LicensesPage() {
|
|||||||
const [assignOpen, setAssignOpen] = useState(false);
|
const [assignOpen, setAssignOpen] = useState(false);
|
||||||
const [editingRow, setEditingRow] = useState<LicenseAssignmentRow | null>(null);
|
const [editingRow, setEditingRow] = useState<LicenseAssignmentRow | null>(null);
|
||||||
const [deleteTarget, setDeleteTarget] = 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 [menuRow, setMenuRow] = useState<{ id: string; x: number; y: number } | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [assignForm, setAssignForm] = useState({
|
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>>>(
|
const columns = useMemo<Array<DataTableColumn<LicenseAssignmentRow>>>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -343,6 +402,13 @@ export function LicensesPage() {
|
|||||||
>
|
>
|
||||||
수정
|
수정
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="block w-full px-3 py-2 text-left text-xs text-rose-300 hover:bg-rose-500/10"
|
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>
|
<p className="pb-6 text-sm text-foreground">라이센스를 삭제할까요?</p>
|
||||||
</Modal>
|
</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} />
|
<LoadingModal open={loadingVisible} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -269,3 +269,32 @@ export function revokeUserLicense(userLicenseId: string) {
|
|||||||
method: 'DELETE'
|
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 })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user