Передаем на тест
This commit is contained in:
213
frontend/src/routes/admin/settings/+page.svelte
Normal file
213
frontend/src/routes/admin/settings/+page.svelte
Normal file
@@ -0,0 +1,213 @@
|
||||
<!-- [DEF:AdminSettingsPage:Component] -->
|
||||
<!--
|
||||
@SEMANTICS: admin, adfs, mappings, configuration
|
||||
@PURPOSE: UI for configuring Active Directory Group to local Role mappings for ADFS SSO.
|
||||
@LAYER: Feature
|
||||
@RELATION: DEPENDS_ON -> frontend.src.services.adminService
|
||||
@RELATION: DEPENDS_ON -> frontend.src.components.auth.ProtectedRoute
|
||||
|
||||
@INVARIANT: Only accessible by users with "admin:settings" permission.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
// [SECTION: IMPORTS]
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import ProtectedRoute from '../../../components/auth/ProtectedRoute.svelte';
|
||||
import { adminService } from '../../../services/adminService';
|
||||
// [/SECTION: IMPORTS]
|
||||
|
||||
let mappings = [];
|
||||
let roles = [];
|
||||
let loading = true;
|
||||
let error = null;
|
||||
|
||||
let showCreateModal = false;
|
||||
let newMapping = {
|
||||
ad_group: '',
|
||||
role_id: ''
|
||||
};
|
||||
|
||||
// [DEF:loadData:Function]
|
||||
/**
|
||||
* @purpose Fetches AD mappings and roles from the backend to populate the UI.
|
||||
* @pre Component is mounted and user has active session.
|
||||
* @post mappings and roles variables are updated with backend data.
|
||||
* @returns {Promise<void>}
|
||||
* @side_effect Updates local 'mappings', 'roles', 'loading', and 'error' states.
|
||||
* @relation CALLS -> adminService.getRoles
|
||||
* @relation CALLS -> adminService.getADGroupMappings
|
||||
*/
|
||||
async function loadData() {
|
||||
console.log('[AdminSettingsPage][loadData][Entry]');
|
||||
loading = true;
|
||||
try {
|
||||
// Fetch roles first as they are required for displaying mapping labels
|
||||
roles = await adminService.getRoles();
|
||||
|
||||
try {
|
||||
mappings = await adminService.getADGroupMappings();
|
||||
} catch (e) {
|
||||
console.warn("[AdminSettingsPage][loadData] AD Mappings endpoint potentially unavailable.");
|
||||
}
|
||||
|
||||
console.log('[AdminSettingsPage][loadData][Coherence:OK]');
|
||||
} catch (e) {
|
||||
error = "Failed to load roles or configuration.";
|
||||
console.error('[AdminSettingsPage][loadData][Coherence:Failed]', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
// [/DEF:loadData:Function]
|
||||
|
||||
// [DEF:handleCreateMapping:Function]
|
||||
/**
|
||||
* @purpose Submits a new AD Group to Role mapping to the backend.
|
||||
* @pre 'newMapping' object contains valid 'ad_group' and 'role_id'.
|
||||
* @post A new mapping is created in the database and the table is refreshed.
|
||||
* @returns {Promise<void>}
|
||||
* @side_effect Closes the modal on success, shows alert on failure.
|
||||
* @relation CALLS -> adminService.createADGroupMapping
|
||||
*/
|
||||
async function handleCreateMapping() {
|
||||
console.log('[AdminSettingsPage][handleCreateMapping][Entry]');
|
||||
|
||||
// Guard Clause (@PRE)
|
||||
if (!newMapping.ad_group || !newMapping.role_id) {
|
||||
alert("Please fill in all fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await adminService.createADGroupMapping(newMapping);
|
||||
showCreateModal = false;
|
||||
// Reset form
|
||||
newMapping = { ad_group: '', role_id: '' };
|
||||
await loadData();
|
||||
console.log('[AdminSettingsPage][handleCreateMapping][Coherence:OK]');
|
||||
} catch (e) {
|
||||
alert("Failed to create mapping: " + (e.message || "Unknown error"));
|
||||
console.error('[AdminSettingsPage][handleCreateMapping][Coherence:Failed]', e);
|
||||
}
|
||||
}
|
||||
// [/DEF:handleCreateMapping:Function]
|
||||
|
||||
onMount(loadData);
|
||||
</script>
|
||||
|
||||
<ProtectedRoute requiredPermission="admin:settings">
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="container mx-auto p-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">{$t.admin.settings.title}</h1>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors"
|
||||
on:click={() => showCreateModal = true}
|
||||
>
|
||||
{$t.admin.settings.add_mapping}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex justify-center py-8">
|
||||
<p class="text-gray-500 animate-pulse">{$t.common.loading}</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded mb-4" role="alert">
|
||||
<p class="font-bold">{$t.common.error}</p>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden border border-gray-200">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.admin.settings.ad_group}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.admin.settings.local_role}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{#each mappings as mapping}
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap font-mono text-sm text-gray-600">{mapping.ad_group}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs font-semibold rounded-full">
|
||||
{roles.find(r => r.id === mapping.role_id)?.name || mapping.role_id}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if mappings.length === 0}
|
||||
<tr>
|
||||
<td colspan="2" class="px-6 py-12 text-center text-gray-500">
|
||||
<div class="flex flex-col items-center">
|
||||
<svg class="w-12 h-12 text-gray-300 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<p>{$t.admin.settings.no_mappings}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showCreateModal}
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div class="bg-white rounded-lg shadow-xl p-6 max-w-md w-full">
|
||||
<h2 class="text-xl font-bold mb-4 border-b pb-2">{$t.admin.settings.modal_title}</h2>
|
||||
<form on:submit|preventDefault={handleCreateMapping}>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{$t.admin.settings.ad_group_dn}</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newMapping.ad_group}
|
||||
class="w-full border border-gray-300 p-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="e.g. CN=SS_ADMINS,OU=Groups,DC=org"
|
||||
required
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">{$t.admin.settings.ad_group_hint}</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{$t.admin.settings.local_role_select}</label>
|
||||
<select
|
||||
bind:value={newMapping.role_id}
|
||||
class="w-full border border-gray-300 p-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>{$t.admin.settings.select_role}</option>
|
||||
{#each roles as role}
|
||||
<option value={role.id}>{role.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-gray-600 hover:text-gray-800 font-medium"
|
||||
on:click={() => showCreateModal = false}
|
||||
>
|
||||
{$t.common.cancel}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-6 py-2 bg-blue-600 text-white rounded font-bold hover:bg-blue-700 shadow-md"
|
||||
>
|
||||
{$t.common.save}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- [/SECTION: TEMPLATE] -->
|
||||
</ProtectedRoute>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<!-- [/DEF:AdminSettingsPage:Component] -->
|
||||
Reference in New Issue
Block a user