32de5b26a8
Blue-green zero-downtime deploys, promote flow validation. Dual auth: local (bcrypt + JWT) and OAuth2/OIDC (any provider). Auth middleware, login page, auth settings UI. Structured logging (slog JSON), config export to YAML. Graceful shutdown with deploy draining. Multi-stage Dockerfile and production docker-compose.yml. Swap phase order: Volumes & Env before UI Polish.
184 lines
5.3 KiB
Go
184 lines
5.3 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// User represents an authenticated user stored in the database.
|
|
type User struct {
|
|
ID string `json:"id"`
|
|
Username string `json:"username"`
|
|
PasswordHash string `json:"-"`
|
|
Email string `json:"email"`
|
|
Role string `json:"role"` // admin, viewer
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// AuthSettings holds the authentication configuration (single-row pattern).
|
|
type AuthSettings struct {
|
|
AuthMode string `json:"auth_mode"` // local, oidc
|
|
OIDCClientID string `json:"oidc_client_id"`
|
|
OIDCClientSecret string `json:"-"`
|
|
OIDCIssuerURL string `json:"oidc_issuer_url"`
|
|
OIDCRedirectURL string `json:"oidc_redirect_url"`
|
|
}
|
|
|
|
// CreateUser inserts a new user record.
|
|
func (s *Store) CreateUser(u User) (User, error) {
|
|
u.ID = uuid.New().String()
|
|
u.CreatedAt = now()
|
|
u.UpdatedAt = u.CreatedAt
|
|
|
|
_, err := s.db.Exec(
|
|
`INSERT INTO users (id, username, password_hash, email, role, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
u.ID, u.Username, u.PasswordHash, u.Email, u.Role, u.CreatedAt, u.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return User{}, fmt.Errorf("insert user: %w", err)
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// GetUserByID returns a single user by its ID.
|
|
func (s *Store) GetUserByID(id string) (User, error) {
|
|
var u User
|
|
err := s.db.QueryRow(
|
|
`SELECT id, username, password_hash, email, role, created_at, updated_at
|
|
FROM users WHERE id = ?`, id,
|
|
).Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Email, &u.Role, &u.CreatedAt, &u.UpdatedAt)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return User{}, fmt.Errorf("user %s: %w", id, ErrNotFound)
|
|
}
|
|
if err != nil {
|
|
return User{}, fmt.Errorf("query user: %w", err)
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// GetUserByUsername returns a single user by username.
|
|
func (s *Store) GetUserByUsername(username string) (User, error) {
|
|
var u User
|
|
err := s.db.QueryRow(
|
|
`SELECT id, username, password_hash, email, role, created_at, updated_at
|
|
FROM users WHERE username = ?`, username,
|
|
).Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Email, &u.Role, &u.CreatedAt, &u.UpdatedAt)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return User{}, fmt.Errorf("user %q: %w", username, ErrNotFound)
|
|
}
|
|
if err != nil {
|
|
return User{}, fmt.Errorf("query user by username: %w", err)
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// GetAllUsers returns every user ordered by username.
|
|
func (s *Store) GetAllUsers() ([]User, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT id, username, password_hash, email, role, created_at, updated_at
|
|
FROM users ORDER BY username`,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query users: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var users []User
|
|
for rows.Next() {
|
|
var u User
|
|
if err := rows.Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Email, &u.Role, &u.CreatedAt, &u.UpdatedAt); err != nil {
|
|
return nil, fmt.Errorf("scan user: %w", err)
|
|
}
|
|
users = append(users, u)
|
|
}
|
|
return users, rows.Err()
|
|
}
|
|
|
|
// UpdateUser updates a user's mutable fields (username, email, role).
|
|
func (s *Store) UpdateUser(u User) error {
|
|
u.UpdatedAt = now()
|
|
result, err := s.db.Exec(
|
|
`UPDATE users SET username=?, email=?, role=?, updated_at=? WHERE id=?`,
|
|
u.Username, u.Email, u.Role, u.UpdatedAt, u.ID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update user: %w", err)
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("user %s: %w", u.ID, ErrNotFound)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateUserPassword updates a user's password hash.
|
|
func (s *Store) UpdateUserPassword(id string, passwordHash string) error {
|
|
ts := now()
|
|
result, err := s.db.Exec(
|
|
`UPDATE users SET password_hash=?, updated_at=? WHERE id=?`,
|
|
passwordHash, ts, id,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update user password: %w", err)
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("user %s: %w", id, ErrNotFound)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteUser removes a user by ID.
|
|
func (s *Store) DeleteUser(id string) error {
|
|
result, err := s.db.Exec(`DELETE FROM users WHERE id = ?`, id)
|
|
if err != nil {
|
|
return fmt.Errorf("delete user: %w", err)
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("user %s: %w", id, ErrNotFound)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UserCount returns the total number of users.
|
|
func (s *Store) UserCount() (int, error) {
|
|
var count int
|
|
err := s.db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&count)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("count users: %w", err)
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
// GetAuthSettings returns the auth settings (single-row pattern, always row id=1).
|
|
func (s *Store) GetAuthSettings() (AuthSettings, error) {
|
|
var as AuthSettings
|
|
err := s.db.QueryRow(
|
|
`SELECT auth_mode, oidc_client_id, oidc_client_secret, oidc_issuer_url, oidc_redirect_url
|
|
FROM auth_settings WHERE id = 1`,
|
|
).Scan(&as.AuthMode, &as.OIDCClientID, &as.OIDCClientSecret, &as.OIDCIssuerURL, &as.OIDCRedirectURL)
|
|
if err != nil {
|
|
return AuthSettings{}, fmt.Errorf("query auth settings: %w", err)
|
|
}
|
|
return as, nil
|
|
}
|
|
|
|
// UpdateAuthSettings updates the auth settings row.
|
|
func (s *Store) UpdateAuthSettings(as AuthSettings) error {
|
|
_, err := s.db.Exec(
|
|
`UPDATE auth_settings SET auth_mode=?, oidc_client_id=?, oidc_client_secret=?, oidc_issuer_url=?, oidc_redirect_url=?
|
|
WHERE id = 1`,
|
|
as.AuthMode, as.OIDCClientID, as.OIDCClientSecret, as.OIDCIssuerURL, as.OIDCRedirectURL,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update auth settings: %w", err)
|
|
}
|
|
return nil
|
|
}
|