Skip to content

Commit 8dcedd5

Browse files
Merge pull request #20 from CAPY-RPI/feature/caps-66-recc-orgs-and-events
Feature/caps 66 recc orgs and events
2 parents 75d2ce2 + 19cf65e commit 8dcedd5

13 files changed

Lines changed: 330 additions & 81 deletions

File tree

internal/database/mocks/Querier.go

Lines changed: 20 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/database/querier.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/database/queries.sql

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,9 @@ ORDER BY e.event_time DESC
8787
LIMIT $2 OFFSET $3;
8888

8989
-- name: CreateEvent :one
90-
WITH updated AS (
91-
INSERT INTO events (title, location, event_time, description)
92-
VALUES ($1, $2, $3, $4)
93-
RETURNING *
94-
)
95-
SELECT v.* FROM events_with_org_ids v
96-
WHERE v.eid = (SELECT eid FROM updated);
90+
INSERT INTO events (title, location, event_time, description)
91+
VALUES ($1, $2, $3, $4)
92+
RETURNING *;
9793

9894
-- name: UpdateEvent :one
9995
WITH updated AS (
@@ -105,8 +101,22 @@ WITH updated AS (
105101
WHERE eid = $1
106102
RETURNING *
107103
)
108-
SELECT v.* FROM events_with_org_ids v
109-
WHERE v.eid = $1;
104+
SELECT
105+
u.eid,
106+
u.location,
107+
u.event_time,
108+
u.description,
109+
u.date_created,
110+
u.date_modified,
111+
u.title,
112+
COALESCE(hosts.org_ids, ARRAY[]::uuid[]) AS org_ids
113+
FROM updated u
114+
LEFT JOIN (
115+
SELECT eh.eid, ARRAY_AGG(eh.oid)::uuid[] AS org_ids
116+
FROM event_hosting eh
117+
WHERE eh.eid = $1
118+
GROUP BY eh.eid
119+
) hosts ON hosts.eid = u.eid;
110120

111121
-- name: DeleteEvent :exec
112122
DELETE FROM events WHERE eid = $1;

internal/database/queries.sql.go

Lines changed: 21 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/handler/events.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
// @Param limit query int false "Limit (default 20, max 100)"
2222
// @Param offset query int false "Offset (default 0)"
2323
// @Success 200 {array} dto.EventResponse
24-
// @Security CookieAuth
2524
// @Router /events [get]
2625
func (h *Handler) ListEvents(w http.ResponseWriter, r *http.Request) {
2726
limit, offset := parsePagination(r)
@@ -91,7 +90,13 @@ func (h *Handler) CreateEvent(w http.ResponseWriter, r *http.Request) {
9190
return
9291
}
9392

94-
h.respondJSON(w, http.StatusCreated, toEventResponse(event))
93+
createdEvent, err := h.queries.GetEventByID(r.Context(), event.Eid)
94+
if err != nil {
95+
h.handleDBError(w, err)
96+
return
97+
}
98+
99+
h.respondJSON(w, http.StatusCreated, toEventResponse(createdEvent))
95100
}
96101

97102
// GetEvent gets an event by ID

internal/handler/organizations.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
// @Param limit query int false "Limit (default 20, max 100)"
2323
// @Param offset query int false "Offset (default 0)"
2424
// @Success 200 {array} dto.OrganizationResponse
25-
// @Security CookieAuth
2625
// @Router /organizations [get]
2726
func (h *Handler) ListOrganizations(w http.ResponseWriter, r *http.Request) {
2827
limit, offset := parsePagination(r)
@@ -316,17 +315,29 @@ func (h *Handler) RemoveOrgMember(w http.ResponseWriter, r *http.Request) {
316315
return
317316
}
318317

319-
if _, ok := h.requireOrgAdmin(w, r, oid); !ok {
320-
return
321-
}
322-
323318
uidStr := chi.URLParam(r, "uid")
324319
uid, err := uuid.Parse(uidStr)
325320
if err != nil {
326321
h.respondError(w, http.StatusBadRequest, "Invalid user ID")
327322
return
328323
}
329324

325+
switch middleware.GetAuthType(r.Context()) {
326+
case "bot":
327+
// Bots retain full access to remove members on behalf of users.
328+
default:
329+
authenticatedUID, _, ok := h.requireAuthenticatedUser(w, r)
330+
if !ok {
331+
return
332+
}
333+
334+
if uid != authenticatedUID {
335+
if _, ok := h.requireOrgAdmin(w, r, oid); !ok {
336+
return
337+
}
338+
}
339+
}
340+
330341
if err := h.queries.RemoveOrgMember(r.Context(), database.RemoveOrgMemberParams{
331342
Uid: uid,
332343
Oid: oid,

internal/handler/organizations_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/capyrpi/api/internal/middleware"
1717
"github.com/go-chi/chi/v5"
1818
"github.com/google/uuid"
19+
"github.com/jackc/pgx/v5/pgtype"
1920
"github.com/stretchr/testify/assert"
2021
"github.com/stretchr/testify/mock"
2122
)
@@ -157,3 +158,80 @@ func TestAddOrgMemberAllowsSelfJoin(t *testing.T) {
157158

158159
assert.Equal(t, http.StatusCreated, rr.Code)
159160
}
161+
162+
func TestRemoveOrgMemberAuthorization(t *testing.T) {
163+
oid := uuid.New()
164+
selfUID := uuid.New()
165+
otherUID := uuid.New()
166+
167+
tests := []struct {
168+
name string
169+
authUID uuid.UUID
170+
targetUID uuid.UUID
171+
setupMock func(*mocks.Querier)
172+
expectedStatus int
173+
}{
174+
{
175+
name: "MemberCanRemoveSelf",
176+
authUID: selfUID,
177+
targetUID: selfUID,
178+
setupMock: func(m *mocks.Querier) {
179+
m.On("RemoveOrgMember", mock.Anything, database.RemoveOrgMemberParams{
180+
Uid: selfUID,
181+
Oid: oid,
182+
}).Return(nil)
183+
},
184+
expectedStatus: http.StatusNoContent,
185+
},
186+
{
187+
name: "AdminCanRemoveOtherMember",
188+
authUID: selfUID,
189+
targetUID: otherUID,
190+
setupMock: func(m *mocks.Querier) {
191+
m.On("IsOrgAdmin", mock.Anything, database.IsOrgAdminParams{
192+
Uid: selfUID,
193+
Oid: oid,
194+
}).Return(pgtype.Bool{Bool: true, Valid: true}, nil)
195+
m.On("RemoveOrgMember", mock.Anything, database.RemoveOrgMemberParams{
196+
Uid: otherUID,
197+
Oid: oid,
198+
}).Return(nil)
199+
},
200+
expectedStatus: http.StatusNoContent,
201+
},
202+
{
203+
name: "NonAdminCannotRemoveOtherMember",
204+
authUID: selfUID,
205+
targetUID: otherUID,
206+
setupMock: func(m *mocks.Querier) {
207+
m.On("IsOrgAdmin", mock.Anything, database.IsOrgAdminParams{
208+
Uid: selfUID,
209+
Oid: oid,
210+
}).Return(pgtype.Bool{Bool: false, Valid: true}, nil)
211+
},
212+
expectedStatus: http.StatusForbidden,
213+
},
214+
}
215+
216+
for _, tt := range tests {
217+
t.Run(tt.name, func(t *testing.T) {
218+
mockQueries := mocks.NewQuerier(t)
219+
tt.setupMock(mockQueries)
220+
221+
h := handler.New(mockQueries, &config.Config{})
222+
223+
req := httptest.NewRequest(http.MethodDelete, "/organizations/"+oid.String()+"/members/"+tt.targetUID.String(), nil)
224+
req = req.WithContext(context.WithValue(context.Background(), middleware.UserClaimsKey, &middleware.UserClaims{UserID: tt.authUID.String()}))
225+
226+
rctx := chi.NewRouteContext()
227+
rctx.URLParams.Add("oid", oid.String())
228+
rctx.URLParams.Add("uid", tt.targetUID.String())
229+
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
230+
231+
rr := httptest.NewRecorder()
232+
http.HandlerFunc(h.RemoveOrgMember).ServeHTTP(rr, req)
233+
234+
assert.Equal(t, tt.expectedStatus, rr.Code)
235+
})
236+
}
237+
}

0 commit comments

Comments
 (0)