# 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. The interface itself speaks five languages (en, es, ca, fr, de), chosen per account. ``` 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, and `Article.replaceReservedLanguageIndex()` so an older database can write an article in any language (see the language gotcha below). - `src/config/db.js` — `connectDB` / `disconnectDB`. - `src/models/` — Mongoose schemas with `timestamps: true`. `Article` (slug, title, summary, content, tags + text index that points its reserved `language_override` at a field no article carries — see the language gotcha below), `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.locale` is the interface language the person picked (`SUPPORTED_LOCALES`: `en es ca fr de`, default `'en'`). `User.role` is `'admin'` or `'member'` (`ROLES`); `User.hasNone()` and `User.ensureAnAdmin()` cover who gets the former — see the roles gotcha below. `EditProposal` — a member's request for a change. `kind` is `'edit'` — a page that exists, named by its `article` `_id` — or `'create'`, a page the wiki does not have yet: `article` stays null until an approval writes one, and `slug`/`baseSlug` keep the address the request asks for and the version group a proposed translation joins. Either way it holds the whole proposed state (title/content/tags/language), a `base` snapshot of the article as it read when the request was sent (the diff's other half, empty for a creation), a `status` (`pending`/`approved`/`rejected`), who sent it (`createdBy` + the name recorded alongside, so it reads after an account closes) and who decided it. - `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), `PUT /api/auth/locale` (interface language, one of `SUPPORTED_LOCALES`). - `src/routes/articles.js` — reads open, writes by role. An **admin**'s save applies straight: `POST /api/articles` creates through `createArticle()` and `PUT /api/articles/slug/:slug` writes through `applyArticleEdit()` (which re-slugs on a title change and refuses a language the version group already has); both answer the wiki's rules through `resolveCreation()` and `requestError()`, so a create and an approved request cannot drift apart. A **member's** same save answers `202` with `{ proposal }` instead — an `EditProposal` upserted per (article, member, pending), or, for a page that does not exist yet, per (member, pending, asked-for slug): saving again *revises* the waiting request, and a draft that retitles itself sends the `proposalId` the first answer carried so it stays one request. `DELETE` by slug runs `requireAuth, requireAdmin` alone, and settles (deletes) that article's proposals with it. - `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). The edit-request shelf: `GET /api/admin/proposals` (every request, decided ones included, each with its proposer, its `base`/proposed text and the article as it stands now), `GET /api/admin/proposals/count` (just `{ pending }` — how many still wait, which is what the sidebar's badge on the Requests link wears), `PUT /api/admin/proposals/:id/approve` (an edit is written through `applyArticleEdit`, a creation through `createArticle` — so a page that appeared meanwhile, or a version group that grew the language asked for, is refused the same honest way and the request stays waiting; a gone article or an already-decided request is an error) and `PUT /api/admin/proposals/:id/reject` (refuses; the page stays put). - `src/middleware/auth.js` — `signToken(user)`, `requireAuth`, which reads `Authorization: Bearer ` 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 answers `{ articles, total, hasMore }`. It filters by article, not by document — `?q=` (regex on title/tags), `?tag=` a tag any version wears, `?lang=` a language any version is written in — and `?limit=` / `?offset=` page it **by article** (the identity, not the document), so `total` counts each article once however many languages it exists in. `?prefer=` (a reader's interface language) picks which version stands for an article — that language's, else English, else the canonical one — but never decides which articles answer. `?preferFirst` additionally **ranks** the answers, leading with the articles that speak that language themselves (the Ctrl+K search popup asks for it; the shelf and home pages do not). Asked for no `limit`, the whole shelf answers at once. - `GET /api/articles/facets` answers `{ total, topics, languages }` counted over the whole shelf (topics lowercased, both tallied by popularity). The `/articles` page draws its topic folders and language chips from this, not from the cards it happens to have on screen. - Reads are public; the writes (`POST /api/articles`, `PUT`/`DELETE` by slug) run through `requireAuth`. Of those, a `PUT` applies straight only for an admin and a `DELETE` is admin-only — a member's `PUT` is answered `202` with a proposal instead (see the roles gotcha below). ## Frontend (`frontend/`) Nuxt 4 (srcDir `app/`, TypeScript, Vue 3 `