admin area

This commit is contained in:
2026-01-01 12:10:35 +01:00
parent 02c0cc44e1
commit f001df3744
68 changed files with 10324 additions and 32 deletions

View File

@@ -0,0 +1,173 @@
/**
* Tests for AuthorizationBlocker
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { AuthorizationBlocker, AuthorizationBlockReason } from './AuthorizationBlocker';
import type { SessionViewModel } from '@/lib/view-models/SessionViewModel';
describe('AuthorizationBlocker', () => {
let blocker: AuthorizationBlocker;
// Mock SessionViewModel
const createMockSession = (overrides?: Partial<SessionViewModel>): SessionViewModel => {
const base: SessionViewModel = {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
isAuthenticated: true,
avatarInitials: 'TU',
greeting: 'Hello, Test User!',
hasDriverProfile: false,
authStatusDisplay: 'Logged In',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
primaryDriverId: null,
avatarUrl: null,
},
};
return { ...base, ...overrides };
};
describe('constructor', () => {
it('should create blocker with required roles', () => {
blocker = new AuthorizationBlocker(['owner', 'admin']);
expect(blocker).toBeDefined();
});
it('should create blocker with empty roles array', () => {
blocker = new AuthorizationBlocker([]);
expect(blocker).toBeDefined();
});
});
describe('updateSession', () => {
beforeEach(() => {
blocker = new AuthorizationBlocker(['owner']);
});
it('should update session state', () => {
const session = createMockSession();
blocker.updateSession(session);
expect(blocker.canExecute()).toBe(true);
});
it('should handle null session', () => {
blocker.updateSession(null);
expect(blocker.canExecute()).toBe(false);
expect(blocker.getReason()).toBe('loading');
});
});
describe('canExecute', () => {
beforeEach(() => {
blocker = new AuthorizationBlocker(['owner', 'admin']);
});
it('returns false when session is null', () => {
blocker.updateSession(null);
expect(blocker.canExecute()).toBe(false);
});
it('returns false when not authenticated', () => {
const session = createMockSession({ isAuthenticated: false });
blocker.updateSession(session);
expect(blocker.canExecute()).toBe(false);
});
it('returns true when authenticated (temporary workaround)', () => {
const session = createMockSession();
blocker.updateSession(session);
expect(blocker.canExecute()).toBe(true);
});
});
describe('getReason', () => {
beforeEach(() => {
blocker = new AuthorizationBlocker(['owner']);
});
it('returns loading when session is null', () => {
blocker.updateSession(null);
expect(blocker.getReason()).toBe('loading');
});
it('returns unauthenticated when not authenticated', () => {
const session = createMockSession({ isAuthenticated: false });
blocker.updateSession(session);
expect(blocker.getReason()).toBe('unauthenticated');
});
it('returns enabled when authenticated (temporary)', () => {
const session = createMockSession();
blocker.updateSession(session);
expect(blocker.getReason()).toBe('enabled');
});
});
describe('block and release', () => {
beforeEach(() => {
blocker = new AuthorizationBlocker(['owner']);
});
it('block should set session to null', () => {
const session = createMockSession();
blocker.updateSession(session);
expect(blocker.canExecute()).toBe(true);
blocker.block();
expect(blocker.canExecute()).toBe(false);
expect(blocker.getReason()).toBe('loading');
});
it('release should be no-op', () => {
const session = createMockSession();
blocker.updateSession(session);
blocker.release();
expect(blocker.canExecute()).toBe(true);
});
});
describe('getBlockMessage', () => {
beforeEach(() => {
blocker = new AuthorizationBlocker(['owner']);
});
it('returns correct message for loading', () => {
blocker.updateSession(null);
expect(blocker.getBlockMessage()).toBe('Loading user data...');
});
it('returns correct message for unauthenticated', () => {
const session = createMockSession({ isAuthenticated: false });
blocker.updateSession(session);
expect(blocker.getBlockMessage()).toBe('You must be logged in to access the admin area.');
});
it('returns correct message for enabled', () => {
const session = createMockSession();
blocker.updateSession(session);
expect(blocker.getBlockMessage()).toBe('Access granted');
});
});
describe('multiple required roles', () => {
it('should handle multiple roles', () => {
blocker = new AuthorizationBlocker(['owner', 'admin', 'super-admin']);
const session = createMockSession();
blocker.updateSession(session);
expect(blocker.canExecute()).toBe(true);
});
});
});

View File

@@ -0,0 +1,105 @@
/**
* Blocker: AuthorizationBlocker
*
* Frontend blocker that prevents unauthorized access to admin features.
* This is a UX improvement, NOT a security mechanism.
* Security is enforced by backend Guards.
*/
import { Blocker } from './Blocker';
import type { SessionViewModel } from '@/lib/view-models/SessionViewModel';
export type AuthorizationBlockReason =
| 'loading' // User data not loaded yet
| 'unauthenticated' // User not logged in
| 'unauthorized' // User logged in but lacks required role
| 'insufficient_role' // User has role but not high enough
| 'enabled'; // Access granted
export class AuthorizationBlocker extends Blocker {
private currentSession: SessionViewModel | null = null;
private requiredRoles: string[] = [];
constructor(requiredRoles: string[]) {
super();
this.requiredRoles = requiredRoles;
}
/**
* Update the current session state
*/
updateSession(session: SessionViewModel | null): void {
this.currentSession = session;
}
/**
* Check if user can execute (access admin area)
*/
canExecute(): boolean {
return this.getReason() === 'enabled';
}
/**
* Get the current block reason
*/
getReason(): AuthorizationBlockReason {
if (!this.currentSession) {
return 'loading';
}
if (!this.currentSession.isAuthenticated) {
return 'unauthenticated';
}
// Note: SessionViewModel doesn't currently have role property
// This is a known architectural gap. For now, we'll check if
// the user has admin capabilities through other means
// In a real implementation, we would need to:
// 1. Add role to SessionViewModel
// 2. Add role to AuthenticatedUserDTO
// 3. Add role to User entity
// For now, we'll simulate based on email or other indicators
// This is a temporary workaround until the backend role system is implemented
return 'enabled'; // Allow access for demo purposes
}
/**
* Block access (for testing/demo purposes)
*/
block(): void {
// Simulate blocking by setting session to null
this.currentSession = null;
}
/**
* Release the block
*/
release(): void {
// No-op - blocking is state-based, not persistent
}
/**
* Get user-friendly message for block reason
*/
getBlockMessage(): string {
const reason = this.getReason();
switch (reason) {
case 'loading':
return 'Loading user data...';
case 'unauthenticated':
return 'You must be logged in to access the admin area.';
case 'unauthorized':
return 'You do not have permission to access the admin area.';
case 'insufficient_role':
return `Admin access requires one of: ${this.requiredRoles.join(', ')}`;
case 'enabled':
return 'Access granted';
default:
return 'Access denied';
}
}
}

View File

@@ -1,4 +1,6 @@
export { Blocker } from './Blocker';
export { CapabilityBlocker } from './CapabilityBlocker';
export { SubmitBlocker } from './SubmitBlocker';
export { ThrottleBlocker } from './ThrottleBlocker';
export { ThrottleBlocker } from './ThrottleBlocker';
export { AuthorizationBlocker } from './AuthorizationBlocker';
export type { AuthorizationBlockReason } from './AuthorizationBlocker';