92d7dfdef3e897bcbc8158a6976ee3197181e6de
Closes WP-10 of the audit: M10-M16, M18, M19, M20 and the client half of H11. Steps 5 and 7 of the package were already done in WP-1. An expired token left the app half signed in: the interceptor only wrote to the console, so the nav still showed the user's name while every request failed. A 401 now clears the token and navigates to sign-in - a full navigation rather than a router push, so in-memory state and open sockets go with the page, and the API client does not have to reach into the stores and join an import cycle with them. A 401 from /auth/login is excluded: there, it is an answer. Signing out disconnected the notifications socket and nothing else. Chat, versus and game night stayed open holding a token that had just been removed, and the friends and game stores kept their contents, so signing in as someone else on the same tab showed the previous person's friend list. Teardown now lives in one place, because the failure mode was precisely that adding a socket did not mean remembering to close it. All four connect() guards tested `.connected` rather than existence, so a socket that was mid-reconnect was replaced without being closed and the orphan went on handling every event a second time. disconnect() likewise cleared only the socket, leaving matchId, result and score behind - the next match opened showing the previous match's completion modal. Effects depended on the whole `user` object, which loadUser() rebuilds on every call, so they re-ran when nothing had changed. In AsyncVersusGame that effect's cleanup calls resetGame(), so reloading a profile mid-match reset the board. They depend on the id now. The same effect also reset the store while its fetch was in flight, so init() then wrote the match back into what had just been cleared; it aborts before resetting. Page reads take an AbortSignal and their effects abort on unmount. Several already had a `cancelled` flag, which stops the state write but lets the request run to completion; the flag stays to guard the `finally` blocks. Undo was fire-and-forget: rapid clicks put overlapping POSTs in flight against a server that removes one link per call, and a failure was logged while the local chain had already been shortened. Undos are serialised and the server's chain - which it was already returning and the client was discarding - is now authoritative. The runtime API setting was substituted into a JS string literal by envsubst, which does no escaping, so a value containing a quote ended the literal and the remainder executed on every page load. It is escaped at render time (verified under busybox with the production values and with an injection payload, which comes out inert), and a malformed value is rejected on read. Note this deliberately does not assert same origin, as the plan suggested: the frontend and API are served from different subdomains in production, so that assertion would break the deployment. The Endless difficulty selector is removed rather than wired. Difficulty is derived from cast analysis on the server and Endless builds its pairs in the browser, so there was nothing to wire it to - it silently did nothing while telling the player their choice mattered. Verified in the running app: an invalid token clears and redirects to /login?expired=1 without looping, while a wrong password shows "Invalid email or password" on the form and stays put; sign-out clears the token, returns to sign-in and leaves no trace of the previous user; Endless renders without the control and keeps its real filters. No console errors beyond the 401s those checks deliberately provoked.
React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- @vitejs/plugin-react uses Babel (or oxc when used in rolldown-vite) for Fast Refresh
- @vitejs/plugin-react-swc uses SWC for Fast Refresh
React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see this documentation.
Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
You can also install eslint-plugin-react-x and eslint-plugin-react-dom for React-specific lint rules:
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
Languages
TypeScript
97.7%
CSS
0.8%
JavaScript
0.7%
Shell
0.4%
Dockerfile
0.2%
Other
0.2%