Merge remote-tracking branch 'origin/dev'
This commit is contained in:
commit
31325d50eb
28 changed files with 2601 additions and 0 deletions
49
backend/.gitignore
vendored
Normal file
49
backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool
|
||||
*.out
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Dependency directories
|
||||
vendor/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Build output
|
||||
/bin/
|
||||
/dist/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Docker volumes
|
||||
/postgres_data/
|
||||
35
backend/Makefile
Normal file
35
backend/Makefile
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
.PHONY: help db-up db-down db-reset sqlc-generate run dev test
|
||||
|
||||
help: ## Mostra esta mensagem de ajuda
|
||||
@echo "Comandos disponíveis:"
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
db-up: ## Inicia o banco de dados PostgreSQL
|
||||
docker-compose up -d
|
||||
@echo "Aguardando banco de dados ficar pronto..."
|
||||
@timeout /t 5 /nobreak > nul
|
||||
@echo "Banco de dados pronto!"
|
||||
|
||||
db-down: ## Para o banco de dados
|
||||
docker-compose down
|
||||
|
||||
db-reset: ## Reseta o banco de dados (apaga todos os dados)
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
@echo "Aguardando banco de dados ficar pronto..."
|
||||
@timeout /t 5 /nobreak > nul
|
||||
@echo "Banco de dados resetado!"
|
||||
|
||||
sqlc-generate: ## Gera código Go a partir das queries SQL
|
||||
sqlc generate
|
||||
|
||||
run: ## Executa a aplicação
|
||||
go run cmd/api/main.go
|
||||
|
||||
dev: db-up sqlc-generate run ## Inicia ambiente de desenvolvimento completo
|
||||
|
||||
test: ## Executa os testes
|
||||
go test -v ./...
|
||||
|
||||
swagger: ## Gera documentação Swagger
|
||||
swag init -g cmd/api/main.go -o docs
|
||||
267
backend/README.md
Normal file
267
backend/README.md
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
# Photum Backend API
|
||||
|
||||
Backend para o sistema Photum, desenvolvido em Go com Gin Framework.
|
||||
|
||||
## 🚀 Tecnologias
|
||||
|
||||
- **Go 1.21+**
|
||||
- **Gin Framework** - Web framework
|
||||
- **PostgreSQL 15** - Banco de dados
|
||||
- **SQLC** - Geração de código type-safe para SQL
|
||||
- **JWT** - Autenticação via tokens
|
||||
- **Swagger** - Documentação da API
|
||||
- **Docker** - Containerização
|
||||
|
||||
## 📋 Pré-requisitos
|
||||
|
||||
- [Go 1.21+](https://golang.org/dl/)
|
||||
- [Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
||||
- [SQLC](https://sqlc.dev/) - `go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest`
|
||||
- [Swag](https://github.com/swaggo/swag) - `go install github.com/swaggo/swag/cmd/swag@latest`
|
||||
|
||||
## ⚙️ Setup Rápido
|
||||
|
||||
### Opção 1: Usando o script PowerShell (Recomendado para Windows)
|
||||
|
||||
```powershell
|
||||
# Iniciar ambiente completo (banco de dados + aplicação)
|
||||
.\setup.ps1 dev
|
||||
|
||||
# Ou comandos individuais:
|
||||
.\setup.ps1 db-up # Apenas iniciar banco de dados
|
||||
.\setup.ps1 sqlc-generate # Gerar código SQLC
|
||||
.\setup.ps1 swagger # Gerar documentação Swagger
|
||||
.\setup.ps1 run # Executar aplicação
|
||||
```
|
||||
|
||||
### Opção 2: Passo a passo manual
|
||||
|
||||
```powershell
|
||||
# 1. Iniciar o banco de dados PostgreSQL
|
||||
docker-compose up -d
|
||||
|
||||
# 2. Aguardar o banco ficar pronto (5-10 segundos)
|
||||
# O schema será aplicado automaticamente
|
||||
|
||||
# 3. Gerar código SQLC (se houver mudanças nas queries)
|
||||
sqlc generate
|
||||
|
||||
# 4. Gerar documentação Swagger
|
||||
swag init -g cmd/api/main.go -o docs
|
||||
|
||||
# 5. Executar a aplicação
|
||||
go run cmd/api/main.go
|
||||
```
|
||||
|
||||
## 🔧 Configuração
|
||||
|
||||
O arquivo `.env` contém as configurações do ambiente:
|
||||
|
||||
```env
|
||||
APP_ENV=dev
|
||||
APP_PORT=8080
|
||||
|
||||
DB_DSN=postgres://user:pass@localhost:5432/photum?sslmode=disable
|
||||
|
||||
JWT_ACCESS_SECRET=troque_essa_chave
|
||||
JWT_REFRESH_SECRET=troque_essa_tbm
|
||||
JWT_ACCESS_TTL_MINUTES=15
|
||||
JWT_REFRESH_TTL_DAYS=30
|
||||
```
|
||||
|
||||
⚠️ **IMPORTANTE**: Altere os secrets JWT antes de usar em produção!
|
||||
|
||||
## 📚 Documentação da API
|
||||
|
||||
Após iniciar a aplicação, acesse:
|
||||
|
||||
**Swagger UI**: http://localhost:8080/swagger/index.html
|
||||
|
||||
## 🔐 Endpoints Disponíveis
|
||||
|
||||
### Autenticação (Público)
|
||||
|
||||
- `POST /auth/register` - Registrar novo usuário
|
||||
- `POST /auth/login` - Login
|
||||
- `POST /auth/refresh` - Renovar access token
|
||||
- `POST /auth/logout` - Logout
|
||||
|
||||
### Protegido (Requer autenticação)
|
||||
|
||||
- `GET /api/me` - Informações do usuário autenticado
|
||||
|
||||
## 🗄️ Estrutura do Banco de Dados
|
||||
|
||||
### Tabela `usuarios`
|
||||
- `id` (UUID) - Primary Key
|
||||
- `email` (VARCHAR) - Único
|
||||
- `senha_hash` (VARCHAR) - Hash bcrypt da senha
|
||||
- `role` (VARCHAR) - Papel do usuário (default: 'profissional')
|
||||
- `ativo` (BOOLEAN) - Status do usuário
|
||||
- `criado_em`, `atualizado_em` (TIMESTAMPTZ)
|
||||
|
||||
### Tabela `refresh_tokens`
|
||||
- `id` (UUID) - Primary Key
|
||||
- `usuario_id` (UUID) - Foreign Key para usuarios
|
||||
- `token_hash` (VARCHAR) - Hash SHA256 do token
|
||||
- `user_agent`, `ip` - Informações do dispositivo
|
||||
- `expira_em` (TIMESTAMPTZ)
|
||||
- `revogado` (BOOLEAN)
|
||||
|
||||
### Tabela `cadastro_profissionais`
|
||||
- Informações detalhadas dos profissionais
|
||||
- Vinculada a `usuarios` via `usuario_id`
|
||||
|
||||
## 🧪 Testando a API
|
||||
|
||||
### Exemplo de Registro
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "andre.fr93@gmail.com",
|
||||
"senha": "j87q9t0"
|
||||
}'
|
||||
```
|
||||
|
||||
### Exemplo de Login
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "andre.fr93@gmail.com",
|
||||
"senha": "j87q9t0"
|
||||
}'
|
||||
```
|
||||
|
||||
### Testando rota protegida
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/api/me \
|
||||
-H "Authorization: Bearer SEU_ACCESS_TOKEN_AQUI"
|
||||
```
|
||||
|
||||
## 🛠️ Comandos Úteis
|
||||
|
||||
```powershell
|
||||
# Ver logs do banco de dados
|
||||
docker-compose logs -f postgres
|
||||
|
||||
# Parar banco de dados
|
||||
docker-compose down
|
||||
|
||||
# Resetar banco de dados (apaga todos os dados)
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
|
||||
# Executar testes
|
||||
go test -v ./...
|
||||
|
||||
# Atualizar dependências
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
## 📁 Estrutura do Projeto
|
||||
|
||||
```
|
||||
photum-backend/
|
||||
├── cmd/
|
||||
│ └── api/
|
||||
│ └── main.go # Entry point da aplicação
|
||||
├── internal/
|
||||
│ ├── auth/ # Módulo de autenticação
|
||||
│ │ ├── handler.go # Handlers HTTP
|
||||
│ │ ├── service.go # Lógica de negócio
|
||||
│ │ ├── tokens.go # Geração de tokens JWT
|
||||
│ │ ├── middleware.go # Middleware de autenticação
|
||||
│ │ └── utils.go # Utilitários (hash de senha)
|
||||
│ ├── config/ # Configurações
|
||||
│ │ └── config.go
|
||||
│ └── db/
|
||||
│ ├── schema.sql # Schema do banco de dados
|
||||
│ ├── queries/ # Queries SQL
|
||||
│ │ ├── usuarios.sql
|
||||
│ │ ├── auth.sql
|
||||
│ │ └── profissionais.sql
|
||||
│ └── generated/ # Código gerado pelo SQLC
|
||||
├── docs/ # Documentação Swagger (gerada)
|
||||
├── .env # Variáveis de ambiente
|
||||
├── docker-compose.yml # Configuração Docker
|
||||
├── sqlc.yaml # Configuração SQLC
|
||||
├── go.mod # Dependências Go
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Erro: "failed to register user"
|
||||
|
||||
**Causa**: Banco de dados não está rodando ou não foi inicializado.
|
||||
|
||||
**Solução**:
|
||||
```powershell
|
||||
# Verificar se Docker está rodando
|
||||
docker ps
|
||||
|
||||
# Se não estiver, iniciar o banco
|
||||
docker-compose up -d
|
||||
|
||||
# Aguardar alguns segundos e tentar novamente
|
||||
```
|
||||
|
||||
### Erro: "connection refused"
|
||||
|
||||
**Causa**: PostgreSQL não está acessível.
|
||||
|
||||
**Solução**:
|
||||
```powershell
|
||||
# Verificar logs do container
|
||||
docker-compose logs postgres
|
||||
|
||||
# Reiniciar o container
|
||||
docker-compose restart postgres
|
||||
```
|
||||
|
||||
### Erro: "relation usuarios does not exist"
|
||||
|
||||
**Causa**: Schema não foi aplicado ao banco.
|
||||
|
||||
**Solução**:
|
||||
```powershell
|
||||
# Resetar banco de dados
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 📝 Notas de Desenvolvimento
|
||||
|
||||
### Fluxo de Autenticação
|
||||
|
||||
1. **Registro**: Cria usuário com senha hasheada (bcrypt)
|
||||
2. **Login**:
|
||||
- Valida credenciais
|
||||
- Gera Access Token (JWT, 15 min)
|
||||
- Gera Refresh Token (UUID hasheado, 30 dias)
|
||||
- Armazena Refresh Token no banco
|
||||
- Retorna Access Token + define cookie HttpOnly com Refresh Token
|
||||
3. **Refresh**: Usa Refresh Token para gerar novo Access Token
|
||||
4. **Logout**: Revoga Refresh Token
|
||||
|
||||
### Segurança
|
||||
|
||||
- Senhas hasheadas com bcrypt (custo 10)
|
||||
- Refresh Tokens armazenados como SHA256 hash
|
||||
- Access Tokens JWT assinados com HS256
|
||||
- Cookies HttpOnly para web (CSRF protection)
|
||||
- Suporte a tokens no body para mobile
|
||||
|
||||
## 📄 Licença
|
||||
|
||||
Este projeto é privado e proprietário.
|
||||
|
||||
## 👥 Contato
|
||||
|
||||
Para dúvidas ou suporte, entre em contato com a equipe de desenvolvimento.
|
||||
|
||||
76
backend/cmd/api/main.go
Normal file
76
backend/cmd/api/main.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"photum-backend/internal/auth"
|
||||
"photum-backend/internal/config"
|
||||
"photum-backend/internal/db"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
|
||||
_ "photum-backend/docs" // Import generated docs
|
||||
)
|
||||
|
||||
// @title Photum Backend API
|
||||
// @version 1.0
|
||||
// @description Backend authentication service for Photum.
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url http://www.swagger.io/support
|
||||
// @contact.email support@swagger.io
|
||||
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
// @host localhost:8080
|
||||
// @BasePath /
|
||||
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
func main() {
|
||||
cfg := config.LoadConfig()
|
||||
log.Printf("Loaded DSN: %s", cfg.DBDsn)
|
||||
|
||||
queries, pool := db.Connect(cfg)
|
||||
defer pool.Close()
|
||||
|
||||
// Auth Service & Handler
|
||||
authService := auth.NewService(queries, cfg)
|
||||
authHandler := auth.NewHandler(authService, cfg)
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
// Public Routes
|
||||
authGroup := r.Group("/auth")
|
||||
{
|
||||
authGroup.POST("/register", authHandler.Register)
|
||||
authGroup.POST("/login", authHandler.Login)
|
||||
authGroup.POST("/refresh", authHandler.Refresh)
|
||||
authGroup.POST("/logout", authHandler.Logout)
|
||||
}
|
||||
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// Protected Routes
|
||||
api := r.Group("/api")
|
||||
api.Use(auth.AuthMiddleware(cfg))
|
||||
{
|
||||
api.GET("/me", func(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
role, _ := c.Get("role")
|
||||
c.JSON(200, gin.H{
|
||||
"user_id": userID,
|
||||
"role": role,
|
||||
"message": "You are authenticated",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
log.Printf("Server running on port %s", cfg.AppPort)
|
||||
r.Run(":" + cfg.AppPort)
|
||||
}
|
||||
23
backend/docker-compose.yml
Normal file
23
backend/docker-compose.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: photum-postgres
|
||||
environment:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: pass
|
||||
POSTGRES_DB: photum
|
||||
ports:
|
||||
- "55432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./internal/db/schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U user -d photum"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
271
backend/docs/docs.go
Normal file
271
backend/docs/docs.go
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||
package docs
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
||||
const docTemplate = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"termsOfService": "http://swagger.io/terms/",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "http://www.swagger.io/support",
|
||||
"email": "support@swagger.io"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"description": "Authenticate user and return access token and refresh token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Login user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Login Request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/auth.loginRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/logout": {
|
||||
"post": {
|
||||
"description": "Revoke refresh token and clear cookie",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Logout user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Refresh Token (optional if in cookie)",
|
||||
"name": "refresh_token",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/refresh": {
|
||||
"post": {
|
||||
"description": "Get a new access token using a valid refresh token (cookie or body)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Refresh access token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Refresh Token (optional if in cookie)",
|
||||
"name": "refresh_token",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/register": {
|
||||
"post": {
|
||||
"description": "Create a new user account with email and password",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Register a new user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Register Request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/auth.registerRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"auth.loginRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"senha"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"senha": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth.registerRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"senha"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"senha": {
|
||||
"type": "string",
|
||||
"minLength": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"BearerAuth": {
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "1.0",
|
||||
Host: "localhost:8080",
|
||||
BasePath: "/",
|
||||
Schemes: []string{},
|
||||
Title: "Photum Backend API",
|
||||
Description: "Backend authentication service for Photum.",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||
}
|
||||
247
backend/docs/swagger.json
Normal file
247
backend/docs/swagger.json
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "Backend authentication service for Photum.",
|
||||
"title": "Photum Backend API",
|
||||
"termsOfService": "http://swagger.io/terms/",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "http://www.swagger.io/support",
|
||||
"email": "support@swagger.io"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
},
|
||||
"version": "1.0"
|
||||
},
|
||||
"host": "localhost:8080",
|
||||
"basePath": "/",
|
||||
"paths": {
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"description": "Authenticate user and return access token and refresh token",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Login user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Login Request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/auth.loginRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/logout": {
|
||||
"post": {
|
||||
"description": "Revoke refresh token and clear cookie",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Logout user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Refresh Token (optional if in cookie)",
|
||||
"name": "refresh_token",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/refresh": {
|
||||
"post": {
|
||||
"description": "Get a new access token using a valid refresh token (cookie or body)",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Refresh access token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Refresh Token (optional if in cookie)",
|
||||
"name": "refresh_token",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/register": {
|
||||
"post": {
|
||||
"description": "Create a new user account with email and password",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"summary": "Register a new user",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Register Request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/auth.registerRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"auth.loginRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"senha"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"senha": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth.registerRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"senha"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"senha": {
|
||||
"type": "string",
|
||||
"minLength": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"BearerAuth": {
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}
|
||||
164
backend/docs/swagger.yaml
Normal file
164
backend/docs/swagger.yaml
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
basePath: /
|
||||
definitions:
|
||||
auth.loginRequest:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
senha:
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
- senha
|
||||
type: object
|
||||
auth.registerRequest:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
senha:
|
||||
minLength: 6
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
- senha
|
||||
type: object
|
||||
host: localhost:8080
|
||||
info:
|
||||
contact:
|
||||
email: support@swagger.io
|
||||
name: API Support
|
||||
url: http://www.swagger.io/support
|
||||
description: Backend authentication service for Photum.
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
termsOfService: http://swagger.io/terms/
|
||||
title: Photum Backend API
|
||||
version: "1.0"
|
||||
paths:
|
||||
/auth/login:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Authenticate user and return access token and refresh token
|
||||
parameters:
|
||||
- description: Login Request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/auth.loginRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Login user
|
||||
tags:
|
||||
- auth
|
||||
/auth/logout:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Revoke refresh token and clear cookie
|
||||
parameters:
|
||||
- description: Refresh Token (optional if in cookie)
|
||||
in: body
|
||||
name: refresh_token
|
||||
schema:
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Logout user
|
||||
tags:
|
||||
- auth
|
||||
/auth/refresh:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get a new access token using a valid refresh token (cookie or body)
|
||||
parameters:
|
||||
- description: Refresh Token (optional if in cookie)
|
||||
in: body
|
||||
name: refresh_token
|
||||
schema:
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Refresh access token
|
||||
tags:
|
||||
- auth
|
||||
/auth/register:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Create a new user account with email and password
|
||||
parameters:
|
||||
- description: Register Request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/auth.registerRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Register a new user
|
||||
tags:
|
||||
- auth
|
||||
securityDefinitions:
|
||||
BearerAuth:
|
||||
in: header
|
||||
name: Authorization
|
||||
type: apiKey
|
||||
swagger: "2.0"
|
||||
81
backend/go.mod
Normal file
81
backend/go.mod
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
module photum-backend
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/jackc/pgx/v5 v5.4.3
|
||||
github.com/joho/godotenv v1.5.1
|
||||
golang.org/x/crypto v0.45.0
|
||||
)
|
||||
|
||||
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/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.3 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.3 // indirect
|
||||
github.com/go-openapi/spec v0.22.1 // indirect
|
||||
github.com/go-openapi/swag v0.25.3 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.3 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.3 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.3 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.3 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.3 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.3 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.3 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/swaggo/files v1.0.1 // indirect
|
||||
github.com/swaggo/gin-swagger v1.6.1 // indirect
|
||||
github.com/swaggo/swag v1.16.6 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.7 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
261
backend/go.sum
Normal file
261
backend/go.sum
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28=
|
||||
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/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=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
|
||||
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
|
||||
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
|
||||
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
|
||||
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
|
||||
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
|
||||
github.com/go-openapi/swag v0.25.3 h1:FAa5wJXyDtI7yUztKDfZxDrSx+8WTg31MfCQ9s3PV+s=
|
||||
github.com/go-openapi/swag v0.25.3/go.mod h1:tX9vI8Mj8Ny+uCEk39I1QADvIPI7lkndX4qCsEqhkS8=
|
||||
github.com/go-openapi/swag/conv v0.25.3 h1:PcB18wwfba7MN5BVlBIV+VxvUUeC2kEuCEyJ2/t2X7E=
|
||||
github.com/go-openapi/swag/conv v0.25.3/go.mod h1:n4Ibfwhn8NJnPXNRhBO5Cqb9ez7alBR40JS4rbASUPU=
|
||||
github.com/go-openapi/swag/jsonname v0.25.3 h1:U20VKDS74HiPaLV7UZkztpyVOw3JNVsit+w+gTXRj0A=
|
||||
github.com/go-openapi/swag/jsonname v0.25.3/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.3 h1:kV7wer79KXUM4Ea4tBdAVTU842Rg6tWstX3QbM4fGdw=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.3/go.mod h1:ILcKqe4HC1VEZmJx51cVuZQ6MF8QvdfXsQfiaCs0z9o=
|
||||
github.com/go-openapi/swag/loading v0.25.3 h1:Nn65Zlzf4854MY6Ft0JdNrtnHh2bdcS/tXckpSnOb2Y=
|
||||
github.com/go-openapi/swag/loading v0.25.3/go.mod h1:xajJ5P4Ang+cwM5gKFrHBgkEDWfLcsAKepIuzTmOb/c=
|
||||
github.com/go-openapi/swag/stringutils v0.25.3 h1:nAmWq1fUTWl/XiaEPwALjp/8BPZJun70iDHRNq/sH6w=
|
||||
github.com/go-openapi/swag/stringutils v0.25.3/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
||||
github.com/go-openapi/swag/typeutils v0.25.3 h1:2w4mEEo7DQt3V4veWMZw0yTPQibiL3ri2fdDV4t2TQc=
|
||||
github.com/go-openapi/swag/typeutils v0.25.3/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.3 h1:LKTJjCn/W1ZfMec0XDL4Vxh8kyAnv1orH5F2OREDUrg=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.3/go.mod h1:Y7QN6Wc5DOBXK14/xeo1cQlq0EA0wvLoSv13gDQoCao=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
||||
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
|
||||
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
188
backend/internal/auth/handler.go
Normal file
188
backend/internal/auth/handler.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"photum-backend/internal/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewHandler(service *Service, cfg *config.Config) *Handler {
|
||||
return &Handler{service: service, cfg: cfg}
|
||||
}
|
||||
|
||||
type registerRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"senha" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
// Register godoc
|
||||
// @Summary Register a new user
|
||||
// @Description Create a new user account with email and password
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body registerRequest true "Register Request"
|
||||
// @Success 201 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /auth/register [post]
|
||||
func (h *Handler) Register(c *gin.Context) {
|
||||
log.Println("Register endpoint called")
|
||||
var req registerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
log.Printf("Bind error: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Attempting to register user: %s", req.Email)
|
||||
user, err := h.service.Register(c.Request.Context(), req.Email, req.Password)
|
||||
if err != nil {
|
||||
if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code == "23505" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "email já cadastrado"})
|
||||
return
|
||||
}
|
||||
log.Printf("Error registering user: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "falha ao registrar usuário"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("User registered: %s", user.Email)
|
||||
c.JSON(http.StatusCreated, gin.H{"id": user.ID, "email": user.Email})
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"senha" binding:"required"`
|
||||
}
|
||||
|
||||
// Login godoc
|
||||
// @Summary Login user
|
||||
// @Description Authenticate user and return access token and refresh token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body loginRequest true "Login Request"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/login [post]
|
||||
func (h *Handler) Login(c *gin.Context) {
|
||||
var req loginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userAgent := c.Request.UserAgent()
|
||||
ip := c.ClientIP()
|
||||
|
||||
accessToken, refreshToken, accessExp, user, err := h.service.Login(
|
||||
c.Request.Context(),
|
||||
req.Email,
|
||||
req.Password,
|
||||
userAgent,
|
||||
ip,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// Set Refresh Token in Cookie (HttpOnly)
|
||||
maxAge := h.cfg.JwtRefreshTTLDays * 24 * 60 * 60
|
||||
secure := h.cfg.AppEnv == "production"
|
||||
c.SetCookie("refresh_token", refreshToken, maxAge, "/", "", secure, true)
|
||||
|
||||
// Use %v for UUID (or .String())
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"access_token": accessToken,
|
||||
"expires_at": accessExp,
|
||||
"user": gin.H{
|
||||
"id": user.ID, // %v works fine; no formatting needed here
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Refresh godoc
|
||||
// @Summary Refresh access token
|
||||
// @Description Get a new access token using a valid refresh token (cookie or body)
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param refresh_token body string false "Refresh Token (optional if in cookie)"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/refresh [post]
|
||||
func (h *Handler) Refresh(c *gin.Context) {
|
||||
// Try to get from cookie first
|
||||
refreshToken, err := c.Cookie("refresh_token")
|
||||
if err != nil {
|
||||
// Try from body if mobile
|
||||
var req struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err == nil {
|
||||
refreshToken = req.RefreshToken
|
||||
}
|
||||
}
|
||||
|
||||
if refreshToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "refresh token required"})
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, accessExp, err := h.service.Refresh(c.Request.Context(), refreshToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"access_token": accessToken,
|
||||
"expires_at": accessExp,
|
||||
})
|
||||
}
|
||||
|
||||
// Logout godoc
|
||||
// @Summary Logout user
|
||||
// @Description Revoke refresh token and clear cookie
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param refresh_token body string false "Refresh Token (optional if in cookie)"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Router /auth/logout [post]
|
||||
func (h *Handler) Logout(c *gin.Context) {
|
||||
refreshToken, err := c.Cookie("refresh_token")
|
||||
if err != nil {
|
||||
// Try from body
|
||||
var req struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err == nil {
|
||||
refreshToken = req.RefreshToken
|
||||
}
|
||||
}
|
||||
|
||||
if refreshToken != "" {
|
||||
_ = h.service.Logout(c.Request.Context(), refreshToken)
|
||||
}
|
||||
|
||||
// Clear cookie
|
||||
secure := h.cfg.AppEnv == "production"
|
||||
c.SetCookie("refresh_token", "", -1, "/", "", secure, true)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
|
||||
}
|
||||
36
backend/internal/auth/middleware.go
Normal file
36
backend/internal/auth/middleware.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"photum-backend/internal/config"
|
||||
)
|
||||
|
||||
func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authorization header required"})
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header format"})
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := parts[1]
|
||||
claims, err := ValidateToken(tokenString, cfg.JwtAccessSecret)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("userID", claims.UserID)
|
||||
c.Set("role", claims.Role)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
128
backend/internal/auth/service.go
Normal file
128
backend/internal/auth/service.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"photum-backend/internal/config"
|
||||
"photum-backend/internal/db/generated"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
queries *generated.Queries
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewService(queries *generated.Queries, cfg *config.Config) *Service {
|
||||
return &Service{queries: queries, cfg: cfg}
|
||||
}
|
||||
|
||||
func (s *Service) Register(ctx context.Context, email, password string) (*generated.Usuario, error) {
|
||||
hash, err := HashPassword(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{
|
||||
Email: email,
|
||||
SenhaHash: hash,
|
||||
Role: "profissional",
|
||||
})
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (s *Service) Login(ctx context.Context, email, password, userAgent, ip string) (string, string, time.Time, *generated.Usuario, error) {
|
||||
user, err := s.queries.GetUsuarioByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return "", "", time.Time{}, nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
if !CheckPasswordHash(password, user.SenhaHash) {
|
||||
return "", "", time.Time{}, nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Convert pgtype.UUID to uuid.UUID
|
||||
userUUID := uuid.UUID(user.ID.Bytes)
|
||||
|
||||
accessToken, accessExp, err := GenerateAccessToken(userUUID, user.Role, s.cfg.JwtAccessSecret, s.cfg.JwtAccessTTLMinutes)
|
||||
if err != nil {
|
||||
return "", "", time.Time{}, nil, err
|
||||
}
|
||||
|
||||
refreshToken, _, err := s.createRefreshToken(ctx, user.ID, userAgent, ip)
|
||||
if err != nil {
|
||||
return "", "", time.Time{}, nil, err
|
||||
}
|
||||
|
||||
// Return access token, refresh token (raw), access expiration, user
|
||||
return accessToken, refreshToken, accessExp, &user, nil
|
||||
}
|
||||
|
||||
func (s *Service) Refresh(ctx context.Context, refreshTokenRaw string) (string, time.Time, error) {
|
||||
// Hash the raw token to find it in DB
|
||||
hash := sha256.Sum256([]byte(refreshTokenRaw))
|
||||
hashString := hex.EncodeToString(hash[:])
|
||||
|
||||
storedToken, err := s.queries.GetRefreshToken(ctx, hashString)
|
||||
if err != nil {
|
||||
return "", time.Time{}, errors.New("invalid refresh token")
|
||||
}
|
||||
|
||||
if storedToken.Revogado {
|
||||
return "", time.Time{}, errors.New("token revoked")
|
||||
}
|
||||
|
||||
if time.Now().After(storedToken.ExpiraEm.Time) {
|
||||
return "", time.Time{}, errors.New("token expired")
|
||||
}
|
||||
|
||||
// Get user to check if active and get role
|
||||
user, err := s.queries.GetUsuarioByID(ctx, storedToken.UsuarioID)
|
||||
if err != nil {
|
||||
return "", time.Time{}, errors.New("user not found")
|
||||
}
|
||||
|
||||
// Convert pgtype.UUID to uuid.UUID
|
||||
userUUID := uuid.UUID(user.ID.Bytes)
|
||||
|
||||
// Generate new access token
|
||||
return GenerateAccessToken(userUUID, user.Role, s.cfg.JwtAccessSecret, s.cfg.JwtAccessTTLMinutes)
|
||||
}
|
||||
|
||||
func (s *Service) Logout(ctx context.Context, refreshTokenRaw string) error {
|
||||
hash := sha256.Sum256([]byte(refreshTokenRaw))
|
||||
hashString := hex.EncodeToString(hash[:])
|
||||
return s.queries.RevokeRefreshToken(ctx, hashString)
|
||||
}
|
||||
|
||||
func (s *Service) createRefreshToken(ctx context.Context, userID pgtype.UUID, userAgent, ip string) (string, time.Time, error) {
|
||||
// Generate random token
|
||||
randomToken := uuid.New().String() // Simple UUID as refresh token
|
||||
|
||||
hash := sha256.Sum256([]byte(randomToken))
|
||||
hashString := hex.EncodeToString(hash[:])
|
||||
|
||||
expiraEm := time.Now().Add(time.Duration(s.cfg.JwtRefreshTTLDays) * 24 * time.Hour)
|
||||
|
||||
// pgtype.Timestamptz conversion
|
||||
pgExpiraEm := pgtype.Timestamptz{
|
||||
Time: expiraEm,
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
_, err := s.queries.CreateRefreshToken(ctx, generated.CreateRefreshTokenParams{
|
||||
UsuarioID: userID,
|
||||
TokenHash: hashString,
|
||||
UserAgent: pgtype.Text{String: userAgent, Valid: userAgent != ""},
|
||||
Ip: pgtype.Text{String: ip, Valid: ip != ""},
|
||||
ExpiraEm: pgExpiraEm,
|
||||
})
|
||||
|
||||
return randomToken, expiraEm, err
|
||||
}
|
||||
47
backend/internal/auth/tokens.go
Normal file
47
backend/internal/auth/tokens.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateAccessToken(userID uuid.UUID, role string, secret string, ttlMinutes int) (string, time.Time, error) {
|
||||
expirationTime := time.Now().Add(time.Duration(ttlMinutes) * time.Minute)
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(secret))
|
||||
return tokenString, expirationTime, err
|
||||
}
|
||||
|
||||
func ValidateToken(tokenString string, secret string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
13
backend/internal/auth/utils.go
Normal file
13
backend/internal/auth/utils.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package auth
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
51
backend/internal/config/config.go
Normal file
51
backend/internal/config/config.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AppEnv string
|
||||
AppPort string
|
||||
DBDsn string
|
||||
JwtAccessSecret string
|
||||
JwtRefreshSecret string
|
||||
JwtAccessTTLMinutes int
|
||||
JwtRefreshTTLDays int
|
||||
}
|
||||
|
||||
func LoadConfig() *Config {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Println("Warning: .env file not found")
|
||||
}
|
||||
|
||||
return &Config{
|
||||
AppEnv: getEnv("APP_ENV", "dev"),
|
||||
AppPort: getEnv("APP_PORT", "8080"),
|
||||
DBDsn: getEnv("DB_DSN", ""),
|
||||
JwtAccessSecret: getEnv("JWT_ACCESS_SECRET", "secret"),
|
||||
JwtRefreshSecret: getEnv("JWT_REFRESH_SECRET", "refresh_secret"),
|
||||
JwtAccessTTLMinutes: getEnvAsInt("JWT_ACCESS_TTL_MINUTES", 15),
|
||||
JwtRefreshTTLDays: getEnvAsInt("JWT_REFRESH_TTL_DAYS", 30),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getEnvAsInt(key string, fallback int) int {
|
||||
strValue := getEnv(key, "")
|
||||
if value, err := strconv.Atoi(strValue); err == nil {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
41
backend/internal/db/db.go
Normal file
41
backend/internal/db/db.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"photum-backend/internal/config"
|
||||
"photum-backend/internal/db/generated"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func Connect(cfg *config.Config) (*generated.Queries, *pgxpool.Pool) {
|
||||
log.Printf("Connecting to database with DSN: %#v", cfg.DBDsn)
|
||||
poolConfig, err := pgxpool.ParseConfig(cfg.DBDsn)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to parse DB DSN: %v", err)
|
||||
}
|
||||
|
||||
// Diagnostic log (password presence only)
|
||||
if poolConfig != nil && poolConfig.ConnConfig != nil {
|
||||
hasPassword := poolConfig.ConnConfig.Password != ""
|
||||
log.Printf("DB config user=%s host=%s port=%d password_set=%t",
|
||||
poolConfig.ConnConfig.User,
|
||||
poolConfig.ConnConfig.Host,
|
||||
poolConfig.ConnConfig.Port,
|
||||
hasPassword,
|
||||
)
|
||||
}
|
||||
|
||||
pool, err := pgxpool.NewWithConfig(context.Background(), poolConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to connect to database: %v", err)
|
||||
}
|
||||
|
||||
if err := pool.Ping(context.Background()); err != nil {
|
||||
log.Fatalf("Database ping failed: %v", err)
|
||||
}
|
||||
|
||||
return generated.New(pool), pool
|
||||
}
|
||||
93
backend/internal/db/generated/auth.sql.go
Normal file
93
backend/internal/db/generated/auth.sql.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: auth.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createRefreshToken = `-- name: CreateRefreshToken :one
|
||||
INSERT INTO refresh_tokens (
|
||||
usuario_id, token_hash, user_agent, ip, expira_em
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5
|
||||
) RETURNING id, usuario_id, token_hash, user_agent, ip, expira_em, revogado, criado_em
|
||||
`
|
||||
|
||||
type CreateRefreshTokenParams struct {
|
||||
UsuarioID pgtype.UUID `json:"usuario_id"`
|
||||
TokenHash string `json:"token_hash"`
|
||||
UserAgent pgtype.Text `json:"user_agent"`
|
||||
Ip pgtype.Text `json:"ip"`
|
||||
ExpiraEm pgtype.Timestamptz `json:"expira_em"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) {
|
||||
row := q.db.QueryRow(ctx, createRefreshToken,
|
||||
arg.UsuarioID,
|
||||
arg.TokenHash,
|
||||
arg.UserAgent,
|
||||
arg.Ip,
|
||||
arg.ExpiraEm,
|
||||
)
|
||||
var i RefreshToken
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UsuarioID,
|
||||
&i.TokenHash,
|
||||
&i.UserAgent,
|
||||
&i.Ip,
|
||||
&i.ExpiraEm,
|
||||
&i.Revogado,
|
||||
&i.CriadoEm,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getRefreshToken = `-- name: GetRefreshToken :one
|
||||
SELECT id, usuario_id, token_hash, user_agent, ip, expira_em, revogado, criado_em FROM refresh_tokens
|
||||
WHERE token_hash = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetRefreshToken(ctx context.Context, tokenHash string) (RefreshToken, error) {
|
||||
row := q.db.QueryRow(ctx, getRefreshToken, tokenHash)
|
||||
var i RefreshToken
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UsuarioID,
|
||||
&i.TokenHash,
|
||||
&i.UserAgent,
|
||||
&i.Ip,
|
||||
&i.ExpiraEm,
|
||||
&i.Revogado,
|
||||
&i.CriadoEm,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const revokeAllUserTokens = `-- name: RevokeAllUserTokens :exec
|
||||
UPDATE refresh_tokens
|
||||
SET revogado = TRUE
|
||||
WHERE usuario_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) RevokeAllUserTokens(ctx context.Context, usuarioID pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, revokeAllUserTokens, usuarioID)
|
||||
return err
|
||||
}
|
||||
|
||||
const revokeRefreshToken = `-- name: RevokeRefreshToken :exec
|
||||
UPDATE refresh_tokens
|
||||
SET revogado = TRUE
|
||||
WHERE token_hash = $1
|
||||
`
|
||||
|
||||
func (q *Queries) RevokeRefreshToken(ctx context.Context, tokenHash string) error {
|
||||
_, err := q.db.Exec(ctx, revokeRefreshToken, tokenHash)
|
||||
return err
|
||||
}
|
||||
32
backend/internal/db/generated/db.go
Normal file
32
backend/internal/db/generated/db.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
59
backend/internal/db/generated/models.go
Normal file
59
backend/internal/db/generated/models.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type CadastroProfissionai struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UsuarioID pgtype.UUID `json:"usuario_id"`
|
||||
Nome string `json:"nome"`
|
||||
FuncaoProfissional string `json:"funcao_profissional"`
|
||||
Endereco pgtype.Text `json:"endereco"`
|
||||
Cidade pgtype.Text `json:"cidade"`
|
||||
Uf pgtype.Text `json:"uf"`
|
||||
Whatsapp pgtype.Text `json:"whatsapp"`
|
||||
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
|
||||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
QtdEstudio pgtype.Int4 `json:"qtd_estudio"`
|
||||
TipoCartao pgtype.Text `json:"tipo_cartao"`
|
||||
Observacao pgtype.Text `json:"observacao"`
|
||||
QualTec pgtype.Int4 `json:"qual_tec"`
|
||||
EducacaoSimpatia pgtype.Int4 `json:"educacao_simpatia"`
|
||||
DesempenhoEvento pgtype.Int4 `json:"desempenho_evento"`
|
||||
DispHorario pgtype.Int4 `json:"disp_horario"`
|
||||
Media pgtype.Numeric `json:"media"`
|
||||
TabelaFree pgtype.Text `json:"tabela_free"`
|
||||
ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"`
|
||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||
}
|
||||
|
||||
type RefreshToken struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UsuarioID pgtype.UUID `json:"usuario_id"`
|
||||
TokenHash string `json:"token_hash"`
|
||||
UserAgent pgtype.Text `json:"user_agent"`
|
||||
Ip pgtype.Text `json:"ip"`
|
||||
ExpiraEm pgtype.Timestamptz `json:"expira_em"`
|
||||
Revogado bool `json:"revogado"`
|
||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
148
backend/internal/db/generated/profissionais.sql.go
Normal file
148
backend/internal/db/generated/profissionais.sql.go
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: profissionais.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createProfissional = `-- name: CreateProfissional :one
|
||||
INSERT INTO cadastro_profissionais (
|
||||
usuario_id, nome, funcao_profissional, 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
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
|
||||
$16, $17, $18, $19, $20, $21, $22, $23
|
||||
) RETURNING id, usuario_id, nome, funcao_profissional, 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, criado_em, atualizado_em
|
||||
`
|
||||
|
||||
type CreateProfissionalParams struct {
|
||||
UsuarioID pgtype.UUID `json:"usuario_id"`
|
||||
Nome string `json:"nome"`
|
||||
FuncaoProfissional string `json:"funcao_profissional"`
|
||||
Endereco pgtype.Text `json:"endereco"`
|
||||
Cidade pgtype.Text `json:"cidade"`
|
||||
Uf pgtype.Text `json:"uf"`
|
||||
Whatsapp pgtype.Text `json:"whatsapp"`
|
||||
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
|
||||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
QtdEstudio pgtype.Int4 `json:"qtd_estudio"`
|
||||
TipoCartao pgtype.Text `json:"tipo_cartao"`
|
||||
Observacao pgtype.Text `json:"observacao"`
|
||||
QualTec pgtype.Int4 `json:"qual_tec"`
|
||||
EducacaoSimpatia pgtype.Int4 `json:"educacao_simpatia"`
|
||||
DesempenhoEvento pgtype.Int4 `json:"desempenho_evento"`
|
||||
DispHorario pgtype.Int4 `json:"disp_horario"`
|
||||
Media pgtype.Numeric `json:"media"`
|
||||
TabelaFree pgtype.Text `json:"tabela_free"`
|
||||
ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateProfissional(ctx context.Context, arg CreateProfissionalParams) (CadastroProfissionai, error) {
|
||||
row := q.db.QueryRow(ctx, createProfissional,
|
||||
arg.UsuarioID,
|
||||
arg.Nome,
|
||||
arg.FuncaoProfissional,
|
||||
arg.Endereco,
|
||||
arg.Cidade,
|
||||
arg.Uf,
|
||||
arg.Whatsapp,
|
||||
arg.CpfCnpjTitular,
|
||||
arg.Banco,
|
||||
arg.Agencia,
|
||||
arg.ContaPix,
|
||||
arg.CarroDisponivel,
|
||||
arg.TemEstudio,
|
||||
arg.QtdEstudio,
|
||||
arg.TipoCartao,
|
||||
arg.Observacao,
|
||||
arg.QualTec,
|
||||
arg.EducacaoSimpatia,
|
||||
arg.DesempenhoEvento,
|
||||
arg.DispHorario,
|
||||
arg.Media,
|
||||
arg.TabelaFree,
|
||||
arg.ExtraPorEquipamento,
|
||||
)
|
||||
var i CadastroProfissionai
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UsuarioID,
|
||||
&i.Nome,
|
||||
&i.FuncaoProfissional,
|
||||
&i.Endereco,
|
||||
&i.Cidade,
|
||||
&i.Uf,
|
||||
&i.Whatsapp,
|
||||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
&i.QtdEstudio,
|
||||
&i.TipoCartao,
|
||||
&i.Observacao,
|
||||
&i.QualTec,
|
||||
&i.EducacaoSimpatia,
|
||||
&i.DesempenhoEvento,
|
||||
&i.DispHorario,
|
||||
&i.Media,
|
||||
&i.TabelaFree,
|
||||
&i.ExtraPorEquipamento,
|
||||
&i.CriadoEm,
|
||||
&i.AtualizadoEm,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getProfissionalByUsuarioID = `-- name: GetProfissionalByUsuarioID :one
|
||||
SELECT id, usuario_id, nome, funcao_profissional, 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, criado_em, atualizado_em FROM cadastro_profissionais
|
||||
WHERE usuario_id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgtype.UUID) (CadastroProfissionai, error) {
|
||||
row := q.db.QueryRow(ctx, getProfissionalByUsuarioID, usuarioID)
|
||||
var i CadastroProfissionai
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UsuarioID,
|
||||
&i.Nome,
|
||||
&i.FuncaoProfissional,
|
||||
&i.Endereco,
|
||||
&i.Cidade,
|
||||
&i.Uf,
|
||||
&i.Whatsapp,
|
||||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
&i.QtdEstudio,
|
||||
&i.TipoCartao,
|
||||
&i.Observacao,
|
||||
&i.QualTec,
|
||||
&i.EducacaoSimpatia,
|
||||
&i.DesempenhoEvento,
|
||||
&i.DispHorario,
|
||||
&i.Media,
|
||||
&i.TabelaFree,
|
||||
&i.ExtraPorEquipamento,
|
||||
&i.CriadoEm,
|
||||
&i.AtualizadoEm,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
79
backend/internal/db/generated/usuarios.sql.go
Normal file
79
backend/internal/db/generated/usuarios.sql.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: usuarios.sql
|
||||
|
||||
package generated
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createUsuario = `-- name: CreateUsuario :one
|
||||
INSERT INTO usuarios (email, senha_hash, role)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, email, senha_hash, role, ativo, criado_em, atualizado_em
|
||||
`
|
||||
|
||||
type CreateUsuarioParams struct {
|
||||
Email string `json:"email"`
|
||||
SenhaHash string `json:"senha_hash"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUsuario(ctx context.Context, arg CreateUsuarioParams) (Usuario, error) {
|
||||
row := q.db.QueryRow(ctx, createUsuario, arg.Email, arg.SenhaHash, arg.Role)
|
||||
var i Usuario
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.SenhaHash,
|
||||
&i.Role,
|
||||
&i.Ativo,
|
||||
&i.CriadoEm,
|
||||
&i.AtualizadoEm,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUsuarioByEmail = `-- name: GetUsuarioByEmail :one
|
||||
SELECT id, email, senha_hash, role, ativo, criado_em, atualizado_em FROM usuarios
|
||||
WHERE email = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUsuarioByEmail(ctx context.Context, email string) (Usuario, error) {
|
||||
row := q.db.QueryRow(ctx, getUsuarioByEmail, email)
|
||||
var i Usuario
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.SenhaHash,
|
||||
&i.Role,
|
||||
&i.Ativo,
|
||||
&i.CriadoEm,
|
||||
&i.AtualizadoEm,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUsuarioByID = `-- name: GetUsuarioByID :one
|
||||
SELECT id, email, senha_hash, role, ativo, criado_em, atualizado_em FROM usuarios
|
||||
WHERE id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (Usuario, error) {
|
||||
row := q.db.QueryRow(ctx, getUsuarioByID, id)
|
||||
var i Usuario
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.SenhaHash,
|
||||
&i.Role,
|
||||
&i.Ativo,
|
||||
&i.CriadoEm,
|
||||
&i.AtualizadoEm,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
20
backend/internal/db/queries/auth.sql
Normal file
20
backend/internal/db/queries/auth.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-- name: CreateRefreshToken :one
|
||||
INSERT INTO refresh_tokens (
|
||||
usuario_id, token_hash, user_agent, ip, expira_em
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetRefreshToken :one
|
||||
SELECT * FROM refresh_tokens
|
||||
WHERE token_hash = $1 LIMIT 1;
|
||||
|
||||
-- name: RevokeRefreshToken :exec
|
||||
UPDATE refresh_tokens
|
||||
SET revogado = TRUE
|
||||
WHERE token_hash = $1;
|
||||
|
||||
-- name: RevokeAllUserTokens :exec
|
||||
UPDATE refresh_tokens
|
||||
SET revogado = TRUE
|
||||
WHERE usuario_id = $1;
|
||||
15
backend/internal/db/queries/profissionais.sql
Normal file
15
backend/internal/db/queries/profissionais.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
-- name: CreateProfissional :one
|
||||
INSERT INTO cadastro_profissionais (
|
||||
usuario_id, nome, funcao_profissional, 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
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
|
||||
$16, $17, $18, $19, $20, $21, $22, $23
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetProfissionalByUsuarioID :one
|
||||
SELECT * FROM cadastro_profissionais
|
||||
WHERE usuario_id = $1 LIMIT 1;
|
||||
12
backend/internal/db/queries/usuarios.sql
Normal file
12
backend/internal/db/queries/usuarios.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-- name: CreateUsuario :one
|
||||
INSERT INTO usuarios (email, senha_hash, role)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetUsuarioByEmail :one
|
||||
SELECT * FROM usuarios
|
||||
WHERE email = $1 LIMIT 1;
|
||||
|
||||
-- name: GetUsuarioByID :one
|
||||
SELECT * FROM usuarios
|
||||
WHERE id = $1 LIMIT 1;
|
||||
51
backend/internal/db/schema.sql
Normal file
51
backend/internal/db/schema.sql
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
CREATE TABLE usuarios (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
senha_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'profissional',
|
||||
ativo BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE cadastro_profissionais (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
usuario_id UUID REFERENCES usuarios(id) ON DELETE SET NULL,
|
||||
nome VARCHAR(255) NOT NULL,
|
||||
funcao_profissional VARCHAR(50) NOT NULL,
|
||||
endereco VARCHAR(255),
|
||||
cidade VARCHAR(100),
|
||||
uf CHAR(2),
|
||||
whatsapp VARCHAR(20),
|
||||
cpf_cnpj_titular VARCHAR(20),
|
||||
banco VARCHAR(100),
|
||||
agencia VARCHAR(20),
|
||||
conta_pix VARCHAR(120),
|
||||
carro_disponivel BOOLEAN DEFAULT FALSE,
|
||||
tem_estudio BOOLEAN DEFAULT FALSE,
|
||||
qtd_estudio INT,
|
||||
tipo_cartao VARCHAR(50),
|
||||
observacao TEXT,
|
||||
qual_tec INT,
|
||||
educacao_simpatia INT,
|
||||
desempenho_evento INT,
|
||||
disp_horario INT,
|
||||
media NUMERIC(3,2),
|
||||
tabela_free VARCHAR(50),
|
||||
extra_por_equipamento BOOLEAN DEFAULT FALSE,
|
||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
usuario_id UUID NOT NULL REFERENCES usuarios(id) ON DELETE CASCADE,
|
||||
token_hash VARCHAR(255) NOT NULL,
|
||||
user_agent VARCHAR(255),
|
||||
ip VARCHAR(45),
|
||||
expira_em TIMESTAMPTZ NOT NULL,
|
||||
revogado BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
100
backend/setup.ps1
Normal file
100
backend/setup.ps1
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Script de setup para Photum Backend
|
||||
|
||||
param(
|
||||
[Parameter(Position=0)]
|
||||
[string]$Command = "help"
|
||||
)
|
||||
|
||||
function Show-Help {
|
||||
Write-Host "=== Photum Backend - Comandos Disponíveis ===" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host " db-up " -ForegroundColor Green -NoNewline
|
||||
Write-Host "Inicia o banco de dados PostgreSQL"
|
||||
Write-Host " db-down " -ForegroundColor Green -NoNewline
|
||||
Write-Host "Para o banco de dados"
|
||||
Write-Host " db-reset " -ForegroundColor Green -NoNewline
|
||||
Write-Host "Reseta o banco de dados (apaga todos os dados)"
|
||||
Write-Host " sqlc-generate " -ForegroundColor Green -NoNewline
|
||||
Write-Host "Gera código Go a partir das queries SQL"
|
||||
Write-Host " swagger " -ForegroundColor Green -NoNewline
|
||||
Write-Host "Gera documentação Swagger"
|
||||
Write-Host " run " -ForegroundColor Green -NoNewline
|
||||
Write-Host "Executa a aplicação"
|
||||
Write-Host " dev " -ForegroundColor Green -NoNewline
|
||||
Write-Host "Inicia ambiente de desenvolvimento completo"
|
||||
Write-Host " test " -ForegroundColor Green -NoNewline
|
||||
Write-Host "Executa os testes"
|
||||
Write-Host ""
|
||||
Write-Host "Uso: .\setup.ps1 <comando>" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Start-Database {
|
||||
Write-Host "Iniciando banco de dados PostgreSQL..." -ForegroundColor Cyan
|
||||
docker-compose up -d
|
||||
Write-Host "Aguardando banco de dados ficar pronto..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 5
|
||||
Write-Host "Banco de dados pronto!" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Stop-Database {
|
||||
Write-Host "Parando banco de dados..." -ForegroundColor Cyan
|
||||
docker-compose down
|
||||
Write-Host "Banco de dados parado!" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Reset-Database {
|
||||
Write-Host "Resetando banco de dados..." -ForegroundColor Cyan
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
Write-Host "Aguardando banco de dados ficar pronto..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 5
|
||||
Write-Host "Banco de dados resetado!" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Generate-SQLC {
|
||||
Write-Host "Gerando código Go a partir das queries SQL..." -ForegroundColor Cyan
|
||||
sqlc generate
|
||||
Write-Host "Código gerado com sucesso!" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Generate-Swagger {
|
||||
Write-Host "Gerando documentação Swagger..." -ForegroundColor Cyan
|
||||
swag init -g cmd/api/main.go -o docs
|
||||
Write-Host "Documentação Swagger gerada!" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Start-Application {
|
||||
Write-Host "Iniciando aplicação..." -ForegroundColor Cyan
|
||||
go run cmd/api/main.go
|
||||
}
|
||||
|
||||
function Start-Dev {
|
||||
Write-Host "=== Iniciando ambiente de desenvolvimento ===" -ForegroundColor Cyan
|
||||
Start-Database
|
||||
Generate-SQLC
|
||||
Generate-Swagger
|
||||
Start-Application
|
||||
}
|
||||
|
||||
function Run-Tests {
|
||||
Write-Host "Executando testes..." -ForegroundColor Cyan
|
||||
go test -v ./...
|
||||
}
|
||||
|
||||
switch ($Command.ToLower()) {
|
||||
"help" { Show-Help }
|
||||
"db-up" { Start-Database }
|
||||
"db-down" { Stop-Database }
|
||||
"db-reset" { Reset-Database }
|
||||
"sqlc-generate" { Generate-SQLC }
|
||||
"swagger" { Generate-Swagger }
|
||||
"run" { Start-Application }
|
||||
"dev" { Start-Dev }
|
||||
"test" { Run-Tests }
|
||||
default {
|
||||
Write-Host "Comando desconhecido: $Command" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Show-Help
|
||||
}
|
||||
}
|
||||
14
backend/sqlc.yaml
Normal file
14
backend/sqlc.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
version: "2"
|
||||
sql:
|
||||
- schema: "internal/db/schema.sql"
|
||||
queries: "internal/db/queries"
|
||||
engine: "postgresql"
|
||||
gen:
|
||||
go:
|
||||
package: "generated"
|
||||
out: "internal/db/generated"
|
||||
sql_package: "pgx/v5"
|
||||
emit_json_tags: true
|
||||
emit_prepared_queries: false
|
||||
emit_interface: false
|
||||
emit_exact_table_names: false
|
||||
Loading…
Reference in a new issue