Initial Version

This commit is contained in:
root
2025-12-21 10:09:54 -05:00
commit 2fbddd7dbc
366 changed files with 41999 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
/**
* Core fetch wrapper - reusable across modules
* Does not depend on stores to avoid bundling issues in library builds
*/
export interface FetchWrapperOptions {
/**
* Optional callback to handle logout on auth failure
* If not provided, only logs error without redirecting
*/
onLogout?: () => void | Promise<void>;
/**
* Enable automatic retry of failed requests after token refresh
* @default true
*/
autoRetry?: boolean;
}
// Mutex to prevent multiple simultaneous refresh attempts
class RefreshMutex {
private promise: Promise<boolean> | null = null;
async acquire(): Promise<boolean> {
if (this.promise) {
return this.promise;
}
this.promise = this.performRefresh();
const result = await this.promise;
this.promise = null;
return result;
}
private async performRefresh(): Promise<boolean> {
try {
const response = await fetch('/security/refresh', {
method: 'POST',
credentials: 'include'
});
return response.ok;
} catch (error) {
console.error('Token refresh failed:', error);
return false;
}
}
}
const tokenRefreshMutex = new RefreshMutex();
export interface RequestCallOptions {
/**
* Override autoRetry for this specific request
* @default true
*/
autoRetry?: boolean;
/**
* Skip calling onLogout callback on 401/403 errors
* Useful for authentication endpoints where 401 means invalid credentials, not session expiry
* @default false
*/
skipLogoutOnError?: boolean;
}
export function createFetchWrapper(options: FetchWrapperOptions = {}) {
const { autoRetry: defaultAutoRetry = true } = options;
return {
get: request('GET', options, defaultAutoRetry),
post: request('POST', options, defaultAutoRetry),
put: request('PUT', options, defaultAutoRetry),
delete: request('DELETE', options, defaultAutoRetry)
};
}
interface RequestOptions {
method: string;
headers: Record<string, string>;
body?: string;
credentials: 'include';
}
function request(method: string, options: FetchWrapperOptions, defaultAutoRetry: boolean) {
return async (url: string, body?: object, callOptions?: RequestCallOptions): Promise<any> => {
const autoRetry = callOptions?.autoRetry ?? defaultAutoRetry;
const requestOptions: RequestOptions = {
method,
headers: getHeaders(url),
credentials: 'include'
};
if (body) {
requestOptions.headers['Content-Type'] = 'application/json';
requestOptions.body = JSON.stringify(body);
}
try {
const response = await fetch(url, requestOptions);
if (response.status === 401 && autoRetry) {
// Try to refresh the token
const refreshSuccess = await tokenRefreshMutex.acquire();
if (refreshSuccess) {
// Retry the original request with the new token
const retryResponse = await fetch(url, requestOptions);
return handleResponse(retryResponse, options, callOptions?.skipLogoutOnError);
}
}
return handleResponse(response, options, callOptions?.skipLogoutOnError);
} catch (error) {
console.error('API error:', error);
throw error;
}
};
}
function getHeaders(_url: string): Record<string, string> {
const headers: Record<string, string> = {};
// Add CSRF token if available
const csrfToken = getCsrfTokenFromCookie();
if (csrfToken) {
headers['X-CSRF-TOKEN'] = csrfToken;
}
return headers;
}
function getCsrfTokenFromCookie(): string | null {
if (typeof document === 'undefined') return null;
const csrfCookie = document.cookie
.split('; ')
.find(row => row.startsWith('X-CSRF-TOKEN='));
return csrfCookie ? csrfCookie.split('=')[1] : null;
}
async function handleResponse(response: Response, options: FetchWrapperOptions, skipLogoutOnError?: boolean): Promise<any> {
const text = await response.text();
const data = text && JSON.parse(text);
if (!response.ok) {
if ([401, 403].includes(response.status) && !skipLogoutOnError) {
// Call logout callback if provided
if (options.onLogout) {
await options.onLogout();
} else {
console.error('Authentication failed. Please log in again.');
}
}
const error: string = (data && data.message) || response.statusText;
throw new Error(error);
}
return data;
}

View File

