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}