feat(auth): adiciona tipo profissional ao schema e corrige avatar

- Adiciona coluna `tipo_profissional` à tabela `usuarios`
- Atualiza handlers e services do Backend Go para persistir o tipo
- Atualiza registro no Frontend para enviar o nome da função (ex: "Cinegrafista")
- Corrige uploads S3 para compatibilidade com Civo (PathStyle)
- Script para definir política pública de leitura no bucket S3
- Adiciona fallback para imagens de avatar na Navbar
This commit is contained in:
NANDO9322 2025-12-22 12:37:42 -03:00
parent f625b6cd6c
commit cd196a0275
24 changed files with 785 additions and 224 deletions

View file

@ -15,6 +15,7 @@ import (
"photum-backend/internal/empresas"
"photum-backend/internal/funcoes"
"photum-backend/internal/profissionais"
"photum-backend/internal/storage"
"photum-backend/internal/tipos_eventos"
"photum-backend/internal/tipos_servicos"
"strings"
@ -70,6 +71,7 @@ func main() {
tiposEventosService := tipos_eventos.NewService(queries)
cadastroFotService := cadastro_fot.NewService(queries)
agendaService := agenda.NewService(queries)
s3Service := storage.NewS3Service(cfg)
// Seed Demo Users
if err := authService.EnsureDemoUsers(context.Background()); err != nil {
@ -77,7 +79,7 @@ func main() {
}
// Initialize handlers
authHandler := auth.NewHandler(authService)
authHandler := auth.NewHandler(authService, s3Service)
profissionaisHandler := profissionais.NewHandler(profissionaisService)
funcoesHandler := funcoes.NewHandler(funcoesService)
cursosHandler := cursos.NewHandler(cursosService)
@ -124,6 +126,7 @@ func main() {
authGroup.POST("/login", authHandler.Login)
authGroup.POST("/refresh", authHandler.Refresh)
authGroup.POST("/logout", authHandler.Logout)
authGroup.POST("/upload-url", authHandler.GetUploadURL)
}
// Public API Routes (Data Lists)

View file

@ -0,0 +1,66 @@
package main
import (
"context"
"fmt"
"log"
"photum-backend/internal/config"
"github.com/aws/aws-sdk-go-v2/aws"
awsConfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
func main() {
// Load config manually or assume env vars are set
cfg := config.LoadConfig()
// Custom Resolver for Civo Object Store
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{
URL: cfg.S3Endpoint,
SigningRegion: region,
}, nil
})
awsCfg, err := awsConfig.LoadDefaultConfig(context.TODO(),
awsConfig.WithRegion(cfg.S3Region),
awsConfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.S3AccessKey, cfg.S3SecretKey, "")),
awsConfig.WithEndpointResolverWithOptions(customResolver),
)
if err != nil {
log.Fatalf("unable to load SDK config, %v", err)
}
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.UsePathStyle = true
})
policy := fmt.Sprintf(`{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::%s/*"]
}
]
}`, cfg.S3Bucket)
log.Printf("Setting policy for bucket: %s...", cfg.S3Bucket)
_, err = client.PutBucketPolicy(context.TODO(), &s3.PutBucketPolicyInput{
Bucket: aws.String(cfg.S3Bucket),
Policy: aws.String(policy),
})
if err != nil {
log.Printf("Error setting policy: %v", err)
log.Println("Ensure your credentials have permission to set bucket policies, or configure it manually in the Civo console.")
} else {
log.Println("Successfully set bucket policy to Public Read!")
}
}

View file

