Overhaul settings and implement user preferences. Also implements dark theme toggle as part of the user settings.

This commit is contained in:
Cameron Redmore 2025-04-25 17:32:33 +01:00
parent b84f0907a8
commit 727746030c
17 changed files with 760 additions and 378 deletions

View file

@ -4,4 +4,16 @@ import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Export the Prisma Client instance for use in other modules
export default prisma;
export default prisma;
// Helper function for consistent error handling
export const handlePrismaError = (res, err, context) =>
{
console.error(`Error ${context}:`, err.message);
// Basic error handling, can be expanded (e.g., check for Prisma-specific error codes)
if (err.code === 'P2025')
{ // Prisma code for record not found
return res.status(404).json({ error: `${context}: Record not found` });
}
res.status(500).json({ error: `Failed to ${context}: ${err.message}` });
};

View file

@ -1,5 +1,6 @@
import { Router } from 'express';
import prisma from '../database.js';
import { handlePrismaError } from '../database.js';
import PDFDocument from 'pdfkit';
import { join } from 'path';
import { generateTodaysSummary } from '../services/mantisSummarizer.js';
@ -9,18 +10,6 @@ const router = Router();
const __dirname = new URL('.', import.meta.url).pathname.replace(/\/$/, '');
// Helper function for consistent error handling
const handlePrismaError = (res, err, context) =>
{
console.error(`Error ${context}:`, err.message);
// Basic error handling, can be expanded (e.g., check for Prisma-specific error codes)
if (err.code === 'P2025')
{ // Prisma code for record not found
return res.status(404).json({ error: `${context}: Record not found` });
}
res.status(500).json({ error: `Failed to ${context}: ${err.message}` });
};
// --- Forms API --- //
// GET /api/forms - List all forms
@ -647,59 +636,4 @@ router.post('/mantis-summaries/generate', async(req, res) =>
}
});
// --- Settings API --- //
// GET /api/settings/:key - Get a specific setting value
router.get('/settings/:key', async(req, res) =>
{
const { key } = req.params;
try
{
const setting = await prisma.setting.findUnique({
where: { key: key },
select: { value: true }
});
if (setting !== null)
{
res.json({ key, value: setting.value });
}
else
{
res.json({ key, value: '' }); // Return empty value if not found
}
}
catch (err)
{
handlePrismaError(res, err, `fetch setting '${key}'`);
}
});
// PUT /api/settings/:key - Update or create a specific setting
router.put('/settings/:key', async(req, res) =>
{
const { key } = req.params;
const { value } = req.body;
if (typeof value === 'undefined')
{
return res.status(400).json({ error: 'Setting value is required in the request body' });
}
try
{
const upsertedSetting = await prisma.setting.upsert({
where: { key: key },
update: { value: String(value) },
create: { key: key, value: String(value) },
select: { key: true, value: true } // Select to return the updated/created value
});
res.status(200).json(upsertedSetting);
}
catch (err)
{
handlePrismaError(res, err, `update setting '${key}'`);
}
});
export default router;

View file

