Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions repo/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package repo

import (
"database/sql"
"errors"

"github.com/nbtca/saturday/model"
"github.com/nbtca/saturday/util"
Expand Down Expand Up @@ -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()
}
25 changes: 25 additions & 0 deletions router/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions router/input_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions router/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 29 additions & 2 deletions service/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions service/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading