Initial Version

This commit is contained in:
root
2025-12-21 10:09:54 -05:00
commit 4ae6befc7b
422 changed files with 47225 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
import { defineStore } from 'pinia';
import type {
IntegrationPointType,
IntegrationPoint,
IntegrationEntry,
IntegrationItem,
IntegrationGroup
} from '@KTXC/types/integrationTypes';
export const useIntegrationStore = defineStore('integrationStore', {
state: () => ({
points: new Map<IntegrationPointType, IntegrationPoint>(),
}),
actions: {
// Ensure an integration point exists
ensurePoint(pointType: IntegrationPointType): void {
if (!this.points.has(pointType)) {
this.points.set(pointType, { items: new Map() });
}
},
// Register a single item to an integration point
registerItem(pointType: IntegrationPointType, item: IntegrationItem): void {
this.ensurePoint(pointType);
const point = this.points.get(pointType)!;
point.items.set(item.id, item);
},
// Register a group to an integration point
registerGroup(pointType: IntegrationPointType, group: IntegrationGroup): void {
this.ensurePoint(pointType);
const point = this.points.get(pointType)!;
point.items.set(group.id, group);
},
// Bulk register from module integrations
registerModuleIntegrations(
moduleHandle: string,
integrations: Record<string, any[]>
): void {
for (const [pointType, entries] of Object.entries(integrations)) {
if (!entries || !Array.isArray(entries)) continue;
entries.forEach((entry: any) => {
const prefixedEntry = this.prefixEntry(moduleHandle, entry);
if (entry.type === 'group' || ('items' in entry && Array.isArray(entry.items))) {
this.registerGroup(pointType as IntegrationPointType, prefixedEntry as IntegrationGroup);
} else {
this.registerItem(pointType as IntegrationPointType, prefixedEntry as IntegrationItem);
}
});
}
},
// Prefix IDs and paths with module handle
prefixEntry(moduleHandle: string, entry: any): IntegrationEntry {
const prefixed: any = {
...entry,
id: entry.id ? `${moduleHandle}.${entry.id}` : `${moduleHandle}.${this.randomID()}`,
moduleHandle,
};
// Remove 'type' field as it's only used for module-side disambiguation
delete prefixed.type;
// Prefix internal paths
if (entry.path) {
prefixed.to = `/m/${moduleHandle}${entry.path}`;
delete prefixed.path;
} else if (entry.to && entry.toType !== 'external') {
prefixed.to = `/m/${moduleHandle}${entry.to}`;
}
// Recursively prefix items in groups
if (entry.items && Array.isArray(entry.items)) {
prefixed.items = entry.items.map((item: any) => this.prefixEntry(moduleHandle, item));
}
return prefixed;
},
// Update a specific item (useful for badges, visibility, etc.)
updateItem(
pointType: IntegrationPointType,
itemId: string,
updates: Partial<IntegrationItem>
): void {
const point = this.points.get(pointType);
if (!point) return;
const item = point.items.get(itemId);
if (item && !('items' in item)) {
point.items.set(itemId, { ...item, ...updates });
}
},
// Update badge for notification icons
updateBadge(
pointType: IntegrationPointType,
itemId: string,
badge: string | number | null,
badgeColor?: string
): void {
this.updateItem(pointType, itemId, { badge, badgeColor });
},
// Toggle visibility
setVisibility(
pointType: IntegrationPointType,
itemId: string,
visible: boolean
): void {
this.updateItem(pointType, itemId, { visible });
},
// Remove an item
unregisterItem(pointType: IntegrationPointType, itemId: string): void {
const point = this.points.get(pointType);
if (point) {
point.items.delete(itemId);
}
},
// Remove all items from a module
unregisterModule(moduleHandle: string): void {
this.points.forEach((point) => {
point.items.forEach((entry, id) => {
if (entry.moduleHandle === moduleHandle) {
point.items.delete(id);
}
});
});
},
reset(): void {
this.points.clear();
},
randomID(length = 8): string {
return Math.random().toString(36).substring(2, 2 + length);
},
},
getters: {
// Get all entries for an integration point, sorted by priority
getPoint: (state) => (pointType: IntegrationPointType): IntegrationEntry[] => {
const point = state.points.get(pointType);
if (!point) return [];
return Array.from(point.items.values())
.filter(entry => entry.visible !== false)
.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
},
// Get items only (no groups)
getItems: (state) => (pointType: IntegrationPointType): IntegrationItem[] => {
const point = state.points.get(pointType);
if (!point) return [];
return Array.from(point.items.values())
.filter((entry): entry is IntegrationItem => !('items' in entry) && entry.visible !== false)
.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
},
// Get groups only
getGroups: (state) => (pointType: IntegrationPointType): IntegrationGroup[] => {
const point = state.points.get(pointType);
if (!point) return [];
return Array.from(point.items.values())
.filter((entry): entry is IntegrationGroup => 'items' in entry && entry.visible !== false)
.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
},
// Get a specific item by ID
getItemById: (state) => (pointType: IntegrationPointType, itemId: string): IntegrationEntry | undefined => {
const point = state.points.get(pointType);
return point?.items.get(itemId);
},
},
});

