feat: add Vue SPA frontend for browsing seasons, divisions, and players#387
Open
feat: add Vue SPA frontend for browsing seasons, divisions, and players#387
Conversation
Add a Vue 3 single-page application accessible at /app alongside the
existing server-rendered Pug interface, which remains fully intact.
Framework choice — Vue 3
- Template syntax (HTML-like directives) is the gentlest transition from
Pug for a team already working in server-rendered HTML
- Vue 3 + Composition API has first-class TypeScript support, consistent
with the rest of the codebase
- Vite is Vue's official build tool: fast HMR in dev, optimised output
in production, minimal configuration needed
- vue-router 4 matches the nested season/division URL structure already
used by the SSR layer
- Smaller runtime footprint than Angular; more opinionated structure than
plain Preact/Solid which helps onboarding
What was added
- client/ — Vue 3 source tree (index.html, vite.config.ts, tsconfig.json)
- src/main.ts — app entry point
- src/App.vue — root component with NavBar + <RouterView>
- src/router/index.ts — routes: /, /seasons, /divisions, /divisions/:id,
/seasons/:s/divisions/:d/players
- src/api/index.ts — thin fetch wrappers for the existing /api/v1/* endpoints
- src/components/NavBar.vue — responsive Bulma navbar, links to Steam auth
- src/views/ — HomeView, SeasonsView, DivisionsView, DivisionView, PlayersView
- package.json: build:server / build:client / dev:client scripts;
build now runs both tsc and vite; vue, vue-router, vite, @vitejs/plugin-vue,
@vue/tsconfig added as devDependencies
- src/server.ts: serves dist/public/ at /app (static) and catches all
/app/* paths with index.html for client-side routing
|
🚅 Deployed to the server-pr-387 environment in rd2l
|
The build stage only copied src/ and tsconfig.json, so the Vite config and Vue source in client/ were absent when `npm run build` ran, causing esbuild to fail with "Could not resolve /app/client/vite.config.ts". Adding `COPY client ./client` before the build step makes the full source available. The built SPA lands in dist/public/ which is already covered by the existing `COPY --from=builder /app/dist ./dist` in the run stage.
CodeQL flagged /app/*splat as performing a file system access (sendFile) without rate limiting, making it a potential DoS vector. Install express-rate-limit and apply a dedicated limiter (120 req/min per IP) to the catch-all handler. Static assets under /app are served by express.static and are not affected — the OS page-cache and Express's built-in ETag/304 handling already make those cheap. Closes CodeQL alert: Missing rate limiting (src/server.ts:338)
Two additional route groups perform expensive operations per request with no rate limiting: - /auth/steam and /auth/steam/return trigger outbound calls to the Steam OpenID / Web API; limited to 20 requests per 15 min per IP. - /rules and /inhouserules call fs.readFile on every request; limited to 60 requests per minute per IP. Uses the same express-rate-limit instance already introduced for the SPA catch-all; only new limiter instances are added for appropriate thresholds per route group.
- fix(api/divisions): rename response key divisionAdmins → admins to
match the DivisionDetail client type and DivisionView template
- feat(api/me): add GET /api/v1/me — returns { loggedIn: false } for
anonymous visitors, or { loggedIn, steamId, displayName, avatar,
isAdmin, activityCheckRequired } for authenticated users
- feat(api/csrf): add GET /api/v1/csrf — returns { token } so the SPA
can obtain a CSRF token before issuing POST requests
- feat(server): accept CSRF token via x-csrf-token request header in
addition to req.body._csrf; this lets the SPA send JSON POSTs without
needing to embed the token in the request body
- feat(client/api): add MeResponse union type, api.me(), and api.post()
helper — post() fetches a fresh CSRF token then performs a JSON POST
with the x-csrf-token header automatically set
- feat(client/NavBar): call api.me() on mount; show avatar + display
name + Sign out when logged in, Sign in with Steam when anonymous;
auth buttons are hidden until the /api/v1/me response arrives
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 adds a new Vue 3 single-page application (SPA) frontend to the RD2L project, providing a user-friendly interface for browsing seasons, divisions, and player information. The SPA is served at
/appand integrates with the existing Express backend API.Key Changes
New Vue SPA Structure: Created a complete Vue 3 application with TypeScript support, Vue Router for client-side navigation, and Bulma CSS framework for styling
client/src/main.ts- Application entry pointclient/src/App.vue- Root component with navigation bar and router outletclient/index.html- HTML template with Bulma and FontAwesome CDN linksclient/tsconfig.json- TypeScript configuration for Vueclient/vite.config.ts- Vite build configuration with relative asset paths for/appmountingAPI Client Layer: Created
client/src/api/index.tswith TypeScript interfaces and thin wrapper functions for backend endpointsSeason,Division,DivisionDetail,Player, andCaptainViews/Pages:
HomeView.vue- Landing page with navigation cards to seasons, divisions, and rulesSeasonsView.vue- Table listing all seasonsDivisionsView.vue- Grid of division cards with Discord linksDivisionView.vue- Detail page showing division admins and linksPlayersView.vue- Tabbed interface showing all players or captains with MMR dataComponents:
NavBar.vue- Navigation bar with responsive mobile menu and Steam login buttonRouting:
client/src/router/index.tswith routes for home, seasons, divisions, and players pagesServer Integration:
src/server.tsto serve static SPA assets from/appand handle client-side routing with a catch-all that returnsindex.htmlpackage.jsonfor building both server and clientNotable Implementation Details
base: './'in Vite config) to work correctly when served from/app/*sub-paths/profile/{steam_id}https://claude.ai/code/session_01H9bg5MmtXCkaQe2SUyAKLu