add searching, total unread message, create user
This commit is contained in:
24
check-tables.js
Normal file
24
check-tables.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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,16 +1,18 @@
|
|||||||
import { db } from '../config/database.js';
|
import { db } from '../config/database.js';
|
||||||
import { users } from '../db/schema.js';
|
import { users } from '../db/schema.js';
|
||||||
import { eq } from 'drizzle-orm';
|
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 { logUserCreation, logRoleChange } from '../services/audit.service.js';
|
||||||
import { formatErrorResponse, ConflictError, NotFoundError } from '../utils/errors.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
|
* POST /api/admin/users
|
||||||
*/
|
*/
|
||||||
export const createUser = async (req, res) => {
|
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 adminId = req.userId;
|
||||||
const ipAddress = req.ip || req.connection.remoteAddress;
|
const ipAddress = req.ip || req.connection.remoteAddress;
|
||||||
const userAgent = req.headers['user-agent'];
|
const userAgent = req.headers['user-agent'];
|
||||||
@@ -27,16 +29,34 @@ export const createUser = async (req, res) => {
|
|||||||
throw new ConflictError('Username už existuje');
|
throw new ConflictError('Username už existuje');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash temporary password
|
// Automaticky vygeneruj temporary password
|
||||||
|
const tempPassword = generateTempPassword(12);
|
||||||
const hashedTempPassword = await hashPassword(tempPassword);
|
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
|
// Vytvor usera
|
||||||
const [newUser] = await db
|
const [newUser] = await db
|
||||||
.insert(users)
|
.insert(users)
|
||||||
.values({
|
.values({
|
||||||
username,
|
username,
|
||||||
|
email: email || null,
|
||||||
|
emailPassword: encryptedEmailPassword,
|
||||||
|
jmapAccountId,
|
||||||
tempPassword: hashedTempPassword,
|
tempPassword: hashedTempPassword,
|
||||||
role: role || 'member',
|
role: 'member', // Vždy member, nie admin
|
||||||
firstName: firstName || null,
|
firstName: firstName || null,
|
||||||
lastName: lastName || null,
|
lastName: lastName || null,
|
||||||
changedPassword: false,
|
changedPassword: false,
|
||||||
@@ -44,7 +64,7 @@ export const createUser = async (req, res) => {
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// Log user creation
|
// 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({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -52,11 +72,18 @@ export const createUser = async (req, res) => {
|
|||||||
user: {
|
user: {
|
||||||
id: newUser.id,
|
id: newUser.id,
|
||||||
username: newUser.username,
|
username: newUser.username,
|
||||||
|
email: newUser.email,
|
||||||
|
firstName: newUser.firstName,
|
||||||
|
lastName: newUser.lastName,
|
||||||
role: newUser.role,
|
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) {
|
} catch (error) {
|
||||||
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as crmEmailService from '../services/crm-email.service.js';
|
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 { formatErrorResponse } from '../utils/errors.js';
|
||||||
import { getUserById } from '../services/auth.service.js';
|
import { getUserById } from '../services/auth.service.js';
|
||||||
|
|
||||||
@@ -76,9 +77,89 @@ export const getUnreadCount = async (req, res) => {
|
|||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const count = await crmEmailService.getUnreadCount(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({
|
res.status(200).json({
|
||||||
success: true,
|
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) {
|
} catch (error) {
|
||||||
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
@@ -121,12 +202,34 @@ export const markThreadRead = async (req, res) => {
|
|||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const { threadId } = req.params;
|
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({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Konverzácia označená ako prečítaná',
|
message: 'Konverzácia označená ako prečítaná',
|
||||||
count: result.count,
|
count: unreadEmails.length,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
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);
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,12 +16,18 @@ router.use(authenticate);
|
|||||||
// Get all emails
|
// Get all emails
|
||||||
router.get('/', crmEmailController.getEmails);
|
router.get('/', crmEmailController.getEmails);
|
||||||
|
|
||||||
// Search emails
|
// Search emails (DB search - searches in stored emails only)
|
||||||
router.get('/search', crmEmailController.searchEmails);
|
router.get('/search', crmEmailController.searchEmails);
|
||||||
|
|
||||||
|
// Search emails using JMAP full-text search (searches in all emails via JMAP)
|
||||||
|
router.get('/search-jmap', crmEmailController.searchEmailsJMAP);
|
||||||
|
|
||||||
// Get unread count
|
// Get unread count
|
||||||
router.get('/unread-count', crmEmailController.getUnreadCount);
|
router.get('/unread-count', crmEmailController.getUnreadCount);
|
||||||
|
|
||||||
|
// Sync latest emails from JMAP
|
||||||
|
router.post('/sync', crmEmailController.syncEmails);
|
||||||
|
|
||||||
// Get email thread (conversation)
|
// Get email thread (conversation)
|
||||||
router.get(
|
router.get(
|
||||||
'/thread/:threadId',
|
'/thread/:threadId',
|
||||||
|
|||||||
@@ -193,10 +193,148 @@ export const discoverContactsFromJMAP = async (jmapConfig, userId, searchTerm =
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search emails using JMAP full-text search
|
||||||
|
* Searches in: from, to, subject, and email body
|
||||||
|
* Returns list of unique senders grouped by email address
|
||||||
|
*/
|
||||||
|
export const searchEmailsJMAP = async (jmapConfig, userId, query, limit = 50, offset = 0) => {
|
||||||
|
try {
|
||||||
|
logger.info(`Searching emails in JMAP (query: "${query}", limit: ${limit}, offset: ${offset})`);
|
||||||
|
|
||||||
|
if (!query || query.trim().length < 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use JMAP search with wildcards for substring matching
|
||||||
|
// Add wildcards (*) to enable partial matching: "ander" -> "*ander*"
|
||||||
|
const wildcardQuery = query.includes('*') ? query : `*${query}*`;
|
||||||
|
|
||||||
|
let queryResponse;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try with 'text' filter first (full-text search if supported)
|
||||||
|
queryResponse = await jmapRequest(jmapConfig, [
|
||||||
|
[
|
||||||
|
'Email/query',
|
||||||
|
{
|
||||||
|
accountId: jmapConfig.accountId,
|
||||||
|
filter: {
|
||||||
|
text: wildcardQuery, // Full-text search with wildcards
|
||||||
|
},
|
||||||
|
sort: [{ property: 'receivedAt', isAscending: false }],
|
||||||
|
position: offset,
|
||||||
|
limit: 200,
|
||||||
|
},
|
||||||
|
'query1',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} catch (textFilterError) {
|
||||||
|
// If 'text' filter fails, fall back to OR conditions with wildcards
|
||||||
|
logger.warn('Text filter failed, falling back to OR conditions', textFilterError);
|
||||||
|
queryResponse = await jmapRequest(jmapConfig, [
|
||||||
|
[
|
||||||
|
'Email/query',
|
||||||
|
{
|
||||||
|
accountId: jmapConfig.accountId,
|
||||||
|
filter: {
|
||||||
|
operator: 'OR',
|
||||||
|
conditions: [
|
||||||
|
{ from: wildcardQuery },
|
||||||
|
{ to: wildcardQuery },
|
||||||
|
{ subject: wildcardQuery },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sort: [{ property: 'receivedAt', isAscending: false }],
|
||||||
|
position: offset,
|
||||||
|
limit: 200,
|
||||||
|
},
|
||||||
|
'query1',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailIds = queryResponse.methodResponses?.[0]?.[1]?.ids;
|
||||||
|
|
||||||
|
if (!emailIds || emailIds.length === 0) {
|
||||||
|
logger.info('No emails found matching search query');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Found ${emailIds.length} emails matching query`);
|
||||||
|
|
||||||
|
// Fetch email metadata
|
||||||
|
const getResponse = await jmapRequest(jmapConfig, [
|
||||||
|
[
|
||||||
|
'Email/get',
|
||||||
|
{
|
||||||
|
accountId: jmapConfig.accountId,
|
||||||
|
ids: emailIds,
|
||||||
|
properties: ['from', 'to', 'subject', 'receivedAt', 'preview'],
|
||||||
|
},
|
||||||
|
'get1',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const emailsList = getResponse.methodResponses[0][1].list;
|
||||||
|
|
||||||
|
// Get existing contacts for this user
|
||||||
|
const existingContacts = await db
|
||||||
|
.select()
|
||||||
|
.from(contacts)
|
||||||
|
.where(eq(contacts.userId, userId));
|
||||||
|
|
||||||
|
const contactEmailsSet = new Set(existingContacts.map((c) => c.email.toLowerCase()));
|
||||||
|
|
||||||
|
// Group by sender (unique senders)
|
||||||
|
const sendersMap = new Map();
|
||||||
|
const myEmail = jmapConfig.username.toLowerCase();
|
||||||
|
|
||||||
|
emailsList.forEach((email) => {
|
||||||
|
const fromEmail = email.from?.[0]?.email;
|
||||||
|
|
||||||
|
if (!fromEmail || fromEmail.toLowerCase() === myEmail) {
|
||||||
|
return; // Skip my own emails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep only the most recent email from each sender
|
||||||
|
if (!sendersMap.has(fromEmail)) {
|
||||||
|
sendersMap.set(fromEmail, {
|
||||||
|
email: fromEmail,
|
||||||
|
name: email.from?.[0]?.name || fromEmail.split('@')[0],
|
||||||
|
latestSubject: email.subject || '(No Subject)',
|
||||||
|
latestDate: email.receivedAt,
|
||||||
|
snippet: email.preview || '',
|
||||||
|
isContact: contactEmailsSet.has(fromEmail.toLowerCase()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to array, sort by date, and apply limit
|
||||||
|
const senders = Array.from(sendersMap.values())
|
||||||
|
.sort((a, b) => new Date(b.latestDate) - new Date(a.latestDate))
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
|
logger.success(`Found ${senders.length} unique senders matching query`);
|
||||||
|
return senders;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to search emails in JMAP', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync emails from a specific sender (when adding as contact)
|
* Sync emails from a specific sender (when adding as contact)
|
||||||
*/
|
*/
|
||||||
export const syncEmailsFromSender = async (jmapConfig, userId, contactId, senderEmail) => {
|
export const syncEmailsFromSender = async (
|
||||||
|
jmapConfig,
|
||||||
|
userId,
|
||||||
|
contactId,
|
||||||
|
senderEmail,
|
||||||
|
options = {}
|
||||||
|
) => {
|
||||||
|
const { limit = 500 } = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(`Syncing emails from sender: ${senderEmail}`);
|
logger.info(`Syncing emails from sender: ${senderEmail}`);
|
||||||
|
|
||||||
@@ -211,7 +349,7 @@ export const syncEmailsFromSender = async (jmapConfig, userId, contactId, sender
|
|||||||
conditions: [{ from: senderEmail }, { to: senderEmail }],
|
conditions: [{ from: senderEmail }, { to: senderEmail }],
|
||||||
},
|
},
|
||||||
sort: [{ property: 'receivedAt', isAscending: false }],
|
sort: [{ property: 'receivedAt', isAscending: false }],
|
||||||
limit: 500,
|
limit,
|
||||||
},
|
},
|
||||||
'query1',
|
'query1',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export const linkEmailSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Create user schema (admin only)
|
// Create user schema (admin only) - temp password sa generuje automaticky
|
||||||
|
// Ak je poskytnutý email, môže byť poskytnuté aj emailPassword pre automatické nastavenie JMAP
|
||||||
export const createUserSchema = z.object({
|
export const createUserSchema = z.object({
|
||||||
username: z
|
username: z
|
||||||
.string({
|
.string({
|
||||||
@@ -70,15 +71,8 @@ export const createUserSchema = z.object({
|
|||||||
/^[a-zA-Z0-9_-]+$/,
|
/^[a-zA-Z0-9_-]+$/,
|
||||||
'Username môže obsahovať iba písmená, čísla, pomlčky a podčiarkovníky'
|
'Username môže obsahovať iba písmená, čísla, pomlčky a podčiarkovníky'
|
||||||
),
|
),
|
||||||
tempPassword: z
|
email: z.string().email('Neplatný formát emailu').max(255).optional(),
|
||||||
.string({
|
emailPassword: z.string().min(1).optional(),
|
||||||
required_error: 'Dočasné heslo je povinné',
|
|
||||||
})
|
|
||||||
.min(8, 'Dočasné heslo musí mať aspoň 8 znakov'),
|
|
||||||
role: z.enum(['admin', 'member'], {
|
|
||||||
required_error: 'Rola je povinná',
|
|
||||||
invalid_type_error: 'Neplatná rola',
|
|
||||||
}),
|
|
||||||
firstName: z.string().max(100).optional(),
|
firstName: z.string().max(100).optional(),
|
||||||
lastName: z.string().max(100).optional(),
|
lastName: z.string().max(100).optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
44
test-search.js
Normal file
44
test-search.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* 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