Two parallel auth paths — Postgres-backed Express sessions for humans and Bearer long-term tokens for scripts — share a 3-character permission ENUM where the first user against an empty database becomes super-admin.
The first POST /api/users/first_signup against an empty database silently creates a 111 super-admin — leaving an unguarded fresh deployment exposed gives a stranger the keys.
Three feature modules cooperate to handle identity: API/Backend/Users (the users
table and the sign-up/login routes), API/Backend/Accounts (admin-only CRUD over the
user list), and API/Backend/LongTermToken (machine credentials). They share session
state set up in server bootstrap and are guarded by the
ensureAdmin / ensureUser middleware defined in scripts/server.js.
Users, passwords, permissions
The User model in API/Backend/Users/models/user.js stores username,
email, a bcrypt hash of password, and a 3-character permission ENUM. The model
hashes on beforeCreate and beforeUpdate hooks, so route code never touches plain
passwords. The legal permission strings are "000" through "111"; in practice only
three are used: "111" (super-admin), "110" (admin), and "001" (regular user).
Each character is a coarse capability bit — ensureAdmin checks the high bits, and
/logged_in simply checks that the last bit is 1.
Sign-up: first user becomes admin
POST /api/users/first_signup is the bootstrap path. It only succeeds when
User.count() === 0, and when it does, it creates the user with permission: "111".
This matches what the README tells operators: the very first account created against an
empty database is the super-admin, and after that the form is hidden. All subsequent
sign-ups go through POST /api/users/signup, which creates permission: "001" users
and refuses unless either the caller is already a super-admin or
AUTH_LOCAL_ALLOW_SIGNUP=true is set in the environment.
Login and session shape
POST /api/users/login looks up the user, runs bcrypt.compare, and on success calls
req.session.regenerate(...) (rotating the session id to prevent fixation) before
writing four fields onto req.session:
req.session.user = user.username;
req.session.uid = user.id;
req.session.token = crypto.randomBytes(128).toString("hex");
req.session.permission = user.permission;
The fresh token is also persisted to users.token so the client can present it later
for "remember me"-style re-login via the MMGISUser cookie (see the useToken branch
in routes/users.js). POST /api/users/logout nulls the DB token and regenerates the
session.
Where session state lives
Sessions are Express's express-session backed by connect-pg-simple, configured in
scripts/server.js around line 130:
app.use(session({
secret: process.env.SECRET || "Shhhh, it is a secret!",
name: "MMGISSession",
resave: false,
saveUninitialized: false,
cookie: cookieOptions, // 24h maxAge; SameSite=None+Secure if THIRD_PARTY_COOKIES
store: new (require("connect-pg-simple")(session))({ pool }),
}));
The pool is a pg.Pool pointed at the same Postgres instance the app uses for
everything else, so server restarts don't log users out and a horizontally scaled
deployment shares state for free.
Long-term tokens
Sessions are for humans. Long-term tokens are for scripts and automation. The
LongTermToken module (API/Backend/LongTermToken/) lets an admin generate a random
hex token with a period (a millisecond TTL or "never") and a created_by_user_id
foreign key. Clients send them as Authorization: Bearer <token>, and
validateLongTermToken in scripts/server.js joins long_term_tokens against users
to recover the creator's permission and missions_managing, attaching them to req
as req.tokenUserPermission etc. The token CRUD routes are themselves admin-only — note
the ensureAdmin(false, true) in LongTermToken/setup.js, where the second arg tells
the guard to refuse token-authenticated callers (you can't bootstrap new tokens with an
existing token).
How routes declare auth requirements
There is no decorator system — each feature module's setup.js composes its own
middleware chain when mounting its router, picking from helpers exposed on the
s (setup context). Two patterns dominate:
- Admin-only modules wedge
s.ensureAdmin()in front of the router (Accounts does this; so does most of Configure). - Public-ish modules just chain
s.checkHeadersCodeInjectionands.setContentTypeand rely onensureUser()further upstream.
The admin SPA at /configure (Configure) is the primary
consumer of the admin-only routes — it's the UI that drives /api/accounts/* and
/api/longtermtoken/* against this same auth surface.
The ORM MMGIS uses for table definitions, model hooks, and the
connect-pg-simple-backed session store. Spatial queries bypass it and use raw SQL
through pg-promise.
Related
- Backend (API server) — Server bootstrap and middlewareHow the MMGIS process starts, composes Express, mounts feature modules via the setup lifecycle, and attaches the WebSocket server.
- Backend (API server) — Feature modules under API/BackendThe repeating shape every backend feature follows, and a tour of the modules that matter (Datasets, Geodatasets, Draw, Config, Stac, Webhooks, Shortener, etc.).
- Configure (admin SPA)A separate React app served at /configure for managing missions, layers, datasets, and users. Has its own webpack build.