Documentation
¶
Overview ¶
Package internal is every implementation of the user module. Nothing outside modules/user can import it, which is the compiler enforcing idea 3: a consumer takes contracts.Service, and taking anything else does not build.
Index ¶
- func RegisterRoutes(api *httpx.API, spec rest.Spec[*contracts.User], svc contracts.Service)
- type Invitation
- type Service
- func (s *Service) ByEmail(_ context.Context, tx db.Tx[db.Tenant], email string) (*contracts.User, error)
- func (s *Service) Deactivate(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID) (*contracts.User, error)
- func (s *Service) Get(_ context.Context, tx db.Tx[db.Tenant], id uuid.UUID) (*contracts.User, error)
- func (s *Service) Invite(ctx context.Context, tx db.Tx[db.Tenant], email, displayName string) (*contracts.User, error)
- func (s *Service) Provision(ctx context.Context, tx db.Tx[db.System], tenantID uuid.UUID, ...) (*contracts.User, error)
- func (s *Service) SetPassword(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID, password string) error
- func (s *Service) SetRoles(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID, roles []string) (*contracts.User, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RegisterRoutes ¶
RegisterRoutes mounts the three lifecycle commands on the same resource the Spec mounts the five CRUD routes on.
They are routes rather than fields of a PATCH because each is a rule about the state the user is in and each publishes an event. Setting a password through a generic update would put a password in a request body beside a display name, and in the update event beside it too; granting a role through one would make "who was made an administrator, and when" a question with no answer. spec.Immutable is the other half of that argument, and rest.Command is the part all three share.
Types ¶
type Invitation ¶
type Invitation struct {
Email string `json:"email" format:"email" maxLength:"320" doc:"The address to invite" example:"ada@acme.example.com"`
DisplayName string `json:"displayName,omitempty" maxLength:"200" doc:"Name to show" example:"Ada Lovelace"`
Roles []string `json:"roles,omitempty" doc:"Roles to grant in the same transaction" example:"admin"`
}
Invitation is what a caller sends to invite somebody. It is a named type and not the anonymous struct it was, because huma names a schema after the Go type behind it and an anonymous one came out of the generator as "InviteInputBody1" — a name with this package's plumbing and a deduplication counter in it, published in the OpenAPI document and compiled into every generated client.
type Service ¶
type Service struct{}
Service is the user lifecycle. It has no fields: everything a command needs arrives with the transaction it is given, which is what lets one instance serve a request, a job and an event handler at once.
func NewService ¶
func NewService() *Service
NewService returns the lifecycle commands. module.go constructs it.
func (*Service) ByEmail ¶
func (s *Service) ByEmail(_ context.Context, tx db.Tx[db.Tenant], email string) (*contracts.User, error)
ByEmail is the login lookup. The comparison is lower(email), which is the expression the unique index in migrations/000007 is built on, so this is an index scan and not a sequential one.
func (*Service) Deactivate ¶
func (s *Service) Deactivate(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID) (*contracts.User, error)
Deactivate stops the user signing in. Deactivating them again changes nothing.
func (*Service) Get ¶
func (s *Service) Get(_ context.Context, tx db.Tx[db.Tenant], id uuid.UUID) (*contracts.User, error)
Get is one user of this tenant.
func (*Service) Invite ¶
func (s *Service) Invite(ctx context.Context, tx db.Tx[db.Tenant], email, displayName string) (*contracts.User, error)
Invite creates a user with no password. They cannot sign in until somebody sets one, which in E3.2 is a link in an email a subscriber to this event sends; until then it is SetPassword, called by an administrator.
func (*Service) Provision ¶
func (s *Service) Provision(ctx context.Context, tx db.Tx[db.System], tenantID uuid.UUID, email, displayName, password string, roles []string, ) (*contracts.User, error)
Provision creates a user in a named tenant, from a transaction that belongs to no tenant. See contracts.Service.Provision.
An empty password makes an invited user rather than an active one, and that is the whole of the second caller: the control plane creates a tenant and has to give it a first administrator, in the same cross-tenant transaction, and an operator who had to choose somebody else's password to do it would be an operator who knows it. The invitation event that follows is what mails them a link. The bootstrap passes a password and gets the active user it prints.
func (*Service) SetPassword ¶
func (s *Service) SetPassword(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID, password string) error
SetPassword hashes and stores a password, and makes an invited user active.
It is not idempotent and must not be: setting a password to the value it already had is still a password change, and a person who did it deliberately has to see it in their own audit trail.
func (*Service) SetRoles ¶
func (s *Service) SetRoles(ctx context.Context, tx db.Tx[db.Tenant], id uuid.UUID, roles []string) (*contracts.User, error)
SetRoles replaces the roles this user holds. The same set again — in any order — changes nothing and publishes nothing: a retried click must not appear twice in an audit of who was made an administrator.