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
40 changes: 20 additions & 20 deletions internal/database/mocks/Querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 19 additions & 9 deletions internal/database/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,9 @@ ORDER BY e.event_time DESC
LIMIT $2 OFFSET $3;

-- name: CreateEvent :one
WITH updated AS (
INSERT INTO events (title, location, event_time, description)
VALUES ($1, $2, $3, $4)
RETURNING *
)
SELECT v.* FROM events_with_org_ids v
WHERE v.eid = (SELECT eid FROM updated);
INSERT INTO events (title, location, event_time, description)
VALUES ($1, $2, $3, $4)
RETURNING *;

-- name: UpdateEvent :one
WITH updated AS (
Expand All @@ -105,8 +101,22 @@ WITH updated AS (
WHERE eid = $1
RETURNING *
)
SELECT v.* FROM events_with_org_ids v
WHERE v.eid = $1;
SELECT
u.eid,
u.location,
u.event_time,
u.description,
u.date_created,
u.date_modified,
u.title,
COALESCE(hosts.org_ids, ARRAY[]::uuid[]) AS org_ids
FROM updated u
LEFT JOIN (
SELECT eh.eid, ARRAY_AGG(eh.oid)::uuid[] AS org_ids
FROM event_hosting eh
WHERE eh.eid = $1
GROUP BY eh.eid
) hosts ON hosts.eid = u.eid;

-- name: DeleteEvent :exec
DELETE FROM events WHERE eid = $1;
Expand Down
33 changes: 21 additions & 12 deletions internal/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions internal/handler/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
// @Param limit query int false "Limit (default 20, max 100)"
// @Param offset query int false "Offset (default 0)"
// @Success 200 {array} dto.EventResponse
// @Security CookieAuth
// @Router /events [get]
func (h *Handler) ListEvents(w http.ResponseWriter, r *http.Request) {
limit, offset := parsePagination(r)
Expand Down Expand Up @@ -91,7 +90,13 @@ func (h *Handler) CreateEvent(w http.ResponseWriter, r *http.Request) {
return
}

h.respondJSON(w, http.StatusCreated, toEventResponse(event))
createdEvent, err := h.queries.GetEventByID(r.Context(), event.Eid)
if err != nil {
h.handleDBError(w, err)
return
}

h.respondJSON(w, http.StatusCreated, toEventResponse(createdEvent))
}

// GetEvent gets an event by ID
Expand Down
21 changes: 16 additions & 5 deletions internal/handler/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
// @Param limit query int false "Limit (default 20, max 100)"
// @Param offset query int false "Offset (default 0)"
// @Success 200 {array} dto.OrganizationResponse
// @Security CookieAuth
// @Router /organizations [get]
func (h *Handler) ListOrganizations(w http.ResponseWriter, r *http.Request) {
limit, offset := parsePagination(r)
Expand Down Expand Up @@ -316,17 +315,29 @@ func (h *Handler) RemoveOrgMember(w http.ResponseWriter, r *http.Request) {
return
}

if _, ok := h.requireOrgAdmin(w, r, oid); !ok {
return
}

uidStr := chi.URLParam(r, "uid")
uid, err := uuid.Parse(uidStr)
if err != nil {
h.respondError(w, http.StatusBadRequest, "Invalid user ID")
return
}

switch middleware.GetAuthType(r.Context()) {
case "bot":
// Bots retain full access to remove members on behalf of users.
default:
authenticatedUID, _, ok := h.requireAuthenticatedUser(w, r)
if !ok {
return
}

if uid != authenticatedUID {
if _, ok := h.requireOrgAdmin(w, r, oid); !ok {
return
}
}
}

if err := h.queries.RemoveOrgMember(r.Context(), database.RemoveOrgMemberParams{
Uid: uid,
Oid: oid,
Expand Down
78 changes: 78 additions & 0 deletions internal/handler/organizations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/capyrpi/api/internal/middleware"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
Expand Down Expand Up @@ -157,3 +158,80 @@ func TestAddOrgMemberAllowsSelfJoin(t *testing.T) {

assert.Equal(t, http.StatusCreated, rr.Code)
}

func TestRemoveOrgMemberAuthorization(t *testing.T) {
oid := uuid.New()
selfUID := uuid.New()
otherUID := uuid.New()

tests := []struct {
name string
authUID uuid.UUID
targetUID uuid.UUID
setupMock func(*mocks.Querier)
expectedStatus int
}{
{
name: "MemberCanRemoveSelf",
authUID: selfUID,
targetUID: selfUID,
setupMock: func(m *mocks.Querier) {
m.On("RemoveOrgMember", mock.Anything, database.RemoveOrgMemberParams{
Uid: selfUID,
Oid: oid,
}).Return(nil)
},
expectedStatus: http.StatusNoContent,
},
{
name: "AdminCanRemoveOtherMember",
authUID: selfUID,
targetUID: otherUID,
setupMock: func(m *mocks.Querier) {
m.On("IsOrgAdmin", mock.Anything, database.IsOrgAdminParams{
Uid: selfUID,
Oid: oid,
}).Return(pgtype.Bool{Bool: true, Valid: true}, nil)
m.On("RemoveOrgMember", mock.Anything, database.RemoveOrgMemberParams{
Uid: otherUID,
Oid: oid,
}).Return(nil)
},
expectedStatus: http.StatusNoContent,
},
{
name: "NonAdminCannotRemoveOtherMember",
authUID: selfUID,
targetUID: otherUID,
setupMock: func(m *mocks.Querier) {
m.On("IsOrgAdmin", mock.Anything, database.IsOrgAdminParams{
Uid: selfUID,
Oid: oid,
}).Return(pgtype.Bool{Bool: false, Valid: true}, nil)
},
expectedStatus: http.StatusForbidden,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockQueries := mocks.NewQuerier(t)
tt.setupMock(mockQueries)

h := handler.New(mockQueries, &config.Config{})

req := httptest.NewRequest(http.MethodDelete, "/organizations/"+oid.String()+"/members/"+tt.targetUID.String(), nil)
req = req.WithContext(context.WithValue(context.Background(), middleware.UserClaimsKey, &middleware.UserClaims{UserID: tt.authUID.String()}))

rctx := chi.NewRouteContext()
rctx.URLParams.Add("oid", oid.String())
rctx.URLParams.Add("uid", tt.targetUID.String())
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))

rr := httptest.NewRecorder()
http.HandlerFunc(h.RemoveOrgMember).ServeHTTP(rr, req)

assert.Equal(t, tt.expectedStatus, rr.Code)
})
}
}
Loading
Loading