@@ -0,0 +1,10 @@
import { useUserStore } from '@KTXC/stores/userStore';
import { createFetchWrapper } from './fetch-wrapper-core';
// Create fetch wrapper with user store logout callback
export const fetchWrapper = createFetchWrapper({
onLogout: () => {
const { logout } = useUserStore();
logout();
}
});

View File

@@ -0,0 +1,6 @@
/**
* Shared utilities entry point for external modules
* This file is built separately and exposed via import map
*/
export { createFetchWrapper, type FetchWrapperOptions } from './fetch-wrapper-core';

103
core/src/utils/modules.ts Normal file
View File

@@ -0,0 +1,103 @@
import type { App } from 'vue';
import { router } from '@KTXC/router';
import { useModuleStore } from '@KTXC/stores/moduleStore';
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
function installModuleCSS(moduleHandle: string, cssPaths: string | string[]): void {
const cssFiles = Array.isArray(cssPaths) ? cssPaths : [cssPaths];
cssFiles.forEach((cssFile: string) => {
const cssPath = `/modules/${moduleHandle}/${cssFile}`;
const existingLink = document.querySelector(`link[href="${cssPath}"]`);
if (!existingLink) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = cssPath;
link.onload = () => {
console.log(`Module Loader - Loaded CSS for ${moduleHandle}: ${cssFile}`);
};
link.onerror = () => {
console.error(`Module Loader - Failed to load CSS for ${moduleHandle}: ${cssPath}`);
};
document.head.appendChild(link);
}
});
}
function installModuleRoutes(moduleHandle: string, routes: any[]): void {
routes.forEach((route: any) => {
// Prefix route name with module handle for safety
const prefixedRoute = {
...route,
path: `/m/${moduleHandle}${route.path}`
};
// Prefix the route name if it exists
if (route.name) {
prefixedRoute.name = `${moduleHandle}.${route.name}`;
}
// Recursively prefix child route names
if (route.children && Array.isArray(route.children)) {
prefixedRoute.children = route.children.map((child: any) => ({
...child,
name: child.name ? `${moduleHandle}.${child.name}` : undefined
}));
}
router.addRoute('private', prefixedRoute);
});
}
function installModuleIntegrations(
moduleHandle: string,
integrations: Record<string, any[]>
): void {
const integrationStore = useIntegrationStore();
integrationStore.registerModuleIntegrations(moduleHandle, integrations);
}
export async function initializeModules(app: App): Promise<void> {
const moduleStore = useModuleStore();
// First, dynamically load modules based on moduleStore boot paths
const availableModules = moduleStore.modules;
const loadPromises: Promise<void>[] = [];
for (const [moduleId, moduleInfo] of Object.entries(availableModules)) {
if (moduleInfo.handle && moduleInfo.boot && !moduleInfo.booted) {
const moduleHandle = moduleInfo.handle;
const moduleUrl = `/modules/${moduleInfo.handle}/${moduleInfo.boot}`;
console.log(`Module Loader - Loading ${moduleInfo.handle} from ${moduleUrl}`);
const loadPromise = import(/* @vite-ignore */ moduleUrl)
.then((module) => {
// Load CSS if module explicitly exports css path(s)
if (module.css) {
installModuleCSS(moduleInfo.handle, module.css);
}
// install module
console.log(`Module Loader - Installing ${moduleInfo.handle}`);
if (module.default && typeof module.default.install === 'function') {
app.use(module.default);
}
// prefix routes with /m/{moduleHandle}
console.log(`Module Loader - Installing Routes ${moduleInfo.handle}`);
if (module.routes) {
installModuleRoutes(moduleHandle, module.routes);
}
// register integrations
console.log(`Module Loader - Installing Integrations ${moduleInfo.handle}`);
if (module.integrations) {
installModuleIntegrations(moduleHandle, module.integrations);
}
})
.catch((error) => {
console.error(`Failed to load module ${moduleId} from ${moduleUrl}:`, error);
});
loadPromises.push(loadPromise);
} else if (!moduleInfo.boot) {
console.warn(`No boot path specified for module: ${moduleId}`);
}
}
// Wait for all dynamic loading to complete
await Promise.all(loadPromises);
}