auth

package
v1.19.1 Latest Latest
Warning

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

Go to latest
Published: May 29, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package auth handles every authentication concern for the v1.3 serve-mode daemon: bcrypt password hashing, DB-backed sessions, double-submit-cookie CSRF protection, and the chi middleware that gates non-public routes.

Phase 3 lands local-auth (email + password) + sessions + CSRF. Phase 4 adds OIDC subjects on top of the same users table. Phase 5 adds bearer-token auth for the REST API.

Index

Constants

View Source
const (
	SessionCookieName         = "__Host-ck_session"
	InsecureSessionCookieName = "ck_session"
)

SessionCookieName is the cookie carrying the opaque session ID when the daemon serves under TLS (the production default). Prefixed __Host- to opt into the strictest browser cookie policy: HTTPS-only, no Domain attribute, Path=/. Modern browsers refuse to honor __Host- prefixed cookies that violate the rules — fail-loud is the right default for an auth cookie.

InsecureSessionCookieName is used when the daemon serves plain HTTP (dev / localhost / behind a TLS-terminating proxy on a private network). __Host- prefix mandates Secure, so we have to use a different name; clients pick which to read via Sessions.CookieName.

View Source
const CSRFCookieName = "ck_csrf"

CSRFCookieName is the readable companion cookie for the double- submit CSRF pattern. Not prefixed __Host- because JS needs to read it for the form/header value submission.

View Source
const CSRFHeaderName = "X-CSRF-Token"

CSRFHeaderName is the request header the middleware checks for the CSRF token on state-mutating requests. The JS side reads CSRFCookieName and sets the header on every fetch.

View Source
const TokenPrefix = "ck_"

TokenPrefix is the human-recognizable tag every issued token carries. Operators see "ck_<32 hex chars>" and know it's a compliancekit token even if they don't recall the source.

Variables

View Source
var ErrEmailAlreadyTaken = errors.New("email already taken")

ErrEmailAlreadyTaken is returned by Create when a row already exists for the given email.

View Source
var ErrInvalidCredentials = errors.New("invalid email or password")

ErrInvalidCredentials is the single error returned by VerifyPassword for both "wrong user" and "wrong password" — never leak which one it was.

View Source
var ErrPasswordTooShort = fmt.Errorf("password must be at least %d characters", minPasswordLength)

ErrPasswordTooShort is returned when a password fails the length check. The handler should surface this as a field-level validation error, not a 500.

View Source
var ErrSessionExpired = errors.New("session expired")

ErrSessionExpired is returned by LoadSession when the cookie's session ID resolves to a row that's past its expires_at.

View Source
var ErrSessionNotFound = errors.New("session not found")

ErrSessionNotFound is returned by LoadSession when there's no row for the cookie's session ID (revoked, expired-and-cleaned, or forged cookie).

View Source
var ErrTokenExpired = errors.New("api token expired")

ErrTokenExpired is returned by Verify when the row's expires_at has elapsed.

View Source
var ErrTokenNotFound = errors.New("api token not recognized")

ErrTokenNotFound is returned by Verify when the bearer doesn't match any row. The middleware treats this as 401.

View Source
var ErrTokenRevoked = errors.New("api token revoked")

ErrTokenRevoked is returned by Verify when the row's revoked_at is non-null.

View Source
var ErrUserNotFound = errors.New("user not found")

ErrUserNotFound is returned by ByEmail / ByID when no row matches. Distinct from a query error so handlers can produce the right HTTP status without leaking the difference between "wrong email" + DB glitch.

Functions

func HashPassword

func HashPassword(password string) (string, error)

HashPassword produces a bcrypt hash suitable for storage in users.password_hash. Returns ErrPasswordTooShort when the input is below the minimum.

func InjectTestSession added in v1.12.0

func InjectTestSession(ctx context.Context, userID string) context.Context

InjectTestSession returns a context carrying a minimal Session for userID. Exported as a test helper so package-external tests (e.g. the scopeGate RBAC tests) can simulate a logged-in user without reaching for the cookie + Load round-trip.

func LoginHandler

func LoginHandler(users *Users, sessions *Sessions) http.HandlerFunc

LoginHandler builds the POST /api/auth/login handler. Validates the credentials via Users + VerifyPassword, issues a Session via Sessions.Create, sets the session + CSRF cookies, and responds with JSON (or a 303 to next= for browser POSTs).

