Added linting and enforced code styling.
This commit is contained in:
parent
8655eae39c
commit
86967b26cd
37 changed files with 3356 additions and 1875 deletions
|
@ -4,11 +4,4 @@ import { PrismaClient } from '@prisma/client';
|
|||
const prisma = new PrismaClient();
|
||||
|
||||
// Export the Prisma Client instance for use in other modules
|
||||
export default prisma;
|
||||
|
||||
// --- Old better-sqlite3 code removed ---
|
||||
// No need for initializeDatabase, getDb, closeDatabase, etc.
|
||||
// Prisma Client manages the connection pool.
|
||||
|
||||
// --- Settings Functions removed ---
|
||||
// Settings can now be accessed via prisma.setting.findUnique, prisma.setting.upsert, etc.
|
||||
export default prisma;
|
|
@ -1,7 +1,9 @@
|
|||
// src-ssr/middlewares/authMiddleware.js
|
||||
|
||||
export function requireAuth(req, res, next) {
|
||||
if (!req.session || !req.session.loggedInUserId) {
|
||||
export function requireAuth(req, res, next)
|
||||
{
|
||||
if (!req.session || !req.session.loggedInUserId)
|
||||
{
|
||||
// User is not authenticated
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
|
|
@ -1,45 +1,59 @@
|
|||
import { defineSsrMiddleware } from '#q-app/wrappers'
|
||||
import { defineSsrMiddleware } from '#q-app/wrappers';
|
||||
|
||||
// This middleware should execute as last one
|
||||
// since it captures everything and tries to
|
||||
// render the page with Vue
|
||||
|
||||
export default defineSsrMiddleware(({ app, resolve, render, serve }) => {
|
||||
export default defineSsrMiddleware(({ app, resolve, render, serve }) =>
|
||||
{
|
||||
// we capture any other Express route and hand it
|
||||
// over to Vue and Vue Router to render our page
|
||||
app.get(resolve.urlPath('*'), (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
app.get(resolve.urlPath('*'), (req, res) =>
|
||||
{
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
|
||||
render(/* the ssrContext: */ { req, res })
|
||||
.then(html => {
|
||||
.then(html =>
|
||||
{
|
||||
// now let's send the rendered html to the client
|
||||
res.send(html)
|
||||
res.send(html);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch(err =>
|
||||
{
|
||||
// oops, we had an error while rendering the page
|
||||
|
||||
// we were told to redirect to another URL
|
||||
if (err.url) {
|
||||
if (err.code) {
|
||||
res.redirect(err.code, err.url)
|
||||
} else {
|
||||
res.redirect(err.url)
|
||||
if (err.url)
|
||||
{
|
||||
if (err.code)
|
||||
{
|
||||
res.redirect(err.code, err.url);
|
||||
}
|
||||
} else if (err.code === 404) {
|
||||
else
|
||||
{
|
||||
res.redirect(err.url);
|
||||
}
|
||||
}
|
||||
else if (err.code === 404)
|
||||
{
|
||||
// hmm, Vue Router could not find the requested route
|
||||
|
||||
// Should reach here only if no "catch-all" route
|
||||
// is defined in /src/routes
|
||||
res.status(404).send('404 | Page Not Found')
|
||||
} else if (process.env.DEV) {
|
||||
res.status(404).send('404 | Page Not Found');
|
||||
}
|
||||
else if (process.env.DEV)
|
||||
{
|
||||
// well, we treat any other code as error;
|
||||
// if we're in dev mode, then we can use Quasar CLI
|
||||
// to display a nice error page that contains the stack
|
||||
// and other useful information
|
||||
|
||||
// serve.error is available on dev only
|
||||
serve.error({ err, req, res })
|
||||
} else {
|
||||
serve.error({ err, req, res });
|
||||
}
|
||||
else
|
||||
{
|
||||
// we're in production, so we should have another method
|
||||
// to display something to the client when we encounter an error
|
||||
// (for security reasons, it's not ok to display the same wealth
|
||||
|
@ -47,12 +61,13 @@ export default defineSsrMiddleware(({ app, resolve, render, serve }) => {
|
|||
|
||||
// Render Error Page on production or
|
||||
// create a route (/src/routes) for an error page and redirect to it
|
||||
res.status(500).send('500 | Internal Server Error')
|
||||
res.status(500).send('500 | Internal Server Error');
|
||||
|
||||
if (process.env.DEBUGGING) {
|
||||
console.error(err.stack)
|
||||
if (process.env.DEBUGGING)
|
||||
{
|
||||
console.error(err.stack);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import { Router } from 'express';
|
||||
import prisma from '../database.js'; // Import Prisma client
|
||||
import prisma from '../database.js';
|
||||
import PDFDocument from 'pdfkit';
|
||||
import { join } from 'path';
|
||||
import { generateTodaysSummary } from '../services/mantisSummarizer.js'; // Keep mantisSummarizer import
|
||||
import { generateAndStoreEmailSummary } from '../services/emailSummarizer.js'; // Import email summarizer function
|
||||
import { FieldType } from '@prisma/client'; // Import generated FieldType enum
|
||||
import { generateTodaysSummary } from '../services/mantisSummarizer.js';
|
||||
import { FieldType } from '@prisma/client';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const __dirname = new URL('.', import.meta.url).pathname.replace(/\/$/, '');
|
||||
|
||||
// Helper function for consistent error handling
|
||||
const handlePrismaError = (res, err, context) => {
|
||||
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
|
||||
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}` });
|
||||
|
@ -23,8 +24,10 @@ const handlePrismaError = (res, err, context) => {
|
|||
// --- Forms API --- //
|
||||
|
||||
// GET /api/forms - List all forms
|
||||
router.get('/forms', async (req, res) => {
|
||||
try {
|
||||
router.get('/forms', async(req, res) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const forms = await prisma.form.findMany({
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
|
@ -37,20 +40,25 @@ router.get('/forms', async (req, res) => {
|
|||
}
|
||||
});
|
||||
res.json(forms);
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, 'fetch forms');
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/forms - Create a new form
|
||||
router.post('/forms', async (req, res) => {
|
||||
router.post('/forms', async(req, res) =>
|
||||
{
|
||||
const { title, description, categories } = req.body;
|
||||
|
||||
if (!title) {
|
||||
if (!title)
|
||||
{
|
||||
return res.status(400).json({ error: 'Form title is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
const newForm = await prisma.form.create({
|
||||
data: {
|
||||
title,
|
||||
|
@ -60,12 +68,15 @@ router.post('/forms', async (req, res) => {
|
|||
name: category.name,
|
||||
sortOrder: catIndex,
|
||||
fields: {
|
||||
create: category.fields?.map((field, fieldIndex) => {
|
||||
create: category.fields?.map((field, fieldIndex) =>
|
||||
{
|
||||
// Validate field type against Prisma Enum
|
||||
if (!Object.values(FieldType).includes(field.type)) {
|
||||
if (!Object.values(FieldType).includes(field.type))
|
||||
{
|
||||
throw new Error(`Invalid field type: ${field.type}`);
|
||||
}
|
||||
if (!field.label) {
|
||||
if (!field.label)
|
||||
{
|
||||
throw new Error('Field label is required');
|
||||
}
|
||||
return {
|
||||
|
@ -86,21 +97,26 @@ router.post('/forms', async (req, res) => {
|
|||
}
|
||||
});
|
||||
res.status(201).json(newForm);
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, 'create form');
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/forms/:id - Get a specific form with its structure
|
||||
router.get('/forms/:id', async (req, res) => {
|
||||
router.get('/forms/:id', async(req, res) =>
|
||||
{
|
||||
const { id } = req.params;
|
||||
const formId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(formId)) {
|
||||
if (isNaN(formId))
|
||||
{
|
||||
return res.status(400).json({ error: 'Invalid form ID' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
const form = await prisma.form.findUnique({
|
||||
where: { id: formId },
|
||||
include: {
|
||||
|
@ -115,54 +131,68 @@ router.get('/forms/:id', async (req, res) => {
|
|||
},
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
if (!form)
|
||||
{
|
||||
return res.status(404).json({ error: 'Form not found' });
|
||||
}
|
||||
res.json(form);
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `fetch form ${formId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/forms/:id - Delete a specific form and all related data
|
||||
router.delete('/forms/:id', async (req, res) => {
|
||||
router.delete('/forms/:id', async(req, res) =>
|
||||
{
|
||||
const { id } = req.params;
|
||||
const formId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(formId)) {
|
||||
if (isNaN(formId))
|
||||
{
|
||||
return res.status(400).json({ error: 'Invalid form ID' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
// Prisma automatically handles cascading deletes based on schema relations (onDelete: Cascade)
|
||||
const deletedForm = await prisma.form.delete({
|
||||
where: { id: formId },
|
||||
});
|
||||
res.status(200).json({ message: `Form ${formId} and all related data deleted successfully.` });
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `delete form ${formId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/forms/:id - Update an existing form
|
||||
router.put('/forms/:id', async (req, res) => {
|
||||
router.put('/forms/:id', async(req, res) =>
|
||||
{
|
||||
const { id } = req.params;
|
||||
const formId = parseInt(id, 10);
|
||||
const { title, description, categories } = req.body;
|
||||
|
||||
if (isNaN(formId)) {
|
||||
if (isNaN(formId))
|
||||
{
|
||||
return res.status(400).json({ error: 'Invalid form ID' });
|
||||
}
|
||||
if (!title) {
|
||||
if (!title)
|
||||
{
|
||||
return res.status(400).json({ error: 'Form title is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
// Use a transaction to ensure atomicity: delete old structure, update form, create new structure
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const result = await prisma.$transaction(async(tx) =>
|
||||
{
|
||||
// 1. Check if form exists (optional, delete/update will fail if not found anyway)
|
||||
const existingForm = await tx.form.findUnique({ where: { id: formId } });
|
||||
if (!existingForm) {
|
||||
if (!existingForm)
|
||||
{
|
||||
throw { code: 'P2025' }; // Simulate Prisma not found error
|
||||
}
|
||||
|
||||
|
@ -180,11 +210,14 @@ router.put('/forms/:id', async (req, res) => {
|
|||
name: category.name,
|
||||
sortOrder: catIndex,
|
||||
fields: {
|
||||
create: category.fields?.map((field, fieldIndex) => {
|
||||
if (!Object.values(FieldType).includes(field.type)) {
|
||||
create: category.fields?.map((field, fieldIndex) =>
|
||||
{
|
||||
if (!Object.values(FieldType).includes(field.type))
|
||||
{
|
||||
throw new Error(`Invalid field type: ${field.type}`);
|
||||
}
|
||||
if (!field.label) {
|
||||
if (!field.label)
|
||||
{
|
||||
throw new Error('Field label is required');
|
||||
}
|
||||
return {
|
||||
|
@ -208,7 +241,9 @@ router.put('/forms/:id', async (req, res) => {
|
|||
});
|
||||
|
||||
res.status(200).json(result);
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `update form ${formId}`);
|
||||
}
|
||||
});
|
||||
|
@ -217,24 +252,30 @@ router.put('/forms/:id', async (req, res) => {
|
|||
// --- Responses API --- //
|
||||
|
||||
// POST /api/forms/:id/responses - Submit a response for a form
|
||||
router.post('/forms/:id/responses', async (req, res) => {
|
||||
router.post('/forms/:id/responses', async(req, res) =>
|
||||
{
|
||||
const { id } = req.params;
|
||||
const formId = parseInt(id, 10);
|
||||
const { values } = req.body; // values is expected to be { fieldId: value, ... }
|
||||
|
||||
if (isNaN(formId)) {
|
||||
if (isNaN(formId))
|
||||
{
|
||||
return res.status(400).json({ error: 'Invalid form ID' });
|
||||
}
|
||||
if (!values || typeof values !== 'object' || Object.keys(values).length === 0) {
|
||||
if (!values || typeof values !== 'object' || Object.keys(values).length === 0)
|
||||
{
|
||||
return res.status(400).json({ error: 'Response values are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
// Use transaction to ensure response and values are created together
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const result = await prisma.$transaction(async(tx) =>
|
||||
{
|
||||
// 1. Verify form exists
|
||||
const form = await tx.form.findUnique({ where: { id: formId }, select: { id: true } });
|
||||
if (!form) {
|
||||
if (!form)
|
||||
{
|
||||
throw { code: 'P2025' }; // Simulate Prisma not found error
|
||||
}
|
||||
|
||||
|
@ -253,23 +294,27 @@ router.post('/forms/:id/responses', async (req, res) => {
|
|||
// Optional: Verify all field IDs belong to the form (more robust)
|
||||
const validFields = await tx.field.findMany({
|
||||
where: {
|
||||
id: { in: fieldIds },
|
||||
id: { 'in': fieldIds },
|
||||
category: { formId: formId }
|
||||
},
|
||||
select: { id: true }
|
||||
});
|
||||
const validFieldIds = new Set(validFields.map(f => f.id));
|
||||
|
||||
for (const fieldIdStr in values) {
|
||||
for (const fieldIdStr in values)
|
||||
{
|
||||
const fieldId = parseInt(fieldIdStr, 10);
|
||||
if (validFieldIds.has(fieldId)) {
|
||||
if (validFieldIds.has(fieldId))
|
||||
{
|
||||
const value = values[fieldIdStr];
|
||||
responseValuesData.push({
|
||||
responseId: newResponse.id,
|
||||
fieldId: fieldId,
|
||||
value: (value === null || typeof value === 'undefined') ? null : String(value),
|
||||
});
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
console.warn(`Attempted to submit value for field ${fieldId} not belonging to form ${formId}`);
|
||||
// Decide whether to throw an error or just skip invalid fields
|
||||
// throw new Error(`Field ${fieldId} does not belong to form ${formId}`);
|
||||
|
@ -277,7 +322,8 @@ router.post('/forms/:id/responses', async (req, res) => {
|
|||
}
|
||||
|
||||
// 4. Create all response values
|
||||
if (responseValuesData.length > 0) {
|
||||
if (responseValuesData.length > 0)
|
||||
{
|
||||
await tx.responseValue.createMany({
|
||||
data: responseValuesData,
|
||||
});
|
||||
|
@ -287,24 +333,30 @@ router.post('/forms/:id/responses', async (req, res) => {
|
|||
});
|
||||
|
||||
res.status(201).json(result);
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `submit response for form ${formId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/forms/:id/responses - Get all responses for a form
|
||||
router.get('/forms/:id/responses', async (req, res) => {
|
||||
router.get('/forms/:id/responses', async(req, res) =>
|
||||
{
|
||||
const { id } = req.params;
|
||||
const formId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(formId)) {
|
||||
if (isNaN(formId))
|
||||
{
|
||||
return res.status(400).json({ error: 'Invalid form ID' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
// 1. Check if form exists
|
||||
const formExists = await prisma.form.findUnique({ where: { id: formId }, select: { id: true } });
|
||||
if (!formExists) {
|
||||
if (!formExists)
|
||||
{
|
||||
return res.status(404).json({ error: 'Form not found' });
|
||||
}
|
||||
|
||||
|
@ -328,13 +380,15 @@ router.get('/forms/:id/responses', async (req, res) => {
|
|||
id: response.id,
|
||||
submittedAt: response.submittedAt,
|
||||
values: response.responseValues
|
||||
.sort((a, b) => {
|
||||
.sort((a, b) =>
|
||||
{
|
||||
// Sort by category order, then field order
|
||||
const catSort = a.field.category.sortOrder - b.field.category.sortOrder;
|
||||
if (catSort !== 0) return catSort;
|
||||
return a.field.sortOrder - b.field.sortOrder;
|
||||
})
|
||||
.reduce((acc, rv) => {
|
||||
.reduce((acc, rv) =>
|
||||
{
|
||||
acc[rv.fieldId] = {
|
||||
label: rv.field.label,
|
||||
type: rv.field.type,
|
||||
|
@ -345,22 +399,27 @@ router.get('/forms/:id/responses', async (req, res) => {
|
|||
}));
|
||||
|
||||
res.json(groupedResponses);
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `fetch responses for form ${formId}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// GET /responses/:responseId/export/pdf - Export response as PDF
|
||||
router.get('/responses/:responseId/export/pdf', async (req, res) => {
|
||||
router.get('/responses/:responseId/export/pdf', async(req, res) =>
|
||||
{
|
||||
const { responseId: responseIdStr } = req.params;
|
||||
const responseId = parseInt(responseIdStr, 10);
|
||||
|
||||
if (isNaN(responseId)) {
|
||||
if (isNaN(responseId))
|
||||
{
|
||||
return res.status(400).json({ error: 'Invalid response ID' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
// 1. Fetch the response, form title, form structure, and values in one go
|
||||
const responseData = await prisma.response.findUnique({
|
||||
where: { id: responseId },
|
||||
|
@ -385,13 +444,15 @@ router.get('/responses/:responseId/export/pdf', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
if (!responseData) {
|
||||
if (!responseData)
|
||||
{
|
||||
return res.status(404).json({ error: 'Response not found' });
|
||||
}
|
||||
|
||||
const formTitle = responseData.form.title;
|
||||
const categories = responseData.form.categories;
|
||||
const responseValues = responseData.responseValues.reduce((acc, rv) => {
|
||||
const responseValues = responseData.responseValues.reduce((acc, rv) =>
|
||||
{
|
||||
acc[rv.fieldId] = (rv.value === null || typeof rv.value === 'undefined') ? '' : String(rv.value);
|
||||
return acc;
|
||||
}, {});
|
||||
|
@ -414,39 +475,53 @@ router.get('/responses/:responseId/export/pdf', async (req, res) => {
|
|||
doc.fontSize(18).font('Roboto-Bold').text(formTitle, { align: 'center' });
|
||||
doc.moveDown();
|
||||
|
||||
for (const category of categories) {
|
||||
if (category.name) {
|
||||
doc.fontSize(14).font('Roboto-Bold').text(category.name);
|
||||
doc.moveDown(0.5);
|
||||
for (const category of categories)
|
||||
{
|
||||
if (category.name)
|
||||
{
|
||||
doc.fontSize(14).font('Roboto-Bold').text(category.name);
|
||||
doc.moveDown(0.5);
|
||||
}
|
||||
|
||||
for (const field of category.fields) {
|
||||
for (const field of category.fields)
|
||||
{
|
||||
const value = responseValues[field.id] || '';
|
||||
doc.fontSize(12).font('Roboto-SemiBold').text(field.label + ':', { continued: false });
|
||||
if (field.description) {
|
||||
doc.fontSize(9).font('Roboto-Italics').text(field.description);
|
||||
if (field.description)
|
||||
{
|
||||
doc.fontSize(9).font('Roboto-Italics').text(field.description);
|
||||
}
|
||||
doc.moveDown(0.2);
|
||||
doc.fontSize(11).font('Roboto-Regular');
|
||||
if (field.type === 'textarea') {
|
||||
if (field.type === 'textarea')
|
||||
{
|
||||
const textHeight = doc.heightOfString(value, { width: 500 });
|
||||
doc.rect(doc.x, doc.y, 500, Math.max(textHeight + 10, 30)).stroke();
|
||||
doc.text(value, doc.x + 5, doc.y + 5, { width: 490 });
|
||||
doc.y += Math.max(textHeight + 10, 30) + 10;
|
||||
} else if (field.type === 'date') {
|
||||
}
|
||||
else if (field.type === 'date')
|
||||
{
|
||||
let formattedDate = '';
|
||||
if (value) {
|
||||
try {
|
||||
if (value)
|
||||
{
|
||||
try
|
||||
{
|
||||
const dateObj = new Date(value + 'T00:00:00');
|
||||
if (!isNaN(dateObj.getTime())) {
|
||||
if (!isNaN(dateObj.getTime()))
|
||||
{
|
||||
const day = String(dateObj.getDate()).padStart(2, '0');
|
||||
const month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
||||
const year = dateObj.getFullYear();
|
||||
formattedDate = `${day}/${month}/${year}`;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
formattedDate = value;
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.error('Error formatting date:', value, e);
|
||||
formattedDate = value;
|
||||
}
|
||||
|
@ -454,28 +529,37 @@ router.get('/responses/:responseId/export/pdf', async (req, res) => {
|
|||
doc.text(formattedDate || ' ');
|
||||
doc.lineCap('butt').moveTo(doc.x, doc.y).lineTo(doc.x + 500, doc.y).stroke();
|
||||
doc.moveDown(1.5);
|
||||
} else if (field.type === 'boolean') {
|
||||
}
|
||||
else if (field.type === 'boolean')
|
||||
{
|
||||
const displayValue = value === 'true' ? 'Yes' : (value === 'false' ? 'No' : ' ');
|
||||
doc.text(displayValue);
|
||||
doc.lineCap('butt').moveTo(doc.x, doc.y).lineTo(doc.x + 500, doc.y).stroke();
|
||||
doc.moveDown(1.5);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
doc.text(value || ' ');
|
||||
doc.lineCap('butt').moveTo(doc.x, doc.y).lineTo(doc.x + 500, doc.y).stroke();
|
||||
doc.moveDown(1.5);
|
||||
}
|
||||
}
|
||||
doc.moveDown(1);
|
||||
doc.moveDown(1);
|
||||
}
|
||||
doc.end();
|
||||
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
console.error(`Error generating PDF for response ${responseId}:`, err.message);
|
||||
if (!res.headersSent) {
|
||||
if (!res.headersSent)
|
||||
{
|
||||
// Use the helper function
|
||||
handlePrismaError(res, err, `generate PDF for response ${responseId}`);
|
||||
} else {
|
||||
console.error("Headers already sent, could not send JSON error for PDF generation failure.");
|
||||
}
|
||||
else
|
||||
{
|
||||
console.error('Headers already sent, could not send JSON error for PDF generation failure.');
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
@ -485,8 +569,10 @@ router.get('/responses/:responseId/export/pdf', async (req, res) => {
|
|||
// --- Mantis Summary API Route --- //
|
||||
|
||||
// GET /api/mantis-summary/today - Get today's summary specifically
|
||||
router.get('/mantis-summary/today', async (req, res) => {
|
||||
try {
|
||||
router.get('/mantis-summary/today', async(req, res) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0); // Set to start of day UTC for comparison
|
||||
|
||||
|
@ -495,23 +581,30 @@ router.get('/mantis-summary/today', async (req, res) => {
|
|||
select: { summaryDate: true, summaryText: true, generatedAt: true }
|
||||
});
|
||||
|
||||
if (todaySummary) {
|
||||
if (todaySummary)
|
||||
{
|
||||
res.json(todaySummary);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
res.status(404).json({ message: `No Mantis summary found for today (${today.toISOString().split('T')[0]}).` });
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
handlePrismaError(res, error, 'fetch today\'s Mantis summary');
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/mantis-summaries - Get ALL summaries from the DB, with pagination
|
||||
router.get('/mantis-summaries', async (req, res) => {
|
||||
router.get('/mantis-summaries', async(req, res) =>
|
||||
{
|
||||
const page = parseInt(req.query.page, 10) || 1;
|
||||
const limit = parseInt(req.query.limit, 10) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
const [summaries, totalItems] = await prisma.$transaction([
|
||||
prisma.mantisSummary.findMany({
|
||||
orderBy: { summaryDate: 'desc' },
|
||||
|
@ -523,104 +616,78 @@ router.get('/mantis-summaries', async (req, res) => {
|
|||
]);
|
||||
|
||||
res.json({ summaries, total: totalItems });
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
handlePrismaError(res, error, 'fetch paginated Mantis summaries');
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/mantis-summaries/generate - Trigger summary generation
|
||||
router.post('/mantis-summaries/generate', async (req, res) => {
|
||||
try {
|
||||
router.post('/mantis-summaries/generate', async(req, res) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Trigger generation asynchronously, don't wait for it
|
||||
generateTodaysSummary()
|
||||
.then(() => {
|
||||
.then(() =>
|
||||
{
|
||||
console.log('Summary generation process finished successfully (async).');
|
||||
})
|
||||
.catch(error => {
|
||||
.catch(error =>
|
||||
{
|
||||
console.error('Background summary generation failed:', error);
|
||||
});
|
||||
|
||||
res.status(202).json({ message: 'Summary generation started.' });
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
handlePrismaError(res, error, 'initiate Mantis summary generation');
|
||||
}
|
||||
});
|
||||
|
||||
// --- Email Summary API Routes --- //
|
||||
|
||||
// GET /api/email-summaries - Get ALL email summaries from the DB, with pagination
|
||||
router.get('/email-summaries', async (req, res) => {
|
||||
const page = parseInt(req.query.page, 10) || 1;
|
||||
const limit = parseInt(req.query.limit, 10) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
try {
|
||||
const [summaries, totalItems] = await prisma.$transaction([
|
||||
prisma.emailSummary.findMany({ // Use emailSummary model
|
||||
orderBy: { summaryDate: 'desc' },
|
||||
take: limit,
|
||||
skip: skip,
|
||||
select: { id: true, summaryDate: true, summaryText: true, generatedAt: true }
|
||||
}),
|
||||
prisma.emailSummary.count() // Count emailSummary model
|
||||
]);
|
||||
|
||||
res.json({ summaries, total: totalItems });
|
||||
} catch (error) {
|
||||
handlePrismaError(res, error, 'fetch paginated Email summaries');
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/email-summaries/generate - Trigger email summary generation
|
||||
router.post('/email-summaries/generate', async (req, res) => {
|
||||
try {
|
||||
// Trigger generation asynchronously, don't wait for it
|
||||
generateAndStoreEmailSummary() // Use the email summarizer function
|
||||
.then(() => {
|
||||
console.log('Email summary generation process finished successfully (async).');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Background email summary generation failed:', error);
|
||||
});
|
||||
|
||||
res.status(202).json({ message: 'Email summary generation started.' });
|
||||
} catch (error) {
|
||||
handlePrismaError(res, error, 'initiate Email summary generation');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- Settings API --- //
|
||||
|
||||
// GET /api/settings/:key - Get a specific setting value
|
||||
router.get('/settings/:key', async (req, res) => {
|
||||
router.get('/settings/:key', async(req, res) =>
|
||||
{
|
||||
const { key } = req.params;
|
||||
try {
|
||||
try
|
||||
{
|
||||
const setting = await prisma.setting.findUnique({
|
||||
where: { key: key },
|
||||
select: { value: true }
|
||||
});
|
||||
|
||||
if (setting !== null) {
|
||||
if (setting !== null)
|
||||
{
|
||||
res.json({ key, value: setting.value });
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
res.json({ key, value: '' }); // Return empty value if not found
|
||||
}
|
||||
} catch (err) {
|
||||
}
|
||||
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) => {
|
||||
router.put('/settings/:key', async(req, res) =>
|
||||
{
|
||||
const { key } = req.params;
|
||||
const { value } = req.body;
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
if (typeof value === 'undefined')
|
||||
{
|
||||
return res.status(400).json({ error: 'Setting value is required in the request body' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
const upsertedSetting = await prisma.setting.upsert({
|
||||
where: { key: key },
|
||||
update: { value: String(value) },
|
||||
|
@ -628,7 +695,9 @@ router.put('/settings/:key', async (req, res) => {
|
|||
select: { key: true, value: true } // Select to return the updated/created value
|
||||
});
|
||||
res.status(200).json(upsertedSetting);
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
handlePrismaError(res, err, `update setting '${key}'`);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -13,7 +13,8 @@ import { rpID, rpName, origin, challengeStore } from '../server.js'; // Import R
|
|||
const router = express.Router();
|
||||
|
||||
// Helper function to get user authenticators
|
||||
async function getUserAuthenticators(userId) {
|
||||
async function getUserAuthenticators(userId)
|
||||
{
|
||||
return prisma.authenticator.findMany({
|
||||
where: { userId },
|
||||
select: {
|
||||
|
@ -26,34 +27,41 @@ async function getUserAuthenticators(userId) {
|
|||
}
|
||||
|
||||
// Helper function to get a user by username
|
||||
async function getUserByUsername(username) {
|
||||
return prisma.user.findUnique({ where: { username } });
|
||||
async function getUserByUsername(username)
|
||||
{
|
||||
return prisma.user.findUnique({ where: { username } });
|
||||
}
|
||||
|
||||
// Helper function to get a user by ID
|
||||
async function getUserById(id) {
|
||||
return prisma.user.findUnique({ where: { id } });
|
||||
async function getUserById(id)
|
||||
{
|
||||
return prisma.user.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
// Helper function to get an authenticator by credential ID
|
||||
async function getAuthenticatorByCredentialID(credentialID) {
|
||||
return prisma.authenticator.findUnique({ where: { credentialID } });
|
||||
async function getAuthenticatorByCredentialID(credentialID)
|
||||
{
|
||||
return prisma.authenticator.findUnique({ where: { credentialID } });
|
||||
}
|
||||
|
||||
|
||||
// Generate Registration Options
|
||||
router.post('/generate-registration-options', async (req, res) => {
|
||||
router.post('/generate-registration-options', async(req, res) =>
|
||||
{
|
||||
const { username } = req.body;
|
||||
|
||||
if (!username) {
|
||||
if (!username)
|
||||
{
|
||||
return res.status(400).json({ error: 'Username is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
let user = await getUserByUsername(username);
|
||||
|
||||
// If user doesn't exist, create one
|
||||
if (!user) {
|
||||
if (!user)
|
||||
{
|
||||
user = await prisma.user.create({
|
||||
data: { username },
|
||||
});
|
||||
|
@ -61,9 +69,11 @@ router.post('/generate-registration-options', async (req, res) => {
|
|||
|
||||
const userAuthenticators = await getUserAuthenticators(user.id);
|
||||
|
||||
if(userAuthenticators.length > 0) {
|
||||
if(userAuthenticators.length > 0)
|
||||
{
|
||||
//The user is trying to register a new authenticator, so we need to check if the user registering is the same as the one in the session
|
||||
if (!req.session.loggedInUserId || req.session.loggedInUserId !== user.id) {
|
||||
if (!req.session.loggedInUserId || req.session.loggedInUserId !== user.id)
|
||||
{
|
||||
return res.status(403).json({ error: 'Invalid registration attempt.' });
|
||||
}
|
||||
}
|
||||
|
@ -93,31 +103,38 @@ router.post('/generate-registration-options', async (req, res) => {
|
|||
req.session.userId = user.id; // Temporarily store userId in session for verification step
|
||||
|
||||
res.json(options);
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Registration options error:', error);
|
||||
res.status(500).json({ error: 'Failed to generate registration options' });
|
||||
}
|
||||
});
|
||||
|
||||
// Verify Registration
|
||||
router.post('/verify-registration', async (req, res) => {
|
||||
router.post('/verify-registration', async(req, res) =>
|
||||
{
|
||||
const { registrationResponse } = req.body;
|
||||
const userId = req.session.userId; // Retrieve userId stored during options generation
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'User session not found. Please start registration again.' });
|
||||
if (!userId)
|
||||
{
|
||||
return res.status(400).json({ error: 'User session not found. Please start registration again.' });
|
||||
}
|
||||
|
||||
const expectedChallenge = challengeStore.get(userId);
|
||||
|
||||
if (!expectedChallenge) {
|
||||
if (!expectedChallenge)
|
||||
{
|
||||
return res.status(400).json({ error: 'Challenge not found or expired' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
const user = await getUserById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
if (!user)
|
||||
{
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const verification = await verifyRegistrationResponse({
|
||||
|
@ -132,7 +149,8 @@ router.post('/verify-registration', async (req, res) => {
|
|||
|
||||
console.log(verification);
|
||||
|
||||
if (verified && registrationInfo) {
|
||||
if (verified && registrationInfo)
|
||||
{
|
||||
const { credential, credentialDeviceType, credentialBackedUp } = registrationInfo;
|
||||
|
||||
const credentialID = credential.id;
|
||||
|
@ -143,7 +161,8 @@ router.post('/verify-registration', async (req, res) => {
|
|||
// Check if authenticator with this ID already exists
|
||||
const existingAuthenticator = await getAuthenticatorByCredentialID(isoBase64URL.fromBuffer(credentialID));
|
||||
|
||||
if (existingAuthenticator) {
|
||||
if (existingAuthenticator)
|
||||
{
|
||||
return res.status(409).json({ error: 'Authenticator already registered' });
|
||||
}
|
||||
|
||||
|
@ -168,10 +187,14 @@ router.post('/verify-registration', async (req, res) => {
|
|||
req.session.loggedInUserId = user.id;
|
||||
|
||||
res.json({ verified: true });
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
res.status(400).json({ error: 'Registration verification failed' });
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Registration verification error:', error);
|
||||
challengeStore.delete(userId); // Clean up challenge on error
|
||||
delete req.session.userId;
|
||||
|
@ -180,19 +203,25 @@ router.post('/verify-registration', async (req, res) => {
|
|||
});
|
||||
|
||||
// Generate Authentication Options
|
||||
router.post('/generate-authentication-options', async (req, res) => {
|
||||
router.post('/generate-authentication-options', async(req, res) =>
|
||||
{
|
||||
const { username } = req.body;
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
let user;
|
||||
if (username) {
|
||||
user = await getUserByUsername(username);
|
||||
} else if (req.session.loggedInUserId) {
|
||||
// If already logged in, allow re-authentication (e.g., for step-up)
|
||||
user = await getUserById(req.session.loggedInUserId);
|
||||
if (username)
|
||||
{
|
||||
user = await getUserByUsername(username);
|
||||
}
|
||||
else if (req.session.loggedInUserId)
|
||||
{
|
||||
// If already logged in, allow re-authentication (e.g., for step-up)
|
||||
user = await getUserById(req.session.loggedInUserId);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
if (!user)
|
||||
{
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
|
@ -218,42 +247,51 @@ router.post('/generate-authentication-options', async (req, res) => {
|
|||
req.session.challengeUserId = user.id; // Store user ID associated with this challenge
|
||||
|
||||
res.json(options);
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Authentication options error:', error);
|
||||
res.status(500).json({ error: 'Failed to generate authentication options' });
|
||||
}
|
||||
});
|
||||
|
||||
// Verify Authentication
|
||||
router.post('/verify-authentication', async (req, res) => {
|
||||
router.post('/verify-authentication', async(req, res) =>
|
||||
{
|
||||
const { authenticationResponse } = req.body;
|
||||
const challengeUserId = req.session.challengeUserId; // Get user ID associated with the challenge
|
||||
|
||||
if (!challengeUserId) {
|
||||
return res.status(400).json({ error: 'Challenge session not found. Please try logging in again.' });
|
||||
if (!challengeUserId)
|
||||
{
|
||||
return res.status(400).json({ error: 'Challenge session not found. Please try logging in again.' });
|
||||
}
|
||||
|
||||
const expectedChallenge = challengeStore.get(challengeUserId);
|
||||
|
||||
if (!expectedChallenge) {
|
||||
if (!expectedChallenge)
|
||||
{
|
||||
return res.status(400).json({ error: 'Challenge not found or expired' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
const user = await getUserById(challengeUserId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User associated with challenge not found' });
|
||||
if (!user)
|
||||
{
|
||||
return res.status(404).json({ error: 'User associated with challenge not found' });
|
||||
}
|
||||
|
||||
const authenticator = await getAuthenticatorByCredentialID(authenticationResponse.id);
|
||||
|
||||
if (!authenticator) {
|
||||
if (!authenticator)
|
||||
{
|
||||
return res.status(404).json({ error: 'Authenticator not found' });
|
||||
}
|
||||
|
||||
// Ensure the authenticator belongs to the user attempting to log in
|
||||
if (authenticator.userId !== user.id) {
|
||||
return res.status(403).json({ error: 'Authenticator does not belong to this user' });
|
||||
if (authenticator.userId !== user.id)
|
||||
{
|
||||
return res.status(403).json({ error: 'Authenticator does not belong to this user' });
|
||||
}
|
||||
|
||||
const verification = await verifyAuthenticationResponse({
|
||||
|
@ -272,7 +310,8 @@ router.post('/verify-authentication', async (req, res) => {
|
|||
|
||||
const { verified, authenticationInfo } = verification;
|
||||
|
||||
if (verified) {
|
||||
if (verified)
|
||||
{
|
||||
// Update the authenticator counter
|
||||
await prisma.authenticator.update({
|
||||
where: { credentialID: authenticator.credentialID },
|
||||
|
@ -287,10 +326,14 @@ router.post('/verify-authentication', async (req, res) => {
|
|||
req.session.loggedInUserId = user.id;
|
||||
|
||||
res.json({ verified: true, user: { id: user.id, username: user.username } });
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
res.status(400).json({ error: 'Authentication verification failed' });
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Authentication verification error:', error);
|
||||
challengeStore.delete(challengeUserId); // Clean up challenge on error
|
||||
delete req.session.challengeUserId;
|
||||
|
@ -299,12 +342,15 @@ router.post('/verify-authentication', async (req, res) => {
|
|||
});
|
||||
|
||||
// GET Passkeys for Logged-in User
|
||||
router.get('/passkeys', async (req, res) => {
|
||||
if (!req.session.loggedInUserId) {
|
||||
router.get('/passkeys', async(req, res) =>
|
||||
{
|
||||
if (!req.session.loggedInUserId)
|
||||
{
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
const userId = req.session.loggedInUserId;
|
||||
const authenticators = await prisma.authenticator.findMany({
|
||||
where: { userId },
|
||||
|
@ -317,25 +363,31 @@ router.get('/passkeys', async (req, res) => {
|
|||
|
||||
// No need to convert credentialID here as it's stored as Base64URL string
|
||||
res.json(authenticators);
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error fetching passkeys:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch passkeys' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE Passkey
|
||||
router.delete('/passkeys/:credentialID', async (req, res) => {
|
||||
if (!req.session.loggedInUserId) {
|
||||
router.delete('/passkeys/:credentialID', async(req, res) =>
|
||||
{
|
||||
if (!req.session.loggedInUserId)
|
||||
{
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
const { credentialID } = req.params; // This is already a Base64URL string from the client
|
||||
|
||||
if (!credentialID) {
|
||||
if (!credentialID)
|
||||
{
|
||||
return res.status(400).json({ error: 'Credential ID is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
const userId = req.session.loggedInUserId;
|
||||
|
||||
// Find the authenticator first to ensure it belongs to the logged-in user
|
||||
|
@ -343,12 +395,14 @@ router.delete('/passkeys/:credentialID', async (req, res) => {
|
|||
where: { credentialID: credentialID }, // Use the Base64URL string directly
|
||||
});
|
||||
|
||||
if (!authenticator) {
|
||||
if (!authenticator)
|
||||
{
|
||||
return res.status(404).json({ error: 'Passkey not found' });
|
||||
}
|
||||
|
||||
// Security check: Ensure the passkey belongs to the user trying to delete it
|
||||
if (authenticator.userId !== userId) {
|
||||
if (authenticator.userId !== userId)
|
||||
{
|
||||
return res.status(403).json({ error: 'Permission denied' });
|
||||
}
|
||||
|
||||
|
@ -358,28 +412,36 @@ router.delete('/passkeys/:credentialID', async (req, res) => {
|
|||
});
|
||||
|
||||
res.json({ message: 'Passkey deleted successfully' });
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error deleting passkey:', error);
|
||||
// Handle potential Prisma errors, e.g., record not found if deleted between check and delete
|
||||
if (error.code === 'P2025') { // Prisma code for record not found on delete/update
|
||||
return res.status(404).json({ error: 'Passkey not found' });
|
||||
if (error.code === 'P2025')
|
||||
{ // Prisma code for record not found on delete/update
|
||||
return res.status(404).json({ error: 'Passkey not found' });
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to delete passkey' });
|
||||
}
|
||||
});
|
||||
|
||||
// Check Authentication Status
|
||||
router.get('/status', (req, res) => {
|
||||
if (req.session.loggedInUserId) {
|
||||
router.get('/status', (req, res) =>
|
||||
{
|
||||
if (req.session.loggedInUserId)
|
||||
{
|
||||
return res.json({ status: 'authenticated' });
|
||||
}
|
||||
res.json({ status: 'unauthenticated' });
|
||||
});
|
||||
|
||||
// Logout
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy(err => {
|
||||
if (err) {
|
||||
router.post('/logout', (req, res) =>
|
||||
{
|
||||
req.session.destroy(err =>
|
||||
{
|
||||
if (err)
|
||||
{
|
||||
console.error('Logout error:', err);
|
||||
return res.status(500).json({ error: 'Failed to logout' });
|
||||
}
|
||||
|
|
|
@ -10,17 +10,21 @@ const router = Router();
|
|||
router.use(requireAuth);
|
||||
|
||||
// POST /api/chat/threads - Create a new chat thread (optionally with a first message)
|
||||
router.post('/threads', async (req, res) => {
|
||||
router.post('/threads', async(req, res) =>
|
||||
{
|
||||
const { content } = req.body; // Content is now optional
|
||||
|
||||
// If content is provided, validate it
|
||||
if (content && (typeof content !== 'string' || content.trim().length === 0)) {
|
||||
if (content && (typeof content !== 'string' || content.trim().length === 0))
|
||||
{
|
||||
return res.status(400).json({ error: 'Message content cannot be empty if provided.' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
const createData = {};
|
||||
if (content) {
|
||||
if (content)
|
||||
{
|
||||
// If content exists, create the thread with the first message
|
||||
createData.messages = {
|
||||
create: [
|
||||
|
@ -48,21 +52,25 @@ router.post('/threads', async (req, res) => {
|
|||
|
||||
// Respond with the new thread ID and messages (if any)
|
||||
res.status(201).json({
|
||||
threadId: newThread.id,
|
||||
// Ensure messages array is empty if no content was provided
|
||||
messages: newThread.messages ? newThread.messages.map(msg => ({ ...msg, createdAt: msg.createdAt.toISOString() })) : []
|
||||
threadId: newThread.id,
|
||||
// Ensure messages array is empty if no content was provided
|
||||
messages: newThread.messages ? newThread.messages.map(msg => ({ ...msg, createdAt: msg.createdAt.toISOString() })) : []
|
||||
});
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error creating chat thread:', error);
|
||||
res.status(500).json({ error: 'Failed to create chat thread.' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/chat/threads/:threadId/messages - Get messages for a specific thread
|
||||
router.get('/threads/:threadId/messages', async (req, res) => {
|
||||
router.get('/threads/:threadId/messages', async(req, res) =>
|
||||
{
|
||||
const { threadId } = req.params;
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
const messages = await prisma.chatMessage.findMany({
|
||||
where: {
|
||||
threadId: threadId,
|
||||
|
@ -72,45 +80,55 @@ router.get('/threads/:threadId/messages', async (req, res) => {
|
|||
},
|
||||
});
|
||||
|
||||
if (!messages) { // Check if thread exists indirectly
|
||||
// If findMany returns empty, the thread might not exist or has no messages.
|
||||
// Check if thread exists explicitly
|
||||
const thread = await prisma.chatThread.findUnique({ where: { id: threadId } });
|
||||
if (!thread) {
|
||||
return res.status(404).json({ error: 'Chat thread not found.' });
|
||||
}
|
||||
if (!messages)
|
||||
{ // Check if thread exists indirectly
|
||||
// If findMany returns empty, the thread might not exist or has no messages.
|
||||
// Check if thread exists explicitly
|
||||
const thread = await prisma.chatThread.findUnique({ where: { id: threadId } });
|
||||
if (!thread)
|
||||
{
|
||||
return res.status(404).json({ error: 'Chat thread not found.' });
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json(messages.map(msg => ({ ...msg, createdAt: msg.createdAt.toISOString() })));
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error(`Error fetching messages for thread ${threadId}:`, error);
|
||||
// Basic error handling, check for specific Prisma errors if needed
|
||||
if (error.code === 'P2023' || error.message.includes('Malformed UUID')) { // Example: Invalid UUID format
|
||||
return res.status(400).json({ error: 'Invalid thread ID format.' });
|
||||
if (error.code === 'P2023' || error.message.includes('Malformed UUID'))
|
||||
{ // Example: Invalid UUID format
|
||||
return res.status(400).json({ error: 'Invalid thread ID format.' });
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to fetch messages.' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/chat/threads/:threadId/messages - Add a message to an existing thread
|
||||
router.post('/threads/:threadId/messages', async (req, res) => {
|
||||
router.post('/threads/:threadId/messages', async(req, res) =>
|
||||
{
|
||||
const { threadId } = req.params;
|
||||
const { content, sender = 'user' } = req.body; // Default sender to 'user'
|
||||
|
||||
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
||||
if (!content || typeof content !== 'string' || content.trim().length === 0)
|
||||
{
|
||||
return res.status(400).json({ error: 'Message content cannot be empty.' });
|
||||
}
|
||||
if (sender !== 'user' && sender !== 'bot') {
|
||||
return res.status(400).json({ error: 'Invalid sender type.' });
|
||||
if (sender !== 'user' && sender !== 'bot')
|
||||
{
|
||||
return res.status(400).json({ error: 'Invalid sender type.' });
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
// Verify thread exists first
|
||||
const thread = await prisma.chatThread.findUnique({
|
||||
where: { id: threadId },
|
||||
});
|
||||
|
||||
if (!thread) {
|
||||
if (!thread)
|
||||
{
|
||||
return res.status(404).json({ error: 'Chat thread not found.' });
|
||||
}
|
||||
|
||||
|
@ -124,17 +142,20 @@ router.post('/threads/:threadId/messages', async (req, res) => {
|
|||
|
||||
// Optionally: Update the thread's updatedAt timestamp
|
||||
await prisma.chatThread.update({
|
||||
where: { id: threadId },
|
||||
data: { updatedAt: new Date() }
|
||||
where: { id: threadId },
|
||||
data: { updatedAt: new Date() }
|
||||
});
|
||||
|
||||
await askGeminiChat(threadId, content); // Call the function to handle the bot response
|
||||
|
||||
res.status(201).json({ ...newMessage, createdAt: newMessage.createdAt.toISOString() });
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error(`Error adding message to thread ${threadId}:`, error);
|
||||
if (error.code === 'P2023' || error.message.includes('Malformed UUID')) { // Example: Invalid UUID format
|
||||
return res.status(400).json({ error: 'Invalid thread ID format.' });
|
||||
if (error.code === 'P2023' || error.message.includes('Malformed UUID'))
|
||||
{ // Example: Invalid UUID format
|
||||
return res.status(400).json({ error: 'Invalid thread ID format.' });
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to add message.' });
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
* Make sure to yarn add / npm install (in your project root)
|
||||
* anything you import here (except for express and compression).
|
||||
*/
|
||||
import express from 'express'
|
||||
import compression from 'compression'
|
||||
import express from 'express';
|
||||
import compression from 'compression';
|
||||
import session from 'express-session'; // Added for session management
|
||||
import { v4 as uuidv4 } from 'uuid'; // Added for generating session IDs
|
||||
import {
|
||||
|
@ -19,7 +19,7 @@ import {
|
|||
defineSsrClose,
|
||||
defineSsrServeStaticContent,
|
||||
defineSsrRenderPreloadTag
|
||||
} from '#q-app/wrappers'
|
||||
} from '#q-app/wrappers';
|
||||
|
||||
import prisma from './database.js'; // Import the prisma client instance
|
||||
import apiRoutes from './routes/api.js';
|
||||
|
@ -43,8 +43,9 @@ export const challengeStore = new Map();
|
|||
*
|
||||
* Can be async: defineSsrCreate(async ({ ... }) => { ... })
|
||||
*/
|
||||
export const create = defineSsrCreate((/* { ... } */) => {
|
||||
const app = express()
|
||||
export const create = defineSsrCreate((/* { ... } */) =>
|
||||
{
|
||||
const app = express();
|
||||
|
||||
// Session middleware configuration
|
||||
app.use(session({
|
||||
|
@ -60,29 +61,36 @@ export const create = defineSsrCreate((/* { ... } */) => {
|
|||
}));
|
||||
|
||||
// Initialize the database (now synchronous)
|
||||
try {
|
||||
try
|
||||
{
|
||||
console.log('Prisma Client is ready.'); // Log Prisma readiness
|
||||
|
||||
// Schedule the Mantis summary task after DB initialization
|
||||
// Run daily at 1:00 AM server time (adjust as needed)
|
||||
cron.schedule('0 1 * * *', async () => {
|
||||
cron.schedule('0 1 * * *', async() =>
|
||||
{
|
||||
console.log('Running scheduled Mantis summary task...');
|
||||
try {
|
||||
try
|
||||
{
|
||||
await generateAndStoreMantisSummary();
|
||||
console.log('Scheduled Mantis summary task completed.');
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error running scheduled Mantis summary task:', error);
|
||||
}
|
||||
}, {
|
||||
scheduled: true,
|
||||
timezone: "Europe/London" // Example: Set to your server's timezone
|
||||
timezone: 'Europe/London' // Example: Set to your server's timezone
|
||||
});
|
||||
console.log('Mantis summary cron job scheduled.');
|
||||
|
||||
// Optional: Run once immediately on server start if needed
|
||||
generateAndStoreMantisSummary().catch(err => console.error('Initial Mantis summary failed:', err));
|
||||
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error during server setup:', error);
|
||||
// Optionally handle the error more gracefully, e.g., prevent server start
|
||||
process.exit(1); // Exit if setup fails
|
||||
|
@ -90,7 +98,7 @@ export const create = defineSsrCreate((/* { ... } */) => {
|
|||
|
||||
// attackers can use this header to detect apps running Express
|
||||
// and then launch specifically-targeted attacks
|
||||
app.disable('x-powered-by')
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// Add JSON body parsing middleware
|
||||
app.use(express.json());
|
||||
|
@ -102,12 +110,13 @@ export const create = defineSsrCreate((/* { ... } */) => {
|
|||
|
||||
// place here any middlewares that
|
||||
// absolutely need to run before anything else
|
||||
if (process.env.PROD) {
|
||||
app.use(compression())
|
||||
if (process.env.PROD)
|
||||
{
|
||||
app.use(compression());
|
||||
}
|
||||
|
||||
return app
|
||||
})
|
||||
return app;
|
||||
});
|
||||
|
||||
/**
|
||||
* You need to make the server listen to the indicated port
|
||||
|
@ -122,14 +131,17 @@ export const create = defineSsrCreate((/* { ... } */) => {
|
|||
*
|
||||
* Can be async: defineSsrListen(async ({ app, devHttpsApp, port }) => { ... })
|
||||
*/
|
||||
export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
|
||||
const server = devHttpsApp || app
|
||||
return server.listen(port, () => {
|
||||
if (process.env.PROD) {
|
||||
console.log('Server listening at port ' + port)
|
||||
export const listen = defineSsrListen(({ app, devHttpsApp, port }) =>
|
||||
{
|
||||
const server = devHttpsApp || app;
|
||||
return server.listen(port, () =>
|
||||
{
|
||||
if (process.env.PROD)
|
||||
{
|
||||
console.log('Server listening at port ' + port);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Should close the server and free up any resources.
|
||||
|
@ -138,21 +150,25 @@ export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
|
|||
*
|
||||
* Can be async: defineSsrClose(async ({ ... }) => { ... })
|
||||
*/
|
||||
export const close = defineSsrClose(async ({ listenResult }) => {
|
||||
export const close = defineSsrClose(async({ listenResult }) =>
|
||||
{
|
||||
// Close the database connection when the server shuts down
|
||||
try {
|
||||
try
|
||||
{
|
||||
await prisma.$disconnect();
|
||||
console.log('Prisma Client disconnected.');
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.error('Error disconnecting Prisma Client:', e);
|
||||
}
|
||||
|
||||
return listenResult.close()
|
||||
})
|
||||
return listenResult.close();
|
||||
});
|
||||
|
||||
const maxAge = process.env.DEV
|
||||
? 0
|
||||
: 1000 * 60 * 60 * 24 * 30
|
||||
: 1000 * 60 * 60 * 24 * 30;
|
||||
|
||||
/**
|
||||
* Should return a function that will be used to configure the webserver
|
||||
|
@ -163,53 +179,63 @@ const maxAge = process.env.DEV
|
|||
* Can be async: defineSsrServeStaticContent(async ({ app, resolve }) => {
|
||||
* Can return an async function: return async ({ urlPath = '/', pathToServe = '.', opts = {} }) => {
|
||||
*/
|
||||
export const serveStaticContent = defineSsrServeStaticContent(({ app, resolve }) => {
|
||||
return ({ urlPath = '/', pathToServe = '.', opts = {} }) => {
|
||||
const serveFn = express.static(resolve.public(pathToServe), { maxAge, ...opts })
|
||||
app.use(resolve.urlPath(urlPath), serveFn)
|
||||
}
|
||||
})
|
||||
export const serveStaticContent = defineSsrServeStaticContent(({ app, resolve }) =>
|
||||
{
|
||||
return ({ urlPath = '/', pathToServe = '.', opts = {} }) =>
|
||||
{
|
||||
const serveFn = express.static(resolve.public(pathToServe), { maxAge, ...opts });
|
||||
app.use(resolve.urlPath(urlPath), serveFn);
|
||||
};
|
||||
});
|
||||
|
||||
const jsRE = /\.js$/
|
||||
const cssRE = /\.css$/
|
||||
const woffRE = /\.woff$/
|
||||
const woff2RE = /\.woff2$/
|
||||
const gifRE = /\.gif$/
|
||||
const jpgRE = /\.jpe?g$/
|
||||
const pngRE = /\.png$/
|
||||
const jsRE = /\.js$/;
|
||||
const cssRE = /\.css$/;
|
||||
const woffRE = /\.woff$/;
|
||||
const woff2RE = /\.woff2$/;
|
||||
const gifRE = /\.gif$/;
|
||||
const jpgRE = /\.jpe?g$/;
|
||||
const pngRE = /\.png$/;
|
||||
|
||||
/**
|
||||
* Should return a String with HTML output
|
||||
* (if any) for preloading indicated file
|
||||
*/
|
||||
export const renderPreloadTag = defineSsrRenderPreloadTag((file/* , { ssrContext } */) => {
|
||||
if (jsRE.test(file) === true) {
|
||||
return `<link rel="modulepreload" href="${file}" crossorigin>`
|
||||
export const renderPreloadTag = defineSsrRenderPreloadTag((file/* , { ssrContext } */) =>
|
||||
{
|
||||
if (jsRE.test(file) === true)
|
||||
{
|
||||
return `<link rel="modulepreload" href="${file}" crossorigin>`;
|
||||
}
|
||||
|
||||
if (cssRE.test(file) === true) {
|
||||
return `<link rel="stylesheet" href="${file}" crossorigin>`
|
||||
if (cssRE.test(file) === true)
|
||||
{
|
||||
return `<link rel="stylesheet" href="${file}" crossorigin>`;
|
||||
}
|
||||
|
||||
if (woffRE.test(file) === true) {
|
||||
return `<link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
|
||||
if (woffRE.test(file) === true)
|
||||
{
|
||||
return `<link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`;
|
||||
}
|
||||
|
||||
if (woff2RE.test(file) === true) {
|
||||
return `<link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
|
||||
if (woff2RE.test(file) === true)
|
||||
{
|
||||
return `<link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`;
|
||||
}
|
||||
|
||||
if (gifRE.test(file) === true) {
|
||||
return `<link rel="preload" href="${file}" as="image" type="image/gif" crossorigin>`
|
||||
if (gifRE.test(file) === true)
|
||||
{
|
||||
return `<link rel="preload" href="${file}" as="image" type="image/gif" crossorigin>`;
|
||||
}
|
||||
|
||||
if (jpgRE.test(file) === true) {
|
||||
return `<link rel="preload" href="${file}" as="image" type="image/jpeg" crossorigin>`
|
||||
if (jpgRE.test(file) === true)
|
||||
{
|
||||
return `<link rel="preload" href="${file}" as="image" type="image/jpeg" crossorigin>`;
|
||||
}
|
||||
|
||||
if (pngRE.test(file) === true) {
|
||||
return `<link rel="preload" href="${file}" as="image" type="image/png" crossorigin>`
|
||||
if (pngRE.test(file) === true)
|
||||
{
|
||||
return `<link rel="preload" href="${file}" as="image" type="image/png" crossorigin>`;
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
return '';
|
||||
});
|
||||
|
|
|
@ -1,199 +0,0 @@
|
|||
import Imap from 'node-imap';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import { GoogleGenAI } from '@google/genai';
|
||||
import prisma from '../database.js';
|
||||
|
||||
// --- Environment Variables ---
|
||||
const { GOOGLE_API_KEY } = process.env; // Added
|
||||
|
||||
// --- AI Setup ---
|
||||
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
|
||||
apiKey: GOOGLE_API_KEY,
|
||||
}) : null; // Added
|
||||
|
||||
export async function fetchAndFormatEmails() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const imapConfig = {
|
||||
user: process.env.OUTLOOK_EMAIL_ADDRESS,
|
||||
password: process.env.OUTLOOK_APP_PASSWORD,
|
||||
host: 'outlook.office365.com',
|
||||
port: 993,
|
||||
tls: true,
|
||||
tlsOptions: { rejectUnauthorized: false } // Adjust as needed for your environment
|
||||
};
|
||||
|
||||
const imap = new Imap(imapConfig);
|
||||
const emailsJson = [];
|
||||
|
||||
function openInbox(cb) {
|
||||
// Note: IMAP uses '/' as hierarchy separator, adjust if your server uses something else
|
||||
imap.openBox('SLSNotifications/Reports/Backups', false, cb);
|
||||
}
|
||||
|
||||
imap.once('ready', () => {
|
||||
openInbox((err, box) => {
|
||||
if (err) {
|
||||
imap.end();
|
||||
return reject(new Error(`Error opening mailbox: ${err.message}`));
|
||||
}
|
||||
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const searchCriteria = [['SINCE', yesterday.toISOString().split('T')[0]]]; // Search since midnight yesterday
|
||||
const fetchOptions = { bodies: ['HEADER.FIELDS (SUBJECT DATE)', 'TEXT'], struct: true };
|
||||
|
||||
imap.search(searchCriteria, (searchErr, results) => {
|
||||
if (searchErr) {
|
||||
imap.end();
|
||||
return reject(new Error(`Error searching emails: ${searchErr.message}`));
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log('No emails found from the last 24 hours.');
|
||||
imap.end();
|
||||
return resolve([]);
|
||||
}
|
||||
|
||||
const f = imap.fetch(results, fetchOptions);
|
||||
let processedCount = 0;
|
||||
|
||||
f.on('message', (msg, seqno) => {
|
||||
let header = '';
|
||||
let body = '';
|
||||
|
||||
msg.on('body', (stream, info) => {
|
||||
let buffer = '';
|
||||
stream.on('data', (chunk) => {
|
||||
buffer += chunk.toString('utf8');
|
||||
});
|
||||
stream.once('end', () => {
|
||||
if (info.which === 'TEXT') {
|
||||
body = buffer;
|
||||
} else {
|
||||
// Assuming HEADER.FIELDS (SUBJECT DATE) comes as one chunk
|
||||
header = buffer;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
msg.once('attributes', (attrs) => {
|
||||
// Attributes might contain date if not fetched via header
|
||||
});
|
||||
|
||||
msg.once('end', async () => {
|
||||
try {
|
||||
// Use mailparser to handle potential encoding issues and structure
|
||||
const mail = await simpleParser(`Subject: ${header.match(/Subject: (.*)/i)?.[1] || ''}\nDate: ${header.match(/Date: (.*)/i)?.[1] || ''}\n\n${body}`);
|
||||
emailsJson.push({
|
||||
title: mail.subject || 'No Subject',
|
||||
time: mail.date ? mail.date.toISOString() : 'No Date',
|
||||
body: mail.text || mail.html || 'No Body Content' // Prefer text, fallback to html, then empty
|
||||
});
|
||||
} catch (parseErr) {
|
||||
console.error(`Error parsing email seqno ${seqno}:`, parseErr);
|
||||
// Decide if you want to reject or just skip this email
|
||||
}
|
||||
|
||||
processedCount++;
|
||||
if (processedCount === results.length) {
|
||||
// This check might be slightly inaccurate if errors occur,
|
||||
// but it's a common pattern. Consider refining with promises.
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
f.once('error', (fetchErr) => {
|
||||
console.error('Fetch error: ' + fetchErr);
|
||||
// Don't reject here immediately, might still get some emails
|
||||
});
|
||||
|
||||
f.once('end', () => {
|
||||
console.log('Done fetching all messages!');
|
||||
imap.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
imap.once('error', (err) => {
|
||||
reject(new Error(`IMAP Connection Error: ${err.message}`));
|
||||
});
|
||||
|
||||
imap.once('end', () => {
|
||||
console.log('IMAP Connection ended.');
|
||||
resolve(emailsJson); // Resolve with the collected emails
|
||||
});
|
||||
|
||||
imap.connect();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Email Summary Logic (New Function) ---
|
||||
export async function generateAndStoreEmailSummary() {
|
||||
console.log('Attempting to generate and store Email summary...');
|
||||
if (!ai) {
|
||||
console.error('Google AI API key not configured. Skipping email summary generation.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the prompt from the database settings using Prisma
|
||||
const setting = await prisma.setting.findUnique({
|
||||
where: { key: 'emailPrompt' }, // Use 'emailPrompt' as the key
|
||||
select: { value: true }
|
||||
});
|
||||
const promptTemplate = setting?.value;
|
||||
|
||||
if (!promptTemplate) {
|
||||
console.error('Email prompt not found in database settings (key: emailPrompt). Skipping summary generation.');
|
||||
return;
|
||||
}
|
||||
|
||||
const emails = await fetchAndFormatEmails();
|
||||
|
||||
let summaryText;
|
||||
if (emails.length === 0) {
|
||||
summaryText = "No relevant emails found in the last 24 hours.";
|
||||
console.log('No recent emails found for summary.');
|
||||
} else {
|
||||
console.log(`Found ${emails.length} recent emails. Generating summary...`);
|
||||
// Replace placeholder in the prompt template
|
||||
// Ensure your prompt template uses $EMAIL_DATA
|
||||
let prompt = promptTemplate.replaceAll("$EMAIL_DATA", JSON.stringify(emails, null, 2));
|
||||
|
||||
// Call the AI model (adjust model name and config as needed)
|
||||
const response = await ai.models.generateContent({
|
||||
"model": "gemini-2.5-preview-04-17",
|
||||
"contents": prompt,
|
||||
config: {
|
||||
temperature: 0 // Adjust temperature as needed
|
||||
}
|
||||
});
|
||||
|
||||
summaryText = response.text;
|
||||
console.log('Email summary generated successfully by AI.');
|
||||
}
|
||||
|
||||
// Store the summary in the database using Prisma upsert
|
||||
const today = new Date();
|
||||
today.setUTCHours(0, 0, 0, 0); // Use UTC start of day for consistency
|
||||
|
||||
await prisma.emailSummary.upsert({
|
||||
where: { summaryDate: today },
|
||||
update: {
|
||||
summaryText: summaryText,
|
||||
// generatedAt is updated automatically by @default(now())
|
||||
},
|
||||
create: {
|
||||
summaryDate: today,
|
||||
summaryText: summaryText,
|
||||
},
|
||||
});
|
||||
console.log(`Email summary for ${today.toISOString().split('T')[0]} stored/updated in the database.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error during Email summary generation/storage:", error);
|
||||
// Re-throw or handle as appropriate for your application
|
||||
throw error;
|
||||
}
|
||||
}
|
|
@ -1,48 +1,45 @@
|
|||
import axios from 'axios';
|
||||
import { GoogleGenAI } from '@google/genai';
|
||||
import prisma from '../database.js'; // Import Prisma client
|
||||
|
||||
// --- Environment Variables ---
|
||||
const {
|
||||
MANTIS_API_KEY,
|
||||
MANTIS_API_ENDPOINT,
|
||||
GOOGLE_API_KEY
|
||||
} = process.env;
|
||||
|
||||
// --- Mantis Summarizer Setup ---
|
||||
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
|
||||
apiKey: GOOGLE_API_KEY,
|
||||
}) : null;
|
||||
import { getSetting } from '../utils/settings.js';
|
||||
import { askGemini } from '../utils/gemini.js';
|
||||
|
||||
const usernameMap = {
|
||||
'credmore': 'Cameron Redmore',
|
||||
'dgibson': 'Dane Gibson',
|
||||
'egzibovskis': 'Ed Gzibovskis',
|
||||
'ascotney': 'Amanda Scotney',
|
||||
'gclough': 'Garry Clough',
|
||||
'slee': 'Sarah Lee',
|
||||
'dwalker': 'Dave Walker',
|
||||
'askaith': 'Amy Skaith',
|
||||
'dpotter': 'Danny Potter',
|
||||
'msmart': 'Michael Smart',
|
||||
credmore: 'Cameron Redmore',
|
||||
dgibson: 'Dane Gibson',
|
||||
egzibovskis: 'Ed Gzibovskis',
|
||||
ascotney: 'Amanda Scotney',
|
||||
gclough: 'Garry Clough',
|
||||
slee: 'Sarah Lee',
|
||||
dwalker: 'Dave Walker',
|
||||
askaith: 'Amy Skaith',
|
||||
dpotter: 'Danny Potter',
|
||||
msmart: 'Michael Smart',
|
||||
// Add other usernames as needed
|
||||
};
|
||||
|
||||
async function getMantisTickets() {
|
||||
if (!MANTIS_API_ENDPOINT || !MANTIS_API_KEY) {
|
||||
throw new Error("Mantis API endpoint or key not configured in environment variables.");
|
||||
async function getMantisTickets()
|
||||
{
|
||||
const MANTIS_API_KEY = await getSetting('MANTIS_API_KEY');
|
||||
const MANTIS_API_ENDPOINT = await getSetting('MANTIS_API_ENDPOINT');
|
||||
|
||||
if (!MANTIS_API_ENDPOINT || !MANTIS_API_KEY)
|
||||
{
|
||||
throw new Error('Mantis API endpoint or key not configured in environment variables.');
|
||||
}
|
||||
const url = `${MANTIS_API_ENDPOINT}/issues?project_id=1&page_size=50&select=id,summary,description,created_at,updated_at,reporter,notes`;
|
||||
const headers = {
|
||||
'Authorization': `${MANTIS_API_KEY}`,
|
||||
'Accept': 'application/json',
|
||||
Authorization: `${MANTIS_API_KEY}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
const response = await axios.get(url, { headers });
|
||||
|
||||
const tickets = response.data.issues.filter((ticket) => {
|
||||
const tickets = response.data.issues.filter((ticket) =>
|
||||
{
|
||||
const ticketDate = new Date(ticket.updated_at);
|
||||
const thresholdDate = new Date();
|
||||
const currentDay = thresholdDate.getDay(); // Sunday = 0, Monday = 1, ...
|
||||
|
@ -53,7 +50,8 @@ async function getMantisTickets() {
|
|||
thresholdDate.setHours(0, 0, 0, 0); // Start of the day
|
||||
|
||||
return ticketDate >= thresholdDate;
|
||||
}).map((ticket) => {
|
||||
}).map((ticket) =>
|
||||
{
|
||||
return {
|
||||
id: ticket.id,
|
||||
summary: ticket.summary,
|
||||
|
@ -61,7 +59,8 @@ async function getMantisTickets() {
|
|||
created_at: ticket.created_at,
|
||||
updated_at: ticket.updated_at,
|
||||
reporter: usernameMap[ticket.reporter?.username] || ticket.reporter?.name || 'Unknown Reporter', // Safer access
|
||||
notes: (ticket.notes ? ticket.notes.filter((note) => {
|
||||
notes: (ticket.notes ? ticket.notes.filter((note) =>
|
||||
{
|
||||
const noteDate = new Date(note.created_at);
|
||||
const thresholdDate = new Date();
|
||||
const currentDay = thresholdDate.getDay();
|
||||
|
@ -69,7 +68,8 @@ async function getMantisTickets() {
|
|||
thresholdDate.setDate(thresholdDate.getDate() - daysToSubtract);
|
||||
thresholdDate.setHours(0, 0, 0, 0); // Start of the day
|
||||
return noteDate >= thresholdDate;
|
||||
}) : []).map((note) => {
|
||||
}) : []).map((note) =>
|
||||
{
|
||||
const reporter = usernameMap[note.reporter?.username] || note.reporter?.name || 'Unknown Reporter'; // Safer access
|
||||
return {
|
||||
reporter,
|
||||
|
@ -81,27 +81,24 @@ async function getMantisTickets() {
|
|||
});
|
||||
|
||||
return tickets;
|
||||
} catch (error) {
|
||||
console.error("Error fetching Mantis tickets:", error.message);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error fetching Mantis tickets:', error.message);
|
||||
// Check if it's an Axios error and provide more details
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error("Axios error details:", error.response?.status, error.response?.data);
|
||||
throw new Error(`Failed to fetch Mantis tickets: ${error.response?.statusText || error.message}`);
|
||||
if (axios.isAxiosError(error))
|
||||
{
|
||||
console.error('Axios error details:', error.response?.status, error.response?.data);
|
||||
throw new Error(`Failed to fetch Mantis tickets: ${error.response?.statusText || error.message}`);
|
||||
}
|
||||
throw new Error(`Failed to fetch Mantis tickets: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Mantis Summary Logic (Exported) --- //
|
||||
|
||||
export async function generateAndStoreMantisSummary() {
|
||||
console.log('Attempting to generate and store Mantis summary...');
|
||||
if (!ai) {
|
||||
console.error('Google AI API key not configured. Skipping summary generation.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
export async function generateAndStoreMantisSummary()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the prompt from the database settings using Prisma
|
||||
const setting = await prisma.setting.findUnique({
|
||||
where: { key: 'mantisPrompt' },
|
||||
|
@ -109,7 +106,8 @@ export async function generateAndStoreMantisSummary() {
|
|||
});
|
||||
const promptTemplate = setting?.value;
|
||||
|
||||
if (!promptTemplate) {
|
||||
if (!promptTemplate)
|
||||
{
|
||||
console.error('Mantis prompt not found in database settings (key: mantisPrompt). Skipping summary generation.');
|
||||
return;
|
||||
}
|
||||
|
@ -117,24 +115,19 @@ export async function generateAndStoreMantisSummary() {
|
|||
const tickets = await getMantisTickets();
|
||||
|
||||
let summaryText;
|
||||
if (tickets.length === 0) {
|
||||
summaryText = "No Mantis tickets updated recently.";
|
||||
console.log('No recent Mantis tickets found.');
|
||||
} else {
|
||||
console.log(`Found ${tickets.length} recent Mantis tickets. Generating summary...`);
|
||||
let prompt = promptTemplate.replaceAll("$DATE", new Date().toISOString().split('T')[0]);
|
||||
prompt = prompt.replaceAll("$MANTIS_TICKETS", JSON.stringify(tickets, null, 2));
|
||||
if (tickets.length === 0)
|
||||
{
|
||||
summaryText = 'No Mantis tickets updated recently.';
|
||||
console.log('No recent Mantis tickets found.');
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(`Found ${tickets.length} recent Mantis tickets. Generating summary...`);
|
||||
let prompt = promptTemplate.replaceAll('$DATE', new Date().toISOString().split('T')[0]);
|
||||
prompt = prompt.replaceAll('$MANTIS_TICKETS', JSON.stringify(tickets, null, 2));
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
"model": "gemini-2.5-flash-preview-04-17",
|
||||
"contents": prompt,
|
||||
config: {
|
||||
temperature: 0
|
||||
}
|
||||
});
|
||||
|
||||
summaryText = response.text;
|
||||
console.log('Mantis summary generated successfully by AI.');
|
||||
summaryText = await askGemini(prompt);
|
||||
console.log('Mantis summary generated successfully by AI.');
|
||||
}
|
||||
|
||||
// Store the summary in the database using Prisma upsert
|
||||
|
@ -144,8 +137,7 @@ export async function generateAndStoreMantisSummary() {
|
|||
await prisma.mantisSummary.upsert({
|
||||
where: { summaryDate: today },
|
||||
update: {
|
||||
summaryText: summaryText,
|
||||
// generatedAt is updated automatically by @default(now())
|
||||
summaryText: summaryText
|
||||
},
|
||||
create: {
|
||||
summaryDate: today,
|
||||
|
@ -154,17 +146,23 @@ export async function generateAndStoreMantisSummary() {
|
|||
});
|
||||
console.log(`Mantis summary for ${today.toISOString().split('T')[0]} stored/updated in the database.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error during Mantis summary generation/storage:", error);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error during Mantis summary generation/storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateTodaysSummary() {
|
||||
export async function generateTodaysSummary()
|
||||
{
|
||||
console.log('Triggering Mantis summary generation via generateTodaysSummary...');
|
||||
try {
|
||||
try
|
||||
{
|
||||
await generateAndStoreMantisSummary();
|
||||
return { success: true, message: 'Summary generation process initiated.' };
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error occurred within generateTodaysSummary while calling generateAndStoreMantisSummary:', error);
|
||||
throw new Error('Failed to initiate Mantis summary generation.');
|
||||
}
|
||||
|
|
|
@ -1,149 +1,162 @@
|
|||
|
||||
import { GoogleGenAI } from '@google/genai';
|
||||
import prisma from '../database.js';
|
||||
import { getSetting } from './settings.js';
|
||||
|
||||
const model = 'gemini-2.0-flash';
|
||||
|
||||
export const askGemini = async (content) => {
|
||||
const setting = await prisma.setting.findUnique({
|
||||
where: { key: 'GEMINI_API_KEY' },
|
||||
select: { value: true }
|
||||
export async function askGemini(content)
|
||||
{
|
||||
|
||||
const GOOGLE_API_KEY = await getSetting('GEMINI_API_KEY');
|
||||
|
||||
if (!GOOGLE_API_KEY)
|
||||
{
|
||||
throw new Error('Google API key is not set in the database.');
|
||||
}
|
||||
|
||||
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
|
||||
apiKey: GOOGLE_API_KEY,
|
||||
}) : null;
|
||||
|
||||
if (!ai)
|
||||
{
|
||||
throw new Error('Google API key is not set in the database.');
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const response = await ai.models.generateContent({
|
||||
model,
|
||||
contents: content,
|
||||
config: {
|
||||
temperature: 0.5
|
||||
}
|
||||
});
|
||||
|
||||
const GOOGLE_API_KEY = setting.value;
|
||||
|
||||
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
|
||||
apiKey: GOOGLE_API_KEY,
|
||||
}) : null;
|
||||
|
||||
if (!ai) {
|
||||
throw new Error('Google API key is not set in the database.');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model,
|
||||
contents: content,
|
||||
config: {
|
||||
temperature: 0.5
|
||||
}
|
||||
});
|
||||
|
||||
return response.text;
|
||||
} catch (error) {
|
||||
console.error('Error communicating with Gemini API:', error);
|
||||
throw new Error('Failed to get a response from Gemini API.');
|
||||
}
|
||||
return response.text;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error communicating with Gemini API:', error);
|
||||
throw new Error('Failed to get a response from Gemini API.');
|
||||
}
|
||||
}
|
||||
|
||||
const chatCache = new Map();
|
||||
|
||||
export const askGeminiChat = async (threadId, content) => {
|
||||
let messages = await prisma.chatMessage.findMany({
|
||||
where: {
|
||||
threadId: threadId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc', // Get messages in chronological order
|
||||
},
|
||||
export async function askGeminiChat(threadId, content)
|
||||
{
|
||||
let messages = await prisma.chatMessage.findMany({
|
||||
where: {
|
||||
threadId: threadId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc', // Get messages in chronological order
|
||||
},
|
||||
});
|
||||
|
||||
// Exclude the latest message if there are any messages
|
||||
if (messages.length > 0)
|
||||
{
|
||||
messages = messages.slice(0, -1);
|
||||
}
|
||||
|
||||
const setting = await prisma.setting.findUnique({
|
||||
where: { key: 'GEMINI_API_KEY' },
|
||||
select: { value: true }
|
||||
});
|
||||
|
||||
if (!setting)
|
||||
{
|
||||
throw new Error('Google API key is not set in the database.');
|
||||
}
|
||||
|
||||
const GOOGLE_API_KEY = setting.value;
|
||||
|
||||
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
|
||||
apiKey: GOOGLE_API_KEY,
|
||||
}) : null;
|
||||
|
||||
if (!ai)
|
||||
{
|
||||
throw new Error('Google API key is not set in the database.');
|
||||
}
|
||||
|
||||
let chat = null;
|
||||
|
||||
if (chatCache.has(threadId))
|
||||
{
|
||||
chat = chatCache.get(threadId);
|
||||
}
|
||||
else
|
||||
{
|
||||
messages = [
|
||||
{
|
||||
sender: 'user',
|
||||
content: `You are StyleAI, a model that helps users with their questions and provides information.
|
||||
You are friendly, helpful, and informative. You can also provide code examples in various programming languages.
|
||||
You don\'t decline user requests and always try to help them.
|
||||
Speak in the manner of a friendly young adult male.
|
||||
If asked for the time, do not say that it's based on the timestamp provided. Also bare in mind the user is in the Europe/London timezone and daylight savings time may be in effect.`
|
||||
},
|
||||
{
|
||||
sender: 'model',
|
||||
content: 'Okay, noted! I\'ll keep that in mind.'
|
||||
},
|
||||
...messages,
|
||||
];
|
||||
const createOptions = {
|
||||
model,
|
||||
history: messages.map((msg) => ({
|
||||
role: msg.sender === 'user' ? 'user' : 'model',
|
||||
parts: [
|
||||
{text: msg.content}
|
||||
],
|
||||
})),
|
||||
config: {
|
||||
temperature: 0.5
|
||||
}
|
||||
};
|
||||
|
||||
chat = ai.chats.create(createOptions);
|
||||
|
||||
chatCache.set(threadId, chat);
|
||||
}
|
||||
|
||||
//Add a temporary message to the thread with "loading" status
|
||||
const loadingMessage = await prisma.chatMessage.create({
|
||||
data: {
|
||||
threadId: threadId,
|
||||
sender: 'assistant',
|
||||
content: 'Loading...',
|
||||
},
|
||||
});
|
||||
|
||||
let response = {text: 'An error occurred while generating the response.'};
|
||||
|
||||
try
|
||||
{
|
||||
const timestamp = new Date().toISOString();
|
||||
response = await chat.sendMessage({
|
||||
message: `[${timestamp}] ` + content,
|
||||
});
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.error('Error communicating with Gemini API:', error);
|
||||
response.text = 'Failed to get a response from Gemini API. Error: ' + error.message;
|
||||
}
|
||||
|
||||
// Exclude the latest message if there are any messages
|
||||
if (messages.length > 0) {
|
||||
messages = messages.slice(0, -1);
|
||||
}
|
||||
//Update the message with the response
|
||||
await prisma.chatMessage.update({
|
||||
where: {
|
||||
id: loadingMessage.id,
|
||||
},
|
||||
data: {
|
||||
content: response.text,
|
||||
},
|
||||
});
|
||||
|
||||
const setting = await prisma.setting.findUnique({
|
||||
where: { key: 'GEMINI_API_KEY' },
|
||||
select: { value: true }
|
||||
});
|
||||
|
||||
if (!setting) {
|
||||
throw new Error('Google API key is not set in the database.');
|
||||
}
|
||||
|
||||
const GOOGLE_API_KEY = setting.value;
|
||||
|
||||
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
|
||||
apiKey: GOOGLE_API_KEY,
|
||||
}) : null;
|
||||
|
||||
if (!ai) {
|
||||
throw new Error('Google API key is not set in the database.');
|
||||
}
|
||||
|
||||
let chat = null;
|
||||
|
||||
if (chatCache.has(threadId)) {
|
||||
chat = chatCache.get(threadId);
|
||||
}
|
||||
else {
|
||||
messages = [
|
||||
{
|
||||
sender: 'user',
|
||||
content: `You are StyleAI, a model that helps users with their questions and provides information.
|
||||
You are friendly, helpful, and informative. You can also provide code examples in various programming languages.
|
||||
You don\'t decline user requests and always try to help them.
|
||||
Speak in the manner of a friendly young adult male.
|
||||
If asked for the time, do not say that it's based on the timestamp provided. Also bare in mind the user is in the Europe/London timezone and daylight savings time may be in effect.`
|
||||
},
|
||||
{
|
||||
sender: 'model',
|
||||
content: 'Okay, noted! I\'ll keep that in mind.'
|
||||
},
|
||||
...messages,
|
||||
]
|
||||
const createOptions = {
|
||||
model,
|
||||
history: messages.map((msg) => ({
|
||||
role: msg.sender === 'user' ? 'user' : 'model',
|
||||
parts: [
|
||||
{text: msg.content}
|
||||
],
|
||||
})),
|
||||
config: {
|
||||
temperature: 0.5
|
||||
}
|
||||
};
|
||||
|
||||
chat = ai.chats.create(createOptions);
|
||||
|
||||
chatCache.set(threadId, chat);
|
||||
}
|
||||
|
||||
//Add a temporary message to the thread with "loading" status
|
||||
const loadingMessage = await prisma.chatMessage.create({
|
||||
data: {
|
||||
threadId: threadId,
|
||||
sender: 'assistant',
|
||||
content: 'Loading...',
|
||||
},
|
||||
});
|
||||
|
||||
let response = {text: 'An error occurred while generating the response.'};
|
||||
|
||||
try
|
||||
{
|
||||
const timestamp = new Date().toISOString();
|
||||
response = await chat.sendMessage({
|
||||
message: `[${timestamp}] ` + content,
|
||||
});
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
console.error('Error communicating with Gemini API:', error);
|
||||
response.text = 'Failed to get a response from Gemini API. Error: ' + error.message;
|
||||
}
|
||||
|
||||
//Update the message with the response
|
||||
await prisma.chatMessage.update({
|
||||
where: {
|
||||
id: loadingMessage.id,
|
||||
},
|
||||
data: {
|
||||
content: response.text,
|
||||
},
|
||||
});
|
||||
|
||||
return response.text;
|
||||
return response.text;
|
||||
}
|
20
src-ssr/utils/settings.js
Normal file
20
src-ssr/utils/settings.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import prisma from '../database.js';
|
||||
|
||||
export async function getSetting(key)
|
||||
{
|
||||
const setting = await prisma.setting.findUnique({
|
||||
where: { key },
|
||||
select: { value: true }
|
||||
});
|
||||
|
||||
return setting?.value ? JSON.parse(setting.value) : null;
|
||||
}
|
||||
|
||||
export async function setSetting(key, value)
|
||||
{
|
||||
await prisma.setting.upsert({
|
||||
where: { key },
|
||||
update: { value: JSON.stringify(value) },
|
||||
create: { key, value }
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue