webauth

package
v0.13.3 Latest Latest
Warning

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

Go to latest
Published: May 9, 2026 License: GPL-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package webauth provides authentication primitives for the graywolf web UI: password hashing, session tokens, and HTTP middleware.

Index

Constants

This section is empty.

Variables

View Source
var ErrSetupAlreadyComplete = errors.New("webauth: setup already complete")

ErrSetupAlreadyComplete is returned by CreateFirstUser when a user already exists in the database.

Functions

func CheckPassword

func CheckPassword(hash, password string) error

CheckPassword compares a bcrypt hash with a plaintext password.

func GenerateSessionToken

func GenerateSessionToken() (string, error)

GenerateSessionToken returns a cryptographically random 32-byte hex token.

func HashPassword

func HashPassword(password string) (string, error)

HashPassword returns a bcrypt hash suitable for storage.

func RequireAuth

func RequireAuth(auth *AuthStore) func(http.Handler) http.Handler

RequireAuth returns middleware that validates the session cookie and populates the request context with the authenticated user. Unauthenticated requests receive a 401 JSON response.

Types

type AuthStore

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

AuthStore persists web users and sessions via GORM.

func NewAuthStore

func NewAuthStore(db *gorm.DB) (*AuthStore, error)

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 (s *AuthStore) CreateSession(ctx context.Context, userID uint32, token string, expiry time.Time) (*WebSession, error)

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 (s *AuthStore) DeleteExpiredSessions(ctx context.Context) (int64, error)

func (*AuthStore) DeleteSession

func (s *AuthStore) DeleteSession(ctx context.Context, token string) error

func (*AuthStore) DeleteUser

func (s *AuthStore) DeleteUser(ctx context.Context, username string) error

func (*AuthStore) GetLastSeenReleaseVersion

func (s *AuthStore) GetLastSeenReleaseVersion(ctx context.Context, userID uint32) (string, error)

GetLastSeenReleaseVersion returns the stored high-water mark for the given user. Empty string is the zero-value default.

func (*AuthStore) GetSessionByToken

func (s *AuthStore) GetSessionByToken(ctx context.Context, token string) (*WebSession, error)

GetSessionByToken returns the session only if it hasn't expired.

func (*AuthStore) GetUserByUsername

func (s *AuthStore) GetUserByUsername(ctx context.Context, username string) (*WebUser, error)

func (*AuthStore) ListUsers

func (s *AuthStore) ListUsers(ctx context.Context) ([]WebUser, error)

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.

func (*AuthStore) UserCount

func (s *AuthStore) UserCount(ctx context.Context) (int64, error)

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

type LoginRequest struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

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

type SetupRequest struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

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

func AuthenticatedUser(r *http.Request) *WebUser

AuthenticatedUser returns the WebUser from the request context, or nil.

Jump to

Keyboard shortcuts

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