import { Router } from 'express'; import { eq, and } from 'drizzle-orm'; import { db } from '../db'; import { organizationSkins, organizations } from '@shared/schema'; import { authenticateToken, requirePermission, type AuthenticatedRequest } from '../middleware/auth.middleware'; import { tenantIsolation } from '../middleware/tenant.middleware'; import { BUILT_IN_SKINS, getBuiltInSkinById } from '../../client/src/themes'; import type { Skin } from '../../client/src/themes/tokens'; const router = Router(); interface SkinListItem { id: string; name: string; description?: string; isEditable: boolean; isBuiltIn: boolean; dbId?: number; } function serializeSkinListItem(skin: Skin, isBuiltIn: boolean, dbId?: number): SkinListItem { return { id: skin.id, name: skin.name, description: skin.description, isEditable: isBuiltIn ? false : true, isBuiltIn, dbId, }; } // GET /api/organization/skins — список скинов (встроенные + пользовательские) router.get('/api/organization/skins', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const customRows = await db .select() .from(organizationSkins) .where(eq(organizationSkins.organizationId, orgId)); const builtInList = BUILT_IN_SKINS.map((skin) => serializeSkinListItem(skin, true)); const customList = customRows.map((row) => serializeSkinListItem({ id: row.skinId, name: row.name, description: row.description ?? undefined, tokens: row.tokens as Skin['tokens'], } as Skin, false, row.id) ); return res.json({ success: true, skins: [...builtInList, ...customList] }); } catch (error) { console.error('Get organization skins error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // GET /api/organization/skins/:id — детали скина router.get('/api/organization/skins/:id', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const skinId = req.params.id; const builtIn = getBuiltInSkinById(skinId); if (builtIn) { return res.json({ success: true, skin: builtIn }); } const [row] = await db .select() .from(organizationSkins) .where(and(eq(organizationSkins.organizationId, orgId), eq(organizationSkins.skinId, skinId))); if (!row) { return res.status(404).json({ success: false, error: 'Скин не найден' }); } const skin: Skin = { id: row.skinId, name: row.name, description: row.description ?? undefined, isEditable: row.isEditable ?? true, tokens: row.tokens as Skin['tokens'], }; return res.json({ success: true, skin }); } catch (error) { console.error('Get organization skin error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // POST /api/organization/skins — создать пользовательский скин router.post('/api/organization/skins', authenticateToken, requirePermission('settings.manage'), tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const { skinId, name, description, tokens, baseSkinId } = req.body; if (!skinId || typeof skinId !== 'string') { return res.status(400).json({ success: false, error: 'skinId обязателен' }); } if (!name || typeof name !== 'string') { return res.status(400).json({ success: false, error: 'Название скина обязательно' }); } if (getBuiltInSkinById(skinId)) { return res.status(400).json({ success: false, error: 'Нельзя создать скин с id встроенного скина' }); } let finalTokens = tokens; if (!finalTokens && baseSkinId) { const base = getBuiltInSkinById(baseSkinId) || await db.query.organizationSkins.findFirst({ where: and(eq(organizationSkins.organizationId, orgId), eq(organizationSkins.skinId, baseSkinId)), }); if (base) { finalTokens = (base as any).tokens ?? (base as Skin).tokens; } } if (!finalTokens) { return res.status(400).json({ success: false, error: 'Токены скина обязательны' }); } const [row] = await db .insert(organizationSkins) .values({ organizationId: orgId, skinId, name: name.trim(), description: description ?? null, tokens: finalTokens, isEditable: true, isActive: false, createdBy: req.user!.id, }) .returning(); const skin: Skin = { id: row.skinId, name: row.name, description: row.description ?? undefined, isEditable: true, tokens: row.tokens as Skin['tokens'], }; return res.status(201).json({ success: true, skin, dbId: row.id }); } catch (error: any) { if (error?.code === '23505') { return res.status(409).json({ success: false, error: 'Скин с таким id уже существует' }); } console.error('Create organization skin error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // PATCH /api/organization/skins/:id — обновить пользовательский скин router.patch('/api/organization/skins/:id', authenticateToken, requirePermission('settings.manage'), tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const skinId = req.params.id; const { name, description, tokens } = req.body; if (getBuiltInSkinById(skinId)) { return res.status(400).json({ success: false, error: 'Встроенные скины нельзя редактировать' }); } const updates: Partial<{ name: string; description: string | null; tokens: object }> = {}; if (name !== undefined) updates.name = name.trim(); if (description !== undefined) updates.description = description ?? null; if (tokens !== undefined) updates.tokens = tokens; const [row] = await db .update(organizationSkins) .set({ ...updates, updatedAt: new Date() }) .where(and(eq(organizationSkins.organizationId, orgId), eq(organizationSkins.skinId, skinId))) .returning(); if (!row) { return res.status(404).json({ success: false, error: 'Скин не найден' }); } const skin: Skin = { id: row.skinId, name: row.name, description: row.description ?? undefined, isEditable: row.isEditable ?? true, tokens: row.tokens as Skin['tokens'], }; return res.json({ success: true, skin }); } catch (error) { console.error('Update organization skin error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // DELETE /api/organization/skins/:id — удалить пользовательский скин router.delete('/api/organization/skins/:id', authenticateToken, requirePermission('settings.manage'), tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const skinId = req.params.id; if (getBuiltInSkinById(skinId)) { return res.status(400).json({ success: false, error: 'Встроенные скины нельзя удалять' }); } const [row] = await db .delete(organizationSkins) .where(and(eq(organizationSkins.organizationId, orgId), eq(organizationSkins.skinId, skinId))) .returning(); if (!row) { return res.status(404).json({ success: false, error: 'Скин не найден' }); } // Если удалён активный скин — сбрасываем на iistwin await db .update(organizations) .set({ activeSkinId: 'iistwin', updatedAt: new Date() }) .where(and(eq(organizations.id, orgId), eq(organizations.activeSkinId, skinId))); return res.json({ success: true }); } catch (error) { console.error('Delete organization skin error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // GET /api/organization/current-skin — текущий активный скин организации router.get('/api/organization/current-skin', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const [org] = await db .select({ activeSkinId: organizations.activeSkinId }) .from(organizations) .where(eq(organizations.id, orgId)); const activeSkinId = org?.activeSkinId ?? 'iistwin'; const builtIn = getBuiltInSkinById(activeSkinId); if (builtIn) { return res.json({ success: true, skinId: activeSkinId, skin: builtIn }); } const [row] = await db .select() .from(organizationSkins) .where(and(eq(organizationSkins.organizationId, orgId), eq(organizationSkins.skinId, activeSkinId))); if (!row) { return res.json({ success: true, skinId: 'iistwin', skin: getBuiltInSkinById('iistwin') }); } const skin: Skin = { id: row.skinId, name: row.name, description: row.description ?? undefined, isEditable: row.isEditable ?? true, tokens: row.tokens as Skin['tokens'], }; return res.json({ success: true, skinId: activeSkinId, skin }); } catch (error) { console.error('Get current organization skin error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // PUT /api/organization/current-skin — установить активный скин router.put('/api/organization/current-skin', authenticateToken, requirePermission('settings.manage'), tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const orgId = req.organizationId!; const { skinId } = req.body; if (!skinId || typeof skinId !== 'string') { return res.status(400).json({ success: false, error: 'skinId обязателен' }); } const isBuiltIn = !!getBuiltInSkinById(skinId); if (!isBuiltIn) { const [custom] = await db .select({ id: organizationSkins.id }) .from(organizationSkins) .where(and(eq(organizationSkins.organizationId, orgId), eq(organizationSkins.skinId, skinId))); if (!custom) { return res.status(404).json({ success: false, error: 'Скин не найден' }); } } await db .update(organizations) .set({ activeSkinId: skinId, updatedAt: new Date() }) .where(eq(organizations.id, orgId)); return res.json({ success: true, skinId }); } catch (error) { console.error('Set current organization skin error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); export default router;