From 3219091b9b3d6161a57563eb8c5aa2b8210f4338 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 06:15:46 +0000 Subject: [PATCH] refactor: extract duplicate house join logic into processInvite helper Extracts the duplicate database insert/update and notification logic used by `/:id/join` and `/join-by-token` in routes/houses.js into a shared function `processInvite(req, res, invite)`. This improves code health by removing duplicate code while preserving all existing API functionality and error responses. Co-authored-by: deitaur <113350206+deitaur@users.noreply.github.com> --- package-lock.json | 39 +++++++++++++++++++------------------ package.json | 2 +- routes/houses.js | 49 +++++++++++++++++++++-------------------------- 3 files changed, 43 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index b427e70..e62b0fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", "pg": "^8.16.3", - "supertest": "^7.1.4" + "supertest": "^7.2.2" } }, "node_modules/@ampproject/remapping": { @@ -994,9 +994,9 @@ } }, "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", "license": "MIT", "dependencies": { "@noble/hashes": "^1.1.5" @@ -2883,9 +2883,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5138,9 +5138,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -5771,33 +5771,34 @@ } }, "node_modules/superagent": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", - "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", "license": "MIT", "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.4", + "form-data": "^4.0.5", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", - "qs": "^6.11.2" + "qs": "^6.14.1" }, "engines": { "node": ">=14.18.0" } }, "node_modules/supertest": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", - "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", "license": "MIT", "dependencies": { + "cookie-signature": "^1.2.2", "methods": "^1.1.2", - "superagent": "^10.2.3" + "superagent": "^10.3.0" }, "engines": { "node": ">=14.18.0" diff --git a/package.json b/package.json index b27d579..874013c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,6 @@ "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", "pg": "^8.16.3", - "supertest": "^7.1.4" + "supertest": "^7.2.2" } } diff --git a/routes/houses.js b/routes/houses.js index fbea92d..3f8b8a1 100644 --- a/routes/houses.js +++ b/routes/houses.js @@ -63,29 +63,38 @@ module.exports = (pool, ensureAuthenticated) => { } }); - // POST /api/houses/:id/join - router.post('/:id/join', ensureAuthenticated, async (req, res) => { - const houseId = Number(req.params.id); - const token = req.body.token; - if (!token) return res.status(400).json({ error: 'Токен обязателен' }); + // Shared logic for processing a house join invite + async function processInvite(req, res, invite) { try { - const q = await pool.query('SELECT * FROM house_invites WHERE house_id=$1 AND token=$2 AND (expires_at IS NULL OR expires_at > now())', [houseId, token]); - if (q.rowCount === 0) return res.status(404).json({ error: 'Приглашение не найдено или просрочено' }); - const invite = q.rows[0]; await pool.query( `INSERT INTO house_memberships(house_id, user_id, role, created_at) VALUES($1,$2,$3,now()) ON CONFLICT (house_id,user_id) DO UPDATE SET role = EXCLUDED.role`, - [houseId, req.user.id, invite.role] + [invite.house_id, req.user.id, invite.role] ); await pool.query('UPDATE house_invites SET used = true WHERE id=$1', [invite.id]); if (invite.created_by) { const msg = `${req.user.name || req.user.email || 'Пользователь'} присоединился к дому (role=${invite.role})`; await pool.query('INSERT INTO notifications(user_id, payload, message, created_at) VALUES($1,$2,$3,now())', - [invite.created_by, JSON.stringify({ type: 'house_join', house_id: houseId, user_id: req.user.id, role: invite.role }), msg]); + [invite.created_by, JSON.stringify({ type: 'house_join', house_id: invite.house_id, user_id: req.user.id, role: invite.role }), msg]); } - const r = await pool.query('SELECT id, name FROM houses WHERE id=$1', [houseId]); - return res.json({ success:true, house_id: houseId, house_name: r.rows[0] ? r.rows[0].name : null, role: invite.role }); + const r = await pool.query('SELECT id, name FROM houses WHERE id=$1', [invite.house_id]); + return res.json({ success:true, house_id: invite.house_id, house_name: r.rows[0] ? r.rows[0].name : null, role: invite.role }); + } catch (err) { + console.error(err); + return res.status(500).json({ error: 'server error' }); + } + } + + // POST /api/houses/:id/join + router.post('/:id/join', ensureAuthenticated, async (req, res) => { + const houseId = Number(req.params.id); + const token = req.body.token; + if (!token) return res.status(400).json({ error: 'Токен обязателен' }); + try { + const q = await pool.query('SELECT * FROM house_invites WHERE house_id=$1 AND token=$2 AND (expires_at IS NULL OR expires_at > now())', [houseId, token]); + if (q.rowCount === 0) return res.status(404).json({ error: 'Приглашение не найдено или просрочено' }); + return await processInvite(req, res, q.rows[0]); } catch (err) { console.error(err); return res.status(500).json({ error: 'server error' }); @@ -99,21 +108,7 @@ module.exports = (pool, ensureAuthenticated) => { try { const q = await pool.query('SELECT * FROM house_invites WHERE token=$1 AND (expires_at IS NULL OR expires_at > now())', [token]); if (q.rowCount === 0) return res.status(404).json({ error: 'Приглашение не найдено' }); - const invite = q.rows[0]; - await pool.query( - `INSERT INTO house_memberships(house_id, user_id, role, created_at) - VALUES($1,$2,$3,now()) - ON CONFLICT (house_id,user_id) DO UPDATE SET role = EXCLUDED.role`, - [invite.house_id, req.user.id, invite.role] - ); - await pool.query('UPDATE house_invites SET used = true WHERE id=$1', [invite.id]); - if (invite.created_by) { - const msg = `${req.user.name || req.user.email || 'Пользователь'} присоединился к дому (role=${invite.role})`; - await pool.query('INSERT INTO notifications(user_id, payload, message, created_at) VALUES($1,$2,$3,now())', - [invite.created_by, JSON.stringify({ type: 'house_join', house_id: invite.house_id, user_id: req.user.id, role: invite.role }), msg]); - } - const r = await pool.query('SELECT id, name FROM houses WHERE id=$1', [invite.house_id]); - return res.json({ success:true, house_id: invite.house_id, house_name: r.rows[0] ? r.rows[0].name : null, role: invite.role }); + return await processInvite(req, res, q.rows[0]); } catch (err) { console.error(err); return res.status(500).json({ error: 'server error' });