@ -2401,7 +2401,7 @@ const docTemplate = `{
},
"/auth/register": {
"post": {
"description": "Register a new user with email, password, name, phone and role",
"description": "Register a new user with email, password, name, phone, role and professional type",
"consumes": [
"application/json"
],
@ -2453,6 +2453,43 @@ const docTemplate = `{
}
}
}
},
"/auth/upload-url": {
"post": {
"description": "Get a pre-signed URL to upload a file directly to S3/Civo",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Get S3 Presigned URL for upload",
"parameters": [
{
"description": "Upload URL Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/auth.uploadURLRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
@ -2599,14 +2636,12 @@ const docTemplate = `{
"type": "string"
},
"empresa_id": {
"description": "Optional, for EVENT_OWNER",
"type": "string"
},
"nome": {
"type": "string"
},
"role": {
"description": "Role is now required",
"type": "string"
},
"senha": {
@ -2615,6 +2650,10 @@ const docTemplate = `{
},
"telefone": {
"type": "string"
},
"tipo_profissional": {
"description": "New field",
"type": "string"
}
}
},
@ -2629,6 +2668,21 @@ const docTemplate = `{
}
}
},
"auth.uploadURLRequest": {
"type": "object",
"required": [
"content_type",
"filename"
],
"properties": {
"content_type": {
"type": "string"
},
"filename": {
"type": "string"
}
}
},
"auth.userResponse": {
"type": "object",
"properties": {
@ -2812,6 +2866,9 @@ const docTemplate = `{
"agencia": {
"type": "string"
},
"avatar_url": {
"type": "string"
},
"banco": {
"type": "string"
},
@ -2973,6 +3030,9 @@ const docTemplate = `{
"agencia": {
"type": "string"
},
"avatar_url": {
"type": "string"
},
"banco": {
"type": "string"
},

View file

@ -2395,7 +2395,7 @@
},
"/auth/register": {
"post": {
"description": "Register a new user with email, password, name, phone and role",
"description": "Register a new user with email, password, name, phone, role and professional type",
"consumes": [
"application/json"
],
@ -2447,6 +2447,43 @@
}
}
}
},
"/auth/upload-url": {
"post": {
"description": "Get a pre-signed URL to upload a file directly to S3/Civo",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"auth"
],
"summary": "Get S3 Presigned URL for upload",
"parameters": [
{
"description": "Upload URL Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/auth.uploadURLRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
@ -2593,14 +2630,12 @@
"type": "string"
},
"empresa_id": {
"description": "Optional, for EVENT_OWNER",
"type": "string"
},
"nome": {
"type": "string"
},
"role": {
"description": "Role is now required",
"type": "string"
},
"senha": {
@ -2609,6 +2644,10 @@
},
"telefone": {
"type": "string"
},
"tipo_profissional": {
"description": "New field",
"type": "string"
}
}
},
@ -2623,6 +2662,21 @@
}
}
},
"auth.uploadURLRequest": {
"type": "object",
"required": [
"content_type",
"filename"
],
"properties": {
"content_type": {
"type": "string"
},
"filename": {
"type": "string"
}
}
},
"auth.userResponse": {
"type": "object",
"properties": {
@ -2806,6 +2860,9 @@
"agencia": {
"type": "string"
},
"avatar_url": {
"type": "string"
},
"banco": {
"type": "string"
},
@ -2967,6 +3024,9 @@
"agencia": {
"type": "string"
},
"avatar_url": {
"type": "string"
},
"banco": {
"type": "string"
},

View file

@ -92,18 +92,19 @@ definitions:
email:
type: string
empresa_id:
description: Optional, for EVENT_OWNER
type: string
nome:
type: string
role:
description: Role is now required
type: string
senha:
minLength: 6
type: string
telefone:
type: string
tipo_profissional:
description: New field
type: string
required:
- email
- nome
@ -117,6 +118,16 @@ definitions:
required:
- role
type: object
auth.uploadURLRequest:
properties:
content_type:
type: string
filename:
type: string
required:
- content_type
- filename
type: object
auth.userResponse:
properties:
ativo:
@ -236,6 +247,8 @@ definitions:
properties:
agencia:
type: string
avatar_url:
type: string
banco:
type: string
carro_disponivel:
@ -343,6 +356,8 @@ definitions:
properties:
agencia:
type: string
avatar_url:
type: string
banco:
type: string
carro_disponivel:
@ -1966,7 +1981,8 @@ paths:
post:
consumes:
- application/json
description: Register a new user with email, password, name, phone and role
description: Register a new user with email, password, name, phone, role and
professional type
parameters:
- description: Register Request
in: body
@ -1998,6 +2014,30 @@ paths:
summary: Register a new user
tags:
- auth
/auth/upload-url:
post:
consumes:
- application/json
description: Get a pre-signed URL to upload a file directly to S3/Civo
parameters:
- description: Upload URL Request
in: body
name: request
required: true
schema:
$ref: '#/definitions/auth.uploadURLRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties:
type: string
type: object
summary: Get S3 Presigned URL for upload
tags:
- auth
securityDefinitions:
BearerAuth:
in: header

View file

@ -15,6 +15,25 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.2.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect

View file

@ -4,6 +4,44 @@ github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9
github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=

View file

@ -5,31 +5,67 @@ import (
"strings"
"photum-backend/internal/profissionais"
"photum-backend/internal/storage"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type Handler struct {
service *Service
service *Service
s3Service *storage.S3Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
func NewHandler(service *Service, s3Service *storage.S3Service) *Handler {
return &Handler{service: service, s3Service: s3Service}
}
type uploadURLRequest struct {
Filename string `json:"filename" binding:"required"`
ContentType string `json:"content_type" binding:"required"`
}
// GetUploadURL godoc
// @Summary Get S3 Presigned URL for upload
// @Description Get a pre-signed URL to upload a file directly to S3/Civo
// @Tags auth
// @Accept json
// @Produce json
// @Param request body uploadURLRequest true "Upload URL Request"
// @Success 200 {object} map[string]string
// @Router /auth/upload-url [post]
func (h *Handler) GetUploadURL(c *gin.Context) {
var req uploadURLRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
uploadURL, publicURL, err := h.s3Service.GeneratePresignedURL(req.Filename, req.ContentType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"upload_url": uploadURL,
"public_url": publicURL,
})
}
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"`
Role string `json:"role" binding:"required"` // Role is now required
EmpresaID string `json:"empresa_id"` // Optional, for EVENT_OWNER
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"`
Role string `json:"role" binding:"required"`
EmpresaID string `json:"empresa_id"`
TipoProfissional string `json:"tipo_profissional"` // New field
}
// Register godoc
// @Summary Register a new user
// @Description Register a new user with email, password, name, phone and role
// @Description Register a new user with email, password, name, phone, role and professional type
// @Tags auth
// @Accept json
// @Produce json
@ -47,16 +83,6 @@ func (h *Handler) Register(c *gin.Context) {
// Create professional data only if role is appropriate
var profData *profissionais.CreateProfissionalInput
// For PHOTOGRAPHER or BUSINESS_OWNER, we might populate this if we were doing 1-step,
// but actually 'nome' and 'telefone' are passed as args now.
// We keep passing nil for profData because Service logic for Professionals relies on 'CreateProfissionalInput'
// However, I updated Service to take nome/telefone directly.
// Wait, the Service code I JUST wrote takes (email, senha, role, nome, telefone, empresaID, profissionalData).
// If role is Photographer, the Service code checks `profissionalData`.
// I should probably populate `profissionalData` if it's a professional.
// PHOTOGRAPHER role is handled by a separate flow (ProfessionalRegister) that calls CreateProfissional after Register.
// We skip creating the partial profile here to avoid duplicates.
if req.Role == "BUSINESS_OWNER" {
profData = &profissionais.CreateProfissionalInput{
Nome: req.Nome,
@ -69,7 +95,7 @@ func (h *Handler) Register(c *gin.Context) {
empresaIDPtr = &req.EmpresaID
}
user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.Telefone, empresaIDPtr, profData)
user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.Telefone, req.TipoProfissional, empresaIDPtr, profData)
if err != nil {
if strings.Contains(err.Error(), "duplicate key") {
c.JSON(http.StatusConflict, gin.H{"error": "email already registered"})
@ -218,6 +244,7 @@ func (h *Handler) Login(c *gin.Context) {
"funcao_profissional_id": uuid.UUID(profData.FuncaoProfissionalID.Bytes).String(),
"funcao_profissional": profData.FuncaoNome.String,
"equipamentos": profData.Equipamentos.String,
"avatar_url": profData.AvatarUrl.String,
}
}

View file

@ -43,7 +43,7 @@ func NewService(queries *generated.Queries, profissionaisService *profissionais.
}
}
func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefone string, empresaID *string, profissionalData *profissionais.CreateProfissionalInput) (*generated.Usuario, error) {
func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefone, tipoProfissional string, empresaID *string, profissionalData *profissionais.CreateProfissionalInput) (*generated.Usuario, error) {
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost)
if err != nil {
@ -52,9 +52,10 @@ func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefo
// Create user
user, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{
Email: email,
SenhaHash: string(hashedPassword),
Role: role,
Email: email,
SenhaHash: string(hashedPassword),
Role: role,
TipoProfissional: toPgText(&tipoProfissional),
})
if err != nil {
return nil, err
@ -311,3 +312,10 @@ func (s *Service) GetUser(ctx context.Context, id string) (*generated.GetUsuario
}
return &user, nil
}
func toPgText(s *string) pgtype.Text {
if s == nil {
return pgtype.Text{Valid: false}
}
return pgtype.Text{String: *s, Valid: true}
}

View file

@ -18,6 +18,11 @@ type Config struct {
JwtRefreshTTLDays int
CorsAllowedOrigins string
SwaggerHost string
S3Endpoint string
S3AccessKey string
S3SecretKey string
S3Bucket string
S3Region string
}
func LoadConfig() *Config {
@ -36,6 +41,11 @@ func LoadConfig() *Config {
JwtRefreshTTLDays: getEnvAsInt("JWT_REFRESH_TTL_DAYS", 30),
CorsAllowedOrigins: getEnv("CORS_ALLOWED_ORIGINS", "*"),
SwaggerHost: getEnv("SWAGGER_HOST", "localhost:8080"),
S3Endpoint: getEnv("S3_ENDPOINT", ""),
S3AccessKey: getEnv("S3_ACCESS_KEY", ""),
S3SecretKey: getEnv("S3_SECRET_KEY", ""),
S3Bucket: getEnv("S3_BUCKET", ""),
S3Region: getEnv("S3_REGION", "nyc1"),
}
}

View file

@ -198,7 +198,7 @@ func (q *Queries) GetAgenda(ctx context.Context, id pgtype.UUID) (Agenda, error)
}
const getAgendaProfessionals = `-- name: GetAgendaProfessionals :many
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.criado_em, p.atualizado_em, f.nome as funcao_nome, u.email
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome, u.email
FROM cadastro_profissionais p
JOIN agenda_profissionais ap ON p.id = ap.profissional_id
LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
@ -232,6 +232,7 @@ type GetAgendaProfessionalsRow struct {
TabelaFree pgtype.Text `json:"tabela_free"`
ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"`
Equipamentos pgtype.Text `json:"equipamentos"`
AvatarUrl pgtype.Text `json:"avatar_url"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
FuncaoNome pgtype.Text `json:"funcao_nome"`
@ -273,6 +274,7 @@ func (q *Queries) GetAgendaProfessionals(ctx context.Context, agendaID pgtype.UU
&i.TabelaFree,
&i.ExtraPorEquipamento,
&i.Equipamentos,
&i.AvatarUrl,
&i.CriadoEm,
&i.AtualizadoEm,
&i.FuncaoNome,

View file

@ -106,6 +106,7 @@ type CadastroProfissionai struct {
TabelaFree pgtype.Text `json:"tabela_free"`
ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"`
Equipamentos pgtype.Text `json:"equipamentos"`
AvatarUrl pgtype.Text `json:"avatar_url"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
}
@ -161,11 +162,12 @@ type TiposServico struct {
}
type Usuario struct {
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
SenhaHash string `json:"senha_hash"`
Role string `json:"role"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
SenhaHash string `json:"senha_hash"`
Role string `json:"role"`
TipoProfissional pgtype.Text `json:"tipo_profissional"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
}

View file

@ -17,11 +17,11 @@ INSERT INTO cadastro_profissionais (
cpf_cnpj_titular, banco, agencia, conta_pix, carro_disponivel,
tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec,
educacao_simpatia, desempenho_evento, disp_horario, media,
tabela_free, extra_por_equipamento, equipamentos
tabela_free, extra_por_equipamento, equipamentos, avatar_url
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
$16, $17, $18, $19, $20, $21, $22, $23, $24
) RETURNING id, usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp, cpf_cnpj_titular, banco, agencia, conta_pix, carro_disponivel, tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec, educacao_simpatia, desempenho_evento, disp_horario, media, tabela_free, extra_por_equipamento, equipamentos, criado_em, atualizado_em
$16, $17, $18, $19, $20, $21, $22, $23, $24, $25
) RETURNING id, usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp, cpf_cnpj_titular, banco, agencia, conta_pix, carro_disponivel, tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec, educacao_simpatia, desempenho_evento, disp_horario, media, tabela_free, extra_por_equipamento, equipamentos, avatar_url, criado_em, atualizado_em
`
type CreateProfissionalParams struct {
@ -49,6 +49,7 @@ type CreateProfissionalParams struct {
TabelaFree pgtype.Text `json:"tabela_free"`
ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"`
Equipamentos pgtype.Text `json:"equipamentos"`
AvatarUrl pgtype.Text `json:"avatar_url"`
}
func (q *Queries) CreateProfissional(ctx context.Context, arg CreateProfissionalParams) (CadastroProfissionai, error) {
@ -77,6 +78,7 @@ func (q *Queries) CreateProfissional(ctx context.Context, arg CreateProfissional
arg.TabelaFree,
arg.ExtraPorEquipamento,
arg.Equipamentos,
arg.AvatarUrl,
)
var i CadastroProfissionai
err := row.Scan(
@ -105,6 +107,7 @@ func (q *Queries) CreateProfissional(ctx context.Context, arg CreateProfissional
&i.TabelaFree,
&i.ExtraPorEquipamento,
&i.Equipamentos,
&i.AvatarUrl,
&i.CriadoEm,
&i.AtualizadoEm,
)
@ -122,7 +125,7 @@ func (q *Queries) DeleteProfissional(ctx context.Context, id pgtype.UUID) error
}
const getProfissionalByID = `-- name: GetProfissionalByID :one
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.criado_em, p.atualizado_em, f.nome as funcao_nome
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome
FROM cadastro_profissionais p
LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
WHERE p.id = $1 LIMIT 1
@ -154,6 +157,7 @@ type GetProfissionalByIDRow struct {
TabelaFree pgtype.Text `json:"tabela_free"`
ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"`
Equipamentos pgtype.Text `json:"equipamentos"`
AvatarUrl pgtype.Text `json:"avatar_url"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
FuncaoNome pgtype.Text `json:"funcao_nome"`
@ -188,6 +192,7 @@ func (q *Queries) GetProfissionalByID(ctx context.Context, id pgtype.UUID) (GetP
&i.TabelaFree,
&i.ExtraPorEquipamento,
&i.Equipamentos,
&i.AvatarUrl,
&i.CriadoEm,
&i.AtualizadoEm,
&i.FuncaoNome,
@ -196,7 +201,7 @@ func (q *Queries) GetProfissionalByID(ctx context.Context, id pgtype.UUID) (GetP
}
const getProfissionalByUsuarioID = `-- name: GetProfissionalByUsuarioID :one
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.criado_em, p.atualizado_em, f.nome as funcao_nome
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome
FROM cadastro_profissionais p
LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
WHERE p.usuario_id = $1 LIMIT 1
@ -228,6 +233,7 @@ type GetProfissionalByUsuarioIDRow struct {
TabelaFree pgtype.Text `json:"tabela_free"`
ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"`
Equipamentos pgtype.Text `json:"equipamentos"`
AvatarUrl pgtype.Text `json:"avatar_url"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
FuncaoNome pgtype.Text `json:"funcao_nome"`
@ -262,6 +268,7 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty
&i.TabelaFree,
&i.ExtraPorEquipamento,
&i.Equipamentos,
&i.AvatarUrl,
&i.CriadoEm,
&i.AtualizadoEm,
&i.FuncaoNome,
@ -270,7 +277,7 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty
}
const listProfissionais = `-- name: ListProfissionais :many
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.criado_em, p.atualizado_em, f.nome as funcao_nome, u.email
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome, u.email
FROM cadastro_profissionais p
LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
LEFT JOIN usuarios u ON p.usuario_id = u.id
@ -303,6 +310,7 @@ type ListProfissionaisRow struct {
TabelaFree pgtype.Text `json:"tabela_free"`
ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"`
Equipamentos pgtype.Text `json:"equipamentos"`
AvatarUrl pgtype.Text `json:"avatar_url"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
FuncaoNome pgtype.Text `json:"funcao_nome"`
@ -344,6 +352,7 @@ func (q *Queries) ListProfissionais(ctx context.Context) ([]ListProfissionaisRow
&i.TabelaFree,
&i.ExtraPorEquipamento,
&i.Equipamentos,
&i.AvatarUrl,
&i.CriadoEm,
&i.AtualizadoEm,
&i.FuncaoNome,
@ -385,9 +394,10 @@ SET
tabela_free = $22,
extra_por_equipamento = $23,
equipamentos = $24,
avatar_url = $25,
atualizado_em = NOW()
WHERE id = $1
RETURNING id, usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp, cpf_cnpj_titular, banco, agencia, conta_pix, carro_disponivel, tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec, educacao_simpatia, desempenho_evento, disp_horario, media, tabela_free, extra_por_equipamento, equipamentos, criado_em, atualizado_em
RETURNING id, usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp, cpf_cnpj_titular, banco, agencia, conta_pix, carro_disponivel, tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec, educacao_simpatia, desempenho_evento, disp_horario, media, tabela_free, extra_por_equipamento, equipamentos, avatar_url, criado_em, atualizado_em
`
type UpdateProfissionalParams struct {
@ -415,6 +425,7 @@ type UpdateProfissionalParams struct {
TabelaFree pgtype.Text `json:"tabela_free"`
ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"`
Equipamentos pgtype.Text `json:"equipamentos"`
AvatarUrl pgtype.Text `json:"avatar_url"`
}
func (q *Queries) UpdateProfissional(ctx context.Context, arg UpdateProfissionalParams) (CadastroProfissionai, error) {
@ -443,6 +454,7 @@ func (q *Queries) UpdateProfissional(ctx context.Context, arg UpdateProfissional
arg.TabelaFree,
arg.ExtraPorEquipamento,
arg.Equipamentos,
arg.AvatarUrl,
)
var i CadastroProfissionai
err := row.Scan(
@ -471,6 +483,7 @@ func (q *Queries) UpdateProfissional(ctx context.Context, arg UpdateProfissional
&i.TabelaFree,
&i.ExtraPorEquipamento,
&i.Equipamentos,
&i.AvatarUrl,
&i.CriadoEm,
&i.AtualizadoEm,
)

View file

@ -45,25 +45,32 @@ func (q *Queries) CreateCadastroCliente(ctx context.Context, arg CreateCadastroC
}
const createUsuario = `-- name: CreateUsuario :one
INSERT INTO usuarios (email, senha_hash, role, ativo)
VALUES ($1, $2, $3, false)
RETURNING id, email, senha_hash, role, ativo, criado_em, atualizado_em
INSERT INTO usuarios (email, senha_hash, role, tipo_profissional, ativo)
VALUES ($1, $2, $3, $4, false)
RETURNING id, email, senha_hash, role, tipo_profissional, ativo, criado_em, atualizado_em
`
type CreateUsuarioParams struct {
Email string `json:"email"`
SenhaHash string `json:"senha_hash"`
Role string `json:"role"`
Email string `json:"email"`
SenhaHash string `json:"senha_hash"`
Role string `json:"role"`
TipoProfissional pgtype.Text `json:"tipo_profissional"`
}
func (q *Queries) CreateUsuario(ctx context.Context, arg CreateUsuarioParams) (Usuario, error) {
row := q.db.QueryRow(ctx, createUsuario, arg.Email, arg.SenhaHash, arg.Role)
row := q.db.QueryRow(ctx, createUsuario,
arg.Email,
arg.SenhaHash,
arg.Role,
arg.TipoProfissional,
)
var i Usuario
err := row.Scan(
&i.ID,
&i.Email,
&i.SenhaHash,
&i.Role,
&i.TipoProfissional,
&i.Ativo,
&i.CriadoEm,
&i.AtualizadoEm,
@ -82,7 +89,7 @@ func (q *Queries) DeleteUsuario(ctx context.Context, id pgtype.UUID) error {
}
const getUsuarioByEmail = `-- name: GetUsuarioByEmail :one
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
SELECT u.id, u.email, u.senha_hash, u.role, u.tipo_profissional, u.ativo, u.criado_em, u.atualizado_em,
COALESCE(cp.nome, cc.nome, '') as nome,
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
e.id as empresa_id,
@ -95,17 +102,18 @@ WHERE u.email = $1 LIMIT 1
`
type GetUsuarioByEmailRow struct {
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
SenhaHash string `json:"senha_hash"`
Role string `json:"role"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
Nome string `json:"nome"`
Whatsapp string `json:"whatsapp"`
EmpresaID pgtype.UUID `json:"empresa_id"`
EmpresaNome pgtype.Text `json:"empresa_nome"`
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
SenhaHash string `json:"senha_hash"`
Role string `json:"role"`
TipoProfissional pgtype.Text `json:"tipo_profissional"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
Nome string `json:"nome"`
Whatsapp string `json:"whatsapp"`
EmpresaID pgtype.UUID `json:"empresa_id"`
EmpresaNome pgtype.Text `json:"empresa_nome"`
}
func (q *Queries) GetUsuarioByEmail(ctx context.Context, email string) (GetUsuarioByEmailRow, error) {
@ -116,6 +124,7 @@ func (q *Queries) GetUsuarioByEmail(ctx context.Context, email string) (GetUsuar
&i.Email,
&i.SenhaHash,
&i.Role,
&i.TipoProfissional,
&i.Ativo,
&i.CriadoEm,
&i.AtualizadoEm,
@ -128,7 +137,7 @@ func (q *Queries) GetUsuarioByEmail(ctx context.Context, email string) (GetUsuar
}
const getUsuarioByID = `-- name: GetUsuarioByID :one
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
SELECT u.id, u.email, u.senha_hash, u.role, u.tipo_profissional, u.ativo, u.criado_em, u.atualizado_em,
COALESCE(cp.nome, cc.nome, '') as nome,
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
e.id as empresa_id,
@ -141,17 +150,18 @@ WHERE u.id = $1 LIMIT 1
`
type GetUsuarioByIDRow struct {
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
SenhaHash string `json:"senha_hash"`
Role string `json:"role"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
Nome string `json:"nome"`
Whatsapp string `json:"whatsapp"`
EmpresaID pgtype.UUID `json:"empresa_id"`
EmpresaNome pgtype.Text `json:"empresa_nome"`
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
SenhaHash string `json:"senha_hash"`
Role string `json:"role"`
TipoProfissional pgtype.Text `json:"tipo_profissional"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
Nome string `json:"nome"`
Whatsapp string `json:"whatsapp"`
EmpresaID pgtype.UUID `json:"empresa_id"`
EmpresaNome pgtype.Text `json:"empresa_nome"`
}
func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (GetUsuarioByIDRow, error) {
@ -162,6 +172,7 @@ func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (GetUsuari
&i.Email,
&i.SenhaHash,
&i.Role,
&i.TipoProfissional,
&i.Ativo,
&i.CriadoEm,
&i.AtualizadoEm,
@ -174,18 +185,19 @@ func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (GetUsuari
}
const listAllUsuarios = `-- name: ListAllUsuarios :many
SELECT id, email, role, ativo, criado_em, atualizado_em
SELECT id, email, role, tipo_profissional, ativo, criado_em, atualizado_em
FROM usuarios
ORDER BY criado_em DESC
`
type ListAllUsuariosRow struct {
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
TipoProfissional pgtype.Text `json:"tipo_profissional"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
}
func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, error) {
@ -201,6 +213,7 @@ func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, er
&i.ID,
&i.Email,
&i.Role,
&i.TipoProfissional,
&i.Ativo,
&i.CriadoEm,
&i.AtualizadoEm,
@ -216,7 +229,7 @@ func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, er
}
const listUsuariosPending = `-- name: ListUsuariosPending :many
SELECT u.id, u.email, u.role, u.ativo, u.criado_em,
SELECT u.id, u.email, u.role, u.tipo_profissional, u.ativo, u.criado_em,
COALESCE(cp.nome, cc.nome, '') as nome,
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
e.id as empresa_id,
@ -230,15 +243,16 @@ ORDER BY u.criado_em DESC
`
type ListUsuariosPendingRow struct {
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
Nome string `json:"nome"`
Whatsapp string `json:"whatsapp"`
EmpresaID pgtype.UUID `json:"empresa_id"`
EmpresaNome pgtype.Text `json:"empresa_nome"`
ID pgtype.UUID `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
TipoProfissional pgtype.Text `json:"tipo_profissional"`
Ativo bool `json:"ativo"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
Nome string `json:"nome"`
Whatsapp string `json:"whatsapp"`
EmpresaID pgtype.UUID `json:"empresa_id"`
EmpresaNome pgtype.Text `json:"empresa_nome"`
}
func (q *Queries) ListUsuariosPending(ctx context.Context) ([]ListUsuariosPendingRow, error) {
@ -254,6 +268,7 @@ func (q *Queries) ListUsuariosPending(ctx context.Context) ([]ListUsuariosPendin
&i.ID,
&i.Email,
&i.Role,
&i.TipoProfissional,
&i.Ativo,
&i.CriadoEm,
&i.Nome,
@ -275,7 +290,7 @@ const updateUsuarioAtivo = `-- name: UpdateUsuarioAtivo :one
UPDATE usuarios
SET ativo = $2, atualizado_em = NOW()
WHERE id = $1
RETURNING id, email, senha_hash, role, ativo, criado_em, atualizado_em
RETURNING id, email, senha_hash, role, tipo_profissional, ativo, criado_em, atualizado_em
`
type UpdateUsuarioAtivoParams struct {
@ -291,6 +306,7 @@ func (q *Queries) UpdateUsuarioAtivo(ctx context.Context, arg UpdateUsuarioAtivo
&i.Email,
&i.SenhaHash,
&i.Role,
&i.TipoProfissional,
&i.Ativo,
&i.CriadoEm,
&i.AtualizadoEm,
@ -302,7 +318,7 @@ const updateUsuarioRole = `-- name: UpdateUsuarioRole :one
UPDATE usuarios
SET role = $2, atualizado_em = NOW()
WHERE id = $1
RETURNING id, email, senha_hash, role, ativo, criado_em, atualizado_em
RETURNING id, email, senha_hash, role, tipo_profissional, ativo, criado_em, atualizado_em
`
type UpdateUsuarioRoleParams struct {
@ -318,6 +334,7 @@ func (q *Queries) UpdateUsuarioRole(ctx context.Context, arg UpdateUsuarioRolePa
&i.Email,
&i.SenhaHash,
&i.Role,
&i.TipoProfissional,
&i.Ativo,
&i.CriadoEm,
&i.AtualizadoEm,

View file

@ -4,10 +4,10 @@ INSERT INTO cadastro_profissionais (
cpf_cnpj_titular, banco, agencia, conta_pix, carro_disponivel,
tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec,
educacao_simpatia, desempenho_evento, disp_horario, media,
tabela_free, extra_por_equipamento, equipamentos
tabela_free, extra_por_equipamento, equipamentos, avatar_url
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
$16, $17, $18, $19, $20, $21, $22, $23, $24
$16, $17, $18, $19, $20, $21, $22, $23, $24, $25
) RETURNING *;
-- name: GetProfissionalByUsuarioID :one
@ -55,6 +55,7 @@ SET
tabela_free = $22,
extra_por_equipamento = $23,
equipamentos = $24,
avatar_url = $25,
atualizado_em = NOW()
WHERE id = $1
RETURNING *;

View file

@ -1,10 +1,10 @@
-- name: CreateUsuario :one
INSERT INTO usuarios (email, senha_hash, role, ativo)
VALUES ($1, $2, $3, false)
INSERT INTO usuarios (email, senha_hash, role, tipo_profissional, ativo)
VALUES ($1, $2, $3, $4, false)
RETURNING *;
-- name: GetUsuarioByEmail :one
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
SELECT u.id, u.email, u.senha_hash, u.role, u.tipo_profissional, u.ativo, u.criado_em, u.atualizado_em,
COALESCE(cp.nome, cc.nome, '') as nome,
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
e.id as empresa_id,
@ -16,7 +16,7 @@ LEFT JOIN empresas e ON cc.empresa_id = e.id
WHERE u.email = $1 LIMIT 1;
-- name: GetUsuarioByID :one
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
SELECT u.id, u.email, u.senha_hash, u.role, u.tipo_profissional, u.ativo, u.criado_em, u.atualizado_em,
COALESCE(cp.nome, cc.nome, '') as nome,
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
e.id as empresa_id,
@ -32,7 +32,7 @@ DELETE FROM usuarios
WHERE id = $1;
-- name: ListUsuariosPending :many
SELECT u.id, u.email, u.role, u.ativo, u.criado_em,
SELECT u.id, u.email, u.role, u.tipo_profissional, u.ativo, u.criado_em,
COALESCE(cp.nome, cc.nome, '') as nome,
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
e.id as empresa_id,
@ -56,7 +56,7 @@ WHERE id = $1
RETURNING *;
-- name: ListAllUsuarios :many
SELECT id, email, role, ativo, criado_em, atualizado_em
SELECT id, email, role, tipo_profissional, ativo, criado_em, atualizado_em
FROM usuarios
ORDER BY criado_em DESC;

View file

@ -5,6 +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',
tipo_profissional VARCHAR(50),
ativo BOOLEAN NOT NULL DEFAULT FALSE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
@ -51,6 +52,7 @@ CREATE TABLE IF NOT EXISTS cadastro_profissionais (
tabela_free VARCHAR(50),
extra_por_equipamento BOOLEAN DEFAULT FALSE,
equipamentos TEXT,
avatar_url VARCHAR(255),
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

View file

@ -42,6 +42,7 @@ type CreateProfissionalInput struct {
TabelaFree *string `json:"tabela_free"`
ExtraPorEquipamento *bool `json:"extra_por_equipamento"`
Equipamentos *string `json:"equipamentos"`
AvatarURL *string `json:"avatar_url"`
}
func (s *Service) Create(ctx context.Context, userID string, input CreateProfissionalInput) (*generated.CadastroProfissionai, error) {
@ -88,6 +89,7 @@ func (s *Service) Create(ctx context.Context, userID string, input CreateProfiss
TabelaFree: toPgText(input.TabelaFree),
ExtraPorEquipamento: toPgBool(input.ExtraPorEquipamento),
Equipamentos: toPgText(input.Equipamentos),
AvatarUrl: toPgText(input.AvatarURL),
}
prof, err := s.queries.CreateProfissional(ctx, params)
@ -137,6 +139,7 @@ type UpdateProfissionalInput struct {
TabelaFree *string `json:"tabela_free"`
ExtraPorEquipamento *bool `json:"extra_por_equipamento"`
Equipamentos *string `json:"equipamentos"`
AvatarURL *string `json:"avatar_url"`
}
func (s *Service) Update(ctx context.Context, id string, input UpdateProfissionalInput) (*generated.CadastroProfissionai, error) {
@ -175,6 +178,7 @@ func (s *Service) Update(ctx context.Context, id string, input UpdateProfissiona
TabelaFree: toPgText(input.TabelaFree),
ExtraPorEquipamento: toPgBool(input.ExtraPorEquipamento),
Equipamentos: toPgText(input.Equipamentos),
AvatarUrl: toPgText(input.AvatarURL),
}
prof, err := s.queries.UpdateProfissional(ctx, params)

View file

@ -0,0 +1,91 @@
package storage
import (
"context"
"fmt"
"log"
"photum-backend/internal/config"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
awsConfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type S3Service struct {
Client *s3.Client
PresignClient *s3.PresignClient
Bucket string
Region string
}
func NewS3Service(cfg *config.Config) *S3Service {
// Custom Resolver for Civo Object Store
customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{
URL: cfg.S3Endpoint,
SigningRegion: region,
}, nil
})
awsCfg, err := awsConfig.LoadDefaultConfig(context.TODO(),
awsConfig.WithRegion(cfg.S3Region),
awsConfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.S3AccessKey, cfg.S3SecretKey, "")),
awsConfig.WithEndpointResolverWithOptions(customResolver),
)
if err != nil {
log.Fatalf("unable to load SDK config, %v", err)
}
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.UsePathStyle = true
})
presignClient := s3.NewPresignClient(client)
return &S3Service{
Client: client,
PresignClient: presignClient,
Bucket: cfg.S3Bucket,
Region: cfg.S3Region,
}
}
// GeneratePresignedURL generates a PUT presigned URL for uploading a file
// returns (uploadUrl, publicUrl, error)
func (s *S3Service) GeneratePresignedURL(filename string, contentType string) (string, string, error) {
key := fmt.Sprintf("photum-dev/%d_%s", time.Now().Unix(), filename)
req, err := s.PresignClient.PresignPutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(s.Bucket),
Key: aws.String(key),
ContentType: aws.String(contentType),
}, s3.WithPresignExpires(15*time.Minute))
if err != nil {
return "", "", fmt.Errorf("failed to sign request: %v", err)
}
// Construct public URL - Path Style
// URL: https://objectstore.nyc1.civo.com/rede5/uploads/...
// We need to clean the endpoint string if it has https:// prefix for Sprintf if we construct manually,
// or just reuse the known endpoint structure.
// cfg.S3Endpoint is "https://objectstore.nyc1.civo.com"
// Assuming s.Client.Options().BaseEndpoint is not easily accessible here without plumbing,
// we will construct it based on the hardcoded knowledge of Civo or pass endpoint to struct.
// But simply: S3Endpoint + "/" + Bucket + "/" + key is the standard path style.
// Note: config.S3Endpoint includes "https://" based on .env
// We entered: S3_ENDPOINT=https://objectstore.nyc1.civo.com
// So: https://objectstore.nyc1.civo.com/rede5/key
// However, we don't have access to cfg here directly, but we rely on hardcoding for Civo in previous step or we should store Endpoint in struct.
// Better to store Endpoint in struct to be clean.
// For now, I'll use the domain directly as I did before, but path style.
publicURL := fmt.Sprintf("https://%s/%s/%s", "objectstore.nyc1.civo.com", s.Bucket, key)
return req.URL, publicURL, nil
}

View file

@ -188,6 +188,9 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
src={getAvatarSrc(user)}
alt="Avatar"
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name || "User")}&background=random&color=fff&size=128`;
}}
/>
</button>
@ -201,6 +204,9 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
src={getAvatarSrc(user)}
alt={user.name}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name || "User")}&background=random&color=fff&size=128`;
}}
/>
</div>
<h3 className="text-white font-bold text-lg mb-1">
@ -291,67 +297,67 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
</div>
) : (
!['entrar', 'cadastro', 'cadastro-profissional'].includes(currentPage) && (
<div className="relative">
<button
onClick={() =>
setIsAccountDropdownOpen(!isAccountDropdownOpen)
}
className="flex items-center gap-2 px-4 py-2 rounded-full hover:bg-gray-100 transition-colors shadow-md"
>
<div className="w-10 h-10 rounded-full border-2 border-brand-gold flex items-center justify-center text-brand-gold hover:bg-brand-gold hover:text-white transition-colors">
<User size={24} />
</div>
<div className="text-left hidden lg:block">
<p className="text-xs text-gray-500">Olá, bem-vindo(a)</p>
<p className="text-xs font-semibold text-gray-700">
Entrar/Cadastrar
</p>
</div>
</button>
{/* Dropdown Popup - Responsivo */}
{isAccountDropdownOpen && (
<div className="absolute right-0 lg:left-1/2 lg:-translate-x-1/2 top-full mt-3 w-72 bg-white rounded-2xl shadow-2xl border border-gray-100 overflow-hidden z-50 fade-in">
{/* Header com ícone */}
<div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-6 text-center">
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-2 border-white/30">
<User size={32} className="text-white" />
</div>
<p className="text-white/70 text-xs mb-1">
Olá, bem-vindo(a)
</p>
<p className="text-white font-semibold text-base">
<div className="relative">
<button
onClick={() =>
setIsAccountDropdownOpen(!isAccountDropdownOpen)
}
className="flex items-center gap-2 px-4 py-2 rounded-full hover:bg-gray-100 transition-colors shadow-md"
>
<div className="w-10 h-10 rounded-full border-2 border-brand-gold flex items-center justify-center text-brand-gold hover:bg-brand-gold hover:text-white transition-colors">
<User size={24} />
</div>
<div className="text-left hidden lg:block">
<p className="text-xs text-gray-500">Olá, bem-vindo(a)</p>
<p className="text-xs font-semibold text-gray-700">
Entrar/Cadastrar
</p>
</div>
</button>
{/* Botões */}
<div className="p-5 space-y-3 bg-gray-50">
<Button
onClick={() => {
onNavigate("entrar");
setIsAccountDropdownOpen(false);
}}
variant="secondary"
className="w-full rounded-xl shadow-sm hover:shadow-md transition-shadow"
>
ENTRAR
</Button>
{/* Dropdown Popup - Responsivo */}
{isAccountDropdownOpen && (
<div className="absolute right-0 lg:left-1/2 lg:-translate-x-1/2 top-full mt-3 w-72 bg-white rounded-2xl shadow-2xl border border-gray-100 overflow-hidden z-50 fade-in">
{/* Header com ícone */}
<div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-6 text-center">
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-2 border-white/30">
<User size={32} className="text-white" />
</div>
<p className="text-white/70 text-xs mb-1">
Olá, bem-vindo(a)
</p>
<p className="text-white font-semibold text-base">
Entrar/Cadastrar
</p>
</div>
<Button
onClick={() => {
onNavigate("cadastro");
setIsAccountDropdownOpen(false);
}}
variant="primary"
className="w-full rounded-xl shadow-sm hover:shadow-md transition-shadow"
>
Cadastre-se agora
</Button>
{/* Botões */}
<div className="p-5 space-y-3 bg-gray-50">
<Button
onClick={() => {
onNavigate("entrar");
setIsAccountDropdownOpen(false);
}}
variant="secondary"
className="w-full rounded-xl shadow-sm hover:shadow-md transition-shadow"
>
ENTRAR
</Button>
<Button
onClick={() => {
onNavigate("cadastro");
setIsAccountDropdownOpen(false);
}}
variant="primary"
className="w-full rounded-xl shadow-sm hover:shadow-md transition-shadow"
>
Cadastre-se agora
</Button>
</div>
</div>
</div>
)}
</div>
)}
</div>
)
)}
</div>
@ -371,6 +377,9 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
src={getAvatarSrc(user)}
alt="Avatar"
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name || "User")}&background=random&color=fff&size=128`;
}}
/>
</button>
@ -384,6 +393,9 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
src={getAvatarSrc(user)}
alt={user.name}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name || "User")}&background=random&color=fff&size=128`;
}}
/>
</div>
<h3 className="text-white font-bold text-lg mb-1">
@ -486,59 +498,59 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
</>
) : (
!['entrar', 'cadastro', 'cadastro-profissional'].includes(currentPage) && (
<div className="relative">
<button
onClick={() =>
setIsAccountDropdownOpen(!isAccountDropdownOpen)
}
className="w-10 h-10 rounded-full border-2 border-brand-gold flex items-center justify-center text-brand-gold shadow-md"
>
<User size={20} />
</button>
<div className="relative">
<button
onClick={() =>
setIsAccountDropdownOpen(!isAccountDropdownOpen)
}
className="w-10 h-10 rounded-full border-2 border-brand-gold flex items-center justify-center text-brand-gold shadow-md"
>
<User size={20} />
</button>
{/* Dropdown Popup Mobile */}
{isAccountDropdownOpen && (
<div className="absolute right-0 top-full mt-3 w-72 bg-white rounded-2xl shadow-2xl border border-gray-100 overflow-hidden z-50 fade-in">
{/* Header com ícone */}
<div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-6 text-center">
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-2 border-white/30">
<User size={32} className="text-white" />
{/* Dropdown Popup Mobile */}
{isAccountDropdownOpen && (
<div className="absolute right-0 top-full mt-3 w-72 bg-white rounded-2xl shadow-2xl border border-gray-100 overflow-hidden z-50 fade-in">
{/* Header com ícone */}
<div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-6 text-center">
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-2 border-white/30">
<User size={32} className="text-white" />
</div>
<p className="text-white/70 text-xs mb-1">
Olá, bem-vindo(a)
</p>
<p className="text-white font-semibold text-base">
Entrar/Cadastrar
</p>
</div>
<p className="text-white/70 text-xs mb-1">
Olá, bem-vindo(a)
</p>
<p className="text-white font-semibold text-base">
Entrar/Cadastrar
</p>
</div>
{/* Botões */}
<div className="p-5 space-y-3 bg-gray-50">
<Button
onClick={() => {
onNavigate("entrar");
setIsAccountDropdownOpen(false);
}}
variant="secondary"
className="w-full rounded-xl shadow-sm hover:shadow-md transition-shadow"
>
ENTRAR
</Button>
{/* Botões */}
<div className="p-5 space-y-3 bg-gray-50">
<Button
onClick={() => {
onNavigate("entrar");
setIsAccountDropdownOpen(false);
}}
variant="secondary"
className="w-full rounded-xl shadow-sm hover:shadow-md transition-shadow"
>
ENTRAR
</Button>
<Button
onClick={() => {
onNavigate("cadastro");
setIsAccountDropdownOpen(false);
}}
variant="primary"
className="w-full rounded-xl shadow-sm hover:shadow-md transition-shadow"
>
Cadastre-se agora
</Button>
<Button
onClick={() => {
onNavigate("cadastro");
setIsAccountDropdownOpen(false);
}}
variant="primary"
className="w-full rounded-xl shadow-sm hover:shadow-md transition-shadow"
>
Cadastre-se agora
</Button>
</div>
</div>
</div>
)}
</div>
)}
</div>
)
)}
</div>
@ -572,6 +584,9 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
src={getAvatarSrc(user)}
className="w-10 h-10 rounded-full mr-3 border-2 border-gray-200"
alt={user.name}
onError={(e) => {
e.currentTarget.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name || "User")}&background=random&color=fff&size=128`;
}}
/>
<div>
<span className="font-bold text-sm block text-gray-900">

View file

@ -34,6 +34,7 @@ export interface ProfessionalData {
tipoCartao: string;
equipamentos: string;
observacao: string;
funcaoLabel?: string;
}
export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
@ -152,7 +153,11 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
return;
}
onSubmit(formData);
const selectedFunction = functions.find(f => f.id === formData.funcaoId);
onSubmit({
...formData,
funcaoLabel: selectedFunction?.nome
});
};
const ufs = [

View file

@ -38,7 +38,7 @@ interface AuthContextType {
user: User | null;
login: (email: string, password?: string) => Promise<boolean>;
logout: () => void;
register: (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string }) => Promise<{ success: boolean; userId?: string; token?: string }>;
register: (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string; tipo_profissional?: string }) => Promise<{ success: boolean; userId?: string; token?: string }>;
availableUsers: User[]; // Helper for the login screen demo
token: string | null;
}
@ -66,11 +66,12 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const mappedUser: User = {
id: backendUser.id,
email: backendUser.email,
name: backendUser.email.split('@')[0],
name: data.profissional?.nome || data.empresa?.nome || backendUser.name || backendUser.nome || backendUser.email.split('@')[0],
role: backendUser.role as UserRole,
ativo: backendUser.ativo,
empresaId: backendUser.company_id || backendUser.empresa_id || backendUser.companyId,
companyName: backendUser.company_name || backendUser.empresa_nome || backendUser.companyName,
avatar: data.profissional?.avatar_url || data.empresa?.avatar_url || backendUser.avatar,
};
if (!backendUser.ativo) {
console.warn("User is not active, logging out.");
@ -146,12 +147,12 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const mappedUser: User = {
id: backendUser.id,
email: backendUser.email,
name: backendUser.email.split('@')[0], // Fallback name or from profile if available
name: data.profissional?.nome || data.empresa?.nome || backendUser.name || backendUser.nome || backendUser.email.split('@')[0],
role: backendUser.role as UserRole,
ativo: backendUser.ativo,
empresaId: backendUser.company_id || backendUser.empresa_id || backendUser.companyId,
companyName: backendUser.company_name || backendUser.empresa_nome || backendUser.companyName,
// ... propagate other fields if needed or fetch profile
avatar: data.profissional?.avatar_url || data.empresa?.avatar_url || backendUser.avatar,
};
setUser(mappedUser);
@ -195,7 +196,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
}
};
const register = async (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string }) => {
const register = async (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string; tipo_profissional?: string }) => {
try {
// Destructure to separate empresaId from the rest
const { empresaId, ...rest } = data;

View file

@ -24,6 +24,7 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
senha: professionalData.senha,
telefone: professionalData.whatsapp,
role: "PHOTOGRAPHER", // Role fixa para profissionais
tipo_profissional: professionalData.funcaoLabel || "", // Envia o nome da função (ex: Cinegrafista)
});
if (!authResult.success) {
@ -31,7 +32,32 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
}
// 2. Criar Perfil Profissional (autenticado)
const { createProfessional } = await import("../services/apiService");
const { createProfessional, getUploadURL, uploadFileToSignedUrl } = await import("../services/apiService");
let avatarUrl = "";
// Upload de Avatar (se existir)
if (professionalData.avatar) {
try {
console.log("Iniciando upload do avatar...");
const uploadRes = await getUploadURL(professionalData.avatar.name, professionalData.avatar.type);
if (uploadRes.error || !uploadRes.data) {
throw new Error(uploadRes.error || "Erro ao obter URL de upload");
}
await uploadFileToSignedUrl(uploadRes.data.upload_url, professionalData.avatar);
avatarUrl = uploadRes.data.public_url;
console.log("Upload concluído. URL:", avatarUrl);
} catch (err) {
console.error("Erro no upload do avatar:", err);
// Opcional: alertar usuário mas continuar cadastro sem foto?
// alert("Erro ao enviar foto. O cadastro prosseguirá sem foto.");
// Ou falhar tudo?
throw new Error("Falha ao enviar foto de perfil: " + (err instanceof Error ? err.message : "Erro desconhecido"));
}
}
// Mapear dados do formulário para o payload esperado pelo backend
// Mapear dados do formulário para o payload esperado pelo backend
// O curl fornecido pelo usuário mostra campos underscore (snake_case)
@ -59,8 +85,8 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
disp_horario: 0,
educacao_simpatia: 0,
qual_tec: 0,
media: 0,
tabela_free: "",
avatar_url: avatarUrl,
};
const profResult = await createProfessional(payload, authResult.token);

View file

@ -650,3 +650,54 @@ export async function updateEventStatus(token: string, eventId: string, status:
return { data: null, error: error instanceof Error ? error.message : "Erro desconhecido", isBackendDown: true };
}
}
/**
* Obtém URL pré-assinada para upload de arquivo
*/
export async function getUploadURL(filename: string, contentType: string): Promise<ApiResponse<{ upload_url: string; public_url: string }>> {
try {
const response = await fetch(`${API_BASE_URL}/auth/upload-url`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ filename, content_type: contentType }),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
data,
error: null,
isBackendDown: false,
};
} catch (error) {
console.error("Error fetching upload URL:", error);
return {
data: null,
error: error instanceof Error ? error.message : "Erro desconhecido",
isBackendDown: true,
};
}
}
/**
* Realiza o upload do arquivo para a URL pré-assinada
*/
export async function uploadFileToSignedUrl(uploadUrl: string, file: File): Promise<void> {
const response = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": file.type,
},
body: file,
});
if (!response.ok) {
throw new Error(`Failed to upload file to S3. Status: ${response.status}`);
}
}