v0.9.88 major change sqlite to postgres

This commit is contained in:
2026-03-20 10:46:29 -04:00
parent 7dc4cfcbce
commit ac7cba0f92
31 changed files with 3729 additions and 2645 deletions

View File

@@ -1,190 +1,148 @@
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 { getDb } = require('../models/db');
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');
// Generic icon storage factory
function makeIconStorage(prefix) {
return multer.diskStorage({
destination: '/app/uploads/logos',
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `${prefix}_${Date.now()}${ext}`);
}
filename: (req, file, cb) => cb(null, `${prefix}_${Date.now()}${path.extname(file.originalname)}`),
});
}
const iconUploadOpts = {
const iconOpts = {
limits: { fileSize: 1 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) cb(null, true);
else cb(new Error('Images only'));
}
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 });
const uploadLogo = multer({ storage: makeIconStorage('logo'), ...iconUploadOpts });
const uploadNewChat = multer({ storage: makeIconStorage('newchat'), ...iconUploadOpts });
const uploadGroupInfo = multer({ storage: makeIconStorage('groupinfo'), ...iconUploadOpts });
// 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 public settings (accessible by all)
router.get('/', (req, res) => {
const db = getDb();
const settings = db.prepare('SELECT key, value FROM settings').all();
const obj = {};
for (const s of settings) obj[s.key] = s.value;
const admin = db.prepare('SELECT email FROM users WHERE is_default_admin = 1').get();
if (admin) obj.admin_email = admin.email;
// Expose app version from Docker build arg env var
obj.app_version = process.env.JAMA_VERSION || process.env.TEAMCHAT_VERSION || 'dev';
obj.user_pass = process.env.USER_PASS || 'user@1234';
res.json({ settings: obj });
// 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.JAMA_VERSION || 'dev';
obj.user_pass = process.env.USER_PASS || 'user@1234';
res.json({ settings: obj });
} catch (e) { res.status(500).json({ error: e.message }); }
});
// Update app name (admin)
router.patch('/app-name', authMiddleware, adminMiddleware, (req, res) => {
router.patch('/app-name', authMiddleware, adminMiddleware, async (req, res) => {
const { name } = req.body;
if (!name?.trim()) return res.status(400).json({ error: 'Name required' });
const db = getDb();
db.prepare("UPDATE settings SET value = ?, updated_at = datetime('now') WHERE key = 'app_name'").run(name.trim());
res.json({ success: true, name: name.trim() });
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 }); }
});
// Upload app logo (admin) — also generates 192x192 and 512x512 PWA icons
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}`;
const srcPath = req.file.path;
try {
// Generate PWA icons from the uploaded logo
const icon192Path = '/app/uploads/logos/pwa-icon-192.png';
const icon512Path = '/app/uploads/logos/pwa-icon-512.png';
await sharp(srcPath)
.resize(192, 192, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
.png()
.toFile(icon192Path);
await sharp(srcPath)
.resize(512, 512, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
.png()
.toFile(icon512Path);
const db = getDb();
db.prepare("UPDATE settings SET value = ?, updated_at = datetime('now') WHERE key = 'logo_url'").run(logoUrl);
// Store the PWA icon paths so the manifest can reference them
db.prepare("INSERT INTO settings (key, value) VALUES ('pwa_icon_192', ?) ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')")
.run('/uploads/logos/pwa-icon-192.png', '/uploads/logos/pwa-icon-192.png');
db.prepare("INSERT INTO settings (key, value) VALUES ('pwa_icon_512', ?) ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')")
.run('/uploads/logos/pwa-icon-512.png', '/uploads/logos/pwa-icon-512.png');
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] Failed to generate PWA icons:', err.message);
// Still save the logo even if icon generation fails
const db = getDb();
db.prepare("UPDATE settings SET value = ?, updated_at = datetime('now') WHERE key = 'logo_url'").run(logoUrl);
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 });
}
});
// Upload New Chat icon (admin)
router.post('/icon-newchat', authMiddleware, adminMiddleware, uploadNewChat.single('icon'), (req, res) => {
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}`;
const db = getDb();
db.prepare("INSERT INTO settings (key, value) VALUES ('icon_newchat', ?) ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')")
.run(iconUrl, iconUrl);
res.json({ iconUrl });
try { await setSetting(req.schema, 'icon_newchat', iconUrl); res.json({ iconUrl }); }
catch (e) { res.status(500).json({ error: e.message }); }
});
// Upload Group Info icon (admin)
router.post('/icon-groupinfo', authMiddleware, adminMiddleware, uploadGroupInfo.single('icon'), (req, res) => {
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}`;
const db = getDb();
db.prepare("INSERT INTO settings (key, value) VALUES ('icon_groupinfo', ?) ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')")
.run(iconUrl, iconUrl);
res.json({ iconUrl });
try { await setSetting(req.schema, 'icon_groupinfo', iconUrl); res.json({ iconUrl }); }
catch (e) { res.status(500).json({ error: e.message }); }
});
// Reset all settings to defaults (admin)
router.patch('/colors', authMiddleware, adminMiddleware, (req, res) => {
router.patch('/colors', authMiddleware, adminMiddleware, async (req, res) => {
const { colorTitle, colorTitleDark, colorAvatarPublic, colorAvatarDm } = req.body;
const db = getDb();
const upd = db.prepare("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')");
if (colorTitle !== undefined) upd.run('color_title', colorTitle || '', colorTitle || '');
if (colorTitleDark !== undefined) upd.run('color_title_dark', colorTitleDark || '', colorTitleDark || '');
if (colorAvatarPublic !== undefined) upd.run('color_avatar_public', colorAvatarPublic || '', colorAvatarPublic || '');
if (colorAvatarDm !== undefined) upd.run('color_avatar_dm', colorAvatarDm || '', colorAvatarDm || '');
res.json({ success: true });
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, (req, res) => {
const db = getDb();
const originalName = process.env.APP_NAME || 'jama';
db.prepare("UPDATE settings SET value = ?, updated_at = datetime('now') WHERE key = 'app_name'").run(originalName);
db.prepare("UPDATE settings SET value = '', updated_at = datetime('now') WHERE key = 'logo_url'").run();
db.prepare("UPDATE settings SET value = '', updated_at = datetime('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')").run();
res.json({ success: true });
router.post('/reset', authMiddleware, adminMiddleware, async (req, res) => {
try {
const originalName = process.env.APP_NAME || 'jama';
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 }); }
});
// ── Registration code ─────────────────────────────────────────────────────────
// Valid codes — in production these would be stored/validated server-side
const VALID_CODES = {
// JAMA-Team: full access — chat, branding, group manager, schedule manager
'JAMA-TEAM-2024': { appType: 'JAMA-Team', branding: true, groupManager: true, scheduleManager: true },
// JAMA-Brand: chat + branding only
'JAMA-BRAND-2024': { appType: 'JAMA-Brand', branding: true, groupManager: false, scheduleManager: false },
// Legacy codes — map to new tiers
'JAMA-FULL-2024': { appType: 'JAMA-Team', branding: true, groupManager: true, scheduleManager: true },
'JAMA-TEAM-2024': { appType:'JAMA-Team', branding:true, groupManager:true, scheduleManager:true },
'JAMA-BRAND-2024': { appType:'JAMA-Brand', branding:true, groupManager:false, scheduleManager:false },
'JAMA-FULL-2024': { appType:'JAMA-Team', branding:true, groupManager:true, scheduleManager:true },
};
router.post('/register', authMiddleware, adminMiddleware, (req, res) => {
router.post('/register', authMiddleware, adminMiddleware, async (req, res) => {
const { code } = req.body;
const db = getDb();
const upd = db.prepare("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')");
if (!code?.trim()) {
// Clear registration
upd.run('registration_code', '', '');
upd.run('app_type', 'JAMA-Chat', 'JAMA-Chat');
upd.run('feature_branding', 'false', 'false');
upd.run('feature_group_manager', 'false', 'false');
upd.run('feature_schedule_manager', 'false', 'false');
return res.json({ success: true, features: { branding: false, groupManager: false, scheduleManager: false, appType: 'JAMA-Chat' } });
}
const match = VALID_CODES[code.trim().toUpperCase()];
if (!match) return res.status(400).json({ error: 'Invalid registration code' });
upd.run('registration_code', code.trim(), code.trim());
upd.run('app_type', match.appType || 'JAMA-Chat', match.appType || 'JAMA-Chat');
upd.run('feature_branding', match.branding ? 'true' : 'false', match.branding ? 'true' : 'false');
upd.run('feature_group_manager', match.groupManager ? 'true' : 'false', match.groupManager ? 'true' : 'false');
upd.run('feature_schedule_manager', match.scheduleManager ? 'true' : 'false', match.scheduleManager ? 'true' : 'false');
res.json({ success: true, features: { branding: match.branding, groupManager: match.groupManager, scheduleManager: match.scheduleManager, appType: match.appType } });
try {
if (!code?.trim()) {
await setSetting(req.schema, 'registration_code', '');
await setSetting(req.schema, 'app_type', 'JAMA-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:'JAMA-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 }); }
});
// Save team management group assignments
router.patch('/team', authMiddleware, adminMiddleware, (req, res) => {
router.patch('/team', authMiddleware, adminMiddleware, async (req, res) => {
const { toolManagers } = req.body;
const db = getDb();
const upd = db.prepare("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')");
if (toolManagers !== undefined) {
const val = JSON.stringify(toolManagers || []);
upd.run('team_tool_managers', val, val);
// Keep legacy keys in sync so existing teamManagerMiddleware still works
upd.run('team_group_managers', val, val);
upd.run('team_schedule_managers', val, val);
}
res.json({ success: true });
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;