import { Router } from 'express'; import { db } from '../lib/db.js'; import { addSseClient, sendEvent, sendToBoth } from '../lib/events.js'; import { requireAuth } from '../middleware/auth.js'; export const partnersRouter = Router(); // Send a partner request (not via link) by target uid partnersRouter.post('/request', requireAuth, (req, res) => { const { toUid } = req.body; const fromUid = req.user?.uid; if (!fromUid || !toUid || fromUid === toUid) return res.status(400).json({ error: 'Invalid uids' }); const now = Date.now(); try { db.db .prepare(`INSERT INTO friend_requests (from_user_id, to_user_id, status, created_at, updated_at) VALUES (?, ?, 'pending', ?, ?)`) .run(fromUid, toUid, now, now); // Notify the recipient about a new incoming request sendEvent(toUid, { type: 'partner:request', fromUserId: fromUid, at: now }); res.json({ ok: true }); } catch (e) { if (e && String(e.message).includes('UNIQUE')) { return res.status(409).json({ error: 'Request already exists' }); } throw e; } }); // List incoming requests for a user partnersRouter.get('/requests/:uid', requireAuth, (req, res) => { const { uid } = req.params; if (!req.user || req.user.uid !== uid) return res.status(403).json({ error: 'Forbidden' }); const rows = db.db .prepare("SELECT id, from_user_id, to_user_id, status, created_at FROM friend_requests WHERE to_user_id = ? AND status = 'pending' ORDER BY created_at DESC") .all(uid); res.json(rows); }); // Accept a request partnersRouter.post('/requests/:id/accept', requireAuth, (req, res) => { const { id } = req.params; const reqRow = db.db.prepare('SELECT * FROM friend_requests WHERE id = ?').get(id); if (!reqRow) return res.status(404).json({ error: 'Not found' }); if (!req.user || req.user.uid !== reqRow.to_user_id) return res.status(403).json({ error: 'Forbidden' }); const now = Date.now(); const userA = reqRow.from_user_id < reqRow.to_user_id ? reqRow.from_user_id : reqRow.to_user_id; const userB = reqRow.from_user_id < reqRow.to_user_id ? reqRow.to_user_id : reqRow.from_user_id; const txn = db.db.transaction(() => { db.db.prepare("UPDATE friend_requests SET status='accepted', updated_at=? WHERE id=?").run(now, id); db.db.prepare('INSERT OR IGNORE INTO friendships (user_a_id, user_b_id, created_at) VALUES (?, ?, ?)').run(userA, userB, now); }); txn(); // Notify both users of partnership established sendToBoth(userA, userB, { type: 'partner:connected', a: userA, b: userB, at: now }); res.json({ ok: true }); }); // Decline a request partnersRouter.post('/requests/:id/decline', requireAuth, (req, res) => { const { id } = req.params; const now = Date.now(); const reqRow = db.db.prepare('SELECT * FROM friend_requests WHERE id = ?').get(id); if (!reqRow) return res.status(404).json({ error: 'Not found' }); if (!req.user || req.user.uid !== reqRow.to_user_id) return res.status(403).json({ error: 'Forbidden' }); db.db.prepare("UPDATE friend_requests SET status='declined', updated_at=? WHERE id=?").run(now, id); // Notify requester that their request was declined sendEvent(reqRow.from_user_id, { type: 'partner:declined', byUserId: reqRow.to_user_id, requestId: reqRow.id, at: now }); res.json({ ok: true }); }); // Check partnership between two users partnersRouter.get('/status', (req, res) => { const { a, b } = req.query; if (!a || !b) return res.status(400).json({ error: 'Missing params' }); const [userA, userB] = a < b ? [a, b] : [b, a]; const exists = db.db .prepare('SELECT 1 FROM friendships WHERE user_a_id=? AND user_b_id=?') .get(userA, userB); res.json({ partnered: !!exists }); }); // Fetch partner for a user (if any) partnersRouter.get('/partner/:uid', requireAuth, (req, res) => { const { uid } = req.params; if (!req.user || req.user.uid !== uid) return res.status(403).json({ error: 'Forbidden' }); const row = db.db .prepare(`SELECT CASE WHEN user_a_id = ? THEN user_b_id ELSE user_a_id END as partner_id FROM friendships WHERE user_a_id = ? OR user_b_id = ? LIMIT 1`) .get(uid, uid, uid); if (!row?.partner_id) return res.json({ partnerId: null }); return res.json({ partnerId: row.partner_id }); }); // SSE: subscribe to partner events for authed user partnersRouter.get('/events/:uid', requireAuth, (req, res) => { const { uid } = req.params; if (!req.user || req.user.uid !== uid) return res.status(403).json({ error: 'Forbidden' }); res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.flushHeaders?.(); const cleanup = addSseClient(uid, res); req.on('close', cleanup); }); // Remove an existing partnership between the authed user and a specific partner partnersRouter.post('/remove', requireAuth, (req, res) => { const { partnerId } = req.body; const uid = req.user?.uid; if (!uid || !partnerId || uid === partnerId) return res.status(400).json({ error: 'Invalid partnerId' }); const userA = uid < partnerId ? uid : partnerId; const userB = uid < partnerId ? partnerId : uid; const existing = db.db .prepare('SELECT id FROM friendships WHERE user_a_id=? AND user_b_id=?') .get(userA, userB); if (!existing?.id) return res.status(404).json({ error: 'No active partnership' }); try { const txn = db.db.transaction(() => { db.db.prepare('DELETE FROM friendships WHERE user_a_id=? AND user_b_id=?').run(userA, userB); // Remove any historical or pending requests between the two users so they can re-invite db.db .prepare('DELETE FROM friend_requests WHERE (from_user_id=? AND to_user_id=?) OR (from_user_id=? AND to_user_id=?)') .run(uid, partnerId, partnerId, uid); }); txn(); // Notify both users of disconnection sendToBoth(userA, userB, { type: 'partner:disconnected', a: userA, b: userB, at: Date.now() }); return res.json({ ok: true }); } catch (e) { return res.status(500).json({ error: 'Failed to remove partnership' }); } }); // Clear any existing partnership for the authed user (fallback helper) partnersRouter.post('/clear', requireAuth, (req, res) => { const { userId } = req.body; const uid = req.user?.uid; if (!uid || !userId || uid !== userId) return res.status(403).json({ error: 'Forbidden' }); try { // Find all partners (normally only one) and delete links + requests const partners = db.db .prepare('SELECT CASE WHEN user_a_id = ? THEN user_b_id ELSE user_a_id END as partner_id FROM friendships WHERE user_a_id = ? OR user_b_id = ?') .all(uid, uid, uid); const txn = db.db.transaction(() => { db.db.prepare('DELETE FROM friendships WHERE user_a_id=? OR user_b_id=?').run(uid, uid); for (const row of partners) { db.db .prepare('DELETE FROM friend_requests WHERE (from_user_id=? AND to_user_id=?) OR (from_user_id=? AND to_user_id=?)') .run(uid, row.partner_id, row.partner_id, uid); } }); txn(); // Notify the user and any former partners for (const row of partners) { const a = uid < row.partner_id ? uid : row.partner_id; const b = uid < row.partner_id ? row.partner_id : uid; sendToBoth(a, b, { type: 'partner:disconnected', a, b, at: Date.now() }); } return res.json({ ok: true }); } catch (e) { return res.status(500).json({ error: 'Failed to clear partnership' }); } });