Skip to content

Commit 0817246

Browse files
beyondnetPeruclaude
andcommitted
feat(identity): wire UserManagementDelegation into SQL schema, router, and nav (FS-14)
- Add 20260522_sqlserver_identity_delegations.sql with full DDL for UserManagementDelegations table (ums_identity schema), CHECK constraints for no-self-delegation and valid window, and 4 performance indexes - Register migration in SqlServerSchemaBootstrapper script order - Add /delegations lazy route in App.tsx → DelegationDashboardScreen - Add 'delegations' NavItemId, route, and pathToTab entry in navigation.config - Add GitMerge icon and delegation nav item to Identity Context module in NavRail - Add userAccounts + delegationManagement translation keys (ES + EN) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ed5b92b commit 0817246

6 files changed

Lines changed: 106 additions & 11 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
-- ============================================================
2+
-- Migration: UserManagementDelegations table
3+
-- Date: 2026-05-22
4+
-- Context: Identity BC — FS-14 Delegated Administration
5+
-- Schema: ums_identity
6+
-- ============================================================
7+
8+
IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = 'ums_identity')
9+
BEGIN
10+
EXEC('CREATE SCHEMA [ums_identity]');
11+
END
12+
GO
13+
14+
IF OBJECT_ID('[ums_identity].[UserManagementDelegations]', 'U') IS NULL
15+
BEGIN
16+
CREATE TABLE [ums_identity].[UserManagementDelegations]
17+
(
18+
-- Identity
19+
[Id] uniqueidentifier NOT NULL,
20+
[TenantId] uniqueidentifier NOT NULL,
21+
22+
-- Participants
23+
[DelegatingAdminId] uniqueidentifier NOT NULL,
24+
[DelegatedAdminId] uniqueidentifier NOT NULL,
25+
26+
-- Scope
27+
[ScopeTypeId] int NOT NULL,
28+
[ScopeId] uniqueidentifier NULL,
29+
30+
-- Permissions
31+
[AllowedActionsJson] nvarchar(500) NOT NULL,
32+
33+
-- Validity window
34+
[ValidFrom] datetimeoffset NOT NULL,
35+
[ValidUntil] datetimeoffset NOT NULL,
36+
[MaxDurationDays] int NULL,
37+
38+
-- Approval
39+
[RequiresApproval] bit NOT NULL DEFAULT 0,
40+
[ApprovalRequestId] uniqueidentifier NULL,
41+
42+
-- Lifecycle
43+
[StatusId] int NOT NULL,
44+
[RevokedAt] datetimeoffset NULL,
45+
[RevokedBy] uniqueidentifier NULL,
46+
[RevocationReason] nvarchar(500) NULL,
47+
48+
-- Audit
49+
[CreatedBy] nvarchar(100) NOT NULL,
50+
[CreatedAtUtc] datetime2 NOT NULL,
51+
[UpdatedBy] nvarchar(100) NULL,
52+
[UpdatedAtUtc] datetime2 NULL,
53+
[AuditTimeSpan] nvarchar(100) NOT NULL,
54+
55+
CONSTRAINT [PK_UserManagementDelegations] PRIMARY KEY ([Id]),
56+
57+
-- INV-DEL2: delegating admin cannot delegate to themselves
58+
CONSTRAINT [CK_UserManagementDelegations_NoSelfDelegation]
59+
CHECK ([DelegatingAdminId] <> [DelegatedAdminId]),
60+
61+
-- INV-DEL3: validity window must be positive
62+
CONSTRAINT [CK_UserManagementDelegations_ValidWindow]
63+
CHECK ([ValidUntil] > [ValidFrom])
64+
);
65+
66+
-- Hot-path: find active delegations for a given delegated admin (scope validation)
67+
CREATE INDEX [IX_UserManagementDelegations_DelegatedAdminId_Status]
68+
ON [ums_identity].[UserManagementDelegations] ([DelegatedAdminId], [StatusId])
69+
INCLUDE ([TenantId], [AllowedActionsJson], [ScopeTypeId], [ScopeId], [ValidFrom], [ValidUntil]);
70+
71+
-- Dashboard: load all delegations a given admin has granted
72+
CREATE INDEX [IX_UserManagementDelegations_DelegatingAdminId]
73+
ON [ums_identity].[UserManagementDelegations] ([DelegatingAdminId]);
74+
75+
-- Tenant-scoped listing (admin portal)
76+
CREATE INDEX [IX_UserManagementDelegations_TenantId_StatusId]
77+
ON [ums_identity].[UserManagementDelegations] ([TenantId], [StatusId]);
78+
79+
-- Background expiry worker: find active delegations past their ValidUntil
80+
CREATE INDEX [IX_UserManagementDelegations_Active_ValidUntil]
81+
ON [ums_identity].[UserManagementDelegations] ([StatusId], [ValidUntil])
82+
WHERE [StatusId] = 1; -- 1 = Active
83+
END
84+
GO