@ -48,7 +48,8 @@ async function getAuthenticatorByCredentialID(credentialID)
// Generate Registration Options
router.post('/generate-registration-options', async(req, res) =>
{
const { username } = req.body;
// Destructure username, email, and fullName from the request body
const { username, email, fullName } = req.body;
if (!username)
{
@ -59,13 +60,18 @@ router.post('/generate-registration-options', async(req, res) =>
{
let user = await getUserByUsername(username);
// If user doesn't exist, create one
// If user doesn't exist, create one with the provided details
if (!user)
{
const userData = { username };
if (email) userData.email = email; // Add email if provided
if (fullName) userData.fullName = fullName; // Add fullName if provided
user = await prisma.user.create({
data: { username },
data: userData,
});
}
// ... rest of the existing logic ...
const userAuthenticators = await getUserAuthenticators(user.id);
@ -107,6 +113,11 @@ router.post('/generate-registration-options', async(req, res) =>
catch (error)
{
console.error('Registration options error:', error);
// Handle potential Prisma unique constraint errors (e.g., email already exists)
if (error.code === 'P2002' && error.meta?.target?.includes('email'))
{
return res.status(409).json({ error: 'Email address is already in use.' });
}
res.status(500).json({ error: 'Failed to generate registration options' });
}
});
@ -190,12 +201,18 @@ router.post('/verify-registration', async(req, res) =>
}
else
{
// This else block was previously misplaced before the if block
res.status(400).json({ error: 'Registration verification failed' });
}
}
catch (error)
{
console.error('Registration verification error:', error);
// Handle potential Prisma unique constraint errors (e.g., email already exists)
if (error.code === 'P2002' && error.meta?.target?.includes('email'))
{
return res.status(409).json({ error: 'Email address is already in use.' });
}
challengeStore.delete(userId); // Clean up challenge on error
delete req.session.userId;
res.status(500).json({ error: 'Failed to verify registration', details: error.message });
@ -437,7 +454,8 @@ router.get('/status', async(req, res) =>
{});
return res.status(401).json({ status: 'unauthenticated' });
}
return res.json({ status: 'authenticated', user: { id: user.id, username: user.username, email: user.email } });
// Include email and fullName in the response
return res.json({ status: 'authenticated', user: { id: user.id, username: user.username, email: user.email, fullName: user.fullName } });
}
res.json({ status: 'unauthenticated' });
});

View file

@ -0,0 +1,51 @@
import express from 'express';
import { getSetting, setSetting, getUserPreference, setUserPreference } from '../utils/settings.js';
import { handlePrismaError } from '../database.js';
const router = express.Router();
// --- Settings API --- //
// GET /api/settings/:key - Get a specific setting value
router.get('/:key', async(req, res) =>
{
const { key } = req.params;
try
{
const setting = await getSetting(key);
if (!setting)
{
return res.status(404).json({ error: `Setting '${key}' not found` });
}
res.status(200).json(setting);
}
catch (err)
{
handlePrismaError(res, err, `fetch setting '${key}'`);
}
});
// PUT /api/settings/:key - Update or create a specific setting
router.put('/:key', async(req, res) =>
{
const { key } = req.params;
const { value } = req.body;
if (typeof value === 'undefined')
{
return res.status(400).json({ error: 'Setting value is required in the request body' });
}
try
{
await setSetting(key, value);
res.status(200).json({ message: `Setting '${key}' updated successfully` });
}
catch (err)
{
handlePrismaError(res, err, `update setting '${key}'`);
}
});
export default router;

View file

@ -0,0 +1,57 @@
import express from 'express';
import { getUserPreference, setUserPreference } from '../utils/settings.js';
const router = express.Router();
// GET /api/user-preferences/:key - Get a user preference for the logged-in user
router.get('/:key', async(req, res) =>
{
const { key } = req.params;
const userId = req.session?.loggedInUserId;
if (!userId)
{
return res.status(401).json({ error: 'Not authenticated' });
}
try
{
const value = await getUserPreference(userId, key);
if (typeof value === 'undefined' || value === null)
{
return res.status(404).json({ error: `Preference '${key}' not found for user` });
}
res.status(200).json({ key, value });
}
catch (err)
{
handlePrismaError(res, err, `fetch user preference '${key}'`);
}
});
// PUT /api/user-preferences/:key - Set a user preference for the logged-in user
router.put('/:key', async(req, res) =>
{
const { key } = req.params;
const { value } = req.body;
const userId = req.session?.loggedInUserId;
if (!userId)
{
return res.status(401).json({ error: 'Not authenticated' });
}
if (typeof value === 'undefined')
{
return res.status(400).json({ error: 'Preference value is required in the request body' });
}
try
{
await setUserPreference(userId, key, value);
res.status(200).json({ message: `Preference '${key}' updated for user` });
}
catch (err)
{
handlePrismaError(res, err, `update user preference '${key}'`);
}
});
export default router;

View file

