From db36e75c46c67efc5db16e8a3b31392e78fd57c4 Mon Sep 17 00:00:00 2001 From: openclaw Date: Wed, 4 Mar 2026 16:51:07 +0000 Subject: [PATCH] feat: implement postal address verification flow --- README.md | 1 + backend/src/routes/addresses.js | 82 +++++++++++++++++++++++++++++++++ backend/src/server.js | 2 + 3 files changed, 85 insertions(+) create mode 100644 backend/src/routes/addresses.js diff --git a/README.md b/README.md index 2ec7d72..3f8b16b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Erster funktionaler Backend-Stand für die Vision: - Angebote + Gegenangebote + Deal-Annahme (`/offers/...`) - Bewertungsgrundlage mit 2-14 Tage Prompt-Fenster (`/reviews/:dealId`) - Datenmodell inkl. postalischer Adress-Verifikation (`backend/sql/schema.sql`) +- Address-Change-Flow mit Briefcode (`/addresses/change-request`, `/addresses/verify`) ## Start diff --git a/backend/src/routes/addresses.js b/backend/src/routes/addresses.js new file mode 100644 index 0000000..2c50115 --- /dev/null +++ b/backend/src/routes/addresses.js @@ -0,0 +1,82 @@ +import { Router } from 'express'; +import { createHash, randomInt } from 'crypto'; +import { z } from 'zod'; +import { pool } from '../db/connection.js'; +import { requireAuth } from '../middleware/auth.js'; + +const router = Router(); + +const hashCode = (code) => createHash('sha256').update(code).digest('hex'); + +router.post('/change-request', requireAuth, async (req, res) => { + const parsed = z.object({ newAddressEncrypted: z.string().min(10) }).safeParse(req.body); + if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); + + const verificationCode = String(randomInt(100000, 999999)); + const verificationCodeHash = hashCode(verificationCode); + + const [result] = await pool.query( + `INSERT INTO address_change_requests (user_id, new_address_encrypted, verification_code_hash) + VALUES (?, ?, ?)`, + [req.user.userId, parsed.data.newAddressEncrypted, verificationCodeHash] + ); + + res.status(201).json({ + requestId: result.insertId, + postalDispatch: 'pending_letter', + note: 'Verification code generated for postal letter dispatch.', + verificationCode + }); +}); + +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(); + + 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(); + throw err; + } finally { + conn.release(); + } + + res.json({ status: 'verified' }); +}); + +export default router; diff --git a/backend/src/server.js b/backend/src/server.js index 5c59a2a..76e6e41 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -4,6 +4,7 @@ import authRoutes from './routes/auth.js'; import helpRequestRoutes from './routes/helpRequests.js'; import offerRoutes from './routes/offers.js'; import reviewRoutes from './routes/reviews.js'; +import addressRoutes from './routes/addresses.js'; dotenv.config(); @@ -16,6 +17,7 @@ app.use('/auth', authRoutes); app.use('/requests', helpRequestRoutes); app.use('/offers', offerRoutes); app.use('/reviews', reviewRoutes); +app.use('/addresses', addressRoutes); const port = Number(process.env.PORT || 3000); app.listen(port, () => {