diff --git a/repo/event.go b/repo/event.go index 3d3ea01..c6ca000 100644 --- a/repo/event.go +++ b/repo/event.go @@ -2,6 +2,7 @@ package repo import ( "database/sql" + "errors" "github.com/nbtca/saturday/model" "github.com/nbtca/saturday/util" @@ -387,3 +388,32 @@ func CreateEventPhotos(eventId int64, photos []model.EventPhoto, check func(stat } return tx.Commit() } + +var ErrPhotoNotFound = errors.New("photo not found") + +func DeleteEventPhoto(eventId, photoId int64, check func(status string) error) (string, error) { + tx, err := db.Beginx() + if err != nil { + return "", err + } + defer tx.Rollback() + if _, err := tx.Exec("SELECT 1 FROM event WHERE event_id = $1 FOR UPDATE", eventId); err != nil { + return "", err + } + var status string + if err := tx.Get(&status, "SELECT status FROM event_view WHERE event_id = $1", eventId); err != nil { + return "", err + } + if err := check(status); err != nil { + return "", err + } + var key string + err = tx.Get(&key, "DELETE FROM event_photo WHERE photo_id = $1 AND event_id = $2 RETURNING object_key", photoId, eventId) + if errors.Is(err, sql.ErrNoRows) { + return "", ErrPhotoNotFound + } + if err != nil { + return "", err + } + return key, tx.Commit() +} diff --git a/router/event.go b/router/event.go index 33557a0..f0815e4 100644 --- a/router/event.go +++ b/router/event.go @@ -293,6 +293,31 @@ func (EventRouter) UploadPhotos(ctx context.Context, input *UploadEventPhotosInp return util.MakeCommonResponse(event), nil } +func (EventRouter) DeletePhoto(ctx context.Context, input *DeleteEventPhotoInput) (*util.CommonResponse[model.Event], error) { + auth, err := middleware.AuthenticateUser(input.Authorization, "member", "admin") + if err != nil { + return nil, err + } + event, err := middleware.LoadEvent(input.EventId) + if err != nil { + return nil, err + } + if err := service.EventServiceApp.RemovePhoto(event, middleware.CreateIdentityFromAuth(auth), input.PhotoId); err != nil { + if errors.Is(err, repo.ErrPhotoNotFound) { + return nil, huma.Error404NotFound(err.Error()) + } + return nil, huma.Error422UnprocessableEntity(err.Error()) + } + event, err = middleware.LoadEvent(input.EventId) + if err != nil { + return nil, err + } + if err := service.LoadPhotos(&event); err != nil { + return nil, huma.Error500InternalServerError(err.Error()) + } + return util.MakeCommonResponse(event), nil +} + func (EventRouter) RejectCommit(ctx context.Context, input *RejectCommitEventInput) (*util.CommonResponse[model.Event], error) { auth, err := middleware.AuthenticateUser(input.Authorization, "admin") if err != nil { diff --git a/router/input_types.go b/router/input_types.go index 8385551..2ee7820 100644 --- a/router/input_types.go +++ b/router/input_types.go @@ -143,6 +143,12 @@ type UploadEventPhotosInput struct { RawBody multipart.Form } +type DeleteEventPhotoInput struct { + MemberAuthInput + EventPathInput + PhotoId int64 `path:"PhotoId" example:"7" doc:"Photo ID"` +} + type RejectCommitEventInput struct { AdminAuthInput EventPathInput diff --git a/router/main.go b/router/main.go index ef97050..9ec2cf1 100644 --- a/router/main.go +++ b/router/main.go @@ -347,6 +347,14 @@ func SetupRouter() *chi.Mux { }, }, EventRouterApp.UploadPhotos) + huma.Register(api, huma.Operation{ + OperationID: "delete-event-photo", + Method: http.MethodDelete, + Path: "/member/events/{EventId}/photos/{PhotoId}", + Summary: "Delete an event photo", + Tags: []string{"Event", "Member"}, + }, EventRouterApp.DeletePhoto) + // Admin authenticated endpoints huma.Register(api, huma.Operation{ OperationID: "create-members", diff --git a/service/event.go b/service/event.go index e0905d9..b014eeb 100644 --- a/service/event.go +++ b/service/event.go @@ -907,8 +907,16 @@ const ( maxPhotosPerEvent = 20 ) +func isPhotoEditable(status string) bool { + return status == util.Accepted || status == util.Committed +} + +func isPhotoOwner(event model.Event, identity model.Identity) bool { + return identity.Role == "admin" || (identity.Id != "" && event.MemberId == identity.Id) +} + func checkPhotoCapacity(status string, count, adding int) error { - if status != util.Accepted && status != util.Committed { + if !isPhotoEditable(status) { return errors.New("photos can only be added to accepted or committed events") } if count+adding > maxPhotosPerEvent { @@ -918,7 +926,7 @@ func checkPhotoCapacity(status string, count, adding int) error { } func ValidatePhotoUpload(event model.Event, identity model.Identity, files []PhotoUpload) ([]string, error) { - if identity.Role != "admin" && (identity.Id == "" || event.MemberId != identity.Id) { + if !isPhotoOwner(event, identity) { return nil, errors.New("only the assigned member or an admin can add photos") } if len(files) == 0 { @@ -988,6 +996,25 @@ func (service EventService) AddPhotos(event *model.Event, identity model.Identit return nil } +func (service EventService) RemovePhoto(event model.Event, identity model.Identity, photoId int64) error { + if !isPhotoOwner(event, identity) { + return errors.New("only the assigned member or an admin can remove photos") + } + key, err := repo.DeleteEventPhoto(event.EventId, photoId, func(status string) error { + if !isPhotoEditable(status) { + return errors.New("photos can only be removed from accepted or committed events") + } + return nil + }) + if err != nil { + return err + } + if err := util.Photos.Delete(key); err != nil { + util.Logger.Error("delete removed photo failed: ", err) + } + return nil +} + func LoadPhotos(event *model.Event) error { photos, err := repo.GetEventPhotos(event.EventId) if err != nil { diff --git a/service/event_test.go b/service/event_test.go index c5a03e5..6713793 100644 --- a/service/event_test.go +++ b/service/event_test.go @@ -119,3 +119,17 @@ func TestSignPhotos(t *testing.T) { t.Errorf("err=%v url=%q", err, event.Photos[0].Url) } } + +func TestRemovePhotoRejectsOtherMembers(t *testing.T) { + store := &memoryStore{} + util.Photos = store + event := model.Event{EventId: 1, MemberId: "2333333333", Status: util.Accepted} + for _, identity := range []model.Identity{{Id: "1111111111", Role: "member"}, {Role: "client"}} { + if err := service.EventServiceApp.RemovePhoto(event, identity, 1); err == nil { + t.Errorf("%v removed a photo", identity) + } + } + if len(store.deleted) != 0 { + t.Errorf("deleted %v", store.deleted) + } +}