Documentation
¶
Overview ¶
Package webauth provides authentication primitives for the graywolf web UI: password hashing, session tokens, and HTTP middleware.
Index ¶
- Variables
- func CheckPassword(hash, password string) error
- func GenerateSessionToken() (string, error)
- func HashPassword(password string) (string, error)
- func RequireAuth(auth *AuthStore) func(http.Handler) http.Handler
- type AuthStore
- func (s *AuthStore) CreateFirstUser(ctx context.Context, username, passwordHash, buildVersion string) (*WebUser, error)
- func (s *AuthStore) CreateSession(ctx context.Context, userID uint32, token string, expiry time.Time) (*WebSession, error)
- func (s *AuthStore) CreateUser(ctx context.Context, username, passwordHash, buildVersion string) (*WebUser, error)
- func (s *AuthStore) DeleteExpiredSessions(ctx context.Context) (int64, error)
- func (s *AuthStore) DeleteSession(ctx context.Context, token string) error
- func (s *AuthStore) DeleteUser(ctx context.Context, username string) error
- func (s *AuthStore) GetLastSeenReleaseVersion(ctx context.Context, userID uint32) (string, error)
- func (s *AuthStore) GetSessionByToken(ctx context.Context, token string) (*WebSession, error)
- func (s *AuthStore) GetUserByUsername(ctx context.Context, username string) (*WebUser, error)
- func (s *AuthStore) ListUsers(ctx context.Context) ([]WebUser, error)
- func (s *AuthStore) SetLastSeenReleaseVersion(ctx context.Context, userID uint32, version string) error
- func (s *AuthStore) UserCount(ctx context.Context) (int64, error)
- type Handlers
- type LoginRequest
- type SetupCreatedResponse
- type SetupRequest
- type SetupStatusResponse
- type StatusResponse
- type WebSession
- type WebUser
Constants ¶
This section is empty.
Variables ¶
var ErrSetupAlreadyComplete = errors.New("webauth: setup already complete")
ErrSetupAlreadyComplete is returned by CreateFirstUser when a user already exists in the database.
Functions ¶
func CheckPassword ¶
CheckPassword compares a bcrypt hash with a plaintext password.
func GenerateSessionToken ¶
GenerateSessionToken returns a cryptographically random 32-byte hex token.
func HashPassword ¶
HashPassword returns a bcrypt hash suitable for storage.
Types ¶
type AuthStore ¶
type AuthStore struct {
// contains filtered or unexported fields
}
AuthStore persists web users and sessions via GORM.
func NewAuthStore ¶
NewAuthStore wraps an existing GORM DB and auto-migrates auth tables.
func (*AuthStore) CreateFirstUser ¶
func (s *AuthStore) CreateFirstUser(ctx context.Context, username, passwordHash, buildVersion string) (*WebUser, error)
CreateFirstUser atomically creates the first user in the system. Returns ErrSetupAlreadyComplete if any user already exists. Safe under concurrent requests.
buildVersion seeds LastSeenReleaseVersion so the first user does not see the release-notes backlog — they just installed, everything is "current" by definition.
Relies on SQLite serializable writers; if we move to a concurrent DB this needs a different strategy (e.g. an explicit advisory lock or an INSERT guarded by a WHERE NOT EXISTS subquery).
func (*AuthStore) CreateSession ¶
func (*AuthStore) CreateUser ¶
func (s *AuthStore) CreateUser(ctx context.Context, username, passwordHash, buildVersion string) (*WebUser, error)
CreateUser inserts a new user and seeds LastSeenReleaseVersion to buildVersion so the user does not see the release-notes backlog on first login. An empty buildVersion is permitted (a CLI utility or test with no build-time version plumbed through) but the user will see every note on first login.
func (*AuthStore) DeleteExpiredSessions ¶
func (*AuthStore) DeleteSession ¶
func (*AuthStore) DeleteUser ¶
func (*AuthStore) GetLastSeenReleaseVersion ¶
GetLastSeenReleaseVersion returns the stored high-water mark for the given user. Empty string is the zero-value default.
func (*AuthStore) GetSessionByToken ¶
GetSessionByToken returns the session only if it hasn't expired.
func (*AuthStore) GetUserByUsername ¶
func (*AuthStore) SetLastSeenReleaseVersion ¶
func (s *AuthStore) SetLastSeenReleaseVersion(ctx context.Context, userID uint32, version string) error
SetLastSeenReleaseVersion records that the user has acknowledged every release note up to and including version. Idempotent. Returns an error if no row matched (stale session whose user was deleted) so the caller's 204 response doesn't lie about the write.
type Handlers ¶
type Handlers struct {
Auth *AuthStore
Secure bool // set true when binding to non-loopback
// Logger receives structured error logs. If nil, slog.Default() is used.
Logger *slog.Logger
// SessionMaxAge, when non-zero, overrides the default 7-day session
// lifetime used for newly-issued session cookies. Zero means use the
// package default (defaultSessionMaxAge).
SessionMaxAge time.Duration
// BuildVersion is the running binary's version string (as reported
// by GET /api/version). Seeded into new users' LastSeenReleaseVersion
// so freshly created accounts don't receive the backlog of
// release-note popups. Empty string is permitted (tests, CLIs).
BuildVersion string
}
Handlers groups the auth HTTP endpoints.
func (*Handlers) CreateFirstUser ¶
func (h *Handlers) CreateFirstUser(w http.ResponseWriter, r *http.Request)
CreateFirstUser creates the first administrative user during setup. Returns 403 if any user already exists.
@Summary Create first-run user @Tags auth @ID createFirstUser @Accept json @Produce json @Param body body webauth.SetupRequest true "Credentials for the first administrator" @Success 201 {object} webauth.SetupCreatedResponse @Failure 400 {object} webtypes.ErrorResponse @Failure 403 {object} webtypes.ErrorResponse @Failure 500 {object} webtypes.ErrorResponse @Router /auth/setup [post]
func (*Handlers) GetSetupStatus ¶
func (h *Handlers) GetSetupStatus(w http.ResponseWriter, r *http.Request)
GetSetupStatus reports whether first-run account creation is still required (i.e. whether the user table is empty).
@Summary Get first-run setup status @Tags auth @ID getSetupStatus @Produce json @Success 200 {object} webauth.SetupStatusResponse @Failure 500 {object} webtypes.ErrorResponse @Router /auth/setup [get]
func (*Handlers) HandleLogin ¶
func (h *Handlers) HandleLogin(w http.ResponseWriter, r *http.Request)
HandleLogin validates credentials, creates a session, and sets a cookie.
Method dispatch is delegated to the calling mux: this handler is registered with a Go 1.22 method-scoped pattern ("POST /api/auth/login") by wiring.go, so the mux produces 405 with an Allow header automatically if a wrong verb arrives.
@Summary Log in @Tags auth @ID login @Accept json @Produce json @Param body body webauth.LoginRequest true "Credentials" @Success 200 {object} webauth.StatusResponse @Header 200 {string} Set-Cookie "Session cookie" @Failure 400 {object} webtypes.ErrorResponse @Failure 401 {object} webtypes.ErrorResponse @Failure 500 {object} webtypes.ErrorResponse @Router /auth/login [post]
func (*Handlers) HandleLogout ¶
func (h *Handlers) HandleLogout(w http.ResponseWriter, r *http.Request)
HandleLogout deletes the session and clears the cookie.
Method dispatch is delegated to the calling mux: this handler is registered with "POST /api/auth/logout", so the mux produces 405 automatically for wrong verbs.
@Summary Log out @Tags auth @ID logout @Produce json @Success 200 {object} webauth.StatusResponse @Header 200 {string} Set-Cookie "Cleared session cookie" @Router /auth/logout [post]
type LoginRequest ¶
LoginRequest is the POST body for /api/auth/login.
type SetupCreatedResponse ¶
type SetupCreatedResponse struct {
Status string `json:"status"`
Username string `json:"username"`
}
SetupCreatedResponse is returned by a successful POST /api/auth/setup.
type SetupRequest ¶
SetupRequest is the POST body for /api/auth/setup (first-run account creation).
type SetupStatusResponse ¶
type SetupStatusResponse struct {
NeedsSetup bool `json:"needs_setup"`
}
SetupStatusResponse is returned by GET /api/auth/setup. NeedsSetup is true only when no users exist in the auth store.
type StatusResponse ¶
type StatusResponse struct {
Status string `json:"status"`
}
StatusResponse is the canonical success body for the auth endpoints that return only a status sentinel (e.g. login, logout).
type WebSession ¶
type WebSession struct {
ID uint32 `gorm:"primaryKey;autoIncrement"`
Token string `gorm:"uniqueIndex;not null"`
UserID uint32 `gorm:"not null;index"`
ExpiresAt time.Time `gorm:"not null;index"`
CreatedAt time.Time
}
WebSession ties a bearer token to a user with an expiry. Table name is "auth_sessions" to avoid collision with configstore's web_sessions.
func (WebSession) TableName ¶
func (WebSession) TableName() string
type WebUser ¶
type WebUser struct {
ID uint32 `gorm:"primaryKey;autoIncrement"`
Username string `gorm:"uniqueIndex;not null"`
PasswordHash string `gorm:"not null"`
LastSeenReleaseVersion string `gorm:"size:20"`
CreatedAt time.Time
UpdatedAt time.Time
}
WebUser is a credential record for the web UI.
LastSeenReleaseVersion is the high-water mark of release-notes acknowledgement. Empty string (the default for AutoMigrate'd existing rows) is treated by releasenotes.Compare as less than any real version, so an existing user on first login after upgrade sees the full backlog. New users (created via CreateFirstUser / CreateUser) are seeded with the running build version so they don't get the backlog. Gorm size:20 leaves headroom for "999.999.999" (the longest strict x.y.z we'd ever emit).
func AuthenticatedUser ¶
AuthenticatedUser returns the WebUser from the request context, or nil.