From f65439fb504be0bda1b12d2cb3cbae2dd53b3557 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Sat, 19 Jul 2025 22:57:44 +0300 Subject: [PATCH] feat: fully implement profile controller; feat: implement file upload handling in controller with size and type validation; feat: add custom validation rules for bio and color hex fields; refactor: enhance request handling with dedicated client info extraction; chore: update profile DTOs with validation tags; docs: profile controller swagger --- backend/docs/docs.go | 256 +++++++++++++++++++++ backend/docs/swagger.json | 256 +++++++++++++++++++++ backend/docs/swagger.yaml | 158 +++++++++++++ backend/internal/controllers/controller.go | 54 ++++- backend/internal/controllers/profile.go | 232 +++++++++++++++++++ backend/internal/controllers/setup.go | 3 +- backend/internal/dto/profile.go | 8 +- backend/internal/errors/general.go | 1 + backend/internal/services/profile.go | 4 +- backend/internal/services/setup.go | 1 + backend/internal/validation/custom.go | 14 ++ 11 files changed, 975 insertions(+), 12 deletions(-) create mode 100644 backend/internal/controllers/profile.go diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 5fc8e96..a5b6ba3 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -269,6 +269,210 @@ const docTemplate = `{ } } }, + "/profile": { + "get": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Get your profile", + "responses": { + "200": { + "description": " ", + "schema": { + "$ref": "#/definitions/dto.ProfileDto" + } + } + } + }, + "patch": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Update your profile", + "parameters": [ + { + "description": " ", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ProfileDto" + } + } + ], + "responses": { + "200": { + "description": " ", + "schema": { + "type": "boolean" + } + } + } + } + }, + "/profile/avatar": { + "post": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "Profile" + ], + "summary": "Upload an avatar", + "parameters": [ + { + "type": "file", + "description": "Avatar image file", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "Uploaded image url", + "schema": { + "type": "string" + } + } + } + } + }, + "/profile/settings": { + "get": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Get your profile settings", + "responses": { + "200": { + "description": " ", + "schema": { + "$ref": "#/definitions/dto.ProfileSettingsDto" + } + } + } + }, + "patch": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Update your profile's settings", + "parameters": [ + { + "description": " ", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ProfileSettingsDto" + } + } + ], + "responses": { + "200": { + "description": " ", + "schema": { + "type": "boolean" + } + } + } + } + }, + "/profile/{username}": { + "get": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Get profile by username", + "parameters": [ + { + "type": "string", + "description": " ", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": " ", + "schema": { + "$ref": "#/definitions/dto.ProfileDto" + } + }, + "403": { + "description": "Restricted profile" + }, + "404": { + "description": "Profile not found" + } + } + } + }, "/service/health": { "get": { "description": "Used internally for checking service health", @@ -294,6 +498,58 @@ const docTemplate = `{ } }, "definitions": { + "dto.ProfileDto": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "avatar_url": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "birthday": { + "type": "integer" + }, + "color": { + "type": "string" + }, + "color_grad": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.ProfileSettingsDto": { + "type": "object", + "properties": { + "captcha": { + "type": "boolean" + }, + "followers_only_interaction": { + "type": "boolean" + }, + "hide_birthday": { + "type": "boolean" + }, + "hide_dates": { + "type": "boolean" + }, + "hide_for_unauthenticated": { + "type": "boolean" + }, + "hide_fulfilled": { + "type": "boolean" + }, + "hide_profile_details": { + "type": "boolean" + } + } + }, "models.ChangePasswordRequest": { "type": "object", "required": [ diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 484d017..be0e835 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -265,6 +265,210 @@ } } }, + "/profile": { + "get": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Get your profile", + "responses": { + "200": { + "description": " ", + "schema": { + "$ref": "#/definitions/dto.ProfileDto" + } + } + } + }, + "patch": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Update your profile", + "parameters": [ + { + "description": " ", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ProfileDto" + } + } + ], + "responses": { + "200": { + "description": " ", + "schema": { + "type": "boolean" + } + } + } + } + }, + "/profile/avatar": { + "post": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "Profile" + ], + "summary": "Upload an avatar", + "parameters": [ + { + "type": "file", + "description": "Avatar image file", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "Uploaded image url", + "schema": { + "type": "string" + } + } + } + } + }, + "/profile/settings": { + "get": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Get your profile settings", + "responses": { + "200": { + "description": " ", + "schema": { + "$ref": "#/definitions/dto.ProfileSettingsDto" + } + } + } + }, + "patch": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Update your profile's settings", + "parameters": [ + { + "description": " ", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ProfileSettingsDto" + } + } + ], + "responses": { + "200": { + "description": " ", + "schema": { + "type": "boolean" + } + } + } + } + }, + "/profile/{username}": { + "get": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Get profile by username", + "parameters": [ + { + "type": "string", + "description": " ", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": " ", + "schema": { + "$ref": "#/definitions/dto.ProfileDto" + } + }, + "403": { + "description": "Restricted profile" + }, + "404": { + "description": "Profile not found" + } + } + } + }, "/service/health": { "get": { "description": "Used internally for checking service health", @@ -290,6 +494,58 @@ } }, "definitions": { + "dto.ProfileDto": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "avatar_url": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "birthday": { + "type": "integer" + }, + "color": { + "type": "string" + }, + "color_grad": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.ProfileSettingsDto": { + "type": "object", + "properties": { + "captcha": { + "type": "boolean" + }, + "followers_only_interaction": { + "type": "boolean" + }, + "hide_birthday": { + "type": "boolean" + }, + "hide_dates": { + "type": "boolean" + }, + "hide_for_unauthenticated": { + "type": "boolean" + }, + "hide_fulfilled": { + "type": "boolean" + }, + "hide_profile_details": { + "type": "boolean" + } + } + }, "models.ChangePasswordRequest": { "type": "object", "required": [ diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 83f1608..8fc360a 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,5 +1,39 @@ basePath: /api/ definitions: + dto.ProfileDto: + properties: + avatar_url: + type: string + bio: + type: string + birthday: + type: integer + color: + type: string + color_grad: + type: string + name: + type: string + required: + - name + type: object + dto.ProfileSettingsDto: + properties: + captcha: + type: boolean + followers_only_interaction: + type: boolean + hide_birthday: + type: boolean + hide_dates: + type: boolean + hide_for_unauthenticated: + type: boolean + hide_fulfilled: + type: boolean + hide_profile_details: + type: boolean + type: object models.ChangePasswordRequest: properties: old_password: @@ -285,6 +319,130 @@ paths: summary: Confirm with code, finish creating the account tags: - Auth + /profile: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: ' ' + schema: + $ref: '#/definitions/dto.ProfileDto' + security: + - JWT: [] + summary: Get your profile + tags: + - Profile + patch: + consumes: + - application/json + parameters: + - description: ' ' + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ProfileDto' + produces: + - application/json + responses: + "200": + description: ' ' + schema: + type: boolean + security: + - JWT: [] + summary: Update your profile + tags: + - Profile + /profile/{username}: + get: + consumes: + - application/json + parameters: + - description: ' ' + in: path + name: username + required: true + type: string + produces: + - application/json + responses: + "200": + description: ' ' + schema: + $ref: '#/definitions/dto.ProfileDto' + "403": + description: Restricted profile + "404": + description: Profile not found + security: + - JWT: [] + summary: Get profile by username + tags: + - Profile + /profile/avatar: + post: + consumes: + - multipart/form-data + parameters: + - description: Avatar image file + in: formData + name: file + required: true + type: file + produces: + - text/plain + responses: + "200": + description: Uploaded image url + schema: + type: string + security: + - JWT: [] + summary: Upload an avatar + tags: + - Profile + /profile/settings: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: ' ' + schema: + $ref: '#/definitions/dto.ProfileSettingsDto' + security: + - JWT: [] + summary: Get your profile settings + tags: + - Profile + patch: + consumes: + - application/json + parameters: + - description: ' ' + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ProfileSettingsDto' + produces: + - application/json + responses: + "200": + description: ' ' + schema: + type: boolean + security: + - JWT: [] + summary: Update your profile's settings + tags: + - Profile /service/health: get: consumes: diff --git a/backend/internal/controllers/controller.go b/backend/internal/controllers/controller.go index e9d3a85..49292c9 100644 --- a/backend/internal/controllers/controller.go +++ b/backend/internal/controllers/controller.go @@ -22,10 +22,14 @@ import ( "easywish/internal/services" "easywish/internal/utils/enums" "easywish/internal/validation" + "fmt" "net/http" + "os" + "path/filepath" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" + "github.com/google/uuid" "go.uber.org/zap" ) @@ -75,12 +79,45 @@ func (ctrl *controllerImpl) Setup(group *gin.RouterGroup, log *zap.Logger, auth } }), method.Function)..., - )} + ) + } +} + +func GetFile(c *gin.Context, name string, maxSize int64, allowedTypes map[string]bool) (*string, error) { + file, err := c.FormFile(name); if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("File '%s': not provided", name)}) + return nil, err + } + + if file.Size > int64(maxSize) { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("File '%s': file too large", name)}) + return nil, fmt.Errorf("File too large") + } + + fileType := file.Header.Get("Content-Type") + if len(allowedTypes) > 0 && !allowedTypes[fileType] { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("File '%s': forbidden file type: %s", name, fileType)}) + return nil, fmt.Errorf("Wrong file type") + } + + folderPath := "/tmp/uploads" + if _, err := os.Stat(folderPath); os.IsNotExist(err) { + os.MkdirAll(folderPath, 0700) + } + + filePath := fmt.Sprintf("%s/%s-%s", folderPath, uuid.New().String(), filepath.Base(file.Filename)) + if err := c.SaveUploadedFile(file, filePath); err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Error saving file"}) + return nil, err + } + + return &filePath, nil } func GetRequest[ModelT any](c *gin.Context) (*dto.Request[ModelT], error) { var body ModelT + if err := c.ShouldBindJSON(&body); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return nil, err @@ -97,6 +134,16 @@ func GetRequest[ModelT any](c *gin.Context) (*dto.Request[ModelT], error) { return nil, err } + cinfo := GetClientInfo(c) + + return &dto.Request[ModelT]{ + Body: body, + User: cinfo, + }, nil +} + +func GetClientInfo(c * gin.Context) (dto.ClientInfo) { + cinfoFromCtx, ok := c.Get("client_info"); if !ok { c.AbortWithStatusJSON( http.StatusInternalServerError, @@ -105,8 +152,5 @@ func GetRequest[ModelT any](c *gin.Context) (*dto.Request[ModelT], error) { } cinfo := cinfoFromCtx.(dto.ClientInfo) - return &dto.Request[ModelT]{ - Body: body, - User: cinfo, - }, nil + return cinfo } diff --git a/backend/internal/controllers/profile.go b/backend/internal/controllers/profile.go new file mode 100644 index 0000000..c87a159 --- /dev/null +++ b/backend/internal/controllers/profile.go @@ -0,0 +1,232 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of Easywish +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package controllers + +import ( + "easywish/internal/dto" + errs "easywish/internal/errors" + "easywish/internal/services" + "easywish/internal/utils/enums" + "errors" + "net/http" + "os" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type ProfileController struct { + log *zap.Logger + ps services.ProfileService +} + +func NewProfileController(_log *zap.Logger, _ps services.ProfileService) Controller { + + ctrl := ProfileController{log: _log, ps: _ps} + + return &controllerImpl{ + Path: "/profile", + Middleware: []gin.HandlerFunc{}, + Methods: []ControllerMethod{ + { + HttpMethod: GET, + Path: "", + Authorization: enums.UserRole, + Middleware: []gin.HandlerFunc{}, + Function: ctrl.getMyProfile, + }, + { + HttpMethod: GET, + Path: "/:username", + Authorization: enums.GuestRole, + Middleware: []gin.HandlerFunc{}, + Function: ctrl.getProfileByUsername, + }, + { + HttpMethod: GET, + Path: "/settings", + Authorization: enums.UserRole, + Middleware: []gin.HandlerFunc{}, + Function: ctrl.getProfileSettings, + }, + { + HttpMethod: PATCH, + Path: "", + Authorization: enums.UserRole, + Middleware: []gin.HandlerFunc{}, + Function: ctrl.updateProfile, + }, + { + HttpMethod: PATCH, + Path: "/settings", + Authorization: enums.UserRole, + Middleware: []gin.HandlerFunc{}, + Function: ctrl.updateProfileSettings, + }, + { + HttpMethod: POST, + Path: "/avatar", + Authorization: enums.UserRole, + Middleware: []gin.HandlerFunc{}, + Function: ctrl.uploadAvatar, + }, + }, + } +} + +// @Summary Get your profile +// @Tags Profile +// @Accept json +// @Produce json +// @Security JWT +// @Success 200 {object} dto.ProfileDto " " +// @Router /profile [get] +func (ctrl *ProfileController) getMyProfile(c *gin.Context) { + cinfo := GetClientInfo(c) + + response, err := ctrl.ps.GetMyProfile(cinfo); if err != nil { + c.Status(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, response) +} + +// @Summary Get profile by username +// @Tags Profile +// @Accept json +// @Produce json +// @Security JWT +// @Param username path string true " " +// @Success 200 {object} dto.ProfileDto " " +// @Failure 404 "Profile not found" +// @Failure 403 "Restricted profile" +// @Router /profile/{username} [get] +func (ctrl *ProfileController) getProfileByUsername(c *gin.Context) { + cinfo := GetClientInfo(c) + + username := c.Param("username"); if username == "" { + c.Status(http.StatusBadRequest) + return + } + + response, err := ctrl.ps.GetProfileByUsername(cinfo, username); if err != nil { + if errors.Is(err, errs.ErrNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "Profile not found"}) + } else if errors.Is(err, errs.ErrForbidden) { + c.JSON(http.StatusForbidden, gin.H{"error": "Access restricted by profile's privacy settings"}) + } else { + c.Status(http.StatusInternalServerError) + } + return + } + + c.JSON(http.StatusOK, response) + + print(cinfo.Username) + panic("Not implemented") +} + +// @Summary Get your profile settings +// @Tags Profile +// @Accept json +// @Produce json +// @Security JWT +// @Success 200 {object} dto.ProfileSettingsDto " " +// @Router /profile/settings [get] +func (ctrl *ProfileController) getProfileSettings(c *gin.Context) { + cinfo := GetClientInfo(c) + + response, err := ctrl.ps.GetProfileSettings(cinfo); if err != nil { + c.Status(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, response) +} + +// @Summary Update your profile +// @Tags Profile +// @Accept json +// @Produce json +// @Security JWT +// @Param request body dto.ProfileDto true " " +// @Success 200 {object} bool " " +// @Router /profile [patch] +func (ctrl *ProfileController) updateProfile(c *gin.Context) { + request, err := GetRequest[dto.ProfileDto](c); if err != nil { + return + } + + response, err := ctrl.ps.UpdateProfile(request.User, request.Body); if err != nil || !response { + c.Status(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, response) +} + +// @Summary Update your profile's settings +// @Tags Profile +// @Accept json +// @Produce json +// @Security JWT +// @Param request body dto.ProfileSettingsDto true " " +// @Success 200 {object} bool " " +// @Router /profile/settings [patch] +func (ctrl *ProfileController) updateProfileSettings(c *gin.Context) { + request, err := GetRequest[dto.ProfileSettingsDto](c); if err != nil { + return + } + + response, err := ctrl.ps.UpdateProfileSettings(request.User, request.Body); if err != nil || !response { + c.Status(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, response) +} + +// @Summary Upload an avatar +// @Tags Profile +// @Accept mpfd +// @Produce plain +// @Security JWT +// @Param file formData file true "Avatar image file" +// @Success 200 {object} string "Uploaded image url" +// @Router /profile/avatar [post] +func (ctrl *ProfileController) uploadAvatar(c *gin.Context) { + cinfo := GetClientInfo(c) + + allowedTypes := map[string]bool{ + "image/jpeg": true, + "image/png": true, + "image/webp": true, + } + fileName, err := GetFile(c, "file", 8*1024*1024, allowedTypes); if err != nil { + return + } + defer os.Remove(*fileName) + + link, err := ctrl.ps.UploadAvatar(cinfo, *fileName); if err != nil { + c.Status(http.StatusInternalServerError) + return + } + + c.String(http.StatusOK, link) +} diff --git a/backend/internal/controllers/setup.go b/backend/internal/controllers/setup.go index 9816a9f..2d3e28c 100644 --- a/backend/internal/controllers/setup.go +++ b/backend/internal/controllers/setup.go @@ -65,8 +65,9 @@ func setupControllers(p SetupControllersParams) { var Module = fx.Module("controllers", fx.Provide( - fx.Annotate(NewAuthController, fx.ResultTags(`group:"controllers"`)), fx.Annotate(NewServiceController, fx.ResultTags(`group:"controllers"`)), + fx.Annotate(NewAuthController, fx.ResultTags(`group:"controllers"`)), + fx.Annotate(NewProfileController, fx.ResultTags(`group:"controllers"`)), ), fx.Invoke(setupControllers), ) diff --git a/backend/internal/dto/profile.go b/backend/internal/dto/profile.go index 8b8de98..3c40915 100644 --- a/backend/internal/dto/profile.go +++ b/backend/internal/dto/profile.go @@ -18,12 +18,12 @@ package dto type ProfileDto struct { - Name string `json:"name"` - Bio string `json:"bio"` + Name string `json:"name" binding:"required" validate:"name"` + Bio string `json:"bio" validate:"bio"` AvatarUrl string `json:"avatar_url"` Birthday int64 `json:"birthday"` - Color string `json:"color"` - ColorGrad string `json:"color_grad"` + Color string `json:"color" validate:"color_hex"` + ColorGrad string `json:"color_grad" validate:"color_hex"` } type ProfileSettingsDto struct { diff --git a/backend/internal/errors/general.go b/backend/internal/errors/general.go index 40e9f1d..2a58602 100644 --- a/backend/internal/errors/general.go +++ b/backend/internal/errors/general.go @@ -26,4 +26,5 @@ var ( ErrBadRequest = errors.New("Bad request") ErrForbidden = errors.New("Access is denied") ErrTooManyRequests = errors.New("Too many requests") + ErrNotFound = errors.New("Resource not found") ) diff --git a/backend/internal/services/profile.go b/backend/internal/services/profile.go index 6a0e1e7..3c141c8 100644 --- a/backend/internal/services/profile.go +++ b/backend/internal/services/profile.go @@ -31,7 +31,7 @@ type ProfileService interface { UpdateProfile(cinfo dto.ClientInfo, newProfile dto.ProfileDto) (bool, error) GetProfileSettings(cinfo dto.ClientInfo) (dto.ProfileSettingsDto, error) UpdateProfileSettings(cinfo dto.ClientInfo, newProfileSettings dto.ProfileSettingsDto) (bool, error) - UploadAvatar(cinfo dto.ClientInfo, filePath string) + UploadAvatar(cinfo dto.ClientInfo, filePath string) (string, error) } type profileServiceImpl struct { @@ -64,6 +64,6 @@ func (p *profileServiceImpl) UpdateProfileSettings(cinfo dto.ClientInfo, newProf panic("unimplemented") } -func (p *profileServiceImpl) UploadAvatar(cinfo dto.ClientInfo, filePath string) { +func (p *profileServiceImpl) UploadAvatar(cinfo dto.ClientInfo, filePath string) (string, error) { panic("unimplemented") } diff --git a/backend/internal/services/setup.go b/backend/internal/services/setup.go index dbe2db1..58c6617 100644 --- a/backend/internal/services/setup.go +++ b/backend/internal/services/setup.go @@ -25,5 +25,6 @@ var Module = fx.Module("services", fx.Provide( NewSmtpService, NewAuthService, + NewProfileService, ), ) diff --git a/backend/internal/validation/custom.go b/backend/internal/validation/custom.go index 98327ae..32937a0 100644 --- a/backend/internal/validation/custom.go +++ b/backend/internal/validation/custom.go @@ -48,6 +48,20 @@ func GetCustomHandlers() []CustomValidatorHandler { return regexp.MustCompile(`^.{1,75}$`).MatchString(username) }}, + { + FieldName: "bio", + Function: func(fl validator.FieldLevel) bool { + username := fl.Field().String() + return regexp.MustCompile(`^.{1,512}$`).MatchString(username) + }}, + + { + FieldName: "color_hex", + Function: func(fl validator.FieldLevel) bool { + username := fl.Field().String() + return regexp.MustCompile(`^#[0-9a-f]{6,6}$`).MatchString(username) + }}, + { FieldName: "password", Function: func(fl validator.FieldLevel) bool {