@@ -450,6 +450,23 @@ def _is_basic_auth_valid(req, env) -> bool:
450450 "CREATE INDEX IF NOT EXISTS idx_sa_session ON session_attendance(session_id)" ,
451451 "CREATE INDEX IF NOT EXISTS idx_sa_user ON session_attendance(user_id)" ,
452452 "CREATE INDEX IF NOT EXISTS idx_at_activity ON activity_tags(activity_id)" ,
453+ # Peer Connections
454+ """CREATE TABLE IF NOT EXISTS peer_connections (
455+ id TEXT PRIMARY KEY,
456+ requester_id TEXT NOT NULL,
457+ addressee_id TEXT NOT NULL,
458+ status TEXT NOT NULL DEFAULT 'pending',
459+ message TEXT,
460+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
461+ updated_at TEXT,
462+ UNIQUE (requester_id, addressee_id),
463+ FOREIGN KEY (requester_id) REFERENCES users(id) ON DELETE CASCADE,
464+ FOREIGN KEY (addressee_id) REFERENCES users(id) ON DELETE CASCADE
465+ )""" ,
466+ "CREATE INDEX IF NOT EXISTS idx_pc_requester ON peer_connections(requester_id)" ,
467+ "CREATE INDEX IF NOT EXISTS idx_pc_addressee ON peer_connections(addressee_id)" ,
468+ "CREATE INDEX IF NOT EXISTS idx_pc_status ON peer_connections(status)" ,
469+
453470]
454471
455472
@@ -1291,6 +1308,25 @@ async def _dispatch(request, env):
12911308 if path == "/api/admin/table-counts" and method == "GET" :
12921309 return await api_admin_table_counts (request , env )
12931310
1311+
1312+ # Peer Connections
1313+ if path == "/api/peers" and method == "GET" :
1314+ return await api_list_peers (request , env )
1315+ if path == "/api/peers/requests" and method == "GET" :
1316+ return await api_list_peer_requests (request , env )
1317+ m_pc1 = re .fullmatch (r"/api/peers/request/([A-Za-z0-9_-]+)" , path )
1318+ if m_pc1 and method == "POST" :
1319+ return await api_send_peer_request (request , env , m_pc1 .group (1 ))
1320+ m_pc2 = re .fullmatch (r"/api/peers/([A-Za-z0-9_-]+)/accept" , path )
1321+ if m_pc2 and method == "POST" :
1322+ return await api_accept_peer_request (request , env , m_pc2 .group (1 ))
1323+ m_pc3 = re .fullmatch (r"/api/peers/([A-Za-z0-9_-]+)/decline" , path )
1324+ if m_pc3 and method == "POST" :
1325+ return await api_decline_peer_request (request , env , m_pc3 .group (1 ))
1326+ m_pc4 = re .fullmatch (r"/api/peers/([A-Za-z0-9_-]+)" , path )
1327+ if m_pc4 and method == "DELETE" :
1328+ return await api_remove_peer (request , env , m_pc4 .group (1 ))
1329+
12941330 return err ("API endpoint not found" , 404 )
12951331
12961332 return await serve_static (path , env )
@@ -1301,4 +1337,155 @@ async def on_fetch(request, env):
13011337 return await _dispatch (request , env )
13021338 except Exception as e :
13031339 capture_exception (e , request , env , "on_fetch_unhandled" )
1304- return err ("Internal server error" , 500 )
1340+ return err ("Internal server error" , 500 )
1341+
1342+ # ---------------------------------------------------------------------------
1343+ # Peer Connections API
1344+ # ---------------------------------------------------------------------------
1345+
1346+ async def api_list_peers (req , env ):
1347+ """GET /api/peers — list accepted connections."""
1348+ user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
1349+ if not user :
1350+ return err ("Authentication required" , 401 )
1351+ enc = env .ENCRYPTION_KEY
1352+ rows = await env .DB .prepare (
1353+ "SELECT pc.id, pc.created_at,"
1354+ " CASE WHEN pc.requester_id=? THEN pc.addressee_id ELSE pc.requester_id END AS peer_id,"
1355+ " u.name AS peer_name_enc"
1356+ " FROM peer_connections pc"
1357+ " JOIN users u ON u.id = CASE WHEN pc.requester_id=? THEN pc.addressee_id ELSE pc.requester_id END"
1358+ " WHERE (pc.requester_id=? OR pc.addressee_id=?) AND pc.status='accepted'"
1359+ " ORDER BY pc.created_at DESC"
1360+ ).bind (user ["id" ], user ["id" ], user ["id" ], user ["id" ]).all ()
1361+ peers = []
1362+ for r in rows .results or []:
1363+ peers .append ({
1364+ "connection_id" : r ["id" ],
1365+ "peer_id" : r ["peer_id" ],
1366+ "peer_name" : await decrypt_aes (r ["peer_name_enc" ] or "" , enc ),
1367+ "connected_at" : r ["created_at" ],
1368+ })
1369+ return ok (peers )
1370+
1371+
1372+ async def api_list_peer_requests (req , env ):
1373+ """GET /api/peers/requests — list pending incoming requests."""
1374+ user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
1375+ if not user :
1376+ return err ("Authentication required" , 401 )
1377+ enc = env .ENCRYPTION_KEY
1378+ rows = await env .DB .prepare (
1379+ "SELECT pc.id, pc.message, pc.created_at, u.name AS requester_name_enc, pc.requester_id"
1380+ " FROM peer_connections pc"
1381+ " JOIN users u ON u.id = pc.requester_id"
1382+ " WHERE pc.addressee_id=? AND pc.status='pending'"
1383+ " ORDER BY pc.created_at DESC"
1384+ ).bind (user ["id" ]).all ()
1385+ requests = []
1386+ for r in rows .results or []:
1387+ requests .append ({
1388+ "connection_id" : r ["id" ],
1389+ "requester_id" : r ["requester_id" ],
1390+ "requester_name" : await decrypt_aes (r ["requester_name_enc" ] or "" , enc ),
1391+ "message" : r ["message" ] or "" ,
1392+ "requested_at" : r ["created_at" ],
1393+ })
1394+ return ok (requests )
1395+
1396+
1397+ async def api_send_peer_request (req , env , addressee_id : str ):
1398+ """POST /api/peers/request/:id — send a connection request."""
1399+ user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
1400+ if not user :
1401+ return err ("Authentication required" , 401 )
1402+ if user ["id" ] == addressee_id :
1403+ return err ("You cannot connect with yourself" , 400 )
1404+ addressee = await env .DB .prepare ("SELECT id FROM users WHERE id=?" ).bind (addressee_id ).first ()
1405+ if not addressee :
1406+ return err ("User not found" , 404 )
1407+ existing = await env .DB .prepare (
1408+ "SELECT id, status FROM peer_connections"
1409+ " WHERE (requester_id=? AND addressee_id=?) OR (requester_id=? AND addressee_id=?)"
1410+ ).bind (user ["id" ], addressee_id , addressee_id , user ["id" ]).first ()
1411+ if existing :
1412+ if existing ["status" ] == "accepted" :
1413+ return err ("Already connected" , 409 )
1414+ if existing ["status" ] == "pending" :
1415+ return err ("Connection request already sent" , 409 )
1416+ if existing ["status" ] == "declined" :
1417+ return err ("Your previous request was declined" , 409 )
1418+ body , bad = await parse_json_object (req )
1419+ if bad :
1420+ return bad
1421+ raw_msg = body .get ("message" )
1422+ message = (raw_msg .strip ()[:200 ] if isinstance (raw_msg , str ) else "" )
1423+ conn_id = new_id ()
1424+ try :
1425+ await env .DB .prepare (
1426+ "INSERT INTO peer_connections (id, requester_id, addressee_id, message)"
1427+ " VALUES (?, ?, ?, ?)"
1428+ ).bind (conn_id , user ["id" ], addressee_id , message ).run ()
1429+ except Exception as exc :
1430+ capture_exception (exc , req , env , "api_send_peer_request.insert" )
1431+ return err ("Failed to send request" , 500 )
1432+ return ok ({"connection_id" : conn_id }, "Connection request sent" )
1433+
1434+
1435+ async def api_accept_peer_request (req , env , connection_id : str ):
1436+ """POST /api/peers/:id/accept — accept a pending request."""
1437+ user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
1438+ if not user :
1439+ return err ("Authentication required" , 401 )
1440+ pc = await env .DB .prepare (
1441+ "SELECT id, status FROM peer_connections WHERE id=? AND addressee_id=?"
1442+ ).bind (connection_id , user ["id" ]).first ()
1443+ if not pc :
1444+ return err ("Connection request not found" , 404 )
1445+ if pc ["status" ] != "pending" :
1446+ return err ("Request is not pending" , 409 )
1447+ try :
1448+ await env .DB .prepare (
1449+ "UPDATE peer_connections SET status='accepted', updated_at=datetime('now') WHERE id=?"
1450+ ).bind (connection_id ).run ()
1451+ except Exception as exc :
1452+ capture_exception (exc , req , env , "api_accept_peer_request.update" )
1453+ return err ("Failed to accept request" , 500 )
1454+ return ok (msg = "Connection accepted" )
1455+
1456+
1457+ async def api_decline_peer_request (req , env , connection_id : str ):
1458+ """POST /api/peers/:id/decline — decline a pending request."""
1459+ user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
1460+ if not user :
1461+ return err ("Authentication required" , 401 )
1462+ pc = await env .DB .prepare (
1463+ "SELECT id, status FROM peer_connections WHERE id=? AND addressee_id=?"
1464+ ).bind (connection_id , user ["id" ]).first ()
1465+ if not pc :
1466+ return err ("Connection request not found" , 404 )
1467+ if pc ["status" ] != "pending" :
1468+ return err ("Request is not pending" , 409 )
1469+ try :
1470+ await env .DB .prepare (
1471+ "UPDATE peer_connections SET status='declined', updated_at=datetime('now') WHERE id=?"
1472+ ).bind (connection_id ).run ()
1473+ except Exception as exc :
1474+ capture_exception (exc , req , env , "api_decline_peer_request.update" )
1475+ return err ("Failed to decline request" , 500 )
1476+ return ok (msg = "Connection declined" )
1477+
1478+
1479+ async def api_remove_peer (req , env , connection_id : str ):
1480+ """DELETE /api/peers/:id — remove an accepted connection."""
1481+ user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
1482+ if not user :
1483+ return err ("Authentication required" , 401 )
1484+ pc = await env .DB .prepare (
1485+ "SELECT id FROM peer_connections"
1486+ " WHERE id=? AND (requester_id=? OR addressee_id=?) AND status='accepted'"
1487+ ).bind (connection_id , user ["id" ], user ["id" ]).first ()
1488+ if not pc :
1489+ return err ("Connection not found" , 404 )
1490+ await env .DB .prepare ("DELETE FROM peer_connections WHERE id=?" ).bind (connection_id ).run ()
1491+ return ok (msg = "Connection removed" )
0 commit comments