const express = require('express'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const sharp = require('sharp'); const router = express.Router(); const { query, queryOne, exec } = require('../models/db'); const { authMiddleware, adminMiddleware } = require('../middleware/auth'); function makeIconStorage(prefix) { return multer.diskStorage({ destination: '/app/uploads/logos', filename: (req, file, cb) => cb(null, `${prefix}_${Date.now()}${path.extname(file.originalname)}`), }); } const iconOpts = { limits: { fileSize: 1 * 1024 * 1024 }, fileFilter: (req, file, cb) => file.mimetype.startsWith('image/') ? cb(null, true) : cb(new Error('Images only')), }; const uploadLogo = multer({ storage: makeIconStorage('logo'), ...iconOpts }); const uploadNewChat = multer({ storage: makeIconStorage('newchat'), ...iconOpts }); const uploadGroupInfo = multer({ storage: makeIconStorage('groupinfo'), ...iconOpts }); // Helper: upsert a setting async function setSetting(schema, key, value) { await exec(schema, "INSERT INTO settings (key,value) VALUES ($1,$2) ON CONFLICT(key) DO UPDATE SET value=$2, updated_at=NOW()", [key, value] ); } // GET /api/settings router.get('/', async (req, res) => { try { const rows = await query(req.schema, 'SELECT key, value FROM settings'); const obj = {}; for (const r of rows) obj[r.key] = r.value; const admin = await queryOne(req.schema, 'SELECT email FROM users WHERE is_default_admin = TRUE'); if (admin) obj.admin_email = admin.email; obj.app_version = process.env.ROSTERCHIRP_VERSION || 'dev'; obj.user_pass = process.env.USER_PASS || 'user@1234'; // Tell the frontend whether this request came from the HOST_DOMAIN. // Used to show/hide the Control Panel menu item — only visible on the host's own domain. const reqHost = (req.headers.host || '').toLowerCase().split(':')[0]; const hostDomain = (process.env.HOST_DOMAIN || '').toLowerCase(); obj.is_host_domain = ( process.env.APP_TYPE === 'host' && !!hostDomain && (reqHost === hostDomain || reqHost === `www.${hostDomain}` || reqHost === 'localhost') ) ? 'true' : 'false'; res.json({ settings: obj }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.patch('/app-name', authMiddleware, adminMiddleware, async (req, res) => { const { name } = req.body; if (!name?.trim()) return res.status(400).json({ error: 'Name required' }); try { await exec(req.schema, "UPDATE settings SET value=$1, updated_at=NOW() WHERE key='app_name'", [name.trim()]); res.json({ success: true, name: name.trim() }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.post('/logo', authMiddleware, adminMiddleware, uploadLogo.single('logo'), async (req, res) => { if (!req.file) return res.status(400).json({ error: 'No file' }); const logoUrl = `/uploads/logos/${req.file.filename}`; try { await sharp(req.file.path).resize(192,192,{fit:'contain',background:{r:255,g:255,b:255,alpha:0}}).png().toFile('/app/uploads/logos/pwa-icon-192.png'); await sharp(req.file.path).resize(512,512,{fit:'contain',background:{r:255,g:255,b:255,alpha:0}}).png().toFile('/app/uploads/logos/pwa-icon-512.png'); await exec(req.schema, "UPDATE settings SET value=$1, updated_at=NOW() WHERE key='logo_url'", [logoUrl]); await setSetting(req.schema, 'pwa_icon_192', '/uploads/logos/pwa-icon-192.png'); await setSetting(req.schema, 'pwa_icon_512', '/uploads/logos/pwa-icon-512.png'); res.json({ logoUrl }); } catch (err) { console.error('[Logo] icon gen failed:', err.message); await exec(req.schema, "UPDATE settings SET value=$1, updated_at=NOW() WHERE key='logo_url'", [logoUrl]); res.json({ logoUrl }); } }); router.post('/icon-newchat', authMiddleware, adminMiddleware, uploadNewChat.single('icon'), async (req, res) => { if (!req.file) return res.status(400).json({ error: 'No file' }); const iconUrl = `/uploads/logos/${req.file.filename}`; try { await setSetting(req.schema, 'icon_newchat', iconUrl); res.json({ iconUrl }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.post('/icon-groupinfo', authMiddleware, adminMiddleware, uploadGroupInfo.single('icon'), async (req, res) => { if (!req.file) return res.status(400).json({ error: 'No file' }); const iconUrl = `/uploads/logos/${req.file.filename}`; try { await setSetting(req.schema, 'icon_groupinfo', iconUrl); res.json({ iconUrl }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.patch('/colors', authMiddleware, adminMiddleware, async (req, res) => { const { colorTitle, colorTitleDark, colorAvatarPublic, colorAvatarDm } = req.body; try { if (colorTitle !== undefined) await setSetting(req.schema, 'color_title', colorTitle || ''); if (colorTitleDark !== undefined) await setSetting(req.schema, 'color_title_dark', colorTitleDark || ''); if (colorAvatarPublic !== undefined) await setSetting(req.schema, 'color_avatar_public', colorAvatarPublic || ''); if (colorAvatarDm !== undefined) await setSetting(req.schema, 'color_avatar_dm', colorAvatarDm || ''); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.post('/reset', authMiddleware, adminMiddleware, async (req, res) => { try { const originalName = process.env.APP_NAME || 'rosterchirp'; await exec(req.schema, "UPDATE settings SET value=$1, updated_at=NOW() WHERE key='app_name'", [originalName]); await exec(req.schema, "UPDATE settings SET value='', updated_at=NOW() WHERE key='logo_url'"); await exec(req.schema, "UPDATE settings SET value='', updated_at=NOW() WHERE key IN ('icon_newchat','icon_groupinfo','pwa_icon_192','pwa_icon_512','color_title','color_title_dark','color_avatar_public','color_avatar_dm')"); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); const VALID_CODES = { 'ROSTERCHIRP-TEAM-2024': { appType:'RosterChirp-Team', branding:true, groupManager:true, scheduleManager:true }, 'ROSTERCHIRP-BRAND-2024': { appType:'RosterChirp-Brand', branding:true, groupManager:false, scheduleManager:false }, 'ROSTERCHIRP-FULL-2024': { appType:'RosterChirp-Team', branding:true, groupManager:true, scheduleManager:true }, }; router.post('/register', authMiddleware, adminMiddleware, async (req, res) => { const { code } = req.body; try { if (!code?.trim()) { await setSetting(req.schema, 'registration_code', ''); await setSetting(req.schema, 'app_type', 'RosterChirp-Chat'); await setSetting(req.schema, 'feature_branding', 'false'); await setSetting(req.schema, 'feature_group_manager', 'false'); await setSetting(req.schema, 'feature_schedule_manager', 'false'); return res.json({ success:true, features:{branding:false,groupManager:false,scheduleManager:false,appType:'RosterChirp-Chat'} }); } const match = VALID_CODES[code.trim().toUpperCase()]; if (!match) return res.status(400).json({ error: 'Invalid registration code' }); await setSetting(req.schema, 'registration_code', code.trim()); await setSetting(req.schema, 'app_type', match.appType); await setSetting(req.schema, 'feature_branding', match.branding ? 'true' : 'false'); await setSetting(req.schema, 'feature_group_manager', match.groupManager ? 'true' : 'false'); await setSetting(req.schema, 'feature_schedule_manager', match.scheduleManager ? 'true' : 'false'); res.json({ success:true, features:{ branding:match.branding, groupManager:match.groupManager, scheduleManager:match.scheduleManager, appType:match.appType } }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.patch('/team', authMiddleware, adminMiddleware, async (req, res) => { const { toolManagers } = req.body; try { if (toolManagers !== undefined) { const val = JSON.stringify(toolManagers || []); await setSetting(req.schema, 'team_tool_managers', val); await setSetting(req.schema, 'team_group_managers', val); await setSetting(req.schema, 'team_schedule_managers', val); } res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); module.exports = router;