173 lines
7.8 KiB
JavaScript
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' });
|
|
}
|
|
});
|