@mohasinac/appkit 4.40.1 → 4.41.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.
- package/dist/features/categories/repository/categories.repository.js +2 -2
- package/dist/features/shell/SectionForm.js +44 -5
- package/dist/features/tester/seed-data/_money-flows.js +65 -49
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +6 -4
- package/dist/features/tester/seed-data/authored/admin__coupons.js +14 -13
- package/dist/features/tester/seed-data/authored/buying__bidding.js +154 -140
- package/dist/features/tester/seed-data/authored/buying__browsing-search.js +4 -4
- package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +59 -56
- package/dist/features/tester/seed-data/authored/buying__buying-coupons.js +18 -18
- package/dist/features/tester/seed-data/authored/buying__cart.js +105 -79
- package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +6 -8
- package/dist/features/tester/seed-data/authored/buying__my-orders.js +13 -13
- package/dist/features/tester/seed-data/authored/buying__offers.js +6 -6
- package/dist/features/tester/seed-data/authored/buying__order-detail-actions.js +8 -7
- package/dist/features/tester/seed-data/authored/buying__product-detail.js +62 -56
- package/dist/features/tester/seed-data/authored/buying__return-request.js +6 -6
- package/dist/features/tester/seed-data/authored/buying__reviews-pagination.js +4 -4
- package/dist/features/tester/seed-data/authored/buying__reviews.js +1 -1
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.js +8 -8
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +1 -1
- package/dist/features/tester/seed-data/authored/content-discovery__coupons.js +29 -16
- package/dist/features/tester/seed-data/authored/content-discovery__event-detail-subroutes.js +9 -9
- package/dist/features/tester/seed-data/authored/content-discovery__events.js +69 -36
- package/dist/features/tester/seed-data/authored/content-discovery__notifications.js +1 -1
- package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__back-to-top-button.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__general-design.js +1 -1
- package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +1 -1
- package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +3 -3
- package/dist/features/tester/seed-data/authored/index.js +0 -2
- package/dist/features/tester/seed-data/authored/page-wiring__data-loss.js +2 -2
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +6 -4
- package/dist/features/tester/seed-data/authored/public-pages__auth-error-pages.js +2 -2
- package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +1 -1
- package/dist/features/tester/seed-data/authored/public-pages__stores-sellers-directories.js +8 -10
- package/dist/features/tester/seed-data/authored/selling__become-seller.js +2 -2
- package/dist/features/tester/seed-data/authored/selling__digital-content-delivery.js +2 -2
- package/dist/features/tester/seed-data/authored/selling__listing-a-product.js +2 -2
- package/dist/features/tester/seed-data/authored/selling__listing-edit-roundtrip.js +2 -2
- package/dist/features/tester/seed-data/authored/selling__listing-lifecycle.js +2 -2
- package/dist/features/tester/seed-data/authored/selling__seller-listing-types.js +2 -2
- package/dist/features/tester/seed-data/authored/seo__og-images.js +3 -4
- package/dist/features/tester/seed-data/authored/seo__page-metadata.js +6 -8
- package/dist/features/tester/seed-data/authored/seo__sitemap-and-robots.js +5 -5
- package/dist/features/tester/seed-data/index.d.ts +0 -10
- package/dist/features/tester/seed-data/index.js +0 -10
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +75 -89
- package/dist/features/tester/seed-data/tester-responses-seed-data.js +5 -5
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -21
- package/dist/seed/addresses-seed-data.js +0 -34
- package/dist/seed/index.d.ts +1 -1
- package/dist/seed/index.js +4 -3
- package/dist/seed/manifest.js +11 -12
- package/dist/seed/notifications-seed-data.js +4 -4
- package/dist/seed/stores-seed-data.js +7 -52
- package/dist/seed/users-seed-data.js +3 -83
- package/package.json +2 -2
- package/scripts/seed-cli.mjs +15 -20
- package/dist/_internal/server/features/classified/actions.d.ts +0 -12
- package/dist/_internal/server/features/classified/actions.js +0 -33
- package/dist/_internal/server/features/digital-code/actions.d.ts +0 -12
- package/dist/_internal/server/features/digital-code/actions.js +0 -33
- package/dist/_internal/server/features/homepage/data.d.ts +0 -18
- package/dist/_internal/server/features/homepage/data.js +0 -70
- package/dist/_internal/server/features/live/actions.d.ts +0 -6
- package/dist/_internal/server/features/live/actions.js +0 -27
- package/dist/_internal/server/features/messages/actions.d.ts +0 -6
- package/dist/_internal/server/features/messages/actions.js +0 -6
- package/dist/_internal/server/features/messages/data.d.ts +0 -10
- package/dist/_internal/server/features/messages/data.js +0 -24
- package/dist/_internal/server/features/messages/index.d.ts +0 -2
- package/dist/_internal/server/features/messages/index.js +0 -2
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +0 -12
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +0 -112
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +0 -2
- package/dist/_internal/shared/features/homepage/section-gate.d.ts +0 -14
- package/dist/_internal/shared/features/homepage/section-gate.js +0 -51
- package/dist/_internal/shared/features/lottery/config-form.d.ts +0 -58
- package/dist/_internal/shared/features/lottery/config-form.js +0 -71
- package/dist/client/api/useFeatureFlags.d.ts +0 -22
- package/dist/client/api/useFeatureFlags.js +0 -27
- package/dist/features/account/address-validation.d.ts +0 -20
- package/dist/features/account/address-validation.js +0 -35
- package/dist/features/account/components/ChatList.d.ts +0 -14
- package/dist/features/account/components/ChatList.js +0 -9
- package/dist/features/account/components/ChatWindow.d.ts +0 -17
- package/dist/features/account/components/ChatWindow.js +0 -6
- package/dist/features/account/components/MessagesView.d.ts +0 -16
- package/dist/features/account/components/MessagesView.js +0 -5
- package/dist/features/account/components/UserOffersView.d.ts +0 -10
- package/dist/features/account/components/UserOffersView.js +0 -5
- package/dist/features/account/constants/addresses.d.ts +0 -13
- package/dist/features/account/constants/addresses.js +0 -43
- package/dist/features/account/hooks/useAddressForm.d.ts +0 -24
- package/dist/features/account/hooks/useAddressForm.js +0 -58
- package/dist/features/admin/actions/chat-actions.d.ts +0 -29
- package/dist/features/admin/actions/chat-actions.js +0 -101
- package/dist/features/admin/components/AdminFeatureFlagsView.d.ts +0 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +0 -109
- package/dist/features/admin/components/AdminSiteView.d.ts +0 -10
- package/dist/features/admin/components/AdminSiteView.js +0 -52
- package/dist/features/admin/hooks/useChat.d.ts +0 -24
- package/dist/features/admin/hooks/useChat.js +0 -100
- package/dist/features/admin/permission-map.d.ts +0 -3
- package/dist/features/admin/permission-map.js +0 -26
- package/dist/features/admin/repository/chat.repository.d.ts +0 -50
- package/dist/features/admin/repository/chat.repository.js +0 -223
- package/dist/features/auth/actions/password-change-otp-actions.d.ts +0 -18
- package/dist/features/auth/actions/password-change-otp-actions.js +0 -88
- package/dist/features/auth/actions/realtime-token-actions.d.ts +0 -11
- package/dist/features/auth/actions/realtime-token-actions.js +0 -38
- package/dist/features/auth/password-change-otp.d.ts +0 -34
- package/dist/features/auth/password-change-otp.js +0 -56
- package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +0 -12
- package/dist/features/classified/components/ClassifiedContactSellerPanel.js +0 -49
- package/dist/features/events/api/route.d.ts +0 -12
- package/dist/features/events/api/route.js +0 -90
- package/dist/features/faq/api/route.d.ts +0 -11
- package/dist/features/faq/api/route.js +0 -52
- package/dist/features/faq/hooks/useFAQs.d.ts +0 -22
- package/dist/features/faq/hooks/useFAQs.js +0 -41
- package/dist/features/media/upload/video-poster.test.d.ts +0 -1
- package/dist/features/media/upload/video-poster.test.js +0 -42
- package/dist/features/messages/actions/index.d.ts +0 -2
- package/dist/features/messages/actions/index.js +0 -2
- package/dist/features/messages/actions/messages-actions.d.ts +0 -15
- package/dist/features/messages/actions/messages-actions.js +0 -33
- package/dist/features/messages/actions/ping-rtdb.d.ts +0 -2
- package/dist/features/messages/actions/ping-rtdb.js +0 -26
- package/dist/features/messages/hooks/useConversation.d.ts +0 -22
- package/dist/features/messages/hooks/useConversation.js +0 -155
- package/dist/features/messages/hooks/useConversations.d.ts +0 -15
- package/dist/features/messages/hooks/useConversations.js +0 -104
- package/dist/features/messages/index.d.ts +0 -6
- package/dist/features/messages/index.js +0 -4
- package/dist/features/messages/realtime.d.ts +0 -32
- package/dist/features/messages/realtime.js +0 -39
- package/dist/features/messages/repository/conversations.repository.d.ts +0 -71
- package/dist/features/messages/repository/conversations.repository.js +0 -284
- package/dist/features/messages/schemas/firestore.d.ts +0 -34
- package/dist/features/messages/schemas/firestore.js +0 -16
- package/dist/features/messages/schemas/index.d.ts +0 -177
- package/dist/features/messages/schemas/index.js +0 -35
- package/dist/features/messages/server.d.ts +0 -6
- package/dist/features/messages/server.js +0 -6
- package/dist/features/products/components/MakeOfferForm.d.ts +0 -20
- package/dist/features/products/components/MakeOfferForm.js +0 -9
- package/dist/features/reviews/api/route.d.ts +0 -21
- package/dist/features/reviews/api/route.js +0 -174
- package/dist/features/seller/api/offers/route.d.ts +0 -17
- package/dist/features/seller/api/offers/route.js +0 -63
- package/dist/features/seller/components/SellerAuctionsView.d.ts +0 -9
- package/dist/features/seller/components/SellerAuctionsView.js +0 -113
- package/dist/features/seller/components/SellerOffersPanel.d.ts +0 -21
- package/dist/features/seller/components/SellerOffersPanel.js +0 -160
- package/dist/features/seller/components/SellerPreOrdersView.d.ts +0 -6
- package/dist/features/seller/components/SellerPreOrdersView.js +0 -148
- package/dist/features/seller/components/SellerPrizeDrawsView.d.ts +0 -6
- package/dist/features/seller/components/SellerPrizeDrawsView.js +0 -152
- package/dist/features/seller/permission-map.d.ts +0 -5
- package/dist/features/seller/permission-map.js +0 -17
- package/dist/features/shell/StepForm.d.ts +0 -80
- package/dist/features/shell/StepForm.js +0 -136
- package/dist/features/stores/api/route.d.ts +0 -12
- package/dist/features/stores/api/route.js +0 -110
- package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.js +0 -33
- package/dist/features/tester/seed-data/authored/account-auth__testing-program.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/account-auth__testing-program.js +0 -138
- package/dist/features/tester/seed-data/bids-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/bids-tester-seed-data.js +0 -37
- package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/blog-tester-seed-data.js +0 -40
- package/dist/features/tester/seed-data/carts-tester-seed-data.d.ts +0 -3
- package/dist/features/tester/seed-data/carts-tester-seed-data.js +0 -70
- package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +0 -218
- package/dist/features/tester/seed-data/coupons-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +0 -76
- package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/events-tester-seed-data.js +0 -193
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +0 -191
- package/dist/features/tester/seed-data/orders-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +0 -329
- package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/products-tester-seed-data.js +0 -815
- package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/stores-tester-seed-data.js +0 -79
- package/dist/features/wishlist/components/WishlistToggleButton.d.ts +0 -12
- package/dist/features/wishlist/components/WishlistToggleButton.js +0 -36
- package/dist/monitoring/analytics.d.ts +0 -80
- package/dist/monitoring/analytics.js +0 -176
- package/dist/next/middleware/pii-redactor.d.ts +0 -32
- package/dist/next/middleware/pii-redactor.js +0 -84
- package/dist/providers/db-firebase/realtime.d.ts +0 -42
- package/dist/providers/db-firebase/realtime.js +0 -138
- package/dist/security/rbac/Can.d.ts +0 -31
- package/dist/security/rbac/Can.js +0 -22
- package/dist/security/rbac/default-roles.d.ts +0 -2
- package/dist/security/rbac/default-roles.js +0 -83
- package/dist/security/rbac/hook.d.ts +0 -20
- package/dist/security/rbac/hook.js +0 -30
- package/dist/security/rbac/index.d.ts +0 -8
- package/dist/security/rbac/index.js +0 -6
- package/dist/security/rbac/middleware.d.ts +0 -51
- package/dist/security/rbac/middleware.js +0 -51
- package/dist/security/rbac/resolver.d.ts +0 -5
- package/dist/security/rbac/resolver.js +0 -38
- package/dist/security/rbac/server.d.ts +0 -24
- package/dist/security/rbac/server.js +0 -42
- package/dist/security/rbac/types.d.ts +0 -22
- package/dist/security/rbac/types.js +0 -2
- package/dist/seed/conversations-seed-data.d.ts +0 -2
- package/dist/seed/conversations-seed-data.js +0 -292
- package/dist/utils/filter.helper.d.ts +0 -18
- package/dist/utils/filter.helper.js +0 -21
- package/dist/utils/pagination.helper.d.ts +0 -23
- package/dist/utils/pagination.helper.js +0 -24
- package/dist/utils/search-tokens.d.ts +0 -2
- package/dist/utils/search-tokens.js +0 -21
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/** Required permission for each admin route path */
|
|
2
|
-
export const ADMIN_PAGE_PERMISSIONS = {
|
|
3
|
-
"/admin": "admin:access",
|
|
4
|
-
"/admin/dashboard": "admin:analytics",
|
|
5
|
-
"/admin/users": "admin:users",
|
|
6
|
-
"/admin/orders": "admin:orders",
|
|
7
|
-
"/admin/products": "admin:products",
|
|
8
|
-
"/admin/reviews": "admin:reviews",
|
|
9
|
-
"/admin/payouts": "admin:payouts",
|
|
10
|
-
"/admin/shipments": "admin:shipments",
|
|
11
|
-
"/admin/catalogue-approvals": "admin:catalogue",
|
|
12
|
-
"/admin/kyc": "admin:kyc",
|
|
13
|
-
"/admin/analytics": "admin:analytics",
|
|
14
|
-
"/admin/activity": "admin:activity",
|
|
15
|
-
"/admin/sessions": "admin:sessions",
|
|
16
|
-
"/admin/newsletter": "admin:newsletter",
|
|
17
|
-
"/admin/search": "admin:search",
|
|
18
|
-
"/admin/feature-flags": "admin:feature-flags",
|
|
19
|
-
"/admin/site-settings": "admin:settings",
|
|
20
|
-
"/admin/navigation": "admin:cms:navigation",
|
|
21
|
-
"/admin/cms": "admin:cms",
|
|
22
|
-
"/admin/carousel": "admin:cms",
|
|
23
|
-
"/admin/homepage-sections": "admin:cms",
|
|
24
|
-
"/admin/copilot": "admin:settings",
|
|
25
|
-
"/admin/reports": "admin:analytics",
|
|
26
|
-
};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { BaseRepository } from "../../../providers/db-firebase";
|
|
2
|
-
import type { ChatRoomCreateInput, ChatRoomDocument } from "../schemas";
|
|
3
|
-
export type { ChatRoomDocument, ChatRoomCreateInput } from "../schemas";
|
|
4
|
-
export { CHAT_ROOM_COLLECTION } from "../schemas";
|
|
5
|
-
declare class ChatRepository extends BaseRepository<ChatRoomDocument> {
|
|
6
|
-
constructor();
|
|
7
|
-
/** Override mapDoc to auto-decrypt PII on every chat room read */
|
|
8
|
-
protected mapDoc<D = ChatRoomDocument>(snap: import("../../../providers/db-firebase").DocumentSnapshot): D;
|
|
9
|
-
/**
|
|
10
|
-
* Create a new chat room (buyer ↔ seller for an order)
|
|
11
|
-
*/
|
|
12
|
-
create(input: ChatRoomCreateInput): Promise<ChatRoomDocument>;
|
|
13
|
-
/**
|
|
14
|
-
* Find existing room by buyerId + ownerId + orderId (idempotent create)
|
|
15
|
-
*/
|
|
16
|
-
findRoom(buyerId: string, ownerId: string, orderId: string): Promise<ChatRoomDocument | null>;
|
|
17
|
-
/**
|
|
18
|
-
* Get all chat room IDs that the user can currently access.
|
|
19
|
-
* Excludes rooms where the user has soft-deleted or where adminDeleted=true.
|
|
20
|
-
*/
|
|
21
|
-
getChatIdsForUser(userId: string): Promise<string[]>;
|
|
22
|
-
/**
|
|
23
|
-
* List all visible rooms for a user (most recent first).
|
|
24
|
-
* Excludes rooms the user has deleted and admin-deleted group rooms.
|
|
25
|
-
*/
|
|
26
|
-
listForUser(userId: string): Promise<ChatRoomDocument[]>;
|
|
27
|
-
/**
|
|
28
|
-
* Update last message preview (called after a new message is sent)
|
|
29
|
-
*/
|
|
30
|
-
updateLastMessage(chatId: string, message: string): Promise<void>;
|
|
31
|
-
/**
|
|
32
|
-
* Soft-delete a 1-1 chat room for a single user.
|
|
33
|
-
*
|
|
34
|
-
* - Adds `uid` to `deletedBy`.
|
|
35
|
-
* - If both participants have now deleted → permanently deletes the
|
|
36
|
-
* Firestore document and the RTDB subtree.
|
|
37
|
-
*
|
|
38
|
-
* @returns `"hidden"` if the room was only hidden for this user,
|
|
39
|
-
* `"deleted"` if it was permanently purged.
|
|
40
|
-
*/
|
|
41
|
-
softDeleteForUser(chatId: string, uid: string): Promise<"hidden" | "deleted">;
|
|
42
|
-
/**
|
|
43
|
-
* Admin hard-delete for group chats.
|
|
44
|
-
*
|
|
45
|
-
* Sets `adminDeleted: true` in Firestore (revokes all access) and
|
|
46
|
-
* removes the RTDB subtree.
|
|
47
|
-
*/
|
|
48
|
-
adminHardDelete(chatId: string): Promise<void>;
|
|
49
|
-
}
|
|
50
|
-
export declare const chatRepository: ChatRepository;
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { normalizeError } from "../../../errors/normalize";
|
|
2
|
-
/**
|
|
3
|
-
* Chat Repository
|
|
4
|
-
*
|
|
5
|
-
* Manages Firestore chat room metadata documents.
|
|
6
|
-
* Actual messages are stored in Realtime Database for live streaming.
|
|
7
|
-
* This repository provides the persistent record of rooms and participants.
|
|
8
|
-
*
|
|
9
|
-
* Deletion semantics
|
|
10
|
-
* ------------------
|
|
11
|
-
* 1-1 rooms:
|
|
12
|
-
* - `softDeleteForUser(chatId, uid)` adds the uid to `deletedBy`.
|
|
13
|
-
* - When BOTH participants are in `deletedBy` the room is permanently deleted
|
|
14
|
-
* (Firestore doc + RTDB subtree) by the same method.
|
|
15
|
-
* - Until then the room is invisible only to the deleting user.
|
|
16
|
-
*
|
|
17
|
-
* Group rooms (admin-created):
|
|
18
|
-
* - Only an admin may delete via `adminHardDelete(chatId)`.
|
|
19
|
-
* - Sets `adminDeleted: true` and deletes the RTDB subtree.
|
|
20
|
-
* - All participants lose access on next token refresh.
|
|
21
|
-
*/
|
|
22
|
-
import { getAdminRealtimeDb } from "../../../providers/db-firebase";
|
|
23
|
-
import { BaseRepository } from "../../../providers/db-firebase";
|
|
24
|
-
import { prepareForFirestore } from "../../../providers/db-firebase";
|
|
25
|
-
import { DatabaseError } from "../../../errors";
|
|
26
|
-
import { CONVERSATION_FIELDS, STORE_FIELDS } from "../../../constants/field-names";
|
|
27
|
-
import { CHAT_ROOM_COLLECTION } from "../schemas";
|
|
28
|
-
import { encryptPiiFields, decryptPiiFields, CHAT_PII_FIELDS, } from "../../../security";
|
|
29
|
-
export { CHAT_ROOM_COLLECTION } from "../schemas";
|
|
30
|
-
class ChatRepository extends BaseRepository {
|
|
31
|
-
constructor() {
|
|
32
|
-
super(CHAT_ROOM_COLLECTION);
|
|
33
|
-
}
|
|
34
|
-
/** Override mapDoc to auto-decrypt PII on every chat room read */
|
|
35
|
-
mapDoc(snap) {
|
|
36
|
-
const raw = super.mapDoc(snap);
|
|
37
|
-
return decryptPiiFields(raw, [
|
|
38
|
-
...CHAT_PII_FIELDS,
|
|
39
|
-
]);
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Create a new chat room (buyer ↔ seller for an order)
|
|
43
|
-
*/
|
|
44
|
-
async create(input) {
|
|
45
|
-
const id = input.isGroup
|
|
46
|
-
? `chat_group_${Date.now()}`
|
|
47
|
-
: `chat_${input.buyerId}_${input.ownerId}_${input.orderId}`;
|
|
48
|
-
const doc = {
|
|
49
|
-
...input,
|
|
50
|
-
deletedBy: [],
|
|
51
|
-
adminDeleted: false,
|
|
52
|
-
createdAt: new Date(),
|
|
53
|
-
updatedAt: new Date(),
|
|
54
|
-
};
|
|
55
|
-
const encrypted = encryptPiiFields({ ...doc }, [...CHAT_PII_FIELDS]);
|
|
56
|
-
await this.db
|
|
57
|
-
.collection(this.collection)
|
|
58
|
-
.doc(id)
|
|
59
|
-
.set(prepareForFirestore(encrypted));
|
|
60
|
-
return { id, ...doc }; // return plaintext to caller
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Find existing room by buyerId + ownerId + orderId (idempotent create)
|
|
64
|
-
*/
|
|
65
|
-
async findRoom(buyerId, ownerId, orderId) {
|
|
66
|
-
const id = `chat_${buyerId}_${ownerId}_${orderId}`;
|
|
67
|
-
return this.findById(id);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Get all chat room IDs that the user can currently access.
|
|
71
|
-
* Excludes rooms where the user has soft-deleted or where adminDeleted=true.
|
|
72
|
-
*/
|
|
73
|
-
async getChatIdsForUser(userId) {
|
|
74
|
-
try {
|
|
75
|
-
const [asBuyer, asSeller] = await Promise.all([
|
|
76
|
-
this.getCollection()
|
|
77
|
-
.where(CONVERSATION_FIELDS.BUYER_ID, "==", userId)
|
|
78
|
-
.where("adminDeleted", "==", false)
|
|
79
|
-
.get(),
|
|
80
|
-
this.getCollection()
|
|
81
|
-
.where(STORE_FIELDS.OWNER_ID, "==", userId)
|
|
82
|
-
.where("adminDeleted", "==", false)
|
|
83
|
-
.get(),
|
|
84
|
-
]);
|
|
85
|
-
const ids = new Set();
|
|
86
|
-
[...asBuyer.docs, ...asSeller.docs].forEach((d) => {
|
|
87
|
-
const deletedBy = d.data().deletedBy ?? [];
|
|
88
|
-
if (!deletedBy.includes(userId)) {
|
|
89
|
-
ids.add(d.id);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
return Array.from(ids);
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
void normalizeError(error);
|
|
96
|
-
throw new DatabaseError(`Failed to get chatIds for user: ${userId}`, error);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* List all visible rooms for a user (most recent first).
|
|
101
|
-
* Excludes rooms the user has deleted and admin-deleted group rooms.
|
|
102
|
-
*/
|
|
103
|
-
async listForUser(userId) {
|
|
104
|
-
try {
|
|
105
|
-
const [asBuyer, asSeller] = await Promise.all([
|
|
106
|
-
this.getCollection()
|
|
107
|
-
.where(CONVERSATION_FIELDS.BUYER_ID, "==", userId)
|
|
108
|
-
.where("adminDeleted", "==", false)
|
|
109
|
-
.orderBy(CONVERSATION_FIELDS.UPDATED_AT, "desc")
|
|
110
|
-
.get(),
|
|
111
|
-
this.getCollection()
|
|
112
|
-
.where(STORE_FIELDS.OWNER_ID, "==", userId)
|
|
113
|
-
.where("adminDeleted", "==", false)
|
|
114
|
-
.orderBy(CONVERSATION_FIELDS.UPDATED_AT, "desc")
|
|
115
|
-
.get(),
|
|
116
|
-
]);
|
|
117
|
-
const seen = new Set();
|
|
118
|
-
const rooms = [];
|
|
119
|
-
[...asBuyer.docs, ...asSeller.docs].forEach((d) => {
|
|
120
|
-
if (seen.has(d.id))
|
|
121
|
-
return;
|
|
122
|
-
seen.add(d.id);
|
|
123
|
-
const data = d.data();
|
|
124
|
-
const deletedBy = data.deletedBy ?? [];
|
|
125
|
-
// Only include rooms the user hasn't personally deleted
|
|
126
|
-
if (!deletedBy.includes(userId)) {
|
|
127
|
-
rooms.push(decryptPiiFields({ id: d.id, ...data }, [...CHAT_PII_FIELDS]));
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
rooms.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
131
|
-
return rooms;
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
void normalizeError(error);
|
|
135
|
-
throw new DatabaseError(`Failed to list rooms for user: ${userId}`, error);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Update last message preview (called after a new message is sent)
|
|
140
|
-
*/
|
|
141
|
-
async updateLastMessage(chatId, message) {
|
|
142
|
-
try {
|
|
143
|
-
await this.db
|
|
144
|
-
.collection(this.collection)
|
|
145
|
-
.doc(chatId)
|
|
146
|
-
.update(prepareForFirestore({
|
|
147
|
-
lastMessage: message.slice(0, 80),
|
|
148
|
-
lastMessageAt: new Date(),
|
|
149
|
-
updatedAt: new Date(),
|
|
150
|
-
}));
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
void normalizeError(error);
|
|
154
|
-
throw new DatabaseError(`Failed to update last message for room: ${chatId}`, error);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Soft-delete a 1-1 chat room for a single user.
|
|
159
|
-
*
|
|
160
|
-
* - Adds `uid` to `deletedBy`.
|
|
161
|
-
* - If both participants have now deleted → permanently deletes the
|
|
162
|
-
* Firestore document and the RTDB subtree.
|
|
163
|
-
*
|
|
164
|
-
* @returns `"hidden"` if the room was only hidden for this user,
|
|
165
|
-
* `"deleted"` if it was permanently purged.
|
|
166
|
-
*/
|
|
167
|
-
async softDeleteForUser(chatId, uid) {
|
|
168
|
-
try {
|
|
169
|
-
const docRef = this.db.collection(this.collection).doc(chatId);
|
|
170
|
-
const snap = await docRef.get();
|
|
171
|
-
if (!snap.exists)
|
|
172
|
-
return "deleted";
|
|
173
|
-
const data = snap.data();
|
|
174
|
-
const deletedBy = Array.from(new Set([...(data.deletedBy ?? []), uid]));
|
|
175
|
-
const bothDeleted = !data.isGroup &&
|
|
176
|
-
deletedBy.includes(data.buyerId) &&
|
|
177
|
-
deletedBy.includes(data.ownerId);
|
|
178
|
-
if (bothDeleted) {
|
|
179
|
-
// Permanently remove Firestore doc + RTDB messages
|
|
180
|
-
await docRef.delete();
|
|
181
|
-
try {
|
|
182
|
-
await getAdminRealtimeDb().ref(`/chat/${chatId}`).remove();
|
|
183
|
-
}
|
|
184
|
-
catch (_err) {
|
|
185
|
-
void normalizeError(_err); // RTDB cleanup is best-effort — Firestore doc already deleted
|
|
186
|
-
}
|
|
187
|
-
return "deleted";
|
|
188
|
-
}
|
|
189
|
-
// Otherwise just record this user's deletion
|
|
190
|
-
await docRef.update(prepareForFirestore({ deletedBy, updatedAt: new Date() }));
|
|
191
|
-
return "hidden";
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
void normalizeError(error);
|
|
195
|
-
throw new DatabaseError(`Failed to soft-delete room ${chatId} for user ${uid}`, error);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Admin hard-delete for group chats.
|
|
200
|
-
*
|
|
201
|
-
* Sets `adminDeleted: true` in Firestore (revokes all access) and
|
|
202
|
-
* removes the RTDB subtree.
|
|
203
|
-
*/
|
|
204
|
-
async adminHardDelete(chatId) {
|
|
205
|
-
try {
|
|
206
|
-
await this.db
|
|
207
|
-
.collection(this.collection)
|
|
208
|
-
.doc(chatId)
|
|
209
|
-
.update(prepareForFirestore({ adminDeleted: true, updatedAt: new Date() }));
|
|
210
|
-
try {
|
|
211
|
-
await getAdminRealtimeDb().ref(`/chat/${chatId}`).remove();
|
|
212
|
-
}
|
|
213
|
-
catch (_err) {
|
|
214
|
-
void normalizeError(_err); // RTDB cleanup is best-effort — adminDeleted flag already revokes access
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
void normalizeError(error);
|
|
219
|
-
throw new DatabaseError(`Failed to admin-delete group room ${chatId}`, error);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
export const chatRepository = new ChatRepository();
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Password Change OTP Actions.
|
|
3
|
-
*
|
|
4
|
-
* Server-side gate for POST /api/user/change-password — see
|
|
5
|
-
* ../password-change-otp.ts for the full incident writeup. Mirrors
|
|
6
|
-
* checkout-value-otp-actions.ts's shape exactly (same crypto primitives,
|
|
7
|
-
* separate Firestore namespace and copy).
|
|
8
|
-
*/
|
|
9
|
-
/** Sends a password-change OTP to the account's registered email. Returns masked email so the UI can confirm which inbox to check. */
|
|
10
|
-
export declare function sendPasswordChangeOtp(userId: string, userEmail: string): Promise<{
|
|
11
|
-
maskedEmail: string;
|
|
12
|
-
}>;
|
|
13
|
-
/** Verifies the 6-digit password-change OTP and marks the Firestore record verified. */
|
|
14
|
-
export declare function verifyPasswordChangeOtp(userId: string, code: string): Promise<void>;
|
|
15
|
-
/** Whether the account has a currently-verified password-change OTP on file. */
|
|
16
|
-
export declare function isPasswordChangeOtpVerified(userId: string): Promise<boolean>;
|
|
17
|
-
/** Consumes the verified OTP so it can't be reused for a future password change. */
|
|
18
|
-
export declare function consumePasswordChangeOtp(userId: string): Promise<void>;
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Password Change OTP Actions.
|
|
3
|
-
*
|
|
4
|
-
* Server-side gate for POST /api/user/change-password — see
|
|
5
|
-
* ../password-change-otp.ts for the full incident writeup. Mirrors
|
|
6
|
-
* checkout-value-otp-actions.ts's shape exactly (same crypto primitives,
|
|
7
|
-
* separate Firestore namespace and copy).
|
|
8
|
-
*/
|
|
9
|
-
import { timingSafeEqual } from "crypto";
|
|
10
|
-
import { ValidationError } from "../../../errors";
|
|
11
|
-
import { serverLogger } from "../../../monitoring";
|
|
12
|
-
import { sendEmail } from "../../contact/email";
|
|
13
|
-
import { getAdminDb } from "../../../providers/db-firebase";
|
|
14
|
-
import { resolveDate } from "../../../utils";
|
|
15
|
-
import { hashOtp, generateOtpCode, PASSWORD_CHANGE_OTP_EXPIRY_MS, PASSWORD_CHANGE_OTP_MAX_ATTEMPTS, passwordChangeOtpRef, enforcePasswordChangeOtpRateLimit, } from "../password-change-otp";
|
|
16
|
-
/** Sends a password-change OTP to the account's registered email. Returns masked email so the UI can confirm which inbox to check. */
|
|
17
|
-
export async function sendPasswordChangeOtp(userId, userEmail) {
|
|
18
|
-
const db = getAdminDb();
|
|
19
|
-
await enforcePasswordChangeOtpRateLimit(db, userId);
|
|
20
|
-
const code = generateOtpCode();
|
|
21
|
-
const codeHash = hashOtp(code);
|
|
22
|
-
const expiresAt = new Date(Date.now() + PASSWORD_CHANGE_OTP_EXPIRY_MS);
|
|
23
|
-
await passwordChangeOtpRef(db, userId).set({
|
|
24
|
-
codeHash,
|
|
25
|
-
expiresAt,
|
|
26
|
-
attempts: 0,
|
|
27
|
-
verified: false,
|
|
28
|
-
createdAt: new Date(),
|
|
29
|
-
});
|
|
30
|
-
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || "LetItRip";
|
|
31
|
-
const [local, domain] = userEmail.split("@");
|
|
32
|
-
const maskedEmail = domain
|
|
33
|
-
? `${local.length <= 2 ? "*".repeat(local.length) : local[0] + "*".repeat(local.length - 2) + local[local.length - 1]}@${domain}`
|
|
34
|
-
: "***";
|
|
35
|
-
await sendEmail({
|
|
36
|
-
to: userEmail,
|
|
37
|
-
subject: `${siteName}: Verify your password change`,
|
|
38
|
-
html: `
|
|
39
|
-
<div style="font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px">
|
|
40
|
-
<h2 style="margin-bottom:8px">Confirm Password Change</h2>
|
|
41
|
-
<p style="color:#555">Someone requested to change this account's password. Enter this code to confirm it was you:</p>
|
|
42
|
-
<div style="font-size:36px;font-weight:bold;letter-spacing:8px;text-align:center;margin:24px 0;padding:16px;background:#f3f4f6;border-radius:8px">${code}</div>
|
|
43
|
-
<p style="color:#888;font-size:12px">This code expires in 10 minutes. If you did not request this, your password has NOT been changed — but consider securing your account.</p>
|
|
44
|
-
</div>
|
|
45
|
-
`,
|
|
46
|
-
});
|
|
47
|
-
serverLogger.info(`Password change OTP sent: uid=${userId}`);
|
|
48
|
-
return { maskedEmail };
|
|
49
|
-
}
|
|
50
|
-
/** Verifies the 6-digit password-change OTP and marks the Firestore record verified. */
|
|
51
|
-
export async function verifyPasswordChangeOtp(userId, code) {
|
|
52
|
-
const db = getAdminDb();
|
|
53
|
-
const ref = passwordChangeOtpRef(db, userId);
|
|
54
|
-
const snap = await ref.get();
|
|
55
|
-
if (!snap.exists) {
|
|
56
|
-
throw new ValidationError("No verification code found. Please request a new code.");
|
|
57
|
-
}
|
|
58
|
-
const otpDoc = snap.data();
|
|
59
|
-
if (otpDoc.verified)
|
|
60
|
-
return;
|
|
61
|
-
if (otpDoc.attempts >= PASSWORD_CHANGE_OTP_MAX_ATTEMPTS) {
|
|
62
|
-
throw new ValidationError("Too many failed attempts. Please request a new code.");
|
|
63
|
-
}
|
|
64
|
-
if (Date.now() > (resolveDate(otpDoc.expiresAt)?.getTime() ?? 0)) {
|
|
65
|
-
throw new ValidationError("Code expired. Please request a new one.");
|
|
66
|
-
}
|
|
67
|
-
const inputHash = hashOtp(code);
|
|
68
|
-
if (!timingSafeEqual(Buffer.from(inputHash, "hex"), Buffer.from(otpDoc.codeHash, "hex"))) {
|
|
69
|
-
await ref.update({ attempts: otpDoc.attempts + 1 });
|
|
70
|
-
throw new ValidationError("Invalid code. Please check and try again.");
|
|
71
|
-
}
|
|
72
|
-
await ref.update({ verified: true, verifiedAt: new Date() });
|
|
73
|
-
serverLogger.info(`Password change OTP verified: uid=${userId}`);
|
|
74
|
-
}
|
|
75
|
-
/** Whether the account has a currently-verified password-change OTP on file. */
|
|
76
|
-
export async function isPasswordChangeOtpVerified(userId) {
|
|
77
|
-
const db = getAdminDb();
|
|
78
|
-
const snap = await passwordChangeOtpRef(db, userId).get();
|
|
79
|
-
if (!snap.exists)
|
|
80
|
-
return false;
|
|
81
|
-
const otpDoc = snap.data();
|
|
82
|
-
return otpDoc.verified === true;
|
|
83
|
-
}
|
|
84
|
-
/** Consumes the verified OTP so it can't be reused for a future password change. */
|
|
85
|
-
export async function consumePasswordChangeOtp(userId) {
|
|
86
|
-
const db = getAdminDb();
|
|
87
|
-
await passwordChangeOtpRef(db, userId).delete();
|
|
88
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Realtime Token Domain Actions — appkit
|
|
3
|
-
*
|
|
4
|
-
* Issues a Firebase custom auth token for Realtime Database subscriptions.
|
|
5
|
-
* Called by letitrip thin wrapper which handles auth + rate limiting.
|
|
6
|
-
*/
|
|
7
|
-
export interface RealtimeTokenResult {
|
|
8
|
-
customToken: string;
|
|
9
|
-
expiresAt: number;
|
|
10
|
-
}
|
|
11
|
-
export declare function issueRealtimeToken(userId: string, userRole: string): Promise<RealtimeTokenResult>;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Realtime Token Domain Actions — appkit
|
|
3
|
-
*
|
|
4
|
-
* Issues a Firebase custom auth token for Realtime Database subscriptions.
|
|
5
|
-
* Called by letitrip thin wrapper which handles auth + rate limiting.
|
|
6
|
-
*/
|
|
7
|
-
import { getAdminAuth } from "../../../providers/db-firebase/index";
|
|
8
|
-
import { conversationsRepository } from "../../messages/repository/conversations.repository";
|
|
9
|
-
import { storeRepository } from "../../stores/repository/store.repository";
|
|
10
|
-
import { serverLogger } from "../../../monitoring/index";
|
|
11
|
-
import { normalizeError } from "../../../errors/normalize";
|
|
12
|
-
export async function issueRealtimeToken(userId, userRole) {
|
|
13
|
-
const conversationIds = {};
|
|
14
|
-
try {
|
|
15
|
-
const buyerConvs = await conversationsRepository.listByBuyer(userId);
|
|
16
|
-
buyerConvs.forEach((c) => { conversationIds[c.id] = true; });
|
|
17
|
-
if (userRole === "seller") {
|
|
18
|
-
const store = await storeRepository.findByOwnerId(userId);
|
|
19
|
-
if (store?.id) {
|
|
20
|
-
const sellerConvs = await conversationsRepository.listByStore(store.id);
|
|
21
|
-
sellerConvs.forEach((c) => { conversationIds[c.id] = true; });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
catch (err) {
|
|
26
|
-
void normalizeError(err);
|
|
27
|
-
serverLogger.warn("Could not resolve conversationIds for realtime token", { userId, err });
|
|
28
|
-
}
|
|
29
|
-
const customToken = await getAdminAuth().createCustomToken(userId, {
|
|
30
|
-
role: userRole,
|
|
31
|
-
conversationIds,
|
|
32
|
-
});
|
|
33
|
-
serverLogger.info("issueRealtimeToken: token issued", {
|
|
34
|
-
userId,
|
|
35
|
-
conversationCount: Object.keys(conversationIds).length,
|
|
36
|
-
});
|
|
37
|
-
return { customToken, expiresAt: Date.now() + 3600000 };
|
|
38
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Password Change OTP.
|
|
3
|
-
*
|
|
4
|
-
* Root-caused 2026-08-20: POST /api/user/change-password validated the
|
|
5
|
-
* *shape* of `currentPassword` (Zod: non-empty, differs from newPassword)
|
|
6
|
-
* but the handler never read or verified it — `getAdminAuth().updateUser()`
|
|
7
|
-
* overwrote the password unconditionally for whatever `uid` the session
|
|
8
|
-
* cookie carried. The only real current-password check
|
|
9
|
-
* (`reauthenticateWithCredential` in the Firebase client SDK) happens in
|
|
10
|
-
* the browser and is trivially bypassed by calling the API directly with a
|
|
11
|
-
* stolen session cookie — a session hijack (XSS, log leak, etc.) was
|
|
12
|
-
* enough to take over the account's password with zero server-enforced
|
|
13
|
-
* identity check. This module gates the actual server-side write behind an
|
|
14
|
-
* email OTP, verified server-side, mirroring the Tier PP checkout-value-OTP
|
|
15
|
-
* pattern exactly (same crypto primitives, reused not duplicated) so a
|
|
16
|
-
* session-cookie thief also needs access to the account's inbox.
|
|
17
|
-
*/
|
|
18
|
-
import { hashOtp, generateOtpCode, CHECKOUT_VALUE_OTP_EXPIRY_MS as PASSWORD_CHANGE_OTP_EXPIRY_MS, CHECKOUT_VALUE_OTP_EXPIRY_MINUTES as PASSWORD_CHANGE_OTP_EXPIRY_MINUTES, CHECKOUT_VALUE_OTP_COOLDOWN_MS as PASSWORD_CHANGE_OTP_COOLDOWN_MS, CHECKOUT_VALUE_OTP_MAX_ATTEMPTS as PASSWORD_CHANGE_OTP_MAX_ATTEMPTS, CHECKOUT_VALUE_OTP_VERIFY_RATE_LIMIT as PASSWORD_CHANGE_OTP_VERIFY_RATE_LIMIT } from "./checkout-value-otp";
|
|
19
|
-
export { hashOtp, generateOtpCode, PASSWORD_CHANGE_OTP_EXPIRY_MS, PASSWORD_CHANGE_OTP_EXPIRY_MINUTES, PASSWORD_CHANGE_OTP_COOLDOWN_MS, PASSWORD_CHANGE_OTP_MAX_ATTEMPTS, PASSWORD_CHANGE_OTP_VERIFY_RATE_LIMIT, };
|
|
20
|
-
type Db = FirebaseFirestore.Firestore;
|
|
21
|
-
/** Single-slot doc per user — a user can only be mid-password-change once. */
|
|
22
|
-
export declare function passwordChangeOtpRef(db: Db, uid: string): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
23
|
-
export declare function passwordChangeOtpRateLimitRef(db: Db, uid: string): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
24
|
-
export interface PasswordChangeOtpDoc {
|
|
25
|
-
codeHash: string;
|
|
26
|
-
expiresAt: FirebaseFirestore.Timestamp;
|
|
27
|
-
attempts: number;
|
|
28
|
-
verified: boolean;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Enforce the per-user send rate-limit.
|
|
32
|
-
* Throws AuthorizationError("passwordChangeOtpRateLimit") when throttled.
|
|
33
|
-
*/
|
|
34
|
-
export declare function enforcePasswordChangeOtpRateLimit(db: Db, uid: string): Promise<void>;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Password Change OTP.
|
|
3
|
-
*
|
|
4
|
-
* Root-caused 2026-08-20: POST /api/user/change-password validated the
|
|
5
|
-
* *shape* of `currentPassword` (Zod: non-empty, differs from newPassword)
|
|
6
|
-
* but the handler never read or verified it — `getAdminAuth().updateUser()`
|
|
7
|
-
* overwrote the password unconditionally for whatever `uid` the session
|
|
8
|
-
* cookie carried. The only real current-password check
|
|
9
|
-
* (`reauthenticateWithCredential` in the Firebase client SDK) happens in
|
|
10
|
-
* the browser and is trivially bypassed by calling the API directly with a
|
|
11
|
-
* stolen session cookie — a session hijack (XSS, log leak, etc.) was
|
|
12
|
-
* enough to take over the account's password with zero server-enforced
|
|
13
|
-
* identity check. This module gates the actual server-side write behind an
|
|
14
|
-
* email OTP, verified server-side, mirroring the Tier PP checkout-value-OTP
|
|
15
|
-
* pattern exactly (same crypto primitives, reused not duplicated) so a
|
|
16
|
-
* session-cookie thief also needs access to the account's inbox.
|
|
17
|
-
*/
|
|
18
|
-
import { AuthorizationError } from "../../errors";
|
|
19
|
-
import { resolveDate } from "../../utils";
|
|
20
|
-
import { USER_COLLECTION } from "./schemas";
|
|
21
|
-
import { hashOtp, generateOtpCode, CHECKOUT_VALUE_OTP_EXPIRY_MS as PASSWORD_CHANGE_OTP_EXPIRY_MS, CHECKOUT_VALUE_OTP_EXPIRY_MINUTES as PASSWORD_CHANGE_OTP_EXPIRY_MINUTES, CHECKOUT_VALUE_OTP_COOLDOWN_MS as PASSWORD_CHANGE_OTP_COOLDOWN_MS, CHECKOUT_VALUE_OTP_MAX_ATTEMPTS as PASSWORD_CHANGE_OTP_MAX_ATTEMPTS, CHECKOUT_VALUE_OTP_VERIFY_RATE_LIMIT as PASSWORD_CHANGE_OTP_VERIFY_RATE_LIMIT, } from "./checkout-value-otp";
|
|
22
|
-
export { hashOtp, generateOtpCode, PASSWORD_CHANGE_OTP_EXPIRY_MS, PASSWORD_CHANGE_OTP_EXPIRY_MINUTES, PASSWORD_CHANGE_OTP_COOLDOWN_MS, PASSWORD_CHANGE_OTP_MAX_ATTEMPTS, PASSWORD_CHANGE_OTP_VERIFY_RATE_LIMIT, };
|
|
23
|
-
/** Single-slot doc per user — a user can only be mid-password-change once. */
|
|
24
|
-
export function passwordChangeOtpRef(db, uid) {
|
|
25
|
-
return db
|
|
26
|
-
.collection(USER_COLLECTION)
|
|
27
|
-
.doc(uid)
|
|
28
|
-
.collection("passwordChangeOtps")
|
|
29
|
-
.doc("current");
|
|
30
|
-
}
|
|
31
|
-
export function passwordChangeOtpRateLimitRef(db, uid) {
|
|
32
|
-
return db
|
|
33
|
-
.collection(USER_COLLECTION)
|
|
34
|
-
.doc(uid)
|
|
35
|
-
.collection("passwordChangeOtpRateLimit")
|
|
36
|
-
.doc("meta");
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Enforce the per-user send rate-limit.
|
|
40
|
-
* Throws AuthorizationError("passwordChangeOtpRateLimit") when throttled.
|
|
41
|
-
*/
|
|
42
|
-
export async function enforcePasswordChangeOtpRateLimit(db, uid) {
|
|
43
|
-
const metaRef = passwordChangeOtpRateLimitRef(db, uid);
|
|
44
|
-
await db.runTransaction(async (tx) => {
|
|
45
|
-
const metaSnap = await tx.get(metaRef);
|
|
46
|
-
const meta = metaSnap.exists
|
|
47
|
-
? metaSnap.data()
|
|
48
|
-
: null;
|
|
49
|
-
const lastSentMs = resolveDate(meta?.lastSentAt)?.getTime() ?? 0;
|
|
50
|
-
const elapsed = Date.now() - lastSentMs;
|
|
51
|
-
if (elapsed < PASSWORD_CHANGE_OTP_COOLDOWN_MS) {
|
|
52
|
-
throw new AuthorizationError("passwordChangeOtpRateLimit");
|
|
53
|
-
}
|
|
54
|
-
tx.set(metaRef, { lastSentAt: new Date() }, { merge: true });
|
|
55
|
-
});
|
|
56
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import type { startClassifiedConversationAction } from "../../../_internal/server/features/classified/actions";
|
|
3
|
-
export interface ClassifiedContactSellerPanelProps {
|
|
4
|
-
productId: string;
|
|
5
|
-
onContactSeller: typeof startClassifiedConversationAction;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* The only purchase-path CTA on a classified listing — no cart, no checkout.
|
|
9
|
-
* Extracted from the old ClassifiedDetailView so the richer, full-parity
|
|
10
|
-
* ClassifiedDetailPageView can slot this into a `renderBuyBar` prop.
|
|
11
|
-
*/
|
|
12
|
-
export declare function ClassifiedContactSellerPanel({ productId, onContactSeller }: ClassifiedContactSellerPanelProps): React.JSX.Element;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { normalizeError } from "../../../errors/normalize";
|
|
4
|
-
import { useState } from "react";
|
|
5
|
-
import Link from "next/link";
|
|
6
|
-
import { Button, Div, Stack, Text } from "../../../ui";
|
|
7
|
-
import { ROUTES } from "../../../next/routing/route-map";
|
|
8
|
-
const __P = {
|
|
9
|
-
p4: "p-[var(--appkit-space-4)]",
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
12
|
-
* The only purchase-path CTA on a classified listing — no cart, no checkout.
|
|
13
|
-
* Extracted from the old ClassifiedDetailView so the richer, full-parity
|
|
14
|
-
* ClassifiedDetailPageView can slot this into a `renderBuyBar` prop.
|
|
15
|
-
*/
|
|
16
|
-
export function ClassifiedContactSellerPanel({ productId, onContactSeller }) {
|
|
17
|
-
const [pending, setPending] = useState(false);
|
|
18
|
-
const [conversation, setConversation] = useState(null);
|
|
19
|
-
const [error, setError] = useState(null);
|
|
20
|
-
async function handleContactSeller() {
|
|
21
|
-
setPending(true);
|
|
22
|
-
setError(null);
|
|
23
|
-
try {
|
|
24
|
-
const result = await onContactSeller({ productId });
|
|
25
|
-
if (result && typeof result === "object" && "ok" in result) {
|
|
26
|
-
if (result.ok) {
|
|
27
|
-
setConversation(result.data);
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
setError(result.error);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
setConversation(result);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch (e) {
|
|
38
|
-
void normalizeError(e);
|
|
39
|
-
setError("Something went wrong");
|
|
40
|
-
}
|
|
41
|
-
finally {
|
|
42
|
-
setPending(false);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (conversation) {
|
|
46
|
-
return (_jsxs(Div, { className: `border border-border bg-muted/40 ${__P.p4}`, rounded: "lg", children: [_jsx(Text, { className: "mb-2", weight: "medium", children: "Conversation started!" }), _jsx(Link, { href: ROUTES.USER.MESSAGES, className: "text-primary underline underline-offset-2", children: "Go to your messages \u2192" })] }));
|
|
47
|
-
}
|
|
48
|
-
return (_jsxs(Stack, { gap: "sm", children: [error && (_jsx(Text, { className: "text-destructive", size: "sm", children: error })), _jsx(Button, { type: "button", variant: "primary", size: "lg", isLoading: pending, disabled: pending, onClick: handleContactSeller, className: "w-full", children: "Contact Seller" })] }));
|
|
49
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* feat-events — Next.js App Router API handler (GET /api/events)
|
|
3
|
-
*
|
|
4
|
-
* Consuming projects can create a 2-line stub:
|
|
5
|
-
*
|
|
6
|
-
* ```ts
|
|
7
|
-
* // app/api/events/route.ts
|
|
8
|
-
* export { GET } from "@mohasinac/feat-events";
|
|
9
|
-
* ```
|
|
10
|
-
*/
|
|
11
|
-
import { NextResponse } from "next/server.js";
|
|
12
|
-
export declare function GET(request: Request): Promise<NextResponse>;
|