- Split 753-line jmap.service.js into modular structure: - jmap/config.js: JMAP configuration functions - jmap/client.js: Base JMAP requests (jmapRequest, getMailboxes, getIdentities) - jmap/discovery.js: Contact discovery from JMAP - jmap/search.js: Email search functionality - jmap/sync.js: Email synchronization - jmap/operations.js: Email operations (markAsRead, sendEmail) - jmap/index.js: Re-exports for backward compatibility - Update all imports across codebase to use new module structure - Translate remaining English error/log messages to Slovak: - email.service.js: JMAP validation messages - admin.service.js: Email account creation error - audit.service.js: Audit event logging error - timesheet.service.js: File deletion error - database.js: Database error message 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
198 lines
5.4 KiB
JavaScript
198 lines
5.4 KiB
JavaScript
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import { db } from '../config/database.js';
|
|
import { timesheets, users } from '../db/schema.js';
|
|
import { and, desc, eq } from 'drizzle-orm';
|
|
import { BadRequestError, ForbiddenError, NotFoundError } from '../utils/errors.js';
|
|
|
|
const ALLOWED_MIME_TYPES = [
|
|
'application/pdf',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'application/vnd.ms-excel',
|
|
];
|
|
|
|
const BASE_UPLOAD_DIR = path.join(process.cwd(), 'uploads', 'timesheets');
|
|
|
|
const sanitizeTimesheet = (record) => ({
|
|
id: record.id,
|
|
fileName: record.fileName,
|
|
fileType: record.fileType,
|
|
fileSize: record.fileSize,
|
|
year: record.year,
|
|
month: record.month,
|
|
isGenerated: record.isGenerated,
|
|
uploadedAt: record.uploadedAt,
|
|
});
|
|
|
|
const detectFileType = (mimeType) => {
|
|
if (!ALLOWED_MIME_TYPES.includes(mimeType)) {
|
|
throw new BadRequestError('Neplatný typ súboru. Povolené sú iba PDF a Excel súbory.');
|
|
}
|
|
return mimeType.includes('sheet') || mimeType.includes('excel') ? 'xlsx' : 'pdf';
|
|
};
|
|
|
|
const buildDestinationPath = (userId, year, month, originalName) => {
|
|
const ext = path.extname(originalName);
|
|
const name = path.basename(originalName, ext);
|
|
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
|
|
const filename = `${name}-${uniqueSuffix}${ext}`;
|
|
const folder = path.join(BASE_UPLOAD_DIR, userId, year.toString(), month.toString());
|
|
const filePath = path.join(folder, filename);
|
|
|
|
return { folder, filename, filePath };
|
|
};
|
|
|
|
const ensureTimesheetExists = async (timesheetId) => {
|
|
const [timesheet] = await db
|
|
.select()
|
|
.from(timesheets)
|
|
.where(eq(timesheets.id, timesheetId))
|
|
.limit(1);
|
|
|
|
if (!timesheet) {
|
|
throw new NotFoundError('Timesheet nenájdený');
|
|
}
|
|
|
|
return timesheet;
|
|
};
|
|
|
|
const assertAccess = (timesheet, { userId, role }) => {
|
|
if (role !== 'admin' && timesheet.userId !== userId) {
|
|
throw new ForbiddenError('Nemáte oprávnenie k tomuto timesheetu');
|
|
}
|
|
};
|
|
|
|
const safeUnlink = async (filePath) => {
|
|
if (!filePath) return;
|
|
try {
|
|
await fs.unlink(filePath);
|
|
} catch (error) {
|
|
// Keep server responsive even if cleanup fails
|
|
console.error('Nepodarilo sa zmazať súbor:', error);
|
|
}
|
|
};
|
|
|
|
export const uploadTimesheet = async ({ userId, year, month, file }) => {
|
|
if (!file) {
|
|
throw new BadRequestError('Súbor nebol nahraný');
|
|
}
|
|
|
|
const parsedYear = parseInt(year);
|
|
const parsedMonth = parseInt(month);
|
|
const fileType = detectFileType(file.mimetype);
|
|
const { folder, filename, filePath } = buildDestinationPath(userId, parsedYear, parsedMonth, file.originalname);
|
|
|
|
await fs.mkdir(folder, { recursive: true });
|
|
|
|
try {
|
|
await fs.writeFile(filePath, file.buffer);
|
|
|
|
const [newTimesheet] = await db
|
|
.insert(timesheets)
|
|
.values({
|
|
userId,
|
|
fileName: file.originalname,
|
|
filePath,
|
|
fileType,
|
|
fileSize: file.size,
|
|
year: parsedYear,
|
|
month: parsedMonth,
|
|
isGenerated: false,
|
|
})
|
|
.returning();
|
|
|
|
return sanitizeTimesheet(newTimesheet);
|
|
} catch (error) {
|
|
await safeUnlink(filePath);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const getTimesheetsForUser = async (userId, { year, month } = {}) => {
|
|
const conditions = [eq(timesheets.userId, userId)];
|
|
|
|
if (year) {
|
|
conditions.push(eq(timesheets.year, parseInt(year)));
|
|
}
|
|
|
|
if (month) {
|
|
conditions.push(eq(timesheets.month, parseInt(month)));
|
|
}
|
|
|
|
return db
|
|
.select({
|
|
id: timesheets.id,
|
|
fileName: timesheets.fileName,
|
|
fileType: timesheets.fileType,
|
|
fileSize: timesheets.fileSize,
|
|
year: timesheets.year,
|
|
month: timesheets.month,
|
|
isGenerated: timesheets.isGenerated,
|
|
uploadedAt: timesheets.uploadedAt,
|
|
})
|
|
.from(timesheets)
|
|
.where(and(...conditions))
|
|
.orderBy(desc(timesheets.uploadedAt));
|
|
};
|
|
|
|
export const getAllTimesheets = async ({ userId, year, month } = {}) => {
|
|
const conditions = [];
|
|
|
|
if (userId) {
|
|
conditions.push(eq(timesheets.userId, userId));
|
|
}
|
|
|
|
if (year) {
|
|
conditions.push(eq(timesheets.year, parseInt(year)));
|
|
}
|
|
|
|
if (month) {
|
|
conditions.push(eq(timesheets.month, parseInt(month)));
|
|
}
|
|
|
|
return db
|
|
.select({
|
|
id: timesheets.id,
|
|
fileName: timesheets.fileName,
|
|
fileType: timesheets.fileType,
|
|
fileSize: timesheets.fileSize,
|
|
year: timesheets.year,
|
|
month: timesheets.month,
|
|
isGenerated: timesheets.isGenerated,
|
|
uploadedAt: timesheets.uploadedAt,
|
|
userId: timesheets.userId,
|
|
username: users.username,
|
|
firstName: users.firstName,
|
|
lastName: users.lastName,
|
|
})
|
|
.from(timesheets)
|
|
.leftJoin(users, eq(timesheets.userId, users.id))
|
|
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
.orderBy(desc(timesheets.uploadedAt));
|
|
};
|
|
|
|
export const getDownloadInfo = async (timesheetId, { userId, role }) => {
|
|
const timesheet = await ensureTimesheetExists(timesheetId);
|
|
assertAccess(timesheet, { userId, role });
|
|
|
|
try {
|
|
await fs.access(timesheet.filePath);
|
|
} catch {
|
|
throw new NotFoundError('Súbor nebol nájdený na serveri');
|
|
}
|
|
|
|
return {
|
|
timesheet,
|
|
filePath: timesheet.filePath,
|
|
fileName: timesheet.fileName,
|
|
};
|
|
};
|
|
|
|
export const deleteTimesheet = async (timesheetId, { userId, role }) => {
|
|
const timesheet = await ensureTimesheetExists(timesheetId);
|
|
assertAccess(timesheet, { userId, role });
|
|
|
|
await safeUnlink(timesheet.filePath);
|
|
await db.delete(timesheets).where(eq(timesheets.id, timesheetId));
|
|
};
|