Compare commits
11 commits
f03b758e18
...
51be362dc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51be362dc4 | ||
|
|
4ee009a730 | ||
|
|
cd7fa3bac2 | ||
|
|
b03b264c5e | ||
|
|
00661fd99e | ||
|
|
507f54eea0 | ||
|
|
6539762c92 | ||
|
|
fe6c17309a | ||
|
|
2d24125498 | ||
|
|
c2dd24f1b3 | ||
|
|
ed38467091 |
7 changed files with 454 additions and 221 deletions
|
|
@ -9,75 +9,144 @@ const router = Router();
|
|||
|
||||
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) => {
|
||||
const parsed = z.object({ newAddress: z.string().min(10) }).safeParse(req.body);
|
||||
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
|
||||
try {
|
||||
const parsed = changeRequestSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid input data',
|
||||
details: parsed.error.flatten()
|
||||
});
|
||||
}
|
||||
|
||||
const verificationCode = String(randomInt(100000, 999999));
|
||||
const verificationCodeHash = hashCode(verificationCode);
|
||||
// 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]
|
||||
);
|
||||
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO address_change_requests (user_id, new_address_encrypted, verification_code_hash)
|
||||
VALUES (?, ?, ?)`,
|
||||
[req.user.userId, encryptText(parsed.data.newAddress), verificationCodeHash]
|
||||
);
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
requestId: result.insertId,
|
||||
postalDispatch: 'pending_letter',
|
||||
note: 'Verification code generated for postal letter dispatch.',
|
||||
verificationCode
|
||||
});
|
||||
const verificationCode = String(randomInt(100000, 999999));
|
||||
const verificationCodeHash = hashCode(verificationCode);
|
||||
|
||||
try {
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO address_change_requests (user_id, new_address_encrypted, verification_code_hash)
|
||||
VALUES (?, ?, ?)`,
|
||||
[req.user.userId, encryptText(parsed.data.newAddress), verificationCodeHash]
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
requestId: result.insertId,
|
||||
postalDispatch: 'pending_letter',
|
||||
note: 'Verification code generated for postal letter dispatch.',
|
||||
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) => {
|
||||
const parsed = z.object({ requestId: z.number().int().positive(), code: z.string().regex(/^\d{6}$/) }).safeParse(req.body);
|
||||
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
|
||||
|
||||
const { requestId, code } = parsed.data;
|
||||
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, user_id, new_address_encrypted, verification_code_hash, status
|
||||
FROM address_change_requests
|
||||
WHERE id = ? LIMIT 1`,
|
||||
[requestId]
|
||||
);
|
||||
|
||||
const request = rows[0];
|
||||
if (!request) return res.status(404).json({ error: 'Request not found' });
|
||||
if (request.user_id !== req.user.userId) return res.status(403).json({ error: 'Forbidden' });
|
||||
if (request.status !== 'pending_letter') return res.status(409).json({ error: 'Request not pending' });
|
||||
|
||||
if (hashCode(code) !== request.verification_code_hash) {
|
||||
return res.status(400).json({ error: 'Invalid verification code' });
|
||||
}
|
||||
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
const parsed = verifyRequestSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid input data',
|
||||
details: parsed.error.flatten()
|
||||
});
|
||||
}
|
||||
|
||||
await conn.query(
|
||||
`UPDATE address_change_requests
|
||||
SET status = 'verified', verified_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[requestId]
|
||||
);
|
||||
const { requestId, code } = parsed.data;
|
||||
|
||||
await conn.query(
|
||||
`INSERT INTO addresses (user_id, address_encrypted, postal_verified_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)`,
|
||||
[req.user.userId, request.new_address_encrypted]
|
||||
);
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, user_id, new_address_encrypted, verification_code_hash, status
|
||||
FROM address_change_requests
|
||||
WHERE id = ? LIMIT 1`,
|
||||
[requestId]
|
||||
);
|
||||
|
||||
await conn.commit();
|
||||
const request = rows[0];
|
||||
if (!request) return res.status(404).json({ error: 'Request not found' });
|
||||
if (request.user_id !== req.user.userId) return res.status(403).json({ error: 'Forbidden' });
|
||||
if (request.status !== 'pending_letter') return res.status(409).json({ error: 'Request not pending' });
|
||||
|
||||
if (hashCode(code) !== request.verification_code_hash) {
|
||||
return res.status(400).json({ error: 'Invalid verification code' });
|
||||
}
|
||||
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
await conn.query(
|
||||
`UPDATE address_change_requests
|
||||
SET status = 'verified', verified_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[requestId]
|
||||
);
|
||||
|
||||
await conn.query(
|
||||
`INSERT INTO addresses (user_id, address_encrypted, postal_verified_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)`,
|
||||
[req.user.userId, request.new_address_encrypted]
|
||||
);
|
||||
|
||||
await conn.commit();
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
console.error('Error in address verification transaction:', err);
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
|
||||
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) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
console.error('Unexpected error in verify route:', err);
|
||||
return res.status(500).json({
|
||||
error: 'Unexpected internal server error'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ status: 'verified' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
|
@ -12,14 +12,51 @@ const registerSchema = z.object({
|
|||
displayName: z.string().min(2).max(120)
|
||||
});
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
const parsed = registerSchema.safeParse(req.body);
|
||||
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
|
||||
|
||||
const { email, password, displayName } = parsed.data;
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(1)
|
||||
});
|
||||
|
||||
// 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 [result] = await pool.query(
|
||||
'INSERT INTO users (email, password_hash, display_name) VALUES (?, ?, ?)',
|
||||
[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' });
|
||||
return res.status(201).json({ token });
|
||||
} 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' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
const parsed = z.object({ email: z.string().email(), password: z.string().min(1) }).safeParse(req.body);
|
||||
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
|
||||
router.post('/login', validateLogin, async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.validatedData;
|
||||
const [rows] = await pool.query('SELECT id, email, password_hash FROM users WHERE email = ? LIMIT 1', [email]);
|
||||
const user = rows[0];
|
||||
|
||||
const { email, password } = parsed.data;
|
||||
const [rows] = await pool.query('SELECT id, email, password_hash FROM users WHERE email = ? LIMIT 1', [email]);
|
||||
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);
|
||||
if (!ok) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const ok = await bcrypt.compare(password, user.password_hash);
|
||||
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' });
|
||||
return res.json({ token });
|
||||
const token = jwt.sign({ userId: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '7d' });
|
||||
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;
|
||||
|
|
@ -6,92 +6,145 @@ import { requireAuth } from '../middleware/auth.js';
|
|||
const router = Router();
|
||||
|
||||
const getDealParticipants = async (dealId) => {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT d.id, hr.requester_id, o.helper_id
|
||||
FROM deals d
|
||||
JOIN help_requests hr ON hr.id = d.request_id
|
||||
JOIN offers o ON o.id = d.offer_id
|
||||
WHERE d.id = ? LIMIT 1`,
|
||||
[dealId]
|
||||
);
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT d.id, hr.requester_id, o.helper_id
|
||||
FROM deals d
|
||||
JOIN help_requests hr ON hr.id = d.request_id
|
||||
JOIN offers o ON o.id = d.offer_id
|
||||
WHERE d.id = ? LIMIT 1`,
|
||||
[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) => {
|
||||
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: parsed.error.flatten() });
|
||||
try {
|
||||
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 deal = await getDealParticipants(dealId);
|
||||
if (!deal) return res.status(404).json({ error: 'Deal not found' });
|
||||
const { dealId, targetUserId } = parsed.data;
|
||||
const deal = await getDealParticipants(dealId);
|
||||
|
||||
if (!deal) {
|
||||
return res.status(404).json({ error: 'Deal not found' });
|
||||
}
|
||||
|
||||
const participants = [deal.requester_id, deal.helper_id];
|
||||
if (!participants.includes(req.user.userId) || !participants.includes(targetUserId) || req.user.userId === targetUserId) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
const participants = [deal.requester_id, deal.helper_id];
|
||||
|
||||
if (!participants.includes(req.user.userId) || !participants.includes(targetUserId) || req.user.userId === targetUserId) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
const [existing] = await pool.query(
|
||||
`SELECT id FROM contact_exchange_requests
|
||||
WHERE deal_id = ? AND requester_id = ? AND target_id = ? LIMIT 1`,
|
||||
[dealId, req.user.userId, targetUserId]
|
||||
);
|
||||
|
||||
if (existing.length) {
|
||||
return res.status(409).json({ error: 'Request already exists' });
|
||||
}
|
||||
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO contact_exchange_requests (deal_id, requester_id, target_id, accepted)
|
||||
VALUES (?, ?, ?, FALSE)`,
|
||||
[dealId, req.user.userId, targetUserId]
|
||||
);
|
||||
|
||||
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' });
|
||||
}
|
||||
|
||||
const [existing] = await pool.query(
|
||||
`SELECT id FROM contact_exchange_requests
|
||||
WHERE deal_id = ? AND requester_id = ? AND target_id = ? LIMIT 1`,
|
||||
[dealId, req.user.userId, targetUserId]
|
||||
);
|
||||
if (existing.length) return res.status(409).json({ error: 'Request already exists' });
|
||||
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO contact_exchange_requests (deal_id, requester_id, target_id, accepted)
|
||||
VALUES (?, ?, ?, FALSE)`,
|
||||
[dealId, req.user.userId, targetUserId]
|
||||
);
|
||||
|
||||
res.status(201).json({ id: result.insertId });
|
||||
});
|
||||
|
||||
router.post('/respond', requireAuth, async (req, res) => {
|
||||
const parsed = z.object({ requestId: z.number().int().positive(), accept: z.boolean() }).safeParse(req.body);
|
||||
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
|
||||
try {
|
||||
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 [rows] = await pool.query(
|
||||
`SELECT id, target_id FROM contact_exchange_requests WHERE id = ? LIMIT 1`,
|
||||
[requestId]
|
||||
);
|
||||
const { requestId, accept } = parsed.data;
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, target_id FROM contact_exchange_requests WHERE id = ? LIMIT 1`,
|
||||
[requestId]
|
||||
);
|
||||
|
||||
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' });
|
||||
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 (accept) {
|
||||
await pool.query('UPDATE contact_exchange_requests SET accepted = TRUE WHERE id = ?', [requestId]);
|
||||
return res.json({ status: 'accepted' });
|
||||
if (accept) {
|
||||
await pool.query('UPDATE contact_exchange_requests SET accepted = TRUE WHERE id = ?', [requestId]);
|
||||
return res.json({ status: 'accepted' });
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM contact_exchange_requests WHERE id = ?', [requestId]);
|
||||
res.json({ status: 'rejected' });
|
||||
} catch (error) {
|
||||
console.error('Error in contacts respond route:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM contact_exchange_requests WHERE id = ?', [requestId]);
|
||||
res.json({ status: 'rejected' });
|
||||
});
|
||||
|
||||
router.get('/deal/:dealId', requireAuth, async (req, res) => {
|
||||
const dealId = Number(req.params.dealId);
|
||||
if (Number.isNaN(dealId)) return res.status(400).json({ error: 'Invalid dealId' });
|
||||
try {
|
||||
const dealId = Number(req.params.dealId);
|
||||
|
||||
if (Number.isNaN(dealId)) {
|
||||
return res.status(400).json({ error: 'Invalid dealId' });
|
||||
}
|
||||
|
||||
const deal = await getDealParticipants(dealId);
|
||||
if (!deal) return res.status(404).json({ error: 'Deal not found' });
|
||||
const deal = await getDealParticipants(dealId);
|
||||
|
||||
if (!deal) {
|
||||
return res.status(404).json({ error: 'Deal not found' });
|
||||
}
|
||||
|
||||
const participants = [deal.requester_id, deal.helper_id];
|
||||
if (!participants.includes(req.user.userId)) return res.status(403).json({ error: 'Forbidden' });
|
||||
const participants = [deal.requester_id, deal.helper_id];
|
||||
|
||||
if (!participants.includes(req.user.userId)) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
const [rows] = await pool.query(
|
||||
`SELECT cer.id, cer.requester_id, cer.target_id, cer.accepted,
|
||||
ru.phone_encrypted AS requester_phone_encrypted,
|
||||
tu.phone_encrypted AS target_phone_encrypted
|
||||
FROM contact_exchange_requests cer
|
||||
JOIN users ru ON ru.id = cer.requester_id
|
||||
JOIN users tu ON tu.id = cer.target_id
|
||||
WHERE cer.deal_id = ? AND cer.accepted = TRUE`,
|
||||
[dealId]
|
||||
);
|
||||
const [rows] = await pool.query(
|
||||
`SELECT cer.id, cer.requester_id, cer.target_id, cer.accepted,
|
||||
ru.phone_encrypted AS requester_phone_encrypted,
|
||||
tu.phone_encrypted AS target_phone_encrypted
|
||||
FROM contact_exchange_requests cer
|
||||
JOIN users ru ON ru.id = cer.requester_id
|
||||
JOIN users tu ON tu.id = cer.target_id
|
||||
WHERE cer.deal_id = ? AND cer.accepted = TRUE`,
|
||||
[dealId]
|
||||
);
|
||||
|
||||
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;
|
||||
|
|
@ -6,31 +6,41 @@ import { requireAuth } from '../middleware/auth.js';
|
|||
const router = Router();
|
||||
|
||||
router.get('/', async (_req, res) => {
|
||||
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
|
||||
FROM help_requests hr
|
||||
JOIN users u ON u.id = hr.requester_id
|
||||
ORDER BY hr.created_at DESC`
|
||||
);
|
||||
res.json(rows);
|
||||
try {
|
||||
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
|
||||
FROM help_requests hr
|
||||
JOIN users u ON u.id = hr.requester_id
|
||||
ORDER BY hr.created_at DESC`
|
||||
);
|
||||
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) => {
|
||||
const parsed = z.object({
|
||||
title: z.string().min(3).max(180),
|
||||
description: z.string().min(5),
|
||||
valueChf: z.number().positive()
|
||||
}).safeParse(req.body);
|
||||
try {
|
||||
const parsed = z.object({
|
||||
title: z.string().min(3).max(180),
|
||||
description: z.string().min(5),
|
||||
valueChf: z.number().positive()
|
||||
}).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 { title, description, valueChf } = parsed.data;
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO help_requests (requester_id, title, description, value_chf) VALUES (?, ?, ?, ?)',
|
||||
[req.user.userId, title, description, valueChf]
|
||||
);
|
||||
const { title, description, valueChf } = parsed.data;
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO help_requests (requester_id, title, description, value_chf) VALUES (?, ?, ?, ?)',
|
||||
[req.user.userId, title, description, valueChf]
|
||||
);
|
||||
|
||||
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;
|
||||
|
|
@ -6,59 +6,98 @@ import { requireAuth } from '../middleware/auth.js';
|
|||
const router = Router();
|
||||
|
||||
router.post('/:requestId', requireAuth, async (req, res) => {
|
||||
const requestId = Number(req.params.requestId);
|
||||
const parsed = z.object({ amountChf: z.number().positive(), message: z.string().max(2000).optional() }).safeParse(req.body);
|
||||
if (!parsed.success || Number.isNaN(requestId)) return res.status(400).json({ error: 'Invalid payload' });
|
||||
try {
|
||||
const requestId = Number(req.params.requestId);
|
||||
if (Number.isNaN(requestId)) {
|
||||
return res.status(400).json({ error: 'Invalid requestId' });
|
||||
}
|
||||
|
||||
const { amountChf, message } = parsed.data;
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO offers (request_id, helper_id, amount_chf, message)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[requestId, req.user.userId, amountChf, message || null]
|
||||
);
|
||||
const parsed = z.object({
|
||||
amountChf: z.number().positive(),
|
||||
message: z.string().max(2000).optional()
|
||||
}).safeParse(req.body);
|
||||
|
||||
await pool.query('UPDATE help_requests SET status = ? WHERE id = ?', ['negotiating', requestId]);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: 'Invalid payload' });
|
||||
}
|
||||
|
||||
res.status(201).json({ id: result.insertId });
|
||||
const { amountChf, message } = parsed.data;
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO offers (request_id, helper_id, amount_chf, message)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[requestId, req.user.userId, amountChf, message || null]
|
||||
);
|
||||
|
||||
await pool.query('UPDATE help_requests SET status = ? WHERE id = ?', ['negotiating', requestId]);
|
||||
|
||||
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) => {
|
||||
const offerId = Number(req.params.offerId);
|
||||
const parsed = z.object({ amountChf: z.number().positive(), message: z.string().max(2000).optional() }).safeParse(req.body);
|
||||
if (!parsed.success || Number.isNaN(offerId)) return res.status(400).json({ error: 'Invalid payload' });
|
||||
try {
|
||||
const offerId = Number(req.params.offerId);
|
||||
if (Number.isNaN(offerId)) {
|
||||
return res.status(400).json({ error: 'Invalid offerId' });
|
||||
}
|
||||
|
||||
const { amountChf, message } = parsed.data;
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO negotiations (offer_id, sender_id, amount_chf, message)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[offerId, req.user.userId, amountChf, message || null]
|
||||
);
|
||||
const parsed = z.object({
|
||||
amountChf: z.number().positive(),
|
||||
message: z.string().max(2000).optional()
|
||||
}).safeParse(req.body);
|
||||
|
||||
await pool.query('UPDATE offers SET status = ? WHERE id = ?', ['countered', offerId]);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: 'Invalid payload' });
|
||||
}
|
||||
|
||||
res.status(201).json({ id: result.insertId });
|
||||
const { amountChf, message } = parsed.data;
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO negotiations (offer_id, sender_id, amount_chf, message)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[offerId, req.user.userId, amountChf, message || null]
|
||||
);
|
||||
|
||||
await pool.query('UPDATE offers SET status = ? WHERE id = ?', ['countered', offerId]);
|
||||
|
||||
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) => {
|
||||
const offerId = Number(req.params.offerId);
|
||||
if (Number.isNaN(offerId)) return res.status(400).json({ error: 'Invalid offerId' });
|
||||
try {
|
||||
const offerId = Number(req.params.offerId);
|
||||
if (Number.isNaN(offerId)) {
|
||||
return res.status(400).json({ error: 'Invalid offerId' });
|
||||
}
|
||||
|
||||
const [offers] = await pool.query(
|
||||
'SELECT id, request_id, amount_chf FROM offers WHERE id = ? LIMIT 1',
|
||||
[offerId]
|
||||
);
|
||||
const offer = offers[0];
|
||||
if (!offer) return res.status(404).json({ error: 'Offer not found' });
|
||||
const [offers] = await pool.query(
|
||||
'SELECT id, request_id, amount_chf FROM offers WHERE id = ? LIMIT 1',
|
||||
[offerId]
|
||||
);
|
||||
const offer = offers[0];
|
||||
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 help_requests SET status = ? WHERE id = ?', ['agreed', offer.request_id]);
|
||||
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]);
|
||||
|
||||
const [dealResult] = await pool.query(
|
||||
'INSERT INTO deals (request_id, offer_id, agreed_amount_chf) VALUES (?, ?, ?)',
|
||||
[offer.request_id, offer.id, offer.amount_chf]
|
||||
);
|
||||
const [dealResult] = await pool.query(
|
||||
'INSERT INTO deals (request_id, offer_id, agreed_amount_chf) VALUES (?, ?, ?)',
|
||||
[offer.request_id, offer.id, offer.amount_chf]
|
||||
);
|
||||
|
||||
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;
|
||||
|
|
@ -7,13 +7,18 @@ import { encryptText } from '../services/encryption.js';
|
|||
const router = Router();
|
||||
|
||||
router.post('/phone', requireAuth, async (req, res) => {
|
||||
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() });
|
||||
try {
|
||||
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() });
|
||||
|
||||
const encryptedPhone = encryptText(parsed.data.phone);
|
||||
await pool.query('UPDATE users SET phone_encrypted = ? WHERE id = ?', [encryptedPhone, req.user.userId]);
|
||||
const encryptedPhone = encryptText(parsed.data.phone);
|
||||
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;
|
||||
|
|
@ -6,24 +6,35 @@ import { requireAuth } from '../middleware/auth.js';
|
|||
const router = Router();
|
||||
|
||||
router.post('/:dealId', requireAuth, async (req, res) => {
|
||||
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);
|
||||
try {
|
||||
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);
|
||||
|
||||
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 earliest = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000);
|
||||
const latest = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
|
||||
const now = new Date();
|
||||
const earliest = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000);
|
||||
const latest = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const { revieweeId, rating, comment } = parsed.data;
|
||||
const { revieweeId, rating, comment } = parsed.data;
|
||||
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO reviews (deal_id, reviewer_id, reviewee_id, rating, comment, earliest_prompt_at, latest_prompt_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[dealId, req.user.userId, revieweeId, rating, comment || null, earliest, latest]
|
||||
);
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO reviews (deal_id, reviewer_id, reviewee_id, rating, comment, earliest_prompt_at, latest_prompt_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[dealId, req.user.userId, revieweeId, rating, comment || null, earliest, latest]
|
||||
);
|
||||
|
||||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue