Repository navigation
Security hardening, bug fixes, tooling, and a cartoon sticker UI redesign - #3
Merged
Merged
Conversation
Ordering - Price every order on the server from the database; client-supplied prices, foreign options and non-positive/fractional amounts are rejected - Validate all server action arguments with zod - Only the owner (or the guest browser that placed it, via a signed HTTP-only cookie) can view, cancel or change an order; admin sees all - Orders returned to the browser only carry the user's id/name/pinyin - Pay Later orders cannot be created by switching an unpaid order's payment method; customers cannot cancel Pay Later orders - Pay Later "no unpaid orders" rule is re-checked under a row lock Money - Balance and points changes lock the user row (no double spending) - Coupon uses are claimed atomically - Order payment, balance top-up and refunds are idempotent - WeChat Pay notifications verify the result code, merchant and amount with a constant-time signature check - Stripe top-ups require payment_status=paid; sessions expire in 31 min - Payments for orders/top-ups that no longer exist are refunded and logged; refunds take back points and restore unprepared inventory - Pruning unpaid orders returns inventory and coupon uses Login and access - Open redirect fixed; OAuth state carries a nonce bound to a cookie - Access token is HTTP-only, SameSite=Lax, Secure on HTTPS, and only identifies the user; permissions are always read from the database - Blocked users can no longer order, pay or top up; admins can block - Internal helpers (fulfillment, notifications, OneLogin tokens) moved out of 'use server' modules and marked server-only Bugs - SMS notifications were sent with the acting admin's token; now use the recipient's token, and top-ups send balanceToppedUp - Denying OneLogin consent crashed (relative redirect) - Pay Later reminders compared only the day of month - Settings re-initialized on every read; missing settings crashed - Admin user page paginated the admin's own orders - Polling intervals were never cleared - Server time zone defaults to Asia/Shanghai - Cron and printer keys compared in constant time; cron accepts a Bearer header; settings, coupons, ad URLs and uploads are validated - Upgrade next 16.3.8, react 19.3, sharp 0.35.5, prisma 7.10
- ESLint 9 flat config (next lint was removed in Next 16) - Vitest unit tests for pricing, redirects, scheduling, validation, settings and rate limiting, plus database tests for balance locking - GitHub Actions: lint, typecheck, test and build against PostgreSQL - Track Prisma migrations with a 0_init baseline; the generated client is no longer committed (it is generated on install) - Development seed script with demo menu and images - README: migrations, seed, checks, cron auth, TZ
- Warm cream/espresso/caramel palette, card surfaces and Flowbite theme overrides (buttons, modals, sidebar) applied across the app - Sticky navbar; home page hero with live store status and opening hours - Menu: sticky category chips, item cards with sale badges and quick add - Item sheet: large image, option chips with price deltas, sticky bar - Carts share one availability hook and show checkout issues inline instead of in hover-only popovers (unusable on phones) - Mobile cart: floating bar with count badge and a bottom sheet - Checkout: payment and pick-up tiles, sticky order summary - Order details: order number ticket with progress steps and wait time - Seed: demo ad banner and hex tag colors - Fix zh-CN sale label missing its percent sign
- docs/screenshots: before/after comparisons of the refreshed UI - AGENTS.md / CLAUDE.md are generated by `next dev` (Next.js 16); committing them keeps the working tree clean
The previous refresh leaned on gradients, glowing blobs, big rounded shadowed cards and pill buttons. Rework it around the café itself: - Palette from the logo: paper background, ink text, brick red accent; no gradients, blurs or drop shadows, small radii, hairline rules - Self-hosted Newsreader (OFL) for prices, order numbers and the home title, so nothing depends on an external font CDN - Home: plain status line, real "today's menu" card with dotted leaders - Menu: category tabs with underline, items as menu rows with leaders and an outlined add button; sale shown as text, not a sticker - Item panel: image beside details on desktop, rectangular option buttons with price deltas - Cart, checkout summary and order page styled as a receipt / ticket stub with a perforated tear line; checkout uses grouped radio lists - Mobile cart is a flush bottom bar; admin badges, sidebar card and charts follow the new palette - Fix home page overflowing horizontally on phones - Refresh docs/screenshots (original / previous / new)
Keep the original cute, cartoon feel and rebuild it as a consistent sticker style instead of the previous experiments: - Butter-cream palette from the original, defined as CSS variables with a dark mode; thick ink outlines and hard offset shadows, pressable buttons, polka-dot background, no gradients or blurs - ZCOOL KuaiLe (self-hosted, OFL) for headings, names and prices - A hand-drawn beluga mascot (happy / sleepy / cheering) on the home page, empty carts and the order ticket - Home: greeting bubble, opening-hours sticker, today's menu strip - Menu: coloured category chips, sticker item cards with price tags - Item sheet, tray, checkout and order ticket rebuilt in the same style; cart issues are shown inline instead of hover-only popovers - Flowbite theme (buttons, inputs, modals, badges, sidebar) and the admin pages follow the new style - docs/DESIGN.md describes the rules; screenshots updated - Stop tracking the AGENTS.md / CLAUDE.md files that next dev generates
The Stripe client was created when its module loaded, so `next build` failed while collecting route configuration whenever STRIPE_KEY was not set (as in CI). Create it on first use instead; only Stripe operations fail when it is not configured. Also run the CI database health check as the whale user.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR fixes several ways to get drinks or balance for free, closes a few data leaks, fixes functional bugs, adds tests and CI, and redesigns the UI in a cartoon sticker style built around a beluga mascot.
Before and after
New pages:
Security fixes
basePrice: "0.01") changed what the server charged. Prices are now computed from the database.-1plus a cheaper one at1, paid with balance, increased the user's balance. Each amount must now be a positive integer.payLater, which put it in the staff queue without ever being paid or pruned. Changing the payment method now checks ownership and only allows online methods.getOrderreturned any order, including the customer's phone number, balance and permissions. Orders are now only visible to their owner (guests via a signed HTTP-only cookie) and to admins, and only expose the user's id, name and pinyin.statenonce (login CSRF), and made the access cookie HTTP-only,SameSite=LaxandSecureon HTTPS. The token only identifies the user; permissions are always read from the database.payment_status = paid. All fulfilment is idempotent. Payments for orders that no longer exist are refunded automatically and logged.'use server'files and markedserver-only. Server action inputs are validated with zod. Coupon checks and order creation are rate limited.npm auditdrops from 30 to 9 findings, and the remaining ones are in build-time tooling only.Bug fixes
next buildwork withoutSTRIPE_KEY.Asia/Shanghai.Tooling
next lintwas removed in Next 16).0_initbaseline. The generated client is no longer committed; it is generated on install.npm run db:seedadds demo data for development.UI
docs/DESIGN.md, and the README now has a screenshots section.Deployment notes
npx prisma migrate resolve --applied 0_initonce, then usenpm run db:migratefor future migrations.HOSTmust behttps://…for the login cookie to be markedSecure.?key=;Authorization: Bearer <CRON_KEY>is now also supported.Testing