Letzter Roadmap-Punkt von V3. Foto-basierte Lichtverhältnis-Analyse pro Stellplatz (Cloud Function analyzeLocation) und darauf aufbauende, rein textbasierte Eignungs-Bewertung einer Pflanze (assessPlantFit) — beides ausschließlich auf Abruf, nicht automatisch (Kostenkontrolle, eigener Anthropic-Key). Neue Stellplatz-Detailseite, Verlinkung im Pflanzen-Detail, Erkennung veralteter Bewertungen bei Umzug/Neuanalyse. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
115 lines
4.8 KiB
Text
115 lines
4.8 KiB
Text
rules_version = '2';
|
||
|
||
// Sicherheitsregeln für LeafItToMe.
|
||
//
|
||
// Grundprinzip: Alle App-Daten hängen an einem Haushalt. Lesen darf nur,
|
||
// wer im Feld `memberUids` des Haushalts steht. Schreiben hängt zusätzlich
|
||
// von der Rolle ab: "member" darf alles, "sitter" (Pflanzen-Sitter) darf
|
||
// nur Aufgaben bestätigen. Das eigene Nutzer-Dokument sieht nur der Nutzer.
|
||
service cloud.firestore {
|
||
match /databases/{database}/documents {
|
||
|
||
function signedIn() {
|
||
return request.auth != null;
|
||
}
|
||
|
||
function householdData(householdId) {
|
||
return get(/databases/$(database)/documents/households/$(householdId)).data;
|
||
}
|
||
|
||
function isMember(householdId) {
|
||
return signedIn()
|
||
&& request.auth.uid in householdData(householdId).memberUids;
|
||
}
|
||
|
||
// Rolle des aufrufenden Nutzers. Haushalte aus der Zeit vor V2 haben
|
||
// noch keine members-Map – dann gilt jeder Eingetragene als "member".
|
||
function roleOf(householdId) {
|
||
return householdData(householdId)
|
||
.get('members', {})
|
||
.get(request.auth.uid, {'role': 'member'})
|
||
.get('role', 'member');
|
||
}
|
||
|
||
// Eigenes Nutzerprofil (enthält u. a. die householdId).
|
||
match /users/{uid} {
|
||
allow read, write: if signedIn() && request.auth.uid == uid;
|
||
}
|
||
|
||
// Einladungscodes: Erstellen darf jedes Mitglied des jeweiligen
|
||
// Haushalts. Gelesen und eingelöst werden Codes ausschließlich über
|
||
// die Cloud Function joinHousehold (Admin-Rechte) – Clients nie direkt.
|
||
match /invites/{code} {
|
||
allow create: if signedIn()
|
||
&& request.resource.data.createdBy == request.auth.uid
|
||
&& isMember(request.resource.data.householdId);
|
||
allow read, update, delete: if false;
|
||
}
|
||
|
||
match /households/{householdId} {
|
||
allow read: if signedIn() && request.auth.uid in resource.data.memberUids;
|
||
// Vom Client aus dürfen volle Mitglieder nur den Namen ändern;
|
||
// jede Mitglieder-Verwaltung (Beitritt, Austritt, Entfernen) läuft
|
||
// ausschließlich über Cloud Functions mit Admin-Rechten.
|
||
// Ausnahme: Jeder darf seinen EIGENEN members-Eintrag pflegen (die App
|
||
// trägt so die E-Mail in Alt-Haushalten nach), die Rolle aber nicht
|
||
// ändern – sonst könnte sich ein Sitter selbst befördern.
|
||
allow update: if signedIn()
|
||
&& request.auth.uid in resource.data.memberUids
|
||
&& (
|
||
(resource.data.get('members', {})
|
||
.get(request.auth.uid, {'role': 'member'})
|
||
.get('role', 'member') == 'member'
|
||
&& request.resource.data.diff(resource.data).affectedKeys()
|
||
.hasOnly(['name']))
|
||
|| (request.resource.data.diff(resource.data).affectedKeys()
|
||
.hasOnly(['members'])
|
||
&& request.resource.data.members
|
||
.diff(resource.data.get('members', {}))
|
||
.affectedKeys()
|
||
.hasOnly([request.auth.uid])
|
||
&& request.resource.data.members[request.auth.uid]
|
||
.get('role', 'member')
|
||
== resource.data.get('members', {})
|
||
.get(request.auth.uid, {'role': 'member'})
|
||
.get('role', 'member'))
|
||
);
|
||
// Anlegen nur, wenn man sich selbst als Mitglied einträgt
|
||
// (passiert automatisch bei der Registrierung).
|
||
allow create: if signedIn()
|
||
&& request.auth.uid in request.resource.data.memberUids;
|
||
allow delete: if false;
|
||
|
||
match /plants/{plantId} {
|
||
allow read: if isMember(householdId);
|
||
allow create, delete: if isMember(householdId)
|
||
&& roleOf(householdId) == 'member';
|
||
// Sitter dürfen ausschließlich die Bestätigungs-Felder ändern.
|
||
allow update: if isMember(householdId)
|
||
&& (roleOf(householdId) == 'member'
|
||
|| request.resource.data.diff(resource.data).affectedKeys()
|
||
.hasOnly(['lastWatered', 'lastWateredBy',
|
||
'lastFertilized', 'lastFertilizedBy',
|
||
'lastRepotted', 'lastRepottedBy',
|
||
'fitStars', 'fitReasoning',
|
||
'fitLocationId', 'fitAssessedAt']));
|
||
|
||
// Untersuchungshistorie (KI-Diagnosen). Speichern und lesen darf
|
||
// jede*r im Haushalt (auch Sitter — die dürfen den Check nutzen);
|
||
// löschen nur volle Mitglieder, nachträglich ändern niemand.
|
||
match /diagnoses/{diagnosisId} {
|
||
allow read, create: if isMember(householdId);
|
||
allow delete: if isMember(householdId)
|
||
&& roleOf(householdId) == 'member';
|
||
allow update: if false;
|
||
}
|
||
}
|
||
|
||
match /locations/{locationId} {
|
||
allow read: if isMember(householdId);
|
||
allow write: if isMember(householdId)
|
||
&& roleOf(householdId) == 'member';
|
||
}
|
||
}
|
||
}
|
||
}
|