spotify/server/dist/routes/partners.js
2025-10-16 13:07:44 +02:00

173 lines
7.8 KiB
JavaScript

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' });
}
});