memi-board 0.52.0 → 0.52.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +297 -297
- package/dist/{commentImages-CzXO42gE.js → commentImages-D_JJnuoj.js} +5 -4
- package/dist/commentImages-D_JJnuoj.js.map +1 -0
- package/dist/composables/useMemiBoardAutoTranslate.d.ts +1 -0
- package/dist/composables/useMemiBoardComments.d.ts +35 -2
- package/dist/composables/useMemiBoardWritingAssistant.d.ts +6 -1
- package/dist/{en-CmPLbFcN.js → en-DSCLJyUd.js} +4 -3
- package/dist/en-DSCLJyUd.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +987 -903
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/CommentAi.vue +93 -0
- package/dist/runtime/components/CommentFeed.vue +3 -3
- package/dist/runtime/components/CommentForm.vue +11 -4
- package/dist/runtime/components/CommentItem.vue +20 -8
- package/dist/runtime/components/Editor.vue +0 -2
- package/dist/runtime/components/MiniComments.vue +3 -3
- package/dist/runtime/components/UserCommentList.vue +3 -3
- package/dist/storage.js +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/utils/localizedFields.d.ts +3 -1
- package/docs/firebase-setup.md +85 -85
- package/docs/firestore.rules.example +58 -58
- package/docs/host-integration.md +80 -80
- package/docs/internationalization.md +76 -72
- package/docs/roles-migration.md +18 -18
- package/docs/storage.rules.example +18 -18
- package/package.json +2 -2
- package/dist/commentImages-CzXO42gE.js.map +0 -1
- package/dist/en-CmPLbFcN.js.map +0 -1
package/docs/firebase-setup.md
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
# Firebase
|
|
1
|
+
# Firebase project setup (first-time install)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
If this is the first time you are adding this module to a project, especially one that has never used Firebase, the README's "Installation" section is not enough. Follow the steps below in order.
|
|
4
4
|
|
|
5
|
-
[
|
|
5
|
+
[Host installation guide](host-integration.md) · [Internationalization and translation](internationalization.md) · [Role migration](roles-migration.md)
|
|
6
6
|
|
|
7
|
-
## 1.
|
|
7
|
+
## 1. Prerequisites
|
|
8
8
|
|
|
9
|
-
- Node.js + pnpm (
|
|
10
|
-
- Nuxt **4.0
|
|
11
|
-
- Firebase CLI: `npm i -g firebase-tools
|
|
9
|
+
- Node.js + pnpm (or npm/yarn)
|
|
10
|
+
- Nuxt **4.0 or later**, `@nuxt/ui` **4.0 or later**
|
|
11
|
+
- Firebase CLI: `npm i -g firebase-tools`, then `firebase login`
|
|
12
12
|
|
|
13
|
-
## 2. Firebase
|
|
13
|
+
## 2. Create a Firebase project (if you don't have one)
|
|
14
14
|
|
|
15
|
-
1. [Firebase
|
|
16
|
-
2. **Firestore Database
|
|
17
|
-
3. **Storage
|
|
18
|
-
4. **Authentication
|
|
15
|
+
1. Create a new project in the [Firebase console](https://console.firebase.google.com).
|
|
16
|
+
2. Enable **Firestore Database** (Native mode).
|
|
17
|
+
3. Enable **Storage**.
|
|
18
|
+
4. Enable **Authentication**, and turn on the providers you need (Google/Apple/Email/Password) under Sign-in method.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
If you are adding the board to an existing Firebase project, skip this step and just confirm that the three services above are enabled.
|
|
21
21
|
|
|
22
|
-
## 3. `firebaseConfig`
|
|
22
|
+
## 3. Get the `firebaseConfig` values
|
|
23
23
|
|
|
24
|
-
Firebase
|
|
24
|
+
Firebase console → Project settings (gear icon) → **General** tab → "Your apps" section → add a web app if there is none → under "SDK setup and configuration", copy the `apiKey`/`authDomain`/`projectId`/`storageBucket`/`messagingSenderId`/`appId` values as-is into `vuefire.config` in `nuxt.config.ts` (this is the host app's nuxt-vuefire config, not memi-board's — see README [Installation](../README.md#installation)).
|
|
25
25
|
|
|
26
|
-
## 4.
|
|
26
|
+
## 4. Connect the host project to the Firebase CLI
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
If the project root does not have `firebase.json`/`.firebaserc` yet:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
firebase init firestore storage
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Or create them manually:
|
|
35
35
|
|
|
36
36
|
```json
|
|
37
37
|
// .firebaserc
|
|
@@ -43,22 +43,22 @@ firebase init firestore storage
|
|
|
43
43
|
{
|
|
44
44
|
"firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json" },
|
|
45
45
|
"storage": {
|
|
46
|
-
"bucket": "<vuefire.config.storageBucket
|
|
46
|
+
"bucket": "<same value as vuefire.config.storageBucket>",
|
|
47
47
|
"rules": "storage.rules"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
`storage.bucket
|
|
52
|
+
`storage.bucket` **must match** the host's `vuefire.config.storageBucket`. Example: `my-app.appspot.com`.
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
If the project already uses the Firebase CLI, don't just check that `firebase.json` has the `firestore.rules`/`storage.rules` paths — check the **bucket name** too.
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
Older projects may have two buckets:
|
|
57
57
|
|
|
58
|
-
- `*.appspot.com` —
|
|
59
|
-
- `*.firebasestorage.app` —
|
|
58
|
+
- `*.appspot.com` — the `storageBucket` from the older web app SDK snippet
|
|
59
|
+
- `*.firebasestorage.app` — the console/CLI default bucket
|
|
60
60
|
|
|
61
|
-
`firebase deploy --only storage
|
|
61
|
+
If `firebase deploy --only storage` has no `bucket`, the rules are deployed only to the **default bucket**. If the app uploads to `appspot.com` but the rules exist only on `firebasestorage.app`, editor images fail with `storage/unauthorized`. If you use both, deploy the same rules as an array:
|
|
62
62
|
|
|
63
63
|
```json
|
|
64
64
|
"storage": [
|
|
@@ -67,111 +67,111 @@ firebase init firestore storage
|
|
|
67
67
|
]
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
If deleting a post fails with `storage/unauthorized` (`access 'memiBoardPosts/{postId}'`), the problem is the **list path**, not the bucket. The `listAll` prefix is `memiBoardPosts/{postId}/`, so `match /memiBoardPosts/{postId} { allow list }` alone is not enough. Copy the `{postId}/{allPaths=**}` block from `docs/storage.rules.example` as-is and run `firebase deploy --only storage`.
|
|
71
71
|
|
|
72
|
-
## 5.
|
|
72
|
+
## 5. Merge and deploy the board Security Rules
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
Merge `docs/firestore.rules.example`/`docs/storage.rules.example` into the project's rules files as described in the README's [Security Model](../README.md#security-model) section, then:
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
77
|
firebase deploy --only firestore:rules,storage
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
**If you merge but don't deploy, writing posts and comments fails with `permission-denied`** — this is the most common installation mistake.
|
|
81
81
|
|
|
82
|
-
### 0.45.1
|
|
82
|
+
### 0.45.1 rule changes and migrating existing hosts
|
|
83
83
|
|
|
84
|
-
0.46
|
|
84
|
+
From 0.46 onward, the global roles for the file management described below are **admin/owner**. staff/manager manage their assigned content, but the default rules do not let them modify or delete other users' files. For a new install, set the first role to `owner`; migrate existing admin and staff accounts using the [role migration](roles-migration.md) procedure.
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
0.45.1
|
|
87
|
+
0.45.1 requires updating the client and the Firestore and Storage rules together. Automatic site deployment does not replace deploying the Firebase rules.
|
|
88
88
|
|
|
89
|
-
|
|
|
89
|
+
| Target | Change |
|
|
90
90
|
|------|-----------|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
91
|
+
| Post updates | Changing `boardId` or `authorUid` is forbidden, and changing `listed` is limited to visibility transitions. |
|
|
92
|
+
| Creating a post and its body together | The body write uses `getAfter()` to check ownership of the parent post after the batch is applied. |
|
|
93
|
+
| Visibility transitions | While the board settings have `visibilityTransition`, creating new posts and comments and regular post edits are locked. Assigned managers may still sync `listed`. |
|
|
94
|
+
| Deletion and audit records | Content authors and assigned managers can clean up related likes. Report records remain even when the post is deleted. |
|
|
95
|
+
| Image and attachment uploads | For existing posts, only the author or a global admin/owner can write. Before the post is saved, the uploaded file's `customMetadata.ownerUid` must equal the signed-in UID. Overwrites also check the existing file's `ownerUid`. |
|
|
96
|
+
| File deletion | If the post document exists, only the author or a global admin/owner can delete; otherwise only the user recorded in the file's `ownerUid`. |
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
Rollout order:
|
|
99
99
|
|
|
100
|
-
1.
|
|
101
|
-
2.
|
|
102
|
-
3.
|
|
103
|
-
4.
|
|
100
|
+
1. In the host app, install `memi-board@^0.45.1` or later and update the lockfile. Any custom upload code must also set `customMetadata: { ownerUid: user.uid }` on images, thumbnails, and attachments.
|
|
101
|
+
2. Merge all functions and per-collection/per-path rules from the example of the version you installed into the host. If you renamed collections, update the Firestore paths inside the functions too. Remove old conditions that allow file writes or deletes based only on `!postExists(postId)`. Access is granted if any overlapping `match` in the rules allows it, so don't leave a broad `allow write` in place.
|
|
102
|
+
3. In the emulator, with two accounts, verify: saving a new post and body together, image and attachment uploads, rejection of overwriting or deleting another user's files, comment like cleanup, and retrying a hide transition.
|
|
103
|
+
4. Deploy the new client and the rules in the same deployment, and apply them to every bucket the app uses. Once the new rules are live, old clients in existing browsers may be rejected because their pre-save uploads lack `ownerUid`; reload the new version and retry. Also check the cross-service IAM setup below.
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
Files on existing posts are authorized by the parent post's author and roles even without `ownerUid`, so no bulk metadata update is needed. However, old orphaned files that have neither a parent post document nor an `ownerUid` cannot be modified or deleted from the client. Do any necessary cleanup from a trusted Admin SDK or the console.
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
If a visibility transition fails, don't manually clear `visibilityTransition` or switch it to the opposite visibility; instead, **save again with the same target visibility** in the settings screen to complete the sync. A hide transition first lowers the content's `listed`, then finalizes `visibility: 'hidden'` last. Returning to public first allows the board to be public, then syncs the content, then clears the transition marker.
|
|
108
108
|
|
|
109
|
-
Storage
|
|
109
|
+
Storage rules can read at most 2 distinct Firestore documents, so the example reads only the post document and the user role document. As a result, admin/owner in Storage keep global file management rights regardless of board assignment. Don't confuse this with the assigned-staff scope in Firestore. See the [official access limits](https://firebase.google.com/docs/storage/security/rules-conditions#enhance_with_firestore).
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
File reads and listing in the example remain public. Services that need files of hidden boards or private drafts to be stored privately must design a separate Storage read policy.
|
|
112
112
|
|
|
113
|
-
###
|
|
113
|
+
### Firestore read access for Storage rules (required host app setup)
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
On image/attachment uploads and file deletion, the example Storage rules use `firestore.exists()` / `firestore.get()` to look up whether the post exists, its `authorUid`, and the user's `role`. The **Storage service account of the host app's Firebase project** therefore needs a Firestore read role. Installing the package or deploying the Firestore rules does not complete this IAM setup.
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
An administrator of the host project sets this up as follows:
|
|
118
118
|
|
|
119
|
-
1.
|
|
120
|
-
2. Google Cloud Console → **IAM
|
|
121
|
-
3. `service-<PROJECT_NUMBER>@gcp-sa-firebasestorage.iam.gserviceaccount.com`
|
|
122
|
-
4.
|
|
119
|
+
1. Run the deploy command above interactively from the host repository. When the Firebase CLI asks whether to add the IAM role for cross-service rules, approve it. You can also grant it from the connection permission prompt shown when saving Storage Rules in the Firebase console.
|
|
120
|
+
2. In the Google Cloud Console → **IAM & Admin → IAM**, select the same Firebase project and turn on **Include Google-provided role grants**.
|
|
121
|
+
3. Find the `service-<PROJECT_NUMBER>@gcp-sa-firebasestorage.iam.gserviceaccount.com` account and confirm it has the **Firebase Rules Firestore Service Agent** (`roles/firebaserules.firestoreServiceAgent`) role. `<PROJECT_NUMBER>` is the numeric project number from the Firebase Project settings, not the project ID. If the role is missing, a host administrator with permission to change IAM adds it to this service account.
|
|
122
|
+
4. After the permission takes effect, verify by uploading a small attachment (under 20 MiB) to a new post while signed in. On existing posts, also verify attachment operations by the author and staff.
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
This role allows reading Firestore documents in the project (`datastore.entities.get`). It is not a role for regular user accounts, and it does not include Firestore write or delete permissions. The ownership and role checks in the Storage rules remain in place.
|
|
125
125
|
|
|
126
|
-
**
|
|
126
|
+
**Do not assume IAM setup is complete just because a `--non-interactive` deploy in CI succeeded.** The host administrator must do the initial setup via an interactive deploy or the IAM console, and confirm the role on the service account above.
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
Even when the size limit is met, as with a 2 MB ZIP, uploads to `memiBoardPosts/{postId}/attachments/...` can fail with `storage/unauthorized` without this permission. Even if the new post document doesn't exist yet, the `firestore.exists()` lookup itself requires the permission. Body image uploads in 0.45.1 need the same Firestore read permission.
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
Official references: [Managing permissions for cross-service Storage Rules](https://firebase.google.com/docs/rules/manage-deploy#manage_permissions_for_cross-service_cloud_storage_security_rules), [Permissions included in the role](https://docs.cloud.google.com/iam/docs/roles-permissions/firebaserules#firebaserules.firestoreServiceAgent).
|
|
131
131
|
|
|
132
|
-
## 6.
|
|
132
|
+
## 6. Sign-in provider setup
|
|
133
133
|
|
|
134
134
|
### Google
|
|
135
135
|
|
|
136
|
-
Firebase
|
|
136
|
+
Just enable Google in Firebase console → Authentication → Sign-in method. No further setup.
|
|
137
137
|
|
|
138
138
|
### Apple
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
This takes more work than Google:
|
|
141
141
|
|
|
142
|
-
1.
|
|
143
|
-
2. [Apple Developer](https://developer.apple.com/account)
|
|
144
|
-
3.
|
|
145
|
-
4.
|
|
146
|
-
5. Team ID,
|
|
142
|
+
1. Enable Apple sign-in in the Firebase console.
|
|
143
|
+
2. Create a new **Services ID** in your [Apple Developer](https://developer.apple.com/account) account.
|
|
144
|
+
3. Register the callback domain in that Services ID's "Web Authentication Configuration" — under Domains, `{authDomain}` (the Firebase project's authDomain, usually `<project-id>.firebaseapp.com`); under Return URL, `https://{authDomain}/__/auth/handler`.
|
|
145
|
+
4. Create a new "Sign in with Apple" key under **Keys** in Apple Developer (even if you already have keys for other purposes, you must create a separate new one).
|
|
146
|
+
5. Enter the Team ID, the Key ID of the key you just created, and its private key (the contents of the `.p8` file) in the Apple sign-in settings in the Firebase console.
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
For details, follow the [official Firebase guide](https://firebase.google.com/docs/auth/web/apple). Without this setup, clicking the Apple sign-in button results in an error.
|
|
149
149
|
|
|
150
|
-
## 7. Firebase AI Logic(Gemini)
|
|
150
|
+
## 7. Enable Firebase AI Logic (Gemini) for AI moderation
|
|
151
151
|
|
|
152
|
-
1. Firebase
|
|
153
|
-
2.
|
|
154
|
-
3.
|
|
155
|
-
`gemini-2.5-flash`
|
|
156
|
-
4. `moderation.enabled`
|
|
152
|
+
1. Enable it in Firebase console → **AI Logic**, and configure it to use the **Gemini Developer API**.
|
|
153
|
+
2. **Pricing plan**: it may work on Spark, but there may be limits and quotas. If they are insufficient, use Blaze.
|
|
154
|
+
3. Model: `gemini-3.5-flash-lite` is recommended for `memiBoard.moderation.model` in the host's `nuxt.config.ts`.
|
|
155
|
+
`gemini-2.5-flash` may return a 404 with **"no longer available to new users" on new projects**.
|
|
156
|
+
4. `moderation.enabled` defaults to `true`, so once enabled it is called automatically when posts are written. To turn it off, set `enabled: false`.
|
|
157
157
|
|
|
158
|
-
## 8. App Check
|
|
158
|
+
## 8. App Check setup (effectively required when AI Logic is enforced)
|
|
159
159
|
|
|
160
|
-
AI Logic / `firebaseml`
|
|
160
|
+
If AI Logic / `firebaseml` is set to **Enforce**, the moderation API fails without an App Check token. memi-board does not initialize App Check. **That is the host app's responsibility.** The default uses limited-use tokens (`useLimitedUseAppCheckTokens: true`), which don't fit the reCAPTCHA v3 provider. For a plugin example, follow the [README App Check section](../README.md#app-check-effectively-required-with-ai-enforce).
|
|
161
161
|
|
|
162
|
-
1. reCAPTCHA Enterprise
|
|
163
|
-
2.
|
|
164
|
-
3. SPA
|
|
165
|
-
4.
|
|
166
|
-
5.
|
|
167
|
-
6.
|
|
162
|
+
1. Create a reCAPTCHA Enterprise **score-based** web key and register that site key for the web app in App Check in the Firebase console. The **appId** of the registered web app must match the host's `vuefire.config.appId`. Never put the secret key in the client.
|
|
163
|
+
2. Include the **Firebase App Check API** in the browser API key restrictions. Keep the APIs that are already allowed.
|
|
164
|
+
3. In an SPA, after `app:mounted`, create a token with `enterprise.execute` and exchange it via `exchangeRecaptchaEnterpriseToken` with `limited_use: true`. `ReCaptchaEnterpriseProvider` renders a hidden widget into an empty `document.body` before mounting and fails with `appCheck/recaptcha-error`.
|
|
165
|
+
4. **Local:** `self.FIREBASE_APPCHECK_DEBUG_TOKEN = true` (prints a UUID to the console; register it under App Check → debug tokens) or a registered fixed UUID.
|
|
166
|
+
5. **Production:** never use debug tokens. Register the real host domain in the reCAPTCHA domains.
|
|
167
|
+
6. Enforce **only AI Logic**. Leave Cloud Storage and Cloud Firestore on monitoring. Only AI Logic accepts limited-use tokens. If you enforce Storage or Firestore, image uploads and post writes are rejected.
|
|
168
168
|
|
|
169
|
-
## 9. Firestore
|
|
169
|
+
## 9. Firestore composite indexes
|
|
170
170
|
|
|
171
|
-
`memiBoardPosts`/`memiBoardComments`/`memiBoardLikes`
|
|
171
|
+
`memiBoardPosts`/`memiBoardComments`/`memiBoardLikes` are all top-level flat collections, and board/post scoping is done with `where` equality filters on the `boardId`/`postId` fields. Merge [docs/firestore.indexes.json.example](firestore.indexes.json.example) into the host's `firestore.indexes.json`, then:
|
|
172
172
|
|
|
173
173
|
```bash
|
|
174
174
|
firebase deploy --only firestore:indexes
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
If a console error shows an index creation link, you can follow it too — the example file above is prepared in advance based on the queries the package actually runs. If you customized collection names (`memiBoard.postsCollection`, etc.), update the `collectionGroup` values to match.
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// memi-board 0.23+ Firestore Rules (flat schema)
|
|
2
2
|
//
|
|
3
3
|
// memiBoardUsers/{uid}
|
|
4
|
-
// memiBoardSettings/{boardId} (
|
|
5
|
-
// memiBoardPosts/{postId}(+ body/main)
|
|
6
|
-
// memiBoardComments/{commentId} postId + boardId
|
|
7
|
-
// memiBoardLikes/{likeId}
|
|
4
|
+
// memiBoardSettings/{boardId} (merges the old memiBoards/{id} stub + settings/config)
|
|
5
|
+
// memiBoardPosts/{postId}(+ body/main) scoped to a board by the boardId field
|
|
6
|
+
// memiBoardComments/{commentId} scoped by the postId + boardId fields
|
|
7
|
+
// memiBoardLikes/{likeId} doc ID = `${postId}_${uid}` or `${commentId}_${uid}`
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
// posts/comments/likes
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// (
|
|
14
|
-
//
|
|
9
|
+
// No categories — boardId is the old category id.
|
|
10
|
+
// posts/comments/likes are all top-level flat collections. Since boardId (and postId) are now
|
|
11
|
+
// document fields rather than paths, permission checks on list/collectionGroup queries
|
|
12
|
+
// depend, as below, on "whether the query's equality (==) filter pins that field to a constant".
|
|
13
|
+
// (The old collectionGroup carve-out is no longer needed — memiBoardPosts is
|
|
14
|
+
// a true flat collection, so cross-author lookups also work as regular list queries.)
|
|
15
15
|
|
|
16
16
|
rules_version = '2';
|
|
17
17
|
service cloud.firestore {
|
|
@@ -20,9 +20,9 @@ service cloud.firestore {
|
|
|
20
20
|
function isSignedIn() {
|
|
21
21
|
return request.auth != null;
|
|
22
22
|
}
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// Even if the host app enables anonymous sign-in for another feature (e.g. a guest queue),
|
|
24
|
+
// block creating board content (posts/comments/likes) by default — reads and isSignedIn()
|
|
25
|
+
// in general are left as-is; this function is used only in the 3 create rules below.
|
|
26
26
|
function isBoardWriter() {
|
|
27
27
|
return isSignedIn() && request.auth.token.firebase.sign_in_provider != 'anonymous';
|
|
28
28
|
}
|
|
@@ -48,8 +48,8 @@ service cloud.firestore {
|
|
|
48
48
|
function commentPath(commentId) {
|
|
49
49
|
return /databases/$(database)/documents/memiBoardComments/$(commentId);
|
|
50
50
|
}
|
|
51
|
-
//
|
|
52
|
-
//
|
|
51
|
+
// One comment image. A missing or empty list is also allowed. The path must be that comment's folder.
|
|
52
|
+
// Host apps that renamed the posts collection must also update the memiBoardPosts prefix.
|
|
53
53
|
function commentImageEntryValid(img, postId, commentId) {
|
|
54
54
|
return img.keys().hasOnly(['name', 'url', 'path', 'size', 'type'])
|
|
55
55
|
&& img.name is string
|
|
@@ -82,7 +82,7 @@ service cloud.firestore {
|
|
|
82
82
|
&& likeId == request.resource.data.postId + '_' + request.auth.uid;
|
|
83
83
|
}
|
|
84
84
|
function isValidCommentLike(likeId) {
|
|
85
|
-
//
|
|
85
|
+
// Older comments may lack boardId. Spoofing is prevented via the comment's postId + the parent post's boardId.
|
|
86
86
|
return request.resource.data.keys().hasOnly(['uid', 'postId', 'boardId', 'createdAt', 'commentId', 'target'])
|
|
87
87
|
&& request.resource.data.target == 'comment'
|
|
88
88
|
&& request.resource.data.commentId is string
|
|
@@ -115,7 +115,7 @@ service cloud.firestore {
|
|
|
115
115
|
return isBoardStaff() && isAllowedStaffForBoard(boardId);
|
|
116
116
|
}
|
|
117
117
|
// A visibility transition freezes new content until every listed flag is synchronized.
|
|
118
|
-
//
|
|
118
|
+
// Notices (pinned, pinnedAt) change only via manager-only updates — an author's regular edit cannot change them.
|
|
119
119
|
function postPinUnchanged() {
|
|
120
120
|
return request.resource.data.get('pinned', false) == resource.data.get('pinned', false)
|
|
121
121
|
&& request.resource.data.get('pinnedAt', null) == resource.data.get('pinnedAt', null);
|
|
@@ -183,8 +183,8 @@ service cloud.firestore {
|
|
|
183
183
|
&& request.resource.data.viewCount is int
|
|
184
184
|
&& request.resource.data.viewCount == resource.data.get('viewCount', 0) + 1;
|
|
185
185
|
}
|
|
186
|
-
//
|
|
187
|
-
// replyCount
|
|
186
|
+
// In a reply create/delete batch the target is a root comment, so users other than its author
|
|
187
|
+
// must be able to update only replyCount by ±1 — otherwise the whole batch, and thus writing a reply, is blocked.
|
|
188
188
|
function isReplyCountUpdate() {
|
|
189
189
|
return isSignedIn()
|
|
190
190
|
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['replyCount'])
|
|
@@ -193,30 +193,30 @@ service cloud.firestore {
|
|
|
193
193
|
&& (request.resource.data.replyCount == resource.data.get('replyCount', 0) + 1
|
|
194
194
|
|| request.resource.data.replyCount == resource.data.get('replyCount', 0) - 1);
|
|
195
195
|
}
|
|
196
|
-
//
|
|
197
|
-
//
|
|
196
|
+
// When a profile name changes, only the copied replyToName in other users' comments that
|
|
197
|
+
// point to me as the reply target may be synced. Author, body, and other fields are immutable.
|
|
198
198
|
function isReplyTargetProfileUpdate() {
|
|
199
199
|
return isSignedIn()
|
|
200
200
|
&& resource.data.replyToUid == request.auth.uid
|
|
201
201
|
&& request.resource.data.replyToUid == resource.data.replyToUid
|
|
202
202
|
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['replyToName']);
|
|
203
203
|
}
|
|
204
|
-
// data.boardId
|
|
205
|
-
// - isBoardAdmin()/isBoardStaff()
|
|
206
|
-
// (request.auth.uid
|
|
207
|
-
// - (isBoardStaff() && canWriteBoard(data.boardId))
|
|
208
|
-
// where('boardId','==', b)
|
|
209
|
-
//
|
|
210
|
-
// get(boardSettingsPath(b))
|
|
204
|
+
// data.boardId is now a document field. For this function to evaluate safely in list queries:
|
|
205
|
+
// - cases where isBoardAdmin()/isBoardStaff() alone yields true are always OK
|
|
206
|
+
// (get() is keyed on request.auth.uid, so it can always be evaluated regardless of document contents)
|
|
207
|
+
// - relying on the (isBoardStaff() && canWriteBoard(data.boardId)) branch requires the query
|
|
208
|
+
// to have a where('boardId','==', b) equality filter — that lets Firestore
|
|
209
|
+
// statically prove data.boardId == b for the entire potential result set and
|
|
210
|
+
// safely run get(boardSettingsPath(b)) as a constant lookup.
|
|
211
211
|
function canReadPost(data) {
|
|
212
212
|
return (data.get('listed', true) == true && data.isPublished == true)
|
|
213
213
|
|| (isSignedIn() && request.auth.uid == data.authorUid)
|
|
214
214
|
|| isBoardAdmin()
|
|
215
215
|
|| isBoardManager(data.boardId);
|
|
216
216
|
}
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
// (
|
|
217
|
+
// Public feeds and author comment lists exclude hidden-board comments with a listed==true equality filter.
|
|
218
|
+
// On a post detail (postId == constant), anyone who can read the parent post can also see hidden comments
|
|
219
|
+
// (e.g. an author reading an assigned staff memo).
|
|
220
220
|
function canReadComment(data) {
|
|
221
221
|
return data.listed == true
|
|
222
222
|
|| (isSignedIn() && request.auth.uid == data.authorUid)
|
|
@@ -231,7 +231,7 @@ service cloud.firestore {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
match /memiBoardUsers/{uid} {
|
|
234
|
-
//
|
|
234
|
+
// Everything except email is public profile info — email is kept separately in a private subcollection.
|
|
235
235
|
allow read: if true;
|
|
236
236
|
allow create: if isSignedIn()
|
|
237
237
|
&& request.auth.uid == uid
|
|
@@ -252,7 +252,7 @@ service cloud.firestore {
|
|
|
252
252
|
&& request.resource.data.moderationBlockCount
|
|
253
253
|
== resource.data.get('moderationBlockCount', 0) + 1
|
|
254
254
|
)
|
|
255
|
-
// 0.47.0
|
|
255
|
+
// 0.47.0 and earlier: increment the confirmed count by 1 before the call.
|
|
256
256
|
|| (
|
|
257
257
|
request.resource.data.diff(resource.data).affectedKeys()
|
|
258
258
|
.hasOnly(['aiWritingCount', 'aiWritingWindowAt', 'updatedAt'])
|
|
@@ -273,8 +273,8 @@ service cloud.firestore {
|
|
|
273
273
|
)
|
|
274
274
|
)
|
|
275
275
|
)
|
|
276
|
-
// 0.47.1+:
|
|
277
|
-
//
|
|
276
|
+
// 0.47.1+: only reserve before the call, and confirm on success. On failure, only release the reservation.
|
|
277
|
+
// The confirmed count only increases by 1 at a time and cannot be decreased by the client.
|
|
278
278
|
|| (
|
|
279
279
|
request.resource.data.diff(resource.data).affectedKeys()
|
|
280
280
|
.hasOnly(['aiWritingPending', 'updatedAt'])
|
|
@@ -312,23 +312,23 @@ service cloud.firestore {
|
|
|
312
312
|
)
|
|
313
313
|
));
|
|
314
314
|
|
|
315
|
-
//
|
|
316
|
-
//
|
|
315
|
+
// A private memo about another author, visible only to me. The doc ID is targetUid, so there is
|
|
316
|
+
// one per target. Even admins can't read it — it is purely for personal reference, not moderation.
|
|
317
317
|
match /memos/{targetUid} {
|
|
318
318
|
allow read, write: if isSignedIn() && request.auth.uid == uid;
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
//
|
|
321
|
+
// Private subcollection holding only the email. Only the user and admins can read and write.
|
|
322
322
|
match /private/{docId} {
|
|
323
323
|
allow read, write: if isBoardAdmin() || (isSignedIn() && request.auth.uid == uid);
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
// create/update
|
|
330
|
-
//
|
|
331
|
-
//
|
|
327
|
+
// Canonical board document that merges the old memiBoards/{boardId} stub + memiBoards/{boardId}/settings/config.
|
|
328
|
+
// List rendering and permission checks both read only this one document.
|
|
329
|
+
// Why create/update are separate: the staff branch on update references resource.data (existing values),
|
|
330
|
+
// but resource doesn't exist at create time, so combining them would make that branch evaluate
|
|
331
|
+
// to an error — board creation was admin-only in older versions too, so separating them is natural.
|
|
332
332
|
match /memiBoardSettings/{boardId} {
|
|
333
333
|
allow read: if true;
|
|
334
334
|
allow create: if isBoardAdmin();
|
|
@@ -381,17 +381,17 @@ service cloud.firestore {
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
match /memiBoardComments/{commentId} {
|
|
384
|
-
//
|
|
385
|
-
//
|
|
384
|
+
// Public feeds and author comment lists filter out hidden-board comments with where('listed','==',true).
|
|
385
|
+
// Post details use a postId equality filter, so hidden comments can be read via parent post permission (canReadPost).
|
|
386
386
|
allow read: if isBoardAdmin() || canReadComment(resource.data);
|
|
387
387
|
|
|
388
388
|
allow create: if isBoardWriter()
|
|
389
389
|
&& request.resource.data.postId is string
|
|
390
390
|
&& request.resource.data.boardId is string
|
|
391
|
-
// boardId
|
|
392
|
-
//
|
|
393
|
-
// canWriteBoardComment()
|
|
394
|
-
//
|
|
391
|
+
// boardId is a denormalized field filled by the client and can be spoofed — to prevent
|
|
392
|
+
// filling in a value different from the post's actual boardId to pose as another board with a looser
|
|
393
|
+
// commentWriteRole and bypass canWriteBoardComment(), check that it matches the parent post's actual
|
|
394
|
+
// boardId (cost: one extra get() per comment creation).
|
|
395
395
|
&& get(postPath(request.resource.data.postId)).data.boardId == request.resource.data.boardId
|
|
396
396
|
&& canWriteBoardComment(request.resource.data.boardId)
|
|
397
397
|
&& boardWritesOpen(request.resource.data.boardId)
|
|
@@ -422,14 +422,14 @@ service cloud.firestore {
|
|
|
422
422
|
);
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
-
//
|
|
426
|
-
//
|
|
425
|
+
// Flat collection for like toggles only. Doc ID convention = `${targetId}_{uid}`.
|
|
426
|
+
// Body: no target (legacy) + 4 fields. Comments: target=='comment' + commentId.
|
|
427
427
|
match /memiBoardLikes/{likeId} {
|
|
428
|
-
// "
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
// resource == null:
|
|
432
|
-
//
|
|
428
|
+
// Your own likes are always readable for the "posts/comments I liked" query (where uid==auth.uid).
|
|
429
|
+
// Otherwise, as in older versions, anyone who can read the post can check whether a like exists
|
|
430
|
+
// via an individual get() (as long as they know the exact uid).
|
|
431
|
+
// resource == null: when getting a like document that doesn't exist yet in a transaction.
|
|
432
|
+
// Without this, accessing resource.data fails and a regular user's first like becomes permission-denied.
|
|
433
433
|
allow read: if isBoardAdmin() || (isSignedIn() && resource == null)
|
|
434
434
|
|| (isSignedIn() && request.auth.uid == resource.data.uid)
|
|
435
435
|
|| (resource != null && canReadPost(get(postPath(resource.data.postId)).data))
|
|
@@ -444,10 +444,10 @@ service cloud.firestore {
|
|
|
444
444
|
|
|
445
445
|
allow delete: if isBoardAdmin() || (isSignedIn()
|
|
446
446
|
&& (request.auth.uid == resource.data.uid || canDeleteLike(resource.data)));
|
|
447
|
-
// update
|
|
447
|
+
// update is not allowed — toggling is always create-or-delete.
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
//
|
|
450
|
+
// Post reports. Doc ID = `${postId}_{uid}`. Contents are readable only by the reporter and staff.
|
|
451
451
|
match /memiBoardReports/{reportId} {
|
|
452
452
|
allow read: if (isSignedIn() && resource == null)
|
|
453
453
|
|| (isSignedIn() && request.auth.uid == resource.data.uid)
|