initialize git, basic setup for crm

This commit is contained in:
richardtekula
2025-11-18 13:53:28 +01:00
commit da01d586fc
47 changed files with 12776 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
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 { logUserCreation, logRoleChange } from '../services/audit.service.js';
import { formatErrorResponse, ConflictError, NotFoundError } from '../utils/errors.js';
/**
* Vytvorenie nového usera s temporary password (admin only)
* POST /api/admin/users
*/
export const createUser = async (req, res) => {
const { username, tempPassword, role, firstName, lastName } = req.body;
const adminId = req.userId;
const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
try {
// Skontroluj či username už neexistuje
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.username, username))
.limit(1);
if (existingUser) {
throw new ConflictError('Username už existuje');
}
// Hash temporary password
const hashedTempPassword = await hashPassword(tempPassword);
// Vytvor usera
const [newUser] = await db
.insert(users)
.values({
username,
tempPassword: hashedTempPassword,
role: role || 'member',
firstName: firstName || null,
lastName: lastName || null,
changedPassword: false,
})
.returning();
// Log user creation
await logUserCreation(adminId, newUser.id, username, role || 'member', ipAddress, userAgent);
res.status(201).json({
success: true,
data: {
user: {
id: newUser.id,
username: newUser.username,
role: newUser.role,
tempPassword: tempPassword, // Vráti plain text password pre admina
},
},
message: 'Používateľ úspešne vytvorený',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Zoznam všetkých userov (admin only)
* GET /api/admin/users
*/
export const getAllUsers = async (req, res) => {
try {
const allUsers = await db
.select({
id: users.id,
username: users.username,
email: users.email,
firstName: users.firstName,
lastName: users.lastName,
role: users.role,
changedPassword: users.changedPassword,
lastLogin: users.lastLogin,
createdAt: users.createdAt,
})
.from(users);
res.status(200).json({
success: true,
data: {
users: allUsers,
count: allUsers.length,
},
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Získanie konkrétneho usera (admin only)
* GET /api/admin/users/:userId
*/
export const getUserById = async (req, res) => {
const { userId } = req.params;
try {
const [user] = await db
.select({
id: users.id,
username: users.username,
email: users.email,
firstName: users.firstName,
lastName: users.lastName,
role: users.role,
changedPassword: users.changedPassword,
lastLogin: users.lastLogin,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
})
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user) {
throw new NotFoundError('Používateľ nenájdený');
}
res.status(200).json({
success: true,
data: { user },
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Zmena role usera (admin only)
* PATCH /api/admin/users/:userId/role
*/
export const changeUserRole = async (req, res) => {
const { userId } = req.params;
const { role } = req.body;
const adminId = req.userId;
const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
try {
// Získaj starú rolu
const [user] = await db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user) {
throw new NotFoundError('Používateľ nenájdený');
}
const oldRole = user.role;
// Update role
await db
.update(users)
.set({
role,
updatedAt: new Date(),
})
.where(eq(users.id, userId));
// Log role change
await logRoleChange(adminId, userId, oldRole, role, ipAddress, userAgent);
res.status(200).json({
success: true,
data: {
userId,
oldRole,
newRole: role,
},
message: 'Rola používateľa bola zmenená',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Zmazanie usera (admin only)
* DELETE /api/admin/users/:userId
*/
export const deleteUser = async (req, res) => {
const { userId } = req.params;
try {
const [user] = await db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user) {
throw new NotFoundError('Používateľ nenájdený');
}
// Zabraň zmazaniu posledného admina
if (user.role === 'admin') {
const [adminCount] = await db
.select({ count: db.$count(users) })
.from(users)
.where(eq(users.role, 'admin'));
if (adminCount.count <= 1) {
throw new ConflictError('Nemôžete zmazať posledného administrátora');
}
}
await db.delete(users).where(eq(users.id, userId));
res.status(200).json({
success: true,
message: 'Používateľ bol zmazaný',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};

View File

@@ -0,0 +1,204 @@
import * as authService from '../services/auth.service.js';
import {
logLoginAttempt,
logPasswordChange,
logEmailLink,
} from '../services/audit.service.js';
import { formatErrorResponse } from '../utils/errors.js';
/**
* KROK 1: Login s temporary password
* POST /api/auth/login
*/
export const login = async (req, res) => {
const { username, password } = req.body;
const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
try {
const result = await authService.loginWithTempPassword(
username,
password,
ipAddress,
userAgent
);
// Log successful login
await logLoginAttempt(username, true, ipAddress, userAgent);
// Nastav cookie s access tokenom (httpOnly, secure)
res.cookie('accessToken', result.tokens.accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 1000, // 1 hodina
});
res.cookie('refreshToken', result.tokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 dní
});
res.status(200).json({
success: true,
data: {
user: result.user,
tokens: result.tokens,
needsPasswordChange: result.needsPasswordChange,
needsEmailSetup: result.needsEmailSetup,
},
message: 'Prihlásenie úspešné',
});
} catch (error) {
// Log failed login
await logLoginAttempt(username, false, ipAddress, userAgent, error.message);
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* KROK 2: Nastavenie nového hesla
* POST /api/auth/set-password
* Requires: authentication
*/
export const setPassword = async (req, res) => {
const { newPassword } = req.body;
const userId = req.userId;
const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
try {
const result = await authService.setNewPassword(userId, newPassword);
// Log password change
await logPasswordChange(userId, ipAddress, userAgent);
res.status(200).json({
success: true,
data: result,
message: 'Heslo úspešne nastavené',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* KROK 3: Pripojenie emailu s JMAP validáciou
* POST /api/auth/link-email
* Requires: authentication
*/
export const linkEmail = async (req, res) => {
const { email, emailPassword } = req.body;
const userId = req.userId;
const ipAddress = req.ip || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
try {
const result = await authService.linkEmail(userId, email, emailPassword);
// Log email link
await logEmailLink(userId, email, ipAddress, userAgent);
res.status(200).json({
success: true,
data: {
email,
accountId: result.accountId,
},
message: 'Email účet úspešne pripojený a overený',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* KROK 3 (alternatíva): Skip email setup
* POST /api/auth/skip-email
* Requires: authentication
*/
export const skipEmail = async (req, res) => {
const userId = req.userId;
try {
const result = await authService.skipEmailSetup(userId);
res.status(200).json({
success: true,
data: result,
message: 'Email setup preskočený',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Logout
* POST /api/auth/logout
* Requires: authentication
*/
export const logout = async (req, res) => {
try {
const result = await authService.logout();
// Vymaž cookies
res.clearCookie('accessToken');
res.clearCookie('refreshToken');
res.status(200).json({
success: true,
message: result.message,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Získanie aktuálnej session info
* GET /api/auth/session
* Requires: authentication
*/
export const getSession = async (req, res) => {
try {
res.status(200).json({
success: true,
data: {
user: req.user,
authenticated: true,
},
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Profil aktuálneho usera
* GET /api/auth/me
* Requires: authentication
*/
export const getMe = async (req, res) => {
try {
res.status(200).json({
success: true,
data: {
user: req.user,
},
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};

View File

@@ -0,0 +1,147 @@
import * as contactService from '../services/contact.service.js';
import { discoverContactsFromJMAP, getJmapConfig } from '../services/jmap.service.js';
import { formatErrorResponse } from '../utils/errors.js';
import { getUserById } from '../services/auth.service.js';
/**
* Get all contacts for authenticated user
* GET /api/contacts
*/
export const getContacts = async (req, res) => {
try {
const userId = req.userId;
const contacts = await contactService.getUserContacts(userId);
res.status(200).json({
success: true,
count: contacts.length,
data: contacts,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Discover potential contacts from JMAP (email senders)
* GET /api/contacts/discover?search=query&limit=50
*/
export const discoverContacts = async (req, res) => {
try {
const userId = req.userId;
const { search = '', limit = 50 } = 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 potentialContacts = await discoverContactsFromJMAP(
jmapConfig,
userId,
search,
parseInt(limit)
);
res.status(200).json({
success: true,
count: potentialContacts.length,
data: potentialContacts,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Add a new contact
* POST /api/contacts
*/
export const addContact = async (req, res) => {
try {
const userId = req.userId;
const { email, name = '', notes = '' } = req.body;
if (!email) {
return res.status(400).json({
success: false,
error: {
message: 'Email je povinný',
statusCode: 400,
},
});
}
// Get user to access JMAP config
const user = await getUserById(userId);
const jmapConfig = getJmapConfig(user);
const contact = await contactService.addContact(userId, jmapConfig, email, name, notes);
res.status(201).json({
success: true,
data: contact,
message: 'Kontakt pridaný a emaily synchronizované',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Remove a contact
* DELETE /api/contacts/:contactId
*/
export const removeContact = async (req, res) => {
try {
const userId = req.userId;
const { contactId } = req.params;
const result = await contactService.removeContact(userId, contactId);
res.status(200).json({
success: true,
message: result.message,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Update a contact
* PATCH /api/contacts/:contactId
*/
export const updateContact = async (req, res) => {
try {
const userId = req.userId;
const { contactId } = req.params;
const { name, notes } = req.body;
const updated = await contactService.updateContact(userId, contactId, { name, notes });
res.status(200).json({
success: true,
data: updated,
message: 'Kontakt aktualizovaný',
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};

View File

@@ -0,0 +1,193 @@
import * as crmEmailService from '../services/crm-email.service.js';
import { markEmailAsRead, sendEmail, getJmapConfig } from '../services/jmap.service.js';
import { formatErrorResponse } from '../utils/errors.js';
import { getUserById } from '../services/auth.service.js';
/**
* Get all emails for authenticated user
* GET /api/emails
*/
export const getEmails = async (req, res) => {
try {
const userId = req.userId;
const emails = await crmEmailService.getUserEmails(userId);
res.status(200).json({
success: true,
count: emails.length,
data: emails,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Get emails by thread (conversation)
* GET /api/emails/thread/:threadId
*/
export const getThread = async (req, res) => {
try {
const userId = req.userId;
const { threadId } = req.params;
const thread = await crmEmailService.getEmailThread(userId, threadId);
res.status(200).json({
success: true,
count: thread.length,
data: thread,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Search emails
* GET /api/emails/search?q=query
*/
export const searchEmails = async (req, res) => {
try {
const userId = req.userId;
const { q } = req.query;
const results = await crmEmailService.searchEmails(userId, q);
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);
}
};
/**
* Get unread count
* GET /api/emails/unread-count
*/
export const getUnreadCount = async (req, res) => {
try {
const userId = req.userId;
const count = await crmEmailService.getUnreadCount(userId);
res.status(200).json({
success: true,
data: { count },
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Mark email as read/unread
* PATCH /api/emails/:jmapId/read
*/
export const markAsRead = async (req, res) => {
try {
const userId = req.userId;
const { jmapId } = req.params;
const { isRead } = req.body;
// Get user to access JMAP config
const user = await getUserById(userId);
const jmapConfig = getJmapConfig(user);
await markEmailAsRead(jmapConfig, userId, jmapId, isRead);
res.status(200).json({
success: true,
message: `Email označený ako ${isRead ? 'prečítaný' : 'neprečítaný'}`,
});
} catch (error) {
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
*/
export const markThreadRead = async (req, res) => {
try {
const userId = req.userId;
const { threadId } = req.params;
const result = await crmEmailService.markThreadAsRead(userId, threadId);
res.status(200).json({
success: true,
message: 'Konverzácia označená ako prečítaná',
count: result.count,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Send email reply
* POST /api/emails/reply
*/
export const replyToEmail = async (req, res) => {
try {
const userId = req.userId;
const { to, subject, body, inReplyTo = null, threadId = null } = req.body;
if (!to || !subject || !body) {
return res.status(400).json({
success: false,
error: {
message: 'Chýbajúce povinné polia: to, subject, body',
statusCode: 400,
},
});
}
// Get user to access JMAP config
const user = await getUserById(userId);
const jmapConfig = getJmapConfig(user);
const result = await sendEmail(jmapConfig, userId, to, subject, body, inReplyTo, threadId);
res.status(200).json({
success: true,
message: 'Email odoslaný',
data: result,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};
/**
* Get emails for a specific contact
* GET /api/emails/contact/:contactId
*/
export const getContactEmails = async (req, res) => {
try {
const userId = req.userId;
const { contactId } = req.params;
const emails = await crmEmailService.getContactEmails(userId, contactId);
res.status(200).json({
success: true,
count: emails.length,
data: emails,
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
res.status(error.statusCode || 500).json(errorResponse);
}
};