internal

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func RegisterRoutes

func RegisterRoutes(api *httpx.API, spec rest.Spec[*contracts.User], svc contracts.Service)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL