Add Timesheets API with file upload and role-based access

Backend Features:
- Timesheets database table (id, userId, fileName, filePath, fileType, fileSize, year, month, timestamps)
- File upload with multer (memory storage, 10MB limit, PDF/Excel validation)
- Structured file storage: uploads/timesheets/{userId}/{year}/{month}/
- RESTful API endpoints:
  * POST /api/timesheets/upload - Upload timesheet
  * GET /api/timesheets/my - Get user's timesheets (with filters)
  * GET /api/timesheets/all - Get all timesheets (admin only)
  * GET /api/timesheets/:id/download - Download file
  * DELETE /api/timesheets/:id - Delete timesheet
- Role-based permissions: users access own files, admins access all
- Proper error handling and file cleanup on errors
- Database migration for timesheets table

Technical:
- Uses req.user.role for permission checks
- Automatic directory creation for user/year/month structure
- Blob URL cleanup and proper file handling
- Integration with existing auth middleware

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
richardtekula
2025-11-21 08:35:30 +01:00
parent 05be898259
commit bb851639b8
27 changed files with 2847 additions and 532 deletions

View File

@@ -1,10 +1,10 @@
import { db } from '../config/database.js';
import { users } from '../db/schema.js';
import { eq } from 'drizzle-orm';
import { hashPassword, generateTempPassword, encryptPassword } from '../utils/password.js';
import { hashPassword, generateTempPassword } from '../utils/password.js';
import { logUserCreation, logRoleChange } from '../services/audit.service.js';
import { formatErrorResponse, ConflictError, NotFoundError } from '../utils/errors.js';
import { validateJmapCredentials } from '../services/email.service.js';
import * as emailAccountService from '../services/email-account.service.js';
/**
* Vytvorenie nového usera s automatic temporary password (admin only)
@@ -33,28 +33,11 @@ export const createUser = async (req, res) => {
const tempPassword = generateTempPassword(12);
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
const [newUser] = await db
.insert(users)
.values({
username,
email: email || null,
emailPassword: encryptedEmailPassword,
jmapAccountId,
tempPassword: hashedTempPassword,
role: 'member', // Vždy member, nie admin
firstName: firstName || null,
@@ -63,6 +46,33 @@ export const createUser = async (req, res) => {
})
.returning();
// Ak sú poskytnuté email credentials, vytvor email account (many-to-many)
let emailAccountCreated = false;
let emailAccountData = null;
if (email && emailPassword) {
try {
// Použij emailAccountService ktorý automaticky vytvorí many-to-many link
const newEmailAccount = await emailAccountService.createEmailAccount(
newUser.id,
email,
emailPassword
);
emailAccountCreated = true;
emailAccountData = {
id: newEmailAccount.id,
email: newEmailAccount.email,
jmapAccountId: newEmailAccount.jmapAccountId,
shared: newEmailAccount.shared,
};
} catch (emailError) {
// Email account sa nepodarilo vytvoriť, ale user bol vytvorený
// Admin môže pridať email account neskôr
console.error('Failed to create email account:', emailError);
}
}
// Log user creation
await logUserCreation(adminId, newUser.id, username, 'member', ipAddress, userAgent);
@@ -72,17 +82,18 @@ export const createUser = async (req, res) => {
user: {
id: newUser.id,
username: newUser.username,
email: newUser.email,
firstName: newUser.firstName,
lastName: newUser.lastName,
role: newUser.role,
jmapAccountId: newUser.jmapAccountId,
emailSetup: !!newUser.jmapAccountId,
emailSetup: emailAccountCreated,
emailAccount: emailAccountData,
tempPassword: tempPassword, // Vráti plain text password pre admina aby ho mohol poslať userovi
},
},
message: newUser.jmapAccountId
? 'Používateľ úspešne vytvorený s emailovým účtom.'
message: emailAccountCreated
? emailAccountData.shared
? 'Používateľ vytvorený a pripojený k existujúcemu zdieľanému email účtu.'
: 'Používateľ úspešne vytvorený s novým emailovým účtom.'
: 'Používateľ úspešne vytvorený. Email môže byť nastavený neskôr.',
});
} catch (error) {
@@ -101,7 +112,6 @@ export const getAllUsers = async (req, res) => {
.select({
id: users.id,
username: users.username,
email: users.email,
firstName: users.firstName,
lastName: users.lastName,
role: users.role,
@@ -136,7 +146,6 @@ export const getUser = async (req, res) => {
.select({
id: users.id,
username: users.username,
email: users.email,
firstName: users.firstName,
lastName: users.lastName,
role: users.role,
@@ -153,9 +162,17 @@ export const getUser = async (req, res) => {
throw new NotFoundError('Používateľ nenájdený');
}
// Get user's email accounts (cez many-to-many)
const userEmailAccounts = await emailAccountService.getUserEmailAccounts(userId);
res.status(200).json({
success: true,
data: { user },
data: {
user: {
...user,
emailAccounts: userEmailAccounts,
},
},
});
} catch (error) {
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');