import Fastify from 'fastify' import cookie from '@fastify/cookie' import cors from '@fastify/cors' import multipart from '@fastify/multipart' import staticFiles from '@fastify/static' import { fileURLToPath } from 'url' import { dirname, join } from 'path' import { initDb } from './db.js' import { cashupRoutes } from './routes/cashup.js' import { newbookRoutes } from './routes/newbook.js' import { reportRoutes } from './routes/reports.js' import { floatRoutes } from './routes/floats.js' import { settingsRoutes } from './routes/settings.js' const __dirname = dirname(fileURLToPath(import.meta.url)) const UPLOADS_DIR = join(__dirname, '..', '..', 'uploads') const app = Fastify({ logger: true, trustProxy: true }) await app.register(cookie) await app.register(cors, { origin: process.env.CORS_ORIGIN || false, credentials: true, }) await app.register(multipart, { limits: { fileSize: 10 * 1024 * 1024 } }) await app.register(staticFiles, { root: UPLOADS_DIR, prefix: '/api/uploads/', decorateReply: false, }) app.get('/health', async () => ({ status: 'healthy' })) await app.register(cashupRoutes) await app.register(newbookRoutes) await app.register(reportRoutes) await app.register(floatRoutes) await app.register(settingsRoutes) // File upload for cash up receipt attachments import { requireAuth } from './auth.js' import { pool } from './db.js' import { createWriteStream } from 'fs' import { mkdir } from 'fs/promises' import { randomUUID } from 'crypto' import { extname } from 'path' app.post('/api/attachments/upload/:cash_up_id', { preHandler: requireAuth }, async (req, reply) => { const cashUpId = parseInt(req.params.cash_up_id) const { rows } = await pool.query('SELECT id FROM cash_ups WHERE id = $1', [cashUpId]) if (!rows.length) return reply.status(404).send({ error: 'Cash up not found' }) const data = await req.file() if (!data) return reply.status(400).send({ error: 'No file uploaded' }) const allowed = ['image/jpeg', 'image/jpg', 'image/png', 'application/pdf'] if (!allowed.includes(data.mimetype)) { return reply.status(400).send({ error: 'Only JPEG, PNG and PDF files are allowed' }) } const ext = extname(data.filename) || '.bin' const filename = randomUUID() + ext const dir = join(UPLOADS_DIR, 'cashup', String(cashUpId)) await mkdir(dir, { recursive: true }) let size = 0 const dest = createWriteStream(join(dir, filename)) for await (const chunk of data.file) { dest.write(chunk); size += chunk.length } await new Promise(r => dest.end(r)) const filePath = `/cashup/${cashUpId}/${filename}` const { rows: ins } = await pool.query( `INSERT INTO cash_count_attachments (cash_up_id, file_name, file_path, file_size, mime_type, uploaded_by) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`, [cashUpId, data.filename, filePath, size, data.mimetype, req.user.email] ) return ins.rows[0] }) app.delete('/api/attachments/:id', { preHandler: requireAuth }, async (req, reply) => { const { rows } = await pool.query('SELECT * FROM cash_count_attachments WHERE id = $1', [req.params.id]) if (!rows.length) return reply.status(404).send({ error: 'Not found' }) // Delete file from disk (best effort) const { unlink } = await import('fs/promises') await unlink(join(UPLOADS_DIR, rows[0].file_path)).catch(() => {}) await pool.query('DELETE FROM cash_count_attachments WHERE id = $1', [req.params.id]) return { message: 'Deleted' } }) try { await initDb() await app.listen({ port: 3001, host: '0.0.0.0' }) } catch (err) { app.log.error(err) process.exit(1) }