Let users edit their profile in account settings
Node · Node · beginner · modification
Adds the backend for the new account settings form: `PATCH /api/me` lets a signed-in user change their display name, time zone and marketing-email opt-in. Input checks live in a small `validateProfile` helper that returns the invalid field names, so the form can highlight them (`400 invalid_profile`); a partial body only touches the fields it sends. The route loads the user, applies the update, saves and answers with the same shape as `GET /api/me`, so the page can re-render from the response.
Express 4.21 API on Node 20 with Sequelize 6 on PostgreSQL. `src/app.js` (unchanged) uses `express.json()` (default 100 kB limit), mounts `app.use('/api/me', requireAuth, meRouter)`, and its error handler answers `400 { error: 'invalid_json' }` for a body `express.json()` refuses (malformed JSON, or a top-level value that is not an object or array) and `500 { error: 'internal' }` for anything else (logged). `requireAuth` (unchanged) answers 401 without a valid session and otherwise sets `req.user = { id }`. The `User` model (`src/models`, unchanged) has `id`, `email`, `passwordHash`, `displayName` (varchar 80), `timezone` (varchar 64, default `UTC`), `marketingEmails` (boolean), `role` (`member` or `admin`; admins manage the organisation, its members and its billing) and `plan` (`free`, `pro` or `team`, which decides the paid features). The settings form sends only the three profile fields. Whitespace in a display name is kept as typed (the form trims it).
Requirements
- Add `PATCH /api/me` for the account settings page: a signed-in user updates their own profile and gets `200` with the same body as `GET /api/me`.
- The editable fields are exactly `displayName` (a string whose JavaScript `length` is 1 to 80, with no control characters U+0000 to U+001F or U+007F), `timezone` (a string of at most 64 characters that `Intl.DateTimeFormat` accepts as a `timeZone`) and `marketingEmails` (a boolean). Each is optional; a field that is absent keeps its value. A present field with an invalid value answers `400 { error: 'invalid_profile', fields: [...] }` naming the invalid fields, and nothing is saved. A JSON array body also answers 400 (malformed JSON and a top-level JSON value that is not an object or array are already answered 400 by the app's `express.json()`); a request without a JSON content type reaches the route as an empty body and changes nothing.
- Every other key in the body is ignored and changes nothing. In particular `email` (changed only through the verified email-change flow), `passwordHash`, `role` (set only by an organisation admin in the admin console) and `plan` (set only by the billing webhook) can never be changed through this endpoint.
- Unchanged: `requireAuth` answers 401 without a session; a user that no longer exists answers `404 { error: 'not_found' }`; database errors go to the app's error handler through `next(err)`.
Files touched
- src/routes/me.js
- src/validation/profile.js
--- src/routes/me.js
const express = require('express');
const { User } = require('../models');
+const { validateProfile } = require('../validation/profile');
const router = express.Router();
});
+router.patch('/', async (req, res, next) => {
+ const problems = validateProfile(req.body);
+ if (problems.length > 0) {
+ return res.status(400).json({ error: 'invalid_profile', fields: problems });
+ }
+ try {
+ const user = await User.findByPk(req.user.id);
+ if (!user) return res.status(404).json({ error: 'not_found' });
+ Object.assign(user, req.body);
+ await user.save();
+ res.json(toPublicUser(user));
+ } catch (err) {
+ next(err);
+ }
+});
+
module.exports = router;
--- src/validation/profile.js
+const MAX_DISPLAY_NAME = 80;
+const MAX_TIMEZONE = 64;
+const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;