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 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ import noteRoutes from './routes/note.routes.js';
|
|||||||
import auditRoutes from './routes/audit.routes.js';
|
import auditRoutes from './routes/audit.routes.js';
|
||||||
import eventRoutes from './routes/event.routes.js';
|
import eventRoutes from './routes/event.routes.js';
|
||||||
import messageRoutes from './routes/message.routes.js';
|
import messageRoutes from './routes/message.routes.js';
|
||||||
|
import userRoutes from './routes/user.routes.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -122,6 +123,7 @@ app.use('/api/notes', noteRoutes);
|
|||||||
app.use('/api/audit-logs', auditRoutes);
|
app.use('/api/audit-logs', auditRoutes);
|
||||||
app.use('/api/events', eventRoutes);
|
app.use('/api/events', eventRoutes);
|
||||||
app.use('/api/messages', messageRoutes);
|
app.use('/api/messages', messageRoutes);
|
||||||
|
app.use('/api/users', userRoutes);
|
||||||
|
|
||||||
// Basic route
|
// Basic route
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
|
|||||||
@@ -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
|
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'set null' }), // voliteľná väzba na firmu
|
||||||
firstName: text('first_name').notNull(),
|
firstName: text('first_name').notNull(),
|
||||||
lastName: text('last_name'),
|
lastName: text('last_name'),
|
||||||
phone: text('phone').notNull(),
|
phone: text('phone'), // optional
|
||||||
email: text('email').notNull(),
|
email: text('email').notNull(),
|
||||||
secondaryEmail: text('secondary_email'),
|
secondaryEmail: text('secondary_email'),
|
||||||
description: text('description'), // popis kontaktu
|
description: text('description'), // popis kontaktu
|
||||||
|
|||||||
@@ -44,10 +44,9 @@ router.get(
|
|||||||
companyController.getCompanyById
|
companyController.getCompanyById
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create new company (admin only)
|
// Create new company (any authenticated user)
|
||||||
router.post(
|
router.post(
|
||||||
'/',
|
'/',
|
||||||
requireAdmin,
|
|
||||||
validateBody(createCompanySchema),
|
validateBody(createCompanySchema),
|
||||||
companyController.createCompany
|
companyController.createCompany
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const router = express.Router()
|
|||||||
const createContactSchema = z.object({
|
const createContactSchema = z.object({
|
||||||
firstName: z.string().min(1, 'Meno je povinné'),
|
firstName: z.string().min(1, 'Meno je povinné'),
|
||||||
lastName: z.string().optional(),
|
lastName: z.string().optional(),
|
||||||
phone: z.string().min(3, 'Telefón je povinný'),
|
phone: z.string().optional().nullable(),
|
||||||
email: z.string().email('Neplatný email'),
|
email: z.string().email('Neplatný email'),
|
||||||
secondaryEmail: z.union([z.string().email('Neplatný email'), z.literal('')]).optional(),
|
secondaryEmail: z.union([z.string().email('Neplatný email'), z.literal('')]).optional(),
|
||||||
companyId: z.union([z.string().uuid(), z.literal(''), z.null()]).optional(),
|
companyId: z.union([z.string().uuid(), z.literal(''), z.null()]).optional(),
|
||||||
|
|||||||
@@ -27,10 +27,9 @@ router.get(
|
|||||||
projectController.getProjectById
|
projectController.getProjectById
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create new project (admin only)
|
// Create new project (any authenticated user)
|
||||||
router.post(
|
router.post(
|
||||||
'/',
|
'/',
|
||||||
requireAdmin,
|
|
||||||
validateBody(createProjectSchema),
|
validateBody(createProjectSchema),
|
||||||
projectController.createProject
|
projectController.createProject
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,19 +27,18 @@ router.get(
|
|||||||
todoController.getTodoById
|
todoController.getTodoById
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create new todo (admin only)
|
// Create new todo (any authenticated user)
|
||||||
router.post(
|
router.post(
|
||||||
'/',
|
'/',
|
||||||
requireAdmin,
|
|
||||||
validateBody(createTodoSchema),
|
validateBody(createTodoSchema),
|
||||||
todoController.createTodo
|
todoController.createTodo
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update todo (admin only)
|
// Update todo (user must have access to the todo)
|
||||||
router.patch(
|
router.patch(
|
||||||
'/:todoId',
|
'/:todoId',
|
||||||
requireAdmin,
|
|
||||||
validateParams(z.object({ todoId: z.string().uuid() })),
|
validateParams(z.object({ todoId: z.string().uuid() })),
|
||||||
|
checkTodoAccess,
|
||||||
validateBody(updateTodoSchema),
|
validateBody(updateTodoSchema),
|
||||||
todoController.updateTodo
|
todoController.updateTodo
|
||||||
);
|
);
|
||||||
|
|||||||
34
src/routes/user.routes.js
Normal file
34
src/routes/user.routes.js
Normal file
@@ -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;
|
||||||
@@ -139,6 +139,14 @@ export const createCompany = async (userId, data) => {
|
|||||||
})
|
})
|
||||||
.returning();
|
.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;
|
return newCompany;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const createPersonalContact = async (userId, contact) => {
|
|||||||
companyId: contact.companyId || null,
|
companyId: contact.companyId || null,
|
||||||
firstName: contact.firstName,
|
firstName: contact.firstName,
|
||||||
lastName: contact.lastName || null,
|
lastName: contact.lastName || null,
|
||||||
phone: contact.phone,
|
phone: contact.phone || null,
|
||||||
email: contact.email,
|
email: contact.email,
|
||||||
secondaryEmail: contact.secondaryEmail || null,
|
secondaryEmail: contact.secondaryEmail || null,
|
||||||
description: contact.description || null,
|
description: contact.description || null,
|
||||||
|
|||||||
@@ -138,6 +138,14 @@ export const createProject = async (userId, data) => {
|
|||||||
})
|
})
|
||||||
.returning();
|
.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;
|
return newProject;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,16 @@ export const createTodo = async (userId, data) => {
|
|||||||
await db.insert(todoUsers).values(todoUserInserts);
|
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;
|
return newTodo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user