@mohasinac/appkit 4.41.0 → 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/shell/SectionForm.js +44 -5
- package/package.json +2 -2
- 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,109 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
4
|
-
import React from "react";
|
|
5
|
-
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
import { Alert, Button, Div, Form, FormActions, Input, Row, Stack, StackedViewShell, Text, Toggle, useToast } from "../../../ui";
|
|
7
|
-
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
|
-
import { apiClient } from "../../../http";
|
|
9
|
-
import { featureFlagsUpdateSchema } from "../schemas/admin-ops-forms";
|
|
10
|
-
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
11
|
-
const __O = {
|
|
12
|
-
hidden: "overflow-hidden",
|
|
13
|
-
};
|
|
14
|
-
// Platform feature keys (flat booleans with rollout % support)
|
|
15
|
-
const PLATFORM_FLAGS = [
|
|
16
|
-
{ key: "blog", label: "Blog Posts", desc: "Publish articles for admins / sellers" },
|
|
17
|
-
{ key: "events", label: "Events", desc: "Raffles, spin wheels, polls" },
|
|
18
|
-
{ key: "reviews", label: "Reviews", desc: "Product review system" },
|
|
19
|
-
{ key: "wishlists", label: "Wishlists", desc: "Per-user saved lists" },
|
|
20
|
-
{ key: "coupons", label: "Coupons", desc: "Discount code system" },
|
|
21
|
-
{ key: "offers", label: "Buyer Offers", desc: "Make-an-offer price negotiation" },
|
|
22
|
-
{ key: "notifications", label: "Notifications", desc: "In-app notification feed" },
|
|
23
|
-
{ key: "chats", label: "Live Chat", desc: "Buyer-seller messaging" },
|
|
24
|
-
{ key: "sellerRegistration", label: "Seller Reg.", desc: "New vendor registration" },
|
|
25
|
-
{ key: "smsVerification", label: "SMS Verify", desc: "SMS OTP verification" },
|
|
26
|
-
{ key: "translations", label: "Translations", desc: "Multi-language support" },
|
|
27
|
-
{ key: "seedPanel", label: "Seed Panel", desc: "Dev seed data panel" },
|
|
28
|
-
];
|
|
29
|
-
// Listing type keys (nested under featureFlags.listingTypes — no rollout %)
|
|
30
|
-
const LISTING_TYPE_FLAGS = [
|
|
31
|
-
{ key: "standard", label: "Standard", desc: "Fixed-price products (Phase 1)" },
|
|
32
|
-
{ key: "auction", label: "Auction", desc: "eBay-style bidding (Phase 1)" },
|
|
33
|
-
{ key: "pre-order", label: "Pre-order", desc: "Future delivery + deposit (Phase 1)" },
|
|
34
|
-
{ key: "prize-draw", label: "Prize Draw", desc: "Raffle reveal window (Phase 1)" },
|
|
35
|
-
{ key: "classified", label: "Classified", desc: "Chat-only / no cart (Phase 2)" },
|
|
36
|
-
{ key: "digital-code", label: "Digital Code", desc: "Steam-key instant delivery (Phase 2)" },
|
|
37
|
-
{ key: "live", label: "Live Items", desc: "Animals / plants + verification (Phase 2)" },
|
|
38
|
-
];
|
|
39
|
-
// Category type keys (nested under featureFlags.categoryTypes — no rollout %)
|
|
40
|
-
const CATEGORY_TYPE_FLAGS = [
|
|
41
|
-
{ key: "category", label: "Categories", desc: "Standard hierarchy tier 1–3" },
|
|
42
|
-
{ key: "sublisting", label: "Sublistings", desc: "Grading / card-number sub-tiers" },
|
|
43
|
-
{ key: "brand", label: "Brands", desc: "Brand storefront pages" },
|
|
44
|
-
{ key: "bundle", label: "Bundles", desc: "Curated product collections" },
|
|
45
|
-
];
|
|
46
|
-
function AccordionSection({ title, defaultOpen = true, children }) {
|
|
47
|
-
const [open, setOpen] = React.useState(defaultOpen);
|
|
48
|
-
return (_jsxs(Div, { className: `${__O.hidden}`, rounded: "lg", border: "default", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => setOpen((v) => !v), paddingX: "md", paddingY: "md", className: "w-full text-left bg-[var(--appkit-color-surface-elevated)]/60", "aria-expanded": open, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: title }), _jsx("svg", { className: `h-4 w-4 text-zinc-400 transition-transform ${open ? "rotate-180" : ""} shrink-0`, fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }) }), open && (_jsx(Div, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", children: children }))] }));
|
|
49
|
-
}
|
|
50
|
-
function FlagRow({ label, desc, checked, onChange, rollout, onRolloutChange, showRollout }) {
|
|
51
|
-
return (_jsxs(Row, { surface: "default", padding: "inline", align: "center", justify: "between", gap: "md", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Toggle, { checked: checked, onChange: onChange, label: label }), _jsx(Text, { className: "mt-0.5 ml-10 truncate", color: "faint", size: "xs", children: desc })] }), showRollout && (_jsxs(Stack, { className: "w-28 shrink-0", gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", children: "Rollout %" }), _jsx(Input, { type: "number", min: 0, max: 100, value: rollout ?? 100, onChange: (e) => onRolloutChange?.(Math.min(100, Math.max(0, Number(e.target.value)))), disabled: !checked, className: "w-full" })] }))] }));
|
|
52
|
-
}
|
|
53
|
-
export function AdminFeatureFlagsView({ labels = {}, renderFlags, ...rest }) {
|
|
54
|
-
const queryClient = useQueryClient();
|
|
55
|
-
const { showToast } = useToast();
|
|
56
|
-
const { data, isLoading, error } = useQuery({
|
|
57
|
-
queryKey: ["admin", "feature-flags"],
|
|
58
|
-
queryFn: async () => {
|
|
59
|
-
const res = await apiClient.get(ADMIN_ENDPOINTS.FEATURE_FLAGS);
|
|
60
|
-
return res.data ?? { flags: {}, rollouts: {} };
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
const [flags, setFlags] = React.useState({});
|
|
64
|
-
const [rollouts, setRollouts] = React.useState({});
|
|
65
|
-
React.useEffect(() => {
|
|
66
|
-
if (!data)
|
|
67
|
-
return;
|
|
68
|
-
setFlags(data.flags ?? {});
|
|
69
|
-
setRollouts(data.rollouts ?? {});
|
|
70
|
-
}, [data]);
|
|
71
|
-
const getPlatformFlag = (key) => Boolean(flags[key]);
|
|
72
|
-
const setPlatformFlag = (key, val) => setFlags((prev) => ({ ...prev, [key]: val }));
|
|
73
|
-
const getListingTypeFlag = (key) => {
|
|
74
|
-
const lt = flags.listingTypes;
|
|
75
|
-
return lt?.[key] ?? false;
|
|
76
|
-
};
|
|
77
|
-
const setListingTypeFlag = (key, val) => setFlags((prev) => ({
|
|
78
|
-
...prev,
|
|
79
|
-
listingTypes: { ...(prev.listingTypes ?? {}), [key]: val },
|
|
80
|
-
}));
|
|
81
|
-
const getCategoryTypeFlag = (key) => {
|
|
82
|
-
const ct = flags.categoryTypes;
|
|
83
|
-
return ct?.[key] ?? false;
|
|
84
|
-
};
|
|
85
|
-
const setCategoryTypeFlag = (key, val) => setFlags((prev) => ({
|
|
86
|
-
...prev,
|
|
87
|
-
categoryTypes: { ...(prev.categoryTypes ?? {}), [key]: val },
|
|
88
|
-
}));
|
|
89
|
-
const saveFlags = useApiMutation({
|
|
90
|
-
errorMessage: "Failed to save feature flags.",
|
|
91
|
-
mutationFn: async () => {
|
|
92
|
-
await apiClient.put(ADMIN_ENDPOINTS.FEATURE_FLAGS, { flags, rollouts });
|
|
93
|
-
},
|
|
94
|
-
onSuccess: async () => {
|
|
95
|
-
await queryClient.invalidateQueries({ queryKey: ["admin", "feature-flags"] });
|
|
96
|
-
showToast("Feature flags saved.", "success");
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
const defaultFlags = () => (_jsxs(Form, { schema: featureFlagsUpdateSchema, onSubmit: (event) => {
|
|
100
|
-
event.preventDefault();
|
|
101
|
-
saveFlags.mutate();
|
|
102
|
-
}, spacing: "md", children: [_jsx(FormErrorSummary, {}), _jsx(AccordionSection, { title: "Platform Features", children: PLATFORM_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getPlatformFlag(key), onChange: (val) => setPlatformFlag(key, val), rollout: rollouts[key], onRolloutChange: (val) => setRollouts((prev) => ({ ...prev, [key]: val })), showRollout: true }, key))) }), _jsx(AccordionSection, { title: "Listing Types", children: LISTING_TYPE_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getListingTypeFlag(key), onChange: (val) => setListingTypeFlag(key, val), showRollout: false }, key))) }), _jsx(AccordionSection, { title: "Category Types", children: CATEGORY_TYPE_FLAGS.map(({ key, label, desc }) => (_jsx(FlagRow, { label: label, desc: desc, checked: getCategoryTypeFlag(key), onChange: (val) => setCategoryTypeFlag(key, val), showRollout: false }, key))) }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: () => {
|
|
103
|
-
setFlags(data?.flags ?? {});
|
|
104
|
-
setRollouts(data?.rollouts ?? {});
|
|
105
|
-
}, disabled: saveFlags.isPending, children: "Reset" }), _jsx(Button, { type: "submit", isLoading: saveFlags.isPending, disabled: saveFlags.isPending, children: "Save All Flags" })] })] }));
|
|
106
|
-
const loadingAlert = isLoading ? (_jsx(Alert, { variant: "info", title: "Loading feature flags", children: "Fetching feature flags\u2026" })) : null;
|
|
107
|
-
const errorAlert = error ? (_jsx(Alert, { variant: "error", title: "Could not load feature flags", children: error instanceof Error ? error.message : "Unknown error" })) : null;
|
|
108
|
-
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: labels.title ?? "Feature Flags", sections: [loadingAlert, errorAlert, renderFlags?.() ?? defaultFlags()] }));
|
|
109
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import type { StackedViewShellProps } from "../../../ui";
|
|
3
|
-
export interface AdminSiteViewProps extends Omit<StackedViewShellProps, "sections"> {
|
|
4
|
-
labels?: {
|
|
5
|
-
title?: string;
|
|
6
|
-
};
|
|
7
|
-
renderTabs?: () => React.ReactNode;
|
|
8
|
-
renderForm?: () => React.ReactNode;
|
|
9
|
-
}
|
|
10
|
-
export declare function AdminSiteView({ labels, renderTabs, renderForm, ...rest }: AdminSiteViewProps): React.JSX.Element;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
4
|
-
import React from "react";
|
|
5
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
import { Alert, Button, Form, FormActions, Input, StackedViewShell, Toggle, } from "../../../ui";
|
|
7
|
-
import { useSiteSettings } from "../../../core/hooks/useSiteSettings";
|
|
8
|
-
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
|
-
import { apiClient } from "../../../http";
|
|
10
|
-
import { announcementBarSchema } from "../schemas/admin-editor-forms";
|
|
11
|
-
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
12
|
-
export function AdminSiteView({ labels = {}, renderTabs, renderForm, ...rest }) {
|
|
13
|
-
const queryClient = useQueryClient();
|
|
14
|
-
const { data, isLoading, error } = useSiteSettings();
|
|
15
|
-
const [announcementEnabled, setAnnouncementEnabled] = React.useState(true);
|
|
16
|
-
const [announcementMessage, setAnnouncementMessage] = React.useState("");
|
|
17
|
-
const [saveMessage, setSaveMessage] = React.useState(null);
|
|
18
|
-
React.useEffect(() => {
|
|
19
|
-
if (!data)
|
|
20
|
-
return;
|
|
21
|
-
setAnnouncementEnabled(data.announcementBar?.enabled ?? true);
|
|
22
|
-
setAnnouncementMessage(data.announcementBar?.message ?? "");
|
|
23
|
-
}, [data]);
|
|
24
|
-
const saveAnnouncement = useApiMutation({
|
|
25
|
-
mutationFn: async () => {
|
|
26
|
-
// Writes go to the admin endpoint. `PATCH /api/site-settings` was
|
|
27
|
-
// removed — that route is public-read-only now. The READ above still
|
|
28
|
-
// uses the public endpoint, which is fine: `announcementBar` is in its
|
|
29
|
-
// allow-list.
|
|
30
|
-
await apiClient.put(ADMIN_ENDPOINTS.ADMIN_SITE, {
|
|
31
|
-
announcementBar: {
|
|
32
|
-
enabled: announcementEnabled,
|
|
33
|
-
message: announcementMessage,
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
},
|
|
37
|
-
onSuccess: async () => {
|
|
38
|
-
await queryClient.invalidateQueries({ queryKey: ["site-settings"] });
|
|
39
|
-
setSaveMessage("Announcement settings saved.");
|
|
40
|
-
},
|
|
41
|
-
onError: () => {
|
|
42
|
-
setSaveMessage("Failed to save announcement settings.");
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
const defaultForm = () => (_jsxs(Form, { schema: announcementBarSchema, onSubmit: (event) => {
|
|
46
|
-
event.preventDefault();
|
|
47
|
-
saveAnnouncement.mutate();
|
|
48
|
-
}, spacing: "md", children: [_jsx(FormErrorSummary, {}), _jsx(Toggle, { checked: announcementEnabled, onChange: setAnnouncementEnabled, label: "Show announcement bar on homepage" }), _jsx(Input, { label: "Announcement message", value: announcementMessage, onChange: (event) => setAnnouncementMessage(event.target.value), placeholder: "Enter homepage announcement", disabled: !announcementEnabled, helperText: "This message is shown at the top of the homepage when enabled." }), _jsx(FormActions, { align: "right", children: _jsx(Button, { type: "submit", disabled: saveAnnouncement.isPending || (announcementEnabled && !announcementMessage.trim()), children: saveAnnouncement.isPending ? "Saving..." : "Save announcement" }) }), saveMessage ? (_jsx(Alert, { variant: saveMessage.startsWith("Failed") ? "error" : "success", title: saveMessage.startsWith("Failed") ? "Save failed" : "Saved", children: saveMessage })) : null] }));
|
|
49
|
-
const loadingAlert = isLoading ? (_jsx(Alert, { variant: "info", title: "Loading settings", children: "Fetching site settings..." })) : null;
|
|
50
|
-
const errorAlert = error ? (_jsx(Alert, { variant: "error", title: "Could not load settings", children: error instanceof Error ? error.message : "Unknown error" })) : null;
|
|
51
|
-
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: labels.title ?? "Site Settings", sections: [renderTabs?.(), loadingAlert, errorAlert, renderForm?.() ?? defaultForm()] }));
|
|
52
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { type IClientRealtimeProvider } from "../../../contracts/client-realtime";
|
|
2
|
-
export interface ChatMessage {
|
|
3
|
-
id: string;
|
|
4
|
-
userId: string;
|
|
5
|
-
userName: string;
|
|
6
|
-
message: string;
|
|
7
|
-
timestamp: number;
|
|
8
|
-
}
|
|
9
|
-
export interface UseChatReturn {
|
|
10
|
-
messages: ChatMessage[];
|
|
11
|
-
sendMessage: (text: string) => Promise<void>;
|
|
12
|
-
isConnected: boolean;
|
|
13
|
-
isLoading: boolean;
|
|
14
|
-
error: string | null;
|
|
15
|
-
}
|
|
16
|
-
export declare function useChat(chatId: string | null, options?: {
|
|
17
|
-
realtimeProvider?: IClientRealtimeProvider;
|
|
18
|
-
}): UseChatReturn;
|
|
19
|
-
export declare function useChatRooms(): import("@tanstack/react-query").UseQueryResult<unknown[], Error>;
|
|
20
|
-
export declare function useCreateChatRoom(): import("@tanstack/react-query").UseMutationResult<unknown, import("@mohasinac/appkit/client").ApiError, {
|
|
21
|
-
orderId: string;
|
|
22
|
-
ownerId: string;
|
|
23
|
-
}, unknown>;
|
|
24
|
-
export declare function useDeleteChatRoom(): import("@tanstack/react-query").UseMutationResult<import("@mohasinac/appkit/client").JsonValue, import("@mohasinac/appkit/client").ApiError, string, unknown>;
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { normalizeError } from "../../../errors/normalize";
|
|
3
|
-
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
4
|
-
import { useEffect, useState, useCallback, useRef } from "react";
|
|
5
|
-
import { useQuery } from "@tanstack/react-query";
|
|
6
|
-
import { getClientRealtimeProvider, } from "../../../contracts/client-realtime";
|
|
7
|
-
import { logger } from "../../../core/Logger";
|
|
8
|
-
import { apiClient } from "../../../http";
|
|
9
|
-
import { nowMs } from "../../../utils";
|
|
10
|
-
import { ADMIN_ENDPOINTS, CHAT_ENDPOINTS, } from "../../../constants/api-endpoints";
|
|
11
|
-
export function useChat(chatId, options) {
|
|
12
|
-
const [messages, setMessages] = useState([]);
|
|
13
|
-
const [isConnected, setIsConnected] = useState(false);
|
|
14
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
15
|
-
const [error, setError] = useState(null);
|
|
16
|
-
const tokenExpiresAtRef = useRef(0);
|
|
17
|
-
const unsubscribeRef = useRef(null);
|
|
18
|
-
const connectAndSubscribe = useCallback(async () => {
|
|
19
|
-
if (!chatId)
|
|
20
|
-
return;
|
|
21
|
-
setIsLoading(true);
|
|
22
|
-
setError(null);
|
|
23
|
-
const provider = options?.realtimeProvider ?? getClientRealtimeProvider();
|
|
24
|
-
try {
|
|
25
|
-
const shouldRefresh = !tokenExpiresAtRef.current ||
|
|
26
|
-
tokenExpiresAtRef.current - nowMs() < 60000;
|
|
27
|
-
if (shouldRefresh) {
|
|
28
|
-
const tokenResponse = await apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {});
|
|
29
|
-
await provider.signInWithToken(tokenResponse.customToken);
|
|
30
|
-
tokenExpiresAtRef.current = tokenResponse.expiresAt;
|
|
31
|
-
}
|
|
32
|
-
if (unsubscribeRef.current) {
|
|
33
|
-
unsubscribeRef.current();
|
|
34
|
-
unsubscribeRef.current = null;
|
|
35
|
-
}
|
|
36
|
-
unsubscribeRef.current = provider.subscribe(`/chat/${chatId}/messages`, (snapshot) => {
|
|
37
|
-
if (snapshot.exists()) {
|
|
38
|
-
const raw = snapshot.val();
|
|
39
|
-
const parsed = Object.entries(raw)
|
|
40
|
-
.map(([id, msg]) => ({ id, ...msg }))
|
|
41
|
-
.sort((a, b) => a.timestamp - b.timestamp);
|
|
42
|
-
setMessages(parsed);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
setMessages([]);
|
|
46
|
-
}
|
|
47
|
-
setIsConnected(true);
|
|
48
|
-
setIsLoading(false);
|
|
49
|
-
}, (err) => {
|
|
50
|
-
logger.error("Chat RTDB subscription error", err);
|
|
51
|
-
setError("Could not connect to chat. Please try again.");
|
|
52
|
-
setIsConnected(false);
|
|
53
|
-
setIsLoading(false);
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
void normalizeError(err);
|
|
58
|
-
logger.error("Failed to authenticate for chat RTDB", err);
|
|
59
|
-
setError("Failed to connect to chat.");
|
|
60
|
-
setIsLoading(false);
|
|
61
|
-
}
|
|
62
|
-
}, [chatId, options?.realtimeProvider]);
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
connectAndSubscribe();
|
|
65
|
-
return () => {
|
|
66
|
-
if (unsubscribeRef.current) {
|
|
67
|
-
unsubscribeRef.current();
|
|
68
|
-
unsubscribeRef.current = null;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}, [connectAndSubscribe]);
|
|
72
|
-
const sendMessage = useCallback(async (text) => {
|
|
73
|
-
if (!chatId || !text.trim())
|
|
74
|
-
return;
|
|
75
|
-
await apiClient.post(CHAT_ENDPOINTS.MESSAGES(chatId), {
|
|
76
|
-
message: text.trim(),
|
|
77
|
-
});
|
|
78
|
-
}, [chatId]);
|
|
79
|
-
return { messages, sendMessage, isConnected, isLoading, error };
|
|
80
|
-
}
|
|
81
|
-
export function useChatRooms() {
|
|
82
|
-
return useQuery({
|
|
83
|
-
queryKey: ["chat", "rooms"],
|
|
84
|
-
queryFn: () => apiClient
|
|
85
|
-
.get(CHAT_ENDPOINTS.LIST)
|
|
86
|
-
.then((r) => r.rooms),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
export function useCreateChatRoom() {
|
|
90
|
-
return useApiMutation({
|
|
91
|
-
mutationFn: (data) => apiClient
|
|
92
|
-
.post(CHAT_ENDPOINTS.LIST, data)
|
|
93
|
-
.then((r) => r.room),
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
export function useDeleteChatRoom() {
|
|
97
|
-
return useApiMutation({
|
|
98
|
-
mutationFn: (chatId) => apiClient.delete(CHAT_ENDPOINTS.BY_ID(chatId)),
|
|
99
|
-
});
|
|
100
|
-
}
|
|
@@ -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>;
|