View File

@@ -0,0 +1,85 @@
import { ref, watch } from 'vue';
import { defineStore } from 'pinia';
import config from '@KTXC/config';
import { useUserStore } from './userStore';
export type MenuMode = 'apps' | 'user-settings' | 'admin-settings';
export const useLayoutStore = defineStore('layout', () => {
// Loading state
const isLoading = ref(false);
// Sidebar state - initialize from settings or config
const userStore = useUserStore();
const sidebarDrawer = ref(userStore.getSetting('sidebar_drawer') ?? config.Sidebar_drawer);
const miniSidebar = ref(userStore.getSetting('mini_sidebar') ?? config.mini_sidebar);
const menuMode = ref<MenuMode>('apps');
// Theme state - initialize from settings or config
const theme = ref(userStore.getSetting('theme') ?? config.actTheme);
const font = ref(userStore.getSetting('font') ?? config.fontTheme);
// Watch and sync sidebar state to settings
watch(sidebarDrawer, (value) => {
userStore.setSetting('sidebar_drawer', value);
});
watch(miniSidebar, (value) => {
userStore.setSetting('mini_sidebar', value);
});
watch(theme, (value) => {
userStore.setSetting('theme', value);
});
watch(font, (value) => {
userStore.setSetting('font', value);
});
// Actions
function toggleSidebarDrawer() {
sidebarDrawer.value = !sidebarDrawer.value;
}
function setMiniSidebar(value: boolean) {
miniSidebar.value = value;
}
function setTheme(value: string) {
theme.value = value;
}
function setFont(value: string) {
font.value = value;
}
function setMenuMode(value: MenuMode) {
menuMode.value = value;
}
function toggleMenuMode() {
// Cycle through: apps -> user-settings -> admin-settings -> apps
const modes: MenuMode[] = ['apps', 'user-settings', 'admin-settings'];
const currentIndex = modes.indexOf(menuMode.value);
const nextIndex = (currentIndex + 1) % modes.length;
menuMode.value = modes[nextIndex];
}
return {
// State
isLoading,
sidebarDrawer,
miniSidebar,
menuMode,
theme,
font,
// Actions
toggleSidebarDrawer,
setMiniSidebar,
setMenuMode,
toggleMenuMode,
setTheme,
setFont
};
});

View File

@@ -0,0 +1,34 @@
import { defineStore } from 'pinia';
import type { ModuleCollection, ModuleObject } from '@KTXC/types/moduleTypes';
export const useModuleStore = defineStore('moduleStore', {
state: () => ({
modules: {} as ModuleCollection,
}),
actions: {
init(data: ModuleCollection) {
this.modules = data ?? {};
},
markBooted(ns: string) {
const targetNs = String(ns).toLowerCase();
Object.keys(this.modules).forEach((key) => {
const mod = this.modules[key] as ModuleObject;
if (!mod) return;
if (String(mod.namespace || mod.handle).toLowerCase() === targetNs) {
mod.booted = true;
}
});
},
reset() {
this.modules = {};
},
},
getters: {
has: (state) => (handleOrNamespace: string) => {
const target = String(handleOrNamespace).toLowerCase();
return Object.values(state.modules).some(
(mod) => mod && (String(mod.handle).toLowerCase() === target || String(mod.namespace).toLowerCase() === target)
);
},
},
});

View File

@@ -0,0 +1,27 @@
import { defineStore } from 'pinia';
export interface TenantState {
id: string | null;
domain: string | null;
label: string | null;
}
export const useTenantStore = defineStore('tenantStore', {
state: () => ({
tenant: null as TenantState | null,
}),
actions: {
init(tenant: Partial<TenantState> | null) {
this.tenant = tenant
? {
id: tenant.id ?? null,
domain: tenant.domain ?? null,
label: tenant.label ?? null,
}
: null;
},
reset() {
this.tenant = null;
},
},
});

View File

