Add debug logging for markContactEmailsAsRead and remove password change restriction
This commit is contained in:
@@ -1,24 +0,0 @@
|
|||||||
import pkg from 'pg';
|
|
||||||
const { Pool } = pkg;
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const pool = new Pool({
|
|
||||||
host: process.env.DB_HOST || 'localhost',
|
|
||||||
port: parseInt(process.env.DB_PORT || '5432'),
|
|
||||||
user: process.env.DB_USER || 'admin',
|
|
||||||
password: process.env.DB_PASSWORD || 'heslo123',
|
|
||||||
database: process.env.DB_NAME || 'crm',
|
|
||||||
});
|
|
||||||
|
|
||||||
const query = "SELECT tablename FROM pg_tables WHERE schemaname = 'public';";
|
|
||||||
pool.query(query).then(res => {
|
|
||||||
console.log('Tabuľky v databáze:');
|
|
||||||
res.rows.forEach(row => console.log(' -', row.tablename));
|
|
||||||
pool.end();
|
|
||||||
process.exit(0);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('Chyba:', err.message);
|
|
||||||
pool.end();
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import crypto from 'crypto';
|
|
||||||
|
|
||||||
const encryptPassword = (text) => {
|
|
||||||
const algorithm = 'aes-256-gcm';
|
|
||||||
const key = crypto.scryptSync(process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-this-in-production', 'salt', 32);
|
|
||||||
const iv = crypto.randomBytes(16);
|
|
||||||
|
|
||||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
|
||||||
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
||||||
encrypted += cipher.final('hex');
|
|
||||||
|
|
||||||
const authTag = cipher.getAuthTag();
|
|
||||||
|
|
||||||
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Usage: node scripts/encrypt-password.js YOUR_PASSWORD
|
|
||||||
const password = process.argv[2];
|
|
||||||
if (!password) {
|
|
||||||
console.error('Usage: node scripts/encrypt-password.js YOUR_PASSWORD');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Encrypted password:');
|
|
||||||
console.log(encryptPassword(password));
|
|
||||||
@@ -15,13 +15,9 @@ const pool = new Pool({
|
|||||||
connectionTimeoutMillis: 2000,
|
connectionTimeoutMillis: 2000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test database connection
|
// Note: Connection logging handled in index.js to avoid circular dependencies
|
||||||
pool.on('connect', () => {
|
|
||||||
console.log('✅ Database connected successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
pool.on('error', (err) => {
|
pool.on('error', (err) => {
|
||||||
console.error('❌ Unexpected database error:', err);
|
console.error('Unexpected database error:', err);
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const getAllUsers = async (req, res) => {
|
|||||||
* Získanie konkrétneho usera (admin only)
|
* Získanie konkrétneho usera (admin only)
|
||||||
* GET /api/admin/users/:userId
|
* GET /api/admin/users/:userId
|
||||||
*/
|
*/
|
||||||
export const getUserById = async (req, res) => {
|
export const getUser = async (req, res) => {
|
||||||
const { userId } = req.params;
|
const { userId } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as contactService from '../services/contact.service.js';
|
|||||||
import { discoverContactsFromJMAP, getJmapConfigFromAccount } from '../services/jmap.service.js';
|
import { discoverContactsFromJMAP, getJmapConfigFromAccount } from '../services/jmap.service.js';
|
||||||
import { formatErrorResponse } from '../utils/errors.js';
|
import { formatErrorResponse } from '../utils/errors.js';
|
||||||
import * as emailAccountService from '../services/email-account.service.js';
|
import * as emailAccountService from '../services/email-account.service.js';
|
||||||
|
import { logger } from '../utils/logger.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all contacts for authenticated user
|
* Get all contacts for authenticated user
|
||||||
@@ -34,18 +35,18 @@ export const discoverContacts = async (req, res) => {
|
|||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const { accountId, search = '', limit = 50 } = req.query;
|
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)
|
// Get email account (or primary if not specified)
|
||||||
let emailAccount;
|
let emailAccount;
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
console.log('📧 Getting email account by ID:', accountId);
|
logger.debug('Getting email account by ID', { accountId });
|
||||||
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
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 {
|
} 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);
|
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) {
|
if (!primaryAccount) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -56,11 +57,11 @@ export const discoverContacts = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
|
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);
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
||||||
console.log('🔧 JMAP Config created:', {
|
logger.debug('JMAP Config created', {
|
||||||
server: jmapConfig.server,
|
server: jmapConfig.server,
|
||||||
username: jmapConfig.username,
|
username: jmapConfig.username,
|
||||||
accountId: jmapConfig.accountId,
|
accountId: jmapConfig.accountId,
|
||||||
@@ -80,8 +81,7 @@ export const discoverContacts = async (req, res) => {
|
|||||||
data: potentialContacts,
|
data: potentialContacts,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ ERROR in discoverContacts:', error);
|
logger.error('ERROR in discoverContacts', { error: error.message, stack: error.stack });
|
||||||
console.error('Error stack:', error.stack);
|
|
||||||
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
res.status(error.statusCode || 500).json(errorResponse);
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
}
|
}
|
||||||
@@ -95,10 +95,10 @@ export const discoverContacts = async (req, res) => {
|
|||||||
export const addContact = async (req, res) => {
|
export const addContact = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.userId;
|
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;
|
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) {
|
if (!email) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -113,10 +113,10 @@ export const addContact = async (req, res) => {
|
|||||||
// Get email account (or primary if not specified)
|
// Get email account (or primary if not specified)
|
||||||
let emailAccount;
|
let emailAccount;
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
console.log('📧 Using provided accountId:', accountId);
|
logger.debug('Using provided accountId', { accountId });
|
||||||
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
||||||
} else {
|
} else {
|
||||||
console.log('📧 No accountId provided, using primary account');
|
logger.debug('No accountId provided, using primary account');
|
||||||
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
|
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
|
||||||
if (!primaryAccount) {
|
if (!primaryAccount) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -128,7 +128,7 @@ export const addContact = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
|
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);
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
||||||
|
|||||||
@@ -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 { markEmailAsRead, sendEmail, getJmapConfig, getJmapConfigFromAccount, syncEmailsFromSender, searchEmailsJMAP as searchEmailsJMAPService } from '../services/jmap.service.js';
|
||||||
import { formatErrorResponse } from '../utils/errors.js';
|
import { formatErrorResponse } from '../utils/errors.js';
|
||||||
import { getUserById } from '../services/auth.service.js';
|
import { getUserById } from '../services/auth.service.js';
|
||||||
|
import { logger } from '../utils/logger.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all emails for authenticated user
|
* Get all emails for authenticated user
|
||||||
@@ -91,7 +92,7 @@ export const getUnreadCount = async (req, res) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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');
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
res.status(error.statusCode || 500).json(errorResponse);
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
}
|
}
|
||||||
@@ -153,7 +154,7 @@ export const syncEmails = async (req, res) => {
|
|||||||
totalSynced += total;
|
totalSynced += total;
|
||||||
totalNew += saved;
|
totalNew += saved;
|
||||||
} catch (syncError) {
|
} 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
|
* Mark entire thread as read
|
||||||
* POST /api/emails/thread/:threadId/read
|
* POST /api/emails/thread/:threadId/read
|
||||||
@@ -224,7 +248,7 @@ export const markThreadRead = async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await markEmailAsRead(jmapConfig, userId, email.jmapId, true);
|
await markEmailAsRead(jmapConfig, userId, email.jmapId, true);
|
||||||
} catch (jmapError) {
|
} 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 userId = req.userId;
|
||||||
const { query = '', limit = 50, offset = 0, accountId } = req.query;
|
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)
|
// Get email account (or primary if not specified)
|
||||||
let emailAccount;
|
let emailAccount;
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
console.log('📧 Using provided accountId:', accountId);
|
logger.debug('Using provided accountId', { accountId });
|
||||||
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
||||||
} else {
|
} else {
|
||||||
console.log('📧 No accountId provided, using primary account');
|
logger.debug('No accountId provided, using primary account');
|
||||||
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
|
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
|
||||||
if (!primaryAccount) {
|
if (!primaryAccount) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -347,7 +371,7 @@ export const searchEmailsJMAP = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
|
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);
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
||||||
@@ -366,7 +390,7 @@ export const searchEmailsJMAP = async (req, res) => {
|
|||||||
data: results,
|
data: results,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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');
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
res.status(error.statusCode || 500).json(errorResponse);
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { logger } from '../../utils/logger.js';
|
||||||
|
|
||||||
export function validateBody(req, res, next) {
|
export function validateBody(req, res, next) {
|
||||||
const data = JSON.stringify({ body: req.body, query: req.query, params: req.params });
|
const data = JSON.stringify({ body: req.body, query: req.query, params: req.params });
|
||||||
const dangerousPatterns = [
|
const dangerousPatterns = [
|
||||||
@@ -10,8 +12,8 @@ export function validateBody(req, res, next) {
|
|||||||
];
|
];
|
||||||
for (const pattern of dangerousPatterns) {
|
for (const pattern of dangerousPatterns) {
|
||||||
if (pattern.test(data)) {
|
if (pattern.test(data)) {
|
||||||
console.warn(`❌ Suspicious input detected: ${data}`);
|
logger.warn('Suspicious input detected', { data: data.substring(0, 100) });
|
||||||
return res.status(400).json({ message: '🚨 Malicious content detected in request data' });
|
return res.status(400).json({ message: 'Malicious content detected in request data' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
import { ValidationError } from '../../utils/errors.js';
|
import { ValidationError } from '../../utils/errors.js';
|
||||||
|
import { logger } from '../../utils/logger.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware na validáciu request body pomocou Zod schema
|
* Middleware na validáciu request body pomocou Zod schema
|
||||||
@@ -34,7 +35,7 @@ export const validateBody = (schema) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log unexpected errors
|
// Log unexpected errors
|
||||||
console.error('Validation error:', error);
|
logger.error('Validation error', { error: error.message });
|
||||||
|
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
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({
|
return res.status(400).json({
|
||||||
success: false,
|
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({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ router.get('/users', adminController.getAllUsers);
|
|||||||
router.get(
|
router.get(
|
||||||
'/users/:userId',
|
'/users/:userId',
|
||||||
validateParams(z.object({ userId: z.string().uuid() })),
|
validateParams(z.object({ userId: z.string().uuid() })),
|
||||||
adminController.getUserById
|
adminController.getUser
|
||||||
);
|
);
|
||||||
|
|
||||||
// Zmena role usera
|
// Zmena role usera
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ router.get(
|
|||||||
crmEmailController.getContactEmails
|
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
|
// Mark email as read/unread
|
||||||
router.patch(
|
router.patch(
|
||||||
'/:jmapId/read',
|
'/:jmapId/read',
|
||||||
|
|||||||
@@ -85,9 +85,10 @@ export const setNewPassword = async (userId, newPassword) => {
|
|||||||
throw new NotFoundError('Používateľ nenájdený');
|
throw new NotFoundError('Používateľ nenájdený');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.changedPassword) {
|
// Allow users to change password anytime (removed restriction)
|
||||||
throw new ValidationError('Heslo už bolo zmenené');
|
// if (user.changedPassword) {
|
||||||
}
|
// throw new ValidationError('Heslo už bolo zmenené');
|
||||||
|
// }
|
||||||
|
|
||||||
// Hash nového hesla
|
// Hash nového hesla
|
||||||
const hashedPassword = await hashPassword(newPassword);
|
const hashedPassword = await hashPassword(newPassword);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { contacts, emails } from '../db/schema.js';
|
|||||||
import { eq, and, desc } from 'drizzle-orm';
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
import { NotFoundError, ConflictError } from '../utils/errors.js';
|
import { NotFoundError, ConflictError } from '../utils/errors.js';
|
||||||
import { syncEmailsFromSender } from './jmap.service.js';
|
import { syncEmailsFromSender } from './jmap.service.js';
|
||||||
|
import { logger } from '../utils/logger.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all contacts for a user
|
* Get all contacts for a user
|
||||||
@@ -61,7 +62,7 @@ export const addContact = async (userId, emailAccountId, jmapConfig, email, name
|
|||||||
try {
|
try {
|
||||||
await syncEmailsFromSender(jmapConfig, userId, emailAccountId, newContact.id, email);
|
await syncEmailsFromSender(jmapConfig, userId, emailAccountId, newContact.id, email);
|
||||||
} catch (error) {
|
} 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
|
// Don't throw - contact was created successfully
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,12 +129,52 @@ export const getUnreadCount = async (userId) => {
|
|||||||
* Mark thread as read
|
* Mark thread as read
|
||||||
*/
|
*/
|
||||||
export const markThreadAsRead = async (userId, threadId) => {
|
export const markThreadAsRead = async (userId, threadId) => {
|
||||||
|
console.log('🟦 markThreadAsRead called:', { userId, threadId });
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.update(emails)
|
.update(emails)
|
||||||
.set({ isRead: true, updatedAt: new Date() })
|
.set({ isRead: true, updatedAt: new Date() })
|
||||||
.where(and(eq(emails.userId, userId), eq(emails.threadId, threadId), eq(emails.isRead, false)))
|
.where(and(eq(emails.userId, userId), eq(emails.threadId, threadId), eq(emails.isRead, false)))
|
||||||
.returning();
|
.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 };
|
return { success: true, count: result.length };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ConflictError,
|
ConflictError,
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
} from '../utils/errors.js';
|
} from '../utils/errors.js';
|
||||||
|
import { logger } from '../utils/logger.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all email accounts for a user
|
* Get all email accounts for a user
|
||||||
@@ -200,14 +201,16 @@ export const toggleEmailAccountStatus = async (accountId, userId, isActive) => {
|
|||||||
export const setPrimaryEmailAccount = async (accountId, userId) => {
|
export const setPrimaryEmailAccount = async (accountId, userId) => {
|
||||||
const account = await getEmailAccountById(accountId, userId);
|
const account = await getEmailAccountById(accountId, userId);
|
||||||
|
|
||||||
|
// Use transaction to prevent race conditions
|
||||||
|
const updated = await db.transaction(async (tx) => {
|
||||||
// Remove primary flag from all accounts
|
// Remove primary flag from all accounts
|
||||||
await db
|
await tx
|
||||||
.update(emailAccounts)
|
.update(emailAccounts)
|
||||||
.set({ isPrimary: false, updatedAt: new Date() })
|
.set({ isPrimary: false, updatedAt: new Date() })
|
||||||
.where(eq(emailAccounts.userId, userId));
|
.where(eq(emailAccounts.userId, userId));
|
||||||
|
|
||||||
// Set new primary account
|
// Set new primary account
|
||||||
const [updated] = await db
|
const [updatedAccount] = await tx
|
||||||
.update(emailAccounts)
|
.update(emailAccounts)
|
||||||
.set({
|
.set({
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
@@ -217,6 +220,9 @@ export const setPrimaryEmailAccount = async (accountId, userId) => {
|
|||||||
.where(eq(emailAccounts.id, accountId))
|
.where(eq(emailAccounts.id, accountId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
return updatedAccount;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: updated.id,
|
id: updated.id,
|
||||||
email: updated.email,
|
email: updated.email,
|
||||||
@@ -260,9 +266,9 @@ export const deleteEmailAccount = async (accountId, userId) => {
|
|||||||
* Get email account with decrypted password (for JMAP operations)
|
* Get email account with decrypted password (for JMAP operations)
|
||||||
*/
|
*/
|
||||||
export const getEmailAccountWithCredentials = async (accountId, userId) => {
|
export const getEmailAccountWithCredentials = async (accountId, userId) => {
|
||||||
console.log('🔐 getEmailAccountWithCredentials called:', { accountId, userId });
|
logger.debug('getEmailAccountWithCredentials called', { accountId, userId });
|
||||||
const account = await getEmailAccountById(accountId, userId);
|
const account = await getEmailAccountById(accountId, userId);
|
||||||
console.log('📦 Account retrieved:', {
|
logger.debug('Account retrieved', {
|
||||||
id: account.id,
|
id: account.id,
|
||||||
email: account.email,
|
email: account.email,
|
||||||
hasPassword: !!account.emailPassword,
|
hasPassword: !!account.emailPassword,
|
||||||
@@ -270,7 +276,7 @@ export const getEmailAccountWithCredentials = async (accountId, userId) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const decryptedPassword = decryptPassword(account.emailPassword);
|
const decryptedPassword = decryptPassword(account.emailPassword);
|
||||||
console.log('🔓 Password decrypted, length:', decryptedPassword?.length);
|
logger.debug('Password decrypted', { passwordLength: decryptedPassword?.length });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: account.id,
|
id: account.id,
|
||||||
|
|||||||
@@ -1,31 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { logger } from '../utils/logger.js';
|
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
|
* Validuje JMAP credentials a vráti account ID
|
||||||
* @param {string} email - Email address
|
* @param {string} email - Email address
|
||||||
@@ -33,8 +8,14 @@ const getJmapSession = async () => {
|
|||||||
* @returns {Promise<{accountId: string, session: object}>}
|
* @returns {Promise<{accountId: string, session: object}>}
|
||||||
*/
|
*/
|
||||||
export const validateJmapCredentials = async (email, password) => {
|
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 {
|
try {
|
||||||
const response = await axios.get(`${JMAP_CONFIG.server}session`, {
|
const response = await axios.get(`${jmapServer}session`, {
|
||||||
auth: {
|
auth: {
|
||||||
username: email,
|
username: email,
|
||||||
password: password,
|
password: password,
|
||||||
@@ -68,188 +49,3 @@ export const validateJmapCredentials = async (email, password) => {
|
|||||||
throw new Error('Nepodarilo sa overiť emailový účet');
|
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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -67,8 +67,12 @@ export const generateVerificationToken = () => {
|
|||||||
* @returns {string} Encrypted password in format: iv:authTag:encrypted
|
* @returns {string} Encrypted password in format: iv:authTag:encrypted
|
||||||
*/
|
*/
|
||||||
export const encryptPassword = (text) => {
|
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 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 iv = crypto.randomBytes(16);
|
||||||
|
|
||||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||||
@@ -86,8 +90,12 @@ export const encryptPassword = (text) => {
|
|||||||
* @returns {string} Plain text password
|
* @returns {string} Plain text password
|
||||||
*/
|
*/
|
||||||
export const decryptPassword = (encryptedText) => {
|
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 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 parts = encryptedText.split(':');
|
||||||
const iv = Buffer.from(parts[0], 'hex');
|
const iv = Buffer.from(parts[0], 'hex');
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* Quick test script for JMAP search endpoint
|
|
||||||
* Run with: node test-search.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const API_URL = 'http://localhost:5000/api';
|
|
||||||
|
|
||||||
async function testSearch() {
|
|
||||||
try {
|
|
||||||
console.log('Testing /emails/search-jmap endpoint...\n');
|
|
||||||
|
|
||||||
// You'll need to replace this with a valid session cookie
|
|
||||||
// Get it from browser DevTools after logging in
|
|
||||||
const cookie = process.env.TEST_COOKIE || '';
|
|
||||||
|
|
||||||
if (!cookie) {
|
|
||||||
console.error('❌ Please set TEST_COOKIE environment variable');
|
|
||||||
console.log(' Get it from browser DevTools > Application > Cookies');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.get(`${API_URL}/emails/search-jmap`, {
|
|
||||||
params: {
|
|
||||||
query: 'test',
|
|
||||||
limit: 10,
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
Cookie: cookie,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Success!');
|
|
||||||
console.log('Status:', response.status);
|
|
||||||
console.log('Data:', JSON.stringify(response.data, null, 2));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error:', error.response?.data || error.message);
|
|
||||||
console.error('Status:', error.response?.status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testSearch();
|
|
||||||
Reference in New Issue
Block a user