diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-01 22:16:11 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-01 22:16:11 +0300 |
| commit | feafe31716cdbac304dbcd0bce6fe7b205747f0a (patch) | |
| tree | 646a3330be1090ee78cd64166b76cd221ec8f847 /internal/service | |
| parent | af29deb33ee25800976b7122236bf7895a5ff39e (diff) | |
Task 1: Create AuthService and route handleBootstrap/handleLogin through it
Introduce service.AuthService interface with Bootstrap and Login methods,
a concrete authService implementation, and a MockAuthService for testing.
Wire AuthService into api.Server and update cmd/mediaplayer/main.go to use it.
This removes direct store access from handleBootstrap and handleLogin,
fixing the DIP violation. Sentinel errors (ErrAlreadyBootstrapped,
ErrInvalidCredentials) are added to the service package so the API layer
can map them to the correct HTTP status codes without leaking DB details.
Files created:
- internal/service/auth.go
Files modified:
- internal/service/service.go
- internal/service/media.go
- internal/service/mock.go
- internal/repository/repository.go
- internal/repository/mock.go
- internal/api/server.go
- internal/api/handlers_auth.go
- internal/api/handlers_test.go
- internal/api/handlers_more_test.go
- cmd/mediaplayer/main.go
Diffstat (limited to 'internal/service')
| -rw-r--r-- | internal/service/auth.go | 86 | ||||
| -rw-r--r-- | internal/service/media.go | 2 | ||||
| -rw-r--r-- | internal/service/mock.go | 20 | ||||
| -rw-r--r-- | internal/service/service.go | 12 |
4 files changed, 120 insertions, 0 deletions
diff --git a/internal/service/auth.go b/internal/service/auth.go new file mode 100644 index 0000000..c52d2f0 --- /dev/null +++ b/internal/service/auth.go @@ -0,0 +1,86 @@ +package service + +import ( + "context" + "fmt" + + "codeberg.org/snonux/player/internal/auth" + "codeberg.org/snonux/player/internal/clock" + "codeberg.org/snonux/player/internal/model" + "codeberg.org/snonux/player/internal/repository" +) + +// authService is the concrete implementation of AuthService. +type authService struct { + store repository.AuthServiceStore + clock clock.Clock + hasher auth.Hasher + sm *auth.SessionManager +} + +// NewAuthService creates a concrete AuthService. +func NewAuthService(store repository.AuthServiceStore, clk clock.Clock, hasher auth.Hasher, sm *auth.SessionManager) AuthService { + return &authService{ + store: store, + clock: clk, + hasher: hasher, + sm: sm, + } +} + +// Bootstrap creates the first admin user when no users exist. +func (s *authService) Bootstrap(ctx context.Context, username, password string) (*AuthResult, error) { + count, err := s.store.CountUsers(ctx) + if err != nil { + return nil, fmt.Errorf("count users: %w", err) + } + if count > 0 { + return nil, ErrAlreadyBootstrapped + } + + hash, err := s.hasher.Hash(password) + if err != nil { + return nil, fmt.Errorf("hash password: %w", err) + } + + user := &model.User{ + Username: username, + PasswordHash: hash, + IsAdmin: true, + CreatedAt: s.clock.Now(), + } + + id, err := s.store.CreateUser(ctx, user) + if err != nil { + return nil, fmt.Errorf("create user: %w", err) + } + user.ID = id + + sessID, err := s.sm.CreateSession(ctx, id) + if err != nil { + return nil, fmt.Errorf("create session: %w", err) + } + + return &AuthResult{User: user, SessionID: sessID}, nil +} + +// Login authenticates a user and creates a session. +func (s *authService) Login(ctx context.Context, username, password string) (*AuthResult, error) { + user, err := s.store.GetUserByUsername(ctx, username) + if err != nil { + return nil, fmt.Errorf("get user: %w", err) + } + if user == nil { + return nil, ErrInvalidCredentials + } + if err := s.hasher.Compare(user.PasswordHash, password); err != nil { + return nil, ErrInvalidCredentials + } + + sessID, err := s.sm.CreateSession(ctx, user.ID) + if err != nil { + return nil, fmt.Errorf("create session: %w", err) + } + + return &AuthResult{User: user, SessionID: sessID}, nil +} diff --git a/internal/service/media.go b/internal/service/media.go index 58bb524..1856592 100644 --- a/internal/service/media.go +++ b/internal/service/media.go @@ -53,6 +53,8 @@ var ( ErrShareExpired = errors.New("share expired") ErrMediaNotFound = errors.New("media not found") ErrUnsupportedExtension = errors.New("unsupported file extension") + ErrAlreadyBootstrapped = errors.New("already bootstrapped") + ErrInvalidCredentials = errors.New("invalid credentials") ) // supportedExtensions lists all file extensions accepted by UploadMedia. diff --git a/internal/service/mock.go b/internal/service/mock.go index f174a62..c6c2301 100644 --- a/internal/service/mock.go +++ b/internal/service/mock.go @@ -18,6 +18,7 @@ var ( _ MediaFavoriteService = (*MockMediaService)(nil) _ MediaNoteService = (*MockMediaService)(nil) _ MediaService = (*MockMediaService)(nil) + _ AuthService = (*MockAuthService)(nil) ) // MockMediaService is a fake MediaService for testing. @@ -240,6 +241,25 @@ func (m *MockAdminService) RevokePermission(ctx context.Context, setID, userID i return nil } +// MockAuthService is a fake AuthService for testing. +type MockAuthService struct { + BootstrapFunc func(ctx context.Context, username, password string) (*AuthResult, error) + LoginFunc func(ctx context.Context, username, password string) (*AuthResult, error) +} + +func (m *MockAuthService) Bootstrap(ctx context.Context, username, password string) (*AuthResult, error) { + if m.BootstrapFunc != nil { + return m.BootstrapFunc(ctx, username, password) + } + return nil, nil +} +func (m *MockAuthService) Login(ctx context.Context, username, password string) (*AuthResult, error) { + if m.LoginFunc != nil { + return m.LoginFunc(ctx, username, password) + } + return nil, nil +} + // MockProgressService is a fake ProgressService for testing. type MockProgressService struct { UpdateProgressFunc func(ctx context.Context, sessionID string, userID, mediaID int64, position float64) error diff --git a/internal/service/service.go b/internal/service/service.go index 3a14626..1ed1443 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -78,6 +78,18 @@ type AdminService interface { RevokePermission(ctx context.Context, setID, userID int64) error } +// AuthService handles bootstrap and login operations. +type AuthService interface { + Bootstrap(ctx context.Context, username, password string) (*AuthResult, error) + Login(ctx context.Context, username, password string) (*AuthResult, error) +} + +// AuthResult contains the authenticated user and session ID. +type AuthResult struct { + User *model.User + SessionID string +} + // ProgressService handles playback progress updates. type ProgressService interface { UpdateProgress(ctx context.Context, sessionID string, userID, mediaID int64, position float64) error |
