feat(rules+trigger): conversations Firestore rules, onMessageWritten listens on conversations path, gitignore
- firestore.rules: conversations doc read/write rules with ciphertext validation, messages subcollection create rules (image or ciphertext text) - onMessageWritten: trigger path changed from question_threads to conversations, passes conversation_id in FCM data, removed questionId resolution (no longer needed) - .gitignore: deduplicate ClaudeReport.md entry
This commit is contained in:
parent
e4cdbac7b1
commit
060ef69ca5
|
|
@ -71,3 +71,4 @@ docs/img/qa.jpeg
|
||||||
docs/img/sam.jpg
|
docs/img/sam.jpg
|
||||||
ClaudeReport.md
|
ClaudeReport.md
|
||||||
ClaudeReport.md
|
ClaudeReport.md
|
||||||
|
ClaudeReport.md
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,39 @@ service cloud.firestore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Conversations: the Messages inbox. Each conversation (the couple chat or a per-question
|
||||||
|
// discussion) holds E2E-encrypted messages; only metadata + the encrypted last-message
|
||||||
|
// preview live on the conversation doc.
|
||||||
|
match /conversations/{conversationId} {
|
||||||
|
allow read: if isCouplesMember(coupleId);
|
||||||
|
// Members may create/merge-update the conversation doc within the allowed shape; any
|
||||||
|
// last-message preview must be ciphertext (encrypted on-device before write).
|
||||||
|
allow write: if isCouplesMember(coupleId)
|
||||||
|
&& request.resource.data.keys().hasOnly(
|
||||||
|
['type', 'questionId', 'createdAt', 'lastMessageAt', 'lastMessagePreview', 'lastMessageSenderId', 'reads'])
|
||||||
|
&& (!('lastMessagePreview' in request.resource.data)
|
||||||
|
|| isCiphertext(request.resource.data.lastMessagePreview));
|
||||||
|
|
||||||
|
// Messages: author-only create; text is ciphertext OR it's an image message pointing at
|
||||||
|
// encrypted Storage bytes; immutable after creation.
|
||||||
|
match /messages/{messageId} {
|
||||||
|
allow read: if isCouplesMember(coupleId);
|
||||||
|
allow create: if isCouplesMember(coupleId)
|
||||||
|
&& coupleEncryptionEnabled(coupleId)
|
||||||
|
&& request.resource.data.authorUserId == request.auth.uid
|
||||||
|
&& request.resource.data.keys().hasOnly(['authorUserId', 'text', 'createdAt', 'type', 'mediaUrl'])
|
||||||
|
&& (
|
||||||
|
(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, delete: if false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Date swipes: per-couple, per-date partner swipe state. The action is E2E
|
// Date swipes: per-couple, per-date partner swipe state. The action is E2E
|
||||||
// ciphertext so the server can't read date preferences; only swipedAt is plaintext.
|
// ciphertext so the server can't read date preferences; only swipedAt is plaintext.
|
||||||
match /date_swipes/{dateIdeaId} {
|
match /date_swipes/{dateIdeaId} {
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,18 @@ import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Firestore trigger that notifies the other partner when a chat message is
|
* Firestore trigger that notifies the other partner when a chat message is
|
||||||
* sent in a question thread.
|
* sent in a conversation (the couple chat or a per-question discussion).
|
||||||
*
|
*
|
||||||
* Path: couples/{coupleId}/question_threads/{threadId}/messages/{messageId}
|
* Path: couples/{coupleId}/conversations/{conversationId}/messages/{messageId}
|
||||||
*
|
*
|
||||||
* Respects the recipient's `notifChatMessage` preference (default: enabled).
|
* Respects the recipient's `notifChatMessage` preference (default: enabled).
|
||||||
*/
|
*/
|
||||||
export const onMessageWritten = functions.firestore
|
export const onMessageWritten = functions.firestore
|
||||||
.document('couples/{coupleId}/question_threads/{threadId}/messages/{messageId}')
|
.document('couples/{coupleId}/conversations/{conversationId}/messages/{messageId}')
|
||||||
.onCreate(async (snap, context) => {
|
.onCreate(async (snap, context) => {
|
||||||
const { coupleId, threadId, messageId } = context.params as {
|
const { coupleId, conversationId, messageId } = context.params as {
|
||||||
coupleId: string
|
coupleId: string
|
||||||
threadId: string
|
conversationId: string
|
||||||
messageId: string
|
messageId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,14 +39,6 @@ export const onMessageWritten = functions.firestore
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// The conversation deep link + the client's "am I already in this thread?" suppression both
|
|
||||||
// key off questionId, so resolve it from the thread doc and pass it through.
|
|
||||||
const threadDoc = await db
|
|
||||||
.collection('couples').doc(coupleId)
|
|
||||||
.collection('question_threads').doc(threadId)
|
|
||||||
.get()
|
|
||||||
const questionId = (threadDoc.data()?.questionId as string | undefined) ?? ''
|
|
||||||
|
|
||||||
const partnerUserDoc = await db.collection('users').doc(partnerId).get()
|
const partnerUserDoc = await db.collection('users').doc(partnerId).get()
|
||||||
|
|
||||||
// Respect the partner's notification preference (opt-out; default is enabled).
|
// Respect the partner's notification preference (opt-out; default is enabled).
|
||||||
|
|
@ -95,8 +87,7 @@ export const onMessageWritten = functions.firestore
|
||||||
data: {
|
data: {
|
||||||
type: 'chat_message',
|
type: 'chat_message',
|
||||||
couple_id: coupleId,
|
couple_id: coupleId,
|
||||||
thread_id: threadId,
|
conversation_id: conversationId,
|
||||||
...(questionId ? { question_id: questionId } : {}),
|
|
||||||
...(authorPhotoUrl ? { sender_avatar_url: authorPhotoUrl } : {}),
|
...(authorPhotoUrl ? { sender_avatar_url: authorPhotoUrl } : {}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -119,6 +110,6 @@ export const onMessageWritten = functions.firestore
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[onMessageWritten] notified partner ${partnerId} for thread ${threadId} in couple ${coupleId}`
|
`[onMessageWritten] notified partner ${partnerId} for conversation ${conversationId} in couple ${coupleId}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue