Add admin-only CRUD endpoints for user management under /api/v1/system/users with full frontend implementation including create, edit, change password, and delete operations with self-protection guards. - Extend AuthDomain interface with ListUsers, GetUser, UpdateUser, UpdateUserPassword, DeleteUser methods - Add List, Update, Delete methods to UserStore with shared scanUser helper - Create UserHandler with 5 endpoints protected by admin middleware - Complete Users.vue admin page with modals and error handling - Make CreateUser accept role parameter for atomic user creation - Fix auth middleware to use database role as source of truth instead of stale JWT claims, preventing privilege persistence after demotion - Fix pre-existing gofmt issues in yt_dlp.go and config.go
194 lines
4.6 KiB
Go
194 lines
4.6 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/huandu/go-sqlbuilder"
|
|
)
|
|
|
|
// User represents a user in the database
|
|
type User struct {
|
|
ID uuid.UUID
|
|
Email string
|
|
PasswordHash string
|
|
Role string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// UserStore handles database operations for users
|
|
type UserStore struct {
|
|
readDB *sql.DB
|
|
writeDB *sql.DB
|
|
}
|
|
|
|
// NewUserStore creates a new UserStore with read/write database separation
|
|
func NewUserStore(readDB, writeDB *sql.DB) *UserStore {
|
|
return &UserStore{
|
|
readDB: readDB,
|
|
writeDB: writeDB,
|
|
}
|
|
}
|
|
|
|
// userColumns lists the columns selected in user queries
|
|
var userColumns = []string{"id", "email", "password_hash", "role", "created_at", "updated_at"}
|
|
|
|
// scanUser scans a single user row into a User struct
|
|
func scanUser(scanner interface{ Scan(dest ...any) error }) (*User, error) {
|
|
var user User
|
|
var idStr string
|
|
var createdAt, updatedAt string
|
|
|
|
if err := scanner.Scan(&idStr, &user.Email, &user.PasswordHash, &user.Role, &createdAt, &updatedAt); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("user not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to scan user: %w", err)
|
|
}
|
|
|
|
var err error
|
|
user.ID, err = uuid.Parse(idStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse user ID: %w", err)
|
|
}
|
|
|
|
if createdAt != "" {
|
|
user.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
|
}
|
|
if updatedAt != "" {
|
|
user.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
|
}
|
|
|
|
return &user, nil
|
|
}
|
|
|
|
// FindByEmail finds a user by email
|
|
func (s *UserStore) FindByEmail(email string) (*User, error) {
|
|
sb := sqlbuilder.NewSelectBuilder()
|
|
sb.Select(userColumns...)
|
|
sb.From("users")
|
|
sb.Where(sb.Equal("email", email))
|
|
sb.Limit(1)
|
|
|
|
query, args := sb.Build()
|
|
return scanUser(s.readDB.QueryRow(query, args...))
|
|
}
|
|
|
|
// FindByID finds a user by ID
|
|
func (s *UserStore) FindByID(id uuid.UUID) (*User, error) {
|
|
sb := sqlbuilder.NewSelectBuilder()
|
|
sb.Select(userColumns...)
|
|
sb.From("users")
|
|
sb.Where(sb.Equal("id", id.String()))
|
|
sb.Limit(1)
|
|
|
|
query, args := sb.Build()
|
|
return scanUser(s.readDB.QueryRow(query, args...))
|
|
}
|
|
|
|
// List returns all users ordered by created_at DESC
|
|
func (s *UserStore) List() ([]*User, error) {
|
|
sb := sqlbuilder.NewSelectBuilder()
|
|
sb.Select(userColumns...)
|
|
sb.From("users")
|
|
sb.OrderByDesc("created_at")
|
|
|
|
query, args := sb.Build()
|
|
rows, err := s.readDB.Query(query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query users: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var users []*User
|
|
for rows.Next() {
|
|
user, err := scanUser(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
users = append(users, user)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating users: %w", err)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// Update updates an existing user's email, password_hash, role, and updated_at
|
|
func (s *UserStore) Update(user *User) error {
|
|
ub := sqlbuilder.NewUpdateBuilder()
|
|
ub.Update("users")
|
|
ub.Set(
|
|
ub.Assign("email", user.Email),
|
|
ub.Assign("password_hash", user.PasswordHash),
|
|
ub.Assign("role", user.Role),
|
|
ub.Assign("updated_at", user.UpdatedAt),
|
|
)
|
|
ub.Where(ub.Equal("id", user.ID.String()))
|
|
|
|
query, args := ub.Build()
|
|
result, err := s.writeDB.Exec(query, args...)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update user: %w", err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get rows affected: %w", err)
|
|
}
|
|
if rowsAffected == 0 {
|
|
return fmt.Errorf("user not found")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete deletes a user by ID
|
|
func (s *UserStore) Delete(id uuid.UUID) error {
|
|
db := sqlbuilder.NewDeleteBuilder()
|
|
db.DeleteFrom("users")
|
|
db.Where(db.Equal("id", id.String()))
|
|
|
|
query, args := db.Build()
|
|
result, err := s.writeDB.Exec(query, args...)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete user: %w", err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get rows affected: %w", err)
|
|
}
|
|
if rowsAffected == 0 {
|
|
return fmt.Errorf("user not found")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Create creates a new user
|
|
func (s *UserStore) Create(user *User) error {
|
|
// Default role to "user" if not set (required by DB CHECK constraint)
|
|
role := user.Role
|
|
if role == "" {
|
|
role = "user"
|
|
}
|
|
|
|
ib := sqlbuilder.NewInsertBuilder()
|
|
ib.InsertInto("users")
|
|
ib.Cols(userColumns...)
|
|
ib.Values(user.ID.String(), user.Email, user.PasswordHash, role, user.CreatedAt, user.UpdatedAt)
|
|
|
|
query, args := ib.Build()
|
|
_, err := s.writeDB.Exec(query, args...)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to insert user: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|