Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
379 lines
10 KiB
JavaScript
379 lines
10 KiB
JavaScript
/**
|
|
* Enhanced Router
|
|
*
|
|
* Provides enhanced routing with guards, middleware, lazy loading, and analytics.
|
|
*/
|
|
|
|
import { Logger } from '../../core/logger.js';
|
|
import { RouteGuard, BuiltInGuards } from './RouteGuard.js';
|
|
import { RouteMiddleware, BuiltInMiddleware } from './RouteMiddleware.js';
|
|
|
|
/**
|
|
* Router - Enhanced routing system
|
|
*/
|
|
export class Router {
|
|
constructor(config = {}) {
|
|
this.config = {
|
|
mode: config.mode || 'history', // 'history' | 'hash'
|
|
base: config.base || '/',
|
|
enableAnalytics: config.enableAnalytics ?? true,
|
|
...config
|
|
};
|
|
|
|
this.routes = new Map();
|
|
this.guards = new Map();
|
|
this.middleware = [];
|
|
this.beforeEachHooks = [];
|
|
this.afterEachHooks = [];
|
|
this.currentRoute = null;
|
|
this.analytics = {
|
|
navigations: [],
|
|
errors: []
|
|
};
|
|
|
|
// Initialize
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Create a new Router instance
|
|
*/
|
|
static create(config = {}) {
|
|
return new Router(config);
|
|
}
|
|
|
|
/**
|
|
* Initialize router
|
|
*/
|
|
init() {
|
|
// Handle browser navigation
|
|
window.addEventListener('popstate', (event) => {
|
|
this.handlePopState(event);
|
|
});
|
|
|
|
Logger.info('[Router] Initialized', {
|
|
mode: this.config.mode,
|
|
base: this.config.base
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Register a route
|
|
*/
|
|
route(path, config) {
|
|
const route = {
|
|
path: this.normalizePath(path),
|
|
component: config.component,
|
|
name: config.name || path,
|
|
title: config.title || null,
|
|
meta: config.meta || {},
|
|
guards: config.guards || [],
|
|
middleware: config.middleware || [],
|
|
lazy: config.lazy ?? false,
|
|
...config
|
|
};
|
|
|
|
this.routes.set(route.path, route);
|
|
|
|
Logger.debug('[Router] Route registered', route);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Register multiple routes
|
|
*/
|
|
routes(routesConfig) {
|
|
routesConfig.forEach(route => {
|
|
this.route(route.path, route);
|
|
});
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Register a guard
|
|
*/
|
|
guard(name, guardFn) {
|
|
const guard = RouteGuard.create(name, guardFn);
|
|
this.guards.set(name, guard);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Register middleware
|
|
*/
|
|
use(middleware) {
|
|
if (typeof middleware === 'function') {
|
|
this.middleware.push(RouteMiddleware.create('anonymous', middleware));
|
|
} else if (middleware instanceof RouteMiddleware) {
|
|
this.middleware.push(middleware);
|
|
} else if (typeof middleware === 'string' && BuiltInMiddleware[middleware]) {
|
|
this.middleware.push(BuiltInMiddleware[middleware]);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Add beforeEach hook
|
|
*/
|
|
beforeEach(hook) {
|
|
this.beforeEachHooks.push(hook);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Add afterEach hook
|
|
*/
|
|
afterEach(hook) {
|
|
this.afterEachHooks.push(hook);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Navigate to a route
|
|
*/
|
|
async navigate(path, options = {}) {
|
|
const normalizedPath = this.normalizePath(path);
|
|
const route = this.routes.get(normalizedPath);
|
|
|
|
if (!route) {
|
|
Logger.warn('[Router] Route not found', normalizedPath);
|
|
return false;
|
|
}
|
|
|
|
const from = this.currentRoute;
|
|
const to = {
|
|
path: normalizedPath,
|
|
name: route.name,
|
|
title: route.title,
|
|
meta: route.meta,
|
|
component: route.component
|
|
};
|
|
|
|
// Execute beforeEach hooks
|
|
for (const hook of this.beforeEachHooks) {
|
|
const result = await hook(to, from);
|
|
if (result === false || typeof result === 'string') {
|
|
if (typeof result === 'string') {
|
|
return await this.navigate(result);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Execute guards
|
|
for (const guardName of route.guards) {
|
|
const guard = this.guards.get(guardName) || BuiltInGuards[guardName];
|
|
if (!guard) {
|
|
Logger.warn('[Router] Guard not found', guardName);
|
|
continue;
|
|
}
|
|
|
|
const result = await guard.execute(to, from);
|
|
if (!result.allowed) {
|
|
if (result.redirect) {
|
|
return await this.navigate(result.redirect);
|
|
}
|
|
Logger.warn('[Router] Navigation blocked by guard', guardName);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Execute middleware
|
|
let middlewareBlocked = false;
|
|
for (const middleware of [...this.middleware, ...route.middleware]) {
|
|
await new Promise((resolve) => {
|
|
middleware.execute(to, from, (allowed) => {
|
|
if (allowed === false) {
|
|
middlewareBlocked = true;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
if (middlewareBlocked) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Load component if lazy
|
|
if (route.lazy && typeof route.component === 'function') {
|
|
try {
|
|
route.component = await route.component();
|
|
} catch (error) {
|
|
Logger.error('[Router] Failed to load lazy component', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Update current route
|
|
this.currentRoute = to;
|
|
|
|
// Update browser history
|
|
if (this.config.mode === 'history') {
|
|
history.pushState({ route: to }, route.title || '', normalizedPath);
|
|
} else {
|
|
window.location.hash = normalizedPath;
|
|
}
|
|
|
|
// Update page title
|
|
if (route.title) {
|
|
document.title = route.title;
|
|
}
|
|
|
|
// Track analytics
|
|
if (this.config.enableAnalytics) {
|
|
this.trackNavigation(to, from);
|
|
}
|
|
|
|
// Execute afterEach hooks
|
|
for (const hook of this.afterEachHooks) {
|
|
await hook(to, from);
|
|
}
|
|
|
|
// Render component
|
|
await this.renderComponent(route, options);
|
|
|
|
Logger.info('[Router] Navigated', { to: normalizedPath });
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Render component
|
|
*/
|
|
async renderComponent(route, options = {}) {
|
|
const container = options.container || document.querySelector('main');
|
|
|
|
if (!container) {
|
|
Logger.error('[Router] Container not found');
|
|
return;
|
|
}
|
|
|
|
if (typeof route.component === 'function') {
|
|
// Component is a function (render function)
|
|
const content = await route.component(route, options);
|
|
if (typeof content === 'string') {
|
|
container.innerHTML = content;
|
|
} else if (content instanceof HTMLElement) {
|
|
container.innerHTML = '';
|
|
container.appendChild(content);
|
|
}
|
|
} else if (typeof route.component === 'string') {
|
|
// Component is a selector or HTML
|
|
const element = document.querySelector(route.component);
|
|
if (element) {
|
|
container.innerHTML = '';
|
|
container.appendChild(element.cloneNode(true));
|
|
} else {
|
|
container.innerHTML = route.component;
|
|
}
|
|
}
|
|
|
|
// Re-initialize modules in new content
|
|
this.reinitializeModules(container);
|
|
}
|
|
|
|
/**
|
|
* Re-initialize modules in container
|
|
*/
|
|
reinitializeModules(container) {
|
|
// Trigger module re-initialization
|
|
const event = new CustomEvent('router:content-updated', {
|
|
detail: { container },
|
|
bubbles: true
|
|
});
|
|
document.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Handle popstate event
|
|
*/
|
|
handlePopState(event) {
|
|
const path = this.config.mode === 'history'
|
|
? window.location.pathname
|
|
: window.location.hash.slice(1);
|
|
|
|
this.navigate(path, { updateHistory: false });
|
|
}
|
|
|
|
/**
|
|
* Normalize path
|
|
*/
|
|
normalizePath(path) {
|
|
// Remove base if present
|
|
if (path.startsWith(this.config.base)) {
|
|
path = path.slice(this.config.base.length);
|
|
}
|
|
|
|
// Ensure leading slash
|
|
if (!path.startsWith('/')) {
|
|
path = '/' + path;
|
|
}
|
|
|
|
// Remove trailing slash (except root)
|
|
if (path !== '/' && path.endsWith('/')) {
|
|
path = path.slice(0, -1);
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* Track navigation for analytics
|
|
*/
|
|
trackNavigation(to, from) {
|
|
const navigation = {
|
|
to: to.path,
|
|
from: from?.path || null,
|
|
timestamp: Date.now(),
|
|
duration: from ? Date.now() - (from.timestamp || Date.now()) : 0
|
|
};
|
|
|
|
this.analytics.navigations.push(navigation);
|
|
|
|
// Limit analytics size
|
|
if (this.analytics.navigations.length > 100) {
|
|
this.analytics.navigations.shift();
|
|
}
|
|
|
|
// Trigger analytics event
|
|
const event = new CustomEvent('router:navigation', {
|
|
detail: navigation,
|
|
bubbles: true
|
|
});
|
|
window.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Get current route
|
|
*/
|
|
getCurrentRoute() {
|
|
return this.currentRoute;
|
|
}
|
|
|
|
/**
|
|
* Get analytics
|
|
*/
|
|
getAnalytics() {
|
|
return {
|
|
...this.analytics,
|
|
totalNavigations: this.analytics.navigations.length,
|
|
totalErrors: this.analytics.errors.length
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Destroy router
|
|
*/
|
|
destroy() {
|
|
this.routes.clear();
|
|
this.guards.clear();
|
|
this.middleware = [];
|
|
this.beforeEachHooks = [];
|
|
this.afterEachHooks = [];
|
|
this.currentRoute = null;
|
|
|
|
Logger.info('[Router] Destroyed');
|
|
}
|
|
}
|
|
|