Sync from /srv/compose/unified-media-manager
This commit is contained in:
121
internal/service/user.go
Normal file
121
internal/service/user.go
Normal file
@@ -0,0 +1,121 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user