Documentation
¶
Overview ¶
Package auth is the first first-party module and the canonical reference: every module the CLI generates has exactly this shape.
Files of a module (vertical layout, not one directory per layer):
module.go -> registration and routes user.entity.go -> the entity user.policy.go -> who may do what user.repo.go -> data access, requires a Grant user.service.go -> business rules user.request.go -> input types and Validate
Index ¶
- Constants
- Variables
- func NewID() (string, error)
- func NormalizeEmail(email string) string
- func SubjectOf(u User) security.Subject
- type CreateUserRequest
- type LoginRequest
- type Module
- type Service
- type TenantResolver
- type User
- type UserPolicy
- type UserRepo
- func (r *UserRepo) Create(ctx context.Context, g security.Grant, u User) (User, error)
- func (r *UserRepo) Delete(ctx context.Context, g security.Grant, id string) error
- func (r *UserRepo) Find(ctx context.Context, g security.Grant, id string) (User, error)
- func (r *UserRepo) FindByEmail(ctx context.Context, g security.Grant, email string) (User, error)
- func (r *UserRepo) List(ctx context.Context, g security.Grant, q data.Query) ([]User, error)
- func (r *UserRepo) Update(ctx context.Context, g security.Grant, u User) (User, error)
Constants ¶
const ( ActionUserView security.Action = "auth.user.view" ActionUserCreate security.Action = "auth.user.create" ActionUserUpdate security.Action = "auth.user.update" ActionUserDelete security.Action = "auth.user.delete" )
Actions of this module. They are constants, not strings at the call site: a typo in an action name would silently authorize nothing, or worse, everything.
Variables ¶
var ErrEmailTaken = errors.New("auth: email already registered in this tenant")
ErrEmailTaken is returned when the tenant already has that address.
var ErrInvalidCredentials = errors.New("auth: invalid credentials")
ErrInvalidCredentials is the single answer to a failed login. The reason lives in the log, never in the response: telling the client which half was wrong turns the endpoint into an account enumeration oracle.
var ErrUserNotFound = errors.New("auth: user not found")
ErrUserNotFound is returned when no row matches, so callers do not have to import database/sql to compare against sql.ErrNoRows.
Functions ¶
func NewID ¶ added in v0.2.0
NewID returns a version 4 UUID as text.
It forwards to data.NewID, which is where the one implementation lives: two id generators in one binary is two answers to "what does an id look like".
func NormalizeEmail ¶ added in v0.2.0
NormalizeEmail lowercases and trims the address.
Addresses are case-insensitive in practice, and storing them normalized is what keeps a plain UNIQUE index correct on every engine -- rather than a functional index in Postgres and a collation in MySQL.
Types ¶
type CreateUserRequest ¶
CreateUserRequest is the input contract. Fields are explicit: there is no mass assignment, so the whole bug class Laravel's $fillable exists to contain does not exist here.
func (CreateUserRequest) Validate ¶
func (r CreateUserRequest) Validate() validation.Errors
Validate reports the errors per field.
type LoginRequest ¶
LoginRequest is the login input. It does not check the password length: the rule that applies here is whether the credentials match, and a length message on login would leak the policy of existing accounts.
func (LoginRequest) Validate ¶
func (r LoginRequest) Validate() validation.Errors
Validate reports the errors per field.
type Module ¶
type Module struct {
// contains filtered or unexported fields
}
Module registers the authentication routes.
func New ¶
func New(svc *Service, tenant TenantResolver) *Module
New returns the module. The service and the tenant resolver are built by the caller, because the wiring is explicit: if you want to know where UserRepo comes from, it is written in the application's main.
A nil resolver means the empty tenant, which is what a single-tenant application that never set one ends up with -- consistent, and still isolated, because every row is written with that same value.
func (*Module) Health ¶
Health reports whether the module can reach its storage. It feeds /_arandu/health, so a database that went away turns into a failing probe rather than a stream of 500s.
func (*Module) Migrations ¶
Migrations declares the schema this module owns.
Every type here spells the same in SQLite, PostgreSQL and MySQL, which is what lets one project develop on a file and deploy on Postgres without a second schema. The three things that would have broken that, and what replaced them:
- uuid columns are TEXT, and the id is generated by the application;
- roles are TEXT holding JSON, not jsonb and not text[];
- created_at has no database default, the value comes from Go.
The email is stored lowercased by the repository, so a plain UNIQUE index is enough and no database-specific case-insensitive collation is needed.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service holds the business rules. It receives its dependencies through the constructor -- explicit wiring, generated by the CLI, no container.
func NewService ¶
NewService wires the module.
func (*Service) Authenticate ¶
Authenticate verifies the credentials and returns the user.
Note that there is no repository call without a Grant: the lookup by email uses SystemGrant, because at this point in the request there is no subject yet. That call is auditable, which is the point -- `aru doctor --strict` lists it.
func (*Service) CreateUser ¶
func (s *Service) CreateUser(ctx context.Context, actor security.Subject, in CreateUserRequest) (User, error)
CreateUser shows the full path: validate, Authorize, Grant, Repository.
func (*Service) EnsureAdmin ¶
EnsureAdmin creates the first administrator of a tenant when it has none.
It is deliberately NOT called at boot: seeding that happens by itself is how a known password ends up in production. Call it from a one-off command, with credentials that come from the environment.
type TenantResolver ¶ added in v0.1.1
TenantResolver decides which tenant a request belongs to.
It is only consulted on login, which is the one moment where there is no session to ask: everywhere else the tenant comes from the Grant, and therefore from the session. That asymmetry is the point -- a tenant taken from the request body or from a header after login would defeat the isolation the whole policy layer is built on.
Phase 2 adds the resolver that reads the host name, which is the default for a real multi-tenant deployment.
func FixedTenant ¶ added in v0.1.1
func FixedTenant(id string) TenantResolver
FixedTenant is the resolver for a single-tenant application: every login belongs to the same tenant.
This is not a "single-tenant mode". It is the same code path with a constant where the resolver would be, which is why an application that starts single and grows into multi changes one line and no queries.
type User ¶
type User struct {
ID string
TenantID string
Email string
Password string
Roles []string
CreatedAt time.Time
}
User is the entity. It has no persistence methods: this is not Active Record.
The Password field holds an argon2id hash and never leaves this type -- see MarshalJSON and LogValue below.
func (User) LogValue ¶
LogValue implements slog.LogValuer, so passing the whole user to a log call records the id and nothing else. This is the safe default: it means a careless log line cannot leak the hash.
func (User) MarshalJSON ¶
MarshalJSON keeps the hash out of any response, log or dump. Without it, a single observability.Dump(ctx, "user", u) would publish the hash on the debug page.
type UserPolicy ¶
type UserPolicy struct{}
UserPolicy is the only authority over who does what with a User.
It denies by default: the switch has no default branch that allows. `aru doctor` fails when a repository exists without a matching policy.
type UserRepo ¶
type UserRepo struct {
// contains filtered or unexported fields
}
UserRepo is the only door to the users table.
Every method starts with g.Check: the Grant is required by the signature, and the check proves the grant was issued for this exact action. Phase 2 generates these bodies from queries.sql with sqlc; the signature and the check do not change when it does.
The SQL is written with "?" placeholders and with types every supported database shares, so the same statements run on SQLite and on PostgreSQL. The dialect rebinds the placeholders; nothing else needs translating.
func NewUserRepo ¶
NewUserRepo returns a repository over an instrumented handle.
func (*UserRepo) Create ¶
Create inserts the user and returns it as stored.
The id and the timestamp are generated here rather than by the database: a DEFAULT that produces a uuid is spelled differently in every engine, and generating them in Go is what keeps one schema working everywhere.
func (*UserRepo) FindByEmail ¶
FindByEmail returns one user by email, scoped to the grant's tenant.
The address is normalized the same way on write and on read, which is what makes a plain UNIQUE index behave case-insensitively without a database specific collation.