Files
tiny-forge/internal/store/users.go
T
alexei.dolgolyov 32de5b26a8 feat(docker-watcher): phase 12 - hardening
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.
2026-03-27 23:20:56 +03:00

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
}