admin area
This commit is contained in:
173
apps/website/lib/blockers/AuthorizationBlocker.test.ts
Normal file
173
apps/website/lib/blockers/AuthorizationBlocker.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
105
apps/website/lib/blockers/AuthorizationBlocker.ts
Normal file
105
apps/website/lib/blockers/AuthorizationBlocker.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user