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:
commit
7a300de997
8 changed files with 125 additions and 32 deletions
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue