Merge pull request #13 from rede5/back-task-5

feat(auth): melhora registro e login com vinculo profissional e status ativo  

 aprimora o fluxo de autenticação, permitindo que o processo de registro já capture dados básicos do profissional (Nome, Telefone) e vincule automaticamente a um perfil na tabela cadastro_profissionais. Também implementa a política de segurança onde novos usuários nascem Inativos por padrão.

Principais Mudanças:

Registro (/auth/register):
Novos campos obrigatórios/opcionais: nome, telefone.
Vínculo Automático: Cria registro na tabela usuarios e cadastro_profissionais numa única transação lógica.
Default Inativo: Usuários agora são criados com ativo = false (alterado na query e no schema), exigindo aprovação posterior.
Login (/auth/login):
Separação da strutura de Request (
loginRequest
 vs 
registerRequest
) para evitar erros de validação.
Resposta agora inclui o status ativo: boolean para que o frontend possa tratar usuários pendentes.
Database:
Ajuste na constraint default da coluna ativo em usuarios.
Impacto: O frontend agora deve tratar o caso de ativo: false no login (ex: mostrar mensagem "Aguardando aprovação") e enviar nome/telefone no registro.
This commit is contained in:
Andre F. Rodrigues 2025-12-10 17:51:23 -03:00 committed by GitHub
commit 7a300de997
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 125 additions and 32 deletions

View file