@ -12,27 +12,30 @@
import dotenv from 'dotenv';
import express from 'express';
import compression from 'compression';
import session from 'express-session'; // Added for session management
import { PrismaSessionStore } from '@quixo3/prisma-session-store'; // Import Prisma session store
import { PrismaClient } from '@prisma/client'; // Import Prisma Client
import { v4 as uuidv4 } from 'uuid'; // Added for generating session IDs
import session from 'express-session';
import { PrismaSessionStore } from '@quixo3/prisma-session-store';
import { PrismaClient } from '@prisma/client';
import { v4 as uuidv4 } from 'uuid';
import apiRoutes from './routes/api.js';
import authRoutes from './routes/auth.js'; // Added for WebAuthn routes
import chatRoutes from './routes/chat.js'; // Added for Chat routes
import authRoutes from './routes/auth.js';
import chatRoutes from './routes/chat.js';
import settingsRoutes from './routes/settings.js';
import userPreferencesRoutes from './routes/userPreferences.js';
import cron from 'node-cron';
import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js';
import { requireAuth } from './middlewares/authMiddleware.js';
dotenv.config();
// Define Relying Party details (Update with your actual details)
export const rpID = process.env.NODE_ENV === 'production' ? 'your-production-domain.com' : 'localhost';
export const rpID = process.env.NODE_ENV === 'production' ? 'stylepoint.uk' : 'localhost';
export const rpName = 'StylePoint';
export const origin = process.env.NODE_ENV === 'production' ? `https://${rpID}` : `http://${rpID}:9000`;
// In-memory store for challenges (Replace with a persistent store in production)
export const challengeStore = new Map();
const prisma = new PrismaClient(); // Instantiate Prisma Client
const prisma = new PrismaClient();
const app = express();
@ -90,12 +93,12 @@ app.disable('x-powered-by');
app.use(express.json());
// Add API routes
app.use('/api', apiRoutes);
app.use('/api/auth', authRoutes);
app.use('/api/chat', chatRoutes);
app.use('/api/chat', requireAuth, chatRoutes);
app.use('/api/user-preferences', requireAuth, userPreferencesRoutes);
app.use('/api/settings', requireAuth, settingsRoutes);
app.use('/api', requireAuth, apiRoutes);
// place here any middlewares that
// absolutely need to run before anything else
if (process.env.PROD)
{
app.use(compression());

View file

@ -99,16 +99,11 @@ export async function generateAndStoreMantisSummary()
{
try
{
// Get the prompt from the database settings using Prisma
const setting = await prisma.setting.findUnique({
where: { key: 'mantisPrompt' },
select: { value: true }
});
const promptTemplate = setting?.value;
const promptTemplate = await getSetting('MANTIS_PROMPT');
if (!promptTemplate)
{
console.error('Mantis prompt not found in database settings (key: mantisPrompt). Skipping summary generation.');
console.error('Mantis prompt not found in database settings (key: MANTIS_PROMPT). Skipping summary generation.');
return;
}

View file

@ -7,14 +7,44 @@ export async function getSetting(key)
select: { value: true }
});
console.log(`getSetting(${key})`, setting);
return setting?.value ? JSON.parse(setting.value) : null;
}
export async function setSetting(key, value)
{
//Replace all CRLFs with LF
if (typeof value === 'string')
{
value = value.replace(/\r\n/g, '\n').trim();
}
await prisma.setting.upsert({
where: { key },
update: { value: JSON.stringify(value) },
create: { key, value }
create: { key, value: JSON.stringify(value) }
});
}
export async function getUserPreference(userId, key)
{
const pref = await prisma.userPreference.findUnique({
where: { userId_key: { userId, key } },
select: { value: true }
});
return pref?.value ? JSON.parse(pref.value) : null;
}
export async function setUserPreference(userId, key, value)
{
if (typeof value === 'string')
{
value = value.replace(/\r\n/g, '\n').trim();
}
await prisma.userPreference.upsert({
where: { userId_key: { userId, key } },
update: { value: JSON.stringify(value) },
create: { userId, key, value: JSON.stringify(value) }
});
}