diff options
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 |
