add searching, total unread message, create user

This commit is contained in:
richardtekula
2025-11-19 08:45:37 +01:00
parent da01d586fc
commit 97f437c1c4
8 changed files with 1338 additions and 90 deletions

View File

@@ -1,16 +1,18 @@
import { db } from '../config/database.js';
import { users } from '../db/schema.js';
import { eq } from 'drizzle-orm';
import { hashPassword, generateTempPassword } from '../utils/password.js';
import { hashPassword, generateTempPassword, encryptPassword } from '../utils/password.js';
import { logUserCreation, logRoleChange } from '../services/audit.service.js';
import { formatErrorResponse, ConflictError, NotFoundError } from '../utils/errors.js';
import { validateJmapCredentials } from '../services/email.service.js';
/**
* Vytvorenie nového usera s temporary password (admin only)
* Vytvorenie nového usera s automatic temporary password (admin only)
* Ak je poskytnutý email a emailPassword, automaticky sa fetchne JMAP account ID
* POST /api/admin/users
*/
export const createUser = async (req, res) => {
const { username, tempPassword, role, firstName, lastName } = req.body;
const { username, email, emailPassword, firstName, lastName } = req.body;
const adminId = req.userId;
const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
@@ -27,16 +29,34 @@ export const createUser = async (req, res) => {
throw new ConflictError('Username už existuje');
}
// Hash temporary password
// Automaticky vygeneruj temporary password
const tempPassword = generateTempPassword(12);
const hashedTempPassword = await hashPassword(tempPassword);
// Ak sú poskytnuté email credentials, validuj ich a získaj JMAP account ID
let jmapAccountId = null;
let encryptedEmailPassword = null;
if (email && emailPassword) {
try {
const { accountId } = await validateJmapCredentials(email, emailPassword);
jmapAccountId = accountId;
encryptedEmailPassword = encryptPassword(emailPassword);
} catch (emailError) {
throw new ConflictError(`Nepodarilo sa overiť emailový účet: ${emailError.message}`);
}
}
// Vytvor usera
const [newUser] = await db
.insert(users)
.values({
username,
email: email || null,
emailPassword: encryptedEmailPassword,
jmapAccountId,
tempPassword: hashedTempPassword,
role: role || 'member',
role: 'member', // Vždy member, nie admin
firstName: firstName || null,
lastName: lastName || null,
changedPassword: false,
@@ -44,7 +64,7 @@ export const createUser = async (req, res) => {
.returning();
// Log user creation
await logUserCreation(adminId, newUser.id, username, role || 'member', ipAddress, userAgent);
await logUserCreation(adminId, newUser.id, username, 'member', ipAddress, userAgent);
res.status(201).json({
success: true,
@@ -52,11 +72,18 @@ export const createUser = async (req, res) => {
user: {
id: newUser.id,
username: newUser.username,
email: newUser.email,
firstName: newUser.firstName,
lastName: newUser.lastName,
role: newUser.role,
tempPassword: tempPassword, // Vráti plain text password pre admina
jmapAccountId: newUser.jmapAccountId,
emailSetup: !!newUser.jmapAccountId,
tempPassword: tempPassword, // Vráti plain text password pre admina aby ho mohol poslať userovi
},
},
message: 'Používateľ úspešne vytvorený',
message: newUser.jmapAccountId
? 'Používateľ úspešne vytvorený s emailovým účtom.'
: 'Používateľ úspešne vytvorený. Email môže byť nastavený neskôr.',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');

View File

@@ -1,5 +1,6 @@
import * as crmEmailService from '../services/crm-email.service.js';
import { markEmailAsRead, sendEmail, getJmapConfig } from '../services/jmap.service.js';
import * as contactService from '../services/contact.service.js';
import { markEmailAsRead, sendEmail, getJmapConfig, syncEmailsFromSender, searchEmailsJMAP as searchEmailsJMAPService } from '../services/jmap.service.js';
import { formatErrorResponse } from '../utils/errors.js';
import { getUserById } from '../services/auth.service.js';
@@ -76,9 +77,89 @@ export const getUnreadCount = async (req, res) => {
const userId = req.userId;
const count = await crmEmailService.getUnreadCount(userId);
const accounts = [];
if (req.user?.email) {
accounts.push({
id: req.user.jmapAccountId || req.user.email,
email: req.user.email,
label: req.user.email,
unread: count,
});
}
res.status(200).json({
success: true,
data: { count },
data: {
count,
totalUnread: count,
accounts,
lastUpdatedAt: new Date().toISOString(),
},
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Sync latest emails for all contacts from JMAP
* POST /api/emails/sync
*/
export const syncEmails = async (req, res) => {
try {
const userId = req.userId;
const user = await getUserById(userId);
if (!user.email || !user.emailPassword || !user.jmapAccountId) {
return res.status(400).json({
success: false,
error: {
message: 'Najprv musíš pripojiť email účet v Profile',
statusCode: 400,
},
});
}
const contacts = await contactService.getUserContacts(userId);
if (!contacts.length) {
return res.status(200).json({
success: true,
message: 'Žiadne kontakty na synchronizáciu',
data: { contacts: 0, synced: 0, newEmails: 0 },
});
}
const jmapConfig = getJmapConfig(user);
let totalSynced = 0;
let totalNew = 0;
for (const contact of contacts) {
try {
const { total, saved } = await syncEmailsFromSender(
jmapConfig,
userId,
contact.id,
contact.email,
{ limit: 50 }
);
totalSynced += total;
totalNew += saved;
} catch (syncError) {
console.error(`Failed to sync emails for contact ${contact.email}`, syncError);
}
}
return res.status(200).json({
success: true,
message: 'Emaily synchronizované',
data: {
contacts: contacts.length,
synced: totalSynced,
newEmails: totalNew,
},
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
@@ -121,12 +202,34 @@ export const markThreadRead = async (req, res) => {
const userId = req.userId;
const { threadId } = req.params;
const result = await crmEmailService.markThreadAsRead(userId, threadId);
const user = await getUserById(userId);
const threadEmails = await crmEmailService.getEmailThread(userId, threadId);
const unreadEmails = threadEmails.filter((email) => !email.isRead);
let jmapConfig = null;
if (user?.email && user?.emailPassword && user?.jmapAccountId) {
jmapConfig = getJmapConfig(user);
}
if (jmapConfig) {
for (const email of unreadEmails) {
if (!email.jmapId) {
continue;
}
try {
await markEmailAsRead(jmapConfig, userId, email.jmapId, true);
} catch (jmapError) {
console.error(`Failed to mark JMAP email ${email.jmapId} as read`, jmapError);
}
}
}
await crmEmailService.markThreadAsRead(userId, threadId);
res.status(200).json({
success: true,
message: 'Konverzácia označená ako prečítaná',
count: result.count,
count: unreadEmails.length,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
@@ -191,3 +294,48 @@ export const getContactEmails = async (req, res) => {
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Search emails using JMAP full-text search
* GET /api/emails/search-jmap?query=text&limit=50&offset=0
* Searches in: from, to, subject, and email body
*/
export const searchEmailsJMAP = async (req, res) => {
try {
const userId = req.userId;
const { query = '', limit = 50, offset = 0 } = req.query;
// Get user to access JMAP config
const user = await getUserById(userId);
// Check if user has JMAP email configured
if (!user.email || !user.emailPassword || !user.jmapAccountId) {
return res.status(400).json({
success: false,
error: {
message: 'Najprv musíš pripojiť email účet v Profile',
statusCode: 400,
},
});
}
const jmapConfig = getJmapConfig(user);
const results = await searchEmailsJMAPService(
jmapConfig,
userId,
query,
parseInt(limit),
parseInt(offset)
);
res.status(200).json({
success: true,
count: results.length,
data: results,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};