Initial Version
This commit is contained in:
183
core/src/stores/integrationStore.ts
Normal file
183
core/src/stores/integrationStore.ts
Normal 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);
|
||||
},
|
||||
},
|
||||
});
|
||||
85
core/src/stores/layoutStore.ts
Normal file
85
core/src/stores/layoutStore.ts
Normal 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
|
||||
};
|
||||
});
|
||||
34
core/src/stores/moduleStore.ts
Normal file
34
core/src/stores/moduleStore.ts
Normal 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)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
27
core/src/stores/tenantStore.ts
Normal file
27
core/src/stores/tenantStore.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
});
|
||||
275
core/src/stores/userStore.ts
Normal file
275
core/src/stores/userStore.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user