Передаем на тест

This commit is contained in:
2026-01-27 16:32:08 +03:00
parent cc244c2d86
commit d3c3a80ed2
42 changed files with 2836 additions and 140 deletions

View 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] -->