summaryrefslogtreecommitdiff
path: root/internal/service
diff options
context:
space:
mode:
Diffstat (limited to 'internal/service')
-rw-r--r--internal/service/auth.go86
-rw-r--r--internal/service/media.go2
-rw-r--r--internal/service/mock.go20
-rw-r--r--internal/service/service.go12
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