package service import ( "context" "fmt" "log/slog" "time" "github.com/TopherMayor/unified-media-manager/internal/db" ) type User struct { ID int64 `json:"id"` Username string `json:"username"` DisplayName string `json:"display_name"` Role string `json:"role"` // admin, power_user, user APIKey string `json:"-"` CreatedAt time.Time `json:"created_at"` } type UserResponse struct { ID int64 `json:"id"` Username string `json:"username"` DisplayName string `json:"display_name"` Role string `json:"role"` CreatedAt time.Time `json:"created_at"` } const userColumns = `id, username, display_name, role, api_key, created_at` type UserService struct { db *db.DB } func NewUserService(database *db.DB) *UserService { return &UserService{db: database} } func userToResponse(u *User) UserResponse { return UserResponse{ ID: u.ID, Username: u.Username, DisplayName: u.DisplayName, Role: u.Role, CreatedAt: u.CreatedAt, } } func (s *UserService) SeedAdmin(ctx context.Context, apiKey string) error { if apiKey == "" { slog.Warn("ADMIN_API_KEY not set, skipping admin seed") return nil } tag, err := s.db.Pool.Exec(ctx, `INSERT INTO users (username, display_name, role, api_key) VALUES ('admin', 'Administrator', 'admin', $1) ON CONFLICT (username) DO NOTHING`, apiKey) if err != nil { return fmt.Errorf("seed admin: %w", err) } if tag.RowsAffected() > 0 { slog.Info("seeded admin user") } return nil } func (s *UserService) GetUserByAPIKey(ctx context.Context, apiKey string) (*User, error) { row := s.db.Pool.QueryRow(ctx, fmt.Sprintf("SELECT %s FROM users WHERE api_key = $1", userColumns), apiKey) var u User err := row.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Role, &u.APIKey, &u.CreatedAt) if err != nil { return nil, fmt.Errorf("user not found") } return &u, nil } func (s *UserService) List(ctx context.Context) ([]UserResponse, error) { rows, err := s.db.Pool.Query(ctx, fmt.Sprintf("SELECT %s FROM users ORDER BY id", userColumns)) if err != nil { return nil, fmt.Errorf("list users: %w", err) } defer rows.Close() var items []UserResponse for rows.Next() { var u User if err := rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Role, &u.APIKey, &u.CreatedAt); err != nil { slog.Error("failed to scan user", "error", err) continue } items = append(items, userToResponse(&u)) } return items, nil } func (s *UserService) GetByID(ctx context.Context, id int64) (*User, error) { row := s.db.Pool.QueryRow(ctx, fmt.Sprintf("SELECT %s FROM users WHERE id = $1", userColumns), id) var u User err := row.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Role, &u.APIKey, &u.CreatedAt) if err != nil { return nil, fmt.Errorf("user not found") } return &u, nil } // GetUser returns the UserResponse (without API key) for display. func (s *UserService) GetUser(ctx context.Context, id int64) (*UserResponse, error) { u, err := s.GetByID(ctx, id) if err != nil { return nil, err } resp := userToResponse(u) return &resp, nil }