@@ -0,0 +1,275 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { router } from '@KTXC/router';
import { authenticationService } from '@KTXC/services/authenticationService';
import { userService } from '@KTXC/services/user/userService';
import type { AuthenticatedUser } from '@KTXC/types/authenticationTypes';
import type { UserProfileInterface } from '@KTXC/types/user/userProfileTypes';
import type { UserSettingsInterface } from '@KTXC/types/user/userSettingsTypes';
import { UserProfile } from '@KTXC/models/userProfile';
import { UserSettings } from '@KTXC/models/userSettings';
const STORAGE_KEY = 'userStore.auth';
// Flush pending updates before page unload
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => {
userService.flushAll();
});
}
export const useUserStore = defineStore('userStore', () => {
// =========================================================================
// State
// =========================================================================
const auth = ref<AuthenticatedUser | null>(
localStorage.getItem(STORAGE_KEY)
? (JSON.parse(localStorage.getItem(STORAGE_KEY)!) as AuthenticatedUser)
: null
);
const profile = ref<UserProfile>(new UserProfile());
const settings = ref<UserSettings>(new UserSettings());
const returnUrl = ref<string | null>(null);
// =========================================================================
// Authentication Getters
// =========================================================================
const isAuthenticated = computed(() => auth.value !== null);
const identifier = computed(() => auth.value?.identifier ?? null);
const identity = computed(() => auth.value?.identity ?? null);
const label = computed(() => auth.value?.label ?? null);
const roles = computed(() => auth.value?.roles ?? []);
const permissions = computed(() => auth.value?.permissions ?? []);
// =========================================================================
// Profile Getters
// =========================================================================
const profileFields = computed(() => profile.value.fields);
const editableProfileFields = computed(() => profile.value.editableFields);
const managedProfileFields = computed(() => profile.value.managedFields);
// =========================================================================
// Authentication Actions
// =========================================================================
function setAuth(authUser: AuthenticatedUser): void {
auth.value = authUser;
localStorage.setItem(STORAGE_KEY, JSON.stringify(authUser));
}
function clearAuth(): void {
auth.value = null;
localStorage.removeItem(STORAGE_KEY);
}
async function logout(): Promise<void> {
try {
// Flush any pending profile/settings updates before logout
await userService.flushAll();
await authenticationService.logout();
} catch (error) {
console.warn('Logout request failed, clearing local state:', error);
} finally {
clearAuth();
clearProfile();
clearSettings();
router.push('/login');
}
}
async function refreshToken(): Promise<boolean> {
try {
await authenticationService.refresh();
return true;
} catch (error) {
await logout();
return false;
}
}
// =========================================================================
// Profile Actions
// =========================================================================
function initProfile(profileData: UserProfileInterface): void {
profile.value = new UserProfile(profileData);
}
function clearProfile(): void {
profile.value = new UserProfile();
}
function getProfileField(key: string): any {
return profile.value.get(key);
}
function setProfileField(key: string, value: any): boolean {
const success = profile.value.set(key, value);
if (success) {
// Debounced update to backend
userService.updateProfile({ [key]: value }).catch(error => {
console.error('Failed to update profile:', error);
});
}
return success;
}
function isProfileFieldEditable(key: string): boolean {
return profile.value.isEditable(key);
}
// =========================================================================
// Settings Actions
// =========================================================================
function initSettings(settingsData: UserSettingsInterface): void {
settings.value = new UserSettings(settingsData);
}
function clearSettings(): void {
settings.value = new UserSettings();
}
function getSetting(key: string): any {
return settings.value.get(key);
}
function setSetting(key: string, value: any): void {
settings.value.set(key, value);
// Debounced update to backend
userService.updateSettings({ [key]: value }).catch(error => {
console.error('Failed to update setting:', error);
});
}
// =========================================================================
// Permission Checking
// =========================================================================
/**
* Check if user has a specific permission
* Supports wildcards: user_manager.users.* matches all user actions
*/
function hasPermission(permission: string): boolean {
const userPermissions = permissions.value;
// Exact match
if (userPermissions.includes(permission)) {
return true;
}
// Wildcard match
for (const userPerm of userPermissions) {
if (userPerm.endsWith('.*')) {
const prefix = userPerm.slice(0, -2);
if (permission.startsWith(prefix + '.')) {
return true;
}
}
}
// Full wildcard
if (userPermissions.includes('*')) {
return true;
}
return false;
}
/**
* Check if user has ANY of the permissions (OR logic)
*/
function hasAnyPermission(perms: string[]): boolean {
return perms.some(p => hasPermission(p));
}
/**
* Check if user has ALL permissions (AND logic)
*/
function hasAllPermissions(perms: string[]): boolean {
return perms.every(p => hasPermission(p));
}
/**
* Check if user has a specific role
*/
function hasRole(role: string): boolean {
return roles.value.includes(role);
}
// =========================================================================
// Initialize from /init endpoint
// =========================================================================
function init(userData: {
auth?: AuthenticatedUser;
profile?: UserProfileInterface;
settings?: UserSettingsInterface
}): void {
if (userData.auth) {
setAuth(userData.auth);
}
if (userData.profile) {
initProfile(userData.profile);
}
if (userData.settings) {
initSettings(userData.settings);
}
}
return {
// State
auth,
profile,
settings,
returnUrl,
// Auth getters
isAuthenticated,
identifier,
identity,
label,
roles,
permissions,
// Profile getters
profileFields,
editableProfileFields,
managedProfileFields,
// Auth actions
setAuth,
clearAuth,
logout,
refreshToken,
// Profile actions
initProfile,
clearProfile,
getProfileField,
setProfileField,
isProfileFieldEditable,
// Settings actions
initSettings,
clearSettings,
getSetting,
setSetting,
// Permission actions
hasPermission,
hasAnyPermission,
hasAllPermissions,
hasRole,
// Init
init,
};
});