feat(settings): Add complete backend Settings module
- Entities: SystemSetting, PlanSetting, TenantSetting, UserPreference - Services: SystemSettingsService, TenantSettingsService, UserPreferencesService - Controllers: system-settings, tenant-settings, user-preferences - DTOs: update-system-setting, update-tenant-setting, update-user-preference, settings-filters - Module and routes registration Implements RF-SETTINGS-001, RF-SETTINGS-002, RF-SETTINGS-003 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b937d39464
commit
8565056de3
@ -23,6 +23,7 @@ import { PurchasesModule } from './modules/purchases';
|
||||
import { InvoicesModule } from './modules/invoices';
|
||||
import { ReportsModule } from './modules/reports';
|
||||
import { DashboardModule } from './modules/dashboard';
|
||||
import { SettingsModule } from './modules/settings';
|
||||
|
||||
// Import entities from all modules for TypeORM
|
||||
import {
|
||||
@ -113,6 +114,13 @@ import {
|
||||
PaymentAllocation,
|
||||
} from './modules/invoices/entities';
|
||||
|
||||
import {
|
||||
SystemSetting,
|
||||
PlanSetting,
|
||||
TenantSetting,
|
||||
UserPreference,
|
||||
} from './modules/settings/entities';
|
||||
|
||||
/**
|
||||
* Get all entities for TypeORM configuration
|
||||
*/
|
||||
@ -181,6 +189,11 @@ export function getAllEntities() {
|
||||
InvoiceItem,
|
||||
Payment,
|
||||
PaymentAllocation,
|
||||
// Settings
|
||||
SystemSetting,
|
||||
PlanSetting,
|
||||
TenantSetting,
|
||||
UserPreference,
|
||||
];
|
||||
}
|
||||
|
||||
@ -240,6 +253,11 @@ export interface ModuleOptions {
|
||||
enabled: boolean;
|
||||
basePath?: string;
|
||||
};
|
||||
settings?: {
|
||||
enabled: boolean;
|
||||
basePath?: string;
|
||||
cacheTtlMs?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -259,6 +277,7 @@ const defaultModuleOptions: ModuleOptions = {
|
||||
invoices: { enabled: true, basePath: '/api' },
|
||||
reports: { enabled: true, basePath: '/api' },
|
||||
dashboard: { enabled: true, basePath: '/api' },
|
||||
settings: { enabled: true, basePath: '/api/v1' },
|
||||
};
|
||||
|
||||
/**
|
||||
@ -398,6 +417,17 @@ export function initializeModules(
|
||||
app.use(dashboardModule.router);
|
||||
console.log('✅ Dashboard module initialized');
|
||||
}
|
||||
|
||||
// Initialize Settings Module
|
||||
if (config.settings?.enabled) {
|
||||
const settingsModule = new SettingsModule({
|
||||
dataSource,
|
||||
basePath: config.settings.basePath,
|
||||
cacheTtlMs: config.settings.cacheTtlMs,
|
||||
});
|
||||
app.use(settingsModule.router);
|
||||
console.log('✅ Settings module initialized');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -477,6 +507,7 @@ export async function createApplication(dataSourceConfig: any): Promise<Express>
|
||||
invoices: true,
|
||||
reports: true,
|
||||
dashboard: true,
|
||||
settings: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -86,6 +86,14 @@ import {
|
||||
WithholdingType,
|
||||
} from '../modules/fiscal/entities/index.js';
|
||||
|
||||
// Import Settings Entities
|
||||
import {
|
||||
SystemSetting,
|
||||
PlanSetting,
|
||||
TenantSetting,
|
||||
UserPreference,
|
||||
} from '../modules/settings/entities/index.js';
|
||||
|
||||
/**
|
||||
* TypeORM DataSource configuration
|
||||
*
|
||||
@ -171,6 +179,11 @@ export const AppDataSource = new DataSource({
|
||||
PaymentMethod,
|
||||
PaymentType,
|
||||
WithholdingType,
|
||||
// Settings Entities
|
||||
SystemSetting,
|
||||
PlanSetting,
|
||||
TenantSetting,
|
||||
UserPreference,
|
||||
],
|
||||
|
||||
// Directorios de migraciones (para uso futuro)
|
||||
|
||||
3
src/modules/settings/controllers/index.ts
Normal file
3
src/modules/settings/controllers/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { SystemSettingsController } from './system-settings.controller';
|
||||
export { TenantSettingsController } from './tenant-settings.controller';
|
||||
export { UserPreferencesController } from './user-preferences.controller';
|
||||
232
src/modules/settings/controllers/system-settings.controller.ts
Normal file
232
src/modules/settings/controllers/system-settings.controller.ts
Normal file
@ -0,0 +1,232 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import { SystemSettingsService } from '../services';
|
||||
import { UpdateSystemSettingDto, CreateSystemSettingDto, SystemSettingsFiltersDto } from '../dto';
|
||||
import { logger } from '../../../shared/utils/logger.js';
|
||||
|
||||
/**
|
||||
* SystemSettingsController
|
||||
*
|
||||
* REST API for managing global system settings.
|
||||
*
|
||||
* Endpoints:
|
||||
* - GET /api/v1/settings/system - List all settings
|
||||
* - GET /api/v1/settings/system/public - List public settings (no auth)
|
||||
* - GET /api/v1/settings/system/categories - List categories
|
||||
* - GET /api/v1/settings/system/:key - Get setting by key
|
||||
* - PUT /api/v1/settings/system/:key - Update setting
|
||||
* - POST /api/v1/settings/system/:key/reset - Reset to default
|
||||
* - POST /api/v1/settings/system - Create new setting (admin)
|
||||
*/
|
||||
export class SystemSettingsController {
|
||||
public router: Router;
|
||||
|
||||
constructor(private readonly systemSettingsService: SystemSettingsService) {
|
||||
this.router = Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
// Public routes (no authentication required)
|
||||
this.router.get('/public', this.getPublicSettings.bind(this));
|
||||
|
||||
// Protected routes
|
||||
this.router.get('/categories', this.getCategories.bind(this));
|
||||
this.router.get('/', this.getAll.bind(this));
|
||||
this.router.get('/:key', this.getByKey.bind(this));
|
||||
this.router.put('/:key', this.update.bind(this));
|
||||
this.router.post('/:key/reset', this.reset.bind(this));
|
||||
this.router.post('/', this.create.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/system
|
||||
* Get all system settings with optional filtering
|
||||
*/
|
||||
private async getAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const { category, isPublic, isEditable, search } = req.query;
|
||||
|
||||
const filters: SystemSettingsFiltersDto = {
|
||||
category: category as string,
|
||||
isPublic: isPublic !== undefined ? isPublic === 'true' : undefined,
|
||||
isEditable: isEditable !== undefined ? isEditable === 'true' : undefined,
|
||||
search: search as string,
|
||||
};
|
||||
|
||||
const settings = await this.systemSettingsService.getAll(filters);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: settings,
|
||||
total: settings.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/system/public
|
||||
* Get all public system settings (no authentication required)
|
||||
*/
|
||||
private async getPublicSettings(_req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const settings = await this.systemSettingsService.getPublicSettings();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: settings,
|
||||
total: settings.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/system/categories
|
||||
* Get all unique setting categories
|
||||
*/
|
||||
private async getCategories(_req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const categories = await this.systemSettingsService.getCategories();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: categories,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/system/:key
|
||||
* Get a single setting by key
|
||||
*/
|
||||
private async getByKey(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const { key } = req.params;
|
||||
const setting = await this.systemSettingsService.getByKey(key);
|
||||
|
||||
if (!setting) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Setting '${key}' not found`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: setting,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/v1/settings/system/:key
|
||||
* Update a system setting
|
||||
*/
|
||||
private async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const { key } = req.params;
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
const dto: UpdateSystemSettingDto = req.body;
|
||||
|
||||
const setting = await this.systemSettingsService.update(key, dto, userId);
|
||||
|
||||
if (!setting) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Setting '${key}' not found`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('System setting updated via API', { key, userId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: setting,
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as Error).message.includes('not editable')) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/settings/system/:key/reset
|
||||
* Reset a setting to its default value
|
||||
*/
|
||||
private async reset(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const { key } = req.params;
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
|
||||
const setting = await this.systemSettingsService.reset(key, userId);
|
||||
|
||||
if (!setting) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Setting '${key}' not found`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('System setting reset via API', { key, userId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: setting,
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as Error).message.includes('no default value')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/settings/system
|
||||
* Create a new system setting (admin only)
|
||||
*/
|
||||
private async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
const dto: CreateSystemSettingDto = req.body;
|
||||
|
||||
const setting = await this.systemSettingsService.create(dto, userId);
|
||||
|
||||
logger.info('System setting created via API', { key: dto.key, userId });
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: setting,
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as Error).message.includes('already exists')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
366
src/modules/settings/controllers/tenant-settings.controller.ts
Normal file
366
src/modules/settings/controllers/tenant-settings.controller.ts
Normal file
@ -0,0 +1,366 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import { TenantSettingsService } from '../services';
|
||||
import {
|
||||
UpdateTenantSettingDto,
|
||||
CreateTenantSettingDto,
|
||||
BulkUpdateTenantSettingsDto,
|
||||
TenantSettingsFiltersDto,
|
||||
} from '../dto';
|
||||
import { logger } from '../../../shared/utils/logger.js';
|
||||
|
||||
/**
|
||||
* TenantSettingsController
|
||||
*
|
||||
* REST API for managing tenant-specific settings.
|
||||
*
|
||||
* Endpoints:
|
||||
* - GET /api/v1/settings/tenant - List all tenant settings
|
||||
* - GET /api/v1/settings/tenant/effective - Get all effective settings (with inheritance)
|
||||
* - GET /api/v1/settings/tenant/:key - Get setting by key
|
||||
* - GET /api/v1/settings/tenant/:key/effective - Get effective value
|
||||
* - PUT /api/v1/settings/tenant/:key - Update setting
|
||||
* - POST /api/v1/settings/tenant - Create new setting
|
||||
* - POST /api/v1/settings/tenant/bulk - Bulk update settings
|
||||
* - POST /api/v1/settings/tenant/:key/reset - Reset to inherited value
|
||||
* - DELETE /api/v1/settings/tenant/:key - Delete setting override
|
||||
*/
|
||||
export class TenantSettingsController {
|
||||
public router: Router;
|
||||
|
||||
constructor(private readonly tenantSettingsService: TenantSettingsService) {
|
||||
this.router = Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
this.router.get('/effective', this.getAllEffective.bind(this));
|
||||
this.router.get('/', this.getAll.bind(this));
|
||||
this.router.get('/:key/effective', this.getEffective.bind(this));
|
||||
this.router.get('/:key', this.get.bind(this));
|
||||
this.router.put('/:key', this.update.bind(this));
|
||||
this.router.post('/bulk', this.bulkUpdate.bind(this));
|
||||
this.router.post('/:key/reset', this.reset.bind(this));
|
||||
this.router.post('/', this.create.bind(this));
|
||||
this.router.delete('/:key', this.delete.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get tenant ID from request
|
||||
*/
|
||||
private getTenantId(req: Request): string | null {
|
||||
return (req.headers['x-tenant-id'] as string) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get plan ID from request (optional)
|
||||
*/
|
||||
private getPlanId(req: Request): string | undefined {
|
||||
return req.headers['x-plan-id'] as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/tenant
|
||||
* Get all tenant settings with optional filtering
|
||||
*/
|
||||
private async getAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
if (!tenantId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Tenant ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { inheritedFrom, isOverridden, search } = req.query;
|
||||
|
||||
const filters: TenantSettingsFiltersDto = {
|
||||
inheritedFrom: inheritedFrom as 'system' | 'plan' | 'custom',
|
||||
isOverridden: isOverridden !== undefined ? isOverridden === 'true' : undefined,
|
||||
search: search as string,
|
||||
};
|
||||
|
||||
const settings = await this.tenantSettingsService.getAll(tenantId, filters);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: settings,
|
||||
total: settings.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/tenant/effective
|
||||
* Get all effective settings with inheritance resolution
|
||||
*/
|
||||
private async getAllEffective(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
if (!tenantId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Tenant ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const planId = this.getPlanId(req);
|
||||
const settings = await this.tenantSettingsService.getAllEffective(tenantId, planId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: settings,
|
||||
total: settings.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/tenant/:key
|
||||
* Get a tenant setting by key (direct, without inheritance)
|
||||
*/
|
||||
private async get(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
if (!tenantId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Tenant ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = req.params;
|
||||
const setting = await this.tenantSettingsService.get(tenantId, key);
|
||||
|
||||
if (!setting) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Tenant setting '${key}' not found`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: setting,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/tenant/:key/effective
|
||||
* Get the effective value for a setting with inheritance
|
||||
*/
|
||||
private async getEffective(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
if (!tenantId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Tenant ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = req.params;
|
||||
const planId = this.getPlanId(req);
|
||||
|
||||
const effective = await this.tenantSettingsService.getEffective(tenantId, key, planId);
|
||||
|
||||
if (!effective) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Setting '${key}' not found`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: effective,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/v1/settings/tenant/:key
|
||||
* Update a tenant setting
|
||||
*/
|
||||
private async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
if (!tenantId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Tenant ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = req.params;
|
||||
const dto: UpdateTenantSettingDto = req.body;
|
||||
|
||||
const setting = await this.tenantSettingsService.update(tenantId, key, dto);
|
||||
|
||||
logger.info('Tenant setting updated via API', { tenantId, key });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: setting,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/settings/tenant
|
||||
* Create a new tenant setting
|
||||
*/
|
||||
private async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
if (!tenantId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Tenant ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dto: CreateTenantSettingDto = req.body;
|
||||
const setting = await this.tenantSettingsService.create(tenantId, dto);
|
||||
|
||||
logger.info('Tenant setting created via API', { tenantId, key: dto.key });
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: setting,
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as Error).message.includes('already exists')) {
|
||||
res.status(409).json({
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/settings/tenant/bulk
|
||||
* Bulk update tenant settings
|
||||
*/
|
||||
private async bulkUpdate(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
if (!tenantId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Tenant ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dto: BulkUpdateTenantSettingsDto = req.body;
|
||||
const settings = await this.tenantSettingsService.bulkUpdate(tenantId, dto.settings);
|
||||
|
||||
logger.info('Tenant settings bulk updated via API', {
|
||||
tenantId,
|
||||
count: dto.settings.length,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: settings,
|
||||
total: settings.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/settings/tenant/:key/reset
|
||||
* Reset a tenant setting to inherited value
|
||||
*/
|
||||
private async reset(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
if (!tenantId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Tenant ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = req.params;
|
||||
const deleted = await this.tenantSettingsService.reset(tenantId, key);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Tenant setting '${key}' not found`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Tenant setting reset via API', { tenantId, key });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Setting '${key}' reset to inherited value`,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/v1/settings/tenant/:key
|
||||
* Delete a tenant setting override
|
||||
*/
|
||||
private async delete(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = this.getTenantId(req);
|
||||
if (!tenantId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Tenant ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = req.params;
|
||||
const deleted = await this.tenantSettingsService.delete(tenantId, key);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Tenant setting '${key}' not found`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Tenant setting deleted via API', { tenantId, key });
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
331
src/modules/settings/controllers/user-preferences.controller.ts
Normal file
331
src/modules/settings/controllers/user-preferences.controller.ts
Normal file
@ -0,0 +1,331 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import { UserPreferencesService } from '../services';
|
||||
import {
|
||||
UpdateUserPreferenceDto,
|
||||
BulkUpdateUserPreferencesDto,
|
||||
SetUserPreferencesDto,
|
||||
UserPreferencesFiltersDto,
|
||||
} from '../dto';
|
||||
import { logger } from '../../../shared/utils/logger.js';
|
||||
|
||||
/**
|
||||
* UserPreferencesController
|
||||
*
|
||||
* REST API for managing user-specific preferences.
|
||||
*
|
||||
* Endpoints:
|
||||
* - GET /api/v1/settings/user - Get all user preferences
|
||||
* - GET /api/v1/settings/user/grouped - Get preferences grouped by category
|
||||
* - GET /api/v1/settings/user/:key - Get preference by key
|
||||
* - PATCH /api/v1/settings/user/:key - Update preference
|
||||
* - PATCH /api/v1/settings/user - Bulk update preferences
|
||||
* - PUT /api/v1/settings/user - Set all preferences
|
||||
* - POST /api/v1/settings/user/:key/reset - Reset preference to default
|
||||
* - POST /api/v1/settings/user/reset - Reset all preferences
|
||||
* - DELETE /api/v1/settings/user/:key - Delete preference
|
||||
*/
|
||||
export class UserPreferencesController {
|
||||
public router: Router;
|
||||
|
||||
constructor(private readonly userPreferencesService: UserPreferencesService) {
|
||||
this.router = Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
this.router.get('/grouped', this.getGrouped.bind(this));
|
||||
this.router.get('/', this.getAll.bind(this));
|
||||
this.router.get('/:key', this.get.bind(this));
|
||||
this.router.patch('/:key', this.update.bind(this));
|
||||
this.router.patch('/', this.bulkUpdate.bind(this));
|
||||
this.router.put('/', this.setAll.bind(this));
|
||||
this.router.post('/reset', this.resetAll.bind(this));
|
||||
this.router.post('/:key/reset', this.reset.bind(this));
|
||||
this.router.delete('/:key', this.delete.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get user ID from request
|
||||
*/
|
||||
private getUserId(req: Request): string | null {
|
||||
return (req.headers['x-user-id'] as string) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/user
|
||||
* Get all user preferences
|
||||
*/
|
||||
private async getAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const userId = this.getUserId(req);
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'User ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { category, search } = req.query;
|
||||
|
||||
const filters: UserPreferencesFiltersDto = {
|
||||
category: category as string,
|
||||
search: search as string,
|
||||
};
|
||||
|
||||
const preferences = await this.userPreferencesService.getAll(userId, filters);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: preferences,
|
||||
total: preferences.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/user/grouped
|
||||
* Get all preferences grouped by category
|
||||
*/
|
||||
private async getGrouped(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const userId = this.getUserId(req);
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'User ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const grouped = await this.userPreferencesService.getGroupedByCategory(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: grouped,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/settings/user/:key
|
||||
* Get a single preference by key
|
||||
*/
|
||||
private async get(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const userId = this.getUserId(req);
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'User ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = req.params;
|
||||
const value = await this.userPreferencesService.getValue(userId, key);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
key,
|
||||
value,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/v1/settings/user/:key
|
||||
* Update a single preference
|
||||
*/
|
||||
private async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const userId = this.getUserId(req);
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'User ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = req.params;
|
||||
const dto: UpdateUserPreferenceDto = req.body;
|
||||
|
||||
const preference = await this.userPreferencesService.update(userId, key, dto);
|
||||
|
||||
logger.debug('User preference updated via API', { userId, key });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: preference,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/v1/settings/user
|
||||
* Bulk update preferences
|
||||
*/
|
||||
private async bulkUpdate(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const userId = this.getUserId(req);
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'User ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dto: BulkUpdateUserPreferencesDto = req.body;
|
||||
const preferences = await this.userPreferencesService.bulkUpdate(userId, dto);
|
||||
|
||||
logger.info('User preferences bulk updated via API', {
|
||||
userId,
|
||||
count: dto.preferences.length,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: preferences,
|
||||
total: preferences.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/v1/settings/user
|
||||
* Set all standard preferences at once
|
||||
*/
|
||||
private async setAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const userId = this.getUserId(req);
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'User ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dto: SetUserPreferencesDto = req.body;
|
||||
const preferences = await this.userPreferencesService.setPreferences(userId, dto);
|
||||
|
||||
logger.info('User preferences set via API', { userId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: preferences,
|
||||
total: preferences.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/settings/user/:key/reset
|
||||
* Reset a preference to default
|
||||
*/
|
||||
private async reset(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const userId = this.getUserId(req);
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'User ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = req.params;
|
||||
const preference = await this.userPreferencesService.reset(userId, key);
|
||||
|
||||
logger.debug('User preference reset via API', { userId, key });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: preference,
|
||||
message: `Preference '${key}' reset to default`,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/settings/user/reset
|
||||
* Reset all preferences to defaults
|
||||
*/
|
||||
private async resetAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const userId = this.getUserId(req);
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'User ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const preferences = await this.userPreferencesService.resetAll(userId);
|
||||
|
||||
logger.info('User preferences reset to defaults via API', { userId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: preferences,
|
||||
total: preferences.length,
|
||||
message: 'All preferences reset to defaults',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/v1/settings/user/:key
|
||||
* Delete a preference
|
||||
*/
|
||||
private async delete(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const userId = this.getUserId(req);
|
||||
if (!userId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'User ID is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = req.params;
|
||||
const deleted = await this.userPreferencesService.delete(userId, key);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: `Preference '${key}' not found`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('User preference deleted via API', { userId, key });
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/modules/settings/dto/index.ts
Normal file
24
src/modules/settings/dto/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export type {
|
||||
UpdateSystemSettingDto,
|
||||
CreateSystemSettingDto,
|
||||
} from './update-system-setting.dto';
|
||||
|
||||
export type {
|
||||
UpdateTenantSettingDto,
|
||||
CreateTenantSettingDto,
|
||||
BulkUpdateTenantSettingsDto,
|
||||
} from './update-tenant-setting.dto';
|
||||
|
||||
export type {
|
||||
UpdateUserPreferenceDto,
|
||||
PreferenceItem,
|
||||
BulkUpdateUserPreferencesDto,
|
||||
SetUserPreferencesDto,
|
||||
} from './update-user-preference.dto';
|
||||
|
||||
export type {
|
||||
SystemSettingsFiltersDto,
|
||||
TenantSettingsFiltersDto,
|
||||
UserPreferencesFiltersDto,
|
||||
SettingsPaginationDto,
|
||||
} from './settings-filters.dto';
|
||||
37
src/modules/settings/dto/settings-filters.dto.ts
Normal file
37
src/modules/settings/dto/settings-filters.dto.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Filters for querying system settings.
|
||||
*/
|
||||
export interface SystemSettingsFiltersDto {
|
||||
category?: string;
|
||||
isPublic?: boolean;
|
||||
isEditable?: boolean;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters for querying tenant settings.
|
||||
*/
|
||||
export interface TenantSettingsFiltersDto {
|
||||
category?: string;
|
||||
inheritedFrom?: 'system' | 'plan' | 'custom';
|
||||
isOverridden?: boolean;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters for querying user preferences.
|
||||
*/
|
||||
export interface UserPreferencesFiltersDto {
|
||||
category?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination parameters for settings queries.
|
||||
*/
|
||||
export interface SettingsPaginationDto {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
26
src/modules/settings/dto/update-system-setting.dto.ts
Normal file
26
src/modules/settings/dto/update-system-setting.dto.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* DTO for updating a system setting.
|
||||
* Only allows updating the value and editable fields that admins should modify.
|
||||
*/
|
||||
export interface UpdateSystemSettingDto {
|
||||
value?: any;
|
||||
description?: string;
|
||||
isPublic?: boolean;
|
||||
isEditable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for creating a system setting.
|
||||
* Used for initial setup or adding new system-wide settings.
|
||||
*/
|
||||
export interface CreateSystemSettingDto {
|
||||
key: string;
|
||||
value: any;
|
||||
dataType?: 'string' | 'number' | 'boolean' | 'json' | 'array' | 'secret';
|
||||
category: string;
|
||||
description?: string;
|
||||
isPublic?: boolean;
|
||||
isEditable?: boolean;
|
||||
defaultValue?: any;
|
||||
validationRules?: Record<string, any>;
|
||||
}
|
||||
31
src/modules/settings/dto/update-tenant-setting.dto.ts
Normal file
31
src/modules/settings/dto/update-tenant-setting.dto.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* DTO for updating a tenant setting.
|
||||
* Allows tenant admins to customize settings for their organization.
|
||||
*/
|
||||
export interface UpdateTenantSettingDto {
|
||||
value: any;
|
||||
isOverridden?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for creating/upserting a tenant setting.
|
||||
* If setting exists, it updates; otherwise creates new.
|
||||
*/
|
||||
export interface CreateTenantSettingDto {
|
||||
key: string;
|
||||
value: any;
|
||||
inheritedFrom?: 'system' | 'plan' | 'custom';
|
||||
isOverridden?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for bulk updating tenant settings.
|
||||
* Useful for settings pages that submit multiple changes at once.
|
||||
*/
|
||||
export interface BulkUpdateTenantSettingsDto {
|
||||
settings: Array<{
|
||||
key: string;
|
||||
value: any;
|
||||
isOverridden?: boolean;
|
||||
}>;
|
||||
}
|
||||
36
src/modules/settings/dto/update-user-preference.dto.ts
Normal file
36
src/modules/settings/dto/update-user-preference.dto.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* DTO for updating a single user preference.
|
||||
*/
|
||||
export interface UpdateUserPreferenceDto {
|
||||
value: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single preference item for bulk operations.
|
||||
*/
|
||||
export interface PreferenceItem {
|
||||
key: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for bulk updating user preferences.
|
||||
* Supports updating multiple preferences in a single request.
|
||||
*/
|
||||
export interface BulkUpdateUserPreferencesDto {
|
||||
preferences: PreferenceItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for setting all user preferences at once.
|
||||
* Typically used for initial preference setup or reset.
|
||||
*/
|
||||
export interface SetUserPreferencesDto {
|
||||
theme?: 'light' | 'dark' | 'system';
|
||||
language?: string;
|
||||
dateFormat?: string;
|
||||
numberFormat?: string;
|
||||
notificationsEmail?: boolean;
|
||||
notificationsPush?: boolean;
|
||||
notificationsInApp?: boolean;
|
||||
}
|
||||
4
src/modules/settings/entities/index.ts
Normal file
4
src/modules/settings/entities/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { SystemSetting } from './system-setting.entity';
|
||||
export { PlanSetting } from './plan-setting.entity';
|
||||
export { TenantSetting } from './tenant-setting.entity';
|
||||
export { UserPreference } from './user-preference.entity';
|
||||
41
src/modules/settings/entities/plan-setting.entity.ts
Normal file
41
src/modules/settings/entities/plan-setting.entity.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* Plan Settings Entity (schema: core_settings.plan_settings)
|
||||
*
|
||||
* Default configuration settings per subscription plan.
|
||||
* These settings override system defaults for tenants on a specific plan.
|
||||
*
|
||||
* Hierarchy: system_settings < plan_settings < tenant_settings
|
||||
*/
|
||||
@Entity({ name: 'plan_settings', schema: 'core_settings' })
|
||||
@Unique(['planId', 'key'])
|
||||
export class PlanSetting {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'plan_id', type: 'uuid' })
|
||||
planId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'key', type: 'varchar', length: 100 })
|
||||
key: string;
|
||||
|
||||
@Column({ name: 'value', type: 'jsonb' })
|
||||
value: any;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
73
src/modules/settings/entities/system-setting.entity.ts
Normal file
73
src/modules/settings/entities/system-setting.entity.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* System Settings Entity (schema: core_settings.system_settings)
|
||||
*
|
||||
* Global system configuration settings that apply across all tenants.
|
||||
* These settings define default behaviors and system-wide parameters.
|
||||
*
|
||||
* Data types supported:
|
||||
* - string: Text values
|
||||
* - number: Numeric values
|
||||
* - boolean: True/false values
|
||||
* - json: Complex JSON objects
|
||||
* - array: JSON arrays
|
||||
* - secret: Encrypted sensitive values (displayed as masked)
|
||||
*/
|
||||
@Entity({ name: 'system_settings', schema: 'core_settings' })
|
||||
@Unique(['key'])
|
||||
export class SystemSetting {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'key', type: 'varchar', length: 100 })
|
||||
key: string;
|
||||
|
||||
@Column({ name: 'value', type: 'jsonb' })
|
||||
value: any;
|
||||
|
||||
@Column({
|
||||
name: 'data_type',
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: 'string',
|
||||
})
|
||||
dataType: 'string' | 'number' | 'boolean' | 'json' | 'array' | 'secret';
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'category', type: 'varchar', length: 50 })
|
||||
category: string;
|
||||
|
||||
@Column({ name: 'description', type: 'text', nullable: true })
|
||||
description: string | null;
|
||||
|
||||
@Column({ name: 'is_public', type: 'boolean', default: false })
|
||||
isPublic: boolean;
|
||||
|
||||
@Column({ name: 'is_editable', type: 'boolean', default: true })
|
||||
isEditable: boolean;
|
||||
|
||||
@Column({ name: 'default_value', type: 'jsonb', nullable: true })
|
||||
defaultValue: any;
|
||||
|
||||
@Column({ name: 'validation_rules', type: 'jsonb', default: '{}' })
|
||||
validationRules: Record<string, any>;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
|
||||
updatedBy: string | null;
|
||||
}
|
||||
54
src/modules/settings/entities/tenant-setting.entity.ts
Normal file
54
src/modules/settings/entities/tenant-setting.entity.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* Tenant Settings Entity (schema: core_settings.tenant_settings)
|
||||
*
|
||||
* Custom configuration settings per tenant.
|
||||
* These settings override both system and plan defaults.
|
||||
*
|
||||
* Hierarchy: system_settings < plan_settings < tenant_settings
|
||||
*
|
||||
* RLS Policy: tenant_id = current_setting('app.current_tenant_id')
|
||||
*/
|
||||
@Entity({ name: 'tenant_settings', schema: 'core_settings' })
|
||||
@Unique(['tenantId', 'key'])
|
||||
export class TenantSetting {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'key', type: 'varchar', length: 100 })
|
||||
key: string;
|
||||
|
||||
@Column({ name: 'value', type: 'jsonb' })
|
||||
value: any;
|
||||
|
||||
@Column({
|
||||
name: 'inherited_from',
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: 'custom',
|
||||
})
|
||||
inheritedFrom: 'system' | 'plan' | 'custom';
|
||||
|
||||
@Column({ name: 'is_overridden', type: 'boolean', default: true })
|
||||
isOverridden: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
51
src/modules/settings/entities/user-preference.entity.ts
Normal file
51
src/modules/settings/entities/user-preference.entity.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* User Preferences Entity (schema: core_settings.user_preferences)
|
||||
*
|
||||
* Personal preferences for individual users.
|
||||
* These are user-specific settings like theme, language, notification preferences.
|
||||
*
|
||||
* Common keys:
|
||||
* - ui.theme: 'light' | 'dark' | 'system'
|
||||
* - ui.language: ISO language code
|
||||
* - ui.dateFormat: Date format string
|
||||
* - ui.numberFormat: Number format locale
|
||||
* - notifications.email: boolean
|
||||
* - notifications.push: boolean
|
||||
* - notifications.inApp: boolean
|
||||
*/
|
||||
@Entity({ name: 'user_preferences', schema: 'core_settings' })
|
||||
@Unique(['userId', 'key'])
|
||||
export class UserPreference {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'user_id', type: 'uuid' })
|
||||
userId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ name: 'key', type: 'varchar', length: 100 })
|
||||
key: string;
|
||||
|
||||
@Column({ name: 'value', type: 'jsonb' })
|
||||
value: any;
|
||||
|
||||
@Column({ name: 'synced_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
|
||||
syncedAt: Date;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
15
src/modules/settings/index.ts
Normal file
15
src/modules/settings/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// Module exports
|
||||
export { SettingsModule, SettingsModuleOptions } from './settings.module';
|
||||
export { createSettingsRoutes } from './settings.routes';
|
||||
|
||||
// Entity exports
|
||||
export * from './entities';
|
||||
|
||||
// Service exports
|
||||
export * from './services';
|
||||
|
||||
// Controller exports
|
||||
export * from './controllers';
|
||||
|
||||
// DTO exports
|
||||
export * from './dto';
|
||||
4
src/modules/settings/services/index.ts
Normal file
4
src/modules/settings/services/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { SystemSettingsService } from './system-settings.service';
|
||||
export { TenantSettingsService, EffectiveSetting } from './tenant-settings.service';
|
||||
export { UserPreferencesService } from './user-preferences.service';
|
||||
export { SettingsService, UserSettingsContext } from './settings.service';
|
||||
215
src/modules/settings/services/settings.service.ts
Normal file
215
src/modules/settings/services/settings.service.ts
Normal file
@ -0,0 +1,215 @@
|
||||
import { SystemSettingsService } from './system-settings.service';
|
||||
import { TenantSettingsService, EffectiveSetting } from './tenant-settings.service';
|
||||
import { UserPreferencesService } from './user-preferences.service';
|
||||
import { SystemSetting, TenantSetting, UserPreference } from '../entities';
|
||||
import { logger } from '../../../shared/utils/logger.js';
|
||||
|
||||
/**
|
||||
* Combined settings response for a user context
|
||||
*/
|
||||
export interface UserSettingsContext {
|
||||
system: SystemSetting[];
|
||||
tenant: EffectiveSetting[];
|
||||
user: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* SettingsService
|
||||
*
|
||||
* Orchestrator service that combines system, tenant, and user preferences
|
||||
* to provide a unified settings interface.
|
||||
*
|
||||
* Features:
|
||||
* - Get all settings for a user context
|
||||
* - Resolve effective settings with proper inheritance
|
||||
* - Validate settings against rules
|
||||
*/
|
||||
export class SettingsService {
|
||||
constructor(
|
||||
private readonly systemSettingsService: SystemSettingsService,
|
||||
private readonly tenantSettingsService: TenantSettingsService,
|
||||
private readonly userPreferencesService: UserPreferencesService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get all settings for a user context
|
||||
* Combines system, tenant effective, and user preferences
|
||||
*/
|
||||
async getSettingsContext(
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
planId?: string
|
||||
): Promise<UserSettingsContext> {
|
||||
const [system, tenant, user] = await Promise.all([
|
||||
this.systemSettingsService.getPublicSettings(),
|
||||
this.tenantSettingsService.getAllEffective(tenantId, planId),
|
||||
this.userPreferencesService.getAllAsMap(userId),
|
||||
]);
|
||||
|
||||
logger.debug('Settings context loaded', {
|
||||
userId,
|
||||
tenantId,
|
||||
systemCount: system.length,
|
||||
tenantCount: tenant.length,
|
||||
userCount: Object.keys(user).length,
|
||||
});
|
||||
|
||||
return { system, tenant, user };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific setting value with full inheritance resolution
|
||||
*/
|
||||
async getSetting(
|
||||
key: string,
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
planId?: string
|
||||
): Promise<any> {
|
||||
// User preferences have highest priority for user-specific keys
|
||||
if (key.startsWith('ui.') || key.startsWith('notifications.')) {
|
||||
const userValue = await this.userPreferencesService.getValue(userId, key);
|
||||
if (userValue !== null) {
|
||||
return userValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Tenant settings next
|
||||
const tenantSetting = await this.tenantSettingsService.getEffective(
|
||||
tenantId,
|
||||
key,
|
||||
planId
|
||||
);
|
||||
if (tenantSetting) {
|
||||
return tenantSetting.value;
|
||||
}
|
||||
|
||||
// Fall back to system setting
|
||||
const systemValue = await this.systemSettingsService.getValue(key);
|
||||
return systemValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple settings at once
|
||||
*/
|
||||
async getSettings(
|
||||
keys: string[],
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
planId?: string
|
||||
): Promise<Record<string, any>> {
|
||||
const results: Record<string, any> = {};
|
||||
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
results[key] = await this.getSetting(key, userId, tenantId, planId);
|
||||
})
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a setting value against its validation rules
|
||||
*/
|
||||
async validateSetting(key: string, value: any): Promise<{ valid: boolean; errors: string[] }> {
|
||||
const setting = await this.systemSettingsService.getByKey(key);
|
||||
|
||||
if (!setting) {
|
||||
return { valid: true, errors: [] };
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
const rules = setting.validationRules || {};
|
||||
|
||||
// Type validation
|
||||
if (setting.dataType === 'number' && typeof value !== 'number') {
|
||||
errors.push(`Value must be a number`);
|
||||
}
|
||||
if (setting.dataType === 'boolean' && typeof value !== 'boolean') {
|
||||
errors.push(`Value must be a boolean`);
|
||||
}
|
||||
if (setting.dataType === 'string' && typeof value !== 'string') {
|
||||
errors.push(`Value must be a string`);
|
||||
}
|
||||
if (setting.dataType === 'array' && !Array.isArray(value)) {
|
||||
errors.push(`Value must be an array`);
|
||||
}
|
||||
|
||||
// Range validation
|
||||
if (rules.min !== undefined && typeof value === 'number' && value < rules.min) {
|
||||
errors.push(`Value must be at least ${rules.min}`);
|
||||
}
|
||||
if (rules.max !== undefined && typeof value === 'number' && value > rules.max) {
|
||||
errors.push(`Value must be at most ${rules.max}`);
|
||||
}
|
||||
|
||||
// Length validation
|
||||
if (rules.minLength !== undefined && typeof value === 'string' && value.length < rules.minLength) {
|
||||
errors.push(`Value must be at least ${rules.minLength} characters`);
|
||||
}
|
||||
if (rules.maxLength !== undefined && typeof value === 'string' && value.length > rules.maxLength) {
|
||||
errors.push(`Value must be at most ${rules.maxLength} characters`);
|
||||
}
|
||||
|
||||
// Enum validation
|
||||
if (rules.enum && !rules.enum.includes(value)) {
|
||||
errors.push(`Value must be one of: ${rules.enum.join(', ')}`);
|
||||
}
|
||||
|
||||
// Pattern validation
|
||||
if (rules.pattern && typeof value === 'string') {
|
||||
const regex = new RegExp(rules.pattern);
|
||||
if (!regex.test(value)) {
|
||||
errors.push(`Value does not match required pattern`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all caches
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.systemSettingsService.clearCache();
|
||||
logger.info('Settings cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize settings for a new user
|
||||
*/
|
||||
async initializeUser(userId: string): Promise<void> {
|
||||
await this.userPreferencesService.initializeDefaults(userId);
|
||||
logger.info('User settings initialized', { userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings by category across all levels
|
||||
*/
|
||||
async getByCategory(
|
||||
category: string,
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
planId?: string
|
||||
): Promise<{
|
||||
system: SystemSetting[];
|
||||
tenant: EffectiveSetting[];
|
||||
user: Record<string, any>;
|
||||
}> {
|
||||
const [systemAll, tenantAll, userAll] = await Promise.all([
|
||||
this.systemSettingsService.getByCategory(category),
|
||||
this.tenantSettingsService.getAllEffective(tenantId, planId),
|
||||
this.userPreferencesService.getGroupedByCategory(userId),
|
||||
]);
|
||||
|
||||
return {
|
||||
system: systemAll,
|
||||
tenant: tenantAll.filter((s) => s.category === category),
|
||||
user: userAll[category] || {},
|
||||
};
|
||||
}
|
||||
}
|
||||
294
src/modules/settings/services/system-settings.service.ts
Normal file
294
src/modules/settings/services/system-settings.service.ts
Normal file
@ -0,0 +1,294 @@
|
||||
import { Repository, ILike, FindOptionsWhere } from 'typeorm';
|
||||
import { SystemSetting } from '../entities';
|
||||
import { UpdateSystemSettingDto, CreateSystemSettingDto, SystemSettingsFiltersDto } from '../dto';
|
||||
import { logger } from '../../../shared/utils/logger.js';
|
||||
|
||||
/**
|
||||
* Cache entry for system settings
|
||||
*/
|
||||
interface CacheEntry {
|
||||
value: any;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemSettingsService
|
||||
*
|
||||
* Manages global system settings that apply across all tenants.
|
||||
* Implements in-memory caching for performance optimization.
|
||||
*
|
||||
* Features:
|
||||
* - CRUD operations for system settings
|
||||
* - In-memory cache with TTL
|
||||
* - Category-based filtering
|
||||
* - Reset to default values
|
||||
*/
|
||||
export class SystemSettingsService {
|
||||
private cache: Map<string, CacheEntry> = new Map();
|
||||
private cacheTtlMs: number = 5 * 60 * 1000; // 5 minutes default
|
||||
|
||||
constructor(private readonly settingRepository: Repository<SystemSetting>) {}
|
||||
|
||||
/**
|
||||
* Sets the cache TTL in milliseconds
|
||||
*/
|
||||
setCacheTtl(ttlMs: number): void {
|
||||
this.cacheTtlMs = ttlMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the entire cache
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache.clear();
|
||||
logger.debug('System settings cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a specific key from cache
|
||||
*/
|
||||
clearCacheKey(key: string): void {
|
||||
this.cache.delete(key);
|
||||
this.cache.delete(`all:${key}`);
|
||||
logger.debug('System settings cache key cleared', { key });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all system settings with optional filtering
|
||||
*/
|
||||
async getAll(filters: SystemSettingsFiltersDto = {}): Promise<SystemSetting[]> {
|
||||
const { category, isPublic, isEditable, search } = filters;
|
||||
|
||||
const where: FindOptionsWhere<SystemSetting>[] = [];
|
||||
const baseWhere: FindOptionsWhere<SystemSetting> = {};
|
||||
|
||||
if (category) {
|
||||
baseWhere.category = category;
|
||||
}
|
||||
|
||||
if (isPublic !== undefined) {
|
||||
baseWhere.isPublic = isPublic;
|
||||
}
|
||||
|
||||
if (isEditable !== undefined) {
|
||||
baseWhere.isEditable = isEditable;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.push(
|
||||
{ ...baseWhere, key: ILike(`%${search}%`) },
|
||||
{ ...baseWhere, description: ILike(`%${search}%`) }
|
||||
);
|
||||
} else {
|
||||
where.push(baseWhere);
|
||||
}
|
||||
|
||||
const settings = await this.settingRepository.find({
|
||||
where: where.length > 0 ? where : undefined,
|
||||
order: { category: 'ASC', key: 'ASC' },
|
||||
});
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all public system settings (for unauthenticated access)
|
||||
*/
|
||||
async getPublicSettings(): Promise<SystemSetting[]> {
|
||||
const cacheKey = 'public:all';
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const settings = await this.settingRepository.find({
|
||||
where: { isPublic: true },
|
||||
order: { category: 'ASC', key: 'ASC' },
|
||||
});
|
||||
|
||||
this.setCache(cacheKey, settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings by category
|
||||
*/
|
||||
async getByCategory(category: string): Promise<SystemSetting[]> {
|
||||
const cacheKey = `category:${category}`;
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const settings = await this.settingRepository.find({
|
||||
where: { category },
|
||||
order: { key: 'ASC' },
|
||||
});
|
||||
|
||||
this.setCache(cacheKey, settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single setting by key
|
||||
*/
|
||||
async getByKey(key: string): Promise<SystemSetting | null> {
|
||||
const cacheKey = `key:${key}`;
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const setting = await this.settingRepository.findOne({
|
||||
where: { key },
|
||||
});
|
||||
|
||||
if (setting) {
|
||||
this.setCache(cacheKey, setting);
|
||||
}
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a setting by key
|
||||
*/
|
||||
async getValue(key: string): Promise<any> {
|
||||
const setting = await this.getByKey(key);
|
||||
return setting?.value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a system setting by key
|
||||
*/
|
||||
async update(
|
||||
key: string,
|
||||
dto: UpdateSystemSettingDto,
|
||||
updatedBy?: string
|
||||
): Promise<SystemSetting | null> {
|
||||
const setting = await this.settingRepository.findOne({ where: { key } });
|
||||
|
||||
if (!setting) {
|
||||
logger.warn('System setting not found for update', { key });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!setting.isEditable) {
|
||||
logger.warn('Attempted to update non-editable setting', { key });
|
||||
throw new Error(`Setting '${key}' is not editable`);
|
||||
}
|
||||
|
||||
Object.assign(setting, {
|
||||
...dto,
|
||||
updatedBy,
|
||||
});
|
||||
|
||||
const updated = await this.settingRepository.save(setting);
|
||||
|
||||
this.clearCacheKey(key);
|
||||
this.cache.delete('public:all');
|
||||
this.cache.delete(`category:${setting.category}`);
|
||||
|
||||
logger.info('System setting updated', { key, updatedBy });
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a setting to its default value
|
||||
*/
|
||||
async reset(key: string, updatedBy?: string): Promise<SystemSetting | null> {
|
||||
const setting = await this.settingRepository.findOne({ where: { key } });
|
||||
|
||||
if (!setting) {
|
||||
logger.warn('System setting not found for reset', { key });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (setting.defaultValue === null || setting.defaultValue === undefined) {
|
||||
logger.warn('Setting has no default value', { key });
|
||||
throw new Error(`Setting '${key}' has no default value`);
|
||||
}
|
||||
|
||||
setting.value = setting.defaultValue;
|
||||
setting.updatedBy = updatedBy ?? null;
|
||||
|
||||
const updated = await this.settingRepository.save(setting);
|
||||
|
||||
this.clearCacheKey(key);
|
||||
this.cache.delete('public:all');
|
||||
this.cache.delete(`category:${setting.category}`);
|
||||
|
||||
logger.info('System setting reset to default', { key, updatedBy });
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new system setting (admin only)
|
||||
*/
|
||||
async create(dto: CreateSystemSettingDto, createdBy?: string): Promise<SystemSetting> {
|
||||
const existing = await this.settingRepository.findOne({ where: { key: dto.key } });
|
||||
if (existing) {
|
||||
throw new Error(`Setting with key '${dto.key}' already exists`);
|
||||
}
|
||||
|
||||
const setting = this.settingRepository.create({
|
||||
...dto,
|
||||
updatedBy: createdBy,
|
||||
});
|
||||
|
||||
const created = await this.settingRepository.save(setting);
|
||||
|
||||
this.clearCache();
|
||||
logger.info('System setting created', { key: dto.key, createdBy });
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique categories
|
||||
*/
|
||||
async getCategories(): Promise<string[]> {
|
||||
const cacheKey = 'categories:all';
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const result = await this.settingRepository
|
||||
.createQueryBuilder('setting')
|
||||
.select('DISTINCT setting.category', 'category')
|
||||
.orderBy('setting.category', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
const categories = result.map((r: { category: string }) => r.category);
|
||||
this.setCache(cacheKey, categories);
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache helper: get from cache if not expired
|
||||
*/
|
||||
private getFromCache(key: string): any {
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && entry.expiresAt > Date.now()) {
|
||||
return entry.value;
|
||||
}
|
||||
if (entry) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache helper: set value in cache with TTL
|
||||
*/
|
||||
private setCache(key: string, value: any): void {
|
||||
this.cache.set(key, {
|
||||
value,
|
||||
expiresAt: Date.now() + this.cacheTtlMs,
|
||||
});
|
||||
}
|
||||
}
|
||||
309
src/modules/settings/services/tenant-settings.service.ts
Normal file
309
src/modules/settings/services/tenant-settings.service.ts
Normal file
@ -0,0 +1,309 @@
|
||||
import { Repository, FindOptionsWhere, ILike } from 'typeorm';
|
||||
import { TenantSetting, SystemSetting, PlanSetting } from '../entities';
|
||||
import { UpdateTenantSettingDto, CreateTenantSettingDto, TenantSettingsFiltersDto } from '../dto';
|
||||
import { logger } from '../../../shared/utils/logger.js';
|
||||
|
||||
/**
|
||||
* Effective setting result combining value with source information
|
||||
*/
|
||||
export interface EffectiveSetting {
|
||||
key: string;
|
||||
value: any;
|
||||
source: 'system' | 'plan' | 'tenant';
|
||||
isOverridden: boolean;
|
||||
category?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TenantSettingsService
|
||||
*
|
||||
* Manages tenant-specific settings with inheritance from system and plan defaults.
|
||||
*
|
||||
* Inheritance hierarchy:
|
||||
* 1. Tenant Settings (highest priority)
|
||||
* 2. Plan Settings (subscription plan defaults)
|
||||
* 3. System Settings (lowest priority, global defaults)
|
||||
*
|
||||
* Features:
|
||||
* - Get effective settings with inheritance resolution
|
||||
* - Override system/plan settings at tenant level
|
||||
* - Reset to inherited values
|
||||
*/
|
||||
export class TenantSettingsService {
|
||||
constructor(
|
||||
private readonly tenantSettingRepository: Repository<TenantSetting>,
|
||||
private readonly systemSettingRepository: Repository<SystemSetting>,
|
||||
private readonly planSettingRepository: Repository<PlanSetting>
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the effective value for a setting key, applying inheritance
|
||||
*/
|
||||
async getEffective(tenantId: string, key: string, planId?: string): Promise<EffectiveSetting | null> {
|
||||
// 1. Check tenant override first
|
||||
const tenantSetting = await this.tenantSettingRepository.findOne({
|
||||
where: { tenantId, key, isOverridden: true },
|
||||
});
|
||||
|
||||
if (tenantSetting) {
|
||||
return {
|
||||
key,
|
||||
value: tenantSetting.value,
|
||||
source: 'tenant',
|
||||
isOverridden: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Check plan settings if planId provided
|
||||
if (planId) {
|
||||
const planSetting = await this.planSettingRepository.findOne({
|
||||
where: { planId, key },
|
||||
});
|
||||
|
||||
if (planSetting) {
|
||||
return {
|
||||
key,
|
||||
value: planSetting.value,
|
||||
source: 'plan',
|
||||
isOverridden: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fall back to system setting
|
||||
const systemSetting = await this.systemSettingRepository.findOne({
|
||||
where: { key },
|
||||
});
|
||||
|
||||
if (systemSetting) {
|
||||
return {
|
||||
key,
|
||||
value: systemSetting.value ?? systemSetting.defaultValue,
|
||||
source: 'system',
|
||||
isOverridden: false,
|
||||
category: systemSetting.category,
|
||||
description: systemSetting.description ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all effective settings for a tenant, merging all sources
|
||||
*/
|
||||
async getAllEffective(tenantId: string, planId?: string): Promise<EffectiveSetting[]> {
|
||||
const effectiveSettings: Map<string, EffectiveSetting> = new Map();
|
||||
|
||||
// 1. Start with system settings (public ones)
|
||||
const systemSettings = await this.systemSettingRepository.find({
|
||||
where: { isPublic: true },
|
||||
order: { category: 'ASC', key: 'ASC' },
|
||||
});
|
||||
|
||||
for (const setting of systemSettings) {
|
||||
effectiveSettings.set(setting.key, {
|
||||
key: setting.key,
|
||||
value: setting.value ?? setting.defaultValue,
|
||||
source: 'system',
|
||||
isOverridden: false,
|
||||
category: setting.category,
|
||||
description: setting.description ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Apply plan settings (if planId provided)
|
||||
if (planId) {
|
||||
const planSettings = await this.planSettingRepository.find({
|
||||
where: { planId },
|
||||
});
|
||||
|
||||
for (const setting of planSettings) {
|
||||
const existing = effectiveSettings.get(setting.key);
|
||||
effectiveSettings.set(setting.key, {
|
||||
key: setting.key,
|
||||
value: setting.value,
|
||||
source: 'plan',
|
||||
isOverridden: false,
|
||||
category: existing?.category,
|
||||
description: existing?.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Apply tenant overrides
|
||||
const tenantSettings = await this.tenantSettingRepository.find({
|
||||
where: { tenantId, isOverridden: true },
|
||||
});
|
||||
|
||||
for (const setting of tenantSettings) {
|
||||
const existing = effectiveSettings.get(setting.key);
|
||||
effectiveSettings.set(setting.key, {
|
||||
key: setting.key,
|
||||
value: setting.value,
|
||||
source: 'tenant',
|
||||
isOverridden: true,
|
||||
category: existing?.category,
|
||||
description: existing?.description,
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(effectiveSettings.values()).sort((a, b) => {
|
||||
const catCompare = (a.category || '').localeCompare(b.category || '');
|
||||
if (catCompare !== 0) return catCompare;
|
||||
return a.key.localeCompare(b.key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tenant setting by key (direct, without inheritance)
|
||||
*/
|
||||
async get(tenantId: string, key: string): Promise<TenantSetting | null> {
|
||||
return this.tenantSettingRepository.findOne({
|
||||
where: { tenantId, key },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tenant settings with optional filtering
|
||||
*/
|
||||
async getAll(
|
||||
tenantId: string,
|
||||
filters: TenantSettingsFiltersDto = {}
|
||||
): Promise<TenantSetting[]> {
|
||||
const { inheritedFrom, isOverridden, search } = filters;
|
||||
|
||||
const where: FindOptionsWhere<TenantSetting>[] = [];
|
||||
const baseWhere: FindOptionsWhere<TenantSetting> = { tenantId };
|
||||
|
||||
if (inheritedFrom) {
|
||||
baseWhere.inheritedFrom = inheritedFrom;
|
||||
}
|
||||
|
||||
if (isOverridden !== undefined) {
|
||||
baseWhere.isOverridden = isOverridden;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.push({ ...baseWhere, key: ILike(`%${search}%`) });
|
||||
} else {
|
||||
where.push(baseWhere);
|
||||
}
|
||||
|
||||
return this.tenantSettingRepository.find({
|
||||
where,
|
||||
order: { key: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or create a tenant setting (upsert)
|
||||
*/
|
||||
async update(
|
||||
tenantId: string,
|
||||
key: string,
|
||||
dto: UpdateTenantSettingDto
|
||||
): Promise<TenantSetting> {
|
||||
let setting = await this.tenantSettingRepository.findOne({
|
||||
where: { tenantId, key },
|
||||
});
|
||||
|
||||
if (setting) {
|
||||
Object.assign(setting, dto);
|
||||
const updated = await this.tenantSettingRepository.save(setting);
|
||||
logger.info('Tenant setting updated', { tenantId, key });
|
||||
return updated;
|
||||
}
|
||||
|
||||
// Create new setting
|
||||
setting = this.tenantSettingRepository.create({
|
||||
tenantId,
|
||||
key,
|
||||
value: dto.value,
|
||||
inheritedFrom: 'custom',
|
||||
isOverridden: dto.isOverridden ?? true,
|
||||
});
|
||||
|
||||
const created = await this.tenantSettingRepository.save(setting);
|
||||
logger.info('Tenant setting created', { tenantId, key });
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new tenant setting
|
||||
*/
|
||||
async create(tenantId: string, dto: CreateTenantSettingDto): Promise<TenantSetting> {
|
||||
const existing = await this.tenantSettingRepository.findOne({
|
||||
where: { tenantId, key: dto.key },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new Error(`Setting '${dto.key}' already exists for this tenant`);
|
||||
}
|
||||
|
||||
const setting = this.tenantSettingRepository.create({
|
||||
...dto,
|
||||
tenantId,
|
||||
inheritedFrom: dto.inheritedFrom ?? 'custom',
|
||||
isOverridden: dto.isOverridden ?? true,
|
||||
});
|
||||
|
||||
const created = await this.tenantSettingRepository.save(setting);
|
||||
logger.info('Tenant setting created', { tenantId, key: dto.key });
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a tenant setting to inherited value
|
||||
*/
|
||||
async reset(tenantId: string, key: string): Promise<boolean> {
|
||||
const result = await this.tenantSettingRepository.delete({ tenantId, key });
|
||||
const deleted = (result.affected ?? 0) > 0;
|
||||
|
||||
if (deleted) {
|
||||
logger.info('Tenant setting reset to inherited', { tenantId, key });
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update tenant settings
|
||||
*/
|
||||
async bulkUpdate(
|
||||
tenantId: string,
|
||||
settings: Array<{ key: string; value: any; isOverridden?: boolean }>
|
||||
): Promise<TenantSetting[]> {
|
||||
const results: TenantSetting[] = [];
|
||||
|
||||
for (const { key, value, isOverridden } of settings) {
|
||||
const updated = await this.update(tenantId, key, {
|
||||
value,
|
||||
isOverridden: isOverridden ?? true,
|
||||
});
|
||||
results.push(updated);
|
||||
}
|
||||
|
||||
logger.info('Tenant settings bulk updated', {
|
||||
tenantId,
|
||||
count: settings.length,
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tenant setting override
|
||||
*/
|
||||
async delete(tenantId: string, key: string): Promise<boolean> {
|
||||
const result = await this.tenantSettingRepository.delete({ tenantId, key });
|
||||
const deleted = (result.affected ?? 0) > 0;
|
||||
|
||||
if (deleted) {
|
||||
logger.info('Tenant setting deleted', { tenantId, key });
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
305
src/modules/settings/services/user-preferences.service.ts
Normal file
305
src/modules/settings/services/user-preferences.service.ts
Normal file
@ -0,0 +1,305 @@
|
||||
import { Repository, FindOptionsWhere, ILike } from 'typeorm';
|
||||
import { UserPreference } from '../entities';
|
||||
import {
|
||||
UpdateUserPreferenceDto,
|
||||
BulkUpdateUserPreferencesDto,
|
||||
SetUserPreferencesDto,
|
||||
UserPreferencesFiltersDto,
|
||||
} from '../dto';
|
||||
import { logger } from '../../../shared/utils/logger.js';
|
||||
|
||||
/**
|
||||
* Default user preferences
|
||||
*/
|
||||
const DEFAULT_PREFERENCES: Record<string, any> = {
|
||||
'ui.theme': 'system',
|
||||
'ui.language': 'es-MX',
|
||||
'ui.dateFormat': 'DD/MM/YYYY',
|
||||
'ui.numberFormat': 'es-MX',
|
||||
'notifications.email': true,
|
||||
'notifications.push': true,
|
||||
'notifications.inApp': true,
|
||||
};
|
||||
|
||||
/**
|
||||
* UserPreferencesService
|
||||
*
|
||||
* Manages personal preferences for individual users.
|
||||
*
|
||||
* Features:
|
||||
* - CRUD operations for user preferences
|
||||
* - Bulk update support
|
||||
* - Default values for new users
|
||||
* - Sync timestamp tracking
|
||||
*/
|
||||
export class UserPreferencesService {
|
||||
constructor(private readonly preferenceRepository: Repository<UserPreference>) {}
|
||||
|
||||
/**
|
||||
* Get all preferences for a user
|
||||
*/
|
||||
async getAll(
|
||||
userId: string,
|
||||
filters: UserPreferencesFiltersDto = {}
|
||||
): Promise<UserPreference[]> {
|
||||
const { category, search } = filters;
|
||||
|
||||
const where: FindOptionsWhere<UserPreference>[] = [];
|
||||
const baseWhere: FindOptionsWhere<UserPreference> = { userId };
|
||||
|
||||
if (search) {
|
||||
where.push({ ...baseWhere, key: ILike(`%${search}%`) });
|
||||
} else {
|
||||
where.push(baseWhere);
|
||||
}
|
||||
|
||||
const preferences = await this.preferenceRepository.find({
|
||||
where,
|
||||
order: { key: 'ASC' },
|
||||
});
|
||||
|
||||
// If filtering by category, filter in memory (keys start with category.)
|
||||
if (category) {
|
||||
const prefix = `${category}.`;
|
||||
return preferences.filter((p) => p.key.startsWith(prefix));
|
||||
}
|
||||
|
||||
return preferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all preferences as a key-value map
|
||||
*/
|
||||
async getAllAsMap(userId: string): Promise<Record<string, any>> {
|
||||
const preferences = await this.preferenceRepository.find({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
const map: Record<string, any> = {};
|
||||
for (const pref of preferences) {
|
||||
map[pref.key] = pref.value;
|
||||
}
|
||||
|
||||
// Fill in defaults for missing keys
|
||||
for (const [key, defaultValue] of Object.entries(DEFAULT_PREFERENCES)) {
|
||||
if (map[key] === undefined) {
|
||||
map[key] = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single preference by key
|
||||
*/
|
||||
async get(userId: string, key: string): Promise<UserPreference | null> {
|
||||
return this.preferenceRepository.findOne({
|
||||
where: { userId, key },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preference value with default fallback
|
||||
*/
|
||||
async getValue(userId: string, key: string): Promise<any> {
|
||||
const preference = await this.get(userId, key);
|
||||
if (preference) {
|
||||
return preference.value;
|
||||
}
|
||||
return DEFAULT_PREFERENCES[key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or create a single preference
|
||||
*/
|
||||
async update(
|
||||
userId: string,
|
||||
key: string,
|
||||
dto: UpdateUserPreferenceDto
|
||||
): Promise<UserPreference> {
|
||||
let preference = await this.preferenceRepository.findOne({
|
||||
where: { userId, key },
|
||||
});
|
||||
|
||||
if (preference) {
|
||||
preference.value = dto.value;
|
||||
preference.syncedAt = new Date();
|
||||
const updated = await this.preferenceRepository.save(preference);
|
||||
logger.debug('User preference updated', { userId, key });
|
||||
return updated;
|
||||
}
|
||||
|
||||
// Create new preference
|
||||
preference = this.preferenceRepository.create({
|
||||
userId,
|
||||
key,
|
||||
value: dto.value,
|
||||
syncedAt: new Date(),
|
||||
});
|
||||
|
||||
const created = await this.preferenceRepository.save(preference);
|
||||
logger.debug('User preference created', { userId, key });
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update multiple preferences
|
||||
*/
|
||||
async bulkUpdate(
|
||||
userId: string,
|
||||
dto: BulkUpdateUserPreferencesDto
|
||||
): Promise<UserPreference[]> {
|
||||
const results: UserPreference[] = [];
|
||||
|
||||
for (const { key, value } of dto.preferences) {
|
||||
const updated = await this.update(userId, key, { value });
|
||||
results.push(updated);
|
||||
}
|
||||
|
||||
logger.info('User preferences bulk updated', {
|
||||
userId,
|
||||
count: dto.preferences.length,
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all standard preferences at once
|
||||
*/
|
||||
async setPreferences(
|
||||
userId: string,
|
||||
dto: SetUserPreferencesDto
|
||||
): Promise<UserPreference[]> {
|
||||
const preferences: Array<{ key: string; value: any }> = [];
|
||||
|
||||
if (dto.theme !== undefined) {
|
||||
preferences.push({ key: 'ui.theme', value: dto.theme });
|
||||
}
|
||||
if (dto.language !== undefined) {
|
||||
preferences.push({ key: 'ui.language', value: dto.language });
|
||||
}
|
||||
if (dto.dateFormat !== undefined) {
|
||||
preferences.push({ key: 'ui.dateFormat', value: dto.dateFormat });
|
||||
}
|
||||
if (dto.numberFormat !== undefined) {
|
||||
preferences.push({ key: 'ui.numberFormat', value: dto.numberFormat });
|
||||
}
|
||||
if (dto.notificationsEmail !== undefined) {
|
||||
preferences.push({ key: 'notifications.email', value: dto.notificationsEmail });
|
||||
}
|
||||
if (dto.notificationsPush !== undefined) {
|
||||
preferences.push({ key: 'notifications.push', value: dto.notificationsPush });
|
||||
}
|
||||
if (dto.notificationsInApp !== undefined) {
|
||||
preferences.push({ key: 'notifications.inApp', value: dto.notificationsInApp });
|
||||
}
|
||||
|
||||
return this.bulkUpdate(userId, { preferences });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a preference to default value
|
||||
*/
|
||||
async reset(userId: string, key: string): Promise<UserPreference | null> {
|
||||
const defaultValue = DEFAULT_PREFERENCES[key];
|
||||
|
||||
if (defaultValue === undefined) {
|
||||
// No default, delete the preference
|
||||
const result = await this.preferenceRepository.delete({ userId, key });
|
||||
if ((result.affected ?? 0) > 0) {
|
||||
logger.info('User preference deleted (no default)', { userId, key });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reset to default value
|
||||
return this.update(userId, key, { value: defaultValue });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all preferences to defaults
|
||||
*/
|
||||
async resetAll(userId: string): Promise<UserPreference[]> {
|
||||
// Delete all existing preferences
|
||||
await this.preferenceRepository.delete({ userId });
|
||||
|
||||
// Create with defaults
|
||||
const results: UserPreference[] = [];
|
||||
for (const [key, value] of Object.entries(DEFAULT_PREFERENCES)) {
|
||||
const preference = this.preferenceRepository.create({
|
||||
userId,
|
||||
key,
|
||||
value,
|
||||
syncedAt: new Date(),
|
||||
});
|
||||
results.push(await this.preferenceRepository.save(preference));
|
||||
}
|
||||
|
||||
logger.info('User preferences reset to defaults', { userId });
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific preference
|
||||
*/
|
||||
async delete(userId: string, key: string): Promise<boolean> {
|
||||
const result = await this.preferenceRepository.delete({ userId, key });
|
||||
const deleted = (result.affected ?? 0) > 0;
|
||||
|
||||
if (deleted) {
|
||||
logger.debug('User preference deleted', { userId, key });
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default preferences for a new user
|
||||
*/
|
||||
async initializeDefaults(userId: string): Promise<UserPreference[]> {
|
||||
const existing = await this.preferenceRepository.find({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (existing.length > 0) {
|
||||
logger.debug('User already has preferences', { userId, count: existing.length });
|
||||
return existing;
|
||||
}
|
||||
|
||||
const results: UserPreference[] = [];
|
||||
for (const [key, value] of Object.entries(DEFAULT_PREFERENCES)) {
|
||||
const preference = this.preferenceRepository.create({
|
||||
userId,
|
||||
key,
|
||||
value,
|
||||
syncedAt: new Date(),
|
||||
});
|
||||
results.push(await this.preferenceRepository.save(preference));
|
||||
}
|
||||
|
||||
logger.info('User preferences initialized with defaults', { userId });
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preferences grouped by category
|
||||
*/
|
||||
async getGroupedByCategory(userId: string): Promise<Record<string, Record<string, any>>> {
|
||||
const preferences = await this.getAllAsMap(userId);
|
||||
const grouped: Record<string, Record<string, any>> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(preferences)) {
|
||||
const [category, ...rest] = key.split('.');
|
||||
const subKey = rest.join('.');
|
||||
|
||||
if (!grouped[category]) {
|
||||
grouped[category] = {};
|
||||
}
|
||||
grouped[category][subKey] = value;
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
}
|
||||
130
src/modules/settings/settings.module.ts
Normal file
130
src/modules/settings/settings.module.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { Router } from 'express';
|
||||
import { DataSource } from 'typeorm';
|
||||
import {
|
||||
SystemSettingsService,
|
||||
TenantSettingsService,
|
||||
UserPreferencesService,
|
||||
SettingsService,
|
||||
} from './services';
|
||||
import {
|
||||
SystemSettingsController,
|
||||
TenantSettingsController,
|
||||
UserPreferencesController,
|
||||
} from './controllers';
|
||||
import {
|
||||
SystemSetting,
|
||||
PlanSetting,
|
||||
TenantSetting,
|
||||
UserPreference,
|
||||
} from './entities';
|
||||
|
||||
/**
|
||||
* Options for configuring the Settings module
|
||||
*/
|
||||
export interface SettingsModuleOptions {
|
||||
dataSource: DataSource;
|
||||
basePath?: string;
|
||||
cacheTtlMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* SettingsModule
|
||||
*
|
||||
* Complete settings management module for multi-tenant applications.
|
||||
*
|
||||
* Features:
|
||||
* - System settings (global configuration)
|
||||
* - Plan settings (subscription-based defaults)
|
||||
* - Tenant settings (organization overrides)
|
||||
* - User preferences (personal settings)
|
||||
*
|
||||
* Settings inheritance: system -> plan -> tenant -> user
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* const settingsModule = new SettingsModule({
|
||||
* dataSource: AppDataSource,
|
||||
* basePath: '/api/v1',
|
||||
* });
|
||||
*
|
||||
* app.use(settingsModule.router);
|
||||
* ```
|
||||
*/
|
||||
export class SettingsModule {
|
||||
public router: Router;
|
||||
public systemSettingsService: SystemSettingsService;
|
||||
public tenantSettingsService: TenantSettingsService;
|
||||
public userPreferencesService: UserPreferencesService;
|
||||
public settingsService: SettingsService;
|
||||
|
||||
private dataSource: DataSource;
|
||||
private basePath: string;
|
||||
private cacheTtlMs: number;
|
||||
|
||||
constructor(options: SettingsModuleOptions) {
|
||||
this.dataSource = options.dataSource;
|
||||
this.basePath = options.basePath || '';
|
||||
this.cacheTtlMs = options.cacheTtlMs || 5 * 60 * 1000; // 5 minutes default
|
||||
|
||||
this.router = Router();
|
||||
this.initializeServices();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all services with their repositories
|
||||
*/
|
||||
private initializeServices(): void {
|
||||
const systemSettingRepository = this.dataSource.getRepository(SystemSetting);
|
||||
const planSettingRepository = this.dataSource.getRepository(PlanSetting);
|
||||
const tenantSettingRepository = this.dataSource.getRepository(TenantSetting);
|
||||
const userPreferenceRepository = this.dataSource.getRepository(UserPreference);
|
||||
|
||||
// Create services
|
||||
this.systemSettingsService = new SystemSettingsService(systemSettingRepository);
|
||||
this.systemSettingsService.setCacheTtl(this.cacheTtlMs);
|
||||
|
||||
this.tenantSettingsService = new TenantSettingsService(
|
||||
tenantSettingRepository,
|
||||
systemSettingRepository,
|
||||
planSettingRepository
|
||||
);
|
||||
|
||||
this.userPreferencesService = new UserPreferencesService(userPreferenceRepository);
|
||||
|
||||
// Create orchestrator service
|
||||
this.settingsService = new SettingsService(
|
||||
this.systemSettingsService,
|
||||
this.tenantSettingsService,
|
||||
this.userPreferencesService
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize routes with controllers
|
||||
*/
|
||||
private initializeRoutes(): void {
|
||||
const systemSettingsController = new SystemSettingsController(this.systemSettingsService);
|
||||
const tenantSettingsController = new TenantSettingsController(this.tenantSettingsService);
|
||||
const userPreferencesController = new UserPreferencesController(this.userPreferencesService);
|
||||
|
||||
// Mount routes
|
||||
this.router.use(`${this.basePath}/settings/system`, systemSettingsController.router);
|
||||
this.router.use(`${this.basePath}/settings/tenant`, tenantSettingsController.router);
|
||||
this.router.use(`${this.basePath}/settings/user`, userPreferencesController.router);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entities managed by this module
|
||||
*/
|
||||
static getEntities(): Function[] {
|
||||
return [SystemSetting, PlanSetting, TenantSetting, UserPreference];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all caches in the module
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.systemSettingsService.clearCache();
|
||||
}
|
||||
}
|
||||
62
src/modules/settings/settings.routes.ts
Normal file
62
src/modules/settings/settings.routes.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Router } from 'express';
|
||||
import { DataSource } from 'typeorm';
|
||||
import {
|
||||
SystemSetting,
|
||||
PlanSetting,
|
||||
TenantSetting,
|
||||
UserPreference,
|
||||
} from './entities';
|
||||
import {
|
||||
SystemSettingsService,
|
||||
TenantSettingsService,
|
||||
UserPreferencesService,
|
||||
} from './services';
|
||||
import {
|
||||
SystemSettingsController,
|
||||
TenantSettingsController,
|
||||
UserPreferencesController,
|
||||
} from './controllers';
|
||||
|
||||
/**
|
||||
* Creates and configures the settings routes
|
||||
*
|
||||
* Routes:
|
||||
* - /api/v1/settings/system/* - System settings (global configuration)
|
||||
* - /api/v1/settings/tenant/* - Tenant settings (organization overrides)
|
||||
* - /api/v1/settings/user/* - User preferences (personal settings)
|
||||
*
|
||||
* @param dataSource - TypeORM DataSource instance
|
||||
* @returns Configured Express Router
|
||||
*/
|
||||
export function createSettingsRoutes(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
|
||||
// Initialize repositories
|
||||
const systemSettingRepository = dataSource.getRepository(SystemSetting);
|
||||
const planSettingRepository = dataSource.getRepository(PlanSetting);
|
||||
const tenantSettingRepository = dataSource.getRepository(TenantSetting);
|
||||
const userPreferenceRepository = dataSource.getRepository(UserPreference);
|
||||
|
||||
// Initialize services
|
||||
const systemSettingsService = new SystemSettingsService(systemSettingRepository);
|
||||
const tenantSettingsService = new TenantSettingsService(
|
||||
tenantSettingRepository,
|
||||
systemSettingRepository,
|
||||
planSettingRepository
|
||||
);
|
||||
const userPreferencesService = new UserPreferencesService(userPreferenceRepository);
|
||||
|
||||
// Initialize controllers
|
||||
const systemSettingsController = new SystemSettingsController(systemSettingsService);
|
||||
const tenantSettingsController = new TenantSettingsController(tenantSettingsService);
|
||||
const userPreferencesController = new UserPreferencesController(userPreferencesService);
|
||||
|
||||
// Mount routes
|
||||
router.use('/system', systemSettingsController.router);
|
||||
router.use('/tenant', tenantSettingsController.router);
|
||||
router.use('/user', userPreferencesController.router);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
export default createSettingsRoutes;
|
||||
Loading…
Reference in New Issue
Block a user