@mohasinac/appkit 4.29.1 → 4.30.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/_internal/client/features/layout/SidebarCollapseToggle.js +1 -1
- package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
- package/dist/_internal/shared/features/blog/schema.js +2 -1
- package/dist/_internal/shared/features/brands/schema.d.ts +2 -2
- package/dist/_internal/shared/features/brands/schema.js +3 -1
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +3 -3
- package/dist/_internal/shared/features/categories/bundle-schemas.js +2 -1
- package/dist/_internal/shared/features/events/schema.d.ts +2 -2
- package/dist/_internal/shared/features/events/schema.js +2 -1
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/features/products/schema.js +3 -2
- package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
- package/dist/_internal/shared/features/reviews/schema.js +2 -1
- package/dist/_internal/shared/types/action-result.d.ts +0 -2
- package/dist/client/api/surface-error.d.ts +9 -0
- package/dist/client/api/surface-error.js +29 -3
- package/dist/client/api/useApiMutation.d.ts +10 -0
- package/dist/client/api/useApiMutation.js +2 -1
- package/dist/core/server-action.d.ts +3 -14
- package/dist/core/server-action.js +11 -2
- package/dist/errors/error-envelope.d.ts +73 -0
- package/dist/errors/error-envelope.js +96 -0
- package/dist/errors/error-handler.js +14 -7
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/components/UserSidebar.js +2 -2
- package/dist/features/account/components/UserSupportView.js +2 -6
- package/dist/features/account/hooks/useProfile.d.ts +6 -0
- package/dist/features/account/hooks/useProfile.js +1 -0
- package/dist/features/admin/components/AdminAdEditorView.js +1 -3
- package/dist/features/admin/components/AdminAddressClustersView.js +1 -3
- package/dist/features/admin/components/AdminAddressEditorView.js +2 -4
- package/dist/features/admin/components/AdminAddressesView.js +1 -3
- package/dist/features/admin/components/AdminAllEventEntriesView.js +1 -3
- package/dist/features/admin/components/AdminBidsView.js +1 -3
- package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
- package/dist/features/admin/components/AdminBrandEditorView.js +4 -5
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +1 -3
- package/dist/features/admin/components/AdminCategoryEditorView.js +2 -4
- package/dist/features/admin/components/AdminContactEditorView.js +1 -3
- package/dist/features/admin/components/AdminContactView.js +2 -6
- package/dist/features/admin/components/AdminCouponEditorView.js +2 -4
- package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -9
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -4
- package/dist/features/admin/components/AdminFeatureEditorView.js +0 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +2 -4
- package/dist/features/admin/components/AdminNavEditorView.js +1 -3
- package/dist/features/admin/components/AdminNavigationView.js +3 -3
- package/dist/features/admin/components/AdminNewsletterView.js +1 -3
- package/dist/features/admin/components/AdminNotificationsView.js +2 -6
- package/dist/features/admin/components/AdminOffersView.js +1 -3
- package/dist/features/admin/components/AdminOrderEditorView.js +1 -3
- package/dist/features/admin/components/AdminPaymentClustersView.js +1 -3
- package/dist/features/admin/components/AdminPaymentMethodsView.js +1 -3
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +2 -4
- package/dist/features/admin/components/AdminProductEditorView.js +2 -4
- package/dist/features/admin/components/AdminReturnRequestsView.js +2 -6
- package/dist/features/admin/components/AdminReviewsView.js +2 -6
- package/dist/features/admin/components/AdminScammerEditorView.js +2 -6
- package/dist/features/admin/components/AdminSectionsView.js +2 -6
- package/dist/features/admin/components/AdminSessionsView.js +1 -3
- package/dist/features/admin/components/AdminSidebar.js +2 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +1 -3
- package/dist/features/admin/components/AdminStoresView.js +2 -6
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -6
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +7 -21
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -4
- package/dist/features/admin/components/AdminUserEditorView.js +6 -18
- package/dist/features/admin/components/AdminUsersView.js +2 -6
- package/dist/features/admin/components/BrandQuickCreateForm.js +2 -4
- package/dist/features/admin/components/CategoryQuickCreateForm.js +2 -4
- package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
- package/dist/features/auth/components/ForgotPasswordView.js +1 -1
- package/dist/features/auth/components/LoginForm.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +1 -1
- package/dist/features/auth/components/ResetPasswordView.js +1 -1
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/repository/session.repository.js +13 -1
- package/dist/features/auth/repository/token.repository.d.ts +25 -2
- package/dist/features/auth/repository/token.repository.js +26 -18
- package/dist/features/blog/actions/blog-actions.d.ts +14 -14
- package/dist/features/blog/actions/blog-actions.js +6 -8
- package/dist/features/blog/schemas/index.d.ts +1 -1
- package/dist/features/blog/schemas/index.js +2 -4
- package/dist/features/categories/api/[id]/route.js +2 -1
- package/dist/features/categories/api/route.js +2 -1
- package/dist/features/events/schemas/index.d.ts +1 -1
- package/dist/features/events/schemas/index.js +2 -4
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/filters/AsyncFacetSection.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +1 -1
- package/dist/features/filters/RangeFilter.js +1 -1
- package/dist/features/filters/SwitchFilter.js +1 -1
- package/dist/features/homepage/api/carousel/[id]/route.js +4 -3
- package/dist/features/homepage/api/carousel/route.js +4 -3
- package/dist/features/homepage/api/route.js +3 -2
- package/dist/features/homepage/components/GoogleReviewsSection.js +1 -1
- package/dist/features/homepage/components/HeroBanner.js +1 -1
- package/dist/features/homepage/components/HeroCarousel.js +1 -1
- package/dist/features/homepage/components/SocialPostCard.js +1 -1
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +4 -4
- package/dist/features/layout/BottomNavbar.js +2 -2
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +24 -12
- package/dist/features/media/AvatarUpload.js +28 -10
- package/dist/features/media/modals/ImageCropModal.js +1 -1
- package/dist/features/media/modals/ImageEditor.js +1 -1
- package/dist/features/products/components/MakeOfferButton.js +1 -1
- package/dist/features/reviews/api/[id]/route.js +3 -2
- package/dist/features/reviews/api/route.js +3 -2
- package/dist/features/search/components/Search.js +2 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +3 -3
- package/dist/features/seller/components/SellerSidebar.js +2 -2
- package/dist/features/store-extensions/components/ReviewDecisionModal.js +1 -1
- package/dist/features/stores/repository/store.repository.d.ts +21 -0
- package/dist/features/stores/repository/store.repository.js +32 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -2
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -7
- package/dist/next/api/api-response.d.ts +29 -12
- package/dist/next/api/api-response.js +9 -8
- package/dist/next/api/apiHandler.d.ts +6 -1
- package/dist/next/api/apiHandler.js +13 -1
- package/dist/next/api/routeHandler.js +6 -6
- package/dist/next/middleware/pii-scrubber.js +5 -2
- package/dist/providers/db-firebase/base.repository.d.ts +14 -0
- package/dist/providers/db-firebase/base.repository.js +22 -0
- package/dist/providers/db-firebase/sieve.d.ts +14 -1
- package/dist/providers/db-firebase/sieve.js +48 -4
- package/dist/security/browser.d.ts +1 -1
- package/dist/security/browser.js +1 -1
- package/dist/security/index.d.ts +2 -2
- package/dist/security/index.js +2 -2
- package/dist/security/pii-encrypt.d.ts +12 -10
- package/dist/security/pii-encrypt.js +91 -29
- package/dist/security/pii-mask.d.ts +39 -1
- package/dist/security/pii-mask.js +33 -4
- package/dist/security/pii-redact.d.ts +0 -13
- package/dist/security/pii-redact.js +3 -1
- package/dist/security/pii-schemas.d.ts +0 -6
- package/dist/security/pii-schemas.js +29 -6
- package/dist/security/settings-encryption.js +31 -3
- package/dist/seed/test-utils.js +6 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/ui/DataTable.js +2 -2
- package/dist/ui/components/CollapsibleSection.js +1 -1
- package/dist/ui/components/ImageGallery.js +1 -1
- package/dist/ui/components/ListingLayout.js +1 -1
- package/dist/ui/forms/FormShell.d.ts +0 -19
- package/dist/ui/forms/FormShell.js +4 -23
- package/dist/ui/forms/apply-zod-issues.d.ts +34 -0
- package/dist/ui/forms/apply-zod-issues.js +38 -0
- package/dist/ui/forms/index.d.ts +2 -1
- package/dist/ui/forms/index.js +2 -1
- package/dist/ui/icons/icon-registry.d.ts +8 -0
- package/dist/ui/icons/icon-registry.js +13 -1
- package/dist/utils/action-response.d.ts +1 -1
- package/dist/utils/media-url.d.ts +49 -0
- package/dist/utils/media-url.js +72 -1
- package/dist/validation/schemas.d.ts +15 -2
- package/dist/validation/schemas.js +18 -22
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +21 -6
|
@@ -113,6 +113,7 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
|
|
|
113
113
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["admin", "users"] });
|
|
114
114
|
// --- Mutations ------------------------------------------------------------
|
|
115
115
|
const saveMutation = useApiMutation({
|
|
116
|
+
errorMessage: "Failed to update user.",
|
|
116
117
|
mutationFn: async () => {
|
|
117
118
|
// ST-2 — build publicProfile partial only when something changed; keeps
|
|
118
119
|
// the PATCH payload minimal and avoids overwriting unrelated subkeys.
|
|
@@ -150,11 +151,9 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
|
|
|
150
151
|
invalidate();
|
|
151
152
|
onClose();
|
|
152
153
|
},
|
|
153
|
-
onError: (err) => {
|
|
154
|
-
showToast(err?.message ?? "Failed to update user.", "error");
|
|
155
|
-
},
|
|
156
154
|
});
|
|
157
155
|
const deleteMutation = useApiMutation({
|
|
156
|
+
errorMessage: "Failed to delete user.",
|
|
158
157
|
mutationFn: async () => {
|
|
159
158
|
await apiClient.delete(ADMIN_ENDPOINTS.USER_BY_ID(userId));
|
|
160
159
|
},
|
|
@@ -164,11 +163,9 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
|
|
|
164
163
|
setDeleteOpen(false);
|
|
165
164
|
onClose();
|
|
166
165
|
},
|
|
167
|
-
onError: (err) => {
|
|
168
|
-
showToast(err?.message ?? "Failed to delete user.", "error");
|
|
169
|
-
},
|
|
170
166
|
});
|
|
171
167
|
const hardBanMutation = useApiMutation({
|
|
168
|
+
errorMessage: "Failed to ban user.",
|
|
172
169
|
mutationFn: async (reason) => {
|
|
173
170
|
await apiClient.post(ADMIN_ENDPOINTS.USER_HARD_BAN(userId), { reason });
|
|
174
171
|
},
|
|
@@ -179,11 +176,9 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
|
|
|
179
176
|
setHardBanReasonInput("");
|
|
180
177
|
onClose();
|
|
181
178
|
},
|
|
182
|
-
onError: (err) => {
|
|
183
|
-
showToast(err?.message ?? "Failed to ban user.", "error");
|
|
184
|
-
},
|
|
185
179
|
});
|
|
186
180
|
const unbanMutation = useApiMutation({
|
|
181
|
+
errorMessage: "Failed to unban user.",
|
|
187
182
|
mutationFn: async () => {
|
|
188
183
|
await apiClient.post(ADMIN_ENDPOINTS.USER_UNBAN(userId), {});
|
|
189
184
|
},
|
|
@@ -192,11 +187,9 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
|
|
|
192
187
|
invalidate();
|
|
193
188
|
onClose();
|
|
194
189
|
},
|
|
195
|
-
onError: (err) => {
|
|
196
|
-
showToast(err?.message ?? "Failed to unban user.", "error");
|
|
197
|
-
},
|
|
198
190
|
});
|
|
199
191
|
const softBanMutation = useApiMutation({
|
|
192
|
+
errorMessage: "Failed to apply soft ban.",
|
|
200
193
|
mutationFn: async (payload) => {
|
|
201
194
|
await apiClient.post(ADMIN_ENDPOINTS.USER_SOFT_BAN(userId), payload);
|
|
202
195
|
},
|
|
@@ -208,11 +201,9 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
|
|
|
208
201
|
setSoftBanReason("");
|
|
209
202
|
setSoftBanExpiry("");
|
|
210
203
|
},
|
|
211
|
-
onError: (err) => {
|
|
212
|
-
showToast(err?.message ?? "Failed to apply soft ban.", "error");
|
|
213
|
-
},
|
|
214
204
|
});
|
|
215
205
|
const liftSoftBanMutation = useApiMutation({
|
|
206
|
+
errorMessage: "Failed to lift soft ban.",
|
|
216
207
|
mutationFn: async (action) => {
|
|
217
208
|
await apiClient.delete(ADMIN_ENDPOINTS.USER_SOFT_BAN_LIFT(userId, action));
|
|
218
209
|
},
|
|
@@ -220,9 +211,6 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, photoU
|
|
|
220
211
|
showToast("Soft ban lifted.", "success");
|
|
221
212
|
invalidate();
|
|
222
213
|
},
|
|
223
|
-
onError: (err) => {
|
|
224
|
-
showToast(err?.message ?? "Failed to lift soft ban.", "error");
|
|
225
|
-
},
|
|
226
214
|
});
|
|
227
215
|
const isHardBanned = currentIsHardBanned ?? false;
|
|
228
216
|
const softBans = currentSoftBans ?? [];
|
|
@@ -52,6 +52,7 @@ export function AdminUsersView({ children, ...props }) {
|
|
|
52
52
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
53
|
}, [banJobEvent.status]);
|
|
54
54
|
const banUser = useApiMutation({
|
|
55
|
+
errorMessage: "Failed to start hard-ban.",
|
|
55
56
|
mutationFn: () => {
|
|
56
57
|
if (!banTargetId)
|
|
57
58
|
throw new Error("No user selected");
|
|
@@ -60,9 +61,6 @@ export function AdminUsersView({ children, ...props }) {
|
|
|
60
61
|
onSuccess: (result) => {
|
|
61
62
|
banJobEvent.subscribe(result.jobId, result.customToken);
|
|
62
63
|
},
|
|
63
|
-
onError: () => {
|
|
64
|
-
toast.showToast("Failed to start hard-ban.", "error");
|
|
65
|
-
},
|
|
66
64
|
});
|
|
67
65
|
const bulkUsers = useBulkAction({
|
|
68
66
|
mutationFn: (payload) => apiClient.post(ADMIN_ENDPOINTS.USERS_BULK, payload),
|
|
@@ -75,14 +73,12 @@ export function AdminUsersView({ children, ...props }) {
|
|
|
75
73
|
},
|
|
76
74
|
});
|
|
77
75
|
const unbanUser = useApiMutation({
|
|
76
|
+
errorMessage: "Failed to lift ban.",
|
|
78
77
|
mutationFn: (uid) => apiClient.post(ADMIN_ENDPOINTS.USER_UNBAN(uid), {}),
|
|
79
78
|
onSuccess: () => {
|
|
80
79
|
toast.showToast("Ban lifted.", "success");
|
|
81
80
|
void queryClient.invalidateQueries({ queryKey: ["admin", "users", "listing"] });
|
|
82
81
|
},
|
|
83
|
-
onError: () => {
|
|
84
|
-
toast.showToast("Failed to lift ban.", "error");
|
|
85
|
-
},
|
|
86
82
|
});
|
|
87
83
|
const handleManageBulkClick = (selection) => {
|
|
88
84
|
const rowId = selection.selectedIds[0];
|
|
@@ -8,7 +8,7 @@ import { apiClient } from "../../../http";
|
|
|
8
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
9
|
import { quickCreateTaxonomySchema } from "../schemas/small-forms";
|
|
10
10
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
11
|
-
import { applyZodIssues } from "../../../ui/forms/
|
|
11
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
12
12
|
function toBrandSlug(str) {
|
|
13
13
|
const base = str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
14
14
|
return base.startsWith("brand-") ? base : `brand-${base}`;
|
|
@@ -19,6 +19,7 @@ export function BrandQuickCreateForm({ onSaved, onCancel }) {
|
|
|
19
19
|
const [isActive, setIsActive] = React.useState(true);
|
|
20
20
|
const { showToast } = useToast();
|
|
21
21
|
const mutation = useApiMutation({
|
|
22
|
+
errorMessage: "Failed to create brand.",
|
|
22
23
|
mutationFn: async () => apiClient.post(ADMIN_ENDPOINTS.BRANDS, {
|
|
23
24
|
name,
|
|
24
25
|
slug: toBrandSlug(name),
|
|
@@ -29,9 +30,6 @@ export function BrandQuickCreateForm({ onSaved, onCancel }) {
|
|
|
29
30
|
const id = res?.data?.id ?? res?.id ?? "";
|
|
30
31
|
onSaved(id, name);
|
|
31
32
|
},
|
|
32
|
-
onError: (err) => {
|
|
33
|
-
showToast(err?.message ?? "Failed to create brand.", "error");
|
|
34
|
-
},
|
|
35
33
|
});
|
|
36
34
|
return (_jsx(Form, { schema: quickCreateTaxonomySchema, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FormErrorSummary, {}), _jsx(Input, { label: "Brand name", value: name, onChange: (e) => setName(e.target.value), required: true, placeholder: "e.g. Hot Wheels", autoFocus: true }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Optional brief description" }), _jsx(Toggle, { label: "Active", checked: isActive, onChange: setIsActive }), _jsxs(Row, { gap: "3", padding: "t-xs", children: [_jsx(Button, { type: "submit", isLoading: mutation.isPending, disabled: mutation.isPending, onClick: () => {
|
|
37
35
|
clearErrors();
|
|
@@ -8,7 +8,7 @@ import { apiClient } from "../../../http";
|
|
|
8
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
9
|
import { quickCreateTaxonomySchema } from "../schemas/small-forms";
|
|
10
10
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
11
|
-
import { applyZodIssues } from "../../../ui/forms/
|
|
11
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
12
12
|
function toCategorySlug(str) {
|
|
13
13
|
return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
14
14
|
}
|
|
@@ -18,6 +18,7 @@ export function CategoryQuickCreateForm({ onSaved, onCancel }) {
|
|
|
18
18
|
const [isActive, setIsActive] = React.useState(true);
|
|
19
19
|
const { showToast } = useToast();
|
|
20
20
|
const mutation = useApiMutation({
|
|
21
|
+
errorMessage: "Failed to create category.",
|
|
21
22
|
mutationFn: async () => apiClient.post(ADMIN_ENDPOINTS.CATEGORIES, {
|
|
22
23
|
name,
|
|
23
24
|
slug: toCategorySlug(name),
|
|
@@ -28,9 +29,6 @@ export function CategoryQuickCreateForm({ onSaved, onCancel }) {
|
|
|
28
29
|
const id = res?.data?.id ?? res?.id ?? "";
|
|
29
30
|
onSaved(id, name);
|
|
30
31
|
},
|
|
31
|
-
onError: (err) => {
|
|
32
|
-
showToast(err?.message ?? "Failed to create category.", "error");
|
|
33
|
-
},
|
|
34
32
|
});
|
|
35
33
|
return (_jsx(Form, { schema: quickCreateTaxonomySchema, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FormErrorSummary, {}), _jsx(Input, { label: "Category name", value: name, onChange: (e) => setName(e.target.value), required: true, placeholder: "e.g. Trading Cards", autoFocus: true }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Optional brief description" }), _jsx(Toggle, { label: "Active", checked: isActive, onChange: setIsActive }), _jsxs(Row, { gap: "3", padding: "t-xs", children: [_jsx(Button, { type: "submit", isLoading: mutation.isPending, disabled: mutation.isPending, onClick: () => {
|
|
36
34
|
clearErrors();
|
|
@@ -7,7 +7,7 @@ import { isAuthError } from "../../../utils/auth-error";
|
|
|
7
7
|
import { Button, CountdownDisplay, Div, LoginRequiredModal, Modal, Row, Span, Stack, Text } from "../../../ui";
|
|
8
8
|
import { Form } from "../../../ui/components/Form";
|
|
9
9
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
10
|
-
import { applyZodIssues } from "../../../ui/forms/
|
|
10
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
11
11
|
import { placeBidSchema } from "../schemas/bid-input";
|
|
12
12
|
import { useLiveAuctionBid } from "../hooks/useLiveAuctionBid";
|
|
13
13
|
import { AUCTION_BUYOUT_WINDOW_MINUTES } from "../../../_internal/shared/checkout/lanes";
|
|
@@ -5,7 +5,7 @@ import { useState } from "react";
|
|
|
5
5
|
import { Alert, Button, Div, Heading, Row, Stack, Text, useToast } from "../../../ui";
|
|
6
6
|
import { Form } from "../../../ui/components/Form";
|
|
7
7
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
8
|
-
import { applyZodIssues } from "../../../ui/forms/
|
|
8
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
9
9
|
import { forgotPasswordSchema } from "../schemas";
|
|
10
10
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
11
11
|
export function ForgotPasswordView({ onSubmit, isLoading = false, error, success, labels = {}, renderBackLink, className = "", }) {
|
|
@@ -6,7 +6,7 @@ import { Alert, Button, Div, Heading, Row, SiteMark, Stack, Text, useToast } fro
|
|
|
6
6
|
import { Form } from "../../../ui/components/Form";
|
|
7
7
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
8
8
|
import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
|
|
9
|
-
import { applyZodIssues } from "../../../ui/forms/
|
|
9
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
10
10
|
import { loginSchema } from "../schemas";
|
|
11
11
|
import { SocialAuthButtons } from "./SocialAuthButtons";
|
|
12
12
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
@@ -7,7 +7,7 @@ import { Alert, Button, Div, Heading, Row, SiteMark, Stack, Text, useToast } fro
|
|
|
7
7
|
import { Form } from "../../../ui/components/Form";
|
|
8
8
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
9
9
|
import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
|
|
10
|
-
import { applyZodIssues } from "../../../ui/forms/
|
|
10
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
11
11
|
import { registerSchema, registerPasswordSchema } from "../schemas";
|
|
12
12
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
13
13
|
const registerClientSchema = registerSchema.extend({
|
|
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
import { Alert, Button, Div, Heading, Row, Stack, Text, useToast } from "../../../ui";
|
|
7
7
|
import { Form } from "../../../ui/components/Form";
|
|
8
8
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
9
|
-
import { applyZodIssues } from "../../../ui/forms/
|
|
9
|
+
import { applyZodIssues } from "../../../ui/forms/apply-zod-issues";
|
|
10
10
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
11
11
|
const resetClientSchema = z.object({
|
|
12
12
|
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Button, Div, Row, Span } from "../../../ui";
|
|
3
3
|
export function SocialAuthButtons({ onGoogle, disabled, dividerLabel, googleLabel, }) {
|
|
4
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: "relative", children: [_jsx(Row, { className: "absolute inset-0", align: "center", children: _jsx(Div, { className: "w-full border-t border-neutral-200" }) }), _jsx(Row, { textSize: "sm", className: "relative", justify: "center", children: _jsx(Span, { padding: "x-xs", surface: "default", color: "muted", children: dividerLabel }) })] }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1", children: _jsxs(Button, { rounded: "md", type: "button", onClick: onGoogle, disabled: disabled, variant: "outline", paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", className: "inline- justify-[center] border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50", children: [_jsxs("svg", { className: "mr-2 h-5 w-5", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { fill: "currentColor", d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" }), _jsx("path", { fill: "currentColor", d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" }), _jsx("path", { fill: "currentColor", d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" }), _jsx("path", { fill: "currentColor", d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" })] }), googleLabel] }) })] }));
|
|
4
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: "relative", children: [_jsx(Row, { className: "absolute inset-0", align: "center", children: _jsx(Div, { className: "w-full border-t border-neutral-200" }) }), _jsx(Row, { textSize: "sm", className: "relative", justify: "center", children: _jsx(Span, { padding: "x-xs", surface: "default", color: "muted", children: dividerLabel }) })] }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1", children: _jsxs(Button, { rounded: "md", type: "button", onClick: onGoogle, disabled: disabled, variant: "outline", paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", className: "inline- justify-[center] border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50", children: [_jsxs("svg", { className: "mr-2 h-5 w-5 shrink-0", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { fill: "currentColor", d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" }), _jsx("path", { fill: "currentColor", d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" }), _jsx("path", { fill: "currentColor", d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" }), _jsx("path", { fill: "currentColor", d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" })] }), googleLabel] }) })] }));
|
|
5
5
|
}
|
|
@@ -57,8 +57,20 @@ export class SessionRepository extends BaseRepository {
|
|
|
57
57
|
async updateActivity(sessionId, data) {
|
|
58
58
|
const updateData = {
|
|
59
59
|
lastActivity: new Date(),
|
|
60
|
-
...(data?.location && { location: data.location }),
|
|
61
60
|
};
|
|
61
|
+
// Firestore `.update()` REPLACES a map field wholesale — it does not merge.
|
|
62
|
+
// Passing `location` straight through therefore destroyed whichever of
|
|
63
|
+
// `{ country, city }` the caller happened to omit, and `location.city` is
|
|
64
|
+
// PII-encrypted, so the loss is unrecoverable. Same footgun the profile
|
|
65
|
+
// route documents for `uiPreferences`.
|
|
66
|
+
//
|
|
67
|
+
// Latent rather than live: both current callers pass no `data` at all, so
|
|
68
|
+
// this branch never runs today. It costs one read, and only on the branch
|
|
69
|
+
// that supplies a location — the hot activity ping stays a single write.
|
|
70
|
+
if (data?.location) {
|
|
71
|
+
const current = await this.findById(sessionId); // mapDoc → already decrypted
|
|
72
|
+
updateData.location = { ...(current?.location ?? {}), ...data.location };
|
|
73
|
+
}
|
|
62
74
|
try {
|
|
63
75
|
await this.update(sessionId, updateData);
|
|
64
76
|
}
|
|
@@ -3,8 +3,23 @@ import { BaseRepository } from "../../../providers/db-firebase";
|
|
|
3
3
|
import { type EmailVerificationTokenDocument, type PasswordResetTokenDocument } from "../schemas";
|
|
4
4
|
export declare class EmailVerificationTokenRepository extends BaseRepository<EmailVerificationTokenDocument> {
|
|
5
5
|
constructor();
|
|
6
|
+
/**
|
|
7
|
+
* Declared, not hand-rolled in a `create` override.
|
|
8
|
+
*
|
|
9
|
+
* The override this replaces computed `encryptPiiFields(...)` and then
|
|
10
|
+
* OVERWROTE the result with `addPiiIndices(data, MAP)` — which returns
|
|
11
|
+
* `{...plaintext, ...indices}` — so the ciphertext was discarded and every
|
|
12
|
+
* verification token's email was written to Firestore in CLEARTEXT. It was
|
|
13
|
+
* invisible because `mapDoc` decrypts on read and `decryptPiiFields` passes
|
|
14
|
+
* plaintext straight through, so reads looked identical either way.
|
|
15
|
+
*
|
|
16
|
+
* `applyWriteHooks` covers create/createWithId/update and the `*InTx` /
|
|
17
|
+
* `*InBatch` family, so no write path can skip encryption the way an
|
|
18
|
+
* override of `create` alone did.
|
|
19
|
+
*/
|
|
20
|
+
protected piiFields: readonly ["email"];
|
|
21
|
+
protected piiIndexMap: Record<string, string>;
|
|
6
22
|
protected mapDoc<D = EmailVerificationTokenDocument>(snap: DocumentSnapshot): D;
|
|
7
|
-
create(data: Partial<EmailVerificationTokenDocument>): Promise<EmailVerificationTokenDocument>;
|
|
8
23
|
findByToken(token: string): Promise<EmailVerificationTokenDocument | null>;
|
|
9
24
|
findByUserId(userId: string): Promise<EmailVerificationTokenDocument[]>;
|
|
10
25
|
findByEmail(email: string): Promise<EmailVerificationTokenDocument[]>;
|
|
@@ -15,8 +30,16 @@ export declare class EmailVerificationTokenRepository extends BaseRepository<Ema
|
|
|
15
30
|
}
|
|
16
31
|
export declare class PasswordResetTokenRepository extends BaseRepository<PasswordResetTokenDocument> {
|
|
17
32
|
constructor();
|
|
33
|
+
/**
|
|
34
|
+
* Same defect as its sibling above, one spread further along: the override
|
|
35
|
+
* this replaces built `{...encrypted, ...addPiiIndices(data, MAP)}`, and
|
|
36
|
+
* because `addPiiIndices` returns `{...plaintext, ...indices}` the plaintext
|
|
37
|
+
* `email` was spread back OVER the ciphertext. Reset-token emails were
|
|
38
|
+
* written in cleartext too.
|
|
39
|
+
*/
|
|
40
|
+
protected piiFields: readonly ["email"];
|
|
41
|
+
protected piiIndexMap: Record<string, string>;
|
|
18
42
|
protected mapDoc<D = PasswordResetTokenDocument>(snap: DocumentSnapshot): D;
|
|
19
|
-
create(data: Partial<PasswordResetTokenDocument>): Promise<PasswordResetTokenDocument>;
|
|
20
43
|
findByToken(token: string): Promise<PasswordResetTokenDocument | null>;
|
|
21
44
|
findByUserId(userId: string): Promise<PasswordResetTokenDocument[]>;
|
|
22
45
|
findByEmail(email: string): Promise<PasswordResetTokenDocument[]>;
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { normalizeError } from "../../../errors/normalize";
|
|
2
2
|
import { DatabaseError } from "../../../errors";
|
|
3
3
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
4
|
-
import { TOKEN_PII_FIELDS, TOKEN_PII_INDEX_MAP,
|
|
4
|
+
import { TOKEN_PII_FIELDS, TOKEN_PII_INDEX_MAP, decryptPiiFields, piiBlindIndex, } from "../../../security";
|
|
5
5
|
import { resolveDate } from "../../../utils";
|
|
6
6
|
import { EMAIL_VERIFICATION_COLLECTION, PASSWORD_RESET_COLLECTION, TOKEN_FIELDS, } from "../schemas";
|
|
7
7
|
export class EmailVerificationTokenRepository extends BaseRepository {
|
|
8
8
|
constructor() {
|
|
9
9
|
super(EMAIL_VERIFICATION_COLLECTION);
|
|
10
|
+
/**
|
|
11
|
+
* Declared, not hand-rolled in a `create` override.
|
|
12
|
+
*
|
|
13
|
+
* The override this replaces computed `encryptPiiFields(...)` and then
|
|
14
|
+
* OVERWROTE the result with `addPiiIndices(data, MAP)` — which returns
|
|
15
|
+
* `{...plaintext, ...indices}` — so the ciphertext was discarded and every
|
|
16
|
+
* verification token's email was written to Firestore in CLEARTEXT. It was
|
|
17
|
+
* invisible because `mapDoc` decrypts on read and `decryptPiiFields` passes
|
|
18
|
+
* plaintext straight through, so reads looked identical either way.
|
|
19
|
+
*
|
|
20
|
+
* `applyWriteHooks` covers create/createWithId/update and the `*InTx` /
|
|
21
|
+
* `*InBatch` family, so no write path can skip encryption the way an
|
|
22
|
+
* override of `create` alone did.
|
|
23
|
+
*/
|
|
24
|
+
this.piiFields = TOKEN_PII_FIELDS;
|
|
25
|
+
this.piiIndexMap = TOKEN_PII_INDEX_MAP;
|
|
10
26
|
}
|
|
11
27
|
mapDoc(snap) {
|
|
12
28
|
const raw = super.mapDoc(snap);
|
|
@@ -14,15 +30,6 @@ export class EmailVerificationTokenRepository extends BaseRepository {
|
|
|
14
30
|
...TOKEN_PII_FIELDS,
|
|
15
31
|
]);
|
|
16
32
|
}
|
|
17
|
-
async create(data) {
|
|
18
|
-
let encrypted = encryptPiiFields(data, [...TOKEN_PII_FIELDS]);
|
|
19
|
-
encrypted = addPiiIndices(data, TOKEN_PII_INDEX_MAP);
|
|
20
|
-
const merged = {
|
|
21
|
-
...encrypted,
|
|
22
|
-
...addPiiIndices(data, TOKEN_PII_INDEX_MAP),
|
|
23
|
-
};
|
|
24
|
-
return super.create(merged);
|
|
25
|
-
}
|
|
26
33
|
async findByToken(token) {
|
|
27
34
|
return this.findOneBy(TOKEN_FIELDS.TOKEN, token);
|
|
28
35
|
}
|
|
@@ -80,6 +87,15 @@ export class EmailVerificationTokenRepository extends BaseRepository {
|
|
|
80
87
|
export class PasswordResetTokenRepository extends BaseRepository {
|
|
81
88
|
constructor() {
|
|
82
89
|
super(PASSWORD_RESET_COLLECTION);
|
|
90
|
+
/**
|
|
91
|
+
* Same defect as its sibling above, one spread further along: the override
|
|
92
|
+
* this replaces built `{...encrypted, ...addPiiIndices(data, MAP)}`, and
|
|
93
|
+
* because `addPiiIndices` returns `{...plaintext, ...indices}` the plaintext
|
|
94
|
+
* `email` was spread back OVER the ciphertext. Reset-token emails were
|
|
95
|
+
* written in cleartext too.
|
|
96
|
+
*/
|
|
97
|
+
this.piiFields = TOKEN_PII_FIELDS;
|
|
98
|
+
this.piiIndexMap = TOKEN_PII_INDEX_MAP;
|
|
83
99
|
}
|
|
84
100
|
mapDoc(snap) {
|
|
85
101
|
const raw = super.mapDoc(snap);
|
|
@@ -87,14 +103,6 @@ export class PasswordResetTokenRepository extends BaseRepository {
|
|
|
87
103
|
...TOKEN_PII_FIELDS,
|
|
88
104
|
]);
|
|
89
105
|
}
|
|
90
|
-
async create(data) {
|
|
91
|
-
let encrypted = encryptPiiFields(data, [...TOKEN_PII_FIELDS]);
|
|
92
|
-
encrypted = {
|
|
93
|
-
...encrypted,
|
|
94
|
-
...addPiiIndices(data, TOKEN_PII_INDEX_MAP),
|
|
95
|
-
};
|
|
96
|
-
return super.create(encrypted);
|
|
97
|
-
}
|
|
98
106
|
async findByToken(token) {
|
|
99
107
|
return this.findOneBy(TOKEN_FIELDS.TOKEN, token);
|
|
100
108
|
}
|
|
@@ -11,10 +11,10 @@ export declare const createBlogPostSchema: z.ZodObject<{
|
|
|
11
11
|
isFeatured: z.ZodDefault<z.ZodBoolean>;
|
|
12
12
|
status: z.ZodDefault<z.ZodEnum<["draft", "published", "archived"]>>;
|
|
13
13
|
coverImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<[z.ZodObject<{
|
|
14
|
-
url: z.ZodString
|
|
14
|
+
url: z.ZodEffects<z.ZodString, string, string>;
|
|
15
15
|
type: z.ZodEnum<["image", "video", "file"]>;
|
|
16
16
|
alt: z.ZodOptional<z.ZodString>;
|
|
17
|
-
thumbnailUrl: z.ZodOptional<z.ZodString
|
|
17
|
+
thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
18
18
|
}, "strip", z.ZodTypeAny, {
|
|
19
19
|
type: "video" | "image" | "file";
|
|
20
20
|
url: string;
|
|
@@ -25,15 +25,15 @@ export declare const createBlogPostSchema: z.ZodObject<{
|
|
|
25
25
|
url: string;
|
|
26
26
|
alt?: string | undefined;
|
|
27
27
|
thumbnailUrl?: string | undefined;
|
|
28
|
-
}>, z.ZodEffects<z.ZodString, {
|
|
28
|
+
}>, z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, {
|
|
29
29
|
url: string;
|
|
30
30
|
type: "image";
|
|
31
31
|
}, string>]>>>;
|
|
32
32
|
contentImages: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
33
|
-
url: z.ZodString
|
|
33
|
+
url: z.ZodEffects<z.ZodString, string, string>;
|
|
34
34
|
type: z.ZodEnum<["image", "video", "file"]>;
|
|
35
35
|
alt: z.ZodOptional<z.ZodString>;
|
|
36
|
-
thumbnailUrl: z.ZodOptional<z.ZodString
|
|
36
|
+
thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
37
37
|
}, "strip", z.ZodTypeAny, {
|
|
38
38
|
type: "video" | "image" | "file";
|
|
39
39
|
url: string;
|
|
@@ -46,10 +46,10 @@ export declare const createBlogPostSchema: z.ZodObject<{
|
|
|
46
46
|
thumbnailUrl?: string | undefined;
|
|
47
47
|
}>, "many">>>;
|
|
48
48
|
additionalImages: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
49
|
-
url: z.ZodString
|
|
49
|
+
url: z.ZodEffects<z.ZodString, string, string>;
|
|
50
50
|
type: z.ZodEnum<["image", "video", "file"]>;
|
|
51
51
|
alt: z.ZodOptional<z.ZodString>;
|
|
52
|
-
thumbnailUrl: z.ZodOptional<z.ZodString
|
|
52
|
+
thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
53
53
|
}, "strip", z.ZodTypeAny, {
|
|
54
54
|
type: "video" | "image" | "file";
|
|
55
55
|
url: string;
|
|
@@ -150,10 +150,10 @@ export declare const updateBlogPostSchema: z.ZodObject<{
|
|
|
150
150
|
isFeatured: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
151
151
|
status: z.ZodOptional<z.ZodDefault<z.ZodEnum<["draft", "published", "archived"]>>>;
|
|
152
152
|
coverImage: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<[z.ZodObject<{
|
|
153
|
-
url: z.ZodString
|
|
153
|
+
url: z.ZodEffects<z.ZodString, string, string>;
|
|
154
154
|
type: z.ZodEnum<["image", "video", "file"]>;
|
|
155
155
|
alt: z.ZodOptional<z.ZodString>;
|
|
156
|
-
thumbnailUrl: z.ZodOptional<z.ZodString
|
|
156
|
+
thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
157
157
|
}, "strip", z.ZodTypeAny, {
|
|
158
158
|
type: "video" | "image" | "file";
|
|
159
159
|
url: string;
|
|
@@ -164,15 +164,15 @@ export declare const updateBlogPostSchema: z.ZodObject<{
|
|
|
164
164
|
url: string;
|
|
165
165
|
alt?: string | undefined;
|
|
166
166
|
thumbnailUrl?: string | undefined;
|
|
167
|
-
}>, z.ZodEffects<z.ZodString, {
|
|
167
|
+
}>, z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, {
|
|
168
168
|
url: string;
|
|
169
169
|
type: "image";
|
|
170
170
|
}, string>]>>>>;
|
|
171
171
|
contentImages: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
172
|
-
url: z.ZodString
|
|
172
|
+
url: z.ZodEffects<z.ZodString, string, string>;
|
|
173
173
|
type: z.ZodEnum<["image", "video", "file"]>;
|
|
174
174
|
alt: z.ZodOptional<z.ZodString>;
|
|
175
|
-
thumbnailUrl: z.ZodOptional<z.ZodString
|
|
175
|
+
thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
176
176
|
}, "strip", z.ZodTypeAny, {
|
|
177
177
|
type: "video" | "image" | "file";
|
|
178
178
|
url: string;
|
|
@@ -185,10 +185,10 @@ export declare const updateBlogPostSchema: z.ZodObject<{
|
|
|
185
185
|
thumbnailUrl?: string | undefined;
|
|
186
186
|
}>, "many">>>>;
|
|
187
187
|
additionalImages: z.ZodOptional<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
188
|
-
url: z.ZodString
|
|
188
|
+
url: z.ZodEffects<z.ZodString, string, string>;
|
|
189
189
|
type: z.ZodEnum<["image", "video", "file"]>;
|
|
190
190
|
alt: z.ZodOptional<z.ZodString>;
|
|
191
|
-
thumbnailUrl: z.ZodOptional<z.ZodString
|
|
191
|
+
thumbnailUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
192
192
|
}, "strip", z.ZodTypeAny, {
|
|
193
193
|
type: "video" | "image" | "file";
|
|
194
194
|
url: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sortBy } from "@mohasinac/appkit";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { mediaUrlSchema } from "../../../validation/schemas";
|
|
3
4
|
import { serverLogger } from "../../../monitoring";
|
|
4
5
|
import { blogRepository } from "../repository/blog.repository";
|
|
5
6
|
import { coerceMediaField, getMediaUrl } from "../../../utils";
|
|
@@ -8,23 +9,20 @@ import { finalizeStagedMediaField, finalizeStagedMediaObject, finalizeStagedMedi
|
|
|
8
9
|
const singleImageMediaSchema = z
|
|
9
10
|
.union([
|
|
10
11
|
z.object({
|
|
11
|
-
url:
|
|
12
|
+
url: mediaUrlSchema,
|
|
12
13
|
type: z.enum(["image", "video", "file"]),
|
|
13
14
|
alt: z.string().optional(),
|
|
14
|
-
thumbnailUrl:
|
|
15
|
+
thumbnailUrl: mediaUrlSchema.optional(),
|
|
15
16
|
}),
|
|
16
|
-
|
|
17
|
-
.string()
|
|
18
|
-
.url()
|
|
19
|
-
.transform((url) => ({ url, type: "image" })),
|
|
17
|
+
mediaUrlSchema.transform((url) => ({ url, type: "image" })),
|
|
20
18
|
])
|
|
21
19
|
.nullable()
|
|
22
20
|
.optional();
|
|
23
21
|
const mediaFieldSchema = z.object({
|
|
24
|
-
url:
|
|
22
|
+
url: mediaUrlSchema,
|
|
25
23
|
type: z.enum(["image", "video", "file"]),
|
|
26
24
|
alt: z.string().optional(),
|
|
27
|
-
thumbnailUrl:
|
|
25
|
+
thumbnailUrl: mediaUrlSchema.optional(),
|
|
28
26
|
});
|
|
29
27
|
export const createBlogPostSchema = z.object({
|
|
30
28
|
title: z.string().min(1),
|
|
@@ -267,7 +267,7 @@ export declare const blogPostSchema: z.ZodObject<{
|
|
|
267
267
|
thumbnailUrl?: string | undefined;
|
|
268
268
|
youtubeId?: string | undefined;
|
|
269
269
|
duration?: number | undefined;
|
|
270
|
-
}>, z.ZodEffects<z.ZodString, {
|
|
270
|
+
}>, z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, {
|
|
271
271
|
url: string;
|
|
272
272
|
type: "image";
|
|
273
273
|
}, string>]>>>, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./firestore";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { mediaUrlSchema } from "../../../validation/schemas";
|
|
3
4
|
import { mediaFieldSchema } from "../../media/types/index";
|
|
4
5
|
import { auditTimestampsShape, firestoreDateSchema } from "../../../schemas/firestore-helpers";
|
|
5
6
|
// ─── Firestore document schema (W2) ───────────────────────────────────────────
|
|
@@ -31,10 +32,7 @@ export const blogPostFirestoreSchema = z.object({
|
|
|
31
32
|
const blogCoverImageSchema = z
|
|
32
33
|
.union([
|
|
33
34
|
mediaFieldSchema,
|
|
34
|
-
|
|
35
|
-
.string()
|
|
36
|
-
.url()
|
|
37
|
-
.transform((url) => ({ url, type: "image" })),
|
|
35
|
+
mediaUrlSchema.transform((url) => ({ url, type: "image" })),
|
|
38
36
|
])
|
|
39
37
|
.nullable()
|
|
40
38
|
.optional()
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { NextResponse } from "next/server.js";
|
|
12
12
|
import { z } from "zod";
|
|
13
|
+
import { mediaUrlSchema } from "../../../../validation/schemas";
|
|
13
14
|
import { getProviders } from "../../../../contracts";
|
|
14
15
|
import { createRouteHandler } from "../../../../next";
|
|
15
16
|
import { normalizeError } from "../../../../errors/normalize";
|
|
@@ -48,7 +49,7 @@ const categoryUpdateSchema = z
|
|
|
48
49
|
tier: z.number().int().min(0).optional(),
|
|
49
50
|
order: z.number().int().min(0).optional(),
|
|
50
51
|
description: z.string().optional(),
|
|
51
|
-
imageUrl:
|
|
52
|
+
imageUrl: mediaUrlSchema.optional(),
|
|
52
53
|
isFeatured: z.boolean().optional(),
|
|
53
54
|
showOnHomepage: z.boolean().optional(),
|
|
54
55
|
isActive: z.boolean().optional(),
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { NextResponse } from "next/server.js";
|
|
16
16
|
import { z } from "zod";
|
|
17
|
+
import { mediaUrlSchema } from "../../../validation/schemas";
|
|
17
18
|
import { getProviders } from "../../../contracts";
|
|
18
19
|
import { createRouteHandler } from "../../../next";
|
|
19
20
|
import { normalizeError } from "../../../errors/normalize";
|
|
@@ -177,7 +178,7 @@ const categoryCreateSchema = z
|
|
|
177
178
|
tier: z.number().int().min(0).optional(),
|
|
178
179
|
order: z.number().int().min(0).optional(),
|
|
179
180
|
description: z.string().optional(),
|
|
180
|
-
imageUrl:
|
|
181
|
+
imageUrl: mediaUrlSchema.optional(),
|
|
181
182
|
isFeatured: z.boolean().optional(),
|
|
182
183
|
showOnHomepage: z.boolean().optional(),
|
|
183
184
|
})
|
|
@@ -467,7 +467,7 @@ export declare const eventItemSchema: z.ZodObject<{
|
|
|
467
467
|
thumbnailUrl?: string | undefined;
|
|
468
468
|
youtubeId?: string | undefined;
|
|
469
469
|
duration?: number | undefined;
|
|
470
|
-
}>, z.ZodEffects<z.ZodString, {
|
|
470
|
+
}>, z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, {
|
|
471
471
|
url: string;
|
|
472
472
|
type: "image";
|
|
473
473
|
}, string>]>>>, {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
export * from "./firestore";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { mediaUrlSchema } from "../../../validation/schemas";
|
|
3
4
|
import { mediaFieldSchema } from "../../media/types/index";
|
|
4
5
|
const eventCoverImageSchema = z
|
|
5
6
|
.union([
|
|
6
7
|
mediaFieldSchema,
|
|
7
|
-
|
|
8
|
-
.string()
|
|
9
|
-
.url()
|
|
10
|
-
.transform((url) => ({ url, type: "image" })),
|
|
8
|
+
mediaUrlSchema.transform((url) => ({ url, type: "image" })),
|
|
11
9
|
])
|
|
12
10
|
.nullable()
|
|
13
11
|
.optional()
|
|
@@ -6,5 +6,5 @@ const __P = {
|
|
|
6
6
|
const CLS_CONTACT_CARD = "flex items-center gap-[var(--appkit-space-3)] rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-elevated)] px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] hover:border-primary/40 transition-colors";
|
|
7
7
|
const CLS_CONTACT_LABEL = "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)]";
|
|
8
8
|
export function ContactCTA({ email, phone, contactHref, labels, }) {
|
|
9
|
-
return (_jsxs(Card, { variant: "outlined", padding: "lg", className: "text-center", children: [_jsx(Row, { className: "mb-6", justify: "center", children: _jsx(Div, { className: `bg-primary/10 ${__P.p8}`, rounded: "full", children: _jsx("svg", { className: "h-12 w-12 text-primary", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }) }) }), _jsx(Heading, { level: 2, className: "mb-3", size: "2xl", weight: "semibold", children: labels?.title ?? "Need More Help?" }), _jsx(Text, { className: "mx-auto mb-6 max-w-2xl", color: "muted", children: labels?.description ?? "Our support team is here to help you." }), _jsxs(Div, { layout: "grid", gap: "4", className: "mb-8 grid-cols-1 md:grid-cols-3", children: [_jsxs(TextLink, { href: `mailto:${email}`, variant: "none", className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.emailUs ?? "Email Us" }), _jsx(Text, { size: "xs", color: "muted", children: email })] }), _jsxs(TextLink, { href: `tel:${phone}`, variant: "none", className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.callUs ?? "Call Us" }), _jsx(Text, { size: "xs", color: "muted", children: phone })] }), _jsxs(TextLink, { href: contactHref, className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.contactForm ?? "Contact Form" }), _jsx(Text, { size: "xs", color: "muted", children: labels?.submitRequest ?? "Submit a request" })] })] }), _jsxs(TextLink, { rounded: "xl", href: contactHref, layout: "inline-flex", align: "center", gap: "sm", className: "bg-primary p-[1.5rem] text-white transition-colors hover:bg-primary/90", weight: "medium", children: [_jsx(Span, { children: labels?.contactTeam ?? "Contact Team" }), _jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })] })] }));
|
|
9
|
+
return (_jsxs(Card, { variant: "outlined", padding: "lg", className: "text-center", children: [_jsx(Row, { className: "mb-6", justify: "center", children: _jsx(Div, { className: `bg-primary/10 ${__P.p8}`, rounded: "full", children: _jsx("svg", { className: "h-12 w-12 text-primary", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" }) }) }) }), _jsx(Heading, { level: 2, className: "mb-3", size: "2xl", weight: "semibold", children: labels?.title ?? "Need More Help?" }), _jsx(Text, { className: "mx-auto mb-6 max-w-2xl", color: "muted", children: labels?.description ?? "Our support team is here to help you." }), _jsxs(Div, { layout: "grid", gap: "4", className: "mb-8 grid-cols-1 md:grid-cols-3", children: [_jsxs(TextLink, { href: `mailto:${email}`, variant: "none", className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.emailUs ?? "Email Us" }), _jsx(Text, { size: "xs", color: "muted", children: email })] }), _jsxs(TextLink, { href: `tel:${phone}`, variant: "none", className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.callUs ?? "Call Us" }), _jsx(Text, { size: "xs", color: "muted", children: phone })] }), _jsxs(TextLink, { href: contactHref, className: CLS_CONTACT_CARD, children: [_jsx(Text, { className: CLS_CONTACT_LABEL, children: labels?.contactForm ?? "Contact Form" }), _jsx(Text, { size: "xs", color: "muted", children: labels?.submitRequest ?? "Submit a request" })] })] }), _jsxs(TextLink, { rounded: "xl", href: contactHref, layout: "inline-flex", align: "center", gap: "sm", className: "bg-primary p-[1.5rem] text-white transition-colors hover:bg-primary/90", weight: "medium", children: [_jsx(Span, { children: labels?.contactTeam ?? "Contact Team" }), _jsx("svg", { className: "h-5 w-5 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })] })] }));
|
|
10
10
|
}
|