Migrate from server-side rendering to Vue.js frontend#384
Migrate from server-side rendering to Vue.js frontend#384
Conversation
| app.get('*', (_req, res) => { | ||
| res.sendFile(path.join(__dirname, '..', 'client', 'dist', 'index.html')) | ||
| }) |
Check failure
Code scanning / CodeQL
Missing rate limiting High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 days ago
In general, the fix is to introduce a rate‑limiting middleware for routes that perform relatively expensive operations (here, serving the SPA HTML via sendFile) and apply that middleware to those routes. For an Express app, a common approach is to use a library such as express-rate-limit to define limits (e.g., max N requests per IP per unit time) and then attach that limiter either globally or to specific routes.
For this codebase, the least invasive fix that preserves existing behavior is:
- Import
express-rate-limitat the top ofsrc/server.ts. - Define a limiter specifically for the SPA catch‑all route, with reasonable defaults (for instance, 100 requests per 15 minutes per IP).
- Apply this limiter as middleware to the
app.get('*', ...)route, leaving the rest of the routing logic unchanged.
Concretely:
- At the top of
src/server.ts, add an import forexpress-rate-limit(using the default import to match the existing ES module style imports). - Near the SPA routing section (before line 262), create a
spaRateLimiterusingrateLimit({ windowMs: ..., max: ... }). - Change
app.get('*', (_req, res) => { ... })toapp.get('*', spaRateLimiter, (_req, res) => { ... }).
This localizes rate‑limiting to the highlighted filesystem‑accessing route and does not affect other API endpoints.
| @@ -12,6 +12,7 @@ | ||
| import passport from 'passport' | ||
| import passportSteam from 'passport-steam' | ||
| import pg from 'pg' | ||
| import rateLimit from 'express-rate-limit' | ||
| const pool = new pg.Pool({ | ||
| ...config.db, | ||
| user: config.db.user || undefined, | ||
| @@ -261,7 +262,13 @@ | ||
|
|
||
| // Serve the Vue SPA for all non-API, non-asset routes | ||
| app.use(express.static(path.join(__dirname, '..', 'client', 'dist'))) | ||
| app.get('*', (_req, res) => { | ||
|
|
||
| const spaRateLimiter = rateLimit({ | ||
| windowMs: 15 * 60 * 1000, // 15 minutes | ||
| max: 100, // limit each IP to 100 SPA index requests per windowMs | ||
| }) | ||
|
|
||
| app.get('*', spaRateLimiter, (_req, res) => { | ||
| res.sendFile(path.join(__dirname, '..', 'client', 'dist', 'index.html')) | ||
| }) | ||
|
|
| @@ -39,7 +39,8 @@ | ||
| "pg-sql": "^1.1.0", | ||
| "redirect-https": "^1.3.1", | ||
| "shortid": "^2.2.17", | ||
| "swiss-pairing": "^1.4.3" | ||
| "swiss-pairing": "^1.4.3", | ||
| "express-rate-limit": "^8.2.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@eslint/js": "^10.0.0", |
| Package | Version | Security advisories |
| express-rate-limit (npm) | 8.2.1 | None |
Implements the frontend framework migration from server-rendered Pug templates to a Vue 3 SPA with Vite, following an incremental strategy that keeps the existing Express/Pug stack functional. Backend changes: - Add 13 new API controllers under src/api/ exposing all data needed by the SPA (me, csrf, rules, profiles, teams, series, registration, admin, seasons-admin, divisions-admin, players-admin) - Modify CSRF middleware to accept X-CSRF-Token request header alongside existing _csrf form body field - Wire all new routes in src/server.ts - Add SPA static serving + catch-all fallback route - Update package.json with build:client and dev:client scripts - Exclude client/ from server tsconfig.json compilation Frontend (client/): - Vue 3 + Vite + TypeScript scaffold - Vue Router with all page routes (public, auth-required, admin) - Pinia stores: auth (user session + CSRF token), season (active season) - API client wrapper with automatic X-CSRF-Token header injection - Bulma CSS via npm (replaces CDN), dynamic bulmaswatch theme switching - 25+ view components covering all app features - Admin CRUD views for seasons, divisions, players, teams, series, roles, admins, banned players, and IP address lookup https://claude.ai/code/session_019yX1hLw84s21udY91pQypp
- Delete all src/pages/ controllers and src/templates/ Pug files - Remove pug and pug-tree dependencies - Remove TemplatesConfig from config types and config factory - Add draftsheet CSV endpoint to players-admin API - Update Dockerfile to build and copy client/dist - Wire SPA static serving + catch-all in server.ts https://claude.ai/code/session_019yX1hLw84s21udY91pQypp
3c0407c to
ccb5650
Compare
Express 5 / path-to-regexp v8 no longer accepts the :param? optional syntax. Split standings and matchups routes into two registrations each (with and without :round) to restore compatibility. https://claude.ai/code/session_019yX1hLw84s21udY91pQypp
This PR represents a major architectural shift from a server-side rendered application using Pug templates to a modern Vue.js single-page application (SPA) with a REST API backend.
Summary
The application has been refactored to separate concerns between frontend and backend. The server now provides REST API endpoints instead of rendering HTML, while a new Vue.js client application handles all UI rendering and user interactions.
Key Changes
Backend (src/)
src/api/:series.ts,teams.ts,profiles.ts,registration.ts,admin.ts,players-admin.ts,seasons-admin.ts,divisions-admin.ts,me.ts,csrf.ts,rules.tsFrontend (client/)
Configuration
Implementation Details
https://claude.ai/code/session_019yX1hLw84s21udY91pQypp