41 lines
3.0 KiB
JavaScript
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};
|
|
});
|