src/apps/ums.api/Ums.Infrastructure/Persistence/SqlServerSchemaBootstrapper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public static partial class SqlServerSchemaBootstrapper
1111
"20260521_sqlserver_platform_outbox.sql",
1212
"20260521_sqlserver_identity_aggregates.sql",
1313
"20260521_sqlserver_authorization_profiles.sql",
14+
"20260522_sqlserver_identity_delegations.sql",
1415
];
1516

1617
public static async Task InitializeAsync(UmsPlatformDbContext dbContext, CancellationToken cancellationToken = default)

src/apps/ums.web-app/src/App.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useThemeStore } from './application/stores/theme.store';
77

88
const TenantDashboardScreen = lazy(() => import('./presentation/identity/tenant/screens/TenantDashboardScreen'));
99
const UserAccountDashboardScreen = lazy(() => import('./presentation/identity/user-account/screens/UserAccountDashboardScreen'));
10+
const DelegationDashboardScreen = lazy(() => import('./presentation/identity/delegation/screens/DelegationDashboardScreen'));
1011
const ProfileScreen = lazy(() => import('./presentation/identity/profile/screens/ProfileScreen'));
1112
const LoginScreen = lazy(() => import('./presentation/identity/profile/screens/LoginScreen'));
1213

@@ -25,12 +26,13 @@ export default function App() {
2526
<MainLayout>
2627
<Suspense fallback={<RouteLoader />}>
2728
<Routes>
28-
<Route path="/" element={<Navigate to="/tenants" replace />} />
29-
<Route path="/tenants" element={<TenantDashboardScreen />} />
30-
<Route path="/users" element={<UserAccountDashboardScreen />} />
31-
<Route path="/profile" element={<ProfileScreen />} />
32-
<Route path="/login" element={<LoginScreen />} />
33-
<Route path="*" element={<Navigate to="/tenants" replace />} />
29+
<Route path="/" element={<Navigate to="/tenants" replace />} />
30+
<Route path="/tenants" element={<TenantDashboardScreen />} />
31+
<Route path="/users" element={<UserAccountDashboardScreen />} />
32+
<Route path="/delegations" element={<DelegationDashboardScreen />} />
33+
<Route path="/profile" element={<ProfileScreen />} />
34+
<Route path="/login" element={<LoginScreen />} />
35+
<Route path="*" element={<Navigate to="/tenants" replace />} />
3436
</Routes>
3537
</Suspense>
3638
</MainLayout>

src/apps/ums.web-app/src/application/i18n/namespaces/identity.translations.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export const identityTranslations = {
44
identityContext: 'Contexto de Identidad',
55
tenant: 'Tenant',
66
tenants: 'Tenants',
7+
userAccounts: 'Cuentas de Usuario',
8+
delegationManagement: 'Delegaciones',
79
systemDiagnostics: 'Diagnóstico del Sistema',
810
profileStats: 'Estadísticas de Perfil',
911
developerSession: 'Sesión de Desarrollador',
@@ -161,6 +163,8 @@ export const identityTranslations = {
161163
identityContext: 'Identity Context',
162164
tenant: 'Tenant',
163165
tenants: 'Tenants',
166+
userAccounts: 'User Accounts',
167+
delegationManagement: 'Delegations',
164168
systemDiagnostics: 'System Diagnostics',
165169
profileStats: 'Profile Statistics',
166170
developerSession: 'Developer Session',

src/apps/ums.web-app/src/presentation/shared/layouts/NavRail.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useState, useMemo } from 'react';
22
import { useNavigate, useLocation } from 'react-router-dom';
33
import { useI18n } from '@app/i18n/use-i18n';
4-
import { Building2, User, LogOut, ChevronRight, ChevronDown, ShieldCheck, Cpu, Users } from 'lucide-react';
4+
import { Building2, User, LogOut, ChevronRight, ChevronDown, ShieldCheck, Cpu, Users, GitMerge } from 'lucide-react';
55
import { NAV_ROUTES, pathToTab, NAV_MODULES } from './navigation.config';
66
import type { NavModule } from './navigation.config';
77

@@ -21,7 +21,7 @@ export const NavRail: React.FC<NavRailProps> = ({ collapsed }) => {
2121
const activeTab = pathToTab(location.pathname);
2222

2323
const modules: NavModule[] = useMemo(() => NAV_MODULES({
24-
ShieldCheck, Building2, Users, Cpu, User, LogOut,
24+
ShieldCheck, Building2, Users, GitMerge, Cpu, User, LogOut,
2525
primaryColorClass: 'text-m3-primary',
2626
indigoColorClass: 'text-indigo-400',
2727
t,

src/apps/ums.web-app/src/presentation/shared/layouts/navigation.config.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import React from 'react';
99

10-
export type NavItemId = 'tenants' | 'users' | 'profile' | 'login';
10+
export type NavItemId = 'tenants' | 'users' | 'delegations' | 'profile' | 'login';
1111

1212
export interface NavItem {
1313
id: NavItemId;
@@ -25,13 +25,15 @@ export interface NavModule {
2525
export const NAV_ROUTES: Record<NavItemId, string> = {
2626
tenants: '/tenants',
2727
users: '/users',
28+
delegations: '/delegations',
2829
profile: '/profile',
2930
login: '/login',
3031
};
3132

3233
export const pathToTab = (pathname: string): NavItemId => {
3334
if (pathname.startsWith('/tenants')) return 'tenants';
3435
if (pathname.startsWith('/users')) return 'users';
36+
if (pathname.startsWith('/delegations')) return 'delegations';
3537
if (pathname.startsWith('/profile')) return 'profile';
3638
if (pathname.startsWith('/login')) return 'login';
3739
return 'tenants';
@@ -41,6 +43,7 @@ interface NavModulesFactoryDeps {
4143
ShieldCheck: React.ComponentType<{ className?: string }>;
4244
Building2: React.ComponentType<{ className?: string }>;
4345
Users: React.ComponentType<{ className?: string }>;
46+
GitMerge: React.ComponentType<{ className?: string }>;
4447
Cpu: React.ComponentType<{ className?: string }>;
4548
User: React.ComponentType<{ className?: string }>;
4649
LogOut: React.ComponentType<{ className?: string }>;
@@ -55,8 +58,9 @@ export const NAV_MODULES = (deps: NavModulesFactoryDeps): NavModule[] => [
5558
nameKey: 'identityContext',
5659
icon: <deps.ShieldCheck className={`w-5 h-5 ${deps.primaryColorClass}`} />,
5760
members: [
58-
{ id: 'tenants', nameKey: 'tenant', icon: <deps.Building2 className="w-4 h-4" /> },
59-
{ id: 'users', nameKey: 'userAccounts', icon: <deps.Users className="w-4 h-4" /> },
61+
{ id: 'tenants', nameKey: 'tenant', icon: <deps.Building2 className="w-4 h-4" /> },
62+
{ id: 'users', nameKey: 'userAccounts', icon: <deps.Users className="w-4 h-4" /> },
63+
{ id: 'delegations', nameKey: 'delegationManagement', icon: <deps.GitMerge className="w-4 h-4" /> },
6064
],
6165
},
6266
{

0 commit comments

Comments
 (0)