fix: rate limiter bump (20/day, 100/week), firestore rules for image messages, storage rules for chat_media, gitignore ClaudeReport
- NotificationRateLimiter: 20 partner/day, 100/week (was 2/4 — too tight for game activity) - firestore.rules: messages create allows type=image with mediaUrl or type=text with ciphertext - storage.rules: chat_media path with 15MB cap - .gitignore: ClaudeReport.md, docs/img
This commit is contained in:
parent
608ddcfc5b
commit
4e2c3fdf0d
|
|
@ -67,3 +67,7 @@ ios_encrypt.md
|
||||||
closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json
|
closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json
|
||||||
DAILY_FUN_IMPLEMENTATION_BATCH_PLAN.md
|
DAILY_FUN_IMPLEMENTATION_BATCH_PLAN.md
|
||||||
gitleaks-after.json
|
gitleaks-after.json
|
||||||
|
docs/img/qa.jpeg
|
||||||
|
docs/img/sam.jpg
|
||||||
|
ClaudeReport.md
|
||||||
|
ClaudeReport.md
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,15 @@ import java.util.concurrent.TimeUnit
|
||||||
/**
|
/**
|
||||||
* Persisted rate limiter for partner-trigger and reminder notifications.
|
* Persisted rate limiter for partner-trigger and reminder notifications.
|
||||||
*
|
*
|
||||||
* Limits:
|
* Limits (sized for a couples app where partner activity is the core loop — the old 2/day +
|
||||||
* - 2 partner-trigger notifications per day
|
* 4/week caps suppressed legitimate game start/finish and partner-action notifications after a
|
||||||
* - 1 reminder notification per day
|
* single game; these are anti-runaway ceilings, not gentle-nudge throttles):
|
||||||
* - 4 total notifications per week
|
* - 20 partner-trigger notifications per day
|
||||||
|
* - 1 reminder notification per day (proactive nudges stay gentle)
|
||||||
|
* - 100 total notifications per week
|
||||||
|
*
|
||||||
|
* Note: chat messages are NOT throttled here — foreground messages show the in-app bubble and
|
||||||
|
* backgrounded ones are displayed by the OS from the FCM notification block, both bypassing this.
|
||||||
*
|
*
|
||||||
* Counts are stored in [SharedPreferences] and reset when a new day or week starts.
|
* Counts are stored in [SharedPreferences] and reset when a new day or week starts.
|
||||||
*/
|
*/
|
||||||
|
|
@ -30,9 +35,9 @@ class NotificationRateLimiter(context: Context) {
|
||||||
private const val KEY_REMINDER_COUNT = "reminder_count"
|
private const val KEY_REMINDER_COUNT = "reminder_count"
|
||||||
private const val KEY_TOTAL_COUNT = "total_count"
|
private const val KEY_TOTAL_COUNT = "total_count"
|
||||||
|
|
||||||
const val MAX_PARTNER_PER_DAY = 2
|
const val MAX_PARTNER_PER_DAY = 20
|
||||||
const val MAX_REMINDER_PER_DAY = 1
|
const val MAX_REMINDER_PER_DAY = 1
|
||||||
const val MAX_TOTAL_PER_WEEK = 4
|
const val MAX_TOTAL_PER_WEEK = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -376,11 +376,20 @@ service cloud.firestore {
|
||||||
// Discussion messages: any couple member can read, but only the author can write/update/delete
|
// Discussion messages: any couple member can read, but only the author can write/update/delete
|
||||||
match /messages/{messageId} {
|
match /messages/{messageId} {
|
||||||
allow read: if isCouplesMember(coupleId);
|
allow read: if isCouplesMember(coupleId);
|
||||||
|
// Text messages carry ciphertext in `text`; image messages carry only a `mediaUrl`
|
||||||
|
// pointing at the encrypted bytes in Storage (the photo itself is E2E-encrypted).
|
||||||
allow create: if isCouplesMember(coupleId)
|
allow create: if isCouplesMember(coupleId)
|
||||||
&& coupleEncryptionEnabled(coupleId)
|
&& coupleEncryptionEnabled(coupleId)
|
||||||
&& request.resource.data.authorUserId == request.auth.uid
|
&& request.resource.data.authorUserId == request.auth.uid
|
||||||
&& request.resource.data.keys().hasOnly(['authorUserId', 'text', 'createdAt'])
|
&& request.resource.data.keys().hasOnly(['authorUserId', 'text', 'createdAt', 'type', 'mediaUrl'])
|
||||||
&& isCiphertext(request.resource.data.text);
|
&& (
|
||||||
|
(request.resource.data.get('type', 'text') == 'image'
|
||||||
|
&& request.resource.data.mediaUrl is string
|
||||||
|
&& request.resource.data.mediaUrl.size() > 0)
|
||||||
|
||
|
||||||
|
(request.resource.data.get('type', 'text') == 'text'
|
||||||
|
&& isCiphertext(request.resource.data.text))
|
||||||
|
);
|
||||||
allow update: if isCouplesMember(coupleId)
|
allow update: if isCouplesMember(coupleId)
|
||||||
&& coupleEncryptionEnabled(coupleId)
|
&& coupleEncryptionEnabled(coupleId)
|
||||||
&& resource.data.authorUserId == request.auth.uid
|
&& resource.data.authorUserId == request.auth.uid
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,16 @@ service firebase.storage {
|
||||||
allow read: if request.auth != null && request.auth.uid == uid;
|
allow read: if request.auth != null && request.auth.uid == uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypted chat media: the author writes under their own path (already E2E-encrypted
|
||||||
|
// ciphertext, so Storage never holds anything readable). The partner reads via the tokenized
|
||||||
|
// download URL, which bypasses these rules — same model as profile photos. 15 MB cap.
|
||||||
|
match /users/{uid}/chat_media/{file} {
|
||||||
|
allow write: if request.auth != null
|
||||||
|
&& request.auth.uid == uid
|
||||||
|
&& request.resource.size < 15 * 1024 * 1024;
|
||||||
|
allow read: if request.auth != null && request.auth.uid == uid;
|
||||||
|
}
|
||||||
|
|
||||||
// Deny all other paths by default.
|
// Deny all other paths by default.
|
||||||
match /{allPaths=**} {
|
match /{allPaths=**} {
|
||||||
allow read, write: if false;
|
allow read, write: if false;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue