feat: Add creator info, team management for companies, and member access control

- Add creator info (username) to companies, projects, and notes responses
- Add company_users table for team management on companies
- Add resourceAccessMiddleware for member access control
- Members can only see resources they are directly assigned to
- Companies, projects, and todos are now filtered by user assignments
- Add personal contacts feature

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
richardtekula
2025-12-12 07:41:57 +01:00
parent 918af3a843
commit 8656fb1db0
14 changed files with 2175 additions and 125 deletions

1392
DOCUMENTATION.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,12 +7,15 @@ import { logCompanyCreated, logCompanyDeleted } from '../services/audit.service.
/**
* Get all companies
* GET /api/companies?search=query
* Members only see companies they are assigned to
*/
export const getAllCompanies = async (req, res, next) => {
try {
const { search } = req.query;
const userId = req.user?.id;
const userRole = req.user?.role;
const companies = await companyService.getAllCompanies(search);
const companies = await companyService.getAllCompanies(search, userId, userRole);
res.status(200).json({
success: true,
@@ -333,9 +336,11 @@ export const deleteCompanyReminder = async (req, res, next) => {
}
};
export const getReminderSummary = async (_req, res, next) => {
export const getReminderSummary = async (req, res, next) => {
try {
const summary = await companyReminderService.getReminderSummary();
const userId = req.user?.id;
const userRole = req.user?.role;
const summary = await companyReminderService.getReminderSummary(userId, userRole);
res.status(200).json({
success: true,
data: summary,
@@ -345,9 +350,11 @@ export const getReminderSummary = async (_req, res, next) => {
}
};
export const getReminderCountsByCompany = async (_req, res, next) => {
export const getReminderCountsByCompany = async (req, res, next) => {
try {
const counts = await companyReminderService.getReminderCountsByCompany();
const userId = req.user?.id;
const userRole = req.user?.role;
const counts = await companyReminderService.getReminderCountsByCompany(userId, userRole);
res.status(200).json({
success: true,
data: counts,
@@ -357,9 +364,11 @@ export const getReminderCountsByCompany = async (_req, res, next) => {
}
};
export const getUpcomingReminders = async (_req, res, next) => {
export const getUpcomingReminders = async (req, res, next) => {
try {
const reminders = await companyReminderService.getUpcomingReminders();
const userId = req.user?.id;
const userRole = req.user?.role;
const reminders = await companyReminderService.getUpcomingReminders(userId, userRole);
res.status(200).json({
success: true,
count: reminders.length,
@@ -369,3 +378,87 @@ export const getUpcomingReminders = async (_req, res, next) => {
next(error);
}
};
/**
* Get company users (team members)
* GET /api/companies/:companyId/users
*/
export const getCompanyUsers = async (req, res, next) => {
try {
const { companyId } = req.params;
const users = await companyService.getCompanyUsers(companyId);
res.status(200).json({
success: true,
count: users.length,
data: users,
});
} catch (error) {
next(error);
}
};
/**
* Assign user to company
* POST /api/companies/:companyId/users
* Body: { userId, role }
*/
export const assignUserToCompany = async (req, res, next) => {
try {
const currentUserId = req.userId;
const { companyId } = req.params;
const { userId, role } = req.body;
const assignment = await companyService.assignUserToCompany(companyId, userId, currentUserId, role);
res.status(201).json({
success: true,
data: assignment,
message: 'Používateľ bol priradený k firme',
});
} catch (error) {
next(error);
}
};
/**
* Remove user from company
* DELETE /api/companies/:companyId/users/:userId
*/
export const removeUserFromCompany = async (req, res, next) => {
try {
const { companyId, userId } = req.params;
const result = await companyService.removeUserFromCompany(companyId, userId);
res.status(200).json({
success: true,
message: result.message,
});
} catch (error) {
next(error);
}
};
/**
* Update user role on company
* PATCH /api/companies/:companyId/users/:userId
* Body: { role }
*/
export const updateUserRoleOnCompany = async (req, res, next) => {
try {
const { companyId, userId } = req.params;
const { role } = req.body;
const assignment = await companyService.updateUserRoleOnCompany(companyId, userId, role);
res.status(200).json({
success: true,
data: assignment,
message: 'Rola používateľa bola aktualizovaná',
});
} catch (error) {
next(error);
}
};

View File

@@ -5,12 +5,15 @@ import { logProjectCreated, logProjectDeleted } from '../services/audit.service.
/**
* Get all projects
* GET /api/projects?search=query&companyId=xxx
* Members only see projects they are assigned to or projects of companies they are assigned to
*/
export const getAllProjects = async (req, res, next) => {
try {
const { search, companyId } = req.query;
const userId = req.user?.id;
const userRole = req.user?.role;
const projects = await projectService.getAllProjects(search, companyId);
const projects = await projectService.getAllProjects(search, companyId, userId, userRole);
res.status(200).json({
success: true,

View File

@@ -4,10 +4,13 @@ import { logTodoCreated, logTodoDeleted, logTodoCompleted } from '../services/au
/**
* Get all todos
* GET /api/todos?search=query&projectId=xxx&companyId=xxx&assignedTo=xxx&status=xxx
* Members only see todos they are assigned to
*/
export const getAllTodos = async (req, res, next) => {
try {
const { search, projectId, companyId, assignedTo, status, completed, priority } = req.query;
const userId = req.user?.id;
const userRole = req.user?.role;
// Handle both 'status' and 'completed' query params
let statusFilter = status;
@@ -24,7 +27,7 @@ export const getAllTodos = async (req, res, next) => {
priority,
};
const todos = await todoService.getAllTodos(filters);
const todos = await todoService.getAllTodos(filters, userId, userRole);
res.status(200).json({
success: true,

View File

@@ -158,6 +158,18 @@ export const companyReminders = pgTable('company_remind', {
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Company Users - many-to-many medzi companies a users (tím firmy)
export const companyUsers = pgTable('company_users', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'cascade' }).notNull(),
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
role: text('role'), // napr. 'lead', 'member', 'viewer' - voliteľné
addedBy: uuid('added_by').references(() => users.id, { onDelete: 'set null' }), // kto pridal používateľa do firmy
addedAt: timestamp('added_at').defaultNow().notNull(),
}, (table) => ({
companyUserUnique: unique('company_user_unique').on(table.companyId, table.userId),
}));
// Project Users - many-to-many medzi projects a users (tím projektu)
export const projectUsers = pgTable('project_users', {
id: uuid('id').primaryKey().defaultRandom(),

View File

@@ -0,0 +1,160 @@
import { db } from '../../config/database.js';
import { companyUsers, projectUsers, todoUsers } from '../../db/schema.js';
import { eq, and } from 'drizzle-orm';
/**
* Univerzálny middleware pre kontrolu prístupu k resources
* Admin má prístup vždy, member len ak je priradený k resource
*
* Použitie:
* checkResourceAccess('company', 'companyId') // pre /companies/:companyId
* checkResourceAccess('project', 'projectId') // pre /projects/:projectId
* checkResourceAccess('todo', 'todoId') // pre /todos/:todoId
*/
// Mapovanie resource typu na junction tabuľku a stĺpce
const resourceConfig = {
company: {
table: companyUsers,
resourceIdColumn: 'companyId',
},
project: {
table: projectUsers,
resourceIdColumn: 'projectId',
},
todo: {
table: todoUsers,
resourceIdColumn: 'todoId',
},
};
/**
* Skontroluje či user má prístup k danému resource
* @param {string} resourceType - Typ resource ('company', 'project', atď.)
* @param {string} userId - ID používateľa
* @param {string} resourceId - ID resource
* @returns {Promise<boolean>}
*/
export const hasAccessToResource = async (resourceType, userId, resourceId) => {
const config = resourceConfig[resourceType];
if (!config) {
throw new Error(`Unknown resource type: ${resourceType}`);
}
const { table, resourceIdColumn } = config;
const [assignment] = await db
.select()
.from(table)
.where(and(
eq(table[resourceIdColumn], resourceId),
eq(table.userId, userId)
))
.limit(1);
return !!assignment;
};
/**
* Middleware factory pre kontrolu prístupu k resource
* @param {string} resourceType - Typ resource ('company', 'project')
* @param {string} paramName - Názov parametra v URL (napr. 'companyId', 'projectId')
*/
export const checkResourceAccess = (resourceType, paramName) => {
return async (req, res, next) => {
// Skontroluj či je user autentifikovaný
if (!req.user) {
return res.status(401).json({
success: false,
error: {
message: 'Musíte byť prihlásený',
statusCode: 401,
},
});
}
// Admin má prístup vždy
if (req.user.role === 'admin') {
return next();
}
const resourceId = req.params[paramName];
if (!resourceId) {
return res.status(400).json({
success: false,
error: {
message: `Chýba parameter ${paramName}`,
statusCode: 400,
},
});
}
try {
const hasAccess = await hasAccessToResource(resourceType, req.user.id, resourceId);
if (!hasAccess) {
return res.status(403).json({
success: false,
error: {
message: 'Nemáte prístup k tomuto zdroju',
statusCode: 403,
},
});
}
next();
} catch (error) {
console.error('Resource access check error:', error);
return res.status(500).json({
success: false,
error: {
message: 'Chyba pri overovaní prístupu',
statusCode: 500,
},
});
}
};
};
/**
* Helper funkcie pre bežné prípady
*/
export const checkCompanyAccess = checkResourceAccess('company', 'companyId');
export const checkProjectAccess = checkResourceAccess('project', 'projectId');
export const checkTodoAccess = checkResourceAccess('todo', 'todoId');
/**
* Získa zoznam resource IDs ku ktorým má user prístup
* Užitočné pre filtrovanie v service vrstvách
* @param {string} resourceType - Typ resource ('company', 'project')
* @param {string} userId - ID používateľa
* @returns {Promise<string[]>} - Zoznam resource IDs
*/
export const getAccessibleResourceIds = async (resourceType, userId) => {
const config = resourceConfig[resourceType];
if (!config) {
throw new Error(`Unknown resource type: ${resourceType}`);
}
const { table, resourceIdColumn } = config;
const assignments = await db
.select({ resourceId: table[resourceIdColumn] })
.from(table)
.where(eq(table.userId, userId));
return assignments.map(a => a.resourceId);
};
/**
* Skontroluje prístup a vráti boolean (pre použitie v services)
* @param {string} resourceType
* @param {string} userId
* @param {string} resourceId
* @param {string} userRole
* @returns {Promise<boolean>}
*/
export const canAccessResource = async (resourceType, userId, resourceId, userRole) => {
if (userRole === 'admin') return true;
return hasAccessToResource(resourceType, userId, resourceId);
};

View File

@@ -2,6 +2,7 @@ import express from 'express';
import * as companyController from '../controllers/company.controller.js';
import { authenticate } from '../middlewares/auth/authMiddleware.js';
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
import { checkCompanyAccess } from '../middlewares/auth/resourceAccessMiddleware.js';
import { validateBody, validateParams } from '../middlewares/security/validateInput.js';
import { createCompanySchema, updateCompanySchema, createCompanyReminderSchema, updateCompanyReminderSchema } from '../validators/crm.validators.js';
import { z } from 'zod';
@@ -23,6 +24,7 @@ router.get('/email-unread', companyController.getCompanyUnreadCounts);
router.get(
'/:companyId/email-threads',
validateParams(z.object({ companyId: z.string().uuid() })),
checkCompanyAccess,
companyController.getCompanyEmailThreads
);
@@ -37,6 +39,7 @@ router.get('/', companyController.getAllCompanies);
router.get(
'/:companyId',
validateParams(z.object({ companyId: z.string().uuid() })),
checkCompanyAccess,
companyController.getCompanyById
);
@@ -69,6 +72,7 @@ router.delete(
router.get(
'/:companyId/notes',
validateParams(z.object({ companyId: z.string().uuid() })),
checkCompanyAccess,
companyController.getCompanyNotes
);
@@ -109,6 +113,7 @@ router.delete(
router.get(
'/:companyId/reminders',
validateParams(z.object({ companyId: z.string().uuid() })),
checkCompanyAccess,
companyController.getCompanyReminders
);
@@ -141,4 +146,46 @@ router.delete(
companyController.deleteCompanyReminder
);
// Company Users (Team Management)
router.get(
'/:companyId/users',
validateParams(z.object({ companyId: z.string().uuid() })),
checkCompanyAccess,
companyController.getCompanyUsers
);
router.post(
'/:companyId/users',
requireAdmin,
validateParams(z.object({ companyId: z.string().uuid() })),
validateBody(z.object({
userId: z.string().uuid(),
role: z.string().optional(),
})),
companyController.assignUserToCompany
);
router.patch(
'/:companyId/users/:userId',
requireAdmin,
validateParams(z.object({
companyId: z.string().uuid(),
userId: z.string().uuid()
})),
validateBody(z.object({
role: z.string().optional(),
})),
companyController.updateUserRoleOnCompany
);
router.delete(
'/:companyId/users/:userId',
requireAdmin,
validateParams(z.object({
companyId: z.string().uuid(),
userId: z.string().uuid()
})),
companyController.removeUserFromCompany
);
export default router;

View File

@@ -2,6 +2,7 @@ import express from 'express';
import * as projectController from '../controllers/project.controller.js';
import { authenticate } from '../middlewares/auth/authMiddleware.js';
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
import { checkProjectAccess } from '../middlewares/auth/resourceAccessMiddleware.js';
import { validateBody, validateParams } from '../middlewares/security/validateInput.js';
import { createProjectSchema, updateProjectSchema } from '../validators/crm.validators.js';
import { z } from 'zod';
@@ -22,6 +23,7 @@ router.get('/', projectController.getAllProjects);
router.get(
'/:projectId',
validateParams(z.object({ projectId: z.string().uuid() })),
checkProjectAccess,
projectController.getProjectById
);
@@ -54,6 +56,7 @@ router.delete(
router.get(
'/:projectId/notes',
validateParams(z.object({ projectId: z.string().uuid() })),
checkProjectAccess,
projectController.getProjectNotes
);
@@ -96,6 +99,7 @@ router.delete(
router.get(
'/:projectId/users',
validateParams(z.object({ projectId: z.string().uuid() })),
checkProjectAccess,
projectController.getProjectUsers
);

View File

@@ -1,6 +1,8 @@
import express from 'express';
import * as todoController from '../controllers/todo.controller.js';
import { authenticate } from '../middlewares/auth/authMiddleware.js';
import { requireAdmin } from '../middlewares/auth/roleMiddleware.js';
import { checkTodoAccess } from '../middlewares/auth/resourceAccessMiddleware.js';
import { validateBody, validateParams } from '../middlewares/security/validateInput.js';
import { createTodoSchema, updateTodoSchema } from '../validators/crm.validators.js';
import { z } from 'zod';
@@ -21,27 +23,31 @@ router.get('/', todoController.getAllTodos);
router.get(
'/:todoId',
validateParams(z.object({ todoId: z.string().uuid() })),
checkTodoAccess,
todoController.getTodoById
);
// Create new todo
// Create new todo (admin only)
router.post(
'/',
requireAdmin,
validateBody(createTodoSchema),
todoController.createTodo
);
// Update todo
// Update todo (admin only)
router.patch(
'/:todoId',
requireAdmin,
validateParams(z.object({ todoId: z.string().uuid() })),
validateBody(updateTodoSchema),
todoController.updateTodo
);
// Delete todo
// Delete todo (admin only)
router.delete(
'/:todoId',
requireAdmin,
validateParams(z.object({ todoId: z.string().uuid() })),
todoController.deleteTodo
);
@@ -50,6 +56,7 @@ router.delete(
router.patch(
'/:todoId/toggle',
validateParams(z.object({ todoId: z.string().uuid() })),
checkTodoAccess,
todoController.toggleTodo
);

View File

@@ -1,7 +1,8 @@
import { db } from '../config/database.js';
import { companies, companyReminders } from '../db/schema.js';
import { eq, desc, sql, and, lte, gte, isNull, or } from 'drizzle-orm';
import { eq, desc, sql, and, lte, gte, isNull, or, inArray } from 'drizzle-orm';
import { NotFoundError, BadRequestError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
const ensureCompanyExists = async (companyId) => {
const [company] = await db
@@ -105,8 +106,17 @@ export const deleteReminder = async (companyId, reminderId) => {
return { success: true, message: 'Reminder bol odstránený' };
};
export const getReminderSummary = async () => {
const [row] = await db
export const getReminderSummary = async (userId = null, userRole = null) => {
// Pre membera filtruj len pristupne firmy
let accessibleCompanyIds = null;
if (userRole && userRole !== 'admin' && userId) {
accessibleCompanyIds = await getAccessibleResourceIds('company', userId);
if (accessibleCompanyIds.length === 0) {
return { total: 0, active: 0, completed: 0 };
}
}
let query = db
.select({
total: sql`COUNT(*)::int`,
active: sql`COUNT(*) FILTER (WHERE ${companyReminders.isChecked} = false)::int`,
@@ -114,6 +124,12 @@ export const getReminderSummary = async () => {
})
.from(companyReminders);
if (accessibleCompanyIds !== null) {
query = query.where(inArray(companyReminders.companyId, accessibleCompanyIds));
}
const [row] = await query;
return {
total: row?.total ?? 0,
active: row?.active ?? 0,
@@ -121,15 +137,30 @@ export const getReminderSummary = async () => {
};
};
export const getReminderCountsByCompany = async () => {
const rows = await db
export const getReminderCountsByCompany = async (userId = null, userRole = null) => {
// Pre membera filtruj len pristupne firmy
let accessibleCompanyIds = null;
if (userRole && userRole !== 'admin' && userId) {
accessibleCompanyIds = await getAccessibleResourceIds('company', userId);
if (accessibleCompanyIds.length === 0) {
return [];
}
}
let query = db
.select({
companyId: companyReminders.companyId,
total: sql`COUNT(*)::int`,
active: sql`COUNT(*) FILTER (WHERE ${companyReminders.isChecked} = false)::int`,
completed: sql`COUNT(*) FILTER (WHERE ${companyReminders.isChecked} = true)::int`,
})
.from(companyReminders)
.from(companyReminders);
if (accessibleCompanyIds !== null) {
query = query.where(inArray(companyReminders.companyId, accessibleCompanyIds));
}
const rows = await query
.groupBy(companyReminders.companyId)
.orderBy(desc(companyReminders.companyId));
@@ -140,12 +171,32 @@ export const getReminderCountsByCompany = async () => {
* Get upcoming reminders for dashboard
* Returns reminders due within the next 5 days that are not checked
* Includes company name for display
* For members: returns only reminders from companies they are assigned to
*/
export const getUpcomingReminders = async () => {
export const getUpcomingReminders = async (userId = null, userRole = null) => {
// Pre membera filtruj len pristupne firmy
let accessibleCompanyIds = null;
if (userRole && userRole !== 'admin' && userId) {
accessibleCompanyIds = await getAccessibleResourceIds('company', userId);
if (accessibleCompanyIds.length === 0) {
return [];
}
}
const now = new Date();
const fiveDaysFromNow = new Date();
fiveDaysFromNow.setDate(fiveDaysFromNow.getDate() + 5);
const conditions = [
eq(companyReminders.isChecked, false),
lte(companyReminders.dueDate, fiveDaysFromNow),
gte(companyReminders.dueDate, now)
];
if (accessibleCompanyIds !== null) {
conditions.push(inArray(companyReminders.companyId, accessibleCompanyIds));
}
const reminders = await db
.select({
id: companyReminders.id,
@@ -158,13 +209,7 @@ export const getUpcomingReminders = async () => {
})
.from(companyReminders)
.leftJoin(companies, eq(companyReminders.companyId, companies.id))
.where(
and(
eq(companyReminders.isChecked, false),
lte(companyReminders.dueDate, fiveDaysFromNow),
gte(companyReminders.dueDate, now)
)
)
.where(and(...conditions))
.orderBy(companyReminders.dueDate);
return reminders;

View File

@@ -1,17 +1,58 @@
import { db } from '../config/database.js';
import { companies, projects, todos, notes, companyReminders } from '../db/schema.js';
import { eq, desc, ilike, or, and } from 'drizzle-orm';
import { companies, projects, todos, notes, companyReminders, companyUsers, users } from '../db/schema.js';
import { eq, desc, ilike, or, and, inArray } from 'drizzle-orm';
import { NotFoundError, ConflictError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
/**
* Get all companies
* Optionally filter by search term
* Returns companies with creator info
* For members: returns only companies they are assigned to
*/
export const getAllCompanies = async (searchTerm = null) => {
let query = db.select().from(companies);
export const getAllCompanies = async (searchTerm = null, userId = null, userRole = null) => {
// Pre membera najprv ziskaj pristupne company IDs
let accessibleCompanyIds = null;
if (userRole && userRole !== 'admin' && userId) {
accessibleCompanyIds = await getAccessibleResourceIds('company', userId);
// Ak member nema pristup k ziadnym firmam, vrat prazdne pole
if (accessibleCompanyIds.length === 0) {
return [];
}
}
let query = db
.select({
id: companies.id,
name: companies.name,
description: companies.description,
address: companies.address,
city: companies.city,
country: companies.country,
phone: companies.phone,
email: companies.email,
website: companies.website,
isActive: companies.isActive,
createdBy: companies.createdBy,
createdAt: companies.createdAt,
updatedAt: companies.updatedAt,
creator: {
id: users.id,
username: users.username,
},
})
.from(companies)
.leftJoin(users, eq(companies.createdBy, users.id));
// Aplikuj filtrovanie pre membera
const conditions = [];
if (accessibleCompanyIds !== null) {
conditions.push(inArray(companies.id, accessibleCompanyIds));
}
if (searchTerm) {
query = query.where(
conditions.push(
or(
ilike(companies.name, `%${searchTerm}%`),
ilike(companies.email, `%${searchTerm}%`),
@@ -20,17 +61,41 @@ export const getAllCompanies = async (searchTerm = null) => {
);
}
if (conditions.length > 0) {
query = query.where(and(...conditions));
}
const result = await query.orderBy(desc(companies.createdAt));
return result;
};
/**
* Get company by ID
* Returns company with creator info
*/
export const getCompanyById = async (companyId) => {
const [company] = await db
.select()
.select({
id: companies.id,
name: companies.name,
description: companies.description,
address: companies.address,
city: companies.city,
country: companies.country,
phone: companies.phone,
email: companies.email,
website: companies.website,
isActive: companies.isActive,
createdBy: companies.createdBy,
createdAt: companies.createdAt,
updatedAt: companies.updatedAt,
creator: {
id: users.id,
username: users.username,
},
})
.from(companies)
.leftJoin(users, eq(companies.createdBy, users.id))
.where(eq(companies.id, companyId))
.limit(1);
@@ -169,3 +234,167 @@ export const getCompanyWithRelations = async (companyId) => {
reminders: companyReminderList,
};
};
/**
* Get all users assigned to a company
*/
export const getCompanyUsers = async (companyId) => {
await getCompanyById(companyId); // Verify company exists
const rawResults = await db
.select()
.from(companyUsers)
.leftJoin(users, eq(companyUsers.userId, users.id))
.where(eq(companyUsers.companyId, companyId))
.orderBy(desc(companyUsers.addedAt));
const assignedUsers = rawResults.map((row) => ({
id: row.company_users.id,
userId: row.company_users.userId,
role: row.company_users.role,
addedBy: row.company_users.addedBy,
addedAt: row.company_users.addedAt,
user: row.users ? {
id: row.users.id,
username: row.users.username,
email: row.users.email,
role: row.users.role,
} : null,
}));
return assignedUsers;
};
/**
* Assign user to company
*/
export const assignUserToCompany = async (companyId, userId, addedByUserId, role = null) => {
await getCompanyById(companyId); // Verify company exists
// Verify user exists
const [user] = await db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user) {
throw new NotFoundError('Používateľ nenájdený');
}
// Check if user is already assigned
const [existing] = await db
.select()
.from(companyUsers)
.where(and(eq(companyUsers.companyId, companyId), eq(companyUsers.userId, userId)))
.limit(1);
if (existing) {
throw new ConflictError('Používateľ je už priradený k firme');
}
// Assign user to company
const [assignment] = await db
.insert(companyUsers)
.values({
companyId,
userId,
role: role || null,
addedBy: addedByUserId,
})
.returning();
// Return with user details
const [row] = await db
.select()
.from(companyUsers)
.leftJoin(users, eq(companyUsers.userId, users.id))
.where(eq(companyUsers.id, assignment.id))
.limit(1);
return {
id: row.company_users.id,
userId: row.company_users.userId,
role: row.company_users.role,
addedBy: row.company_users.addedBy,
addedAt: row.company_users.addedAt,
user: row.users ? {
id: row.users.id,
username: row.users.username,
email: row.users.email,
role: row.users.role,
} : null,
};
};
/**
* Remove user from company
*/
export const removeUserFromCompany = async (companyId, userId) => {
await getCompanyById(companyId); // Verify company exists
// Check if user is assigned
const [existing] = await db
.select()
.from(companyUsers)
.where(and(eq(companyUsers.companyId, companyId), eq(companyUsers.userId, userId)))
.limit(1);
if (!existing) {
throw new NotFoundError('Používateľ nie je priradený k firme');
}
// Remove assignment
await db
.delete(companyUsers)
.where(and(eq(companyUsers.companyId, companyId), eq(companyUsers.userId, userId)));
return { success: true, message: 'Používateľ bol odstránený z firmy' };
};
/**
* Update user role on company
*/
export const updateUserRoleOnCompany = async (companyId, userId, role) => {
await getCompanyById(companyId); // Verify company exists
// Check if user is assigned
const [existing] = await db
.select()
.from(companyUsers)
.where(and(eq(companyUsers.companyId, companyId), eq(companyUsers.userId, userId)))
.limit(1);
if (!existing) {
throw new NotFoundError('Používateľ nie je priradený k firme');
}
// Update role
const [updated] = await db
.update(companyUsers)
.set({ role: role || null })
.where(and(eq(companyUsers.companyId, companyId), eq(companyUsers.userId, userId)))
.returning();
// Return with user details
const [row] = await db
.select()
.from(companyUsers)
.leftJoin(users, eq(companyUsers.userId, users.id))
.where(eq(companyUsers.id, updated.id))
.limit(1);
return {
id: row.company_users.id,
userId: row.company_users.userId,
role: row.company_users.role,
addedBy: row.company_users.addedBy,
addedAt: row.company_users.addedAt,
user: row.users ? {
id: row.users.id,
username: row.users.username,
email: row.users.email,
role: row.users.role,
} : null,
};
};

View File

@@ -1,16 +1,35 @@
import { db } from '../config/database.js';
import { notes, companies, projects, todos, contacts } from '../db/schema.js';
import { notes, companies, projects, todos, contacts, users } from '../db/schema.js';
import { eq, desc, ilike, or, and } from 'drizzle-orm';
import { NotFoundError } from '../utils/errors.js';
/**
* Get all notes
* Optionally filter by search, company, project, todo, or contact
* Returns notes with creator info
*/
export const getAllNotes = async (filters = {}) => {
const { searchTerm, companyId, projectId, todoId, contactId } = filters;
let query = db.select().from(notes);
let query = db
.select({
id: notes.id,
title: notes.title,
content: notes.content,
companyId: notes.companyId,
projectId: notes.projectId,
todoId: notes.todoId,
contactId: notes.contactId,
createdBy: notes.createdBy,
createdAt: notes.createdAt,
updatedAt: notes.updatedAt,
creator: {
id: users.id,
username: users.username,
},
})
.from(notes)
.leftJoin(users, eq(notes.createdBy, users.id));
const conditions = [];
@@ -227,22 +246,56 @@ export const deleteNote = async (noteId) => {
/**
* Get notes by company ID
* Returns notes with creator info
*/
export const getNotesByCompanyId = async (companyId) => {
return await db
.select()
.select({
id: notes.id,
title: notes.title,
content: notes.content,
companyId: notes.companyId,
projectId: notes.projectId,
todoId: notes.todoId,
contactId: notes.contactId,
createdBy: notes.createdBy,
createdAt: notes.createdAt,
updatedAt: notes.updatedAt,
creator: {
id: users.id,
username: users.username,
},
})
.from(notes)
.leftJoin(users, eq(notes.createdBy, users.id))
.where(eq(notes.companyId, companyId))
.orderBy(desc(notes.createdAt));
};
/**
* Get notes by project ID
* Returns notes with creator info
*/
export const getNotesByProjectId = async (projectId) => {
return await db
.select()
.select({
id: notes.id,
title: notes.title,
content: notes.content,
companyId: notes.companyId,
projectId: notes.projectId,
todoId: notes.todoId,
contactId: notes.contactId,
createdBy: notes.createdBy,
createdAt: notes.createdAt,
updatedAt: notes.updatedAt,
creator: {
id: users.id,
username: users.username,
},
})
.from(notes)
.leftJoin(users, eq(notes.createdBy, users.id))
.where(eq(notes.projectId, projectId))
.orderBy(desc(notes.createdAt));
};

View File

@@ -1,17 +1,56 @@
import { db } from '../config/database.js';
import { projects, todos, notes, timesheets, companies, projectUsers, users } from '../db/schema.js';
import { eq, desc, ilike, or, and } from 'drizzle-orm';
import { eq, desc, ilike, or, and, inArray } from 'drizzle-orm';
import { NotFoundError, ConflictError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
/**
* Get all projects
* Optionally filter by search term or company
* Returns projects with creator info
* For members: returns only projects they are directly assigned to
*/
export const getAllProjects = async (searchTerm = null, companyId = null) => {
let query = db.select().from(projects);
export const getAllProjects = async (searchTerm = null, companyId = null, userId = null, userRole = null) => {
// Pre membera ziskaj pristupne project IDs
let accessibleProjectIds = null;
if (userRole && userRole !== 'admin' && userId) {
// Ziskaj projekty kde je user priamo priradeny
accessibleProjectIds = await getAccessibleResourceIds('project', userId);
// Ak member nema pristup k ziadnym projektom, vrat prazdne pole
if (accessibleProjectIds.length === 0) {
return [];
}
}
let query = db
.select({
id: projects.id,
name: projects.name,
description: projects.description,
companyId: projects.companyId,
status: projects.status,
startDate: projects.startDate,
endDate: projects.endDate,
createdBy: projects.createdBy,
createdAt: projects.createdAt,
updatedAt: projects.updatedAt,
creator: {
id: users.id,
username: users.username,
},
})
.from(projects)
.leftJoin(users, eq(projects.createdBy, users.id));
const conditions = [];
// Filtrovanie pre membera - projekt je pristupny iba ak je user priamo priradeny
if (accessibleProjectIds !== null) {
conditions.push(inArray(projects.id, accessibleProjectIds));
}
if (searchTerm) {
conditions.push(
or(
@@ -35,11 +74,28 @@ export const getAllProjects = async (searchTerm = null, companyId = null) => {
/**
* Get project by ID
* Returns project with creator info
*/
export const getProjectById = async (projectId) => {
const [project] = await db
.select()
.select({
id: projects.id,
name: projects.name,
description: projects.description,
companyId: projects.companyId,
status: projects.status,
startDate: projects.startDate,
endDate: projects.endDate,
createdBy: projects.createdBy,
createdAt: projects.createdAt,
updatedAt: projects.updatedAt,
creator: {
id: users.id,
username: users.username,
},
})
.from(projects)
.leftJoin(users, eq(projects.createdBy, users.id))
.where(eq(projects.id, projectId))
.limit(1);

View File

@@ -2,106 +2,50 @@ import { db } from '../config/database.js';
import { todos, todoUsers, notes, projects, companies, users } from '../db/schema.js';
import { eq, desc, ilike, or, and, inArray } from 'drizzle-orm';
import { NotFoundError } from '../utils/errors.js';
import { getAccessibleResourceIds } from '../middlewares/auth/resourceAccessMiddleware.js';
/**
* Get all todos
* Optionally filter by search, project, company, assigned user, or status
* For members: returns only todos they are assigned to
*/
export const getAllTodos = async (filters = {}) => {
export const getAllTodos = async (filters = {}, userId = null, userRole = null) => {
const { searchTerm, projectId, companyId, assignedTo, status, priority } = filters;
// If filtering by assignedTo, we need to join with todo_users
// Pre membera filtruj len todos kde je priradeny
let accessibleTodoIds = null;
if (userRole && userRole !== 'admin' && userId) {
accessibleTodoIds = await getAccessibleResourceIds('todo', userId);
// Ak member nema pristup k ziadnym todos, vrat prazdne pole
if (accessibleTodoIds.length === 0) {
return [];
}
}
// Build query conditions
const conditions = [];
// Member access filter - only todos they are assigned to
if (accessibleTodoIds !== null) {
conditions.push(inArray(todos.id, accessibleTodoIds));
}
// If filtering by assignedTo, we need additional filter
if (assignedTo) {
const todoIdsWithUser = await db
.select({ todoId: todoUsers.todoId })
.from(todoUsers)
.where(eq(todoUsers.userId, assignedTo));
const todoIds = todoIdsWithUser.map((row) => row.todoId);
const assignedTodoIds = todoIdsWithUser.map((row) => row.todoId);
if (todoIds.length === 0) {
if (assignedTodoIds.length === 0) {
return [];
}
let query = db.select().from(todos).where(inArray(todos.id, todoIds));
const conditions = [];
if (searchTerm) {
conditions.push(
or(
ilike(todos.title, `%${searchTerm}%`),
ilike(todos.description, `%${searchTerm}%`)
)
);
}
if (projectId) {
conditions.push(eq(todos.projectId, projectId));
}
if (companyId) {
conditions.push(eq(todos.companyId, companyId));
}
if (status) {
conditions.push(eq(todos.status, status));
}
if (priority) {
conditions.push(eq(todos.priority, priority));
}
if (conditions.length > 0) {
query = query.where(and(...conditions));
}
const result = await query.orderBy(desc(todos.createdAt));
// Fetch assigned users for all todos
if (result.length > 0) {
const todoIds = result.map(todo => todo.id);
const assignedUsersData = await db
.select({
todoId: todoUsers.todoId,
userId: users.id,
username: users.username,
firstName: users.firstName,
lastName: users.lastName,
})
.from(todoUsers)
.innerJoin(users, eq(todoUsers.userId, users.id))
.where(inArray(todoUsers.todoId, todoIds));
// Group assigned users by todoId
const usersByTodoId = {};
for (const row of assignedUsersData) {
if (!usersByTodoId[row.todoId]) {
usersByTodoId[row.todoId] = [];
}
usersByTodoId[row.todoId].push({
id: row.userId,
username: row.username,
firstName: row.firstName,
lastName: row.lastName,
});
}
// Attach assigned users to each todo
return result.map(todo => ({
...todo,
assignedUsers: usersByTodoId[todo.id] || [],
}));
}
return result;
conditions.push(inArray(todos.id, assignedTodoIds));
}
// No assignedTo filter - simple query
let query = db.select().from(todos);
const conditions = [];
if (searchTerm) {
conditions.push(
or(
@@ -127,6 +71,8 @@ export const getAllTodos = async (filters = {}) => {
conditions.push(eq(todos.priority, priority));
}
let query = db.select().from(todos);
if (conditions.length > 0) {
query = query.where(and(...conditions));
}
@@ -135,18 +81,18 @@ export const getAllTodos = async (filters = {}) => {
// Fetch assigned users for all todos
if (result.length > 0) {
const todoIds = result.map(todo => todo.id);
const resultTodoIds = result.map(todo => todo.id);
const assignedUsersData = await db
.select({
todoId: todoUsers.todoId,
userId: users.id,
odUserId: users.id,
username: users.username,
firstName: users.firstName,
lastName: users.lastName,
})
.from(todoUsers)
.innerJoin(users, eq(todoUsers.userId, users.id))
.where(inArray(todoUsers.todoId, todoIds));
.where(inArray(todoUsers.todoId, resultTodoIds));
// Group assigned users by todoId
const usersByTodoId = {};
@@ -155,7 +101,7 @@ export const getAllTodos = async (filters = {}) => {
usersByTodoId[row.todoId] = [];
}
usersByTodoId[row.todoId].push({
id: row.userId,
id: row.odUserId,
username: row.username,
firstName: row.firstName,
lastName: row.lastName,