package albums import ( "context" "errors" "strings" "shared-album/internal/auth" "shared-album/internal/httpx" "shared-album/internal/models" "shared-album/internal/roles" "shared-album/internal/sse" "shared-album/internal/storage" ) type MemberLookup interface { Role(ctx context.Context, albumID, userID string) (string, error) } type PhotoLookup interface { PhotoInAlbum(ctx context.Context, photoID, albumID string) (bool, error) } type Service struct { store *Store members MemberLookup photos PhotoLookup hub *sse.Hub storage *storage.Client } func NewService(store *Store, members MemberLookup, hub *sse.Hub, storage *storage.Client, photos PhotoLookup) *Service { return &Service{store: store, members: members, photos: photos, hub: hub, storage: storage} } type CreateInput struct { Title string Description string } func (s *Service) Create(ctx context.Context, userID string, in CreateInput) (*models.Album, error) { title := strings.TrimSpace(in.Title) if title == "" { return nil, httpx.BadRequest("invalid_title", "title is required") } a := &models.Album{ ID: auth.NewID(), OwnerID: userID, Title: title, Description: strings.TrimSpace(in.Description), } if err := s.store.Create(ctx, a); err != nil { return nil, err } return a, nil } func (s *Service) List(ctx context.Context, userID string) ([]models.Album, error) { list, err := s.store.ListForUser(ctx, userID) if err != nil { return nil, err } for i := range list { s.attachCoverURL(ctx, &list[i]) } return list, nil } func (s *Service) attachCoverURL(ctx context.Context, a *models.Album) { if a == nil || a.CoverThumbKey == "" || s.storage == nil { return } if url, err := s.storage.PresignGet(ctx, a.CoverThumbKey); err == nil { a.CoverURL = url } } func (s *Service) Access(ctx context.Context, albumID, userID string) (*models.Album, string, error) { a, err := s.store.GetByID(ctx, albumID) if err != nil { if errors.Is(err, ErrNotFound) { return nil, "", httpx.NotFound("album_not_found", "album not found") } return nil, "", err } s.attachCoverURL(ctx, a) if a.OwnerID == userID { return a, roles.Owner, nil } role, err := s.members.Role(ctx, albumID, userID) if err != nil { return nil, "", err } if role == "" { return nil, "", httpx.NotFound("album_not_found", "album not found") } return a, role, nil } func (s *Service) Get(ctx context.Context, albumID, userID string) (*models.Album, string, error) { return s.Access(ctx, albumID, userID) } type UpdateInput struct { Title *string Description *string CoverPhotoID *string ClearCover bool } func (s *Service) Update(ctx context.Context, albumID, userID string, in UpdateInput) (*models.Album, error) { a, role, err := s.Access(ctx, albumID, userID) if err != nil { return nil, err } if !roles.CanEditAlbum(role) { return nil, httpx.Forbidden("forbidden", "only owner and admin can edit the album") } if in.Title != nil { title := strings.TrimSpace(*in.Title) if title == "" { return nil, httpx.BadRequest("invalid_title", "title cannot be empty") } a.Title = title } if in.Description != nil { a.Description = strings.TrimSpace(*in.Description) } if in.ClearCover { a.CoverPhotoID = nil } else if in.CoverPhotoID != nil { ok, err := s.photos.PhotoInAlbum(ctx, *in.CoverPhotoID, albumID) if err != nil { return nil, err } if !ok { return nil, httpx.BadRequest("invalid_cover", "cover photo must belong to the album") } a.CoverPhotoID = in.CoverPhotoID } if err := s.store.Update(ctx, a); err != nil { return nil, err } if fresh, err := s.store.GetByID(ctx, albumID); err == nil { a.CoverThumbKey = fresh.CoverThumbKey a.CoverURL = "" s.attachCoverURL(ctx, a) } s.hub.Publish(albumID, "album.updated", a) return a, nil } func (s *Service) Delete(ctx context.Context, albumID, userID string) error { a, role, err := s.Access(ctx, albumID, userID) if err != nil { return err } if role != roles.Owner { return httpx.Forbidden("forbidden", "only the owner can delete the album") } if err := s.store.Delete(ctx, a.ID); err != nil { return err } if s.storage != nil { _ = s.storage.DeletePrefix(ctx, "albums/"+a.ID+"/") } s.hub.Publish(albumID, "album.deleted", map[string]string{"id": a.ID}) return nil }