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 84 | 2x 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 };
});
|