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
75 changes: 74 additions & 1 deletion backend/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2455,6 +2455,73 @@ const docTemplate = `{
}
}
},
"/api/v1/trips/{tripID}/activities/{activityID}/rsvps/{userID}": {
"delete": {
"description": "Removes a user's RSVP from an activity. Only the activity proposer or a trip admin can remove another user's RSVP.",
"tags": [
"activities"
],
"summary": "Remove activity RSVP",
"parameters": [
{
"type": "string",
"description": "Trip ID",
"name": "tripID",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Activity ID",
"name": "activityID",
"in": "path",
"required": true
},
{
"type": "string",
"description": "User ID of the RSVP to remove",
"name": "userID",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
}
}
}
},
"/api/v1/trips/{tripID}/activity": {
"get": {
"description": "Returns all unread events in the caller's trip feed. Events persist until explicitly dismissed via the mark-read endpoint.",
Expand Down Expand Up @@ -3121,7 +3188,7 @@ const docTemplate = `{
}
},
"delete": {
"description": "Removes a user from a trip",
"description": "Removes a user from a trip. Only admins can remove members.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Authorization description is incorrect for membership removal.

Line 3191 says only admins can remove members, but this endpoint also allows users to remove themselves. Please update the annotation/source comment and regenerate Swagger so the contract matches runtime behavior.

Suggested documentation fix
- "description": "Removes a user from a trip. Only admins can remove members.",
+ "description": "Removes a user from a trip. Trip admins can remove any member, and users can remove themselves.",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/docs/docs.go` at line 3191, The API docs string "Removes a user from
a trip. Only admins can remove members." is incorrect; update the endpoint's
annotation/comment (the description string shown in docs.go for the trip
membership removal endpoint) to reflect that admins can remove other members and
users may remove themselves (e.g., "Removes a user from a trip. Admins may
remove other members; users may remove themselves."), then regenerate the
Swagger/OpenAPI spec so the contract matches runtime behavior.

"tags": [
"memberships"
],
Expand Down Expand Up @@ -3159,6 +3226,12 @@ const docTemplate = `{
"$ref": "#/definitions/errs.APIError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
},
"404": {
"description": "Not Found",
"schema": {
Expand Down
75 changes: 74 additions & 1 deletion backend/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2449,6 +2449,73 @@
}
}
},
"/api/v1/trips/{tripID}/activities/{activityID}/rsvps/{userID}": {
"delete": {
"description": "Removes a user's RSVP from an activity. Only the activity proposer or a trip admin can remove another user's RSVP.",
"tags": [
"activities"
],
"summary": "Remove activity RSVP",
"parameters": [
{
"type": "string",
"description": "Trip ID",
"name": "tripID",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Activity ID",
"name": "activityID",
"in": "path",
"required": true
},
{
"type": "string",
"description": "User ID of the RSVP to remove",
"name": "userID",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
}
}
}
},
"/api/v1/trips/{tripID}/activity": {
"get": {
"description": "Returns all unread events in the caller's trip feed. Events persist until explicitly dismissed via the mark-read endpoint.",
Expand Down Expand Up @@ -3115,7 +3182,7 @@
}
},
"delete": {
"description": "Removes a user from a trip",
"description": "Removes a user from a trip. Only admins can remove members.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Authorization description is incorrect for membership removal.

Line 3185 states that only admins can remove members, but this endpoint also allows users to remove themselves. This creates an inaccurate API contract for clients and test expectations.

Proposed doc fix
-                "description": "Removes a user from a trip. Only admins can remove members.",
+                "description": "Removes a user from a trip. Caller must be a trip admin or the target user themself.",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/docs/swagger.json` at line 3185, Update the operation's "description"
for the membership removal endpoint to reflect that both admins and the user
themself can remove a membership (not "Only admins"), e.g. change the text in
the "description" property associated with the remove-member operation so it
states that admins can remove members and individual users may remove their own
membership; ensure the revised description is applied to the same remove-user
endpoint's "description" field in swagger.json so clients and tests read the
corrected contract.

"tags": [
"memberships"
],
Expand Down Expand Up @@ -3153,6 +3220,12 @@
"$ref": "#/definitions/errs.APIError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/errs.APIError"
}
},
"404": {
"description": "Not Found",
"schema": {
Expand Down
52 changes: 51 additions & 1 deletion backend/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3517,6 +3517,52 @@ paths:
summary: Get activity RSVPs
tags:
- activities
/api/v1/trips/{tripID}/activities/{activityID}/rsvps/{userID}:
delete:
description: Removes a user's RSVP from an activity. Only the activity proposer
or a trip admin can remove another user's RSVP.
parameters:
- description: Trip ID
in: path
name: tripID
required: true
type: string
- description: Activity ID
in: path
name: activityID
required: true
type: string
- description: User ID of the RSVP to remove
in: path
name: userID
required: true
type: string
responses:
"204":
description: No Content
"400":
description: Bad Request
schema:
$ref: '#/definitions/errs.APIError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/errs.APIError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/errs.APIError'
"404":
description: Not Found
schema:
$ref: '#/definitions/errs.APIError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/errs.APIError'
summary: Remove activity RSVP
tags:
- activities
/api/v1/trips/{tripID}/activities/parse-link:
post:
consumes:
Expand Down Expand Up @@ -3978,7 +4024,7 @@ paths:
- memberships
/api/v1/trips/{tripID}/memberships/{userID}:
delete:
description: Removes a user from a trip
description: Removes a user from a trip. Only admins can remove members.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Description is incomplete: self-removal is also permitted.

Per the PR objectives, the caller may be an admin or the user themself. The current text ("Only admins can remove members") would mislead API consumers into thinking a member cannot leave a trip via this endpoint.

Proposed wording
-      description: Removes a user from a trip. Only admins can remove members.
+      description: Removes a user from a trip. Only a trip admin or the user
+        themself can perform this action. The last remaining admin cannot be
+        removed.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
description: Removes a user from a trip. Only admins can remove members.
description: Removes a user from a trip. Only a trip admin or the user
themself can perform this action. The last remaining admin cannot be
removed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/docs/swagger.yaml` at line 4011, Update the OpenAPI description
string for the "Removes a user from a trip" operation to reflect that removal is
allowed by either an admin or the user themself (self-removal), not only admins;
modify the description value currently reading "Removes a user from a trip. Only
admins can remove members." to a clearer sentence such as "Removes a user from a
trip. This action may be performed by an admin or by the user themself
(self-removal)." so API consumers understand permitted callers.

operationId: removeMember
parameters:
- description: Trip ID
Expand All @@ -4002,6 +4048,10 @@ paths:
description: Unauthorized
schema:
$ref: '#/definitions/errs.APIError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/errs.APIError'
"404":
description: Not Found
schema:
Expand Down
39 changes: 37 additions & 2 deletions backend/internal/controllers/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,6 @@ func (ctrl *ActivityController) AddCategoryToActivity(c *fiber.Ctx) error {
return errs.InvalidUUID()
}

// Use request model for validation
req := models.AddCategoryToActivityRequest{
CategoryName: c.Params("categoryName"),
}
Expand Down Expand Up @@ -408,7 +407,6 @@ func (ctrl *ActivityController) RemoveCategoryFromActivity(c *fiber.Ctx) error {
return errs.InvalidUUID()
}

// Use request model for validation
req := models.AddCategoryToActivityRequest{
CategoryName: c.Params("categoryName"),
}
Expand Down Expand Up @@ -506,6 +504,43 @@ func (ctrl *ActivityController) GetActivityRSVPs(c *fiber.Ctx) error {
return c.Status(http.StatusOK).JSON(rsvps)
}

// RemoveActivityRSVP removes a specific user's RSVP from an activity.
// @Summary Remove activity RSVP
// @Description Removes a user's RSVP from an activity. Only the activity proposer or a trip admin can remove another user's RSVP.
// @Tags activities
// @Param tripID path string true "Trip ID"
// @Param activityID path string true "Activity ID"
// @Param userID path string true "User ID of the RSVP to remove"
// @Success 204 "No Content"
// @Failure 400 {object} errs.APIError
// @Failure 401 {object} errs.APIError
// @Failure 403 {object} errs.APIError
// @Failure 404 {object} errs.APIError
// @Failure 500 {object} errs.APIError
// @Router /api/v1/trips/{tripID}/activities/{activityID}/rsvps/{userID} [delete]
func (ctrl *ActivityController) RemoveActivityRSVP(c *fiber.Ctx) error {
tripID, activityID, err := ctrl.parseTripAndActivityIDs(c)
if err != nil {
return err
}

targetUserID, err := validators.ValidateID(c.Params("userID"))
if err != nil {
return errs.InvalidUUID()
}

callerID, err := validators.ExtractUserID(c)
if err != nil {
return err
}

if err := ctrl.activityService.RemoveActivityRSVP(c.Context(), tripID, activityID, callerID, targetUserID); err != nil {
return err
}

return c.SendStatus(http.StatusNoContent)
}

// @Summary Parse link into activity data
// @Description Fetches a URL and extracts structured activity fields (name, description, thumbnail) for form autofill. Supports Airbnb, Booking.com, TikTok, Instagram, and generic travel blog URLs.
// @Tags activities
Expand Down
20 changes: 19 additions & 1 deletion backend/internal/controllers/membership.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,14 @@ func (ctrl *MembershipController) UpdateMembership(c *fiber.Ctx) error {
}

// @Summary Remove member from trip
// @Description Removes a user from a trip
// @Description Removes a user from a trip. Only admins can remove members.
// @Tags memberships
// @Param tripID path string true "Trip ID"
// @Param userID path string true "User ID"
// @Success 204 "No Content"
// @Failure 400 {object} errs.APIError
// @Failure 401 {object} errs.APIError
// @Failure 403 {object} errs.APIError
// @Failure 404 {object} errs.APIError
// @Failure 500 {object} errs.APIError
// @Router /api/v1/trips/{tripID}/memberships/{userID} [delete]
Expand All @@ -233,6 +234,23 @@ func (ctrl *MembershipController) RemoveMember(c *fiber.Ctx) error {
return errs.InvalidUUID()
}

// Admins can remove anyone; members can only remove themselves
authUserID, ok := c.Locals("userID").(string)
if !ok {
return errs.Unauthorized()
}
authUserUUID, err := validators.ValidateID(authUserID)
if err != nil {
return errs.Unauthorized()
}
isAdmin, err := ctrl.membershipService.IsAdmin(c.Context(), tripID, authUserUUID)
if err != nil {
return err
}
if !isAdmin && authUserUUID != userID {
return errs.Forbidden()
}

if err := ctrl.membershipService.RemoveMember(c.Context(), tripID, userID); err != nil {
return err
}
Expand Down
Loading
Loading