func LogoutHandler

func LogoutHandler(sessions *Sessions) http.HandlerFunc

LogoutHandler builds the POST /api/auth/logout handler. Destroys the current session + clears cookies. Browser flows redirect to /login; JSON callers get 204.

func MeHandler

func MeHandler(users *Users) http.HandlerFunc

MeHandler builds the GET /api/auth/me handler. Returns the current session's user info; should be behind RequireAuth.

func Mount added in v1.3.1

func Mount(r chi.Router, users *Users, sessions *Sessions)

Mount installs the auth HTTP routes onto r:

POST /api/auth/login   → LoginHandler (form + JSON)
POST /api/auth/logout  → LogoutHandler (destroy session, clear cookies)
GET  /api/auth/me      → MeHandler   (returns current session's user)

/me sits behind RequireAuth; login + logout are intentionally unauthenticated. This helper was missing in v1.3.0 — the individual handlers shipped in phase 3 but never got wired onto the daemon's router; the bug surfaced as a 404 from the UI login form's POST. v1.3.1 closes the gap.

func RequireScope

func RequireScope(needed Scope, next http.Handler) http.Handler

RequireScope wraps next in a 403-on-missing-scope check. Pull the token from context (RequireToken must have run first); fail loud when the token's grants don't cover the route's requirement.

func VerifyPassword

func VerifyPassword(storedHash, candidate string) error

VerifyPassword constant-time-compares the candidate against the stored bcrypt hash. Returns nil on match, ErrInvalidCredentials on mismatch (also returned when the stored hash is empty — accounts provisioned via OIDC only).

Types

type IssueResult

type IssueResult struct {
	Token     *Token
	Plaintext string // "ck_<32 hex chars>" — show to operator once, never re-displayable
}

IssueResult is what Issue returns to the caller — including the plaintext token, which is shown to the operator ONCE and never stored unhashed.

type LoginRequest

type LoginRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
	Next     string `json:"next,omitempty"` // post-login redirect path
}

LoginRequest is the JSON body POST /api/auth/login accepts. The HTML login form (phase 11) submits the same fields via application/x-www-form-urlencoded — the handler accepts both.

type LoginResponse

type LoginResponse struct {
	UserID      string `json:"user_id"`
	Email       string `json:"email"`
	IsAdmin     bool   `json:"is_admin"`
	DisplayName string `json:"display_name,omitempty"`
}

LoginResponse is the JSON body on a successful POST. Browser flows receive a 303 redirect to LoginRequest.Next instead.

type OIDC

type OIDC struct {
	// contains filtered or unexported fields
}

OIDC encapsulates the runtime state for one configured upstream. Build via NewOIDC; mount its handlers on the chi router.

func NewOIDC

func NewOIDC(ctx context.Context, cfg OIDCConfig, users *Users, sessions *Sessions, st *store.Store) (*OIDC, error)

NewOIDC discovers the provider's metadata and returns a ready-to- mount handler set. GitHub takes a short-circuit path (no OIDC discovery, only OAuth2 + a userinfo HTTP fetch).

func (*OIDC) CallbackHandler

func (o *OIDC) CallbackHandler() http.HandlerFunc

CallbackHandler completes the flow: verifies state, exchanges the code for tokens, resolves the user identity (from id_token claims or upstream /user endpoint depending on provider), find-or-creates a row in users, issues a session, sets cookies, redirects to "/".

func (*OIDC) Config

func (o *OIDC) Config() OIDCConfig

Config returns the OIDCConfig the handler was constructed with; useful for the v1.4 settings page to surface what's configured.

func (*OIDC) LoginHandler

func (o *OIDC) LoginHandler() http.HandlerFunc

LoginHandler kicks off the authorization-code flow: generates a random state, sets it in a short-lived cookie, redirects the user to the upstream's authorize URL.

func (*OIDC) Mount added in v1.5.1

func (o *OIDC) Mount(r chi.Router)

Mount wires the per-provider routes under /oidc/{id}/login and /oidc/{id}/callback. v1.5.1 F15 — `auth.NewOIDC` + every handler shipped in v1.3 with unit tests, but the routes were never mounted onto the daemon's chi router. The login template even advertised OIDC; the corresponding paths returned 404 in production.

type OIDCConfig

