@mohasinac/appkit 3.2.6 → 3.2.7
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/_internal/shared/actions/action-registry.js +62 -0
- package/dist/constants/api-endpoints.d.ts +12 -0
- package/dist/constants/api-endpoints.js +4 -0
- package/dist/features/about/components/PublicProfileView.js +8 -5
- package/dist/features/addresses/repository/addresses.repository.d.ts +14 -1
- package/dist/features/addresses/repository/addresses.repository.js +122 -1
- package/dist/features/addresses/schemas/firestore.d.ts +22 -3
- package/dist/features/addresses/schemas/firestore.js +25 -0
- package/dist/features/addresses/schemas/index.d.ts +2 -2
- package/dist/features/admin/components/AdminAddressClustersView.d.ts +5 -0
- package/dist/features/admin/components/AdminAddressClustersView.js +38 -0
- package/dist/features/admin/components/AdminAddressesView.d.ts +5 -0
- package/dist/features/admin/components/AdminAddressesView.js +137 -0
- package/dist/features/admin/components/AdminPaymentClustersView.d.ts +5 -0
- package/dist/features/admin/components/AdminPaymentClustersView.js +37 -0
- package/dist/features/admin/components/AdminPaymentMethodsView.d.ts +5 -0
- package/dist/features/admin/components/AdminPaymentMethodsView.js +130 -0
- package/dist/features/admin/components/index.d.ts +8 -0
- package/dist/features/admin/components/index.js +4 -0
- package/dist/features/auth/actions/profile-actions.d.ts +3 -1
- package/dist/features/auth/actions/profile-actions.js +29 -2
- package/dist/features/cart/schemas/index.d.ts +6 -6
- package/dist/features/orders/schemas/index.d.ts +4 -4
- package/dist/features/payments/repository/saved-payment-methods.repository.d.ts +48 -0
- package/dist/features/payments/repository/saved-payment-methods.repository.js +236 -0
- package/dist/features/payments/schemas/index.d.ts +1 -0
- package/dist/features/payments/schemas/index.js +1 -0
- package/dist/features/payments/schemas/saved-methods-firestore.d.ts +63 -0
- package/dist/features/payments/schemas/saved-methods-firestore.js +60 -0
- package/dist/features/payments/server.d.ts +2 -0
- package/dist/features/payments/server.js +2 -0
- package/dist/features/promotions/schemas/index.d.ts +6 -6
- package/dist/features/reviews/repository/reviews.repository.d.ts +12 -0
- package/dist/features/reviews/repository/reviews.repository.js +24 -0
- package/dist/features/reviews/schemas/firestore.d.ts +5 -1
- package/dist/features/reviews/schemas/firestore.js +2 -0
- package/dist/features/reviews/schemas/index.d.ts +6 -6
- package/dist/features/seller/components/SellerAddressesView.js +7 -3
- package/dist/features/support/schemas/index.d.ts +8 -8
- package/dist/features/wishlist/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/next/routing/route-map.d.ts +8 -0
- package/dist/next/routing/route-map.js +4 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +66 -66
- package/dist/schemas/webhooks/razorpay.d.ts +50 -50
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-schemas.d.ts +2 -0
- package/dist/security/pii-schemas.js +2 -0
- package/package.json +1 -2
|
@@ -1559,6 +1559,68 @@ export const ACTIONS = {
|
|
|
1559
1559
|
confirmKind: "danger",
|
|
1560
1560
|
},
|
|
1561
1561
|
},
|
|
1562
|
+
// ── Address ban management ─────────────────────────────────────────────
|
|
1563
|
+
"ban-address": {
|
|
1564
|
+
id: "admin.ban-address",
|
|
1565
|
+
label: "Ban Address",
|
|
1566
|
+
ariaLabel: "Ban this address",
|
|
1567
|
+
description: "Ban an address from being used on the platform.",
|
|
1568
|
+
kind: "danger",
|
|
1569
|
+
permissions: ["admin", "moderator"],
|
|
1570
|
+
confirmation: {
|
|
1571
|
+
title: "Ban this address?",
|
|
1572
|
+
body: "The address will be blocked from use at checkout across all accounts.",
|
|
1573
|
+
confirmLabel: "Ban address",
|
|
1574
|
+
confirmKind: "danger",
|
|
1575
|
+
},
|
|
1576
|
+
},
|
|
1577
|
+
"approve-unban": {
|
|
1578
|
+
id: "admin.approve-unban",
|
|
1579
|
+
label: "Approve Unban",
|
|
1580
|
+
ariaLabel: "Approve unban request",
|
|
1581
|
+
description: "Approve an address unban request and restore it to active.",
|
|
1582
|
+
kind: "primary",
|
|
1583
|
+
permissions: ["admin"],
|
|
1584
|
+
},
|
|
1585
|
+
"reject-unban": {
|
|
1586
|
+
id: "admin.reject-unban",
|
|
1587
|
+
label: "Reject Unban",
|
|
1588
|
+
ariaLabel: "Reject unban request",
|
|
1589
|
+
description: "Reject an address unban request — address stays banned.",
|
|
1590
|
+
kind: "secondary",
|
|
1591
|
+
permissions: ["admin"],
|
|
1592
|
+
},
|
|
1593
|
+
// ── Payment method ban management ──────────────────────────────────────
|
|
1594
|
+
"ban-payment-method": {
|
|
1595
|
+
id: "admin.ban-payment-method",
|
|
1596
|
+
label: "Ban Payment Method",
|
|
1597
|
+
ariaLabel: "Ban this payment method",
|
|
1598
|
+
description: "Ban a payment identifier from being used on the platform.",
|
|
1599
|
+
kind: "danger",
|
|
1600
|
+
permissions: ["admin", "moderator"],
|
|
1601
|
+
confirmation: {
|
|
1602
|
+
title: "Ban this payment method?",
|
|
1603
|
+
body: "The payment identifier will be blocked from checkout across all accounts.",
|
|
1604
|
+
confirmLabel: "Ban payment method",
|
|
1605
|
+
confirmKind: "danger",
|
|
1606
|
+
},
|
|
1607
|
+
},
|
|
1608
|
+
"approve-payment-unban": {
|
|
1609
|
+
id: "admin.approve-payment-unban",
|
|
1610
|
+
label: "Approve Unban",
|
|
1611
|
+
ariaLabel: "Approve payment method unban",
|
|
1612
|
+
description: "Approve a payment method unban request.",
|
|
1613
|
+
kind: "primary",
|
|
1614
|
+
permissions: ["admin"],
|
|
1615
|
+
},
|
|
1616
|
+
"reject-payment-unban": {
|
|
1617
|
+
id: "admin.reject-payment-unban",
|
|
1618
|
+
label: "Reject Unban",
|
|
1619
|
+
ariaLabel: "Reject payment method unban",
|
|
1620
|
+
description: "Reject a payment method unban request — stays banned.",
|
|
1621
|
+
kind: "secondary",
|
|
1622
|
+
permissions: ["admin"],
|
|
1623
|
+
},
|
|
1562
1624
|
},
|
|
1563
1625
|
CART: {
|
|
1564
1626
|
"clear-cart": {
|
|
@@ -137,6 +137,10 @@ export declare const ADMIN_ENDPOINTS: {
|
|
|
137
137
|
readonly SCAMMER_BY_ID: (id: string) => string;
|
|
138
138
|
readonly ADDRESSES: "/api/admin/addresses";
|
|
139
139
|
readonly ADDRESS_BY_ID: (id: string) => string;
|
|
140
|
+
readonly ADDRESS_CLUSTERS: "/api/admin/addresses/clusters";
|
|
141
|
+
readonly PAYMENT_METHODS: "/api/admin/payment-methods";
|
|
142
|
+
readonly PAYMENT_METHOD_BY_ID: (id: string) => string;
|
|
143
|
+
readonly PAYMENT_METHOD_CLUSTERS: "/api/admin/payment-methods/clusters";
|
|
140
144
|
readonly GROUPED_LISTINGS: "/api/admin/grouped-listings";
|
|
141
145
|
readonly GROUPED_LISTING_BY_ID: (id: string) => string;
|
|
142
146
|
readonly ADMIN_FULFILLMENT: (storeId: string) => string;
|
|
@@ -472,6 +476,10 @@ export declare const API_ENDPOINTS: {
|
|
|
472
476
|
readonly SCAMMER_BY_ID: (id: string) => string;
|
|
473
477
|
readonly ADDRESSES: "/api/admin/addresses";
|
|
474
478
|
readonly ADDRESS_BY_ID: (id: string) => string;
|
|
479
|
+
readonly ADDRESS_CLUSTERS: "/api/admin/addresses/clusters";
|
|
480
|
+
readonly PAYMENT_METHODS: "/api/admin/payment-methods";
|
|
481
|
+
readonly PAYMENT_METHOD_BY_ID: (id: string) => string;
|
|
482
|
+
readonly PAYMENT_METHOD_CLUSTERS: "/api/admin/payment-methods/clusters";
|
|
475
483
|
readonly GROUPED_LISTINGS: "/api/admin/grouped-listings";
|
|
476
484
|
readonly GROUPED_LISTING_BY_ID: (id: string) => string;
|
|
477
485
|
readonly ADMIN_FULFILLMENT: (storeId: string) => string;
|
|
@@ -809,6 +817,10 @@ export declare const API_ROUTES: {
|
|
|
809
817
|
readonly SCAMMER_BY_ID: (id: string) => string;
|
|
810
818
|
readonly ADDRESSES: "/api/admin/addresses";
|
|
811
819
|
readonly ADDRESS_BY_ID: (id: string) => string;
|
|
820
|
+
readonly ADDRESS_CLUSTERS: "/api/admin/addresses/clusters";
|
|
821
|
+
readonly PAYMENT_METHODS: "/api/admin/payment-methods";
|
|
822
|
+
readonly PAYMENT_METHOD_BY_ID: (id: string) => string;
|
|
823
|
+
readonly PAYMENT_METHOD_CLUSTERS: "/api/admin/payment-methods/clusters";
|
|
812
824
|
readonly GROUPED_LISTINGS: "/api/admin/grouped-listings";
|
|
813
825
|
readonly GROUPED_LISTING_BY_ID: (id: string) => string;
|
|
814
826
|
readonly ADMIN_FULFILLMENT: (storeId: string) => string;
|
|
@@ -156,6 +156,10 @@ export const ADMIN_ENDPOINTS = {
|
|
|
156
156
|
SCAMMER_BY_ID: (id) => `/api/admin/scammers/${id}`,
|
|
157
157
|
ADDRESSES: "/api/admin/addresses",
|
|
158
158
|
ADDRESS_BY_ID: (id) => `/api/admin/addresses/${id}`,
|
|
159
|
+
ADDRESS_CLUSTERS: "/api/admin/addresses/clusters",
|
|
160
|
+
PAYMENT_METHODS: "/api/admin/payment-methods",
|
|
161
|
+
PAYMENT_METHOD_BY_ID: (id) => `/api/admin/payment-methods/${id}`,
|
|
162
|
+
PAYMENT_METHOD_CLUSTERS: "/api/admin/payment-methods/clusters",
|
|
159
163
|
GROUPED_LISTINGS: "/api/admin/grouped-listings",
|
|
160
164
|
GROUPED_LISTING_BY_ID: (id) => `/api/admin/grouped-listings/${id}`,
|
|
161
165
|
ADMIN_FULFILLMENT: (storeId) => `/api/store/fulfillment?storeId=${encodeURIComponent(storeId)}`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import Link from "next/link";
|
|
3
|
-
import { getPublicUserProfile, getProfileStoreProducts,
|
|
3
|
+
import { getPublicUserProfile, getProfileStoreProducts, getReviewsAuthoredBy, getReviewsReceivedBy } from "../../auth/actions/profile-actions";
|
|
4
4
|
import { storeRepository } from "../../stores/repository/store.repository";
|
|
5
5
|
import { ROUTES } from "../../../constants";
|
|
6
6
|
import { PAGE_CONTAINER } from "../../../_internal/shared/styles/page";
|
|
@@ -63,11 +63,11 @@ export async function PublicProfileView({ userId, }) {
|
|
|
63
63
|
const t = await getTranslations("publicProfile");
|
|
64
64
|
const profile = await getPublicUserProfile(userId).catch(() => null);
|
|
65
65
|
const storeId = profile?.storeSlug ?? null;
|
|
66
|
-
const [products,
|
|
66
|
+
const [products, store, reviewsAuthored, reviewsReceivedAsBuyer] = await Promise.all([
|
|
67
67
|
storeId ? getProfileStoreProducts(storeId).catch(() => []) : Promise.resolve([]),
|
|
68
|
-
storeId ? getSellerReviews(storeId).catch(() => []) : Promise.resolve([]),
|
|
69
68
|
storeId ? storeRepository.findById(storeId).catch(() => null) : Promise.resolve(null),
|
|
70
69
|
getReviewsAuthoredBy(userId).catch(() => []),
|
|
70
|
+
getReviewsReceivedBy(userId).catch(() => []),
|
|
71
71
|
]);
|
|
72
72
|
const isSeller = isSellerUser(profile) || isAdminUser(profile);
|
|
73
73
|
// For sellers: lead with store identity (name + logo). For buyers: user identity.
|
|
@@ -83,7 +83,7 @@ export async function PublicProfileView({ userId, }) {
|
|
|
83
83
|
const pub = profile?.publicProfile;
|
|
84
84
|
const stats = profile?.stats;
|
|
85
85
|
const listingCount = products.length;
|
|
86
|
-
const reviewCount = reviewsAuthored.length +
|
|
86
|
+
const reviewCount = reviewsAuthored.length + reviewsReceivedAsBuyer.length;
|
|
87
87
|
const itemsSold = stats?.itemsSold ?? 0;
|
|
88
88
|
const auctionsWon = stats?.auctionsWon ?? 0;
|
|
89
89
|
const totalOrders = stats?.totalOrders ?? 0;
|
|
@@ -92,7 +92,7 @@ export async function PublicProfileView({ userId, }) {
|
|
|
92
92
|
const storeDescription = store?.storeDescription ?? pub?.storeDescription;
|
|
93
93
|
const profileHeroCtx = { displayName, photoURL, memberSince, isSeller, storeSlug, flex, page };
|
|
94
94
|
const statItems = buildProfileStatItems(t, { listingCount, reviewCount, itemsSold, auctionsWon, totalOrders, isSeller });
|
|
95
|
-
return (_jsxs(Div, { className: "-mx-4 md:-mx-6 lg:-mx-8 -mt-6 sm:-mt-8 lg:-mt-10", children: [renderProfileHero(t, profileHeroCtx), _jsxs(Stack, { gap: "2xl", className: `${page.container.md}`, paddingY: "y-3xl", children: [renderProfileStatsRow(flex, statItems), renderProfileBioSection(pub), renderStoreDescriptionSection(isSeller, storeSlug ?? null, storeDescription ?? null, storeName, t), renderProfileListingsSection(t, products, storeSlug ?? null), renderAuthoredReviewsSection(t, reviewsAuthored, displayName),
|
|
95
|
+
return (_jsxs(Div, { className: "-mx-4 md:-mx-6 lg:-mx-8 -mt-6 sm:-mt-8 lg:-mt-10", children: [renderProfileHero(t, profileHeroCtx), _jsxs(Stack, { gap: "2xl", className: `${page.container.md}`, paddingY: "y-3xl", children: [renderProfileStatsRow(flex, statItems), renderProfileBioSection(pub), renderStoreDescriptionSection(isSeller, storeSlug ?? null, storeDescription ?? null, storeName, t), renderProfileListingsSection(t, products, storeSlug ?? null), renderAuthoredReviewsSection(t, reviewsAuthored, displayName), reviewsReceivedAsBuyer.length > 0 && renderBuyerReceivedReviewsSection(t, reviewsReceivedAsBuyer), _jsx(Row, { justify: "center", padding: "t-xs", children: _jsxs(Link, { href: String(ROUTES.HOME), className: "text-sm text-zinc-400 dark:text-zinc-400 hover:text-neutral-600 dark:hover:text-zinc-300", children: ["\u2190 ", t("backHome")] }) })] })] }));
|
|
96
96
|
}
|
|
97
97
|
function buildProfileStatItems(t, ctx) {
|
|
98
98
|
const { listingCount, reviewCount, itemsSold, auctionsWon, totalOrders, isSeller } = ctx;
|
|
@@ -136,3 +136,6 @@ function renderProfileReviewsSection(t, reviews, storeSlug) {
|
|
|
136
136
|
function renderAuthoredReviewsSection(t, reviews, displayName) {
|
|
137
137
|
return (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-4", children: t("reviewsAuthoredTitle", { name: displayName }) }), reviews.length === 0 ? (_jsxs(Div, { rounded: "2xl", padding: "3xl", className: "text-center", border: "default", surface: "subtle", children: [_jsx(Star, { className: CLS_EMPTY_ICON }), _jsx(Text, { variant: "secondary", size: "sm", children: t("noReviewsAuthored", { name: displayName }) })] })) : (_jsx(Grid, { gap: "md", className: "grid-cols-1 sm:grid-cols-2", children: reviews.slice(0, 6).map((review) => _jsx(ReviewCard, { review: review }, review.id)) }))] }));
|
|
138
138
|
}
|
|
139
|
+
function renderBuyerReceivedReviewsSection(t, reviews) {
|
|
140
|
+
return (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-4", children: t("reviewsReceivedAsBuyerTitle") || "Reviews Received" }), _jsx(Grid, { gap: "md", className: "grid-cols-1 sm:grid-cols-2", children: reviews.slice(0, 6).map((review) => _jsx(ReviewCard, { review: review }, review.id)) })] }));
|
|
141
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseRepository, type DocumentSnapshot } from "../../../providers/db-firebase";
|
|
2
|
-
import { type AddressCreateInput, type AddressDocument, type AddressOwnerType, type AddressUpdateInput } from "../schemas";
|
|
2
|
+
import { type AddressBanStatus, type AddressCreateInput, type AddressDocument, type AddressOwnerType, type AddressUpdateInput } from "../schemas";
|
|
3
3
|
export declare class AddressesRepository extends BaseRepository<AddressDocument> {
|
|
4
4
|
constructor();
|
|
5
5
|
private decryptAddress;
|
|
@@ -15,6 +15,19 @@ export declare class AddressesRepository extends BaseRepository<AddressDocument>
|
|
|
15
15
|
deleteForOwner(ownerType: AddressOwnerType, ownerId: string, addressId: string): Promise<void>;
|
|
16
16
|
setDefault(ownerType: AddressOwnerType, ownerId: string, addressId: string): Promise<AddressDocument>;
|
|
17
17
|
deleteAllForOwner(ownerType: AddressOwnerType, ownerId: string): Promise<number>;
|
|
18
|
+
private computeAddressHash;
|
|
19
|
+
banAllForOwner(ownerType: AddressOwnerType, ownerId: string, banData: {
|
|
20
|
+
banReason: string;
|
|
21
|
+
bannedBy: string;
|
|
22
|
+
}): Promise<number>;
|
|
23
|
+
unbanAutoForOwner(ownerType: AddressOwnerType, ownerId: string): Promise<number>;
|
|
24
|
+
listByAddressHash(addressHash: string): Promise<AddressDocument[]>;
|
|
25
|
+
listByBanStatus(banStatus: AddressBanStatus, limit?: number, offset?: number): Promise<AddressDocument[]>;
|
|
26
|
+
banById(id: string, banData: {
|
|
27
|
+
banReason: string;
|
|
28
|
+
bannedBy: string;
|
|
29
|
+
}): Promise<AddressDocument>;
|
|
30
|
+
clearBanById(id: string): Promise<void>;
|
|
18
31
|
private clearDefaultFlag;
|
|
19
32
|
}
|
|
20
33
|
export declare const addressesRepository: AddressesRepository;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
1
2
|
import { normalizeError } from "../../../errors/normalize";
|
|
2
3
|
/**
|
|
3
4
|
* AddressesRepository — SB-UNI-A 2026-05-13
|
|
@@ -86,6 +87,7 @@ export class AddressesRepository extends BaseRepository {
|
|
|
86
87
|
...input,
|
|
87
88
|
ownerType,
|
|
88
89
|
ownerId,
|
|
90
|
+
addressHash: this.computeAddressHash(input.postalCode, input.addressLine1, input.addressLine2),
|
|
89
91
|
createdAt: now,
|
|
90
92
|
updatedAt: now,
|
|
91
93
|
};
|
|
@@ -121,7 +123,14 @@ export class AddressesRepository extends BaseRepository {
|
|
|
121
123
|
if (input.isDefault) {
|
|
122
124
|
await this.clearDefaultFlag(ownerType, ownerId);
|
|
123
125
|
}
|
|
124
|
-
|
|
126
|
+
const updateData = { ...input };
|
|
127
|
+
if (input.postalCode || input.addressLine1 || input.addressLine2 !== undefined) {
|
|
128
|
+
const postalCode = input.postalCode ?? existing.postalCode;
|
|
129
|
+
const line1 = input.addressLine1 ?? existing.addressLine1;
|
|
130
|
+
const line2 = input.addressLine2 ?? existing.addressLine2;
|
|
131
|
+
updateData.addressHash = this.computeAddressHash(postalCode, line1, line2);
|
|
132
|
+
}
|
|
133
|
+
return this.update(addressId, updateData);
|
|
125
134
|
}
|
|
126
135
|
async deleteForOwner(ownerType, ownerId, addressId) {
|
|
127
136
|
const existing = await this.findById(addressId);
|
|
@@ -157,6 +166,118 @@ export class AddressesRepository extends BaseRepository {
|
|
|
157
166
|
throw new DatabaseError(`Failed to delete all addresses for ${ownerType}:${ownerId}`, error);
|
|
158
167
|
}
|
|
159
168
|
}
|
|
169
|
+
computeAddressHash(postalCode, line1, line2) {
|
|
170
|
+
const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
171
|
+
return createHash("sha256")
|
|
172
|
+
.update([postalCode, line1, line2 ?? ""].map(norm).join("|"))
|
|
173
|
+
.digest("hex");
|
|
174
|
+
}
|
|
175
|
+
async banAllForOwner(ownerType, ownerId, banData) {
|
|
176
|
+
try {
|
|
177
|
+
const snap = await this.getCollection()
|
|
178
|
+
.where(ADDRESS_FIELDS.OWNER_TYPE, "==", ownerType)
|
|
179
|
+
.where(ADDRESS_FIELDS.OWNER_ID, "==", ownerId)
|
|
180
|
+
.get();
|
|
181
|
+
if (snap.empty)
|
|
182
|
+
return 0;
|
|
183
|
+
const now = new Date();
|
|
184
|
+
const batch = this.db.batch();
|
|
185
|
+
snap.docs.forEach((doc) => {
|
|
186
|
+
batch.update(doc.ref, {
|
|
187
|
+
[ADDRESS_FIELDS.BAN_STATUS]: "banned",
|
|
188
|
+
[ADDRESS_FIELDS.BAN_REASON]: banData.banReason,
|
|
189
|
+
[ADDRESS_FIELDS.BANNED_BY]: banData.bannedBy,
|
|
190
|
+
[ADDRESS_FIELDS.BANNED_AT]: now,
|
|
191
|
+
[ADDRESS_FIELDS.AUTO_BANNED]: true,
|
|
192
|
+
[ADDRESS_FIELDS.UPDATED_AT]: now,
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
await batch.commit();
|
|
196
|
+
return snap.size;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
void normalizeError(error);
|
|
200
|
+
throw new DatabaseError(`Failed to ban addresses for ${ownerType}:${ownerId}`, error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async unbanAutoForOwner(ownerType, ownerId) {
|
|
204
|
+
try {
|
|
205
|
+
const snap = await this.getCollection()
|
|
206
|
+
.where(ADDRESS_FIELDS.OWNER_TYPE, "==", ownerType)
|
|
207
|
+
.where(ADDRESS_FIELDS.OWNER_ID, "==", ownerId)
|
|
208
|
+
.where(ADDRESS_FIELDS.AUTO_BANNED, "==", true)
|
|
209
|
+
.get();
|
|
210
|
+
if (snap.empty)
|
|
211
|
+
return 0;
|
|
212
|
+
const now = new Date();
|
|
213
|
+
const batch = this.db.batch();
|
|
214
|
+
snap.docs.forEach((doc) => {
|
|
215
|
+
batch.update(doc.ref, {
|
|
216
|
+
[ADDRESS_FIELDS.BAN_STATUS]: null,
|
|
217
|
+
[ADDRESS_FIELDS.BAN_REASON]: null,
|
|
218
|
+
[ADDRESS_FIELDS.BANNED_BY]: null,
|
|
219
|
+
[ADDRESS_FIELDS.BANNED_AT]: null,
|
|
220
|
+
[ADDRESS_FIELDS.AUTO_BANNED]: null,
|
|
221
|
+
[ADDRESS_FIELDS.UPDATED_AT]: now,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
await batch.commit();
|
|
225
|
+
return snap.size;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
void normalizeError(error);
|
|
229
|
+
throw new DatabaseError(`Failed to unban addresses for ${ownerType}:${ownerId}`, error);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async listByAddressHash(addressHash) {
|
|
233
|
+
try {
|
|
234
|
+
const snap = await this.getCollection()
|
|
235
|
+
.where(ADDRESS_FIELDS.ADDRESS_HASH, "==", addressHash)
|
|
236
|
+
.get();
|
|
237
|
+
return snap.docs.map((d) => this.mapDoc(d));
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
void normalizeError(error);
|
|
241
|
+
throw new DatabaseError(`Failed to lookup addresses by hash`, error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async listByBanStatus(banStatus, limit = 50, offset = 0) {
|
|
245
|
+
try {
|
|
246
|
+
const snap = await this.getCollection()
|
|
247
|
+
.where(ADDRESS_FIELDS.BAN_STATUS, "==", banStatus)
|
|
248
|
+
.orderBy(ADDRESS_FIELDS.BANNED_AT, "desc")
|
|
249
|
+
.limit(limit)
|
|
250
|
+
.offset(offset)
|
|
251
|
+
.get();
|
|
252
|
+
return snap.docs.map((d) => this.mapDoc(d));
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
void normalizeError(error);
|
|
256
|
+
throw new DatabaseError(`Failed to list addresses by banStatus:${banStatus}`, error);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async banById(id, banData) {
|
|
260
|
+
return this.update(id, {
|
|
261
|
+
banStatus: "banned",
|
|
262
|
+
banReason: banData.banReason,
|
|
263
|
+
bannedBy: banData.bannedBy,
|
|
264
|
+
bannedAt: new Date(),
|
|
265
|
+
autoBanned: false,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
async clearBanById(id) {
|
|
269
|
+
const now = new Date();
|
|
270
|
+
await this.db.collection(ADDRESSES_COLLECTION).doc(id).update({
|
|
271
|
+
[ADDRESS_FIELDS.BAN_STATUS]: null,
|
|
272
|
+
[ADDRESS_FIELDS.BAN_REASON]: null,
|
|
273
|
+
[ADDRESS_FIELDS.BANNED_BY]: null,
|
|
274
|
+
[ADDRESS_FIELDS.BANNED_AT]: null,
|
|
275
|
+
[ADDRESS_FIELDS.AUTO_BANNED]: null,
|
|
276
|
+
[ADDRESS_FIELDS.UNBAN_REQUEST_NOTE]: null,
|
|
277
|
+
[ADDRESS_FIELDS.UNBAN_REQUESTED_AT]: null,
|
|
278
|
+
[ADDRESS_FIELDS.UPDATED_AT]: now,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
160
281
|
async clearDefaultFlag(ownerType, ownerId) {
|
|
161
282
|
try {
|
|
162
283
|
const snapshot = await this.getCollection()
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
11
11
|
export type AddressOwnerType = "user" | "store";
|
|
12
|
+
export type AddressBanStatus = "banned" | "unban_requested" | "suspicious";
|
|
12
13
|
export interface AddressDocument extends BaseDocument {
|
|
13
14
|
ownerType: AddressOwnerType;
|
|
14
15
|
ownerId: string;
|
|
@@ -23,12 +24,22 @@ export interface AddressDocument extends BaseDocument {
|
|
|
23
24
|
postalCode: string;
|
|
24
25
|
country: string;
|
|
25
26
|
isDefault: boolean;
|
|
27
|
+
/** SHA-256(normalised postalCode|line1|line2) — stored unencrypted for cross-account dedup. */
|
|
28
|
+
addressHash?: string;
|
|
29
|
+
banStatus?: AddressBanStatus;
|
|
30
|
+
banReason?: string;
|
|
31
|
+
bannedBy?: string;
|
|
32
|
+
bannedAt?: Date;
|
|
33
|
+
/** true when cascaded from a user hard-ban (vs manually banned by admin). */
|
|
34
|
+
autoBanned?: boolean;
|
|
35
|
+
unbanRequestNote?: string;
|
|
36
|
+
unbanRequestedAt?: Date;
|
|
26
37
|
}
|
|
27
38
|
export declare const ADDRESSES_COLLECTION: "addresses";
|
|
28
|
-
export declare const ADDRESS_INDEXED_FIELDS: readonly ["ownerType", "ownerId", "isDefault", "createdAt"];
|
|
39
|
+
export declare const ADDRESS_INDEXED_FIELDS: readonly ["ownerType", "ownerId", "isDefault", "addressHash", "banStatus", "autoBanned", "createdAt"];
|
|
29
40
|
export declare const DEFAULT_ADDRESS_DATA: Partial<AddressDocument>;
|
|
30
|
-
export declare const ADDRESS_PUBLIC_FIELDS: readonly ["id", "ownerType", "ownerId", "label", "fullName", "phone", "addressLine1", "addressLine2", "landmark", "city", "state", "postalCode", "country", "isDefault", "createdAt", "updatedAt"];
|
|
31
|
-
export declare const ADDRESS_UPDATABLE_FIELDS: readonly ["label", "fullName", "phone", "addressLine1", "addressLine2", "landmark", "city", "state", "postalCode", "country", "isDefault"];
|
|
41
|
+
export declare const ADDRESS_PUBLIC_FIELDS: readonly ["id", "ownerType", "ownerId", "label", "fullName", "phone", "addressLine1", "addressLine2", "landmark", "city", "state", "postalCode", "country", "isDefault", "addressHash", "banStatus", "banReason", "bannedAt", "autoBanned", "unbanRequestNote", "unbanRequestedAt", "createdAt", "updatedAt"];
|
|
42
|
+
export declare const ADDRESS_UPDATABLE_FIELDS: readonly ["label", "fullName", "phone", "addressLine1", "addressLine2", "landmark", "city", "state", "postalCode", "country", "isDefault", "banStatus", "banReason", "bannedBy", "bannedAt", "autoBanned", "unbanRequestNote", "unbanRequestedAt"];
|
|
32
43
|
export type AddressCreateInput = Omit<AddressDocument, "id" | "ownerType" | "ownerId" | "createdAt" | "updatedAt">;
|
|
33
44
|
export type AddressUpdateInput = Partial<Pick<AddressDocument, (typeof ADDRESS_UPDATABLE_FIELDS)[number]>>;
|
|
34
45
|
export declare const ADDRESS_FIELDS: {
|
|
@@ -46,6 +57,14 @@ export declare const ADDRESS_FIELDS: {
|
|
|
46
57
|
readonly POSTAL_CODE: "postalCode";
|
|
47
58
|
readonly COUNTRY: "country";
|
|
48
59
|
readonly IS_DEFAULT: "isDefault";
|
|
60
|
+
readonly ADDRESS_HASH: "addressHash";
|
|
61
|
+
readonly BAN_STATUS: "banStatus";
|
|
62
|
+
readonly BAN_REASON: "banReason";
|
|
63
|
+
readonly BANNED_BY: "bannedBy";
|
|
64
|
+
readonly BANNED_AT: "bannedAt";
|
|
65
|
+
readonly AUTO_BANNED: "autoBanned";
|
|
66
|
+
readonly UNBAN_REQUEST_NOTE: "unbanRequestNote";
|
|
67
|
+
readonly UNBAN_REQUESTED_AT: "unbanRequestedAt";
|
|
49
68
|
readonly CREATED_AT: "createdAt";
|
|
50
69
|
readonly UPDATED_AT: "updatedAt";
|
|
51
70
|
};
|
|
@@ -12,6 +12,9 @@ export const ADDRESS_INDEXED_FIELDS = [
|
|
|
12
12
|
"ownerType",
|
|
13
13
|
"ownerId",
|
|
14
14
|
"isDefault",
|
|
15
|
+
"addressHash",
|
|
16
|
+
"banStatus",
|
|
17
|
+
"autoBanned",
|
|
15
18
|
"createdAt",
|
|
16
19
|
];
|
|
17
20
|
export const DEFAULT_ADDRESS_DATA = {
|
|
@@ -32,6 +35,13 @@ export const ADDRESS_PUBLIC_FIELDS = [
|
|
|
32
35
|
"postalCode",
|
|
33
36
|
"country",
|
|
34
37
|
"isDefault",
|
|
38
|
+
"addressHash",
|
|
39
|
+
"banStatus",
|
|
40
|
+
"banReason",
|
|
41
|
+
"bannedAt",
|
|
42
|
+
"autoBanned",
|
|
43
|
+
"unbanRequestNote",
|
|
44
|
+
"unbanRequestedAt",
|
|
35
45
|
"createdAt",
|
|
36
46
|
"updatedAt",
|
|
37
47
|
];
|
|
@@ -47,6 +57,13 @@ export const ADDRESS_UPDATABLE_FIELDS = [
|
|
|
47
57
|
"postalCode",
|
|
48
58
|
"country",
|
|
49
59
|
"isDefault",
|
|
60
|
+
"banStatus",
|
|
61
|
+
"banReason",
|
|
62
|
+
"bannedBy",
|
|
63
|
+
"bannedAt",
|
|
64
|
+
"autoBanned",
|
|
65
|
+
"unbanRequestNote",
|
|
66
|
+
"unbanRequestedAt",
|
|
50
67
|
];
|
|
51
68
|
export const ADDRESS_FIELDS = {
|
|
52
69
|
ID: "id",
|
|
@@ -63,6 +80,14 @@ export const ADDRESS_FIELDS = {
|
|
|
63
80
|
POSTAL_CODE: "postalCode",
|
|
64
81
|
COUNTRY: "country",
|
|
65
82
|
IS_DEFAULT: "isDefault",
|
|
83
|
+
ADDRESS_HASH: "addressHash",
|
|
84
|
+
BAN_STATUS: "banStatus",
|
|
85
|
+
BAN_REASON: "banReason",
|
|
86
|
+
BANNED_BY: "bannedBy",
|
|
87
|
+
BANNED_AT: "bannedAt",
|
|
88
|
+
AUTO_BANNED: "autoBanned",
|
|
89
|
+
UNBAN_REQUEST_NOTE: "unbanRequestNote",
|
|
90
|
+
UNBAN_REQUESTED_AT: "unbanRequestedAt",
|
|
66
91
|
CREATED_AT: "createdAt",
|
|
67
92
|
UPDATED_AT: "updatedAt",
|
|
68
93
|
};
|
|
@@ -41,13 +41,13 @@ export declare const addressFirestoreSchema: z.ZodObject<{
|
|
|
41
41
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
42
42
|
}, z.ZodTypeAny, "passthrough">;
|
|
43
43
|
label: string;
|
|
44
|
+
isDefault: boolean;
|
|
44
45
|
state: string;
|
|
45
46
|
city: string;
|
|
46
47
|
addressLine1: string;
|
|
47
48
|
fullName: string;
|
|
48
49
|
ownerType: "user" | "store";
|
|
49
50
|
ownerId: string;
|
|
50
|
-
isDefault: boolean;
|
|
51
51
|
postalCode: string;
|
|
52
52
|
addressLine2?: string | undefined;
|
|
53
53
|
landmark?: string | undefined;
|
|
@@ -62,13 +62,13 @@ export declare const addressFirestoreSchema: z.ZodObject<{
|
|
|
62
62
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
63
63
|
}, z.ZodTypeAny, "passthrough">;
|
|
64
64
|
label: string;
|
|
65
|
+
isDefault: boolean;
|
|
65
66
|
state: string;
|
|
66
67
|
city: string;
|
|
67
68
|
addressLine1: string;
|
|
68
69
|
fullName: string;
|
|
69
70
|
ownerType: "user" | "store";
|
|
70
71
|
ownerId: string;
|
|
71
|
-
isDefault: boolean;
|
|
72
72
|
postalCode: string;
|
|
73
73
|
addressLine2?: string | undefined;
|
|
74
74
|
landmark?: string | undefined;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { Badge, Button, Div, Heading, Row, Span, Stack, Text, useToast } from "../../../ui";
|
|
6
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
7
|
+
import { apiClient } from "../../../http";
|
|
8
|
+
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
9
|
+
const BAN_BADGE = {
|
|
10
|
+
banned: "danger",
|
|
11
|
+
unban_requested: "warning",
|
|
12
|
+
suspicious: "secondary",
|
|
13
|
+
};
|
|
14
|
+
export function AdminAddressClustersView(_props) {
|
|
15
|
+
const queryClient = useQueryClient();
|
|
16
|
+
const { showToast } = useToast();
|
|
17
|
+
const { data, isLoading, error } = useQuery({
|
|
18
|
+
queryKey: ["admin", "address-clusters"],
|
|
19
|
+
queryFn: async () => {
|
|
20
|
+
const res = await apiClient.get(ADMIN_ENDPOINTS.ADDRESS_CLUSTERS);
|
|
21
|
+
return res;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const flagMutation = useApiMutation({
|
|
25
|
+
mutationFn: async (id) => {
|
|
26
|
+
await apiClient.patch(ADMIN_ENDPOINTS.ADDRESS_BY_ID(id), { action: "flag_suspicious" });
|
|
27
|
+
},
|
|
28
|
+
onSuccess: () => {
|
|
29
|
+
showToast("Address flagged as suspicious.", "success");
|
|
30
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "address-clusters"] });
|
|
31
|
+
},
|
|
32
|
+
onError: (err) => {
|
|
33
|
+
showToast(err.message ?? "Failed to flag address.", "error");
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const clusters = data?.clusters ?? [];
|
|
37
|
+
return (_jsxs(Stack, { gap: "lg", padding: "page", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", color: "primary", children: "Address Clusters" }), _jsx(Text, { size: "sm", color: "muted", className: "mt-1", children: "Users sharing the same physical address. Flagging is informational only \u2014 users are not blocked." })] }), isLoading && (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), error && (_jsx(Div, { surface: "danger-surface", color: "error", padding: "inline", rounded: "xl", textSize: "sm", children: "Failed to load clusters." })), !isLoading && clusters.length === 0 && (_jsx(Div, { surface: "muted", padding: "y-4xl", rounded: "xl", className: "text-center", children: _jsx(Text, { color: "muted", size: "sm", children: "No shared addresses found." }) })), clusters.map((cluster) => (_jsxs(Stack, { surface: "card", padding: "md", gap: "md", rounded: "xl", border: "default", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", children: "Hash" }), _jsx(Span, { size: "xs", color: "muted", className: "font-mono truncate max-w-xs", children: cluster.addressHash })] }), _jsxs(Badge, { variant: "secondary", size: "sm", children: [cluster.addresses.length, " accounts"] })] }), _jsx(Stack, { gap: "xs", children: cluster.addresses.map((addr) => (_jsxs(Row, { justify: "between", align: "center", gap: "sm", surface: "muted", padding: "inlineSm", rounded: "lg", children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", color: "primary", className: "truncate", children: [addr.ownerType === "user" ? "User" : "Store", ": ", addr.ownerId] }), addr.city && (_jsx(Text, { size: "xs", color: "muted", children: [addr.city, addr.state, addr.postalCode].filter(Boolean).join(", ") }))] }), _jsxs(Row, { gap: "xs", align: "center", children: [addr.banStatus && (_jsx(Badge, { variant: BAN_BADGE[addr.banStatus] ?? "secondary", size: "sm", children: addr.banStatus.replace(/_/g, " ") })), addr.banStatus !== "suspicious" && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => flagMutation.mutate(addr.id), disabled: flagMutation.isPending, children: "Flag Suspicious" }))] })] }, addr.id))) })] }, cluster.addressHash)))] }));
|
|
38
|
+
}
|