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",
"required": true,
"schema": {
"$ref": "#/definitions/auth.authRequest"
"$ref": "#/definitions/auth.loginRequest"
}
}
],
@ -1700,7 +1700,7 @@ const docTemplate = `{
},
"/auth/register": {
"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": [
"application/json"
],
@ -1718,7 +1718,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/auth.authRequest"
"$ref": "#/definitions/auth.registerRequest"
}
}
],
@ -1778,7 +1778,7 @@ const docTemplate = `{
}
}
},
"auth.authRequest": {
"auth.loginRequest": {
"type": "object",
"required": [
"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": {
"type": "object",
"properties": {
"ativo": {
"type": "boolean"
},
"email": {
"type": "string"
},

View file

@ -1581,7 +1581,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/auth.authRequest"
"$ref": "#/definitions/auth.loginRequest"
}
}
],
@ -1694,7 +1694,7 @@
},
"/auth/register": {
"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": [
"application/json"
],
@ -1712,7 +1712,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/auth.authRequest"
"$ref": "#/definitions/auth.registerRequest"
}
}
],
@ -1772,7 +1772,7 @@
}
}
},
"auth.authRequest": {
"auth.loginRequest": {
"type": "object",
"required": [
"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": {
"type": "object",
"properties": {
"ativo": {
"type": "boolean"
},
"email": {
"type": "string"
},

View file

@ -15,7 +15,7 @@ definitions:
required:
- ano_semestre
type: object
auth.authRequest:
auth.loginRequest:
properties:
email:
type: string
@ -36,8 +36,26 @@ definitions:
user:
$ref: '#/definitions/auth.userResponse'
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:
properties:
ativo:
type: boolean
email:
type: string
id:
@ -1338,7 +1356,7 @@ paths:
name: request
required: true
schema:
$ref: '#/definitions/auth.authRequest'
$ref: '#/definitions/auth.loginRequest'
produces:
- application/json
responses:
@ -1416,15 +1434,15 @@ paths:
post:
consumes:
- application/json
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
parameters:
- description: Register Request
in: body
name: request
required: true
schema:
$ref: '#/definitions/auth.authRequest'
$ref: '#/definitions/auth.registerRequest'
produces:
- application/json
responses:

View file

@ -18,32 +18,40 @@ func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
type authRequest struct {
Email string `json:"email" binding:"required,email"`
Senha string `json:"senha" binding:"required,min=6"`
type registerRequest struct {
Email string `json:"email" binding:"required,email"`
Senha string `json:"senha" binding:"required,min=6"`
Nome string `json:"nome" binding:"required"`
Telefone string `json:"telefone"`
}
// Register godoc
// @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
// @Accept json
// @Produce json
// @Param request body authRequest true "Register Request"
// @Param request body registerRequest true "Register Request"
// @Success 201 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/register [post]
func (h *Handler) Register(c *gin.Context) {
var req authRequest
var req registerRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Default simplified registration: Role="profissional", No professional data yet
// Default simplified registration: 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)
if err != nil {
@ -58,6 +66,11 @@ func (h *Handler) Register(c *gin.Context) {
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 {
AccessToken string `json:"access_token"`
ExpiresAt string `json:"expires_at"`
@ -69,6 +82,7 @@ type userResponse struct {
ID string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
Ativo bool `json:"ativo"`
}
// Login godoc
@ -77,13 +91,13 @@ type userResponse struct {
// @Tags auth
// @Accept json
// @Produce json
// @Param request body authRequest true "Login Request"
// @Param request body loginRequest true "Login Request"
// @Success 200 {object} loginResponse
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/login [post]
func (h *Handler) Login(c *gin.Context) {
var req authRequest
var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@ -112,6 +126,7 @@ func (h *Handler) Login(c *gin.Context) {
ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email,
Role: user.Role,
Ativo: user.Ativo,
},
}

View file

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

View file

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

View file

@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS usuarios (
email VARCHAR(255) UNIQUE NOT NULL,
senha_hash VARCHAR(255) NOT NULL,
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(),
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")
}
funcaoUUID, err := uuid.Parse(input.FuncaoProfissionalID)
if err != nil {
return nil, errors.New("invalid funcao_profissional_id")
var funcaoUUID uuid.UUID
var funcaoValid bool
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{
UsuarioID: pgtype.UUID{Bytes: usuarioUUID, Valid: true},
Nome: input.Nome,
FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: true},
FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: funcaoValid},
Endereco: toPgText(input.Endereco),
Cidade: toPgText(input.Cidade),
Uf: toPgText(input.Uf),