110 lines
2.8 KiB
TypeScript
110 lines
2.8 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Get the current block reason
|
|
*/
|
|
getReason(): AuthorizationBlockReason {
|
|
if (!this.currentSession) {
|
|
// Session is null - this means unauthenticated (not loading)
|
|
// Loading state is handled by AuthContext
|
|
return 'unauthenticated';
|
|
}
|
|
|
|
if (!this.currentSession.isAuthenticated) {
|
|
return 'unauthenticated';
|
|
}
|
|
|
|
// If no roles are required, allow access
|
|
if (this.requiredRoles.length === 0) {
|
|
return 'enabled';
|
|
}
|
|
|
|
// Check if user has a role
|
|
if (!this.currentSession.role) {
|
|
return 'unauthorized';
|
|
}
|
|
|
|
// Check if user's role matches any of the required roles
|
|
if (this.requiredRoles.includes(this.currentSession.role)) {
|
|
return 'enabled';
|
|
}
|
|
|
|
// User has a role but it's not in the required list
|
|
return 'insufficient_role';
|
|
}
|
|
|
|
/**
|
|
* Check if user can execute (access admin area)
|
|
*/
|
|
canExecute(): boolean {
|
|
const reason = this.getReason();
|
|
return reason === 'enabled';
|
|
}
|
|
|
|
/**
|
|
* 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 'unauthenticated':
|
|
return 'You must be logged in to access this area.';
|
|
case 'unauthorized':
|
|
return 'You do not have permission to access this area.';
|
|
case 'insufficient_role':
|
|
return `Access requires one of: ${this.requiredRoles.join(', ')}`;
|
|
case 'enabled':
|
|
return 'Access granted';
|
|
default:
|
|
return 'Access denied';
|
|
}
|
|
}
|
|
} |