admin area
This commit is contained in:
140
apps/website/lib/gateways/AuthGateway.ts
Normal file
140
apps/website/lib/gateways/AuthGateway.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Gateway: AuthGateway
|
||||
*
|
||||
* Component-based gateway that manages authentication state and access control.
|
||||
* Follows clean architecture by orchestrating between auth context and blockers.
|
||||
*
|
||||
* Gateways are the entry point for component-level access control.
|
||||
* They coordinate between services, blockers, and the UI.
|
||||
*/
|
||||
|
||||
import type { SessionViewModel } from '@/lib/view-models/SessionViewModel';
|
||||
import type { AuthContextValue } from '@/lib/auth/AuthContext';
|
||||
import { AuthorizationBlocker } from '@/lib/blockers/AuthorizationBlocker';
|
||||
|
||||
export interface AuthGatewayConfig {
|
||||
/** Required roles for access (empty array = any authenticated user) */
|
||||
requiredRoles?: string[];
|
||||
/** Whether to redirect if unauthorized */
|
||||
redirectOnUnauthorized?: boolean;
|
||||
/** Redirect path if unauthorized */
|
||||
unauthorizedRedirectPath?: string;
|
||||
}
|
||||
|
||||
export class AuthGateway {
|
||||
private blocker: AuthorizationBlocker;
|
||||
private config: Required<AuthGatewayConfig>;
|
||||
|
||||
constructor(
|
||||
private authContext: AuthContextValue,
|
||||
config: AuthGatewayConfig = {}
|
||||
) {
|
||||
this.config = {
|
||||
requiredRoles: config.requiredRoles || [],
|
||||
redirectOnUnauthorized: config.redirectOnUnauthorized ?? true,
|
||||
unauthorizedRedirectPath: config.unauthorizedRedirectPath || '/auth/login',
|
||||
};
|
||||
|
||||
this.blocker = new AuthorizationBlocker(this.config.requiredRoles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user has access
|
||||
*/
|
||||
canAccess(): boolean {
|
||||
// Update blocker with current session
|
||||
this.blocker.updateSession(this.authContext.session);
|
||||
|
||||
return this.blocker.canExecute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current access state
|
||||
*/
|
||||
getAccessState(): {
|
||||
canAccess: boolean;
|
||||
reason: string;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
} {
|
||||
const reason = this.blocker.getReason();
|
||||
|
||||
return {
|
||||
canAccess: this.canAccess(),
|
||||
reason: this.blocker.getBlockMessage(),
|
||||
isLoading: reason === 'loading',
|
||||
isAuthenticated: this.authContext.session?.isAuthenticated ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce access control - throws if access denied
|
||||
* Used for programmatic access control
|
||||
*/
|
||||
enforceAccess(): void {
|
||||
if (!this.canAccess()) {
|
||||
const reason = this.blocker.getBlockMessage();
|
||||
throw new Error(`Access denied: ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to unauthorized page if needed
|
||||
* Returns true if redirect was performed
|
||||
*/
|
||||
redirectIfUnauthorized(): boolean {
|
||||
if (this.canAccess()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.config.redirectOnUnauthorized) {
|
||||
// Note: We can't use router here since this is a pure class
|
||||
// The component using this gateway should handle the redirect
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get redirect path for unauthorized access
|
||||
*/
|
||||
getUnauthorizedRedirectPath(): string {
|
||||
return this.config.unauthorizedRedirectPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the gateway state (e.g., after login/logout)
|
||||
*/
|
||||
refresh(): void {
|
||||
this.blocker.updateSession(this.authContext.session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is loading
|
||||
*/
|
||||
isLoading(): boolean {
|
||||
return this.blocker.getReason() === 'loading';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return this.authContext.session?.isAuthenticated ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session
|
||||
*/
|
||||
getSession(): SessionViewModel | null {
|
||||
return this.authContext.session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block reason for debugging
|
||||
*/
|
||||
getBlockReason(): string {
|
||||
return this.blocker.getReason();
|
||||
}
|
||||
}
|
||||
72
apps/website/lib/gateways/AuthGuard.tsx
Normal file
72
apps/website/lib/gateways/AuthGuard.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Component: AuthGuard
|
||||
*
|
||||
* Protects routes that require authentication but not specific roles.
|
||||
* Uses the same Gateway pattern for consistency.
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { RouteGuard } from './RouteGuard';
|
||||
|
||||
interface AuthGuardProps {
|
||||
children: ReactNode;
|
||||
/**
|
||||
* Path to redirect to if not authenticated
|
||||
*/
|
||||
redirectPath?: string;
|
||||
/**
|
||||
* Custom loading component (optional)
|
||||
*/
|
||||
loadingComponent?: ReactNode;
|
||||
/**
|
||||
* Custom unauthorized component (optional)
|
||||
*/
|
||||
unauthorizedComponent?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* AuthGuard Component
|
||||
*
|
||||
* Protects child components requiring authentication.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* <AuthGuard>
|
||||
* <ProtectedPage />
|
||||
* </AuthGuard>
|
||||
* ```
|
||||
*/
|
||||
export function AuthGuard({
|
||||
children,
|
||||
redirectPath = '/auth/login',
|
||||
loadingComponent,
|
||||
unauthorizedComponent,
|
||||
}: AuthGuardProps) {
|
||||
return (
|
||||
<RouteGuard
|
||||
config={{
|
||||
requiredRoles: [], // Any authenticated user
|
||||
redirectOnUnauthorized: true,
|
||||
unauthorizedRedirectPath: redirectPath,
|
||||
}}
|
||||
loadingComponent={loadingComponent}
|
||||
unauthorizedComponent={unauthorizedComponent}
|
||||
>
|
||||
{children}
|
||||
</RouteGuard>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* useAuth Hook
|
||||
*
|
||||
* Simplified hook for checking authentication status.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* const { isAuthenticated, loading } = useAuth();
|
||||
* ```
|
||||
*/
|
||||
export { useRouteGuard as useAuthAccess } from './RouteGuard';
|
||||
137
apps/website/lib/gateways/RouteGuard.tsx
Normal file
137
apps/website/lib/gateways/RouteGuard.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Component: RouteGuard
|
||||
*
|
||||
* Higher-order component that protects routes using Gateways and Blockers.
|
||||
* Follows clean architecture by separating concerns:
|
||||
* - Gateway handles access logic
|
||||
* - Blocker handles prevention logic
|
||||
* - Component handles UI rendering
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import { AuthGateway, AuthGatewayConfig } from './AuthGateway';
|
||||
import { LoadingState } from '@/components/shared/LoadingState';
|
||||
|
||||
interface RouteGuardProps {
|
||||
children: ReactNode;
|
||||
config?: AuthGatewayConfig;
|
||||
/**
|
||||
* Custom loading component (optional)
|
||||
*/
|
||||
loadingComponent?: ReactNode;
|
||||
/**
|
||||
* Custom unauthorized component (optional)
|
||||
*/
|
||||
unauthorizedComponent?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* RouteGuard Component
|
||||
*
|
||||
* Protects child components based on authentication and authorization rules.
|
||||
* Uses Gateway pattern for access control.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* <RouteGuard config={{ requiredRoles: ['owner', 'admin'] }}>
|
||||
* <AdminDashboard />
|
||||
* </RouteGuard>
|
||||
* ```
|
||||
*/
|
||||
export function RouteGuard({
|
||||
children,
|
||||
config = {},
|
||||
loadingComponent,
|
||||
unauthorizedComponent,
|
||||
}: RouteGuardProps) {
|
||||
const router = useRouter();
|
||||
const authContext = useAuth();
|
||||
const [gateway] = useState(() => new AuthGateway(authContext, config));
|
||||
const [accessState, setAccessState] = useState(gateway.getAccessState());
|
||||
|
||||
// Update gateway when auth context changes
|
||||
useEffect(() => {
|
||||
gateway.refresh();
|
||||
setAccessState(gateway.getAccessState());
|
||||
}, [authContext.session, authContext.loading, gateway]);
|
||||
|
||||
// Handle redirects
|
||||
useEffect(() => {
|
||||
if (!accessState.canAccess && !accessState.isLoading) {
|
||||
if (config.redirectOnUnauthorized !== false) {
|
||||
const redirectPath = gateway.getUnauthorizedRedirectPath();
|
||||
|
||||
// Use a small delay to show unauthorized message briefly
|
||||
const timer = setTimeout(() => {
|
||||
router.push(redirectPath);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}, [accessState, gateway, router, config.redirectOnUnauthorized]);
|
||||
|
||||
// Show loading state
|
||||
if (accessState.isLoading) {
|
||||
return loadingComponent || (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<LoadingState message="Loading..." className="min-h-screen" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show unauthorized state
|
||||
if (!accessState.canAccess) {
|
||||
return unauthorizedComponent || (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="bg-iron-gray p-8 rounded-lg border border-charcoal-outline max-w-md text-center">
|
||||
<h2 className="text-xl font-bold text-racing-red mb-4">Access Denied</h2>
|
||||
<p className="text-gray-300 mb-6">{accessState.reason}</p>
|
||||
<button
|
||||
onClick={() => router.push('/auth/login')}
|
||||
className="px-4 py-2 bg-primary-blue text-white rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Go to Login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render protected content
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
/**
|
||||
* useRouteGuard Hook
|
||||
*
|
||||
* Hook for programmatic access control within components.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* const { canAccess, reason, isLoading } = useRouteGuard({ requiredRoles: ['admin'] });
|
||||
* ```
|
||||
*/
|
||||
export function useRouteGuard(config: AuthGatewayConfig = {}) {
|
||||
const authContext = useAuth();
|
||||
const [gateway] = useState(() => new AuthGateway(authContext, config));
|
||||
const [state, setState] = useState(gateway.getAccessState());
|
||||
|
||||
useEffect(() => {
|
||||
gateway.refresh();
|
||||
setState(gateway.getAccessState());
|
||||
}, [authContext.session, authContext.loading, gateway]);
|
||||
|
||||
return {
|
||||
canAccess: state.canAccess,
|
||||
reason: state.reason,
|
||||
isLoading: state.isLoading,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
enforceAccess: () => gateway.enforceAccess(),
|
||||
redirectIfUnauthorized: () => gateway.redirectIfUnauthorized(),
|
||||
};
|
||||
}
|
||||
13
apps/website/lib/gateways/index.ts
Normal file
13
apps/website/lib/gateways/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Gateways - Component-based access control
|
||||
*
|
||||
* Follows clean architecture by separating concerns:
|
||||
* - Blockers: Prevent execution (frontend UX)
|
||||
* - Gateways: Orchestrate access control
|
||||
* - Guards: Enforce security (backend)
|
||||
*/
|
||||
|
||||
export { AuthGateway } from './AuthGateway';
|
||||
export type { AuthGatewayConfig } from './AuthGateway';
|
||||
export { RouteGuard, useRouteGuard } from './RouteGuard';
|
||||
export { AuthGuard, useAuthAccess } from './AuthGuard';
|
||||
Reference in New Issue
Block a user