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,39 +1,47 @@
|
||||
import { pgTable, text, timestamp, boolean, uuid, pgEnum } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, text, timestamp, boolean, uuid, pgEnum, unique, integer } from 'drizzle-orm/pg-core';
|
||||
|
||||
// Role enum
|
||||
export const roleEnum = pgEnum('role', ['admin', 'member']);
|
||||
|
||||
// Users table - hlavná tabuľka používateľov
|
||||
// Users table - používatelia systému
|
||||
export const users = pgTable('users', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
username: text('username').notNull().unique(),
|
||||
email: text('email').unique(),
|
||||
emailPassword: text('email_password'), // Heslo k emailovému účtu (encrypted)
|
||||
jmapAccountId: text('jmap_account_id'), // JMAP account ID z truemail
|
||||
firstName: text('first_name'),
|
||||
lastName: text('last_name'),
|
||||
password: text('password'), // bcrypt hash (null ak ešte nenastavené)
|
||||
tempPassword: text('temp_password'), // dočasné heslo (bcrypt hash)
|
||||
changedPassword: boolean('changed_password').default(false), // či si užívateľ zmenil heslo
|
||||
changedPassword: boolean('changed_password').default(false),
|
||||
role: roleEnum('role').default('member').notNull(),
|
||||
lastLogin: timestamp('last_login'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Email Accounts table - viacero emailových účtov pre jedného usera
|
||||
// Email Accounts table - emailové účty (môžu byť zdieľané medzi viacerými používateľmi)
|
||||
export const emailAccounts = pgTable('email_accounts', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
email: text('email').notNull(),
|
||||
email: text('email').notNull().unique(), // Email adresa
|
||||
emailPassword: text('email_password').notNull(), // Heslo k emailovému účtu (encrypted)
|
||||
jmapAccountId: text('jmap_account_id').notNull(), // JMAP account ID z truemail
|
||||
isPrimary: boolean('is_primary').default(false).notNull(), // primárny email účet
|
||||
isActive: boolean('is_active').default(true).notNull(), // či je účet aktívny
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// User Email Accounts - many-to-many medzi users a emailAccounts
|
||||
// Umožňuje zdieľať email účty medzi viacerými používateľmi
|
||||
export const userEmailAccounts = pgTable('user_email_accounts', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
emailAccountId: uuid('email_account_id').references(() => emailAccounts.id, { onDelete: 'cascade' }).notNull(),
|
||||
isPrimary: boolean('is_primary').default(false).notNull(), // primárny email účet pre daného usera
|
||||
addedAt: timestamp('added_at').defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
// Jeden user môže mať email account len raz
|
||||
userEmailUnique: unique('user_email_unique').on(table.userId, table.emailAccountId),
|
||||
}));
|
||||
|
||||
// Audit logs - kompletný audit trail všetkých akcií
|
||||
export const auditLogs = pgTable('audit_logs', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
@@ -50,23 +58,27 @@ export const auditLogs = pgTable('audit_logs', {
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Contacts table - ľudia s ktorými komunikujeme cez email
|
||||
// Contacts table - kontakty patriace k emailovému účtu
|
||||
// Kontakty sú zdieľané medzi všetkými používateľmi, ktorí majú prístup k danému email accountu
|
||||
export const contacts = pgTable('contacts', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
emailAccountId: uuid('email_account_id').references(() => emailAccounts.id, { onDelete: 'cascade' }).notNull(),
|
||||
email: text('email').notNull(),
|
||||
name: text('name'),
|
||||
notes: text('notes'),
|
||||
addedBy: uuid('added_by').references(() => users.id, { onDelete: 'set null' }), // kto pridal kontakt
|
||||
addedAt: timestamp('added_at').defaultNow().notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
}, (table) => ({
|
||||
// Unique constraint: jeden email môže byť len raz v rámci email accountu
|
||||
accountEmailUnique: unique('account_email_unique').on(table.emailAccountId, table.email),
|
||||
}));
|
||||
|
||||
// Emails table - uložené emaily z JMAP (iba pre pridané kontakty)
|
||||
// Emails table - uložené emaily z JMAP
|
||||
// Emaily patria k email accountu a sú zdieľané medzi všetkými používateľmi s prístupom
|
||||
export const emails = pgTable('emails', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
emailAccountId: uuid('email_account_id').references(() => emailAccounts.id, { onDelete: 'cascade' }).notNull(),
|
||||
contactId: uuid('contact_id').references(() => contacts.id, { onDelete: 'cascade' }),
|
||||
jmapId: text('jmap_id').unique(),
|
||||
@@ -78,7 +90,23 @@ export const emails = pgTable('emails', {
|
||||
subject: text('subject'),
|
||||
body: text('body'),
|
||||
isRead: boolean('is_read').default(false).notNull(),
|
||||
sentByUserId: uuid('sent_by_user_id').references(() => users.id, { onDelete: 'set null' }), // kto poslal odpoveď (null ak prijatý email)
|
||||
date: timestamp('date'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Timesheets table - nahrané timesheets od používateľov
|
||||
export const timesheets = pgTable('timesheets', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), // kto nahral timesheet
|
||||
fileName: text('file_name').notNull(), // originálny názov súboru
|
||||
filePath: text('file_path').notNull(), // cesta k súboru na serveri
|
||||
fileType: text('file_type').notNull(), // 'pdf' alebo 'xlsx'
|
||||
fileSize: integer('file_size').notNull(), // veľkosť súboru v bytoch
|
||||
year: integer('year').notNull(), // rok (napr. 2024)
|
||||
month: integer('month').notNull(), // mesiac (1-12)
|
||||
uploadedAt: timestamp('uploaded_at').defaultNow().notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user