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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user