@ -1587,7 +1587,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/auth.authRequest" "$ref": "#/definitions/auth.loginRequest"
} }
} }
], ],
@ -1700,7 +1700,7 @@ const docTemplate = `{
}, },
"/auth/register": { "/auth/register": {
"post": { "post": {
"description": "Register a new user (defaults to 'profissional' role) with email and password", "description": "Register a new user (defaults to 'profissional' role) with email, password, name and phone",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -1718,7 +1718,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/auth.authRequest" "$ref": "#/definitions/auth.registerRequest"
} }
} }
], ],
@ -1778,7 +1778,7 @@ const docTemplate = `{
} }
} }
}, },
"auth.authRequest": { "auth.loginRequest": {
"type": "object", "type": "object",
"required": [ "required": [
"email", "email",
@ -1809,9 +1809,35 @@ const docTemplate = `{
} }
} }
}, },
"auth.registerRequest": {
"type": "object",
"required": [
"email",
"nome",
"senha"
],
"properties": {
"email": {
"type": "string"
},
"nome": {
"type": "string"
},
"senha": {
"type": "string",
"minLength": 6
},
"telefone": {
"type": "string"
}
}
},
"auth.userResponse": { "auth.userResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"ativo": {
"type": "boolean"
},
"email": { "email": {
"type": "string" "type": "string"
}, },

View file

@ -1581,7 +1581,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/auth.authRequest" "$ref": "#/definitions/auth.loginRequest"
} }
} }
], ],
@ -1694,7 +1694,7 @@
}, },
"/auth/register": { "/auth/register": {
"post": { "post": {
"description": "Register a new user (defaults to 'profissional' role) with email and password", "description": "Register a new user (defaults to 'profissional' role) with email, password, name and phone",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -1712,7 +1712,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/auth.authRequest" "$ref": "#/definitions/auth.registerRequest"
} }
} }
], ],
@ -1772,7 +1772,7 @@
} }
} }
}, },
"auth.authRequest": { "auth.loginRequest": {
"type": "object", "type": "object",
"required": [ "required": [
"email", "email",
@ -1803,9 +1803,35 @@
} }
} }
}, },
"auth.registerRequest": {
"type": "object",
"required": [
"email",
"nome",
"senha"
],
"properties": {
"email": {
"type": "string"
},
"nome": {
"type": "string"
},
"senha": {
"type": "string",
"minLength": 6
},
"telefone": {
"type": "string"
}
}
},
"auth.userResponse": { "auth.userResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"ativo": {
"type": "boolean"
},
"email": { "email": {
"type": "string" "type": "string"
}, },

View file

@ -15,7 +15,7 @@ definitions:
required: required:
- ano_semestre - ano_semestre
type: object type: object
auth.authRequest: auth.loginRequest:
properties: properties:
email: email:
type: string type: string
@ -36,8 +36,26 @@ definitions:
user: user:
$ref: '#/definitions/auth.userResponse' $ref: '#/definitions/auth.userResponse'
type: object type: object
auth.registerRequest:
properties:
email:
type: string
nome:
type: string
senha:
minLength: 6
type: string
telefone:
type: string
required:
- email
- nome
- senha
type: object
auth.userResponse: auth.userResponse:
properties: properties:
ativo:
type: boolean
email: email:
type: string type: string
id: id:
@ -1338,7 +1356,7 @@ paths:
name: request name: request
required: true required: true
schema: schema:
$ref: '#/definitions/auth.authRequest' $ref: '#/definitions/auth.loginRequest'
produces: produces:
- application/json - application/json
responses: responses:
@ -1416,15 +1434,15 @@ paths:
post: post:
consumes: consumes:
- application/json - application/json
description: Register a new user (defaults to 'profissional' role) with email description: Register a new user (defaults to 'profissional' role) with email,
and password password, name and phone
parameters: parameters:
- description: Register Request - description: Register Request
in: body in: body
name: request name: request
required: true required: true
schema: schema:
$ref: '#/definitions/auth.authRequest' $ref: '#/definitions/auth.registerRequest'
produces: produces:
- application/json - application/json
responses: responses:

View file

@ -18,32 +18,40 @@ func NewHandler(service *Service) *Handler {
return &Handler{service: service} return &Handler{service: service}
} }
type authRequest struct { type registerRequest struct {
Email string `json:"email" binding:"required,email"` Email string `json:"email" binding:"required,email"`
Senha string `json:"senha" binding:"required,min=6"` Senha string `json:"senha" binding:"required,min=6"`
Nome string `json:"nome" binding:"required"`
Telefone string `json:"telefone"`
} }
// Register godoc // Register godoc
// @Summary Register a new user // @Summary Register a new user
// @Description Register a new user (defaults to 'profissional' role) with email and password // @Description Register a new user (defaults to 'profissional' role) with email, password, name and phone
// @Tags auth // @Tags auth
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param request body authRequest true "Register Request" // @Param request body registerRequest true "Register Request"
// @Success 201 {object} map[string]string // @Success 201 {object} map[string]string
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /auth/register [post] // @Router /auth/register [post]
func (h *Handler) Register(c *gin.Context) { func (h *Handler) Register(c *gin.Context) {
var req authRequest var req registerRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
// Default simplified registration: Role="profissional", No professional data yet // Default simplified registration: Role="profissional"
role := "profissional" role := "profissional"
var profData *profissionais.CreateProfissionalInput = nil
// Create professional data from input
profData := &profissionais.CreateProfissionalInput{
Nome: req.Nome,
Whatsapp: &req.Telefone, // Map Telefone to Whatsapp
// FuncaoProfissionalID is left empty intentionally
}
_, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, role, profData) _, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, role, profData)
if err != nil { if err != nil {
@ -58,6 +66,11 @@ func (h *Handler) Register(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{"message": "user created"}) c.JSON(http.StatusCreated, gin.H{"message": "user created"})
} }
type loginRequest struct {
Email string `json:"email" binding:"required,email"`
Senha string `json:"senha" binding:"required,min=6"`
}
type loginResponse struct { type loginResponse struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
ExpiresAt string `json:"expires_at"` ExpiresAt string `json:"expires_at"`
@ -69,6 +82,7 @@ type userResponse struct {
ID string `json:"id"` ID string `json:"id"`
Email string `json:"email"` Email string `json:"email"`
Role string `json:"role"` Role string `json:"role"`
Ativo bool `json:"ativo"`
} }
// Login godoc // Login godoc
@ -77,13 +91,13 @@ type userResponse struct {
// @Tags auth // @Tags auth
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param request body authRequest true "Login Request" // @Param request body loginRequest true "Login Request"
// @Success 200 {object} loginResponse // @Success 200 {object} loginResponse
// @Failure 401 {object} map[string]string // @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /auth/login [post] // @Router /auth/login [post]
func (h *Handler) Login(c *gin.Context) { func (h *Handler) Login(c *gin.Context) {
var req authRequest var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
@ -112,6 +126,7 @@ func (h *Handler) Login(c *gin.Context) {
ID: uuid.UUID(user.ID.Bytes).String(), ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email, Email: user.Email,
Role: user.Role, Role: user.Role,
Ativo: user.Ativo,
}, },
} }

View file

@ -12,8 +12,8 @@ import (
) )
const createUsuario = `-- name: CreateUsuario :one const createUsuario = `-- name: CreateUsuario :one
INSERT INTO usuarios (email, senha_hash, role) INSERT INTO usuarios (email, senha_hash, role, ativo)
VALUES ($1, $2, $3) VALUES ($1, $2, $3, false)
RETURNING id, email, senha_hash, role, ativo, criado_em, atualizado_em RETURNING id, email, senha_hash, role, ativo, criado_em, atualizado_em
` `

View file

@ -1,6 +1,6 @@
-- name: CreateUsuario :one -- name: CreateUsuario :one
INSERT INTO usuarios (email, senha_hash, role) INSERT INTO usuarios (email, senha_hash, role, ativo)
VALUES ($1, $2, $3) VALUES ($1, $2, $3, false)
RETURNING *; RETURNING *;
-- name: GetUsuarioByEmail :one -- name: GetUsuarioByEmail :one

View file

@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS usuarios (
email VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL,
senha_hash VARCHAR(255) NOT NULL, senha_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'profissional', role VARCHAR(50) NOT NULL DEFAULT 'profissional',
ativo BOOLEAN NOT NULL DEFAULT TRUE, ativo BOOLEAN NOT NULL DEFAULT FALSE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW() atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );

View file

@ -50,15 +50,23 @@ func (s *Service) Create(ctx context.Context, userID string, input CreateProfiss
return nil, errors.New("invalid usuario_id from context") return nil, errors.New("invalid usuario_id from context")
} }
funcaoUUID, err := uuid.Parse(input.FuncaoProfissionalID) var funcaoUUID uuid.UUID
if err != nil { var funcaoValid bool
return nil, errors.New("invalid funcao_profissional_id") if input.FuncaoProfissionalID != "" {
parsed, err := uuid.Parse(input.FuncaoProfissionalID)
if err != nil {
return nil, errors.New("invalid funcao_profissional_id")
}
funcaoUUID = parsed
funcaoValid = true
} else {
funcaoValid = false
} }
params := generated.CreateProfissionalParams{ params := generated.CreateProfissionalParams{
UsuarioID: pgtype.UUID{Bytes: usuarioUUID, Valid: true}, UsuarioID: pgtype.UUID{Bytes: usuarioUUID, Valid: true},
Nome: input.Nome, Nome: input.Nome,
FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: true}, FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: funcaoValid},
Endereco: toPgText(input.Endereco), Endereco: toPgText(input.Endereco),
Cidade: toPgText(input.Cidade), Cidade: toPgText(input.Cidade),
Uf: toPgText(input.Uf), Uf: toPgText(input.Uf),