First commit
This commit is contained in:
152
AGENTS.md
Normal file
152
AGENTS.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Mathew — a wiki for mathematics
|
||||
|
||||
A collaborative wiki of math concepts. Articles are written in Markdown with LaTeX
|
||||
(KaTeX) and stored in MongoDB behind an Express API; a Nuxt 4 app renders and edits them.
|
||||
|
||||
```
|
||||
mathew/
|
||||
├── dev.sh # starts/stops both dev servers together
|
||||
├── backend/ Express 5 + Mongoose API (port 5000)
|
||||
└── frontend/ Nuxt 4 / Vue 3 wiki UI (port 3000)
|
||||
```
|
||||
|
||||
## Running the project
|
||||
|
||||
Requires MongoDB on `localhost:27017` (database `mathew`).
|
||||
|
||||
```bash
|
||||
./dev.sh # both dev servers in foreground (Ctrl+C stops both)
|
||||
./dev.sh start|stop|restart|status
|
||||
./dev.sh logs [all|backend|frontend]
|
||||
```
|
||||
|
||||
Or per-service: `npm run dev` inside `backend/` or `frontend/`
|
||||
(backend uses `node --watch`; backend has no build step).
|
||||
|
||||
Seed sample articles: `npm run seed` in `backend/`.
|
||||
|
||||
Environment: `backend/.env` (`PORT=5000`, `MONGO_URI=mongodb://127.0.0.1:27017/mathew`,
|
||||
`JWT_SECRET` — signs session tokens, `AUTH_TOKEN_TTL` — how long one lasts, default `7d`;
|
||||
see `.env.example`). Without `JWT_SECRET` the API warns and uses an insecure dev key.
|
||||
Frontend API URL comes from `NUXT_PUBLIC_API_BASE`
|
||||
(default `http://localhost:5000`, read via `runtimeConfig.public.apiBase`).
|
||||
|
||||
There is **no test suite, linter, or typecheck script** configured — don't invent commands.
|
||||
|
||||
## Backend (`backend/`)
|
||||
|
||||
Plain JavaScript, ESM (`"type": "module"`, `.js` files with relative `.js` import specifiers).
|
||||
|
||||
- `src/index.js` — app setup: CORS, JSON body, `/health`, routers, 404 handler, central
|
||||
error handler (Mongoose `ValidationError`/`CastError` → 400), graceful shutdown. After
|
||||
the connection it calls `User.ensureAnAdmin()` so somebody can always open the dashboard.
|
||||
- `src/config/db.js` — `connectDB` / `disconnectDB`.
|
||||
- `src/models/` — Mongoose schemas with `timestamps: true`. `Article` (slug, title,
|
||||
summary, content, tags + text index), `Item` (simple demo resource),
|
||||
`User` (username + `passwordHash`, `select: false`; `setPassword`/`checkPassword`
|
||||
via bcrypt, `toPublic()` for responses, and `credentialProblem()` as the single
|
||||
place account rules are checked). The profile picture lives on `User.avatar` as a
|
||||
small data URL — no file storage. `User.role` is `'admin'` or `'member'`
|
||||
(`ROLES`); `User.hasNone()` and `User.ensureAnAdmin()` cover who gets the
|
||||
former — see the roles gotcha below.
|
||||
- `src/routes/` — one Express `Router` per resource, mounted in `index.js`.
|
||||
- `src/routes/auth.js` — `POST /api/auth/register` (creates the account and returns
|
||||
its session; the wiki's **first** account is created as an admin),
|
||||
`POST /api/auth/login`, `GET /api/auth/me` (who a token belongs to).
|
||||
Login answers the same way to a wrong name and a wrong password. Account updates,
|
||||
all behind `requireAuth`: `PUT /api/auth/me` (rename), `PUT /api/auth/password`
|
||||
(needs the current one), `PUT /api/auth/avatar` (data URL, `""` clears).
|
||||
- `src/routes/admin.js` — mounted at `/api/admin`, everything behind
|
||||
`requireAuth, requireAdmin`: `GET /api/admin/users` (every account, newest
|
||||
first, with `createdAt`), `PUT /api/admin/users/:id/role` (`role` is `'admin'`
|
||||
or `'member'`; an admin's own role is the one they cannot change), and
|
||||
`DELETE /api/admin/users/:id` (closes an account for good — never their own,
|
||||
by the same rule; the articles it wrote stay, since articles name no author).
|
||||
- `src/middleware/auth.js` — `signToken(user)`, `requireAuth`, which reads
|
||||
`Authorization: Bearer <token>` and sets `req.user = { id, username }`; anything
|
||||
else is answered with 401 `{ message }`. `requireAdmin` runs after it and reads
|
||||
the role off the **account** (403 `{ message }` for a member), so removing the
|
||||
role works the next time the dashboard is asked for.
|
||||
- `src/seed.js` — sample articles.
|
||||
|
||||
Conventions to follow:
|
||||
|
||||
- Every handler is `async` with `try { ... } catch (err) { next(err); }`; the central
|
||||
error handler in `index.js` formats responses (`{ message }` everywhere).
|
||||
- Articles are addressed by **slug**, not by Mongo id (`/api/articles/slug/:slug`).
|
||||
`slugify()` / `uniqueSlug()` (in `routes/articles.js`) generate uniquified slugs
|
||||
(`-2`, `-3`, ... suffixes); POST requires `title`, PUT re-slugs when the title changes.
|
||||
- The list endpoint (`GET /api/articles`) selects `-content` (no body) and supports
|
||||
`?q=` (regex search on title/summary/tags) and `?tag=`.
|
||||
- Reads are public; the writes (`POST /api/articles`, `PUT`/`DELETE` by slug) run
|
||||
through `requireAuth`.
|
||||
|
||||
## Frontend (`frontend/`)
|
||||
|
||||
Nuxt 4 (srcDir `app/`, TypeScript, Vue 3 `<script setup>` SFCs). No UI framework —
|
||||
hand-rolled SCSS (devDep `sass`) in `app/assets/css/`: `main.scss` `@use`s one
|
||||
partial per section (`_tokens`, `_layout`, `_editor`, `_book`, …); shared shapes
|
||||
(popups, dropdowns, segmented controls, avatars) are mixins in `_mixins.scss`, and
|
||||
the accent themes generate from a map in `_tokens.scss`. Colours stay CSS custom
|
||||
properties so themes swap at runtime via `data-theme`/`data-accent` attributes
|
||||
(set pre-paint by an inline script in `nuxt.config.ts`).
|
||||
|
||||
- `app/pages/` — `/` (search), `/wiki/[slug]` (article + TOC), `/wiki/[slug]/edit`, `/new`,
|
||||
`/admin` (the dashboard: every account and its role, plus the buttons that hand the
|
||||
role on or take it back — an admin alone sees anything but a refusal here).
|
||||
The two editor routes wrap their form in `AuthGate`, so it shows only with a session.
|
||||
- `app/components/` — PascalCase SFCs (header, editor, MarkdownView, TocNav, etc.).
|
||||
`AuthGate.vue` is the editor's stand-in for a signed-out visitor; `AuthModal.vue`
|
||||
is the popup that signs them in; `ProfileModal.vue` is the account popup the
|
||||
sidebar's account row opens (picture, username, password, sign out). `ConfirmModal.vue`
|
||||
is the yes/no popup any destructive step asks through (the editor's discard, the
|
||||
dashboard's delete). All popups (teleported overlays, closed by esc/backdrop/
|
||||
navigation like the other popups — opening one calls `hide()` on the others,
|
||||
they never stack). A row of actions folds into a ⋮ menu built on the shared
|
||||
`.kebab*` styles: `ArticleActionsMenu.vue` on an article's title row,
|
||||
`AccountActionsMenu.vue` on a row of the dashboard (the role and the account).
|
||||
- `app/composables/` — `useTopics` (topics = distinct tags computed client-side from the
|
||||
full article list, shared via `useState`), `useRecentlyViewed` (localStorage),
|
||||
`useAuth` (the session: token in the `mathew-session` cookie so the server sees it
|
||||
too, `signIn`/`signUp`/`signOut`, `authHeaders()` for write calls, `restoreSession()`
|
||||
asking `/auth/me` who a stored token belongs to, plus `updateUsername`/
|
||||
`changePassword`/`setAvatar` for the profile popup, and `isAdmin` — whether the
|
||||
account holds the admin role, which is what the sidebar's Admin link and the
|
||||
dashboard are drawn from), `useSignInPopup` (open state of
|
||||
that popup, shaped like `useSettings`/`useSearch`), `useProfileMenu` (same, for the
|
||||
profile popup), `useArticleActionsMenu` (same, for an article's ⋮ menu), and
|
||||
`useAccountActionsMenu` (same, for a dashboard row's ⋮ — it holds the **id** of
|
||||
the row that is open, so one row's menu is open at a time rather than a
|
||||
boolean per row).
|
||||
- `app/plugins/auth.ts` — runs `restoreSession()` once at startup on server and client,
|
||||
so the sidebar's account row and the editor's gate are right in the first render.
|
||||
- `app/utils/markdown.ts` — the single render pipeline: `markdown-it` (**html: false**,
|
||||
raw HTML is escaped) + `markdown-it-texmath` + KaTeX (`$...$` inline, `$$...$$` display,
|
||||
macros `\R \N \Z \Q \C`) + highlight.js, sanitized with DOMPurify. It also builds the
|
||||
TOC. Render all article Markdown through this, not ad-hoc renderers.
|
||||
- API calls use `$fetch` against `${useRuntimeConfig().public.apiBase}/api`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Reading is open; **writing takes an account**. `POST /api/articles` and `PUT`/`DELETE`
|
||||
by slug need `Authorization: Bearer <token>` from `/api/auth/login` or
|
||||
`/api/auth/register`. The frontend keeps that token in the `mathew-session` cookie
|
||||
(readable by the app, sent as a header — it is not an httpOnly session cookie), so a
|
||||
set `JWT_SECRET` matters in production; anyone with the dev key can mint a session.
|
||||
- A 401 from a write means the held session is dead; the editor keeps the draft and
|
||||
opens the sign-in popup over it rather than signing out and unmounting the form.
|
||||
- Anyone may register, and an account cannot edit another's article (articles store
|
||||
no author). Roles are the one difference between accounts: `role` is `'admin'` or
|
||||
`'member'`. **The first account ever registered is the admin** — and on a wiki
|
||||
whose accounts all predate the role, `User.ensureAnAdmin()` gives it to the oldest
|
||||
one at startup. Admins promote and demote from `/admin`; nobody may change their
|
||||
own role, so there is always at least one admin left to open the page. The same
|
||||
menu deletes an account — through `ConfirmModal`, and never the admin's own, for
|
||||
the same reason. Articles outlive the account that wrote them.
|
||||
- Profile pictures are stored as data URLs on the account (the browser shrinks them
|
||||
to a 256px square before upload); `express.json` allows 1.5 MB bodies so one fits
|
||||
a request. A 401 from a profile update means the session died — the popup signs
|
||||
out and opens the sign-in popup over it.
|
||||
- Changing an article's title can change its slug; old URLs 404 (no redirects).
|
||||
- Frontend dev server may fall back to port 3001 if 3000 is taken.
|
||||
- `frontend/types/markdown-it-texmath.d.ts` provides types for the untyped texmath package.
|
||||
Reference in New Issue
Block a user