diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..eb170ae --- /dev/null +++ b/backend/.gitignore @@ -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/ diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 0000000..d347cad --- /dev/null +++ b/backend/Makefile @@ -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 diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..d559011 --- /dev/null +++ b/backend/README.md @@ -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. + diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go new file mode 100644 index 0000000..f525da2 --- /dev/null +++ b/backend/cmd/api/main.go @@ -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) +} diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..e3ff7ab --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' + +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: diff --git a/backend/docs/docs.go b/backend/docs/docs.go new file mode 100644 index 0000000..d163eb4 --- /dev/null +++ b/backend/docs/docs.go @@ -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) +} diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json new file mode 100644 index 0000000..1d42bd1 --- /dev/null +++ b/backend/docs/swagger.json @@ -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" + } + } +} \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml new file mode 100644 index 0000000..ca0297b --- /dev/null +++ b/backend/docs/swagger.yaml @@ -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" diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..74d8849 --- /dev/null +++ b/backend/go.mod @@ -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 +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..24d39fe --- /dev/null +++ b/backend/go.sum @@ -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= diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go new file mode 100644 index 0000000..4731c6a --- /dev/null +++ b/backend/internal/auth/handler.go @@ -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"}) +} diff --git a/backend/internal/auth/middleware.go b/backend/internal/auth/middleware.go new file mode 100644 index 0000000..99dafb7 --- /dev/null +++ b/backend/internal/auth/middleware.go @@ -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() + } +} diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go new file mode 100644 index 0000000..874a508 --- /dev/null +++ b/backend/internal/auth/service.go @@ -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 +} diff --git a/backend/internal/auth/tokens.go b/backend/internal/auth/tokens.go new file mode 100644 index 0000000..1170957 --- /dev/null +++ b/backend/internal/auth/tokens.go @@ -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 +} diff --git a/backend/internal/auth/utils.go b/backend/internal/auth/utils.go new file mode 100644 index 0000000..34f3138 --- /dev/null +++ b/backend/internal/auth/utils.go @@ -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 +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..06a821c --- /dev/null +++ b/backend/internal/config/config.go @@ -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 +} diff --git a/backend/internal/db/db.go b/backend/internal/db/db.go new file mode 100644 index 0000000..4ba229b --- /dev/null +++ b/backend/internal/db/db.go @@ -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 +} diff --git a/backend/internal/db/generated/auth.sql.go b/backend/internal/db/generated/auth.sql.go new file mode 100644 index 0000000..d50ec62 --- /dev/null +++ b/backend/internal/db/generated/auth.sql.go @@ -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 +} diff --git a/backend/internal/db/generated/db.go b/backend/internal/db/generated/db.go new file mode 100644 index 0000000..a332b02 --- /dev/null +++ b/backend/internal/db/generated/db.go @@ -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, + } +} diff --git a/backend/internal/db/generated/models.go b/backend/internal/db/generated/models.go new file mode 100644 index 0000000..0a75691 --- /dev/null +++ b/backend/internal/db/generated/models.go @@ -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"` +} diff --git a/backend/internal/db/generated/profissionais.sql.go b/backend/internal/db/generated/profissionais.sql.go new file mode 100644 index 0000000..c1c6c60 --- /dev/null +++ b/backend/internal/db/generated/profissionais.sql.go @@ -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 +} diff --git a/backend/internal/db/generated/usuarios.sql.go b/backend/internal/db/generated/usuarios.sql.go new file mode 100644 index 0000000..396d6f3 --- /dev/null +++ b/backend/internal/db/generated/usuarios.sql.go @@ -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 +} diff --git a/backend/internal/db/queries/auth.sql b/backend/internal/db/queries/auth.sql new file mode 100644 index 0000000..228fecb --- /dev/null +++ b/backend/internal/db/queries/auth.sql @@ -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; diff --git a/backend/internal/db/queries/profissionais.sql b/backend/internal/db/queries/profissionais.sql new file mode 100644 index 0000000..f55e83e --- /dev/null +++ b/backend/internal/db/queries/profissionais.sql @@ -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; diff --git a/backend/internal/db/queries/usuarios.sql b/backend/internal/db/queries/usuarios.sql new file mode 100644 index 0000000..2476916 --- /dev/null +++ b/backend/internal/db/queries/usuarios.sql @@ -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; diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql new file mode 100644 index 0000000..7a2fd87 --- /dev/null +++ b/backend/internal/db/schema.sql @@ -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() +); diff --git a/backend/setup.ps1 b/backend/setup.ps1 new file mode 100644 index 0000000..1c4f4ab --- /dev/null +++ b/backend/setup.ps1 @@ -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 " -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 + } +} diff --git a/backend/sqlc.yaml b/backend/sqlc.yaml new file mode 100644 index 0000000..51525de --- /dev/null +++ b/backend/sqlc.yaml @@ -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