Skip to content

Security hardening, bug fixes, tooling, and a cartoon sticker UI redesign - #3

Merged
Meinianda-L merged 8 commits into
mainfrom
claude/dazzling-lovelace-9cma60
Oct 4, 2026
Merged

Meinianda-L merged 8 commits into
mainfrom
claude/dazzling-lovelace-9cma60

Conversation

@Meinianda-L

@Meinianda-L Meinianda-L commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

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

Page Before → After
Home Home
Menu Menu
Item details Item details
Checkout Checkout
User dashboard Dashboard
Home (mobile) Menu (mobile)
Home mobile Menu mobile

New pages:

Tray Order ticket Item details (mobile) Tray (mobile)
Tray Ticket Item mobile Tray mobile

Security fixes

  • Prices came from the client. Editing the cart in sessionStorage (e.g. basePrice: "0.01") changed what the server charged. Prices are now computed from the database.
  • Negative amounts minted balance. One item at -1 plus a cheaper one at 1, paid with balance, increased the user's balance. Each amount must now be a positive integer.
  • Free Pay Later orders. Anyone could switch an unpaid order to 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.
  • Order data leak. getOrder returned 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.
  • Anyone could cancel anyone's unpaid order. Cancelling now checks ownership.
  • Balance double spending. Concurrent balance payments could spend the same balance twice. Balance and points updates now lock the user row.
  • Login. Fixed an open redirect, added an OAuth state nonce (login CSRF), and made the access cookie HTTP-only, SameSite=Lax and Secure on HTTPS. The token only identifies the user; permissions are always read from the database.
  • Payment webhooks. WeChat Pay notifications now check the result code, merchant ID and amount, with a constant-time signature check. Stripe top-ups require payment_status = paid. All fulfilment is idempotent. Payments for orders that no longer exist are refunded automatically and logged.
  • Internal helpers were moved out of 'use server' files and marked server-only. Server action inputs are validated with zod. Coupon checks and order creation are rate limited.
  • Upgraded Next.js (16.3.8), React, sharp and Prisma. npm audit drops from 30 to 9 findings, and the remaining ones are in build-time tooling only.

Bug fixes

  • SMS notifications were sent with the acting admin's OneLogin token. They now use the recipient's token, and balance top-ups now send a notification.
  • Pruning unpaid orders now returns their reserved inventory and coupon uses, and expires their Stripe session.
  • Refunds can no longer run twice; they take back points and restore inventory for drinks that weren't made.
  • Denying OneLogin consent caused a 500 error.
  • Pay Later reminders compared only the day of the month.
  • Settings were re-initialised on every read (about 15 queries each time), and a missing setting crashed the app.
  • The admin user page paginated the admin's own orders instead of the user's.
  • Polling intervals were never cleared.
  • The Stripe client is now created on first use, so the app and next build work without STRIPE_KEY.
  • The server time zone now defaults to Asia/Shanghai.
  • Blocked users can no longer order, pay or top up, and admins can now block users from the user page.

Tooling

  • ESLint 9 flat config (next lint was removed in Next 16).
  • Vitest unit tests (49), including database tests for concurrent balance updates.
  • GitHub Actions running lint, typecheck, tests and build against PostgreSQL.
  • Prisma migrations are now tracked with a 0_init baseline. The generated client is no longer committed; it is generated on install.
  • npm run db:seed adds demo data for development.

UI

  • A cartoon sticker style that keeps the original cream and yellow palette: thick outlines, hard shadows, a self-hosted rounded font (ZCOOL KuaiLe), and a hand-drawn beluga mascot. Dark mode is supported.
  • Inline cart warnings replace the hover-only popovers, which didn't work on phones.
  • The design rules are in docs/DESIGN.md, and the README now has a screenshots section.

Deployment notes

  1. Existing database: run npx prisma migrate resolve --applied 0_init once, then use npm run db:migrate for future migrations.
  2. HOST must be https://… for the login cookie to be marked Secure.
  3. Cron jobs can keep using ?key=; Authorization: Bearer <CRON_KEY> is now also supported.
  4. Users who are already logged in stay logged in.

Testing

  • CI (lint, typecheck, 49 tests, build against a fresh PostgreSQL) passes.
  • End-to-end checks against a production build passed (18): price tampering, negative amounts, guest order access, login CSRF, cookie flags, balance payment and refund, revoked admin access, and blocked users.
  • WeChat notification checks passed (14): bad signature, wrong merchant, wrong amount, idempotency, orphan payments, concurrent top-ups, cron auth, and inventory and coupon restore on prune.

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.
@Meinianda-L
Meinianda-L merged commit c030f95 into main Oct 4, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant