POST /users/{id}/promote and /demote set a user's role (admin/user), admin-only
(403 otherwise). Mirrors the verify endpoints: SetRole use case + repo method,
validated role. Unit, admin-flow, and integration tests included.
SciMesh userservice
Authentication service for SciMesh, in Go on PostgreSQL. It owns user accounts and issues the JWTs the coordinator trusts. It is a separate bounded context from the coordinator: its own database, its own binary. The only thing shared between the two services is the JWT signing secret.
Built as a modular monolith following Clean Architecture — one binary, four layers, dependencies pointing strictly inward:
infra config, DB pool, clock, HTTP server ← drivers
transport HTTP handlers + JWT middleware ← incoming
storage SQL repository ← outgoing
usecase Register / Login + PORTS (interfaces) ← application rules
domain User, Role, invariants ← business rules
auth bcrypt hasher, HS256 JWT issuer ← crypto adapters
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /health |
none | Liveness probe (checks the database) |
| POST | /register |
none | Create an account (always role user) |
| POST | /login |
none | Verify credentials, return a signed JWT |
| GET | /me |
Bearer JWT | Return the caller's own account |
| POST | /users/{id}/verify |
Bearer admin | Grant the trusted-contributor badge |
| POST | /users/{id}/unverify |
Bearer admin | Revoke the badge |
| POST | /users/{id}/promote |
Bearer admin | Set the user's role to admin |
| POST | /users/{id}/demote |
Bearer admin | Set the user's role back to user |
Two independent attributes live on an account:
role—useroradmin. Governs what you may do with your own jobs. Registration always creates auser; promotion toadminis a manual database operation, never a request.verified— a boolean trust badge, granted only by an admin (the/verifyendpoints above, 403 for anyone else). It tells the coordinator whether this user's volunteer workers are trusted: a verified contributor's results are accepted directly, an unverified one's must pass quorum cross-checking. Defaults to false.
Both attributes ride in the JWT (role, verified claims), so the coordinator
reads them from the signed token without ever calling this service.
How it connects to the coordinator
The coordinator never calls this service at runtime. A client logs in here, gets
a JWT, and presents it to the coordinator, which verifies the signature locally
with the same JWT_SECRET and reads sub (the user id) into jobs.owner_id.
That link is off by default: until the coordinator is given a matching
JWT_SECRET, it accepts only the shared worker token and stores owner_id as
NULL. Set the same secret (≥ 32 bytes, byte-for-byte identical) on both services
to turn it on.
Run
# whole stack: Postgres + migrations + the service on :8081
make up
# or locally against your own Postgres
cp .env.example .env # then edit JWT_SECRET and DATABASE_URL
make run
Verify
make test # unit tests
make check # vet, lint, race, integration, smoke — needs Docker
make smoke # end-to-end against a running service
Password hashing uses bcrypt (golang.org/x/crypto/bcrypt); the salt and cost
are embedded in the stored hash, so there is no separate salt column. Tokens are
HS256 (github.com/golang-jwt/jwt/v5).