From 1f9b54d719ba7a66c69a549b5f50c9319dc50e1d Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Mon, 5 Jan 2026 13:30:02 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20resolve=20problemas=20de=20cadastro,=20s?= =?UTF-8?q?eletor=20de=20localiza=C3=A7=C3=A3o=20e=20swagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Corrige violação de restrição de role no Registro de Candidato (usa 'candidate' em minúsculo) - Corrige erro de chave duplicada para slug da empresa adicionando timestamp ao workspace do candidato - Corrige crash no LocationPicker tratando respostas nulas no frontend e retornando arrays vazios no backend - Corrige documentação do Swagger para o endpoint de Login e adiciona definição de segurança BearerAuth --- backend/cmd/api/main.go | 3 + backend/docs/docs.go | 186 ++++++++++++------ backend/docs/swagger.json | 186 ++++++++++++------ backend/docs/swagger.yaml | 122 ++++++++---- .../internal/api/handlers/core_handlers.go | 2 +- .../core/usecases/auth/register_candidate.go | 5 +- .../postgres/location_repository.go | 8 +- frontend/src/components/location-picker.tsx | 7 +- 8 files changed, 355 insertions(+), 164 deletions(-) diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index 70dda9f..90beaef 100755 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -19,6 +19,9 @@ import ( // @version 1.0 // @description API for GoHorseJobs recruitment platform. // @BasePath / +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization func main() { // Load .env file if err := godotenv.Load(); err != nil { diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 43436b6..7f7749a 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -15,6 +15,41 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/admin/storage/test-connection": { + "post": { + "description": "Checks if the current configuration (DB or Env) allows access to the S3 bucket.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Test Object Storage Connection", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/applications": { "get": { "description": "List all applications for a job", @@ -262,7 +297,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "type": "object" + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest" } } ], @@ -288,6 +323,23 @@ const docTemplate = `{ } } }, + "/api/v1/auth/logout": { + "post": { + "description": "Clears the httpOnly JWT cookie, effectively logging the user out.", + "tags": [ + "Auth" + ], + "summary": "User Logout", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, "/api/v1/companies": { "get": { "description": "Returns a list of all companies.", @@ -1636,63 +1688,6 @@ const docTemplate = `{ } } }, - "/api/v1/system/credentials": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Saves encrypted credentials payload (e.g. Stripe key encrypted by Backoffice)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "System" - ], - "summary": "Save Credentials", - "parameters": [ - { - "description": "Credentials Payload", - "name": "request", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "400": { - "description": "Invalid Request", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "string" - } - } - } - } - }, "/api/v1/tokens": { "post": { "security": [ @@ -1954,6 +1949,63 @@ const docTemplate = `{ } } }, + "/api/v1/users/me/password": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates the current user's password.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Update My Password", + "parameters": [ + { + "description": "Password Details", + "name": "password", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid Request", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, "/api/v1/users/me/profile": { "patch": { "security": [ @@ -2112,6 +2164,17 @@ const docTemplate = `{ } }, "definitions": { + "github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, "github_com_rede5_gohorsejobs_backend_internal_core_dto.SaveFCMTokenRequest": { "type": "object", "properties": { @@ -2766,6 +2829,13 @@ const docTemplate = `{ } } } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } } }` diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index b72d773..03cfa0d 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -8,6 +8,41 @@ }, "basePath": "/", "paths": { + "/admin/storage/test-connection": { + "post": { + "description": "Checks if the current configuration (DB or Env) allows access to the S3 bucket.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Test Object Storage Connection", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/applications": { "get": { "description": "List all applications for a job", @@ -255,7 +290,7 @@ "in": "body", "required": true, "schema": { - "type": "object" + "$ref": "#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest" } } ], @@ -281,6 +316,23 @@ } } }, + "/api/v1/auth/logout": { + "post": { + "description": "Clears the httpOnly JWT cookie, effectively logging the user out.", + "tags": [ + "Auth" + ], + "summary": "User Logout", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object" + } + } + } + } + }, "/api/v1/companies": { "get": { "description": "Returns a list of all companies.", @@ -1629,63 +1681,6 @@ } } }, - "/api/v1/system/credentials": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Saves encrypted credentials payload (e.g. Stripe key encrypted by Backoffice)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "System" - ], - "summary": "Save Credentials", - "parameters": [ - { - "description": "Credentials Payload", - "name": "request", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "400": { - "description": "Invalid Request", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "string" - } - } - } - } - }, "/api/v1/tokens": { "post": { "security": [ @@ -1947,6 +1942,63 @@ } } }, + "/api/v1/users/me/password": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates the current user's password.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Update My Password", + "parameters": [ + { + "description": "Password Details", + "name": "password", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid Request", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, "/api/v1/users/me/profile": { "patch": { "security": [ @@ -2105,6 +2157,17 @@ } }, "definitions": { + "github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, "github_com_rede5_gohorsejobs_backend_internal_core_dto.SaveFCMTokenRequest": { "type": "object", "properties": { @@ -2759,5 +2822,12 @@ } } } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } } } \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 2f10387..b210c25 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,5 +1,12 @@ basePath: / definitions: + github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest: + properties: + email: + type: string + password: + type: string + type: object github_com_rede5_gohorsejobs_backend_internal_core_dto.SaveFCMTokenRequest: properties: platform: @@ -468,6 +475,30 @@ info: title: GoHorseJobs API version: "1.0" paths: + /admin/storage/test-connection: + post: + consumes: + - application/json + description: Checks if the current configuration (DB or Env) allows access to + the S3 bucket. + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + summary: Test Object Storage Connection + tags: + - admin /api/v1/applications: get: consumes: @@ -630,7 +661,7 @@ paths: name: login required: true schema: - type: object + $ref: '#/definitions/github_com_rede5_gohorsejobs_backend_internal_core_dto.LoginRequest' produces: - application/json responses: @@ -649,6 +680,17 @@ paths: summary: User Login tags: - Auth + /api/v1/auth/logout: + post: + description: Clears the httpOnly JWT cookie, effectively logging the user out. + responses: + "200": + description: OK + schema: + type: object + summary: User Logout + tags: + - Auth /api/v1/companies: get: consumes: @@ -1512,43 +1554,6 @@ paths: summary: List All Tickets (Admin) tags: - Support - /api/v1/system/credentials: - post: - consumes: - - application/json - description: Saves encrypted credentials payload (e.g. Stripe key encrypted - by Backoffice) - parameters: - - description: Credentials Payload - in: body - name: request - required: true - schema: - additionalProperties: - type: string - type: object - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: - type: string - type: object - "400": - description: Invalid Request - schema: - type: string - "500": - description: Internal Server Error - schema: - type: string - security: - - BearerAuth: [] - summary: Save Credentials - tags: - - System /api/v1/tokens: post: consumes: @@ -1783,6 +1788,42 @@ paths: summary: Upload Avatar tags: - Users + /api/v1/users/me/password: + patch: + consumes: + - application/json + description: Updates the current user's password. + parameters: + - description: Password Details + in: body + name: password + required: true + schema: + type: object + produces: + - application/json + responses: + "204": + description: No Content + schema: + type: string + "400": + description: Invalid Request + schema: + type: string + "401": + description: Unauthorized + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + security: + - BearerAuth: [] + summary: Update My Password + tags: + - Users /api/v1/users/me/profile: patch: consumes: @@ -1815,4 +1856,9 @@ paths: summary: Update My Profile tags: - Users +securityDefinitions: + BearerAuth: + in: header + name: Authorization + type: apiKey swagger: "2.0" diff --git a/backend/internal/api/handlers/core_handlers.go b/backend/internal/api/handlers/core_handlers.go index 048772c..474d7e3 100644 --- a/backend/internal/api/handlers/core_handlers.go +++ b/backend/internal/api/handlers/core_handlers.go @@ -59,7 +59,7 @@ func NewCoreHandlers(l *auth.LoginUseCase, reg *auth.RegisterCandidateUseCase, c // @Tags Auth // @Accept json // @Produce json -// @Param login body object true "Login Credentials" +// @Param login body dto.LoginRequest true "Login Credentials" // @Success 200 {object} object // @Failure 400 {string} string "Invalid Request" // @Failure 401 {string} string "Unauthorized" diff --git a/backend/internal/core/usecases/auth/register_candidate.go b/backend/internal/core/usecases/auth/register_candidate.go index 6f5614e..a7b65b9 100644 --- a/backend/internal/core/usecases/auth/register_candidate.go +++ b/backend/internal/core/usecases/auth/register_candidate.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log" + "time" "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" "github.com/rede5/gohorsejobs/backend/internal/core/dto" @@ -44,7 +45,7 @@ func (uc *RegisterCandidateUseCase) Execute(ctx context.Context, input dto.Regis // NOTE: ID is empty - will be auto-generated by DB (SERIAL) candidateCompany := entity.NewCompany( "", // DB generates SERIAL id - fmt.Sprintf("Candidate - %s", input.Name), + fmt.Sprintf("Candidate - %s (%d)", input.Name, time.Now().Unix()), nil, // No document for candidates nil, // No contact - will use user's contact info ) @@ -65,7 +66,7 @@ func (uc *RegisterCandidateUseCase) Execute(ctx context.Context, input dto.Regis } // Assign Role - user.AssignRole(entity.Role{Name: "CANDIDATE"}) + user.AssignRole(entity.Role{Name: entity.RoleCandidate}) saved, err := uc.userRepo.Save(ctx, user) if err != nil { diff --git a/backend/internal/infrastructure/persistence/postgres/location_repository.go b/backend/internal/infrastructure/persistence/postgres/location_repository.go index ff9a0ce..73705b3 100644 --- a/backend/internal/infrastructure/persistence/postgres/location_repository.go +++ b/backend/internal/infrastructure/persistence/postgres/location_repository.go @@ -23,7 +23,7 @@ func (r *LocationRepository) ListCountries(ctx context.Context) ([]*entity.Count } defer rows.Close() - var countries []*entity.Country + countries := []*entity.Country{} for rows.Next() { c := &entity.Country{} // Scan columns. Be careful with NULL handling if fields are nullable, but schema says NOT NULL mostly. @@ -61,7 +61,7 @@ func (r *LocationRepository) ListStates(ctx context.Context, countryID int64) ([ } defer rows.Close() - var states []*entity.State + states := []*entity.State{} for rows.Next() { s := &entity.State{} var iso2, typeStr sql.NullString @@ -88,7 +88,7 @@ func (r *LocationRepository) ListCities(ctx context.Context, stateID int64) ([]* } defer rows.Close() - var cities []*entity.City + cities := []*entity.City{} for rows.Next() { c := &entity.City{} // schema: latitude NOT NULL, longitude NOT NULL. @@ -123,7 +123,7 @@ func (r *LocationRepository) Search(ctx context.Context, query string, countryID } defer rows.Close() - var results []*entity.LocationSearchResult + results := []*entity.LocationSearchResult{} for rows.Next() { res := &entity.LocationSearchResult{} var stateID sql.NullInt64 diff --git a/frontend/src/components/location-picker.tsx b/frontend/src/components/location-picker.tsx index e9415e9..62fba7e 100644 --- a/frontend/src/components/location-picker.tsx +++ b/frontend/src/components/location-picker.tsx @@ -27,9 +27,10 @@ export function LocationPicker({ value, onChange }: LocationPickerProps) { // Initial Load useEffect(() => { locationsApi.listCountries().then(res => { - setCountries(res); + const data = res || []; + setCountries(data); // Default to Brazil if available (User is Brazilian based on context) - const br = res.find(c => c.iso2 === "BR"); + const br = data.find(c => c.iso2 === "BR"); if (br) setSelectedCountry(br.id.toString()); }).catch(console.error); }, []); @@ -156,7 +157,7 @@ export function LocationPicker({ value, onChange }: LocationPickerProps) { - {countries.map((c) => ( + {Array.isArray(countries) && countries.map((c) => ( {c.emoji}