add branding option
All checks were successful
Build & Push Docker Image / build (push) Successful in 1m10s
All checks were successful
Build & Push Docker Image / build (push) Successful in 1m10s
This commit is contained in:
@@ -170,6 +170,12 @@ export async function initDatabase() {
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_room_shares_room_id ON room_shares(room_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_room_shares_user_id ON room_shares(user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
} else {
|
||||
await db.exec(`
|
||||
@@ -222,6 +228,12 @@ export async function initDatabase() {
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_room_shares_room_id ON room_shares(room_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_room_shares_user_id ON room_shares(user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import authRoutes from './routes/auth.js';
|
||||
import roomRoutes from './routes/rooms.js';
|
||||
import recordingRoutes from './routes/recordings.js';
|
||||
import adminRoutes from './routes/admin.js';
|
||||
import brandingRoutes from './routes/branding.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -31,6 +32,7 @@ async function start() {
|
||||
app.use('/api/rooms', roomRoutes);
|
||||
app.use('/api/recordings', recordingRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
app.use('/api/branding', brandingRoutes);
|
||||
|
||||
// Serve static files in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
160
server/routes/branding.js
Normal file
160
server/routes/branding.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getDb } from '../config/database.js';
|
||||
import { authenticateToken, requireAdmin } from '../middleware/auth.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Ensure uploads/branding directory exists
|
||||
const brandingDir = path.join(__dirname, '..', '..', 'uploads', 'branding');
|
||||
if (!fs.existsSync(brandingDir)) {
|
||||
fs.mkdirSync(brandingDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Multer config for logo upload
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, brandingDir),
|
||||
filename: (req, file, cb) => {
|
||||
const ext = path.extname(file.originalname).toLowerCase() || '.png';
|
||||
cb(null, `logo${ext}`);
|
||||
},
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 2 * 1024 * 1024 }, // 2MB
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowed = /\.(jpg|jpeg|png|gif|svg|webp|ico)$/i;
|
||||
const mimeAllowed = /^image\/(jpeg|png|gif|svg\+xml|webp|x-icon|vnd\.microsoft\.icon)$/;
|
||||
if (allowed.test(path.extname(file.originalname)) && mimeAllowed.test(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only image files are allowed'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Helper: get setting from DB
|
||||
async function getSetting(key) {
|
||||
const db = getDb();
|
||||
const row = await db.get('SELECT value FROM settings WHERE key = ?', [key]);
|
||||
return row?.value || null;
|
||||
}
|
||||
|
||||
// Helper: set setting in DB
|
||||
async function setSetting(key, value) {
|
||||
const db = getDb();
|
||||
// Upsert
|
||||
const existing = await db.get('SELECT key FROM settings WHERE key = ?', [key]);
|
||||
if (existing) {
|
||||
await db.run('UPDATE settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?', [value, key]);
|
||||
} else {
|
||||
await db.run('INSERT INTO settings (key, value) VALUES (?, ?)', [key, value]);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: delete setting from DB
|
||||
async function deleteSetting(key) {
|
||||
const db = getDb();
|
||||
await db.run('DELETE FROM settings WHERE key = ?', [key]);
|
||||
}
|
||||
|
||||
// Helper: find current logo file on disk
|
||||
function findLogoFile() {
|
||||
if (!fs.existsSync(brandingDir)) return null;
|
||||
const files = fs.readdirSync(brandingDir);
|
||||
const logo = files.find(f => f.startsWith('logo.'));
|
||||
return logo ? path.join(brandingDir, logo) : null;
|
||||
}
|
||||
|
||||
// GET /api/branding - Get branding settings (public)
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const appName = await getSetting('app_name');
|
||||
const logoFile = findLogoFile();
|
||||
|
||||
res.json({
|
||||
appName: appName || 'Redlight',
|
||||
hasLogo: !!logoFile,
|
||||
logoUrl: logoFile ? '/api/branding/logo' : null,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Get branding error:', err);
|
||||
res.status(500).json({ error: 'Could not load branding' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/branding/logo - Serve logo file (public)
|
||||
router.get('/logo', (req, res) => {
|
||||
const logoFile = findLogoFile();
|
||||
if (!logoFile) {
|
||||
return res.status(404).json({ error: 'No logo found' });
|
||||
}
|
||||
res.sendFile(logoFile);
|
||||
});
|
||||
|
||||
// POST /api/branding/logo - Upload logo (admin only)
|
||||
router.post('/logo', authenticateToken, requireAdmin, (req, res) => {
|
||||
upload.single('logo')(req, res, async (err) => {
|
||||
if (err) {
|
||||
if (err instanceof multer.MulterError) {
|
||||
return res.status(400).json({ error: err.code === 'LIMIT_FILE_SIZE' ? 'File too large (max 2MB)' : err.message });
|
||||
}
|
||||
return res.status(400).json({ error: err.message });
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
// Remove old logo files that don't match the new extension
|
||||
const files = fs.readdirSync(brandingDir);
|
||||
for (const f of files) {
|
||||
if (f.startsWith('logo.') && f !== req.file.filename) {
|
||||
fs.unlinkSync(path.join(brandingDir, f));
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
logoUrl: '/api/branding/logo',
|
||||
message: 'Logo uploaded',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// DELETE /api/branding/logo - Remove logo (admin only)
|
||||
router.delete('/logo', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const logoFile = findLogoFile();
|
||||
if (logoFile) {
|
||||
fs.unlinkSync(logoFile);
|
||||
}
|
||||
res.json({ message: 'Logo removed' });
|
||||
} catch (err) {
|
||||
console.error('Delete logo error:', err);
|
||||
res.status(500).json({ error: 'Could not remove logo' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/branding/name - Update app name (admin only)
|
||||
router.put('/name', authenticateToken, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { appName } = req.body;
|
||||
if (!appName || !appName.trim()) {
|
||||
return res.status(400).json({ error: 'App name is required' });
|
||||
}
|
||||
await setSetting('app_name', appName.trim());
|
||||
res.json({ appName: appName.trim() });
|
||||
} catch (err) {
|
||||
console.error('Update app name error:', err);
|
||||
res.status(500).json({ error: 'Could not update app name' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user