Files
michaelschiemer/resources/js/modules/router/Router.js
Michael Schiemer 36ef2a1e2c
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
fix: Gitea Traefik routing and connection pool optimization
- 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
2025-11-09 14:46:15 +01:00

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');
}
}