All files users.ts

100% Statements 39/39
100% Branches 11/11
100% Functions 2/2
100% Lines 34/34

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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 842x 2x 2x   2x 1x   2x   2x               2x 6x 6x   5x 5x 5x   5x 5x 5x   5x         5x 5x   5x 5x                         2x                             2x 3x 3x   2x 2x 14x 14x   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();
 
const REGION = "europe-west2";
 
/**
 * Create (or refresh) the caller's `users/{uid}` document. Idempotent — the client
 * calls this on every auth resolution (anonymous create OR recovery sign-in), so
 * `createdAt` is only stamped once and the current providers are recorded each time.
 * Client writes to `users/*` are forbidden by the security rules; this is the only writer.
 */
export const initUser = onCall<{ locale?: string }>({ region: REGION }, async (request) => {
  const uid = request.auth?.uid;
  if (!uid) throw new HttpsError("unauthenticated", "Sign in first.");
 
  const firebase = request.auth?.token?.firebase;
  const signInProvider = firebase?.sign_in_provider ?? "anonymous";
  const linkedProviders = Object.keys(firebase?.identities ?? {});
 
  const ref = db.doc(`users/${uid}`);
  const snap = await ref.get();
  const created = !snap.exists;
 
  const data: Record<string, unknown> = {
    signInProvider,
    linkedProviders,
    updatedAt: FieldValue.serverTimestamp(),
  };
  if (created) data.createdAt = FieldValue.serverTimestamp();
  if (request.data?.locale) data.locale = request.data.locale;
 
  await ref.set(data, { merge: true });
  return { created };
});
 
type ProfileInput = {
  displayName?: string;
  gender?: string;
  age?: number;
  relationship?: string;
  anniversary?: string;
  notificationsEnabled?: boolean;
  completed?: boolean;
};
 
const PROFILE_FIELDS = [
  "displayName",
  "gender",
  "age",
  "relationship",
  "anniversary",
  "notificationsEnabled",
  "completed",
] as const;
 
/**
 * Persist onboarding answers onto the caller's user doc. Only the whitelisted
 * profile fields are accepted (arbitrary keys are ignored), keeping the
 * server-owned document tamper-proof while the client stays the source of input.
 */
export const updateProfile = onCall<ProfileInput>({ region: REGION }, async (request) => {
  const uid = request.auth?.uid;
  if (!uid) throw new HttpsError("unauthenticated", "Sign in first.");
 
  const updates: Record<string, unknown> = {};
  for (const field of PROFILE_FIELDS) {
    const value = request.data?.[field];
    if (value !== undefined) updates[field] = value;
  }
  if (Object.keys(updates).length === 0) {
    throw new HttpsError("invalid-argument", "No profile fields to update.");
  }
  updates.updatedAt = FieldValue.serverTimestamp();
 
  await db.doc(`users/${uid}`).set(updates, { merge: true });
  return { ok: true };
});