diff --git a/backend/src/routes/cartCheckout.ts b/backend/src/routes/cartCheckout.ts index 5e2e3be..dd63a7a 100644 --- a/backend/src/routes/cartCheckout.ts +++ b/backend/src/routes/cartCheckout.ts @@ -1,4 +1,5 @@ import { Router, Request, Response } from 'express'; +import type { PoolClient } from 'pg'; import { pool } from '../db'; import { asyncRoute } from '../asyncRoute'; import { requireCustomer } from '../middleware/customerAuth'; @@ -38,7 +39,7 @@ interface LockedCart { // Locks the customer's cart, verifies every item is still reserved to them, // and returns { cartId, items: [{id, name, price_cents}], totalCents }. -async function loadLockedCart(client: any, customerId: number): Promise { +async function loadLockedCart(client: PoolClient, customerId: number): Promise { const { rows: cartRows } = await client.query(`SELECT id FROM carts WHERE customer_id = $1`, [customerId]); if (!cartRows.length) return null; const cartId = cartRows[0].id; @@ -64,7 +65,7 @@ type OpenedCheckout = // items. The caller owns the transaction — on `ok: false` it should roll back // and return the error as a 400. async function openCheckout( - client: any, + client: PoolClient, customerId: number, shippingAddressId: number, processor: string, @@ -145,7 +146,7 @@ router.post('/paypal/create', requireCustomer, asyncRoute(async (req: Request, r // Returns the sold item ids and the buyer, so the caller can notify favoriters // *after* COMMIT. Sending inside the transaction would email people about a // sale that then rolled back, and would hold the transaction open for SMTP. -async function completeCheckout(client: any, checkoutId: number, processor: string, processorOrderId: string | null, rawEvent: unknown): Promise<{ itemIds: number[]; buyerId: number | null }> { +async function completeCheckout(client: PoolClient, checkoutId: number, processor: string, processorOrderId: string | null, rawEvent: unknown): Promise<{ itemIds: number[]; buyerId: number | null }> { const { rows: checkoutItems } = await client.query( `SELECT item_id, price_cents FROM checkout_items WHERE checkout_id = $1`, [checkoutId] diff --git a/backend/src/routes/customers.ts b/backend/src/routes/customers.ts index 20a06fc..eae43c9 100755 --- a/backend/src/routes/customers.ts +++ b/backend/src/routes/customers.ts @@ -32,7 +32,21 @@ async function createSession(customerId: number): Promise { return token; } -function publicCustomer(c: any) { +// The subset of a customers row that is safe to return to the customer it +// belongs to. Typed as its own shape rather than `any` so that adding a column +// to the table — a password hash, a token, an internal note — cannot silently +// start being echoed back by a `...c` somewhere downstream. +interface CustomerRow { + id: number; + email: string; + name: string | null; + email_verified: boolean; + marketing_consent: boolean; + favorite_alerts: boolean; + created_at: Date; +} + +function publicCustomer(c: CustomerRow) { return { id: c.id, email: c.email, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 76d40ae..75b509e 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,15 @@ import { useEffect, useState, useCallback, useMemo } from 'react'; -import { Layout, Typography, Switch, Row, Col, Spin, Button, theme, Badge, Empty, Alert } from 'antd'; +import Layout from 'antd/es/layout'; +import Typography from 'antd/es/typography'; +import Switch from 'antd/es/switch'; +import Row from 'antd/es/row'; +import Col from 'antd/es/col'; +import Spin from 'antd/es/spin'; +import Button from 'antd/es/button'; +import theme from 'antd/es/theme'; +import Badge from 'antd/es/badge'; +import Empty from 'antd/es/empty'; +import Alert from 'antd/es/alert'; import { ShoppingCartOutlined, FilterOutlined } from '@ant-design/icons'; import { Link, useLocation, useSearchParams } from 'react-router-dom'; import { Item, FilterOptions, fetchItems, fetchFilterOptions } from './api'; diff --git a/frontend/src/admin/Admin.tsx b/frontend/src/admin/Admin.tsx index f967ef9..7acacc5 100755 --- a/frontend/src/admin/Admin.tsx +++ b/frontend/src/admin/Admin.tsx @@ -1,9 +1,22 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { - Layout, Table, Button, Drawer, Form, Input, InputNumber, Upload, Modal, - Space, Tag, Typography, Switch, message, Image as AntImage, theme, Tabs, - Select -} from 'antd'; +import Layout from 'antd/es/layout'; +import Table from 'antd/es/table'; +import Button from 'antd/es/button'; +import Drawer from 'antd/es/drawer'; +import Form from 'antd/es/form'; +import Input from 'antd/es/input'; +import InputNumber from 'antd/es/input-number'; +import Upload from 'antd/es/upload'; +import Modal from 'antd/es/modal'; +import Space from 'antd/es/space'; +import Tag from 'antd/es/tag'; +import Typography from 'antd/es/typography'; +import Switch from 'antd/es/switch'; +import message from 'antd/es/message'; +import AntImage from 'antd/es/image'; +import theme from 'antd/es/theme'; +import Tabs from 'antd/es/tabs'; +import Select from 'antd/es/select'; import { UploadOutlined, DeleteOutlined } from '@ant-design/icons'; import type { UploadFile } from 'antd/es/upload/interface'; import MDEditor from '@uiw/react-md-editor'; diff --git a/frontend/src/admin/Customers.tsx b/frontend/src/admin/Customers.tsx index f9a9f7f..b798480 100755 --- a/frontend/src/admin/Customers.tsx +++ b/frontend/src/admin/Customers.tsx @@ -1,5 +1,14 @@ import { useEffect, useState } from 'react'; -import { Table, Drawer, Descriptions, Tag, Typography, Spin, Empty, Modal, Button, message } from 'antd'; +import Table from 'antd/es/table'; +import Drawer from 'antd/es/drawer'; +import Descriptions from 'antd/es/descriptions'; +import Tag from 'antd/es/tag'; +import Typography from 'antd/es/typography'; +import Spin from 'antd/es/spin'; +import Empty from 'antd/es/empty'; +import Modal from 'antd/es/modal'; +import Button from 'antd/es/button'; +import message from 'antd/es/message'; import type { ColumnsType } from 'antd/es/table'; import { fetchCustomers, fetchCustomerDetail, fetchReservedItems, releaseReservedItem, diff --git a/frontend/src/admin/Settings.tsx b/frontend/src/admin/Settings.tsx index 9f8b916..8b4c8b7 100644 --- a/frontend/src/admin/Settings.tsx +++ b/frontend/src/admin/Settings.tsx @@ -1,5 +1,10 @@ import { useEffect, useState } from 'react'; -import { Form, InputNumber, Button, Typography, message, Card } from 'antd'; +import Form from 'antd/es/form'; +import InputNumber from 'antd/es/input-number'; +import Button from 'antd/es/button'; +import Typography from 'antd/es/typography'; +import message from 'antd/es/message'; +import Card from 'antd/es/card'; import { fetchAdminSettings, updateAdminSettings } from './adminSettingsApi'; const { Title, Text } = Typography; diff --git a/frontend/src/cart/Cart.tsx b/frontend/src/cart/Cart.tsx index 39440e1..b968c36 100644 --- a/frontend/src/cart/Cart.tsx +++ b/frontend/src/cart/Cart.tsx @@ -1,8 +1,20 @@ import { useEffect, useState } from 'react'; -import { - Layout, Typography, List, Button, Empty, Card, Radio, Form, Input, - Checkbox, Modal, message, Tag, Spin, theme, Space -} from 'antd'; +import Layout from 'antd/es/layout'; +import Typography from 'antd/es/typography'; +import List from 'antd/es/list'; +import Button from 'antd/es/button'; +import Empty from 'antd/es/empty'; +import Card from 'antd/es/card'; +import Radio from 'antd/es/radio'; +import Form from 'antd/es/form'; +import Input from 'antd/es/input'; +import Checkbox from 'antd/es/checkbox'; +import Modal from 'antd/es/modal'; +import message from 'antd/es/message'; +import Tag from 'antd/es/tag'; +import Spin from 'antd/es/spin'; +import theme from 'antd/es/theme'; +import Space from 'antd/es/space'; import { ArrowLeftOutlined } from '@ant-design/icons'; import { useNavigate, Link } from 'react-router-dom'; import { @@ -109,9 +121,9 @@ export default function Cart() { useEffect(() => { if (!paypalReady || !selectedAddressId || items.length === 0) return; const container = document.getElementById('paypal-cart-buttons'); - if (!container || !(window as any).paypal) return; + if (!container || !window.paypal) return; container.innerHTML = ''; - (window as any).paypal.Buttons({ + window.paypal.Buttons({ createOrder: async () => { const { orderID } = await createCartPaypalOrder(selectedAddressId); return orderID; diff --git a/frontend/src/components/ItemCard.tsx b/frontend/src/components/ItemCard.tsx index 77c196a..67f7fbd 100755 --- a/frontend/src/components/ItemCard.tsx +++ b/frontend/src/components/ItemCard.tsx @@ -1,5 +1,13 @@ import { useState, useRef } from 'react'; -import { Card, Badge, Typography, Carousel, Button, message, Tag, Modal, Tooltip } from 'antd'; +import Card from 'antd/es/card'; +import Badge from 'antd/es/badge'; +import Typography from 'antd/es/typography'; +import Carousel from 'antd/es/carousel'; +import Button from 'antd/es/button'; +import message from 'antd/es/message'; +import Tag from 'antd/es/tag'; +import Modal from 'antd/es/modal'; +import Tooltip from 'antd/es/tooltip'; import { LeftOutlined, RightOutlined, HeartOutlined, HeartFilled } from '@ant-design/icons'; import type { CarouselRef } from 'antd/es/carousel'; import { Item } from '../api'; diff --git a/frontend/src/customer/Account.tsx b/frontend/src/customer/Account.tsx index ccb7441..25be6aa 100755 --- a/frontend/src/customer/Account.tsx +++ b/frontend/src/customer/Account.tsx @@ -1,5 +1,12 @@ import { useEffect, useState } from 'react'; -import { Typography, Switch, Button, Table, Modal, message, Space, Divider } from 'antd'; +import Typography from 'antd/es/typography'; +import Switch from 'antd/es/switch'; +import Button from 'antd/es/button'; +import Table from 'antd/es/table'; +import Modal from 'antd/es/modal'; +import message from 'antd/es/message'; +import Space from 'antd/es/space'; +import Divider from 'antd/es/divider'; import { useNavigate } from 'react-router-dom'; import { fetchMyOrders, OrderHistoryItem, updateConsent, exportMyData, deleteMyAccount } from './customerApi'; import { setFavoriteAlerts } from './favoritesApi'; diff --git a/frontend/src/customer/PrivacyPolicy.tsx b/frontend/src/customer/PrivacyPolicy.tsx index 7edb54f..ef4d23d 100755 --- a/frontend/src/customer/PrivacyPolicy.tsx +++ b/frontend/src/customer/PrivacyPolicy.tsx @@ -1,4 +1,5 @@ -import { Typography, Card } from 'antd'; +import Typography from 'antd/es/typography'; +import Card from 'antd/es/card'; const { Title, Paragraph } = Typography; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index c59e085..273675d 100755 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,7 +2,8 @@ import React, { useEffect, useState } from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter, Routes, Route, useLocation, useNavigate } from 'react-router-dom'; import type { Location } from 'react-router-dom'; -import { ConfigProvider, theme as antdTheme } from 'antd'; +import ConfigProvider from 'antd/es/config-provider'; +import antdTheme from 'antd/es/theme'; import 'antd/dist/reset.css'; import Button from 'antd/es/button'; import ModalDialog from 'antd/es/modal'; diff --git a/frontend/src/paypal.ts b/frontend/src/paypal.ts index 7aeb5c5..9207089 100755 --- a/frontend/src/paypal.ts +++ b/frontend/src/paypal.ts @@ -1,7 +1,33 @@ +// The PayPal JS SDK is injected at runtime by the script tag below, so there is +// no package to import types from. This declares the sliver of it this app +// actually uses, rather than repeating `(window as any).paypal` at each call +// site — three casts that each silently opted out of type checking. +// +// Deliberately narrow: it describes what is called here, not the whole SDK. A +// wider guess would be fiction, and a wrong shape typed confidently is worse +// than an honest `unknown`. +export interface PaypalButtonsConfig { + createOrder: () => Promise; + onApprove: (data: { orderID: string }) => Promise | void; + onError: (err: unknown) => void; +} + +export interface PaypalSdk { + Buttons: (config: PaypalButtonsConfig) => { render: (selector: string) => void }; +} + +declare global { + interface Window { + // Absent until the SDK script has loaded, which is what loadPaypalSdk and + // every caller has to check before using it. + paypal?: PaypalSdk; + } +} + let loadPromise: Promise | null = null; export function loadPaypalSdk(clientId: string, currency: string): Promise { - if ((window as any).paypal) return Promise.resolve(); + if (window.paypal) return Promise.resolve(); if (loadPromise) return loadPromise; loadPromise = new Promise((resolve, reject) => { const script = document.createElement('script');