@mohasinac/appkit 4.39.0 → 4.39.1
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.
|
@@ -37,7 +37,7 @@ declare class ReviewRepository extends BaseRepository<ReviewDocument> {
|
|
|
37
37
|
* it can be reasoned about).
|
|
38
38
|
*/
|
|
39
39
|
findApprovedByProduct(productId: string, limit?: number): Promise<ReviewDocument[]>;
|
|
40
|
-
findApprovedByStore(storeId: string): Promise<ReviewDocument[]>;
|
|
40
|
+
findApprovedByStore(storeId: string, limit?: number): Promise<ReviewDocument[]>;
|
|
41
41
|
findByUser(userId: string): Promise<ReviewDocument[]>;
|
|
42
42
|
findApprovedByUser(userId: string): Promise<ReviewDocument[]>;
|
|
43
43
|
findPending(): Promise<ReviewDocument[]>;
|
|
@@ -101,13 +101,13 @@ class ReviewRepository extends BaseRepository {
|
|
|
101
101
|
const snapshot = await query.get();
|
|
102
102
|
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
103
103
|
}
|
|
104
|
-
async findApprovedByStore(storeId) {
|
|
104
|
+
async findApprovedByStore(storeId, limit = 50) {
|
|
105
105
|
const snapshot = await this.db
|
|
106
106
|
.collection(this.collection)
|
|
107
107
|
.where(REVIEW_FIELDS.STORE_ID, "==", storeId)
|
|
108
108
|
.where(REVIEW_FIELDS.STATUS, "==", "approved")
|
|
109
109
|
.orderBy(REVIEW_FIELDS.CREATED_AT, "desc")
|
|
110
|
-
.limit(
|
|
110
|
+
.limit(limit)
|
|
111
111
|
.get();
|
|
112
112
|
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
113
113
|
}
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { NextResponse } from "next/server.js";
|
|
22
22
|
import { getProviders } from "../../../../../contracts";
|
|
23
|
+
import { reviewRepository } from "../../../../reviews/repository/reviews.repository";
|
|
24
|
+
import { maskPublicReview } from "../../../../../security/pii-mask";
|
|
23
25
|
import { normalizeError } from "../../../../../errors/normalize";
|
|
24
26
|
/**
|
|
25
27
|
* Upper bound on the reviews scanned to build a store's aggregate. The average
|
|
@@ -81,14 +83,22 @@ export async function GET(request, context) {
|
|
|
81
83
|
* the store's reviews are one equality away. This also turns 21 Firestore
|
|
82
84
|
* queries into 1, well inside Rule #6's ~3-round-trip budget.
|
|
83
85
|
*/
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
/*
|
|
87
|
+
* 🛑 `reviewRepository`, NOT `db.getRepository("reviews")`.
|
|
88
|
+
*
|
|
89
|
+
* The generic provider repository returns the RAW document. `userName` is
|
|
90
|
+
* PII-encrypted at rest, so it came back as the literal string
|
|
91
|
+
* `enc:v1:…` — measured in production the moment this endpoint started
|
|
92
|
+
* returning rows at all — and so did `userNameIndex`, the HMAC blind index,
|
|
93
|
+
* `searchTxt`, `reportCount` and the moderation `status`. Only
|
|
94
|
+
* `reviewRepository.mapDoc` runs `decryptPiiFields`.
|
|
95
|
+
*
|
|
96
|
+
* This was INVISIBLE until the itemsSold fix (Root Cause #100): the
|
|
97
|
+
* endpoint returned zero reviews, so there was nothing to leak. A fix that
|
|
98
|
+
* makes a surface work for the first time is a fix that exposes whatever
|
|
99
|
+
* the surface does next.
|
|
100
|
+
*/
|
|
101
|
+
const allFlat = (await reviewRepository.findApprovedByStore(store.id, STORE_REVIEW_SCAN_CAP));
|
|
92
102
|
// Compute aggregate metrics from ALL reviews (unfiltered)
|
|
93
103
|
const ratingDistribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
|
94
104
|
let ratingSum = 0;
|
|
@@ -184,11 +194,38 @@ export async function GET(request, context) {
|
|
|
184
194
|
for (const p of fetched)
|
|
185
195
|
if (p)
|
|
186
196
|
productMap.set(p.id, p);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
/*
|
|
198
|
+
* 🛑 AN ALLOW-LIST, not a spread. `...review` published every field the
|
|
199
|
+
* document happens to carry — including `userNameIndex` (an HMAC blind
|
|
200
|
+
* index, which lets a caller confirm a guessed name), `searchTxt`,
|
|
201
|
+
* `reportCount` and the moderation `status`. Root Cause #70: a payload
|
|
202
|
+
* built by spreading publishes everything nobody thought to remove, and is
|
|
203
|
+
* blind to fields added later.
|
|
204
|
+
*
|
|
205
|
+
* `userName` is MASKED, matching every other public review surface —
|
|
206
|
+
* `maskPublicReview` is what `listReviewsForProduct` already applies.
|
|
207
|
+
*/
|
|
208
|
+
const reviewsWithProduct = pageSlice.map((review) => {
|
|
209
|
+
const r = review;
|
|
210
|
+
const masked = maskPublicReview({ ...r, userName: String(r.userName ?? "") });
|
|
211
|
+
return {
|
|
212
|
+
id: r.id,
|
|
213
|
+
rating: r.rating,
|
|
214
|
+
title: r.title,
|
|
215
|
+
comment: r.comment ?? r.body,
|
|
216
|
+
images: r.images ?? [],
|
|
217
|
+
video: r.video,
|
|
218
|
+
verified: r.verified ?? r.isVerifiedPurchase,
|
|
219
|
+
helpfulCount: r.helpfulCount ?? 0,
|
|
220
|
+
sellerReply: r.sellerReply,
|
|
221
|
+
sellerRepliedAt: r.sellerRepliedAt,
|
|
222
|
+
createdAt: r.createdAt,
|
|
223
|
+
userName: masked.userName,
|
|
224
|
+
productId: r.productId,
|
|
225
|
+
productTitle: productMap.get(review.productId)?.title ?? review.productTitle,
|
|
226
|
+
productMainImage: productMap.get(review.productId)?.mainImage ?? null,
|
|
227
|
+
};
|
|
228
|
+
});
|
|
192
229
|
return NextResponse.json({
|
|
193
230
|
success: true,
|
|
194
231
|
data: {
|