mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 06:41:46 +00:00
refactor: extract business logic from routes into reusable service modules
This commit is contained in:
+47
-151
@@ -1,27 +1,36 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { db, canAccessTrip } from '../db/database';
|
||||
import { db } from '../db/database';
|
||||
import { authenticate } from '../middleware/auth';
|
||||
import { broadcast } from '../websocket';
|
||||
import { checkPermission } from '../services/permissions';
|
||||
import { AuthRequest } from '../types';
|
||||
import {
|
||||
verifyTripAccess,
|
||||
listItems,
|
||||
createItem,
|
||||
updateItem,
|
||||
deleteItem,
|
||||
bulkImport,
|
||||
listBags,
|
||||
createBag,
|
||||
updateBag,
|
||||
deleteBag,
|
||||
applyTemplate,
|
||||
getCategoryAssignees,
|
||||
updateCategoryAssignees,
|
||||
reorderItems,
|
||||
} from '../services/packingService';
|
||||
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
function verifyTripOwnership(tripId: string | number, userId: number) {
|
||||
return canAccessTrip(tripId, userId);
|
||||
}
|
||||
|
||||
router.get('/', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
const items = db.prepare(
|
||||
'SELECT * FROM packing_items WHERE trip_id = ? ORDER BY sort_order ASC, created_at ASC'
|
||||
).all(tripId);
|
||||
|
||||
const items = listItems(tripId);
|
||||
res.json({ items });
|
||||
});
|
||||
|
||||
@@ -29,9 +38,9 @@ router.get('/', authenticate, (req: Request, res: Response) => {
|
||||
router.post('/import', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
const { items } = req.body; // [{ name, category?, quantity? }]
|
||||
const { items } = req.body;
|
||||
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
if (!checkPermission('packing_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
@@ -39,35 +48,7 @@ router.post('/import', authenticate, (req: Request, res: Response) => {
|
||||
|
||||
if (!Array.isArray(items) || items.length === 0) return res.status(400).json({ error: 'items must be a non-empty array' });
|
||||
|
||||
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_items WHERE trip_id = ?').get(tripId) as { max: number | null };
|
||||
let sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
|
||||
|
||||
const stmt = db.prepare('INSERT INTO packing_items (trip_id, name, checked, category, weight_grams, bag_id, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
||||
const created: any[] = [];
|
||||
const insertAll = db.transaction(() => {
|
||||
for (const item of items) {
|
||||
if (!item.name?.trim()) continue;
|
||||
const checked = item.checked ? 1 : 0;
|
||||
const weight = item.weight_grams ? parseInt(item.weight_grams) || null : null;
|
||||
// Resolve bag by name if provided
|
||||
let bagId = null;
|
||||
if (item.bag?.trim()) {
|
||||
const bagName = item.bag.trim();
|
||||
const existing = db.prepare('SELECT id FROM packing_bags WHERE trip_id = ? AND name = ?').get(tripId, bagName) as { id: number } | undefined;
|
||||
if (existing) {
|
||||
bagId = existing.id;
|
||||
} else {
|
||||
const BAG_COLORS = ['#6366f1', '#ec4899', '#f97316', '#10b981', '#06b6d4', '#8b5cf6', '#ef4444', '#f59e0b'];
|
||||
const bagCount = (db.prepare('SELECT COUNT(*) as c FROM packing_bags WHERE trip_id = ?').get(tripId) as { c: number }).c;
|
||||
const newBag = db.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)').run(tripId, bagName, BAG_COLORS[bagCount % BAG_COLORS.length]);
|
||||
bagId = newBag.lastInsertRowid;
|
||||
}
|
||||
}
|
||||
const result = stmt.run(tripId, item.name.trim(), checked, item.category?.trim() || 'Other', weight, bagId, sortOrder++);
|
||||
created.push(db.prepare('SELECT * FROM packing_items WHERE id = ?').get(result.lastInsertRowid));
|
||||
}
|
||||
});
|
||||
insertAll();
|
||||
const created = bulkImport(tripId, items);
|
||||
|
||||
res.status(201).json({ items: created, count: created.length });
|
||||
for (const item of created) {
|
||||
@@ -80,7 +61,7 @@ router.post('/', authenticate, (req: Request, res: Response) => {
|
||||
const { tripId } = req.params;
|
||||
const { name, category, checked } = req.body;
|
||||
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
if (!checkPermission('packing_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
@@ -88,14 +69,7 @@ router.post('/', authenticate, (req: Request, res: Response) => {
|
||||
|
||||
if (!name) return res.status(400).json({ error: 'Item name is required' });
|
||||
|
||||
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_items WHERE trip_id = ?').get(tripId) as { max: number | null };
|
||||
const sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
|
||||
|
||||
const result = db.prepare(
|
||||
'INSERT INTO packing_items (trip_id, name, checked, category, sort_order) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(tripId, name, checked ? 1 : 0, category || 'Allgemein', sortOrder);
|
||||
|
||||
const item = db.prepare('SELECT * FROM packing_items WHERE id = ?').get(result.lastInsertRowid);
|
||||
const item = createItem(tripId, { name, category, checked });
|
||||
res.status(201).json({ item });
|
||||
broadcast(tripId, 'packing:created', { item }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
@@ -105,36 +79,15 @@ router.put('/:id', authenticate, (req: Request, res: Response) => {
|
||||
const { tripId, id } = req.params;
|
||||
const { name, checked, category, weight_grams, bag_id } = req.body;
|
||||
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
if (!checkPermission('packing_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
return res.status(403).json({ error: 'No permission' });
|
||||
|
||||
const item = db.prepare('SELECT * FROM packing_items WHERE id = ? AND trip_id = ?').get(id, tripId);
|
||||
if (!item) return res.status(404).json({ error: 'Item not found' });
|
||||
const updated = updateItem(tripId, id, { name, checked, category, weight_grams, bag_id }, Object.keys(req.body));
|
||||
if (!updated) return res.status(404).json({ error: 'Item not found' });
|
||||
|
||||
db.prepare(`
|
||||
UPDATE packing_items SET
|
||||
name = COALESCE(?, name),
|
||||
checked = CASE WHEN ? IS NOT NULL THEN ? ELSE checked END,
|
||||
category = COALESCE(?, category),
|
||||
weight_grams = CASE WHEN ? THEN ? ELSE weight_grams END,
|
||||
bag_id = CASE WHEN ? THEN ? ELSE bag_id END
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
name || null,
|
||||
checked !== undefined ? 1 : null,
|
||||
checked ? 1 : 0,
|
||||
category || null,
|
||||
'weight_grams' in req.body ? 1 : 0,
|
||||
weight_grams ?? null,
|
||||
'bag_id' in req.body ? 1 : 0,
|
||||
bag_id ?? null,
|
||||
id
|
||||
);
|
||||
|
||||
const updated = db.prepare('SELECT * FROM packing_items WHERE id = ?').get(id);
|
||||
res.json({ item: updated });
|
||||
broadcast(tripId, 'packing:updated', { item: updated }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
@@ -143,16 +96,14 @@ router.delete('/:id', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, id } = req.params;
|
||||
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
if (!checkPermission('packing_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
return res.status(403).json({ error: 'No permission' });
|
||||
|
||||
const item = db.prepare('SELECT id FROM packing_items WHERE id = ? AND trip_id = ?').get(id, tripId);
|
||||
if (!item) return res.status(404).json({ error: 'Item not found' });
|
||||
if (!deleteItem(tripId, id)) return res.status(404).json({ error: 'Item not found' });
|
||||
|
||||
db.prepare('DELETE FROM packing_items WHERE id = ?').run(id);
|
||||
res.json({ success: true });
|
||||
broadcast(tripId, 'packing:deleted', { itemId: Number(id) }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
@@ -162,9 +113,9 @@ router.delete('/:id', authenticate, (req: Request, res: Response) => {
|
||||
router.get('/bags', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
const bags = db.prepare('SELECT * FROM packing_bags WHERE trip_id = ? ORDER BY sort_order, id').all(tripId);
|
||||
const bags = listBags(tripId);
|
||||
res.json({ bags });
|
||||
});
|
||||
|
||||
@@ -172,14 +123,12 @@ router.post('/bags', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
const { name, color } = req.body;
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
if (!checkPermission('packing_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
return res.status(403).json({ error: 'No permission' });
|
||||
if (!name?.trim()) return res.status(400).json({ error: 'Name is required' });
|
||||
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_bags WHERE trip_id = ?').get(tripId) as { max: number | null };
|
||||
const result = db.prepare('INSERT INTO packing_bags (trip_id, name, color, sort_order) VALUES (?, ?, ?, ?)').run(tripId, name.trim(), color || '#6366f1', (maxOrder.max ?? -1) + 1);
|
||||
const bag = db.prepare('SELECT * FROM packing_bags WHERE id = ?').get(result.lastInsertRowid);
|
||||
const bag = createBag(tripId, { name, color });
|
||||
res.status(201).json({ bag });
|
||||
broadcast(tripId, 'packing:bag-created', { bag }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
@@ -188,14 +137,12 @@ router.put('/bags/:bagId', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, bagId } = req.params;
|
||||
const { name, color, weight_limit_grams } = req.body;
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
if (!checkPermission('packing_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
return res.status(403).json({ error: 'No permission' });
|
||||
const bag = db.prepare('SELECT * FROM packing_bags WHERE id = ? AND trip_id = ?').get(bagId, tripId);
|
||||
if (!bag) return res.status(404).json({ error: 'Bag not found' });
|
||||
db.prepare('UPDATE packing_bags SET name = COALESCE(?, name), color = COALESCE(?, color), weight_limit_grams = ? WHERE id = ?').run(name?.trim() || null, color || null, weight_limit_grams ?? null, bagId);
|
||||
const updated = db.prepare('SELECT * FROM packing_bags WHERE id = ?').get(bagId);
|
||||
const updated = updateBag(tripId, bagId, { name, color, weight_limit_grams });
|
||||
if (!updated) return res.status(404).json({ error: 'Bag not found' });
|
||||
res.json({ bag: updated });
|
||||
broadcast(tripId, 'packing:bag-updated', { bag: updated }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
@@ -203,13 +150,11 @@ router.put('/bags/:bagId', authenticate, (req: Request, res: Response) => {
|
||||
router.delete('/bags/:bagId', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, bagId } = req.params;
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
if (!checkPermission('packing_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
return res.status(403).json({ error: 'No permission' });
|
||||
const bag = db.prepare('SELECT * FROM packing_bags WHERE id = ? AND trip_id = ?').get(bagId, tripId);
|
||||
if (!bag) return res.status(404).json({ error: 'Bag not found' });
|
||||
db.prepare('DELETE FROM packing_bags WHERE id = ?').run(bagId);
|
||||
if (!deleteBag(tripId, bagId)) return res.status(404).json({ error: 'Bag not found' });
|
||||
res.json({ success: true });
|
||||
broadcast(tripId, 'packing:bag-deleted', { bagId: Number(bagId) }, req.headers['x-socket-id'] as string);
|
||||
});
|
||||
@@ -220,31 +165,14 @@ router.post('/apply-template/:templateId', authenticate, (req: Request, res: Res
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, templateId } = req.params;
|
||||
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
if (!checkPermission('packing_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
return res.status(403).json({ error: 'No permission' });
|
||||
|
||||
const templateItems = db.prepare(`
|
||||
SELECT ti.name, tc.name as category
|
||||
FROM packing_template_items ti
|
||||
JOIN packing_template_categories tc ON ti.category_id = tc.id
|
||||
WHERE tc.template_id = ?
|
||||
ORDER BY tc.sort_order, ti.sort_order
|
||||
`).all(templateId) as { name: string; category: string }[];
|
||||
if (templateItems.length === 0) return res.status(404).json({ error: 'Template not found or empty' });
|
||||
|
||||
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_items WHERE trip_id = ?').get(tripId) as { max: number | null };
|
||||
let sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
|
||||
|
||||
const insert = db.prepare('INSERT INTO packing_items (trip_id, name, checked, category, sort_order) VALUES (?, ?, 0, ?, ?)');
|
||||
const added: any[] = [];
|
||||
for (const ti of templateItems) {
|
||||
const result = insert.run(tripId, ti.name, ti.category, sortOrder++);
|
||||
const item = db.prepare('SELECT * FROM packing_items WHERE id = ?').get(result.lastInsertRowid);
|
||||
added.push(item);
|
||||
}
|
||||
const added = applyTemplate(tripId, templateId);
|
||||
if (!added) return res.status(404).json({ error: 'Template not found or empty' });
|
||||
|
||||
res.json({ items: added, count: added.length });
|
||||
broadcast(tripId, 'packing:template-applied', { items: added }, req.headers['x-socket-id'] as string);
|
||||
@@ -255,23 +183,10 @@ router.post('/apply-template/:templateId', authenticate, (req: Request, res: Res
|
||||
router.get('/category-assignees', authenticate, (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId } = req.params;
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT pca.category_name, pca.user_id, u.username, u.avatar
|
||||
FROM packing_category_assignees pca
|
||||
JOIN users u ON pca.user_id = u.id
|
||||
WHERE pca.trip_id = ?
|
||||
`).all(tripId);
|
||||
|
||||
// Group by category
|
||||
const assignees: Record<string, { user_id: number; username: string; avatar: string | null }[]> = {};
|
||||
for (const row of rows as any[]) {
|
||||
if (!assignees[row.category_name]) assignees[row.category_name] = [];
|
||||
assignees[row.category_name].push({ user_id: row.user_id, username: row.username, avatar: row.avatar });
|
||||
}
|
||||
|
||||
const assignees = getCategoryAssignees(tripId);
|
||||
res.json({ assignees });
|
||||
});
|
||||
|
||||
@@ -280,26 +195,14 @@ router.put('/category-assignees/:categoryName', authenticate, (req: Request, res
|
||||
const { tripId, categoryName } = req.params;
|
||||
const { user_ids } = req.body;
|
||||
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
if (!checkPermission('packing_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
return res.status(403).json({ error: 'No permission' });
|
||||
|
||||
const cat = decodeURIComponent(categoryName);
|
||||
db.prepare('DELETE FROM packing_category_assignees WHERE trip_id = ? AND category_name = ?').run(tripId, cat);
|
||||
|
||||
if (Array.isArray(user_ids) && user_ids.length > 0) {
|
||||
const insert = db.prepare('INSERT OR IGNORE INTO packing_category_assignees (trip_id, category_name, user_id) VALUES (?, ?, ?)');
|
||||
for (const uid of user_ids) insert.run(tripId, cat, uid);
|
||||
}
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT pca.user_id, u.username, u.avatar
|
||||
FROM packing_category_assignees pca
|
||||
JOIN users u ON pca.user_id = u.id
|
||||
WHERE pca.trip_id = ? AND pca.category_name = ?
|
||||
`).all(tripId, cat);
|
||||
const rows = updateCategoryAssignees(tripId, cat, user_ids);
|
||||
|
||||
res.json({ assignees: rows });
|
||||
broadcast(tripId, 'packing:assignees', { category: cat, assignees: rows }, req.headers['x-socket-id'] as string);
|
||||
@@ -322,20 +225,13 @@ router.put('/reorder', authenticate, (req: Request, res: Response) => {
|
||||
const { tripId } = req.params;
|
||||
const { orderedIds } = req.body;
|
||||
|
||||
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||
const trip = verifyTripAccess(tripId, authReq.user.id);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
|
||||
if (!checkPermission('packing_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id))
|
||||
return res.status(403).json({ error: 'No permission' });
|
||||
|
||||
const update = db.prepare('UPDATE packing_items SET sort_order = ? WHERE id = ? AND trip_id = ?');
|
||||
const updateMany = db.transaction((ids: number[]) => {
|
||||
ids.forEach((id, index) => {
|
||||
update.run(index, id, tripId);
|
||||
});
|
||||
});
|
||||
|
||||
updateMany(orderedIds);
|
||||
reorderItems(tripId, orderedIds);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user