type OIDCConfig struct {
	// ID is the URL-safe identifier ("google", "okta-corp",
	// "github-enterprise") that namespaces the callback URL.
	ID string

	// Provider drives the discovery + userinfo strategy.
	Provider OIDCProvider

	// IssuerURL is the OIDC issuer (for Google, Okta, generic).
	// Ignored when Provider == OIDCProviderGitHub.
	IssuerURL string

	// ClientID + ClientSecret are the OAuth2 app credentials issued
	// by the upstream provider.
	ClientID     string
	ClientSecret string

	// RedirectURL is the absolute callback URL the upstream invokes
	// after the user grants consent. Must match what the operator
	// registered with the provider exactly (scheme + host + path).
	RedirectURL string

	// Scopes default to the standard OIDC set ("openid", "profile",
	// "email") for OIDC providers and {"user:email"} for GitHub. Set
	// to override.
	Scopes []string
}

OIDCConfig is one configured OIDC integration. Operators register one of these per upstream they accept logins from; the daemon supports many simultaneously (the routes are /oidc/{provider-id}/login and /oidc/{provider-id}/callback).

type OIDCProvider

type OIDCProvider string

OIDCProvider names the upstream identity service. Google + Okta + "custom" use the standard OIDC discovery flow against an issuer URL; GitHub uses a separate OAuth2-only path (it doesn't ship OpenID Connect, only OAuth2 + a /user endpoint we read separately).

const (
	OIDCProviderGoogle OIDCProvider = "google"
	OIDCProviderOkta   OIDCProvider = "okta"
	OIDCProviderGitHub OIDCProvider = "github"
	OIDCProviderCustom OIDCProvider = "custom"
)

type OIDCProviderButton added in v1.5.1

type OIDCProviderButton struct {
	ID    string // url-safe id; matches the path segment in /oidc/{id}/login
	Label string // human display label, e.g. "Sign in with Google"
}

OIDCProviderButton is the display-side projection of an OIDC config for rendering on the /login page. v1.5.1 F15 — populated by the daemon at boot for every configured provider and passed to the UI via UI.SetOIDCProviders.

type SAML added in v1.12.0

type SAML struct {
	// contains filtered or unexported fields
}

SAML encapsulates the runtime state for one configured SAML IdP. Build via NewSAML; mount its handlers on the chi router with Mount.

func NewSAML added in v1.12.0

func NewSAML(ctx context.Context, cfg SAMLConfig, users *Users, sessions *Sessions, st *store.Store) (*SAML, error)

NewSAML parses the IdP cert + the SP keypair and assembles the crewjam/saml middleware. Returns a ready-to-mount SAML handler.

func (*SAML) Button added in v1.12.0

func (s *SAML) Button() SAMLProviderButton

Button returns the /login-page projection of this connection.

func (*SAML) Config added in v1.12.0

func (s *SAML) Config() SAMLConfig

Config returns the SAMLConfig the handler was constructed with; useful for /settings to surface what's configured.

func (*SAML) Mount added in v1.12.0

func (s *SAML) Mount(r chi.Router)

Mount wires the per-IdP routes under /saml/{id}/{login,acs,metadata}.

type SAMLConfig added in v1.12.0

type SAMLConfig struct {
	// ID is the URL-safe identifier ("okta-corp", "azure-prod") that
	// namespaces every route + the SP metadata's EntityID.
	ID string

	// Label is the human display label, e.g. "Sign in with Okta".
	Label string

	// EntryPoint is the IdP's SingleSignOnService URL — the SP redirects
	// the browser here on /saml/{id}/login. Operators copy it from the
	// IdP metadata.
	EntryPoint string

	// IDPMetadataXML is the entire IdP metadata document. Used to
	// recover the IdP's signing certificate so the SP can verify
	// assertions. Mutually exclusive with IDPCertPEM.
	IDPMetadataXML string

	// IDPCertPEM is the IdP's signing cert in PEM. When the operator
	// can't / won't paste the whole metadata blob, this is the smallest
	// thing the SP needs.
	IDPCertPEM string

	// EntityID is the SP's own EntityID. Defaults to
	// {RootURL}/saml/{id}/metadata when empty.
	EntityID string

	// RootURL is the daemon's externally-visible URL ("https://compliancekit.example.com").
	// Used to build the ACS and metadata URLs.
	RootURL string

	// SPCertPEM + SPKeyPEM are the SP's signing keypair. Generated by
	// the operator (a short-lived self-signed cert works; the IdP only
	// uses it to verify SP-side message signatures).
	SPCertPEM string
	SPKeyPEM  string

	// AllowIDPInitiated permits unsolicited assertions. Defaults true
	// because every major IdP exposes "launch app" tiles that produce
	// IdP-initiated flows; disable when defense-in-depth matters more
	// than the convenience.
	AllowIDPInitiated bool

	// SignRequests asks the SP to sign outbound AuthnRequests. Defaults
	// off because not every IdP enforces signature verification on the
	// SP side; turn on when the IdP requires it.
	SignRequests bool
}

SAMLConfig is one configured SAML SSO connection. Operators register one per IdP they accept logins from; the daemon supports many.

type SAMLProviderButton added in v1.12.0

type SAMLProviderButton struct {
	ID    string // url-safe id; matches the /saml/{id}/login path
	Label string // human display label
}

SAMLProviderButton is the display-side projection of a SAMLConfig for rendering on the /login page alongside OIDCProviderButton.

type Scope

type Scope string

Scope identifies a granular permission an API token can carry. Scopes are namespaced "<resource>:<verb>" with the convention that "*" wildcard at any segment grants every value at that level. Granted scopes are checked at the route handler via RequireScope.

const (
	// ScopeScansRead lists / reads scans + their findings + resources.
	ScopeScansRead Scope = "scans:read"

	// ScopeScansWrite triggers new scans (POST /api/v1/scans).
	ScopeScansWrite Scope = "scans:write"

	// ScopeFindingsRead reads findings standalone (also covered by
	// scans:read on the per-scan path; this scope is for the global
	// filtered explorer endpoints v1.5 ships).
	ScopeFindingsRead Scope = "findings:read"

	// ScopeWaiversRead lists waivers.
	ScopeWaiversRead Scope = "waivers:read"

	// ScopeWaiversWrite mutates the waivers set (add / edit / expire).
	ScopeWaiversWrite Scope = "waivers:write"

	// ScopeSettingsRead reads provider + check + framework + schedule
	// config.
	ScopeSettingsRead Scope = "settings:read"

	// ScopeSettingsWrite mutates the same.
	ScopeSettingsWrite Scope = "settings:write"

	// ScopeAdmin is the operator-superpower bit: covers everything,
	// plus user + token + webhook management. Tokens issued by the
	// v1.4 settings page top out at non-admin unless the operator
	// explicitly grants this.
	ScopeAdmin Scope = "*"
)

func (Scope) ScopeRBAC added in v1.12.0

func (s Scope) ScopeRBAC() (resource, action string, ok bool)

ScopeRBAC is the (resource, action) tuple a Scope corresponds to in the v1.12 RBAC grid. Returns ok=false for ScopeAdmin (it's the universal wildcard and doesn't map to a single tuple) and for any unknown scope so callers can fall back to the legacy check.

Strings are deliberate over pubrbac.Resource / pubrbac.Action so the auth package stays import-free of pkg/compliancekit/rbac (avoiding a dependency cycle through internal/server/rbac → auth).

type Session

type Session struct {
	ID         string
	UserID     string
	CSRFToken  string
	CreatedAt  time.Time
	LastSeenAt time.Time
	ExpiresAt  time.Time
	UserAgent  string
	IP         string
}

Session is the in-memory shape returned by LoadSession + companion methods. Field set matches the sessions table.

func FromContext

func FromContext(ctx context.Context) *Session

FromContext retrieves the *Session installed by RequireAuth. Returns nil when the route is unauthenticated.

type Sessions

type Sessions struct {

	// SecureCookies controls whether the session + CSRF cookies carry
	// the Secure attribute + __Host- prefix. Default is true (production
	// safe). Flip to false via `compliancekit serve --insecure-cookies`
	// for plain-HTTP dev / internal-network deploys where Secure cookies
	// get silently dropped by browsers.
	SecureCookies bool
	// contains filtered or unexported fields
}

Sessions is the persistence layer for the session table. Both the http middleware and the login/logout handlers go through this type.

func NewSessions

func NewSessions(st *store.Store) *Sessions

NewSessions returns a Sessions handle bound to st with secure cookies enabled (production default). Callers can flip SecureCookies = false for dev / plain-HTTP usage.

func (*Sessions) ClearCookies added in v1.5.1

func (s *Sessions) ClearCookies(w http.ResponseWriter)

ClearCookies tells the browser to drop both cookies. Used by logout + by middleware when a stored session is missing / expired.

func (*Sessions) CookieName added in v1.5.1

func (s *Sessions) CookieName() string

CookieName returns the session cookie name appropriate to the current SecureCookies setting. Use this from any handler that needs to read the cookie off the request.

func (*Sessions) Create

func (s *Sessions) Create(ctx context.Context, userID, userAgent, ip string) (*Session, error)

Create issues a new session row for userID. Returns the session + the plaintext cookie value the caller must set on the response. Both the session ID and the CSRF token are 256-bit hex strings.

func (*Sessions) Destroy

func (s *Sessions) Destroy(ctx context.Context, sid string) error

Destroy invalidates a session row. Idempotent.

func (*Sessions) DestroyForUser

func (s *Sessions) DestroyForUser(ctx context.Context, userID string) error

DestroyForUser deletes every session for userID — used by password change + the "log me out everywhere" affordance the v1.4 settings page will eventually expose.

func (*Sessions) ListActiveForUser added in v1.12.0

func (s *Sessions) ListActiveForUser(ctx context.Context, userID string) ([]*Session, error)

ListActiveForUser returns every non-expired session owned by userID ordered by most-recent first. Powers the v1.12 phase 5 admin surface ("active sessions for X").

func (*Sessions) ListAllActive added in v1.12.0

func (s *Sessions) ListAllActive(ctx context.Context) ([]*Session, error)

ListAllActive returns every non-expired session across every user. Powers the org-wide "all signed-in users" view. Bounded at 500 rows so a runaway loop can't OOM the daemon.

func (*Sessions) Load

func (s *Sessions) Load(ctx context.Context, sid string) (*Session, error)

Load fetches the session row by ID. Touches last_seen_at on success so an active session doesn't drift toward expiry. Returns ErrSessionNotFound / ErrSessionExpired distinct from other errors so the middleware can clear the cookie cleanly.

func (*Sessions) RequireAuth

func (s *Sessions) RequireAuth(next http.Handler) http.Handler

RequireAuth is the chi middleware factory that gates a route on a valid session cookie. On missing / expired session the cookies are cleared and the response is 401 (for /api routes) or a 303 redirect to /login (for everything else). The decision is driven by the request's Accept header + path prefix — pure-JSON callers get the machine-friendly status; browser callers get the human redirect.

func (*Sessions) RequireCSRF

func (s *Sessions) RequireCSRF(next http.Handler) http.Handler

RequireCSRF gates state-mutating requests on the double-submit cookie check. Reads the X-CSRF-Token header (set by client JS from the readable ck_csrf cookie) and compares it constant-time against the session's csrf_token. The session must already be installed by RequireAuth — chain RequireAuth before RequireCSRF.

Safe methods (GET / HEAD / OPTIONS) pass through unchecked; the browser doesn't trigger CSRF on those.

Token-auth callers (Authorization: Bearer ck_…) skip the CSRF check entirely — bearer tokens are not browser-resident credentials so cross-site requests can't smuggle them; CSRF protects only against cookie-based session hijacks.

func (*Sessions) SetCookies added in v1.5.1

func (s *Sessions) SetCookies(w http.ResponseWriter, sess *Session)

SetCookies writes the session + CSRF cookies onto w. In secure mode the session cookie is named __Host-ck_session and carries Secure + HttpOnly + SameSite=Lax + Path=/ (the __Host- prefix mandates the absence of Domain). In insecure mode the cookie is ck_session and drops Secure so plain-HTTP browsers will keep it. ck_csrf is the readable companion (not HttpOnly) so client-side JS can mirror it into the X-CSRF-Token header on every state-mutating request.

type Token

type Token struct {
	ID         string
	UserID     string
	Name       string
	Prefix     string
	Scopes     []Scope
	CreatedAt  time.Time
	LastUsedAt time.Time
	ExpiresAt  time.Time
	RevokedAt  time.Time
}

Token is the in-memory shape returned by Tokens.Verify (the row minus token_hash, which never leaves the package). Scopes are already parsed.

func TokenFromContext

func TokenFromContext(ctx context.Context) *Token

TokenFromContext retrieves the *Token installed by RequireToken. Returns nil when the route is unauthenticated or session-auth was used instead.

func (*Token) HasScope

func (t *Token) HasScope(s Scope) bool

HasScope reports whether t grants the given scope. ScopeAdmin short-circuits to true. Otherwise the granted scope must equal requested OR be the "<resource>:*" wildcard for the same resource.

type Tokens

type Tokens struct {
	// contains filtered or unexported fields
}

Tokens is the persistence layer for the api_tokens table.

func NewTokens

func NewTokens(st *store.Store) *Tokens

NewTokens returns a Tokens handle bound to st.

func (*Tokens) Issue

func (t *Tokens) Issue(ctx context.Context, userID, name string, scopes []Scope, expiresAt *time.Time) (*IssueResult, error)

Issue creates a new token for userID with the given scopes. The plaintext is returned exactly once via IssueResult; the daemon stores only a SHA-256 hash (fast + deterministic — bcrypt's per-request cost is wrong for tokens since they're presented every API call; the secrecy comes from the 128-bit random body).

func (*Tokens) List

func (t *Tokens) List(ctx context.Context, userID string) ([]*Token, error)

List returns every (non-revoked) token issued for userID. Doesn't include the hash — operators have no use for it post-issue.

func (*Tokens) RequireToken

func (t *Tokens) RequireToken(next http.Handler) http.Handler

RequireToken is the middleware that gates a route on a valid bearer token. Reads Authorization: Bearer <token>, verifies via Tokens.Verify, stashes the token + the owning user in context.

If both RequireAuth and RequireToken are mounted on the same route, either path of authentication satisfies it — the handler should resolve "who am I" by first checking TokenFromContext, falling back to FromContext for session auth.

func (*Tokens) Revoke

func (t *Tokens) Revoke(ctx context.Context, tokenID string) error

Revoke marks tokenID as revoked. Idempotent — already-revoked returns nil. Subsequent Verify of the same plaintext returns ErrTokenRevoked.

func (*Tokens) Verify

func (t *Tokens) Verify(ctx context.Context, presented string) (*Token, error)

Verify resolves a presented bearer token to a Token + the owning user ID. Hashes the input + compares to token_hash (UNIQUE in the schema). Updates last_used_at best-effort. Returns the typed sentinels ErrTokenNotFound / ErrTokenExpired / ErrTokenRevoked so the middleware picks the right HTTP status.

type User

type User struct {
	ID           string
	Email        string
	DisplayName  string
	PasswordHash string // empty for OIDC-only accounts
	OIDCSubject  string // empty for local-only accounts
	OIDCProvider string // empty for local-only accounts
	IsAdmin      bool
	CreatedAt    time.Time
	LastLoginAt  time.Time
}

User is the in-memory shape returned by Users.ByEmail + ByID.

type Users

type Users struct {
	// contains filtered or unexported fields
}

Users is the persistence layer for the users table. Kept tiny in phase 3 — phase 11 (UI shell) + v1.4 settings page add the full CRUD surface.

func NewUsers

func NewUsers(st *store.Store) *Users

NewUsers returns a Users handle bound to st.

func (*Users) All added in v1.8.0

func (u *Users) All(ctx context.Context) ([]*User, error)

All returns every user in the directory, ordered by display_name then email. v1.8 phase 2 — drives the assignee dropdown + the resource-owner picker. Cap at 500 rows; daemon admin uses a more targeted query if the directory ever grows past that.

func (*Users) ByEmail

func (u *Users) ByEmail(ctx context.Context, email string) (*User, error)

ByEmail looks up a user by exact (case-folded) email.

func (*Users) ByID

func (u *Users) ByID(ctx context.Context, id string) (*User, error)

ByID looks up a user by primary key.

func (*Users) Create

func (u *Users) Create(ctx context.Context, email, displayName, password string, isAdmin bool) (*User, error)

Create inserts a new local-auth user with the given email + plain password (the password is hashed via HashPassword before storage). Returns the persisted User. isAdmin grants admin rights.

func (*Users) TouchLastLogin

func (u *Users) TouchLastLogin(ctx context.Context, id string) error

TouchLastLogin updates last_login_at for the user. Best-effort — callers ignore the error; a missed update doesn't break login.

Jump to

Keyboard shortcuts

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