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:
parent
f625b6cd6c
commit
cd196a0275
24 changed files with 785 additions and 224 deletions
|
|
@ -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)
|
||||
|
|
|
|||
66
backend/cmd/tools/set_bucket_public.go
Normal file
66
backend/cmd/tools/set_bucket_public.go
Normal 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!")
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 *;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
91
backend/internal/storage/s3.go
Normal file
91
backend/internal/storage/s3.go
Normal 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
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue