All files pairing.ts

100% Statements 23/23
100% Branches 5/5
100% Functions 1/1
100% Lines 19/19

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 412x 2x 2x   2x 1x   2x                   2x 7x 7x   5x 5x   3x 3x   2x 2x   1x 1x         1x   1x    
import { getApps, initializeApp } from "firebase-admin/app";
import { FieldValue, getFirestore } from "firebase-admin/firestore";
import { HttpsError, onCall } from "firebase-functions/https";
 
if (getApps().length === 0) {
  initializeApp();
}
const db = getFirestore();
 
/**
 * Pair the caller with the partner who minted `code`. Couples are created
 * server-side only (security rules forbid client writes to `couples`), so
 * membership can never be forged. A couple is two symmetric members.
 *
 * Skeleton: expects an `invites/{code}` doc `{ inviterId }`. Hardening to do —
 * single-use codes, expiry, rate limiting, already-paired guards.
 */
export const pairWithCode = onCall<{ code: string }>({ region: "europe-west2" }, async (request) => {
  const uid = request.auth?.uid;
  if (!uid) throw new HttpsError("unauthenticated", "Sign in first.");
 
  const code = request.data.code?.trim().toUpperCase();
  if (!code) throw new HttpsError("invalid-argument", "Missing invite code.");
 
  const inviteSnap = await db.doc(`invites/${code}`).get();
  if (!inviteSnap.exists) throw new HttpsError("not-found", "That code doesn't look right.");
 
  const inviterId = inviteSnap.get("inviterId") as string;
  if (inviterId === uid) throw new HttpsError("failed-precondition", "You can't pair with yourself.");
 
  const coupleRef = db.collection("couples").doc();
  await coupleRef.set({
    members: [inviterId, uid],
    createdAt: FieldValue.serverTimestamp(),
    togetherSince: FieldValue.serverTimestamp(),
  });
  await inviteSnap.ref.delete();
 
  return { coupleId: coupleRef.id };
});