From 47b68e672b0ec6314e4088a62322c10084339fa4 Mon Sep 17 00:00:00 2001 From: richardtekula Date: Fri, 16 Jan 2026 07:08:42 +0100 Subject: [PATCH] feat: Member permissions, optional phone, public users endpoint - Allow members to create todos, companies, projects - Auto-assign creator to resources (companyUsers, projectUsers, todoUsers) - Add public /api/users endpoint for all authenticated users - Make phone field optional in personal contacts (schema + validation) - Update todo routes to use checkTodoAccess for updates Co-Authored-By: Claude Opus 4.5 --- src/app.js | 2 ++ src/db/schema.js | 2 +- src/routes/company.routes.js | 3 +-- src/routes/personal-contact.routes.js | 2 +- src/routes/project.routes.js | 3 +-- src/routes/todo.routes.js | 7 +++-- src/routes/user.routes.js | 34 ++++++++++++++++++++++++ src/services/company.service.js | 8 ++++++ src/services/personal-contact.service.js | 2 +- src/services/project.service.js | 8 ++++++ src/services/todo.service.js | 10 +++++++ 11 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 src/routes/user.routes.js diff --git a/src/app.js b/src/app.js index bd1b016..30a334c 100644 --- a/src/app.js +++ b/src/app.js @@ -28,6 +28,7 @@ import noteRoutes from './routes/note.routes.js'; import auditRoutes from './routes/audit.routes.js'; import eventRoutes from './routes/event.routes.js'; import messageRoutes from './routes/message.routes.js'; +import userRoutes from './routes/user.routes.js'; const app = express(); @@ -122,6 +123,7 @@ app.use('/api/notes', noteRoutes); app.use('/api/audit-logs', auditRoutes); app.use('/api/events', eventRoutes); app.use('/api/messages', messageRoutes); +app.use('/api/users', userRoutes); // Basic route app.get('/', (req, res) => { diff --git a/src/db/schema.js b/src/db/schema.js index fc38db3..fabaaa7 100644 --- a/src/db/schema.js +++ b/src/db/schema.js @@ -87,7 +87,7 @@ export const personalContacts = pgTable('personal_contacts', { companyId: uuid('company_id').references(() => companies.id, { onDelete: 'set null' }), // voliteľná väzba na firmu firstName: text('first_name').notNull(), lastName: text('last_name'), - phone: text('phone').notNull(), + phone: text('phone'), // optional email: text('email').notNull(), secondaryEmail: text('secondary_email'), description: text('description'), // popis kontaktu diff --git a/src/routes/company.routes.js b/src/routes/company.routes.js index 3f0659c..89c35b5 100644 --- a/src/routes/company.routes.js +++ b/src/routes/company.routes.js @@ -44,10 +44,9 @@ router.get( companyController.getCompanyById ); -// Create new company (admin only) +// Create new company (any authenticated user) router.post( '/', - requireAdmin, validateBody(createCompanySchema), companyController.createCompany ); diff --git a/src/routes/personal-contact.routes.js b/src/routes/personal-contact.routes.js index fc1fccf..43cc1f0 100644 --- a/src/routes/personal-contact.routes.js +++ b/src/routes/personal-contact.routes.js @@ -9,7 +9,7 @@ const router = express.Router() const createContactSchema = z.object({ firstName: z.string().min(1, 'Meno je povinné'), lastName: z.string().optional(), - phone: z.string().min(3, 'Telefón je povinný'), + phone: z.string().optional().nullable(), email: z.string().email('Neplatný email'), secondaryEmail: z.union([z.string().email('Neplatný email'), z.literal('')]).optional(), companyId: z.union([z.string().uuid(), z.literal(''), z.null()]).optional(), diff --git a/src/routes/project.routes.js b/src/routes/project.routes.js index ef38d69..9683c79 100644 --- a/src/routes/project.routes.js +++ b/src/routes/project.routes.js @@ -27,10 +27,9 @@ router.get( projectController.getProjectById ); -// Create new project (admin only) +// Create new project (any authenticated user) router.post( '/', - requireAdmin, validateBody(createProjectSchema), projectController.createProject ); diff --git a/src/routes/todo.routes.js b/src/routes/todo.routes.js index 4caadc3..786b19f 100644 --- a/src/routes/todo.routes.js +++ b/src/routes/todo.routes.js @@ -27,19 +27,18 @@ router.get( todoController.getTodoById ); -// Create new todo (admin only) +// Create new todo (any authenticated user) router.post( '/', - requireAdmin, validateBody(createTodoSchema), todoController.createTodo ); -// Update todo (admin only) +// Update todo (user must have access to the todo) router.patch( '/:todoId', - requireAdmin, validateParams(z.object({ todoId: z.string().uuid() })), + checkTodoAccess, validateBody(updateTodoSchema), todoController.updateTodo ); diff --git a/src/routes/user.routes.js b/src/routes/user.routes.js new file mode 100644 index 0000000..7278b7c --- /dev/null +++ b/src/routes/user.routes.js @@ -0,0 +1,34 @@ +import express from 'express'; +import { authenticate } from '../middlewares/auth/authMiddleware.js'; +import { db } from '../config/database.js'; +import { users } from '../db/schema.js'; + +const router = express.Router(); + +// All user routes require authentication +router.use(authenticate); + +/** + * Get all users (basic info only - for dropdowns, assignment, etc.) + * Available to all authenticated users + */ +router.get('/', async (req, res, next) => { + try { + const allUsers = await db + .select({ + id: users.id, + username: users.username, + firstName: users.firstName, + lastName: users.lastName, + role: users.role, + }) + .from(users) + .orderBy(users.username); + + res.json(allUsers); + } catch (error) { + next(error); + } +}); + +export default router; diff --git a/src/services/company.service.js b/src/services/company.service.js index 7368907..65d807e 100644 --- a/src/services/company.service.js +++ b/src/services/company.service.js @@ -139,6 +139,14 @@ export const createCompany = async (userId, data) => { }) .returning(); + // Auto-assign the creator to the company so they can access it + await db.insert(companyUsers).values({ + companyId: newCompany.id, + userId: userId, + role: 'creator', + addedBy: userId, + }); + return newCompany; }; diff --git a/src/services/personal-contact.service.js b/src/services/personal-contact.service.js index 6897dde..4192868 100644 --- a/src/services/personal-contact.service.js +++ b/src/services/personal-contact.service.js @@ -67,7 +67,7 @@ export const createPersonalContact = async (userId, contact) => { companyId: contact.companyId || null, firstName: contact.firstName, lastName: contact.lastName || null, - phone: contact.phone, + phone: contact.phone || null, email: contact.email, secondaryEmail: contact.secondaryEmail || null, description: contact.description || null, diff --git a/src/services/project.service.js b/src/services/project.service.js index 545becd..21e158c 100644 --- a/src/services/project.service.js +++ b/src/services/project.service.js @@ -138,6 +138,14 @@ export const createProject = async (userId, data) => { }) .returning(); + // Auto-assign the creator to the project so they can access it + await db.insert(projectUsers).values({ + projectId: newProject.id, + userId: userId, + role: 'creator', + addedBy: userId, + }); + return newProject; }; diff --git a/src/services/todo.service.js b/src/services/todo.service.js index a85fa30..eb411fb 100644 --- a/src/services/todo.service.js +++ b/src/services/todo.service.js @@ -207,6 +207,16 @@ export const createTodo = async (userId, data) => { await db.insert(todoUsers).values(todoUserInserts); } + // Auto-assign the creator to the todo so they can access it (if not already assigned) + const creatorAlreadyAssigned = assignedUserIds && assignedUserIds.includes(userId); + if (!creatorAlreadyAssigned) { + await db.insert(todoUsers).values({ + todoId: newTodo.id, + userId: userId, + assignedBy: userId, + }); + } + return newTodo; };