Files
2026-09-09 10:12:49 +02:00

41 lines
3.0 KiB
JavaScript

const {initializeApp}=require('firebase-admin/app');
const {getFirestore,FieldValue}=require('firebase-admin/firestore');
const {getAuth}=require('firebase-admin/auth');
const {onCall,HttpsError}=require('firebase-functions/v2/https');
initializeApp();
const db=getFirestore();
const options={region:'europe-west1',maxInstances:3};
function uidOf(request){if(!request.auth)throw new HttpsError('unauthenticated','Connexion requise.');return request.auth.uid;}
exports.deleteEntity=onCall(options,async request=>{
const uid=uidOf(request),{kind,id}=request.data??{};
if(!['measurement','session'].includes(kind)||typeof id!=='string'||!/^[a-zA-Z0-9-]{1,100}$/.test(id))throw new HttpsError('invalid-argument','Entité invalide.');
const user=db.doc(`users/${uid}`),ref=user.collection(kind==='measurement'?'measurements':'sessions').doc(id);
// Tombstone and deletion are atomic. It remains even if cleanup below retries.
await db.runTransaction(async tx=>{const profile=await tx.get(user);if(profile.data()?.accountState!=='active')throw new HttpsError('failed-precondition','Compte indisponible.');const tombstone=user.collection('tombstones').doc(`${kind}_${id}`);const existing=await tx.get(tombstone);if(!existing.exists)tx.create(tombstone,{schemaVersion:1,entityId:id,entityKind:kind,createdAt:FieldValue.serverTimestamp(),updatedAt:FieldValue.serverTimestamp()});tx.delete(ref);if(kind==='measurement')tx.delete(user.collection('measurementAnnotations').doc(id));});
if(kind==='measurement'){
// Bounded batches; repeated calls finish cleanup after a transient failure.
for(const field of ['beforeMeasurementId','afterMeasurementId']){
let page;
do {page=await user.collection('sessions').where(field,'==',id).limit(200).get();if(page.empty)break;const batch=db.batch();for(const d of page.docs)batch.update(d.ref,{[field]:null,revision:FieldValue.increment(1),updatedAt:FieldValue.serverTimestamp()});await batch.commit();}while(!page.empty);
}
}
// Derived data is disposable. Recompute before exposing it again.
await db.recursiveDelete(user.collection('dailyStats'));
await db.recursiveDelete(user.collection('baselines'));
return {deleted:true};
});
exports.deleteAccount=onCall(options,async request=>{
const uid=uidOf(request);
if(Date.now()/1000-request.auth.token.auth_time>300)throw new HttpsError('failed-precondition','Reconnecte-toi avant de supprimer ton compte.');
const user=db.doc(`users/${uid}`);
await user.set({accountState:'deleting',updatedAt:FieldValue.serverTimestamp()},{merge:true});
// Keep blocking profile until all child collections have been removed.
for(const collection of await user.listCollections())await db.recursiveDelete(collection);
await getAuth().revokeRefreshTokens(uid);
try{await getAuth().deleteUser(uid);}catch(e){if(e.code!=='auth/user-not-found')throw e;}
// Retain a minimal deleting marker to deny old ID-token replay until expiry.
// No profile or health information remains in the marker.
await user.set({schemaVersion:1,accountState:'deleting',updatedAt:FieldValue.serverTimestamp()});
return {deleted:true};
});