Add debug logging for markContactEmailsAsRead and remove password change restriction

This commit is contained in:
richardtekula
2025-11-20 08:00:14 +01:00
parent 51714c8edd
commit 178b18baa5
20 changed files with 152 additions and 394 deletions

View File

@@ -15,13 +15,9 @@ const pool = new Pool({
connectionTimeoutMillis: 2000,
});
// Test database connection
pool.on('connect', () => {
console.log('✅ Database connected successfully');
});
// Note: Connection logging handled in index.js to avoid circular dependencies
pool.on('error', (err) => {
console.error('Unexpected database error:', err);
console.error('Unexpected database error:', err);
process.exit(-1);
});

View File

@@ -128,7 +128,7 @@ export const getAllUsers = async (req, res) => {
* Získanie konkrétneho usera (admin only)
* GET /api/admin/users/:userId
*/
export const getUserById = async (req, res) => {
export const getUser = async (req, res) => {
const { userId } = req.params;
try {

View File

@@ -2,6 +2,7 @@ import * as contactService from '../services/contact.service.js';
import { discoverContactsFromJMAP, getJmapConfigFromAccount } from '../services/jmap.service.js';
import { formatErrorResponse } from '../utils/errors.js';
import * as emailAccountService from '../services/email-account.service.js';
import { logger } from '../utils/logger.js';
/**
* Get all contacts for authenticated user
@@ -34,18 +35,18 @@ export const discoverContacts = async (req, res) => {
const userId = req.userId;
const { accountId, search = '', limit = 50 } = req.query;
console.log('🔍 discoverContacts called:', { userId, accountId, search, limit });
logger.debug('discoverContacts called', { userId, accountId, search, limit });
// Get email account (or primary if not specified)
let emailAccount;
if (accountId) {
console.log('📧 Getting email account by ID:', accountId);
logger.debug('Getting email account by ID', { accountId });
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
console.log('Email account retrieved:', { id: emailAccount.id, email: emailAccount.email });
logger.debug('Email account retrieved', { id: emailAccount.id, email: emailAccount.email });
} else {
console.log('📧 No accountId provided, getting primary account for user:', userId);
logger.debug('No accountId provided, getting primary account', { userId });
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
console.log('🔑 Primary account:', primaryAccount ? { id: primaryAccount.id, email: primaryAccount.email } : 'NOT FOUND');
logger.debug('Primary account', primaryAccount ? { id: primaryAccount.id, email: primaryAccount.email } : { found: false });
if (!primaryAccount) {
return res.status(400).json({
success: false,
@@ -56,11 +57,11 @@ export const discoverContacts = async (req, res) => {
});
}
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
console.log('Email account retrieved from primary:', { id: emailAccount.id, email: emailAccount.email });
logger.debug('Email account retrieved from primary', { id: emailAccount.id, email: emailAccount.email });
}
const jmapConfig = getJmapConfigFromAccount(emailAccount);
console.log('🔧 JMAP Config created:', {
logger.debug('JMAP Config created', {
server: jmapConfig.server,
username: jmapConfig.username,
accountId: jmapConfig.accountId,
@@ -80,8 +81,7 @@ export const discoverContacts = async (req, res) => {
data: potentialContacts,
});
} catch (error) {
console.error('ERROR in discoverContacts:', error);
console.error('Error stack:', error.stack);
logger.error('ERROR in discoverContacts', { error: error.message, stack: error.stack });
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
@@ -95,10 +95,10 @@ export const discoverContacts = async (req, res) => {
export const addContact = async (req, res) => {
try {
const userId = req.userId;
console.log('📦 Full req.body:', JSON.stringify(req.body, null, 2));
logger.debug('Full req.body', { body: req.body });
const { email, name = '', notes = '', accountId } = req.body;
console.log(' addContact called:', { userId, email, name, accountId });
logger.debug('addContact called', { userId, email, name, accountId });
if (!email) {
return res.status(400).json({
@@ -113,10 +113,10 @@ export const addContact = async (req, res) => {
// Get email account (or primary if not specified)
let emailAccount;
if (accountId) {
console.log('📧 Using provided accountId:', accountId);
logger.debug('Using provided accountId', { accountId });
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
} else {
console.log('📧 No accountId provided, using primary account');
logger.debug('No accountId provided, using primary account');
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
if (!primaryAccount) {
return res.status(400).json({
@@ -128,7 +128,7 @@ export const addContact = async (req, res) => {
});
}
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
console.log('📧 Using primary account:', primaryAccount.id);
logger.debug('Using primary account', { accountId: primaryAccount.id });
}
const jmapConfig = getJmapConfigFromAccount(emailAccount);

View File

@@ -4,6 +4,7 @@ import * as emailAccountService from '../services/email-account.service.js';
import { markEmailAsRead, sendEmail, getJmapConfig, getJmapConfigFromAccount, syncEmailsFromSender, searchEmailsJMAP as searchEmailsJMAPService } from '../services/jmap.service.js';
import { formatErrorResponse } from '../utils/errors.js';
import { getUserById } from '../services/auth.service.js';
import { logger } from '../utils/logger.js';
/**
* Get all emails for authenticated user
@@ -91,7 +92,7 @@ export const getUnreadCount = async (req, res) => {
},
});
} catch (error) {
console.error('ERROR in getUnreadCount:', error);
logger.error('ERROR in getUnreadCount', { error: error.message });
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
@@ -153,7 +154,7 @@ export const syncEmails = async (req, res) => {
totalSynced += total;
totalNew += saved;
} catch (syncError) {
console.error(`Failed to sync emails for contact ${contact.email}`, syncError);
logger.error('Failed to sync emails for contact', { contactEmail: contact.email, error: syncError.message });
}
}
@@ -198,6 +199,29 @@ export const markAsRead = async (req, res) => {
}
};
/**
* Mark all emails from contact as read
* POST /api/emails/contact/:contactId/read
*/
export const markContactEmailsRead = async (req, res) => {
try {
const userId = req.userId;
const { contactId } = req.params;
const result = await crmEmailService.markContactEmailsAsRead(userId, contactId);
res.status(200).json({
success: true,
message: `Označených ${result.count} emailov ako prečítaných`,
data: result,
});
} catch (error) {
logger.error('ERROR in markContactEmailsRead', { error: error.message });
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Mark entire thread as read
* POST /api/emails/thread/:threadId/read
@@ -224,7 +248,7 @@ export const markThreadRead = async (req, res) => {
try {
await markEmailAsRead(jmapConfig, userId, email.jmapId, true);
} catch (jmapError) {
console.error(`Failed to mark JMAP email ${email.jmapId} as read`, jmapError);
logger.error('Failed to mark JMAP email as read', { jmapId: email.jmapId, error: jmapError.message });
}
}
}
@@ -327,15 +351,15 @@ export const searchEmailsJMAP = async (req, res) => {
const userId = req.userId;
const { query = '', limit = 50, offset = 0, accountId } = req.query;
console.log('🔍 searchEmailsJMAP called:', { userId, query, limit, offset, accountId });
logger.debug('searchEmailsJMAP called', { userId, query, limit, offset, accountId });
// Get email account (or primary if not specified)
let emailAccount;
if (accountId) {
console.log('📧 Using provided accountId:', accountId);
logger.debug('Using provided accountId', { accountId });
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
} else {
console.log('📧 No accountId provided, using primary account');
logger.debug('No accountId provided, using primary account');
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
if (!primaryAccount) {
return res.status(400).json({
@@ -347,7 +371,7 @@ export const searchEmailsJMAP = async (req, res) => {
});
}
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
console.log('📧 Using primary account:', primaryAccount.id);
logger.debug('Using primary account', { accountId: primaryAccount.id });
}
const jmapConfig = getJmapConfigFromAccount(emailAccount);
@@ -366,7 +390,7 @@ export const searchEmailsJMAP = async (req, res) => {
data: results,
});
} catch (error) {
console.error('ERROR in searchEmailsJMAP:', error);
logger.error('ERROR in searchEmailsJMAP', { error: error.message });
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}

View File

@@ -57,34 +57,3 @@ export const authenticate = async (req, res, next) => {
});
}
};
/**
* Optional authentication - nepovinnné overenie
* Ak je token poskytnutý, overí ho, ale nehodí error ak nie je
*/
export const optionalAuthenticate = async (req, res, next) => {
try {
let token = null;
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7);
}
if (!token && req.cookies && req.cookies.accessToken) {
token = req.cookies.accessToken;
}
if (token) {
const decoded = verifyAccessToken(token);
const user = await getUserById(decoded.id);
req.user = user;
req.userId = user.id;
}
next();
} catch (error) {
// Ignoruj chyby, len pokračuj bez user objektu
next();
}
};

