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() 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 }