From 35e031bd69a3a5e31cc052b86a1472615349b8f5 Mon Sep 17 00:00:00 2001 From: David Oduneye Date: Wed, 21 Feb 2024 18:19:43 -0500 Subject: [PATCH 1/3] wrapped routes with perms --- backend/src/auth/permissions.go | 118 +++++++++++---------- backend/src/controllers/auth.go | 10 +- backend/src/controllers/category.go | 2 +- backend/src/controllers/club.go | 4 +- backend/src/controllers/tag.go | 40 +++++-- backend/src/docs/docs.go | 101 ++++++++++++++---- backend/src/docs/swagger.json | 101 ++++++++++++++---- backend/src/docs/swagger.yaml | 71 ++++++++++--- backend/src/middleware/auth.go | 29 +++-- backend/src/middleware/club.go | 7 +- backend/src/middleware/middleware.go | 10 +- backend/src/middleware/user.go | 7 +- backend/src/models/tag.go | 7 +- backend/src/server/routes/auth.go | 8 +- backend/src/server/routes/category.go | 21 +++- backend/src/server/routes/club.go | 27 +++-- backend/src/server/routes/club_contact.go | 5 +- backend/src/server/routes/club_event.go | 3 +- backend/src/server/routes/club_member.go | 5 +- backend/src/server/routes/club_tag.go | 7 +- backend/src/server/routes/event.go | 3 +- backend/src/server/routes/tag.go | 11 +- backend/src/server/routes/user.go | 20 +++- backend/src/server/routes/user_follower.go | 4 +- backend/src/server/routes/user_member.go | 2 +- backend/src/server/routes/user_tag.go | 3 +- backend/src/server/server.go | 38 +++---- backend/src/services/tag.go | 25 ++++- backend/src/transactions/tag.go | 10 ++ 29 files changed, 492 insertions(+), 207 deletions(-) diff --git a/backend/src/auth/permissions.go b/backend/src/auth/permissions.go index ab361fa5f..88071381e 100644 --- a/backend/src/auth/permissions.go +++ b/backend/src/auth/permissions.go @@ -5,82 +5,90 @@ import "github.com/GenerateNU/sac/backend/src/models" type Permission string const ( - UserReadAll Permission = "user:readAll" - UserRead Permission = "user:read" - UserWrite Permission = "user:write" - UserDelete Permission = "user:delete" + // User Management + UserRead Permission = "user:read" + UserWrite Permission = "user:write" + UserDelete Permission = "user:delete" + UserManageProfile Permission = "user:manage_profile" + UserReadAll Permission = "user:read_all" - TagReadAll Permission = "tag:readAll" - TagRead Permission = "tag:read" - TagWrite Permission = "tag:write" - TagCreate Permission = "tag:create" - TagDelete Permission = "tag:delete" + // Tag Management + TagRead Permission = "tag:read" + TagCreate Permission = "tag:create" + TagWrite Permission = "tag:write" + TagDelete Permission = "tag:delete" - ClubReadAll Permission = "club:readAll" - ClubRead Permission = "club:read" - ClubWrite Permission = "club:write" - ClubCreate Permission = "club:create" - ClubDelete Permission = "club:delete" + // Club Management + ClubRead Permission = "club:read" + ClubCreate Permission = "club:create" + ClubWrite Permission = "club:write" + ClubDelete Permission = "club:delete" + ClubManageMembers Permission = "club:manage_members" + ClubManageFollowers Permission = "club:manage_followers" - PointOfContactReadAll Permission = "pointOfContact:readAll" - PointOfContactRead Permission = "pointOfContact:read" - PointOfContactCreate Permission = "pointOfContact:create" - PointOfContactWrite Permission = "pointOfContact:write" - PointOfContactDelete Permission = "pointOfContact:delete" + // Point of Contact Management + PointOfContactRead Permission = "pointOfContact:read" + PointOfContactCreate Permission = "pointOfContact:create" + PointOfContactWrite Permission = "pointOfContact:write" + PointOfContactDelete Permission = "pointOfContact:delete" - CommentReadAll Permission = "comment:readAll" - CommentRead Permission = "comment:read" - CommentCreate Permission = "comment:create" - CommentWrite Permission = "comment:write" - CommentDelete Permission = "comment:delete" + // Comment Management + CommentRead Permission = "comment:read" + CommentCreate Permission = "comment:create" + CommentWrite Permission = "comment:write" + CommentDelete Permission = "comment:delete" - EventReadAll Permission = "event:readAll" - EventRead Permission = "event:read" - EventWrite Permission = "event:write" - EventCreate Permission = "event:create" - EventDelete Permission = "event:delete" + // Event Management + EventRead Permission = "event:read" + EventCreate Permission = "event:create" + EventWrite Permission = "event:write" + EventDelete Permission = "event:delete" + EventManageRSVPs Permission = "event:manage_rsvps" - ContactReadAll Permission = "contact:readAll" - ContactRead Permission = "contact:read" - ContactWrite Permission = "contact:write" - ContactCreate Permission = "contact:create" - ContactDelete Permission = "contact:delete" + // Contact Management + ContactRead Permission = "contact:read" + ContactCreate Permission = "contact:create" + ContactWrite Permission = "contact:write" + ContactDelete Permission = "contact:delete" - CategoryReadAll Permission = "category:readAll" - CategoryRead Permission = "category:read" - CategoryWrite Permission = "category:write" - CategoryCreate Permission = "category:create" - CategoryDelete Permission = "category:delete" + // Category Management + CategoryRead Permission = "category:read" + CategoryCreate Permission = "category:create" + CategoryWrite Permission = "category:write" + CategoryDelete Permission = "category:delete" - NotificationReadAll Permission = "notification:readAll" - NotificationRead Permission = "notification:read" - NotificationWrite Permission = "notification:write" - NotificationCreate Permission = "notification:create" - NotificationDelete Permission = "notification:delete" + // Notification Management + NotificationRead Permission = "notification:read" + NotificationCreate Permission = "notification:create" + NotificationWrite Permission = "notification:write" + NotificationDelete Permission = "notification:delete" + + // Global Permissions (for convenience) + ReadAll Permission = "all:read" + CreateAll Permission = "all:create" + WriteAll Permission = "all:write" + DeleteAll Permission = "all:delete" ) var rolePermissions = map[models.UserRole][]Permission{ models.Super: { - UserRead, UserReadAll, UserWrite, UserDelete, + UserRead, UserWrite, UserDelete, UserManageProfile, UserReadAll, TagRead, TagCreate, TagWrite, TagDelete, - ClubRead, ClubCreate, ClubWrite, ClubDelete, + ClubRead, ClubCreate, ClubWrite, ClubDelete, ClubManageMembers, ClubManageFollowers, PointOfContactRead, PointOfContactCreate, PointOfContactWrite, PointOfContactDelete, CommentRead, CommentCreate, CommentWrite, CommentDelete, - EventRead, EventCreate, EventWrite, EventDelete, + EventRead, EventCreate, EventWrite, EventDelete, EventManageRSVPs, ContactRead, ContactCreate, ContactWrite, ContactDelete, CategoryRead, CategoryCreate, CategoryWrite, CategoryDelete, NotificationRead, NotificationCreate, NotificationWrite, NotificationDelete, - UserReadAll, TagReadAll, ClubReadAll, PointOfContactReadAll, CommentReadAll, EventReadAll, ContactReadAll, CategoryReadAll, NotificationReadAll, + ReadAll, CreateAll, WriteAll, DeleteAll, }, models.Student: { - UserRead, + UserRead, UserManageProfile, TagRead, - ClubRead, - PointOfContactRead, - CommentRead, - EventRead, - ContactRead, - CategoryRead, + ClubRead, EventRead, + CommentRead, CommentCreate, + ContactRead, PointOfContactRead, NotificationRead, }, } diff --git a/backend/src/controllers/auth.go b/backend/src/controllers/auth.go index e46bf507f..549085556 100644 --- a/backend/src/controllers/auth.go +++ b/backend/src/controllers/auth.go @@ -165,7 +165,7 @@ func (a *AuthController) Logout(c *fiber.Ctx) error { // @Failure 401 {object} errors.Error // @Failure 404 {object} errors.Error // @Failure 500 {object} errors.Error -// @Router /auth/update-password [post] +// @Router /auth/update-password/:userID [post] func (a *AuthController) UpdatePassword(c *fiber.Ctx) error { var userBody models.UpdatePasswordRequestBody @@ -173,13 +173,7 @@ func (a *AuthController) UpdatePassword(c *fiber.Ctx) error { return errors.FailedToParseRequestBody.FiberError(c) } - claims, err := auth.From(c) - if err != nil { - return err.FiberError(c) - } - - err = a.authService.UpdatePassword(claims.Issuer, userBody) - if err != nil { + if err := a.authService.UpdatePassword(c.Params("userID"), userBody); err != nil { return err.FiberError(c) } diff --git a/backend/src/controllers/category.go b/backend/src/controllers/category.go index e43a6f85e..7179e694a 100644 --- a/backend/src/controllers/category.go +++ b/backend/src/controllers/category.go @@ -62,7 +62,7 @@ func (cat *CategoryController) CreateCategory(c *fiber.Ctx) error { // @Failure 404 {string} errors.Error // @Failure 500 {string} errors.Error // @Router /category/ [get] -func (cat *CategoryController) GetCategories(c *fiber.Ctx) error { +func (cat *CategoryController) GetAllCategories(c *fiber.Ctx) error { defaultLimit := 10 defaultPage := 1 diff --git a/backend/src/controllers/club.go b/backend/src/controllers/club.go index 7a0c7275f..39b2ec04c 100644 --- a/backend/src/controllers/club.go +++ b/backend/src/controllers/club.go @@ -15,7 +15,7 @@ func NewClubController(clubService services.ClubServiceInterface) *ClubControlle return &ClubController{clubService: clubService} } -// GetAllClubs godoc +// GetClubs godoc // // @Summary Retrieve all clubs // @Description Retrieves all clubs @@ -28,7 +28,7 @@ func NewClubController(clubService services.ClubServiceInterface) *ClubControlle // @Failure 400 {object} errors.Error // @Failure 500 {object} errors.Error // @Router /club/ [get] -func (cl *ClubController) GetAllClubs(c *fiber.Ctx) error { +func (cl *ClubController) GetClubs(c *fiber.Ctx) error { var queryParams models.ClubQueryParams queryParams.Limit = 10 // default limit diff --git a/backend/src/controllers/tag.go b/backend/src/controllers/tag.go index 647d7f6ea..26f7fef44 100644 --- a/backend/src/controllers/tag.go +++ b/backend/src/controllers/tag.go @@ -1,6 +1,8 @@ package controllers import ( + "strconv" + "github.com/GenerateNU/sac/backend/src/errors" "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/services" @@ -16,9 +18,7 @@ func NewTagController(tagService services.TagServiceInterface) *TagController { return &TagController{tagService: tagService} } -// GetAllTags godoc - -// CreateTag creates a new tag. +// GetTags godoc // // @Summary Retrieve all tags // @Description Retrieves all tags @@ -29,12 +29,38 @@ func NewTagController(tagService services.TagServiceInterface) *TagController { // @Param page query int false "Page" // @Success 200 {object} []models.Tag // @Failure 400 {object} errors.Error -// @Failure 401 {object} errors.Error // @Failure 404 {object} errors.Error // @Failure 500 {object} errors.Error // @Router /tags [get] +func (t *TagController) GetTags(c *fiber.Ctx) error { + defaultLimit := 10 + defaultPage := 1 + + tags, err := t.tagService.GetTags(c.Query("limit", strconv.Itoa(defaultLimit)), c.Query("page", strconv.Itoa(defaultPage))) + if err != nil { + return err.FiberError(c) + } + + return c.Status(fiber.StatusOK).JSON(&tags) +} + +// CreateTag godoc +// +// @Summary Create a tag +// @Description Creates a tag +// @ID create-tag +// @Tags tag +// @Accept json +// @Produce json +// @Param tagBody body models.CreateTagRequestBody true "Tag Body" +// @Success 201 {object} models.Tag +// @Failure 400 {object} errors.Error +// @Failure 401 {object} errors.Error +// @Failure 404 {object} errors.Error +// @Failure 500 {object} errors.Error +// @Router /tags/ [post] func (t *TagController) CreateTag(c *fiber.Ctx) error { - var tagBody models.TagRequestBody + var tagBody models.CreateTagRequestBody if err := c.BodyParser(&tagBody); err != nil { return errors.FailedToParseRequestBody.FiberError(c) @@ -79,7 +105,7 @@ func (t *TagController) GetTag(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param tagID path string true "Tag ID" -// @Param tag body models.TagRequestBody true "Tag" +// @Param tag body models.UpdateTagRequestBody true "Tag" // @Success 200 {object} models.Tag // @Failure 400 {object} errors.Error // @Failure 401 {object} errors.Error @@ -87,7 +113,7 @@ func (t *TagController) GetTag(c *fiber.Ctx) error { // @Failure 500 {object} errors.Error // @Router /tags/{tagID} [put] func (t *TagController) UpdateTag(c *fiber.Ctx) error { - var tagBody models.TagRequestBody + var tagBody models.UpdateTagRequestBody if err := c.BodyParser(&tagBody); err != nil { return errors.FailedToParseRequestBody.FiberError(c) diff --git a/backend/src/docs/docs.go b/backend/src/docs/docs.go index 2fd8e351f..f4567f619 100644 --- a/backend/src/docs/docs.go +++ b/backend/src/docs/docs.go @@ -182,7 +182,7 @@ const docTemplate = `{ } } }, - "/auth/update-password": { + "/auth/update-password/:userID": { "post": { "description": "Updates a user's password", "consumes": [ @@ -2097,6 +2097,59 @@ const docTemplate = `{ "$ref": "#/definitions/errors.Error" } }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errors.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errors.Error" + } + } + } + } + }, + "/tags/": { + "post": { + "description": "Creates a tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tag" + ], + "summary": "Create a tag", + "operationId": "create-tag", + "parameters": [ + { + "description": "Tag Body", + "name": "tagBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreateTagRequestBody" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Tag" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.Error" + } + }, "401": { "description": "Unauthorized", "schema": { @@ -2192,7 +2245,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.TagRequestBody" + "$ref": "#/definitions/models.UpdateTagRequestBody" } } ], @@ -3399,6 +3452,22 @@ const docTemplate = `{ } } }, + "models.CreateTagRequestBody": { + "type": "object", + "required": [ + "category_id", + "name" + ], + "properties": { + "category_id": { + "type": "string" + }, + "name": { + "type": "string", + "maxLength": 255 + } + } + }, "models.CreateUserRequestBody": { "type": "object", "required": [ @@ -3714,22 +3783,6 @@ const docTemplate = `{ } } }, - "models.TagRequestBody": { - "type": "object", - "required": [ - "category_id", - "name" - ], - "properties": { - "category_id": { - "type": "string" - }, - "name": { - "type": "string", - "maxLength": 255 - } - } - }, "models.UpdateClubRequestBody": { "type": "object", "required": [ @@ -3843,6 +3896,18 @@ const docTemplate = `{ } } }, + "models.UpdateTagRequestBody": { + "type": "object", + "properties": { + "category_id": { + "type": "string" + }, + "name": { + "type": "string", + "maxLength": 255 + } + } + }, "models.UpdateUserRequestBody": { "type": "object", "properties": { diff --git a/backend/src/docs/swagger.json b/backend/src/docs/swagger.json index ed347770f..15b74f97b 100644 --- a/backend/src/docs/swagger.json +++ b/backend/src/docs/swagger.json @@ -176,7 +176,7 @@ } } }, - "/auth/update-password": { + "/auth/update-password/:userID": { "post": { "description": "Updates a user's password", "consumes": [ @@ -2091,6 +2091,59 @@ "$ref": "#/definitions/errors.Error" } }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errors.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errors.Error" + } + } + } + } + }, + "/tags/": { + "post": { + "description": "Creates a tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "tag" + ], + "summary": "Create a tag", + "operationId": "create-tag", + "parameters": [ + { + "description": "Tag Body", + "name": "tagBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreateTagRequestBody" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Tag" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.Error" + } + }, "401": { "description": "Unauthorized", "schema": { @@ -2186,7 +2239,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.TagRequestBody" + "$ref": "#/definitions/models.UpdateTagRequestBody" } } ], @@ -3393,6 +3446,22 @@ } } }, + "models.CreateTagRequestBody": { + "type": "object", + "required": [ + "category_id", + "name" + ], + "properties": { + "category_id": { + "type": "string" + }, + "name": { + "type": "string", + "maxLength": 255 + } + } + }, "models.CreateUserRequestBody": { "type": "object", "required": [ @@ -3708,22 +3777,6 @@ } } }, - "models.TagRequestBody": { - "type": "object", - "required": [ - "category_id", - "name" - ], - "properties": { - "category_id": { - "type": "string" - }, - "name": { - "type": "string", - "maxLength": 255 - } - } - }, "models.UpdateClubRequestBody": { "type": "object", "required": [ @@ -3837,6 +3890,18 @@ } } }, + "models.UpdateTagRequestBody": { + "type": "object", + "properties": { + "category_id": { + "type": "string" + }, + "name": { + "type": "string", + "maxLength": 255 + } + } + }, "models.UpdateUserRequestBody": { "type": "object", "properties": { diff --git a/backend/src/docs/swagger.yaml b/backend/src/docs/swagger.yaml index 27aa97ecd..fa7ae8996 100644 --- a/backend/src/docs/swagger.yaml +++ b/backend/src/docs/swagger.yaml @@ -307,6 +307,17 @@ definitions: minimum: 1 type: integer type: object + models.CreateTagRequestBody: + properties: + category_id: + type: string + name: + maxLength: 255 + type: string + required: + - category_id + - name + type: object models.CreateUserRequestBody: properties: college: @@ -533,17 +544,6 @@ definitions: - category_id - name type: object - models.TagRequestBody: - properties: - category_id: - type: string - name: - maxLength: 255 - type: string - required: - - category_id - - name - type: object models.UpdateClubRequestBody: properties: application_link: @@ -622,6 +622,14 @@ definitions: minimum: 1 type: integer type: object + models.UpdateTagRequestBody: + properties: + category_id: + type: string + name: + maxLength: 255 + type: string + type: object models.UpdateUserRequestBody: properties: college: @@ -842,7 +850,7 @@ paths: summary: Refreshes a user's access token tags: - auth - /auth/update-password: + /auth/update-password/:userID: post: consumes: - application/json @@ -2120,6 +2128,41 @@ paths: description: Bad Request schema: $ref: '#/definitions/errors.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/errors.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/errors.Error' + summary: Retrieve all tags + tags: + - tag + /tags/: + post: + consumes: + - application/json + description: Creates a tag + operationId: create-tag + parameters: + - description: Tag Body + in: body + name: tagBody + required: true + schema: + $ref: '#/definitions/models.CreateTagRequestBody' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.Tag' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errors.Error' "401": description: Unauthorized schema: @@ -2132,7 +2175,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/errors.Error' - summary: Retrieve all tags + summary: Create a tag tags: - tag /tags/{tagID}: @@ -2218,7 +2261,7 @@ paths: name: tag required: true schema: - $ref: '#/definitions/models.TagRequestBody' + $ref: '#/definitions/models.UpdateTagRequestBody' produces: - application/json responses: diff --git a/backend/src/middleware/auth.go b/backend/src/middleware/auth.go index 521c3569e..06ccec137 100644 --- a/backend/src/middleware/auth.go +++ b/backend/src/middleware/auth.go @@ -18,7 +18,8 @@ var paths = []string{ "/api/v1/auth/logout", } -func SuperSkipper(h fiber.Handler) fiber.Handler { +// Deprecated +func (m *AuthMiddlewareService) Skip(h fiber.Handler) fiber.Handler { return skip.New(h, func(c *fiber.Ctx) bool { claims, err := auth.From(c) if err != nil { @@ -32,19 +33,31 @@ func SuperSkipper(h fiber.Handler) fiber.Handler { }) } -func (m *MiddlewareService) Authenticate(c *fiber.Ctx) error { +func (m *AuthMiddlewareService) IsSuper(c *fiber.Ctx) bool { + claims, err := auth.From(c) + if err != nil { + _ = err.FiberError(c) + return false + } + if claims == nil { + return false + } + return claims.Role == string(models.Super) +} + +func (m *AuthMiddlewareService) Authenticate(c *fiber.Ctx) error { if slices.Contains(paths, c.Path()) { return c.Next() } token, err := auth.ParseAccessToken(c.Cookies("access_token"), m.AuthSettings.AccessKey) if err != nil { - return errors.FailedToParseAccessToken.FiberError(c) + return errors.Unauthorized.FiberError(c) } claims, ok := token.Claims.(*auth.CustomClaims) if !ok || !token.Valid { - return errors.FailedToValidateAccessToken.FiberError(c) + return errors.Unauthorized.FiberError(c) } if auth.IsBlacklisted(c.Cookies("access_token")) { @@ -56,7 +69,7 @@ func (m *MiddlewareService) Authenticate(c *fiber.Ctx) error { return c.Next() } -func (m *MiddlewareService) Authorize(requiredPermissions ...auth.Permission) func(c *fiber.Ctx) error { +func (m *AuthMiddlewareService) Authorize(requiredPermissions ...auth.Permission) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { claims, fromErr := auth.From(c) if fromErr != nil { @@ -67,13 +80,9 @@ func (m *MiddlewareService) Authorize(requiredPermissions ...auth.Permission) fu return c.Next() } - if c.Cookies("access_token") == "" || c.Cookies("refresh_token") == "" { - return errors.Unauthorized.FiberError(c) - } - role, err := auth.GetRoleFromToken(c.Cookies("access_token"), m.AuthSettings.AccessKey) if err != nil { - return errors.FailedToParseAccessToken.FiberError(c) + return errors.Unauthorized.FiberError(c) } userPermissions := auth.GetPermissions(models.UserRole(*role)) diff --git a/backend/src/middleware/club.go b/backend/src/middleware/club.go index 942fba76a..96f965a11 100644 --- a/backend/src/middleware/club.go +++ b/backend/src/middleware/club.go @@ -10,7 +10,12 @@ import ( "github.com/gofiber/fiber/v2" ) -func (m *MiddlewareService) ClubAuthorizeById(c *fiber.Ctx) error { +// Authorizes admins of the specific club to make this request, skips check if super user +func (m *AuthMiddlewareService) ClubAuthorizeById(c *fiber.Ctx) error { + if m.IsSuper(c) { + return c.Next() + } + clubUUID, err := utilities.ValidateID(c.Params("clubID")) if err != nil { return errors.FailedToValidateID.FiberError(c) diff --git a/backend/src/middleware/middleware.go b/backend/src/middleware/middleware.go index ab09f8e61..5f7aa04e4 100644 --- a/backend/src/middleware/middleware.go +++ b/backend/src/middleware/middleware.go @@ -8,21 +8,23 @@ import ( "gorm.io/gorm" ) -type MiddlewareInterface interface { +type AuthMiddlewareInterface interface { ClubAuthorizeById(c *fiber.Ctx) error UserAuthorizeById(c *fiber.Ctx) error Authenticate(c *fiber.Ctx) error Authorize(requiredPermissions ...auth.Permission) func(c *fiber.Ctx) error + Skip(h fiber.Handler) fiber.Handler + IsSuper(c *fiber.Ctx) bool } -type MiddlewareService struct { +type AuthMiddlewareService struct { DB *gorm.DB Validate *validator.Validate AuthSettings config.AuthSettings } -func NewMiddlewareService(db *gorm.DB, validate *validator.Validate, authSettings config.AuthSettings) *MiddlewareService { - return &MiddlewareService{ +func NewAuthAuthMiddlewareService(db *gorm.DB, validate *validator.Validate, authSettings config.AuthSettings) *AuthMiddlewareService { + return &AuthMiddlewareService{ DB: db, Validate: validate, AuthSettings: authSettings, diff --git a/backend/src/middleware/user.go b/backend/src/middleware/user.go index 16bafe7ec..6767670b8 100644 --- a/backend/src/middleware/user.go +++ b/backend/src/middleware/user.go @@ -7,7 +7,12 @@ import ( "github.com/gofiber/fiber/v2" ) -func (m *MiddlewareService) UserAuthorizeById(c *fiber.Ctx) error { +// Authorizes admins of the specific club to make this request, skips check if super user +func (m *AuthMiddlewareService) UserAuthorizeById(c *fiber.Ctx) error { + if m.IsSuper(c) { + return c.Next() + } + idAsUUID, err := utilities.ValidateID(c.Params("userID")) if err != nil { return errors.FailedToValidateID.FiberError(c) diff --git a/backend/src/models/tag.go b/backend/src/models/tag.go index 36c71381a..560e74d1b 100644 --- a/backend/src/models/tag.go +++ b/backend/src/models/tag.go @@ -14,7 +14,12 @@ type Tag struct { Event []Event `gorm:"many2many:event_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"` } -type TagRequestBody struct { +type CreateTagRequestBody struct { Name string `json:"name" validate:"required,max=255"` CategoryID uuid.UUID `json:"category_id" validate:"required,uuid4"` } + +type UpdateTagRequestBody struct { + Name string `json:"name" validate:"omitempty,max=255"` + CategoryID uuid.UUID `json:"category_id" validate:"omitempty,uuid4"` +} diff --git a/backend/src/server/routes/auth.go b/backend/src/server/routes/auth.go index a3933c62a..6d017c1dc 100644 --- a/backend/src/server/routes/auth.go +++ b/backend/src/server/routes/auth.go @@ -3,12 +3,13 @@ package routes import ( "github.com/GenerateNU/sac/backend/src/config" "github.com/GenerateNU/sac/backend/src/controllers" + "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" "github.com/gofiber/fiber/v2" ) -func Auth(router fiber.Router, authService services.AuthServiceInterface, authSettings config.AuthSettings) { - authController := controllers.NewAuthController(authService, authSettings) +func Auth(router fiber.Router, authService services.AuthServiceInterface, settings config.AuthSettings, authMiddleware *middleware.AuthMiddlewareService) { + authController := controllers.NewAuthController(authService, settings) // api/v1/auth/* auth := router.Group("/auth") @@ -17,5 +18,6 @@ func Auth(router fiber.Router, authService services.AuthServiceInterface, authSe auth.Get("/logout", authController.Logout) auth.Get("/refresh", authController.Refresh) auth.Get("/me", authController.Me) - auth.Post("/update-password", authController.UpdatePassword) + auth.Post("/update-password/:userID", authMiddleware.UserAuthorizeById, authController.UpdatePassword) + // auth.Post("/reset-password/:userID", middleware.Skip(authMiddleware.UserAuthorizeById), authController.ResetPassword) } diff --git a/backend/src/server/routes/category.go b/backend/src/server/routes/category.go index 1bffb59f2..135c83649 100644 --- a/backend/src/server/routes/category.go +++ b/backend/src/server/routes/category.go @@ -1,21 +1,32 @@ package routes import ( + "github.com/GenerateNU/sac/backend/src/auth" "github.com/GenerateNU/sac/backend/src/controllers" + "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" + "gorm.io/gorm" ) -func Category(router fiber.Router, categoryService services.CategoryServiceInterface) fiber.Router { +func CategoryRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate, authMiddleware *middleware.AuthMiddlewareService) { + categoryIDRoute := Category(router, services.NewCategoryService(db, validate), authMiddleware) + + CategoryTag(categoryIDRoute, services.NewCategoryTagService(db, validate)) +} + +func Category(router fiber.Router, categoryService services.CategoryServiceInterface, authMiddleware *middleware.AuthMiddlewareService) fiber.Router { categoryController := controllers.NewCategoryController(categoryService) + // api/v1/categories/* categories := router.Group("/categories") - categories.Post("/", categoryController.CreateCategory) - categories.Get("/", categoryController.GetCategories) + categories.Post("/", authMiddleware.Authorize(auth.CreateAll), categoryController.CreateCategory) + categories.Get("/", categoryController.GetAllCategories) categories.Get("/:categoryID", categoryController.GetCategory) - categories.Delete("/:categoryID", categoryController.DeleteCategory) - categories.Patch("/:categoryID", categoryController.UpdateCategory) + categories.Delete("/:categoryID", authMiddleware.Authorize(auth.DeleteAll), categoryController.DeleteCategory) + categories.Patch("/:categoryID", authMiddleware.Authorize(auth.WriteAll), categoryController.UpdateCategory) return categories } diff --git a/backend/src/server/routes/club.go b/backend/src/server/routes/club.go index c6b93a929..e44385e9a 100644 --- a/backend/src/server/routes/club.go +++ b/backend/src/server/routes/club.go @@ -1,28 +1,41 @@ package routes import ( - "github.com/GenerateNU/sac/backend/src/auth" + p "github.com/GenerateNU/sac/backend/src/auth" "github.com/GenerateNU/sac/backend/src/controllers" "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" + "gorm.io/gorm" ) -func Club(router fiber.Router, clubService services.ClubServiceInterface, middlewareService middleware.MiddlewareInterface) fiber.Router { +func ClubRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate, authMiddleware *middleware.AuthMiddlewareService) { + clubIDRouter := Club(router, services.NewClubService(db, validate), authMiddleware) + + ClubTag(clubIDRouter, services.NewClubTagService(db, validate), authMiddleware) + ClubFollower(clubIDRouter, services.NewClubFollowerService(db)) + ClubMember(clubIDRouter, services.NewClubMemberService(db, validate), authMiddleware) + ClubContact(clubIDRouter, services.NewClubContactService(db, validate), authMiddleware) + ClubEvent(clubIDRouter, services.NewClubEventService(db)) +} + +func Club(router fiber.Router, clubService services.ClubServiceInterface, authMiddleware *middleware.AuthMiddlewareService) fiber.Router { clubController := controllers.NewClubController(clubService) clubs := router.Group("/clubs") - clubs.Get("/", middlewareService.Authorize(auth.ClubReadAll), clubController.GetAllClubs) - clubs.Post("/", clubController.CreateClub) + clubs.Get("/", clubController.GetClubs) + + // TODO: design issue if we want to allow "anyone" (that an admin allows) to create a club + clubs.Post("/", authMiddleware.Authorize(p.CreateAll), clubController.CreateClub) // api/v1/clubs/:clubID/* clubsID := clubs.Group("/:clubID") - clubsID.Use(middleware.SuperSkipper(middlewareService.UserAuthorizeById)) clubsID.Get("/", clubController.GetClub) - clubsID.Patch("/", middlewareService.Authorize(auth.ClubWrite), clubController.UpdateClub) - clubsID.Delete("/", middlewareService.Authorize(auth.ClubDelete), clubController.DeleteClub) + clubsID.Patch("/", authMiddleware.ClubAuthorizeById, clubController.UpdateClub) + clubsID.Delete("/", authMiddleware.Authorize(p.DeleteAll), clubController.DeleteClub) return clubsID } diff --git a/backend/src/server/routes/club_contact.go b/backend/src/server/routes/club_contact.go index 4d0055c5d..e8d36300b 100644 --- a/backend/src/server/routes/club_contact.go +++ b/backend/src/server/routes/club_contact.go @@ -2,16 +2,17 @@ package routes import ( "github.com/GenerateNU/sac/backend/src/controllers" + "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" "github.com/gofiber/fiber/v2" ) -func ClubContact(clubsIDRouter fiber.Router, clubContactService services.ClubContactServiceInterface) { +func ClubContact(clubsIDRouter fiber.Router, clubContactService services.ClubContactServiceInterface, authMiddleware *middleware.AuthMiddlewareService) { clubContactController := controllers.NewClubContactController(clubContactService) clubContacts := clubsIDRouter.Group("/contacts") // api/v1/clubs/:clubID/contacts/* clubContacts.Get("/", clubContactController.GetClubContacts) - clubContacts.Put("/", clubContactController.PutContact) + clubContacts.Put("/", authMiddleware.ClubAuthorizeById, clubContactController.PutContact) } diff --git a/backend/src/server/routes/club_event.go b/backend/src/server/routes/club_event.go index 601cd133d..7819174f6 100644 --- a/backend/src/server/routes/club_event.go +++ b/backend/src/server/routes/club_event.go @@ -2,12 +2,11 @@ package routes import ( "github.com/GenerateNU/sac/backend/src/controllers" - "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" "github.com/gofiber/fiber/v2" ) -func ClubEvent(clubIDRouter fiber.Router, clubEventService services.ClubEventServiceInterface, middlewareService middleware.MiddlewareInterface) { +func ClubEvent(clubIDRouter fiber.Router, clubEventService services.ClubEventServiceInterface) { clubEventController := controllers.NewClubEventController(clubEventService) // api/v1/clubs/:clubID/events/* diff --git a/backend/src/server/routes/club_member.go b/backend/src/server/routes/club_member.go index 7096c19fa..263519a65 100644 --- a/backend/src/server/routes/club_member.go +++ b/backend/src/server/routes/club_member.go @@ -2,15 +2,16 @@ package routes import ( "github.com/GenerateNU/sac/backend/src/controllers" + "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" "github.com/gofiber/fiber/v2" ) -func ClubMember(clubsIDRouter fiber.Router, clubMemberService services.ClubMemberServiceInterface) { +func ClubMember(clubsIDRouter fiber.Router, clubMemberService services.ClubMemberServiceInterface, authMiddleware *middleware.AuthMiddlewareService) { clubMemberController := controllers.NewClubMemberController(clubMemberService) clubMember := clubsIDRouter.Group("/members") // api/v1/clubs/:clubID/members/* - clubMember.Get("/", clubMemberController.GetClubMembers) + clubMember.Get("/", authMiddleware.ClubAuthorizeById, clubMemberController.GetClubMembers) } diff --git a/backend/src/server/routes/club_tag.go b/backend/src/server/routes/club_tag.go index 5940a5afd..708d0fd73 100644 --- a/backend/src/server/routes/club_tag.go +++ b/backend/src/server/routes/club_tag.go @@ -2,16 +2,17 @@ package routes import ( "github.com/GenerateNU/sac/backend/src/controllers" + "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" "github.com/gofiber/fiber/v2" ) -func ClubTag(router fiber.Router, clubTagService services.ClubTagServiceInterface) { +func ClubTag(router fiber.Router, clubTagService services.ClubTagServiceInterface, authMiddleware *middleware.AuthMiddlewareService) { clubTagController := controllers.NewClubTagController(clubTagService) clubTags := router.Group("/tags") - clubTags.Post("/", clubTagController.CreateClubTags) + clubTags.Post("/", authMiddleware.ClubAuthorizeById, clubTagController.CreateClubTags) clubTags.Get("/", clubTagController.GetClubTags) - clubTags.Delete("/:tagID", clubTagController.DeleteClubTag) + clubTags.Delete("/:tagID", authMiddleware.ClubAuthorizeById, clubTagController.DeleteClubTag) } diff --git a/backend/src/server/routes/event.go b/backend/src/server/routes/event.go index 2cc4ee4cf..ccae17b47 100644 --- a/backend/src/server/routes/event.go +++ b/backend/src/server/routes/event.go @@ -2,11 +2,12 @@ package routes import ( "github.com/GenerateNU/sac/backend/src/controllers" + "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" "github.com/gofiber/fiber/v2" ) -func Event(router fiber.Router, eventService services.EventServiceInterface) { +func Event(router fiber.Router, eventService services.EventServiceInterface, authMiddleware *middleware.AuthMiddlewareService) { eventController := controllers.NewEventController(eventService) // api/v1/events/* diff --git a/backend/src/server/routes/tag.go b/backend/src/server/routes/tag.go index 6bd9bf8b8..6188c7eb6 100644 --- a/backend/src/server/routes/tag.go +++ b/backend/src/server/routes/tag.go @@ -1,18 +1,21 @@ package routes import ( + p "github.com/GenerateNU/sac/backend/src/auth" "github.com/GenerateNU/sac/backend/src/controllers" + "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" "github.com/gofiber/fiber/v2" ) -func Tag(router fiber.Router, tagService services.TagServiceInterface) { +func Tag(router fiber.Router, tagService services.TagServiceInterface, authMiddleware *middleware.AuthMiddlewareService) { tagController := controllers.NewTagController(tagService) tags := router.Group("/tags") tags.Get("/:tagID", tagController.GetTag) - tags.Post("/", tagController.CreateTag) - tags.Patch("/:tagID", tagController.UpdateTag) - tags.Delete("/:tagID", tagController.DeleteTag) + tags.Get("/", tagController.GetTags) + tags.Post("/", authMiddleware.Authorize(p.CreateAll), tagController.CreateTag) + tags.Patch("/:tagID", authMiddleware.Authorize(p.WriteAll), tagController.UpdateTag) + tags.Delete("/:tagID", authMiddleware.Authorize(p.DeleteAll), tagController.DeleteTag) } diff --git a/backend/src/server/routes/user.go b/backend/src/server/routes/user.go index 6a019dfe5..3190be5aa 100644 --- a/backend/src/server/routes/user.go +++ b/backend/src/server/routes/user.go @@ -1,28 +1,38 @@ package routes import ( - "github.com/GenerateNU/sac/backend/src/auth" + p "github.com/GenerateNU/sac/backend/src/auth" "github.com/GenerateNU/sac/backend/src/controllers" "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" + "gorm.io/gorm" ) -func User(router fiber.Router, userService services.UserServiceInterface, middlewareService middleware.MiddlewareInterface) fiber.Router { +func UserRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate, authMiddleware *middleware.AuthMiddlewareService) { + userIDRouter := User(router, services.NewUserService(db, validate), authMiddleware) + + UserTag(userIDRouter, services.NewUserTagService(db, validate)) + UserFollower(userIDRouter, services.NewUserFollowerService(db, validate)) + UserMember(userIDRouter, services.NewUserMemberService(db)) +} + +func User(router fiber.Router, userService services.UserServiceInterface, authMiddleware *middleware.AuthMiddlewareService) fiber.Router { userController := controllers.NewUserController(userService) // api/v1/users/* users := router.Group("/users") users.Post("/", userController.CreateUser) - users.Get("/", middleware.SuperSkipper(middlewareService.Authorize(auth.UserReadAll)), userController.GetUsers) + users.Get("/", authMiddleware.Authorize(p.ReadAll), userController.GetUsers) // api/v1/users/:userID/* usersID := users.Group("/:userID") - usersID.Use(middleware.SuperSkipper(middlewareService.UserAuthorizeById)) + usersID.Use(authMiddleware.UserAuthorizeById) usersID.Get("/", userController.GetUser) usersID.Patch("/", userController.UpdateUser) usersID.Delete("/", userController.DeleteUser) - return users + return router } diff --git a/backend/src/server/routes/user_follower.go b/backend/src/server/routes/user_follower.go index a6f355280..58c37a773 100644 --- a/backend/src/server/routes/user_follower.go +++ b/backend/src/server/routes/user_follower.go @@ -6,10 +6,10 @@ import ( "github.com/gofiber/fiber/v2" ) -func UserFollower(usersIDRouter fiber.Router, userFollowerService services.UserFollowerServiceInterface) { +func UserFollower(router fiber.Router, userFollowerService services.UserFollowerServiceInterface) { userFollowerController := controllers.NewUserFollowerController(userFollowerService) - userFollower := usersIDRouter.Group("/:userID/follower") + userFollower := router.Group("/follower") // api/v1/users/:userID/follower/* userFollower.Post("/:clubID", userFollowerController.CreateFollowing) diff --git a/backend/src/server/routes/user_member.go b/backend/src/server/routes/user_member.go index c52e0519d..e2796de11 100644 --- a/backend/src/server/routes/user_member.go +++ b/backend/src/server/routes/user_member.go @@ -9,7 +9,7 @@ import ( func UserMember(usersRouter fiber.Router, userMembershipService services.UserMemberServiceInterface) { userMemberController := controllers.NewUserMemberController(userMembershipService) - userMember := usersRouter.Group("/:userID/member") + userMember := usersRouter.Group("/member") // api/v1/users/:userID/member/* userMember.Post("/:clubID", userMemberController.CreateMembership) diff --git a/backend/src/server/routes/user_tag.go b/backend/src/server/routes/user_tag.go index 1b777ff47..59f10ca73 100644 --- a/backend/src/server/routes/user_tag.go +++ b/backend/src/server/routes/user_tag.go @@ -9,7 +9,8 @@ import ( func UserTag(router fiber.Router, userTagService services.UserTagServiceInterface) { userTagController := controllers.NewUserTagController(userTagService) - userTags := router.Group("/:userID/tags") + // api/v1/user/:userID/tags/* + userTags := router.Group("/tags") userTags.Post("/", userTagController.CreateUserTags) userTags.Get("/", userTagController.GetUserTags) diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 94d0e412e..b110c2312 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -1,6 +1,8 @@ package server import ( + "fmt" + "github.com/GenerateNU/sac/backend/src/config" "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/server/routes" @@ -10,6 +12,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/requestid" "gorm.io/gorm" @@ -28,38 +31,22 @@ func Init(db *gorm.DB, settings config.Settings) *fiber.App { validate, err := utilities.RegisterCustomValidators() if err != nil { - panic(err) + panic(fmt.Sprintf("Error registering custom validators: %s", err)) } - middlewareService := middleware.NewMiddlewareService(db, validate, settings.Auth) + authMiddleware := middleware.NewAuthAuthMiddlewareService(db, validate, settings.Auth) apiv1 := app.Group("/api/v1") - apiv1.Use(middlewareService.Authenticate) + apiv1.Use(authMiddleware.Authenticate) routes.Utility(app) - - routes.Auth(apiv1, services.NewAuthService(db, validate), settings.Auth) - - userRouter := routes.User(apiv1, services.NewUserService(db, validate), middlewareService) - routes.UserTag(userRouter, services.NewUserTagService(db, validate)) - routes.UserFollower(userRouter, services.NewUserFollowerService(db, validate)) - routes.UserMember(userRouter, services.NewUserMemberService(db)) - + routes.Auth(apiv1, services.NewAuthService(db, validate), settings.Auth, authMiddleware) + routes.UserRoutes(apiv1, db, validate, authMiddleware) routes.Contact(apiv1, services.NewContactService(db, validate)) - - clubsIDRouter := routes.Club(apiv1, services.NewClubService(db, validate), middlewareService) - routes.ClubTag(clubsIDRouter, services.NewClubTagService(db, validate)) - routes.ClubFollower(clubsIDRouter, services.NewClubFollowerService(db)) - routes.ClubMember(clubsIDRouter, services.NewClubMemberService(db, validate)) - routes.ClubContact(clubsIDRouter, services.NewClubContactService(db, validate)) - routes.ClubEvent(clubsIDRouter, services.NewClubEventService(db), middlewareService) - - routes.Tag(apiv1, services.NewTagService(db, validate)) - - categoryRouter := routes.Category(apiv1, services.NewCategoryService(db, validate)) - routes.CategoryTag(categoryRouter, services.NewCategoryTagService(db, validate)) - - routes.Event(apiv1, services.NewEventService(db, validate)) + routes.ClubRoutes(apiv1, db, validate, authMiddleware) + routes.Tag(apiv1, services.NewTagService(db, validate), authMiddleware) + routes.CategoryRoutes(apiv1, db, validate, authMiddleware) + routes.Event(apiv1, services.NewEventService(db, validate), authMiddleware) return app } @@ -78,6 +65,7 @@ func newFiberApp() *fiber.App { app.Use(logger.New(logger.Config{ Format: "[${time}] ${ip}:${port} ${pid} ${locals:requestid} ${status} - ${latency} ${method} ${path}\n", })) + app.Use(limiter.New()) // TODO: currently wrapping the whole app, makes more sense for specific endpoints like update password return app } diff --git a/backend/src/services/tag.go b/backend/src/services/tag.go index d703bb23d..39abbc0e3 100644 --- a/backend/src/services/tag.go +++ b/backend/src/services/tag.go @@ -10,9 +10,10 @@ import ( ) type TagServiceInterface interface { - CreateTag(tagBody models.TagRequestBody) (*models.Tag, *errors.Error) + GetTags(limit string, page string) ([]models.Tag, *errors.Error) + CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag, *errors.Error) GetTag(id string) (*models.Tag, *errors.Error) - UpdateTag(id string, tagBody models.TagRequestBody) (*models.Tag, *errors.Error) + UpdateTag(id string, tagBody models.UpdateTagRequestBody) (*models.Tag, *errors.Error) DeleteTag(id string) *errors.Error } @@ -25,7 +26,7 @@ func NewTagService(db *gorm.DB, validate *validator.Validate) *TagService { return &TagService{DB: db, Validate: validate} } -func (t *TagService) CreateTag(tagBody models.TagRequestBody) (*models.Tag, *errors.Error) { +func (t *TagService) CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag, *errors.Error) { if err := t.Validate.Struct(tagBody); err != nil { return nil, &errors.FailedToValidateTag } @@ -38,6 +39,22 @@ func (t *TagService) CreateTag(tagBody models.TagRequestBody) (*models.Tag, *err return transactions.CreateTag(t.DB, *tag) } +func (t *TagService) GetTags(limit string, page string) ([]models.Tag, *errors.Error) { + limitAsInt, err := utilities.ValidateNonNegative(limit) + if err != nil { + return nil, &errors.FailedToValidateLimit + } + + pageAsInt, err := utilities.ValidateNonNegative(page) + if err != nil { + return nil, &errors.FailedToValidatePage + } + + offset := (*pageAsInt - 1) * *limitAsInt + + return transactions.GetTags(t.DB, *limitAsInt, offset) +} + func (t *TagService) GetTag(tagID string) (*models.Tag, *errors.Error) { tagIDAsUUID, idErr := utilities.ValidateID(tagID) @@ -48,7 +65,7 @@ func (t *TagService) GetTag(tagID string) (*models.Tag, *errors.Error) { return transactions.GetTag(t.DB, *tagIDAsUUID) } -func (t *TagService) UpdateTag(tagID string, tagBody models.TagRequestBody) (*models.Tag, *errors.Error) { +func (t *TagService) UpdateTag(tagID string, tagBody models.UpdateTagRequestBody) (*models.Tag, *errors.Error) { tagIDAsUUID, idErr := utilities.ValidateID(tagID) if idErr != nil { diff --git a/backend/src/transactions/tag.go b/backend/src/transactions/tag.go index dd939e4a2..057b937d4 100644 --- a/backend/src/transactions/tag.go +++ b/backend/src/transactions/tag.go @@ -48,6 +48,16 @@ func GetTag(db *gorm.DB, tagID uuid.UUID) (*models.Tag, *errors.Error) { return &tag, nil } +func GetTags(db *gorm.DB, limit int, offset int) ([]models.Tag, *errors.Error) { + var tags []models.Tag + + if err := db.Limit(limit).Offset(offset).Find(&tags).Error; err != nil { + return nil, &errors.FailedToGetTags + } + + return tags, nil +} + func UpdateTag(db *gorm.DB, id uuid.UUID, tag models.Tag) (*models.Tag, *errors.Error) { if err := db.Model(&models.Tag{}).Where("id = ?", id).Updates(tag).First(&tag, id).Error; err != nil { if stdliberrors.Is(err, gorm.ErrRecordNotFound) { From 4f872c603136a63699bb1cabe43a64b7b457ccaf Mon Sep 17 00:00:00 2001 From: David Oduneye Date: Thu, 22 Feb 2024 10:30:58 -0500 Subject: [PATCH 2/3] complete --- backend/src/controllers/auth.go | 11 ++++++----- backend/src/models/user.go | 6 +++--- backend/src/server/routes/auth.go | 17 ++++++++++++++++- backend/src/server/routes/club.go | 3 +-- backend/src/server/routes/user.go | 3 ++- backend/src/server/server.go | 2 -- backend/src/services/auth.go | 14 +++++++------- config/local.yml | 2 +- 8 files changed, 36 insertions(+), 22 deletions(-) diff --git a/backend/src/controllers/auth.go b/backend/src/controllers/auth.go index 549085556..f3bc3db8d 100644 --- a/backend/src/controllers/auth.go +++ b/backend/src/controllers/auth.go @@ -45,7 +45,7 @@ func (a *AuthController) Me(c *fiber.Ctx) error { return err.FiberError(c) } - return c.JSON(user) + return c.Status(fiber.StatusOK).JSON(user) } // Login godoc @@ -76,7 +76,7 @@ func (a *AuthController) Login(c *fiber.Ctx) error { accessToken, refreshToken, err := auth.CreateTokenPair(user.ID.String(), string(user.Role), a.AuthSettings) if err != nil { - return err.FiberError(c) + return errors.Unauthorized.FiberError(c) } // Set the tokens in the response @@ -106,17 +106,17 @@ func (a *AuthController) Refresh(c *fiber.Ctx) error { // Extract id from refresh token claims, err := auth.ExtractRefreshClaims(refreshTokenValue, a.AuthSettings.RefreshKey) if err != nil { - return err.FiberError(c) + return errors.Unauthorized.FiberError(c) } role, err := a.authService.GetRole(claims.Issuer) if err != nil { - return err.FiberError(c) + return errors.Unauthorized.FiberError(c) } accessToken, err := auth.RefreshAccessToken(refreshTokenValue, string(*role), a.AuthSettings.RefreshKey, a.AuthSettings.AccessTokenExpiry, a.AuthSettings.AccessKey) if err != nil { - return err.FiberError(c) + return errors.Unauthorized.FiberError(c) } // Set the access token in the response (e.g., in a cookie or JSON response) @@ -165,6 +165,7 @@ func (a *AuthController) Logout(c *fiber.Ctx) error { // @Failure 401 {object} errors.Error // @Failure 404 {object} errors.Error // @Failure 500 {object} errors.Error +// @Failure 429 {object} // @Router /auth/update-password/:userID [post] func (a *AuthController) UpdatePassword(c *fiber.Ctx) error { var userBody models.UpdatePasswordRequestBody diff --git a/backend/src/models/user.go b/backend/src/models/user.go index 308580e10..bf6b76597 100644 --- a/backend/src/models/user.go +++ b/backend/src/models/user.go @@ -80,12 +80,12 @@ type UpdateUserRequestBody struct { type LoginUserResponseBody struct { Email string `json:"email" validate:"required,email"` - Password string `json:"password" validate:"min=8,max=255"` + Password string `json:"password" validate:"min=8,max=255,password"` } type UpdatePasswordRequestBody struct { - OldPassword string `json:"old_password" validate:"required,password"` - NewPassword string `json:"new_password" validate:"required,password"` + OldPassword string `json:"old_password" validate:"required,password,min=8,max=255"` + NewPassword string `json:"new_password" validate:"required,password,nefield=OldPassword,min=8,max=255"` } type CreateUserTagsBody struct { diff --git a/backend/src/server/routes/auth.go b/backend/src/server/routes/auth.go index 6d017c1dc..f13af2ee6 100644 --- a/backend/src/server/routes/auth.go +++ b/backend/src/server/routes/auth.go @@ -1,11 +1,15 @@ package routes import ( + "fmt" + "time" + "github.com/GenerateNU/sac/backend/src/config" "github.com/GenerateNU/sac/backend/src/controllers" "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/limiter" ) func Auth(router fiber.Router, authService services.AuthServiceInterface, settings config.AuthSettings, authMiddleware *middleware.AuthMiddlewareService) { @@ -18,6 +22,17 @@ func Auth(router fiber.Router, authService services.AuthServiceInterface, settin auth.Get("/logout", authController.Logout) auth.Get("/refresh", authController.Refresh) auth.Get("/me", authController.Me) - auth.Post("/update-password/:userID", authMiddleware.UserAuthorizeById, authController.UpdatePassword) + auth.Post("/update-password/:userID", limiter.New(limiter.Config{ + Max: 2, + Expiration: 1 * time.Minute, + KeyGenerator: func(c *fiber.Ctx) string { + return fmt.Sprintf("%s-%s", c.IP(), c.Params("userId")) + }, + LimitReached: func(c *fiber.Ctx) error { + return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ + "message": "Too many requests", + }) + }, + }), authMiddleware.UserAuthorizeById, authController.UpdatePassword) // auth.Post("/reset-password/:userID", middleware.Skip(authMiddleware.UserAuthorizeById), authController.ResetPassword) } diff --git a/backend/src/server/routes/club.go b/backend/src/server/routes/club.go index e44385e9a..bc25ff21a 100644 --- a/backend/src/server/routes/club.go +++ b/backend/src/server/routes/club.go @@ -23,11 +23,10 @@ func ClubRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate, func Club(router fiber.Router, clubService services.ClubServiceInterface, authMiddleware *middleware.AuthMiddlewareService) fiber.Router { clubController := controllers.NewClubController(clubService) + // api/v1/clubs/* clubs := router.Group("/clubs") clubs.Get("/", clubController.GetClubs) - - // TODO: design issue if we want to allow "anyone" (that an admin allows) to create a club clubs.Post("/", authMiddleware.Authorize(p.CreateAll), clubController.CreateClub) // api/v1/clubs/:clubID/* diff --git a/backend/src/server/routes/user.go b/backend/src/server/routes/user.go index 3190be5aa..68c4ae98b 100644 --- a/backend/src/server/routes/user.go +++ b/backend/src/server/routes/user.go @@ -23,13 +23,14 @@ func User(router fiber.Router, userService services.UserServiceInterface, authMi // api/v1/users/* users := router.Group("/users") + users.Post("/", userController.CreateUser) users.Get("/", authMiddleware.Authorize(p.ReadAll), userController.GetUsers) // api/v1/users/:userID/* usersID := users.Group("/:userID") - usersID.Use(authMiddleware.UserAuthorizeById) + usersID.Use(authMiddleware.UserAuthorizeById) usersID.Get("/", userController.GetUser) usersID.Patch("/", userController.UpdateUser) usersID.Delete("/", userController.DeleteUser) diff --git a/backend/src/server/server.go b/backend/src/server/server.go index b110c2312..68314e1c2 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -12,7 +12,6 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/requestid" "gorm.io/gorm" @@ -65,7 +64,6 @@ func newFiberApp() *fiber.App { app.Use(logger.New(logger.Config{ Format: "[${time}] ${ip}:${port} ${pid} ${locals:requestid} ${status} - ${latency} ${method} ${path}\n", })) - app.Use(limiter.New()) // TODO: currently wrapping the whole app, makes more sense for specific endpoints like update password return app } diff --git a/backend/src/services/auth.go b/backend/src/services/auth.go index 6ced8fbf9..176312c07 100644 --- a/backend/src/services/auth.go +++ b/backend/src/services/auth.go @@ -14,7 +14,7 @@ type AuthServiceInterface interface { GetRole(id string) (*models.UserRole, *errors.Error) Me(id string) (*models.User, *errors.Error) Login(userBody models.LoginUserResponseBody) (*models.User, *errors.Error) - UpdatePassword(id string, userBody models.UpdatePasswordRequestBody) *errors.Error + UpdatePassword(id string, passwordBody models.UpdatePasswordRequestBody) *errors.Error } type AuthService struct { @@ -77,28 +77,28 @@ func (a *AuthService) GetRole(id string) (*models.UserRole, *errors.Error) { return &role, nil } -func (a *AuthService) UpdatePassword(id string, userBody models.UpdatePasswordRequestBody) *errors.Error { +func (a *AuthService) UpdatePassword(id string, passwordBody models.UpdatePasswordRequestBody) *errors.Error { idAsUint, idErr := utilities.ValidateID(id) if idErr != nil { return idErr } // TODO: Validate password - // if err := a.Validate.Struct(userBody); err != nil { - // return &errors.FailedToValidateUser - // } + if err := a.Validate.Struct(passwordBody); err != nil { + return &errors.FailedToValidateUser + } passwordHash, err := transactions.GetUserPasswordHash(a.DB, *idAsUint) if err != nil { return err } - correct, passwordErr := auth.ComparePasswordAndHash(userBody.OldPassword, passwordHash) + correct, passwordErr := auth.ComparePasswordAndHash(passwordBody.OldPassword, passwordHash) if passwordErr != nil || !correct { return &errors.FailedToValidateUser } - hash, hashErr := auth.ComputePasswordHash(userBody.NewPassword) + hash, hashErr := auth.ComputePasswordHash(passwordBody.NewPassword) if hashErr != nil { return &errors.FailedToValidateUser } diff --git a/config/local.yml b/config/local.yml index 2ad38c2dd..1acbc27ef 100644 --- a/config/local.yml +++ b/config/local.yml @@ -10,7 +10,7 @@ database: databasename: sac requiressl: false superuser: - password: password + password: Password#!1 auth: accesskey: g(r|##*?>\Qp}h37e+,T2 accesstokenexpiry: 60 # in minutes From 15a55cb08adf14d9b01593cf6296812d37199161 Mon Sep 17 00:00:00 2001 From: David Oduneye Date: Thu, 22 Feb 2024 13:20:20 -0500 Subject: [PATCH 3/3] complete --- backend/src/server/routes/club.go | 2 +- backend/src/server/routes/club_follower.go | 5 +++-- backend/src/server/routes/club_tag.go | 2 +- backend/src/server/routes/contact.go | 5 +++-- backend/src/server/routes/event.go | 12 +++++----- backend/src/server/routes/tag.go | 2 +- backend/src/server/routes/user.go | 12 +++++----- backend/src/server/server.go | 2 +- backend/src/services/user_member.go | 1 - backend/src/utilities/response.go | 26 +++++++++++++++++++++- 10 files changed, 47 insertions(+), 22 deletions(-) diff --git a/backend/src/server/routes/club.go b/backend/src/server/routes/club.go index bc25ff21a..68303c378 100644 --- a/backend/src/server/routes/club.go +++ b/backend/src/server/routes/club.go @@ -14,7 +14,7 @@ func ClubRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate, clubIDRouter := Club(router, services.NewClubService(db, validate), authMiddleware) ClubTag(clubIDRouter, services.NewClubTagService(db, validate), authMiddleware) - ClubFollower(clubIDRouter, services.NewClubFollowerService(db)) + ClubFollower(clubIDRouter, services.NewClubFollowerService(db), authMiddleware) ClubMember(clubIDRouter, services.NewClubMemberService(db, validate), authMiddleware) ClubContact(clubIDRouter, services.NewClubContactService(db, validate), authMiddleware) ClubEvent(clubIDRouter, services.NewClubEventService(db)) diff --git a/backend/src/server/routes/club_follower.go b/backend/src/server/routes/club_follower.go index 4050538e2..8a1713451 100644 --- a/backend/src/server/routes/club_follower.go +++ b/backend/src/server/routes/club_follower.go @@ -2,15 +2,16 @@ package routes import ( "github.com/GenerateNU/sac/backend/src/controllers" + "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" "github.com/gofiber/fiber/v2" ) -func ClubFollower(clubsIDRouter fiber.Router, clubFollowerService services.ClubFollowerServiceInterface) { +func ClubFollower(clubsIDRouter fiber.Router, clubFollowerService services.ClubFollowerServiceInterface, authMiddleware *middleware.AuthMiddlewareService) { clubFollowerController := controllers.NewClubFollowerController(clubFollowerService) clubFollower := clubsIDRouter.Group("/followers") // api/clubs/:clubID/followers/* - clubFollower.Get("/", clubFollowerController.GetClubFollowers) + clubFollower.Get("/", authMiddleware.ClubAuthorizeById, clubFollowerController.GetClubFollowers) } diff --git a/backend/src/server/routes/club_tag.go b/backend/src/server/routes/club_tag.go index 708d0fd73..75aee7b53 100644 --- a/backend/src/server/routes/club_tag.go +++ b/backend/src/server/routes/club_tag.go @@ -12,7 +12,7 @@ func ClubTag(router fiber.Router, clubTagService services.ClubTagServiceInterfac clubTags := router.Group("/tags") - clubTags.Post("/", authMiddleware.ClubAuthorizeById, clubTagController.CreateClubTags) clubTags.Get("/", clubTagController.GetClubTags) + clubTags.Post("/", authMiddleware.ClubAuthorizeById, clubTagController.CreateClubTags) clubTags.Delete("/:tagID", authMiddleware.ClubAuthorizeById, clubTagController.DeleteClubTag) } diff --git a/backend/src/server/routes/contact.go b/backend/src/server/routes/contact.go index a5c7beddb..56e4969de 100644 --- a/backend/src/server/routes/contact.go +++ b/backend/src/server/routes/contact.go @@ -2,11 +2,12 @@ package routes import ( "github.com/GenerateNU/sac/backend/src/controllers" + "github.com/GenerateNU/sac/backend/src/middleware" "github.com/GenerateNU/sac/backend/src/services" "github.com/gofiber/fiber/v2" ) -func Contact(router fiber.Router, contactService services.ContactServiceInterface) { +func Contact(router fiber.Router, contactService services.ContactServiceInterface, authMiddleware *middleware.AuthMiddlewareService) { contactController := controllers.NewContactController(contactService) // api/v1/contacts/* @@ -14,5 +15,5 @@ func Contact(router fiber.Router, contactService services.ContactServiceInterfac contacts.Get("/", contactController.GetContacts) contacts.Get("/:contactID", contactController.GetContact) - contacts.Delete("/:contactID", contactController.DeleteContact) + contacts.Delete("/:contactID", authMiddleware.UserAuthorizeById, contactController.DeleteContact) } diff --git a/backend/src/server/routes/event.go b/backend/src/server/routes/event.go index ccae17b47..f71b32d53 100644 --- a/backend/src/server/routes/event.go +++ b/backend/src/server/routes/event.go @@ -14,16 +14,16 @@ func Event(router fiber.Router, eventService services.EventServiceInterface, aut events := router.Group("/events") events.Get("/", eventController.GetAllEvents) - events.Post("/", eventController.CreateEvent) + events.Post("/", authMiddleware.ClubAuthorizeById, eventController.CreateEvent) // api/v1/events/:eventID/* eventID := events.Group("/:eventID") eventID.Get("/", eventController.GetEvent) eventID.Get("/series", eventController.GetSeriesByEventID) - events.Patch("/", eventController.UpdateEvent) - eventID.Delete("/", eventController.DeleteEvent) - eventID.Delete("/series", eventController.DeleteSeriesByEventID) + events.Patch("/", authMiddleware.ClubAuthorizeById, eventController.UpdateEvent) + eventID.Delete("/", authMiddleware.ClubAuthorizeById, eventController.DeleteEvent) + eventID.Delete("/series", authMiddleware.ClubAuthorizeById, eventController.DeleteSeriesByEventID) // api/v1/events/:eventID/series/* series := router.Group("/series") @@ -32,6 +32,6 @@ func Event(router fiber.Router, eventService services.EventServiceInterface, aut seriesID := series.Group("/:seriesID") seriesID.Get("/", eventController.GetSeriesByID) - seriesID.Patch("/", eventController.UpdateSeriesByID) - seriesID.Delete("/", eventController.DeleteSeriesByID) + seriesID.Patch("/", authMiddleware.ClubAuthorizeById, eventController.UpdateSeriesByID) + seriesID.Delete("/", authMiddleware.ClubAuthorizeById, eventController.DeleteSeriesByID) } diff --git a/backend/src/server/routes/tag.go b/backend/src/server/routes/tag.go index 6188c7eb6..47d15cfb8 100644 --- a/backend/src/server/routes/tag.go +++ b/backend/src/server/routes/tag.go @@ -13,8 +13,8 @@ func Tag(router fiber.Router, tagService services.TagServiceInterface, authMiddl tags := router.Group("/tags") - tags.Get("/:tagID", tagController.GetTag) tags.Get("/", tagController.GetTags) + tags.Get("/:tagID", tagController.GetTag) tags.Post("/", authMiddleware.Authorize(p.CreateAll), tagController.CreateTag) tags.Patch("/:tagID", authMiddleware.Authorize(p.WriteAll), tagController.UpdateTag) tags.Delete("/:tagID", authMiddleware.Authorize(p.DeleteAll), tagController.DeleteTag) diff --git a/backend/src/server/routes/user.go b/backend/src/server/routes/user.go index 68c4ae98b..1cea2b693 100644 --- a/backend/src/server/routes/user.go +++ b/backend/src/server/routes/user.go @@ -11,11 +11,11 @@ import ( ) func UserRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate, authMiddleware *middleware.AuthMiddlewareService) { - userIDRouter := User(router, services.NewUserService(db, validate), authMiddleware) + usersRouter := User(router, services.NewUserService(db, validate), authMiddleware) - UserTag(userIDRouter, services.NewUserTagService(db, validate)) - UserFollower(userIDRouter, services.NewUserFollowerService(db, validate)) - UserMember(userIDRouter, services.NewUserMemberService(db)) + UserTag(usersRouter, services.NewUserTagService(db, validate)) + UserFollower(usersRouter, services.NewUserFollowerService(db, validate)) + UserMember(usersRouter, services.NewUserMemberService(db)) } func User(router fiber.Router, userService services.UserServiceInterface, authMiddleware *middleware.AuthMiddlewareService) fiber.Router { @@ -29,11 +29,11 @@ func User(router fiber.Router, userService services.UserServiceInterface, authMi // api/v1/users/:userID/* usersID := users.Group("/:userID") - usersID.Use(authMiddleware.UserAuthorizeById) + usersID.Get("/", userController.GetUser) usersID.Patch("/", userController.UpdateUser) usersID.Delete("/", userController.DeleteUser) - return router + return usersID } diff --git a/backend/src/server/server.go b/backend/src/server/server.go index 68314e1c2..2321106ab 100644 --- a/backend/src/server/server.go +++ b/backend/src/server/server.go @@ -41,7 +41,7 @@ func Init(db *gorm.DB, settings config.Settings) *fiber.App { routes.Utility(app) routes.Auth(apiv1, services.NewAuthService(db, validate), settings.Auth, authMiddleware) routes.UserRoutes(apiv1, db, validate, authMiddleware) - routes.Contact(apiv1, services.NewContactService(db, validate)) + routes.Contact(apiv1, services.NewContactService(db, validate), authMiddleware) routes.ClubRoutes(apiv1, db, validate, authMiddleware) routes.Tag(apiv1, services.NewTagService(db, validate), authMiddleware) routes.CategoryRoutes(apiv1, db, validate, authMiddleware) diff --git a/backend/src/services/user_member.go b/backend/src/services/user_member.go index 18d9c9a3f..87a828f92 100644 --- a/backend/src/services/user_member.go +++ b/backend/src/services/user_member.go @@ -27,7 +27,6 @@ func (u *UserMemberService) CreateMembership(userID string, clubID string) *erro if err != nil { return err } - clubIdAsUUID, err := utilities.ValidateID(clubID) if err != nil { return err diff --git a/backend/src/utilities/response.go b/backend/src/utilities/response.go index f3b95efda..32058c2fe 100644 --- a/backend/src/utilities/response.go +++ b/backend/src/utilities/response.go @@ -1,6 +1,10 @@ package utilities -import "github.com/gofiber/fiber/v2" +import ( + "fmt" + + "github.com/gofiber/fiber/v2" +) // For swagger docs: type SuccessResponse struct { @@ -10,3 +14,23 @@ type SuccessResponse struct { func FiberMessage(c *fiber.Ctx, statusCode int, response string) error { return c.Status(statusCode).JSON(fiber.Map{"message": response}) } + +func FiberSuccess(c *fiber.Ctx, response string) error { + return FiberMessage(c, fiber.StatusOK, response) +} + +func FiberError(c *fiber.Ctx, statusCode int, response string) error { + return FiberMessage(c, statusCode, response) +} + +func Test(input string) fiber.Handler { + return func(c *fiber.Ctx) error { + fmt.Printf("Test: %s\n", input) + fmt.Printf("Method: %s\n", c.Method()) + fmt.Printf("Path: %s\n", c.Path()) + fmt.Printf("Route Name: %s\n", c.Route().Name) + fmt.Printf("Route Path: %s\n", c.Route().Path) + fmt.Printf("Route Method: %s\n", c.Route().Method) + return c.Next() + } +}