View File

@@ -1,3 +1,5 @@
import { logger } from '../../utils/logger.js';
export function validateBody(req, res, next) {
const data = JSON.stringify({ body: req.body, query: req.query, params: req.params });
const dangerousPatterns = [
@@ -10,8 +12,8 @@ export function validateBody(req, res, next) {
];
for (const pattern of dangerousPatterns) {
if (pattern.test(data)) {
console.warn(`Suspicious input detected: ${data}`);
return res.status(400).json({ message: '🚨 Malicious content detected in request data' });
logger.warn('Suspicious input detected', { data: data.substring(0, 100) });
return res.status(400).json({ message: 'Malicious content detected in request data' });
}
}
next();

View File

@@ -1,5 +1,6 @@
import { ZodError } from 'zod';
import { ValidationError } from '../../utils/errors.js';
import { logger } from '../../utils/logger.js';
/**
* Middleware na validáciu request body pomocou Zod schema
@@ -34,7 +35,7 @@ export const validateBody = (schema) => {
}
// Log unexpected errors
console.error('Validation error:', error);
logger.error('Validation error', { error: error.message });
return res.status(400).json({
success: false,
@@ -74,7 +75,7 @@ export const validateQuery = (schema) => {
});
}
console.error('Query validation error:', error);
logger.error('Query validation error', { error: error.message });
return res.status(400).json({
success: false,
@@ -114,7 +115,7 @@ export const validateParams = (schema) => {
});
}
console.error('Params validation error:', error);
logger.error('Params validation error', { error: error.message });
return res.status(400).json({
success: false,

View File

@@ -28,7 +28,7 @@ router.get('/users', adminController.getAllUsers);
router.get(
'/users/:userId',
validateParams(z.object({ userId: z.string().uuid() })),
adminController.getUserById
adminController.getUser
);
// Zmena role usera

View File

@@ -49,6 +49,13 @@ router.get(
crmEmailController.getContactEmails
);
// Mark all emails from contact as read
router.post(
'/contact/:contactId/read',
validateParams(z.object({ contactId: z.string().uuid() })),
crmEmailController.markContactEmailsRead
);
// Mark email as read/unread
router.patch(
'/:jmapId/read',

View File

@@ -85,9 +85,10 @@ export const setNewPassword = async (userId, newPassword) => {
throw new NotFoundError('Používateľ nenájdený');
}
if (user.changedPassword) {
throw new ValidationError('Heslo už bolo zmenené');
}
// Allow users to change password anytime (removed restriction)
// if (user.changedPassword) {
// throw new ValidationError('Heslo už bolo zmenené');
// }
// Hash nového hesla
const hashedPassword = await hashPassword(newPassword);

View File

@@ -3,6 +3,7 @@ import { contacts, emails } from '../db/schema.js';
import { eq, and, desc } from 'drizzle-orm';
import { NotFoundError, ConflictError } from '../utils/errors.js';
import { syncEmailsFromSender } from './jmap.service.js';
import { logger } from '../utils/logger.js';
/**
* Get all contacts for a user
@@ -61,7 +62,7 @@ export const addContact = async (userId, emailAccountId, jmapConfig, email, name
try {
await syncEmailsFromSender(jmapConfig, userId, emailAccountId, newContact.id, email);
} catch (error) {
console.error('Failed to sync emails for new contact:', error);
logger.error('Failed to sync emails for new contact', { error: error.message });
// Don't throw - contact was created successfully
}

View File

@@ -129,12 +129,52 @@ export const getUnreadCount = async (userId) => {
* Mark thread as read
*/
export const markThreadAsRead = async (userId, threadId) => {
console.log('🟦 markThreadAsRead called:', { userId, threadId });
const result = await db
.update(emails)
.set({ isRead: true, updatedAt: new Date() })
.where(and(eq(emails.userId, userId), eq(emails.threadId, threadId), eq(emails.isRead, false)))
.returning();
console.log('✅ markThreadAsRead result:', { count: result.length, threadId });
return { success: true, count: result.length };
};
/**
* Mark all emails from a contact as read
*/
export const markContactEmailsAsRead = async (userId, contactId) => {
console.log('🟦 markContactEmailsAsRead called:', { userId, contactId });
// First, check what emails exist for this contact (including already read ones)
const allContactEmails = await db
.select({
id: emails.id,
contactId: emails.contactId,
isRead: emails.isRead,
from: emails.from,
subject: emails.subject,
})
.from(emails)
.where(and(eq(emails.userId, userId), eq(emails.contactId, contactId)));
console.log('📧 All emails for this contact:', {
total: allContactEmails.length,
unread: allContactEmails.filter(e => !e.isRead).length,
read: allContactEmails.filter(e => e.isRead).length,
sampleEmails: allContactEmails.slice(0, 3),
});
const result = await db
.update(emails)
.set({ isRead: true, updatedAt: new Date() })
.where(and(eq(emails.userId, userId), eq(emails.contactId, contactId), eq(emails.isRead, false)))
.returning();
console.log('✅ markContactEmailsAsRead result:', { count: result.length, contactId });
return { success: true, count: result.length };
};

View File

@@ -9,6 +9,7 @@ import {
ConflictError,
AuthenticationError,
} from '../utils/errors.js';
import { logger } from '../utils/logger.js';
/**
* Get all email accounts for a user
@@ -200,22 +201,27 @@ export const toggleEmailAccountStatus = async (accountId, userId, isActive) => {
export const setPrimaryEmailAccount = async (accountId, userId) => {
const account = await getEmailAccountById(accountId, userId);
// Remove primary flag from all accounts
await db
.update(emailAccounts)
.set({ isPrimary: false, updatedAt: new Date() })
.where(eq(emailAccounts.userId, userId));
// Use transaction to prevent race conditions
const updated = await db.transaction(async (tx) => {
// Remove primary flag from all accounts
await tx
.update(emailAccounts)
.set({ isPrimary: false, updatedAt: new Date() })
.where(eq(emailAccounts.userId, userId));
// Set new primary account
const [updated] = await db
.update(emailAccounts)
.set({
isPrimary: true,
isActive: true, // Primary account must be active
updatedAt: new Date(),
})
.where(eq(emailAccounts.id, accountId))
.returning();
// Set new primary account
const [updatedAccount] = await tx
.update(emailAccounts)
.set({
isPrimary: true,
isActive: true, // Primary account must be active
updatedAt: new Date(),
})
.where(eq(emailAccounts.id, accountId))
.returning();
return updatedAccount;
});
return {
id: updated.id,
@@ -260,9 +266,9 @@ export const deleteEmailAccount = async (accountId, userId) => {
* Get email account with decrypted password (for JMAP operations)
*/
export const getEmailAccountWithCredentials = async (accountId, userId) => {
console.log('🔐 getEmailAccountWithCredentials called:', { accountId, userId });
logger.debug('getEmailAccountWithCredentials called', { accountId, userId });
const account = await getEmailAccountById(accountId, userId);
console.log('📦 Account retrieved:', {
logger.debug('Account retrieved', {
id: account.id,
email: account.email,
hasPassword: !!account.emailPassword,
@@ -270,7 +276,7 @@ export const getEmailAccountWithCredentials = async (accountId, userId) => {
});
const decryptedPassword = decryptPassword(account.emailPassword);
console.log('🔓 Password decrypted, length:', decryptedPassword?.length);
logger.debug('Password decrypted', { passwordLength: decryptedPassword?.length });
return {
id: account.id,

View File

@@ -1,31 +1,6 @@
import axios from 'axios';
import { logger } from '../utils/logger.js';
const JMAP_CONFIG = {
server: process.env.JMAP_SERVER || 'https://mail.truemail.sk/jmap/',
username: process.env.JMAP_USERNAME || 'info1_test@truemail.sk',
password: process.env.JMAP_PASSWORD || 'info1',
accountId: process.env.JMAP_ACCOUNT_ID || 'ba',
};
/**
* Získa JMAP session
*/
const getJmapSession = async () => {
try {
const response = await axios.get(`${JMAP_CONFIG.server}session`, {
auth: {
username: JMAP_CONFIG.username,
password: JMAP_CONFIG.password,
},
});
return response.data;
} catch (error) {
logger.error('Failed to get JMAP session', error);
throw new Error('Email service nedostupný');
}
};
/**
* Validuje JMAP credentials a vráti account ID
* @param {string} email - Email address
@@ -33,8 +8,14 @@ const getJmapSession = async () => {
* @returns {Promise<{accountId: string, session: object}>}
*/
export const validateJmapCredentials = async (email, password) => {
const jmapServer = process.env.JMAP_SERVER;
if (!jmapServer) {
throw new Error('JMAP_SERVER environment variable is not configured');
}
try {
const response = await axios.get(`${JMAP_CONFIG.server}session`, {
const response = await axios.get(`${jmapServer}session`, {
auth: {
username: email,
password: password,
@@ -68,188 +49,3 @@ export const validateJmapCredentials = async (email, password) => {
throw new Error('Nepodarilo sa overiť emailový účet');
}
};
/**
* Pošle email pomocou JMAP
*/
const sendJmapEmail = async ({ to, subject, htmlBody, textBody }) => {
try {
const session = await getJmapSession();
const apiUrl = session.apiUrl;
const emailObject = {
from: [{ email: JMAP_CONFIG.username }],
to: [{ email: to }],
subject,
htmlBody: [
{
partId: '1',
type: 'text/html',
value: htmlBody,
},
],
textBody: [
{
partId: '2',
type: 'text/plain',
value: textBody || subject,
},
],
};
const response = await axios.post(
apiUrl,
{
using: ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail'],
methodCalls: [
[
'Email/set',
{
accountId: JMAP_CONFIG.accountId,
create: {
draft: emailObject,
},
},
'0',
],
[
'EmailSubmission/set',
{
accountId: JMAP_CONFIG.accountId,
create: {
submission: {
emailId: '#draft',
envelope: {
mailFrom: { email: JMAP_CONFIG.username },
rcptTo: [{ email: to }],
},
},
},
},
'1',
],
],
},
{
auth: {
username: JMAP_CONFIG.username,
password: JMAP_CONFIG.password,
},
headers: {
'Content-Type': 'application/json',
},
}
);
logger.success(`Email sent to ${to}`);
return response.data;
} catch (error) {
logger.error(`Failed to send email to ${to}`, error);
throw new Error('Nepodarilo sa odoslať email');
}
};
/**
* Email templates
*/
export const sendVerificationEmail = async (to, username, verificationToken) => {
const verificationUrl = `${process.env.BETTER_AUTH_URL}/api/auth/verify-email?token=${verificationToken}`;
const htmlBody = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.button {
display: inline-block;
padding: 12px 24px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 20px 0;
}
.footer { margin-top: 30px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<h2>Vitajte v CRM systéme, ${username}!</h2>
<p>Prosím, verifikujte svoju emailovú adresu kliknutím na tlačidlo nižšie:</p>
<a href="${verificationUrl}" class="button">Verifikovať email</a>
<p>Alebo skopírujte tento link do prehliadača:</p>
<p>${verificationUrl}</p>
<p class="footer">
Tento link vyprší za 24 hodín.<br>
Ak ste tento email neočakávali, môžete ho ignorovať.
</p>
</div>
</body>
</html>
`;
const textBody = `
Vitajte v CRM systéme, ${username}!
Prosím, verifikujte svoju emailovú adresu kliknutím na tento link:
${verificationUrl}
Tento link vyprší za 24 hodín.
Ak ste tento email neočakávali, môžete ho ignorovať.
`;
return sendJmapEmail({
to,
subject: 'Verifikácia emailu - CRM systém',
htmlBody,
textBody,
});
};
export const sendWelcomeEmail = async (to, username) => {
const htmlBody = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
</style>
</head>
<body>
<div class="container">
<h2>Vitajte v CRM systéme, ${username}!</h2>
<p>Váš účet bol úspešne vytvorený a nastavený.</p>
<p>Môžete sa prihlásiť a začať používať systém.</p>
</div>
</body>
</html>
`;
const textBody = `
Vitajte v CRM systéme, ${username}!
Váš účet bol úspešne vytvorený a nastavený.
Môžete sa prihlásiť a začať používať systém.
`;
return sendJmapEmail({
to,
subject: 'Vitajte v CRM systéme',
htmlBody,
textBody,
});
};
/**
* Asynchrónne posielanie emailov (non-blocking)
*/
export const sendEmailAsync = (emailFunction, ...args) => {
// Spustí email sending v pozadí
emailFunction(...args).catch((error) => {
logger.error('Async email sending failed', error);
});
};

View File

@@ -67,8 +67,12 @@ export const generateVerificationToken = () => {
* @returns {string} Encrypted password in format: iv:authTag:encrypted
*/
export const encryptPassword = (text) => {
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET environment variable is required for password encryption');
}
const algorithm = 'aes-256-gcm';
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
const key = crypto.scryptSync(process.env.JWT_SECRET, 'salt', 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
@@ -86,8 +90,12 @@ export const encryptPassword = (text) => {
* @returns {string} Plain text password
*/
export const decryptPassword = (encryptedText) => {
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET environment variable is required for password decryption');
}
const algorithm = 'aes-256-gcm';
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
const key = crypto.scryptSync(process.env.JWT_SECRET, 'salt', 32);
const parts = encryptedText.split(':');
const iv = Buffer.from(parts[0], 'hex');