Users
A user is one person in one tenant: the address they sign in with, a name to
show, a lifecycle status and the roles they hold. The module promises that the
two changes an audit has to be able to find — a grant and a deactivation — each
have one door and each say so in an event, and that a password is stored as an
argon2id hash that appears in no request, no response and no screen. It takes
no dependencies at all: a user belongs to a tenant by carrying its id, which
row-level security matches on. The module is
modules/user; this page states nothing that tree
does not.
The map
features/user in the recordWhat it promises
Entity |
|
Events |
|
Permissions |
|
Service |
|
Routes and screens
| Method | Path | Does | Authorization | Publishes |
|---|---|---|---|---|
|
|
Invite somebody into this tenant |
|
|
|
|
List users |
|
— |
|
|
Create a user |
|
|
|
|
Read a user |
|
— |
|
|
Update a user |
|
|
|
|
Delete a user |
|
|
|
|
Deactivate a user |
|
|
|
|
Set a user’s roles |
|
|
|
|
Set a user’s password |
|
|
-
GET/POST /api/v1/user/usersandGET/PATCH/DELETE /api/v1/user/users/{id}— the five routesrest.Spec.Mountregisters, with soft delete; a create or a patch that namesstatusorrolesis refused with the route that owns the field. -
POST /api/v1/user/invitations— creates somebody who cannot sign in yet and publishesuser.invited; roles named in the invitation are granted in the same transaction, so one request is one event about who was made what; an address already here is a conflict. It is a collection of its own because there is no user yet to command. It and the commands are ininternal/handler.go. -
POST /api/v1/user/users/{id}/set-password— stores the hash and makes an invited user active; the same password again is still a change and still an event; a deactivated user is refused with a conflict. -
POST /api/v1/user/users/{id}/roles— replaces the roles held, trimmed, lower-cased, deduplicated and sorted; the same set in any order changes nothing and publishes nothing. -
POST /api/v1/user/users/{id}/deactivate— stops the person signing in; a second time changes nothing. Their sessions are the auth module’s business. -
Screens: the generated list, detail and forms under
/admin/user/users, and the navigation entry "Users" for anyone withuser:read;module.gosays why the generated create form does not offerroles. Hand-written pages: none; an invitation screen would have to be one.
Authorization
| Authorization | Who passes | Routes |
|---|---|---|
|
a member of the tenant |
|
|
a member of the tenant |
|
Events, jobs and subscriptions
| Event | Published by | Handled by |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-
Publishes the seven events above; all seven are listed in the manifest, so the boot gate can check every route’s declaration against it.
-
Jobs: none —
Jobs: nilinmodule.go. -
Subscriptions: none —
Subscriptions: nil; the auth module is what listens foruser.invited.
What it needs
|
Interface |
Supplied by |
none |
|
|
Configuration: none.
Who uses it
-
auth —
auth.Deps.Usersisauthcontracts.Users(ByEmail,Get,SetPassword), the narrow half of theServicemain passes it; it subscribes touser.invitedto mail the set-password link, and callscontracts.EqualWorkon the path where it found no user, so a login for an address nobody has costs what a wrong password costs. -
notification —
notification.Deps.Recipientsis its ownRecipientLookup, satisfied inapps/platformkit/modules.goby an adapter overService.Get. -
tenant —
tenant.Deps.Inviteistenantcontracts.Inviter, satisfied inapps/platformkit/modules.gobyfirstAdmin, an adapter overService.Provisionwith no password and the admin role;platformkit bootstrapcreates the first administrator throughProvisiontoo, in the same transaction as the tenant. -
A private client module — takes
user.Servicefor a display name. -
The admin and the native shell reach it through
httpx.Resource, not throughcontracts/.
Verification
-
go test ./modules/user/…— the fake and the real service pass one conformance suite;TestALifecycleChangeHasExactlyOneDoorproves a create or a patch namingrolesorstatusis refused with the door to use while a patch ofdisplayNameand therolescommand work;TestThePasswordIsInNoResponseAndNoRequestproves no body carries a password or a hash; the internal tests prove one address is one row per tenant and invisible to another, that the stored hash is argon2id carrying its parameters, and thatProvisionlands in the named tenant with one event. -
No browser journey of its own:
make e2esigns in asadmin@e2e.test, the administratorplatformkit bootstrapprovisioned, and opens no user screen (e2e/admin-tasks.spec.ts). -
Not proven:
…/deactivateover HTTP, which the conformance suite covers only at the service, and the generated user screens in a browser. The invitation route is driven over the wire outside this module:apps/platformkit/app_test.goinvites somebody throughPOST /api/v1/user/invitationsand reads the set-password token out of the mail that follows.