All files invites.ts

100% Statements 35/35
100% Branches 4/4
100% Functions 2/2
100% Lines 31/31

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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 632x   2x 2x 2x   2x   2x 1x   2x   2x   2x 2x 2x 2x     2x 208x 208x 1248x   208x                 2x 6x 6x 4x 1x           3x 8x 8x 8x   2x 2x           2x     1x    
import { randomInt } from "node:crypto";
 
import { getApps, initializeApp } from "firebase-admin/app";
import { FieldValue, getFirestore, Timestamp } from "firebase-admin/firestore";
import { HttpsError, onCall } from "firebase-functions/https";
 
import { isAnonymous } from "./authGuard";
 
if (getApps().length === 0) {
  initializeApp();
}
const db = getFirestore();
 
const REGION = "europe-west2";
// Crockford-style alphabet — no I, L, O, U, 0 or 1, so a code is unambiguous to read aloud.
const ALPHABET = "23456789ABCDEFGHJKMNPQRSTVWXYZ";
const CODE_LENGTH = 6;
const INVITE_TTL_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
const MAX_ATTEMPTS = 5;
 
/** A short, human-friendly, single-use invite code: `GLM-XXXXXX`. */
export function generateCode(): string {
  let body = "";
  for (let i = 0; i < CODE_LENGTH; i++) {
    body += ALPHABET[randomInt(ALPHABET.length)];
  }
  return `GLM-${body}`;
}
 
/**
 * Mint an invite code the caller can share with their partner. The code maps to
 * the caller's uid (`invites/{code}` → { inviterId }); `pairWithCode` consumes it.
 * Codes are single-use + expiring, and `invites/*` is locked to functions only.
 * Anonymous callers are rejected — you must connect a provider before pairing.
 */
export const createInvite = onCall({ region: REGION }, async (request) => {
  const uid = request.auth?.uid;
  if (!uid) throw new HttpsError("unauthenticated", "Sign in first.");
  if (isAnonymous(request.auth)) {
    throw new HttpsError(
      "failed-precondition",
      "Connect your account before inviting your partner.",
    );
  }
 
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
    const code = generateCode();
    const ref = db.doc(`invites/${code}`);
    if ((await ref.get()).exists) continue;
 
    const expiresAt = Timestamp.fromMillis(Date.now() + INVITE_TTL_MS);
    await ref.set({
      inviterId: uid,
      createdAt: FieldValue.serverTimestamp(),
      expiresAt,
      used: false,
    });
    return { code, expiresAt: expiresAt.toMillis() };
  }
 
  throw new HttpsError("resource-exhausted", "Could not mint a unique code. Please try again.");
});