Compare commits

...

11 commits

7 changed files with 454 additions and 221 deletions

View file

@ -9,13 +9,50 @@ const router = Router();
const hashCode = (code) => createHash('sha256').update(code).digest('hex'); const hashCode = (code) => createHash('sha256').update(code).digest('hex');
// Schema for change request validation
const changeRequestSchema = z.object({
newAddress: z.string().min(10).max(500)
});
// Schema for verification request validation
const verifyRequestSchema = z.object({
requestId: z.number().int().positive(),
code: z.string().regex(/^\d{6}$/)
});
router.post('/change-request', requireAuth, async (req, res) => { router.post('/change-request', requireAuth, async (req, res) => {
const parsed = z.object({ newAddress: z.string().min(10) }).safeParse(req.body); try {
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); const parsed = changeRequestSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: 'Invalid input data',
details: parsed.error.flatten()
});
}
// Check if user already has an address
try {
const [existingRows] = await pool.query(
`SELECT id FROM addresses WHERE user_id = ? LIMIT 1`,
[req.user.userId]
);
if (existingRows.length === 0) {
return res.status(400).json({
error: 'User must have an existing address to request a change'
});
}
} catch (err) {
console.error('Error checking existing address:', err);
return res.status(500).json({
error: 'Internal server error while checking existing address'
});
}
const verificationCode = String(randomInt(100000, 999999)); const verificationCode = String(randomInt(100000, 999999));
const verificationCodeHash = hashCode(verificationCode); const verificationCodeHash = hashCode(verificationCode);
try {
const [result] = await pool.query( const [result] = await pool.query(
`INSERT INTO address_change_requests (user_id, new_address_encrypted, verification_code_hash) `INSERT INTO address_change_requests (user_id, new_address_encrypted, verification_code_hash)
VALUES (?, ?, ?)`, VALUES (?, ?, ?)`,
@ -28,14 +65,33 @@ router.post('/change-request', requireAuth, async (req, res) => {
note: 'Verification code generated for postal letter dispatch.', note: 'Verification code generated for postal letter dispatch.',
verificationCode verificationCode
}); });
} catch (err) {
console.error('Error in address change request:', err);
return res.status(500).json({
error: 'Internal server error while processing address change request'
});
}
} catch (err) {
console.error('Unexpected error in change-request route:', err);
return res.status(500).json({
error: 'Unexpected internal server error'
});
}
}); });
router.post('/verify', requireAuth, async (req, res) => { router.post('/verify', requireAuth, async (req, res) => {
const parsed = z.object({ requestId: z.number().int().positive(), code: z.string().regex(/^\d{6}$/) }).safeParse(req.body); try {
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); const parsed = verifyRequestSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: 'Invalid input data',
details: parsed.error.flatten()
});
}
const { requestId, code } = parsed.data; const { requestId, code } = parsed.data;
try {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT id, user_id, new_address_encrypted, verification_code_hash, status `SELECT id, user_id, new_address_encrypted, verification_code_hash, status
FROM address_change_requests FROM address_change_requests
@ -72,12 +128,25 @@ router.post('/verify', requireAuth, async (req, res) => {
await conn.commit(); await conn.commit();
} catch (err) { } catch (err) {
await conn.rollback(); await conn.rollback();
console.error('Error in address verification transaction:', err);
throw err; throw err;
} finally { } finally {
conn.release(); conn.release();
} }
res.json({ status: 'verified' }); res.json({ status: 'verified' });
} catch (err) {
console.error('Error in address verification:', err);
return res.status(500).json({
error: 'Internal server error while verifying address'
});
}
} catch (err) {
console.error('Unexpected error in verify route:', err);
return res.status(500).json({
error: 'Unexpected internal server error'
});
}
}); });
export default router; export default router;

View file

@ -12,14 +12,51 @@ const registerSchema = z.object({
displayName: z.string().min(2).max(120) displayName: z.string().min(2).max(120)
}); });
router.post('/register', async (req, res) => { const loginSchema = z.object({
const parsed = registerSchema.safeParse(req.body); email: z.string().email(),
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); password: z.string().min(1)
});
const { email, password, displayName } = parsed.data; // Middleware für Validierung
const validateRegister = (req, res, next) => {
try {
const parsed = registerSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: 'Validation failed',
details: parsed.error.flatten()
});
}
req.validatedData = parsed.data;
next();
} catch (err) {
console.error('Validation error:', err);
return res.status(500).json({ error: 'Internal server error during validation' });
}
};
const validateLogin = (req, res, next) => {
try {
const parsed = loginSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: 'Validation failed',
details: parsed.error.flatten()
});
}
req.validatedData = parsed.data;
next();
} catch (err) {
console.error('Validation error:', err);
return res.status(500).json({ error: 'Internal server error during validation' });
}
};
router.post('/register', validateRegister, async (req, res) => {
try {
const { email, password, displayName } = req.validatedData;
const passwordHash = await bcrypt.hash(password, 12); const passwordHash = await bcrypt.hash(password, 12);
try {
const [result] = await pool.query( const [result] = await pool.query(
'INSERT INTO users (email, password_hash, display_name) VALUES (?, ?, ?)', 'INSERT INTO users (email, password_hash, display_name) VALUES (?, ?, ?)',
[email, passwordHash, displayName] [email, passwordHash, displayName]
@ -28,26 +65,35 @@ router.post('/register', async (req, res) => {
const token = jwt.sign({ userId: result.insertId, email }, process.env.JWT_SECRET, { expiresIn: '7d' }); const token = jwt.sign({ userId: result.insertId, email }, process.env.JWT_SECRET, { expiresIn: '7d' });
return res.status(201).json({ token }); return res.status(201).json({ token });
} catch (err) { } catch (err) {
if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'Email already exists' }); console.error('Registration error:', err);
if (err.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ error: 'Email already exists' });
}
return res.status(500).json({ error: 'Registration failed' }); return res.status(500).json({ error: 'Registration failed' });
} }
}); });
router.post('/login', async (req, res) => { router.post('/login', validateLogin, async (req, res) => {
const parsed = z.object({ email: z.string().email(), password: z.string().min(1) }).safeParse(req.body); try {
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); const { email, password } = req.validatedData;
const { email, password } = parsed.data;
const [rows] = await pool.query('SELECT id, email, password_hash FROM users WHERE email = ? LIMIT 1', [email]); const [rows] = await pool.query('SELECT id, email, password_hash FROM users WHERE email = ? LIMIT 1', [email]);
const user = rows[0]; const user = rows[0];
if (!user) return res.status(401).json({ error: 'Invalid credentials' }); if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const ok = await bcrypt.compare(password, user.password_hash); const ok = await bcrypt.compare(password, user.password_hash);
if (!ok) return res.status(401).json({ error: 'Invalid credentials' }); if (!ok) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '7d' }); const token = jwt.sign({ userId: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '7d' });
return res.json({ token }); return res.status(200).json({ token });
} catch (err) {
console.error('Login error:', err);
return res.status(500).json({ error: 'Login failed' });
}
}); });
export default router; export default router;

View file

@ -6,6 +6,7 @@ import { requireAuth } from '../middleware/auth.js';
const router = Router(); const router = Router();
const getDealParticipants = async (dealId) => { const getDealParticipants = async (dealId) => {
try {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT d.id, hr.requester_id, o.helper_id `SELECT d.id, hr.requester_id, o.helper_id
FROM deals d FROM deals d
@ -16,17 +17,31 @@ const getDealParticipants = async (dealId) => {
); );
return rows[0] || null; return rows[0] || null;
} catch (error) {
throw new Error('Database error while fetching deal participants');
}
}; };
router.post('/request', requireAuth, async (req, res) => { router.post('/request', requireAuth, async (req, res) => {
const parsed = z.object({ dealId: z.number().int().positive(), targetUserId: z.number().int().positive() }).safeParse(req.body); try {
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); const parsed = z.object({
dealId: z.number().int().positive(),
targetUserId: z.number().int().positive()
}).safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid input data', details: parsed.error.flatten() });
}
const { dealId, targetUserId } = parsed.data; const { dealId, targetUserId } = parsed.data;
const deal = await getDealParticipants(dealId); const deal = await getDealParticipants(dealId);
if (!deal) return res.status(404).json({ error: 'Deal not found' });
if (!deal) {
return res.status(404).json({ error: 'Deal not found' });
}
const participants = [deal.requester_id, deal.helper_id]; const participants = [deal.requester_id, deal.helper_id];
if (!participants.includes(req.user.userId) || !participants.includes(targetUserId) || req.user.userId === targetUserId) { if (!participants.includes(req.user.userId) || !participants.includes(targetUserId) || req.user.userId === targetUserId) {
return res.status(403).json({ error: 'Forbidden' }); return res.status(403).json({ error: 'Forbidden' });
} }
@ -36,7 +51,10 @@ router.post('/request', requireAuth, async (req, res) => {
WHERE deal_id = ? AND requester_id = ? AND target_id = ? LIMIT 1`, WHERE deal_id = ? AND requester_id = ? AND target_id = ? LIMIT 1`,
[dealId, req.user.userId, targetUserId] [dealId, req.user.userId, targetUserId]
); );
if (existing.length) return res.status(409).json({ error: 'Request already exists' });
if (existing.length) {
return res.status(409).json({ error: 'Request already exists' });
}
const [result] = await pool.query( const [result] = await pool.query(
`INSERT INTO contact_exchange_requests (deal_id, requester_id, target_id, accepted) `INSERT INTO contact_exchange_requests (deal_id, requester_id, target_id, accepted)
@ -45,11 +63,22 @@ router.post('/request', requireAuth, async (req, res) => {
); );
res.status(201).json({ id: result.insertId }); res.status(201).json({ id: result.insertId });
} catch (error) {
console.error('Error in contacts request route:', error);
res.status(500).json({ error: 'Internal server error' });
}
}); });
router.post('/respond', requireAuth, async (req, res) => { router.post('/respond', requireAuth, async (req, res) => {
const parsed = z.object({ requestId: z.number().int().positive(), accept: z.boolean() }).safeParse(req.body); try {
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); const parsed = z.object({
requestId: z.number().int().positive(),
accept: z.boolean()
}).safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid input data', details: parsed.error.flatten() });
}
const { requestId, accept } = parsed.data; const { requestId, accept } = parsed.data;
const [rows] = await pool.query( const [rows] = await pool.query(
@ -58,8 +87,14 @@ router.post('/respond', requireAuth, async (req, res) => {
); );
const row = rows[0]; const row = rows[0];
if (!row) return res.status(404).json({ error: 'Request not found' });
if (row.target_id !== req.user.userId) return res.status(403).json({ error: 'Forbidden' }); if (!row) {
return res.status(404).json({ error: 'Request not found' });
}
if (row.target_id !== req.user.userId) {
return res.status(403).json({ error: 'Forbidden' });
}
if (accept) { if (accept) {
await pool.query('UPDATE contact_exchange_requests SET accepted = TRUE WHERE id = ?', [requestId]); await pool.query('UPDATE contact_exchange_requests SET accepted = TRUE WHERE id = ?', [requestId]);
@ -68,17 +103,31 @@ router.post('/respond', requireAuth, async (req, res) => {
await pool.query('DELETE FROM contact_exchange_requests WHERE id = ?', [requestId]); await pool.query('DELETE FROM contact_exchange_requests WHERE id = ?', [requestId]);
res.json({ status: 'rejected' }); res.json({ status: 'rejected' });
} catch (error) {
console.error('Error in contacts respond route:', error);
res.status(500).json({ error: 'Internal server error' });
}
}); });
router.get('/deal/:dealId', requireAuth, async (req, res) => { router.get('/deal/:dealId', requireAuth, async (req, res) => {
try {
const dealId = Number(req.params.dealId); const dealId = Number(req.params.dealId);
if (Number.isNaN(dealId)) return res.status(400).json({ error: 'Invalid dealId' });
if (Number.isNaN(dealId)) {
return res.status(400).json({ error: 'Invalid dealId' });
}
const deal = await getDealParticipants(dealId); const deal = await getDealParticipants(dealId);
if (!deal) return res.status(404).json({ error: 'Deal not found' });
if (!deal) {
return res.status(404).json({ error: 'Deal not found' });
}
const participants = [deal.requester_id, deal.helper_id]; const participants = [deal.requester_id, deal.helper_id];
if (!participants.includes(req.user.userId)) return res.status(403).json({ error: 'Forbidden' });
if (!participants.includes(req.user.userId)) {
return res.status(403).json({ error: 'Forbidden' });
}
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT cer.id, cer.requester_id, cer.target_id, cer.accepted, `SELECT cer.id, cer.requester_id, cer.target_id, cer.accepted,
@ -92,6 +141,10 @@ router.get('/deal/:dealId', requireAuth, async (req, res) => {
); );
res.json(rows); res.json(rows);
} catch (error) {
console.error('Error in contacts deal route:', error);
res.status(500).json({ error: 'Internal server error' });
}
}); });
export default router; export default router;

View file

@ -6,6 +6,7 @@ import { requireAuth } from '../middleware/auth.js';
const router = Router(); const router = Router();
router.get('/', async (_req, res) => { router.get('/', async (_req, res) => {
try {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT hr.id, hr.title, hr.description, hr.value_chf, hr.status, hr.created_at, u.display_name requester_name `SELECT hr.id, hr.title, hr.description, hr.value_chf, hr.status, hr.created_at, u.display_name requester_name
FROM help_requests hr FROM help_requests hr
@ -13,9 +14,14 @@ router.get('/', async (_req, res) => {
ORDER BY hr.created_at DESC` ORDER BY hr.created_at DESC`
); );
res.json(rows); res.json(rows);
} catch (error) {
console.error('Error fetching help requests:', error);
res.status(500).json({ error: 'Internal server error' });
}
}); });
router.post('/', requireAuth, async (req, res) => { router.post('/', requireAuth, async (req, res) => {
try {
const parsed = z.object({ const parsed = z.object({
title: z.string().min(3).max(180), title: z.string().min(3).max(180),
description: z.string().min(5), description: z.string().min(5),
@ -31,6 +37,10 @@ router.post('/', requireAuth, async (req, res) => {
); );
res.status(201).json({ id: result.insertId }); res.status(201).json({ id: result.insertId });
} catch (error) {
console.error('Error creating help request:', error);
res.status(500).json({ error: 'Internal server error' });
}
}); });
export default router; export default router;

View file

@ -6,9 +6,20 @@ import { requireAuth } from '../middleware/auth.js';
const router = Router(); const router = Router();
router.post('/:requestId', requireAuth, async (req, res) => { router.post('/:requestId', requireAuth, async (req, res) => {
try {
const requestId = Number(req.params.requestId); const requestId = Number(req.params.requestId);
const parsed = z.object({ amountChf: z.number().positive(), message: z.string().max(2000).optional() }).safeParse(req.body); if (Number.isNaN(requestId)) {
if (!parsed.success || Number.isNaN(requestId)) return res.status(400).json({ error: 'Invalid payload' }); return res.status(400).json({ error: 'Invalid requestId' });
}
const parsed = z.object({
amountChf: z.number().positive(),
message: z.string().max(2000).optional()
}).safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid payload' });
}
const { amountChf, message } = parsed.data; const { amountChf, message } = parsed.data;
const [result] = await pool.query( const [result] = await pool.query(
@ -20,12 +31,27 @@ router.post('/:requestId', requireAuth, async (req, res) => {
await pool.query('UPDATE help_requests SET status = ? WHERE id = ?', ['negotiating', requestId]); await pool.query('UPDATE help_requests SET status = ? WHERE id = ?', ['negotiating', requestId]);
res.status(201).json({ id: result.insertId }); res.status(201).json({ id: result.insertId });
} catch (error) {
console.error('Error in POST /offers/:requestId:', error);
res.status(500).json({ error: 'Internal server error' });
}
}); });
router.post('/negotiation/:offerId', requireAuth, async (req, res) => { router.post('/negotiation/:offerId', requireAuth, async (req, res) => {
try {
const offerId = Number(req.params.offerId); const offerId = Number(req.params.offerId);
const parsed = z.object({ amountChf: z.number().positive(), message: z.string().max(2000).optional() }).safeParse(req.body); if (Number.isNaN(offerId)) {
if (!parsed.success || Number.isNaN(offerId)) return res.status(400).json({ error: 'Invalid payload' }); return res.status(400).json({ error: 'Invalid offerId' });
}
const parsed = z.object({
amountChf: z.number().positive(),
message: z.string().max(2000).optional()
}).safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid payload' });
}
const { amountChf, message } = parsed.data; const { amountChf, message } = parsed.data;
const [result] = await pool.query( const [result] = await pool.query(
@ -37,18 +63,27 @@ router.post('/negotiation/:offerId', requireAuth, async (req, res) => {
await pool.query('UPDATE offers SET status = ? WHERE id = ?', ['countered', offerId]); await pool.query('UPDATE offers SET status = ? WHERE id = ?', ['countered', offerId]);
res.status(201).json({ id: result.insertId }); res.status(201).json({ id: result.insertId });
} catch (error) {
console.error('Error in POST /offers/negotiation/:offerId:', error);
res.status(500).json({ error: 'Internal server error' });
}
}); });
router.post('/accept/:offerId', requireAuth, async (req, res) => { router.post('/accept/:offerId', requireAuth, async (req, res) => {
try {
const offerId = Number(req.params.offerId); const offerId = Number(req.params.offerId);
if (Number.isNaN(offerId)) return res.status(400).json({ error: 'Invalid offerId' }); if (Number.isNaN(offerId)) {
return res.status(400).json({ error: 'Invalid offerId' });
}
const [offers] = await pool.query( const [offers] = await pool.query(
'SELECT id, request_id, amount_chf FROM offers WHERE id = ? LIMIT 1', 'SELECT id, request_id, amount_chf FROM offers WHERE id = ? LIMIT 1',
[offerId] [offerId]
); );
const offer = offers[0]; const offer = offers[0];
if (!offer) return res.status(404).json({ error: 'Offer not found' }); if (!offer) {
return res.status(404).json({ error: 'Offer not found' });
}
await pool.query('UPDATE offers SET status = ? WHERE id = ?', ['accepted', offerId]); await pool.query('UPDATE offers SET status = ? WHERE id = ?', ['accepted', offerId]);
await pool.query('UPDATE help_requests SET status = ? WHERE id = ?', ['agreed', offer.request_id]); await pool.query('UPDATE help_requests SET status = ? WHERE id = ?', ['agreed', offer.request_id]);
@ -59,6 +94,10 @@ router.post('/accept/:offerId', requireAuth, async (req, res) => {
); );
res.status(201).json({ dealId: dealResult.insertId }); res.status(201).json({ dealId: dealResult.insertId });
} catch (error) {
console.error('Error in POST /offers/accept/:offerId:', error);
res.status(500).json({ error: 'Internal server error' });
}
}); });
export default router; export default router;

View file

@ -7,13 +7,18 @@ import { encryptText } from '../services/encryption.js';
const router = Router(); const router = Router();
router.post('/phone', requireAuth, async (req, res) => { router.post('/phone', requireAuth, async (req, res) => {
try {
const parsed = z.object({ phone: z.string().min(6).max(40) }).safeParse(req.body); const parsed = z.object({ phone: z.string().min(6).max(40) }).safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
const encryptedPhone = encryptText(parsed.data.phone); const encryptedPhone = encryptText(parsed.data.phone);
await pool.query('UPDATE users SET phone_encrypted = ? WHERE id = ?', [encryptedPhone, req.user.userId]); await pool.query('UPDATE users SET phone_encrypted = ? WHERE id = ?', [encryptedPhone, req.user.userId]);
res.json({ status: 'updated' }); res.status(200).json({ status: 'updated' });
} catch (error) {
console.error('Error updating phone:', error);
res.status(500).json({ error: 'Internal server error' });
}
}); });
export default router; export default router;

View file

@ -6,10 +6,17 @@ import { requireAuth } from '../middleware/auth.js';
const router = Router(); const router = Router();
router.post('/:dealId', requireAuth, async (req, res) => { router.post('/:dealId', requireAuth, async (req, res) => {
try {
const dealId = Number(req.params.dealId); const dealId = Number(req.params.dealId);
const parsed = z.object({ revieweeId: z.number().int().positive(), rating: z.number().int().min(1).max(5), comment: z.string().max(2000).optional() }).safeParse(req.body); const parsed = z.object({
revieweeId: z.number().int().positive(),
rating: z.number().int().min(1).max(5),
comment: z.string().max(2000).optional()
}).safeParse(req.body);
if (!parsed.success || Number.isNaN(dealId)) return res.status(400).json({ error: 'Invalid payload' }); if (!parsed.success || Number.isNaN(dealId)) {
return res.status(400).json({ error: 'Invalid payload' });
}
const now = new Date(); const now = new Date();
const earliest = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000); const earliest = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000);
@ -24,6 +31,10 @@ router.post('/:dealId', requireAuth, async (req, res) => {
); );
res.status(201).json({ id: result.insertId }); res.status(201).json({ id: result.insertId });
} catch (error) {
console.error('Error creating review:', error);
res.status(500).json({ error: 'Internal server error' });
}
}); });
export default router; export default router;