printCounts/crafts/logistics were materialized in first-encounter order
(originGoods had no orderBy), so the H5 print/craft/logistics button
groups rendered in an unstable, per-family arbitrary order. Add
OPTION_DISPLAY_ORDER (单面→双面, 烫画→直喷→不打印, 不包邮→包邮) and
stable-sort the aggregated arrays before materializing; out-of-vocabulary
free-text labels (e.g. 海运) sink to the end preserving encounter order.
Fix traversal order with orderBy id asc so recomputes are fully
deterministic. Add recompute:families script for one-off re-materialize.
- Add User.role (enum Role/ADMIN) and User.tokenVersion with migration
- Login now issues short-lived access token (30m default) + 7d refresh
token, both embedding tokenVersion and a typ discriminator
- Tokens delivered via HttpOnly SameSite cookies (ir_at, ir_rt scoped
to /auth); refresh token never leaves the cookie
- New endpoints: POST /auth/refresh (rotation), GET /auth/me,
POST /auth/logout (bumps tokenVersion, revoking all tokens)
- JWT strategy accepts bearer or cookie, rejects refresh tokens, and
verifies tokenVersion + user existence on every request
- Global RolesGuard: authenticated routes require ADMIN unless widened
via @Roles(...)
- Admin SPA: session fully cookie-based, no token in localStorage;
router guard restores session via /auth/me; axios auto-refreshes once
on 401; stale localStorage keys cleaned up
- Lock public registration to first-user bootstrap (403 afterwards)
- Require JwtAuthGuard on upload + whitelist png/jpg/webp/gif (SVG/XSS blocked)
- Add global throttling (login/register 5/min, upload 10/min)
- Add helmet security headers; serve uploads with nosniff
- Replace permissive CORS (origin:true+credentials) with CORS_ORIGINS whitelist
- Disable Swagger outside development; sanitize 500 error responses
- Enforce 32+ char JWT_SECRET; make token expiry configurable (TOKEN_EXPIRES_IN)
- Re-check user in DB on every JWT validation (revocation on user delete)
- Dummy bcrypt compare to prevent login user-enumeration via timing
- Map malformed BigInt inputs to 400 instead of 500
- Widen .gitignore to .env* and add apps/api/.env.example
- Disable Nuxt devtools and sourcemaps