├── .env ├── api └── viacep │ └── viacep.go ├── cmd └── webserver │ └── main.go ├── config ├── env │ └── env.go └── logger │ └── logger.go ├── docker-compose.yml ├── docs ├── custom │ ├── custom_css.go │ └── custom_layout.go ├── docs.go ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── http_client.http ├── internal ├── common │ └── utils │ │ └── decode_jwt.go ├── database │ ├── connection.go │ ├── migrations │ │ ├── 000001_init.down.sql │ │ ├── 000001_init.up.sql │ │ ├── 000002_init.down.sql │ │ ├── 000002_init.up.sql │ │ ├── 000003_init.down.sql │ │ └── 000003_init.up.sql │ ├── queries │ │ ├── categories.sql │ │ ├── products.sql │ │ └── users.sql │ └── sqlc │ │ ├── categories.sql.go │ │ ├── db.go │ │ ├── models.go │ │ ├── products.sql.go │ │ └── users.sql.go ├── dto │ ├── category_dto.go │ ├── product_dto.go │ └── user_dto.go ├── entity │ ├── category_entity.go │ ├── product_entity.go │ └── user_entity.go ├── handler │ ├── auth_handler.go │ ├── category_handler.go │ ├── httperr │ │ └── httperr.go │ ├── interface_handler.go │ ├── middleware │ │ └── logger_middleware.go │ ├── product_handler.go │ ├── response │ │ ├── category_response.go │ │ ├── product_response.go │ │ └── user_response.go │ ├── routes │ │ ├── docs_route.go │ │ └── routes.go │ ├── user_handler.go │ └── validation │ │ └── http_validation.go ├── repository │ ├── categoryrepository │ │ ├── category_interface_repository.go │ │ └── category_repository.go │ ├── productrepository │ │ ├── product_interface_repository.go │ │ └── product_repository.go │ ├── transaction │ │ └── run_transaction.go │ └── userrepository │ │ ├── user_interface_repository.go │ │ └── user_repository.go └── service │ ├── categoryservice │ ├── category_interface_service.go │ └── category_service.go │ ├── productservice │ ├── product_interface_service.go │ └── product_service.go │ └── userservice │ ├── auth_service.go │ ├── user_interface_service.go │ └── user_service.go ├── makefile ├── readme.md └── sqlc.yaml /.env: -------------------------------------------------------------------------------- 1 | GO_ENV="development" 2 | GO_PORT="8080" 3 | 4 | POSTGRES_DB="golang_api_users" 5 | POSTGRES_USER="golang_api_users" 6 | POSTGRES_PASSWORD="golang_api_users" 7 | POSTGRES_HOST="localhost" 8 | POSTGRES_PORT="5432" 9 | DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?sslmode=disable" 10 | 11 | VIA_CEP_URL="https://viacep.com.br/ws" 12 | 13 | ## JWT 14 | JWT_SECRET=secret 15 | JWT_EXPIRES_IN=19999 16 | -------------------------------------------------------------------------------- /api/viacep/viacep.go: -------------------------------------------------------------------------------- 1 | package viacep 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/wiliamvj/api-users-golang/config/env" 9 | ) 10 | 11 | type ViaCepResponse struct { 12 | CEP string `json:"cep"` 13 | Logradouro string `json:"logradouro"` 14 | Complemento string `json:"complemento"` 15 | Bairro string `json:"bairro"` 16 | Localidade string `json:"localidade"` 17 | UF string `json:"uf"` 18 | IBGE string `json:"ibge"` 19 | GIA string `json:"gia"` 20 | DDD string `json:"ddd"` 21 | SIAFI string `json:"siafi"` 22 | } 23 | 24 | func GetCep(cep string) (*ViaCepResponse, error) { 25 | url := fmt.Sprintf("%s/%s/json", env.Env.ViaCepURL, cep) 26 | var viaCepResponse ViaCepResponse 27 | 28 | resp, err := http.Get(url) 29 | if err != nil { 30 | return nil, err 31 | } 32 | defer resp.Body.Close() 33 | 34 | err = json.NewDecoder(resp.Body).Decode(&viaCepResponse) 35 | if err != nil { 36 | return nil, err 37 | } 38 | if viaCepResponse.CEP == "" { 39 | return nil, fmt.Errorf("cep not found") 40 | } 41 | return &viaCepResponse, nil 42 | } 43 | -------------------------------------------------------------------------------- /cmd/webserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net/http" 7 | 8 | "github.com/go-chi/chi" 9 | "github.com/wiliamvj/api-users-golang/config/env" 10 | "github.com/wiliamvj/api-users-golang/config/logger" 11 | _ "github.com/wiliamvj/api-users-golang/docs" 12 | "github.com/wiliamvj/api-users-golang/internal/database" 13 | "github.com/wiliamvj/api-users-golang/internal/database/sqlc" 14 | "github.com/wiliamvj/api-users-golang/internal/handler" 15 | "github.com/wiliamvj/api-users-golang/internal/handler/routes" 16 | "github.com/wiliamvj/api-users-golang/internal/repository/categoryrepository" 17 | "github.com/wiliamvj/api-users-golang/internal/repository/productrepository" 18 | "github.com/wiliamvj/api-users-golang/internal/repository/userrepository" 19 | "github.com/wiliamvj/api-users-golang/internal/service/categoryservice" 20 | "github.com/wiliamvj/api-users-golang/internal/service/productservice" 21 | "github.com/wiliamvj/api-users-golang/internal/service/userservice" 22 | ) 23 | 24 | func main() { 25 | logger.InitLogger() 26 | slog.Info("starting api") 27 | 28 | _, err := env.LoadingConfig(".") 29 | if err != nil { 30 | slog.Error("failed to load environment variables", err, slog.String("package", "main")) 31 | return 32 | } 33 | dbConnection, err := database.NewDBConnection() 34 | if err != nil { 35 | slog.Error("error to connect to database", "err", err, slog.String("package", "main")) 36 | return 37 | } 38 | 39 | queries := sqlc.New(dbConnection) 40 | 41 | // user 42 | userRepo := userrepository.NewUserRepository(dbConnection, queries) 43 | newUserService := userservice.NewUserService(userRepo) 44 | 45 | // category 46 | categoryRepo := categoryrepository.NewCategoryRepository(dbConnection, queries) 47 | newCategoryService := categoryservice.NewCategoryService(categoryRepo) 48 | 49 | // product 50 | productRepo := productrepository.NewProductRepository(dbConnection, queries) 51 | productsService := productservice.NewProductService(productRepo) 52 | 53 | newHandler := handler.NewHandler(newUserService, newCategoryService, productsService) 54 | 55 | // init routes 56 | router := chi.NewRouter() 57 | routes.InitRoutes(router, newHandler) 58 | routes.InitDocsRoutes(router) 59 | 60 | port := fmt.Sprintf(":%s", env.Env.GoPort) 61 | slog.Info(fmt.Sprintf("server running on port %s", port)) 62 | err = http.ListenAndServe(port, router) 63 | if err != nil { 64 | slog.Error("error to start server", err, slog.String("package", "main")) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /config/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "github.com/go-chi/jwtauth" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | var Env *config 9 | 10 | type config struct { 11 | GoEnv string `mapstructure:"GO_ENV"` 12 | GoPort string `mapstructure:"GO_PORT"` 13 | DatabaseURL string `mapstructure:"DATABASE_URL"` 14 | ViaCepURL string `mapstructure:"VIA_CEP_URL"` 15 | JwtSecret string `mapstructure:"JWT_SECRET"` 16 | JwtExpiresIn int `mapstructure:"JWT_EXPIRES_IN"` 17 | TokenAuth *jwtauth.JWTAuth 18 | } 19 | 20 | func LoadingConfig(path string) (*config, error) { 21 | viper.SetConfigFile("app_config") 22 | viper.SetConfigType("env") 23 | viper.AddConfigPath(path) 24 | viper.SetConfigFile(".env") 25 | viper.AutomaticEnv() 26 | 27 | err := viper.ReadInConfig() 28 | if err != nil { 29 | return nil, err 30 | } 31 | err = viper.Unmarshal(&Env) 32 | if err != nil { 33 | return nil, err 34 | } 35 | Env.TokenAuth = jwtauth.New("HS256", []byte(Env.JwtSecret), nil) 36 | return Env, nil 37 | } 38 | -------------------------------------------------------------------------------- /config/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | ) 7 | 8 | func InitLogger() { 9 | logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 10 | slog.SetDefault(logger) 11 | } 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | postgres: 5 | container_name: postgres_apigo 6 | image: postgres:14.5 7 | environment: 8 | POSTGRES_HOST: ${POSTGRES_HOST} 9 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 10 | POSTGRES_USER: ${POSTGRES_USER} 11 | POSTGRES_DB: ${POSTGRES_DB} 12 | PG_DATA: /var/lib/postgresql/data 13 | ports: 14 | - 5432:5432 15 | volumes: 16 | - apigo:/var/lib/postgresql/data 17 | volumes: 18 | apigo: 19 | -------------------------------------------------------------------------------- /docs/custom/custom_layout.go: -------------------------------------------------------------------------------- 1 | package custom 2 | 3 | import "fmt" 4 | 5 | var CustomJS = fmt.Sprintf(` 6 | // set custom title 7 | document.title = 'Swagger Dark Mode With Go'; 8 | 9 | // dark mode 10 | const style = document.createElement('style'); 11 | style.innerHTML = %s; 12 | document.head.appendChild(style); 13 | `, "`"+customCSS+"`") 14 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs Code generated by swaggo/swag. DO NOT EDIT 2 | package docs 3 | 4 | import "github.com/swaggo/swag" 5 | 6 | const docTemplate = `{ 7 | "schemes": {{ marshal .Schemes }}, 8 | "swagger": "2.0", 9 | "info": { 10 | "description": "{{escape .Description}}", 11 | "title": "{{.Title}}", 12 | "contact": {}, 13 | "version": "{{.Version}}" 14 | }, 15 | "host": "{{.Host}}", 16 | "basePath": "{{.BasePath}}", 17 | "paths": { 18 | "/category": { 19 | "post": { 20 | "description": "Endpoint for create category", 21 | "consumes": [ 22 | "application/json" 23 | ], 24 | "produces": [ 25 | "application/json" 26 | ], 27 | "tags": [ 28 | "category" 29 | ], 30 | "summary": "Create new category", 31 | "parameters": [ 32 | { 33 | "description": "Create category dto", 34 | "name": "body", 35 | "in": "body", 36 | "required": true, 37 | "schema": { 38 | "$ref": "#/definitions/dto.CreateCategoryDto" 39 | } 40 | } 41 | ], 42 | "responses": { 43 | "200": { 44 | "description": "OK" 45 | }, 46 | "400": { 47 | "description": "Bad Request", 48 | "schema": { 49 | "$ref": "#/definitions/httperr.RestErr" 50 | } 51 | }, 52 | "500": { 53 | "description": "Internal Server Error", 54 | "schema": { 55 | "$ref": "#/definitions/httperr.RestErr" 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | "/product": { 62 | "get": { 63 | "description": "Endpoint for search product", 64 | "consumes": [ 65 | "application/json" 66 | ], 67 | "produces": [ 68 | "application/json" 69 | ], 70 | "tags": [ 71 | "product" 72 | ], 73 | "summary": "Search products", 74 | "parameters": [ 75 | { 76 | "description": "Search products", 77 | "name": "body", 78 | "in": "body", 79 | "required": true, 80 | "schema": { 81 | "$ref": "#/definitions/dto.FindProductDto" 82 | } 83 | } 84 | ], 85 | "responses": { 86 | "200": { 87 | "description": "OK", 88 | "schema": { 89 | "$ref": "#/definitions/response.ProductResponse" 90 | } 91 | }, 92 | "400": { 93 | "description": "Bad Request", 94 | "schema": { 95 | "$ref": "#/definitions/httperr.RestErr" 96 | } 97 | }, 98 | "500": { 99 | "description": "Internal Server Error", 100 | "schema": { 101 | "$ref": "#/definitions/httperr.RestErr" 102 | } 103 | } 104 | } 105 | }, 106 | "post": { 107 | "description": "Endpoint for create product", 108 | "consumes": [ 109 | "application/json" 110 | ], 111 | "produces": [ 112 | "application/json" 113 | ], 114 | "tags": [ 115 | "product" 116 | ], 117 | "summary": "Create new product", 118 | "parameters": [ 119 | { 120 | "description": "Create product dto", 121 | "name": "body", 122 | "in": "body", 123 | "required": true, 124 | "schema": { 125 | "$ref": "#/definitions/dto.CreateProductDto" 126 | } 127 | } 128 | ], 129 | "responses": { 130 | "200": { 131 | "description": "OK" 132 | }, 133 | "400": { 134 | "description": "Bad Request", 135 | "schema": { 136 | "$ref": "#/definitions/httperr.RestErr" 137 | } 138 | }, 139 | "500": { 140 | "description": "Internal Server Error", 141 | "schema": { 142 | "$ref": "#/definitions/httperr.RestErr" 143 | } 144 | } 145 | } 146 | } 147 | }, 148 | "/product/{id}": { 149 | "delete": { 150 | "description": "Endpoint for update product", 151 | "consumes": [ 152 | "application/json" 153 | ], 154 | "produces": [ 155 | "application/json" 156 | ], 157 | "tags": [ 158 | "product" 159 | ], 160 | "summary": "Delete product", 161 | "parameters": [ 162 | { 163 | "type": "string", 164 | "description": "product id", 165 | "name": "id", 166 | "in": "path", 167 | "required": true 168 | } 169 | ], 170 | "responses": { 171 | "200": { 172 | "description": "OK" 173 | }, 174 | "400": { 175 | "description": "Bad Request", 176 | "schema": { 177 | "$ref": "#/definitions/httperr.RestErr" 178 | } 179 | }, 180 | "500": { 181 | "description": "Internal Server Error", 182 | "schema": { 183 | "$ref": "#/definitions/httperr.RestErr" 184 | } 185 | } 186 | } 187 | }, 188 | "patch": { 189 | "description": "Endpoint for update product", 190 | "consumes": [ 191 | "application/json" 192 | ], 193 | "produces": [ 194 | "application/json" 195 | ], 196 | "tags": [ 197 | "product" 198 | ], 199 | "summary": "Update product", 200 | "parameters": [ 201 | { 202 | "description": "Update product dto", 203 | "name": "body", 204 | "in": "body", 205 | "required": true, 206 | "schema": { 207 | "$ref": "#/definitions/dto.UpdateProductDto" 208 | } 209 | }, 210 | { 211 | "type": "string", 212 | "description": "product id", 213 | "name": "id", 214 | "in": "path", 215 | "required": true 216 | } 217 | ], 218 | "responses": { 219 | "200": { 220 | "description": "OK" 221 | }, 222 | "400": { 223 | "description": "Bad Request", 224 | "schema": { 225 | "$ref": "#/definitions/httperr.RestErr" 226 | } 227 | }, 228 | "500": { 229 | "description": "Internal Server Error", 230 | "schema": { 231 | "$ref": "#/definitions/httperr.RestErr" 232 | } 233 | } 234 | } 235 | } 236 | }, 237 | "/user": { 238 | "get": { 239 | "security": [ 240 | { 241 | "ApiKeyAuth": [] 242 | } 243 | ], 244 | "description": "Get many users", 245 | "consumes": [ 246 | "application/json" 247 | ], 248 | "produces": [ 249 | "application/json" 250 | ], 251 | "tags": [ 252 | "user" 253 | ], 254 | "summary": "Get many users", 255 | "responses": { 256 | "200": { 257 | "description": "OK", 258 | "schema": { 259 | "$ref": "#/definitions/response.ManyUsersResponse" 260 | } 261 | }, 262 | "400": { 263 | "description": "Bad Request", 264 | "schema": { 265 | "$ref": "#/definitions/httperr.RestErr" 266 | } 267 | }, 268 | "404": { 269 | "description": "Not Found", 270 | "schema": { 271 | "$ref": "#/definitions/httperr.RestErr" 272 | } 273 | }, 274 | "500": { 275 | "description": "Internal Server Error", 276 | "schema": { 277 | "$ref": "#/definitions/httperr.RestErr" 278 | } 279 | } 280 | } 281 | }, 282 | "post": { 283 | "description": "Endpoint for create user", 284 | "consumes": [ 285 | "application/json" 286 | ], 287 | "produces": [ 288 | "application/json" 289 | ], 290 | "tags": [ 291 | "user" 292 | ], 293 | "summary": "Create new user", 294 | "parameters": [ 295 | { 296 | "description": "Create user dto", 297 | "name": "body", 298 | "in": "body", 299 | "required": true, 300 | "schema": { 301 | "$ref": "#/definitions/dto.CreateUserDto" 302 | } 303 | } 304 | ], 305 | "responses": { 306 | "200": { 307 | "description": "OK" 308 | }, 309 | "400": { 310 | "description": "Bad Request", 311 | "schema": { 312 | "$ref": "#/definitions/httperr.RestErr" 313 | } 314 | }, 315 | "500": { 316 | "description": "Internal Server Error", 317 | "schema": { 318 | "$ref": "#/definitions/httperr.RestErr" 319 | } 320 | } 321 | } 322 | }, 323 | "delete": { 324 | "security": [ 325 | { 326 | "ApiKeyAuth": [] 327 | } 328 | ], 329 | "description": "delete user by id", 330 | "consumes": [ 331 | "application/json" 332 | ], 333 | "produces": [ 334 | "application/json" 335 | ], 336 | "tags": [ 337 | "user" 338 | ], 339 | "summary": "Delete user", 340 | "parameters": [ 341 | { 342 | "type": "string", 343 | "description": "user id", 344 | "name": "id", 345 | "in": "path", 346 | "required": true 347 | } 348 | ], 349 | "responses": { 350 | "200": { 351 | "description": "OK" 352 | }, 353 | "400": { 354 | "description": "Bad Request", 355 | "schema": { 356 | "$ref": "#/definitions/httperr.RestErr" 357 | } 358 | }, 359 | "404": { 360 | "description": "Not Found", 361 | "schema": { 362 | "$ref": "#/definitions/httperr.RestErr" 363 | } 364 | }, 365 | "500": { 366 | "description": "Internal Server Error", 367 | "schema": { 368 | "$ref": "#/definitions/httperr.RestErr" 369 | } 370 | } 371 | } 372 | }, 373 | "patch": { 374 | "security": [ 375 | { 376 | "ApiKeyAuth": [] 377 | } 378 | ], 379 | "description": "Endpoint for update user", 380 | "consumes": [ 381 | "application/json" 382 | ], 383 | "produces": [ 384 | "application/json" 385 | ], 386 | "tags": [ 387 | "user" 388 | ], 389 | "summary": "Update user", 390 | "parameters": [ 391 | { 392 | "description": "Update user dto", 393 | "name": "body", 394 | "in": "body", 395 | "schema": { 396 | "$ref": "#/definitions/dto.UpdateUserDto" 397 | } 398 | } 399 | ], 400 | "responses": { 401 | "200": { 402 | "description": "OK" 403 | }, 404 | "400": { 405 | "description": "Bad Request", 406 | "schema": { 407 | "$ref": "#/definitions/httperr.RestErr" 408 | } 409 | }, 410 | "404": { 411 | "description": "Not Found", 412 | "schema": { 413 | "$ref": "#/definitions/httperr.RestErr" 414 | } 415 | }, 416 | "500": { 417 | "description": "Internal Server Error", 418 | "schema": { 419 | "$ref": "#/definitions/httperr.RestErr" 420 | } 421 | } 422 | } 423 | } 424 | }, 425 | "/user/password": { 426 | "get": { 427 | "security": [ 428 | { 429 | "ApiKeyAuth": [] 430 | } 431 | ], 432 | "description": "Endpoint for Update user password", 433 | "consumes": [ 434 | "application/json" 435 | ], 436 | "produces": [ 437 | "application/json" 438 | ], 439 | "tags": [ 440 | "user" 441 | ], 442 | "summary": "Update user password", 443 | "parameters": [ 444 | { 445 | "type": "string", 446 | "description": "user id", 447 | "name": "id", 448 | "in": "path", 449 | "required": true 450 | }, 451 | { 452 | "description": "Update user password dto", 453 | "name": "body", 454 | "in": "body", 455 | "required": true, 456 | "schema": { 457 | "$ref": "#/definitions/dto.UpdateUserPasswordDto" 458 | } 459 | } 460 | ], 461 | "responses": { 462 | "200": { 463 | "description": "OK" 464 | }, 465 | "400": { 466 | "description": "Bad Request", 467 | "schema": { 468 | "$ref": "#/definitions/httperr.RestErr" 469 | } 470 | }, 471 | "500": { 472 | "description": "Internal Server Error", 473 | "schema": { 474 | "$ref": "#/definitions/httperr.RestErr" 475 | } 476 | } 477 | } 478 | } 479 | } 480 | }, 481 | "definitions": { 482 | "dto.CreateCategoryDto": { 483 | "type": "object", 484 | "required": [ 485 | "title" 486 | ], 487 | "properties": { 488 | "title": { 489 | "type": "string", 490 | "maxLength": 30, 491 | "minLength": 3 492 | } 493 | } 494 | }, 495 | "dto.CreateProductDto": { 496 | "type": "object", 497 | "required": [ 498 | "categories", 499 | "description", 500 | "price", 501 | "title" 502 | ], 503 | "properties": { 504 | "categories": { 505 | "type": "array", 506 | "minItems": 1, 507 | "items": { 508 | "type": "string" 509 | } 510 | }, 511 | "description": { 512 | "type": "string", 513 | "maxLength": 500, 514 | "minLength": 3 515 | }, 516 | "price": { 517 | "type": "integer", 518 | "minimum": 1 519 | }, 520 | "title": { 521 | "type": "string", 522 | "maxLength": 40, 523 | "minLength": 3 524 | } 525 | } 526 | }, 527 | "dto.CreateUserDto": { 528 | "type": "object", 529 | "required": [ 530 | "cep", 531 | "email", 532 | "name", 533 | "password" 534 | ], 535 | "properties": { 536 | "cep": { 537 | "type": "string", 538 | "maxLength": 8, 539 | "minLength": 8 540 | }, 541 | "email": { 542 | "type": "string" 543 | }, 544 | "name": { 545 | "type": "string", 546 | "maxLength": 30, 547 | "minLength": 3 548 | }, 549 | "password": { 550 | "type": "string", 551 | "maxLength": 30, 552 | "minLength": 8 553 | } 554 | } 555 | }, 556 | "dto.FindProductDto": { 557 | "type": "object", 558 | "properties": { 559 | "categories": { 560 | "type": "array", 561 | "minItems": 1, 562 | "items": { 563 | "type": "string" 564 | } 565 | }, 566 | "search": { 567 | "type": "string", 568 | "maxLength": 40, 569 | "minLength": 2 570 | } 571 | } 572 | }, 573 | "dto.UpdateProductDto": { 574 | "type": "object", 575 | "properties": { 576 | "categories": { 577 | "type": "array", 578 | "minItems": 1, 579 | "items": { 580 | "type": "string" 581 | } 582 | }, 583 | "description": { 584 | "type": "string", 585 | "maxLength": 500, 586 | "minLength": 3 587 | }, 588 | "price": { 589 | "type": "integer", 590 | "minimum": 1 591 | }, 592 | "title": { 593 | "type": "string", 594 | "maxLength": 40, 595 | "minLength": 3 596 | } 597 | } 598 | }, 599 | "dto.UpdateUserDto": { 600 | "type": "object", 601 | "properties": { 602 | "cep": { 603 | "type": "string", 604 | "maxLength": 8, 605 | "minLength": 8 606 | }, 607 | "email": { 608 | "type": "string" 609 | }, 610 | "name": { 611 | "type": "string", 612 | "maxLength": 30, 613 | "minLength": 3 614 | } 615 | } 616 | }, 617 | "dto.UpdateUserPasswordDto": { 618 | "type": "object", 619 | "required": [ 620 | "old_password", 621 | "password" 622 | ], 623 | "properties": { 624 | "old_password": { 625 | "type": "string", 626 | "maxLength": 30, 627 | "minLength": 8 628 | }, 629 | "password": { 630 | "type": "string", 631 | "maxLength": 30, 632 | "minLength": 8 633 | } 634 | } 635 | }, 636 | "httperr.Fields": { 637 | "type": "object", 638 | "properties": { 639 | "field": { 640 | "type": "string" 641 | }, 642 | "message": { 643 | "type": "string" 644 | }, 645 | "value": {} 646 | } 647 | }, 648 | "httperr.RestErr": { 649 | "type": "object", 650 | "properties": { 651 | "code": { 652 | "type": "integer" 653 | }, 654 | "error": { 655 | "type": "string" 656 | }, 657 | "fields": { 658 | "type": "array", 659 | "items": { 660 | "$ref": "#/definitions/httperr.Fields" 661 | } 662 | }, 663 | "message": { 664 | "type": "string" 665 | } 666 | } 667 | }, 668 | "response.CategoryResponse": { 669 | "type": "object", 670 | "properties": { 671 | "id": { 672 | "type": "string" 673 | }, 674 | "title": { 675 | "type": "string" 676 | } 677 | } 678 | }, 679 | "response.ManyUsersResponse": { 680 | "type": "object", 681 | "properties": { 682 | "users": { 683 | "type": "array", 684 | "items": { 685 | "$ref": "#/definitions/response.UserResponse" 686 | } 687 | } 688 | } 689 | }, 690 | "response.ProductResponse": { 691 | "type": "object", 692 | "properties": { 693 | "categories": { 694 | "type": "array", 695 | "items": { 696 | "$ref": "#/definitions/response.CategoryResponse" 697 | } 698 | }, 699 | "created_at": { 700 | "type": "string" 701 | }, 702 | "description": { 703 | "type": "string" 704 | }, 705 | "id": { 706 | "type": "string" 707 | }, 708 | "price": { 709 | "type": "integer" 710 | }, 711 | "title": { 712 | "type": "string" 713 | } 714 | } 715 | }, 716 | "response.UserAddress": { 717 | "type": "object", 718 | "properties": { 719 | "cep": { 720 | "type": "string" 721 | }, 722 | "city": { 723 | "type": "string" 724 | }, 725 | "complement": { 726 | "type": "string" 727 | }, 728 | "street": { 729 | "type": "string" 730 | }, 731 | "uf": { 732 | "type": "string" 733 | } 734 | } 735 | }, 736 | "response.UserResponse": { 737 | "type": "object", 738 | "properties": { 739 | "address": { 740 | "$ref": "#/definitions/response.UserAddress" 741 | }, 742 | "created_at": { 743 | "type": "string" 744 | }, 745 | "email": { 746 | "type": "string" 747 | }, 748 | "id": { 749 | "type": "string" 750 | }, 751 | "name": { 752 | "type": "string" 753 | }, 754 | "updated_at": { 755 | "type": "string" 756 | } 757 | } 758 | } 759 | } 760 | }` 761 | 762 | // SwaggerInfo holds exported Swagger Info so clients can modify it 763 | var SwaggerInfo = &swag.Spec{ 764 | Version: "1.0", 765 | Host: "", 766 | BasePath: "", 767 | Schemes: []string{}, 768 | Title: "API users", 769 | Description: "", 770 | InfoInstanceName: "swagger", 771 | SwaggerTemplate: docTemplate, 772 | LeftDelim: "{{", 773 | RightDelim: "}}", 774 | } 775 | 776 | func init() { 777 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 778 | } 779 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "API users", 5 | "contact": {}, 6 | "version": "1.0" 7 | }, 8 | "paths": { 9 | "/category": { 10 | "post": { 11 | "description": "Endpoint for create category", 12 | "consumes": [ 13 | "application/json" 14 | ], 15 | "produces": [ 16 | "application/json" 17 | ], 18 | "tags": [ 19 | "category" 20 | ], 21 | "summary": "Create new category", 22 | "parameters": [ 23 | { 24 | "description": "Create category dto", 25 | "name": "body", 26 | "in": "body", 27 | "required": true, 28 | "schema": { 29 | "$ref": "#/definitions/dto.CreateCategoryDto" 30 | } 31 | } 32 | ], 33 | "responses": { 34 | "200": { 35 | "description": "OK" 36 | }, 37 | "400": { 38 | "description": "Bad Request", 39 | "schema": { 40 | "$ref": "#/definitions/httperr.RestErr" 41 | } 42 | }, 43 | "500": { 44 | "description": "Internal Server Error", 45 | "schema": { 46 | "$ref": "#/definitions/httperr.RestErr" 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "/product": { 53 | "get": { 54 | "description": "Endpoint for search product", 55 | "consumes": [ 56 | "application/json" 57 | ], 58 | "produces": [ 59 | "application/json" 60 | ], 61 | "tags": [ 62 | "product" 63 | ], 64 | "summary": "Search products", 65 | "parameters": [ 66 | { 67 | "description": "Search products", 68 | "name": "body", 69 | "in": "body", 70 | "required": true, 71 | "schema": { 72 | "$ref": "#/definitions/dto.FindProductDto" 73 | } 74 | } 75 | ], 76 | "responses": { 77 | "200": { 78 | "description": "OK", 79 | "schema": { 80 | "$ref": "#/definitions/response.ProductResponse" 81 | } 82 | }, 83 | "400": { 84 | "description": "Bad Request", 85 | "schema": { 86 | "$ref": "#/definitions/httperr.RestErr" 87 | } 88 | }, 89 | "500": { 90 | "description": "Internal Server Error", 91 | "schema": { 92 | "$ref": "#/definitions/httperr.RestErr" 93 | } 94 | } 95 | } 96 | }, 97 | "post": { 98 | "description": "Endpoint for create product", 99 | "consumes": [ 100 | "application/json" 101 | ], 102 | "produces": [ 103 | "application/json" 104 | ], 105 | "tags": [ 106 | "product" 107 | ], 108 | "summary": "Create new product", 109 | "parameters": [ 110 | { 111 | "description": "Create product dto", 112 | "name": "body", 113 | "in": "body", 114 | "required": true, 115 | "schema": { 116 | "$ref": "#/definitions/dto.CreateProductDto" 117 | } 118 | } 119 | ], 120 | "responses": { 121 | "200": { 122 | "description": "OK" 123 | }, 124 | "400": { 125 | "description": "Bad Request", 126 | "schema": { 127 | "$ref": "#/definitions/httperr.RestErr" 128 | } 129 | }, 130 | "500": { 131 | "description": "Internal Server Error", 132 | "schema": { 133 | "$ref": "#/definitions/httperr.RestErr" 134 | } 135 | } 136 | } 137 | } 138 | }, 139 | "/product/{id}": { 140 | "delete": { 141 | "description": "Endpoint for update product", 142 | "consumes": [ 143 | "application/json" 144 | ], 145 | "produces": [ 146 | "application/json" 147 | ], 148 | "tags": [ 149 | "product" 150 | ], 151 | "summary": "Delete product", 152 | "parameters": [ 153 | { 154 | "type": "string", 155 | "description": "product id", 156 | "name": "id", 157 | "in": "path", 158 | "required": true 159 | } 160 | ], 161 | "responses": { 162 | "200": { 163 | "description": "OK" 164 | }, 165 | "400": { 166 | "description": "Bad Request", 167 | "schema": { 168 | "$ref": "#/definitions/httperr.RestErr" 169 | } 170 | }, 171 | "500": { 172 | "description": "Internal Server Error", 173 | "schema": { 174 | "$ref": "#/definitions/httperr.RestErr" 175 | } 176 | } 177 | } 178 | }, 179 | "patch": { 180 | "description": "Endpoint for update product", 181 | "consumes": [ 182 | "application/json" 183 | ], 184 | "produces": [ 185 | "application/json" 186 | ], 187 | "tags": [ 188 | "product" 189 | ], 190 | "summary": "Update product", 191 | "parameters": [ 192 | { 193 | "description": "Update product dto", 194 | "name": "body", 195 | "in": "body", 196 | "required": true, 197 | "schema": { 198 | "$ref": "#/definitions/dto.UpdateProductDto" 199 | } 200 | }, 201 | { 202 | "type": "string", 203 | "description": "product id", 204 | "name": "id", 205 | "in": "path", 206 | "required": true 207 | } 208 | ], 209 | "responses": { 210 | "200": { 211 | "description": "OK" 212 | }, 213 | "400": { 214 | "description": "Bad Request", 215 | "schema": { 216 | "$ref": "#/definitions/httperr.RestErr" 217 | } 218 | }, 219 | "500": { 220 | "description": "Internal Server Error", 221 | "schema": { 222 | "$ref": "#/definitions/httperr.RestErr" 223 | } 224 | } 225 | } 226 | } 227 | }, 228 | "/user": { 229 | "get": { 230 | "security": [ 231 | { 232 | "ApiKeyAuth": [] 233 | } 234 | ], 235 | "description": "Get many users", 236 | "consumes": [ 237 | "application/json" 238 | ], 239 | "produces": [ 240 | "application/json" 241 | ], 242 | "tags": [ 243 | "user" 244 | ], 245 | "summary": "Get many users", 246 | "responses": { 247 | "200": { 248 | "description": "OK", 249 | "schema": { 250 | "$ref": "#/definitions/response.ManyUsersResponse" 251 | } 252 | }, 253 | "400": { 254 | "description": "Bad Request", 255 | "schema": { 256 | "$ref": "#/definitions/httperr.RestErr" 257 | } 258 | }, 259 | "404": { 260 | "description": "Not Found", 261 | "schema": { 262 | "$ref": "#/definitions/httperr.RestErr" 263 | } 264 | }, 265 | "500": { 266 | "description": "Internal Server Error", 267 | "schema": { 268 | "$ref": "#/definitions/httperr.RestErr" 269 | } 270 | } 271 | } 272 | }, 273 | "post": { 274 | "description": "Endpoint for create user", 275 | "consumes": [ 276 | "application/json" 277 | ], 278 | "produces": [ 279 | "application/json" 280 | ], 281 | "tags": [ 282 | "user" 283 | ], 284 | "summary": "Create new user", 285 | "parameters": [ 286 | { 287 | "description": "Create user dto", 288 | "name": "body", 289 | "in": "body", 290 | "required": true, 291 | "schema": { 292 | "$ref": "#/definitions/dto.CreateUserDto" 293 | } 294 | } 295 | ], 296 | "responses": { 297 | "200": { 298 | "description": "OK" 299 | }, 300 | "400": { 301 | "description": "Bad Request", 302 | "schema": { 303 | "$ref": "#/definitions/httperr.RestErr" 304 | } 305 | }, 306 | "500": { 307 | "description": "Internal Server Error", 308 | "schema": { 309 | "$ref": "#/definitions/httperr.RestErr" 310 | } 311 | } 312 | } 313 | }, 314 | "delete": { 315 | "security": [ 316 | { 317 | "ApiKeyAuth": [] 318 | } 319 | ], 320 | "description": "delete user by id", 321 | "consumes": [ 322 | "application/json" 323 | ], 324 | "produces": [ 325 | "application/json" 326 | ], 327 | "tags": [ 328 | "user" 329 | ], 330 | "summary": "Delete user", 331 | "parameters": [ 332 | { 333 | "type": "string", 334 | "description": "user id", 335 | "name": "id", 336 | "in": "path", 337 | "required": true 338 | } 339 | ], 340 | "responses": { 341 | "200": { 342 | "description": "OK" 343 | }, 344 | "400": { 345 | "description": "Bad Request", 346 | "schema": { 347 | "$ref": "#/definitions/httperr.RestErr" 348 | } 349 | }, 350 | "404": { 351 | "description": "Not Found", 352 | "schema": { 353 | "$ref": "#/definitions/httperr.RestErr" 354 | } 355 | }, 356 | "500": { 357 | "description": "Internal Server Error", 358 | "schema": { 359 | "$ref": "#/definitions/httperr.RestErr" 360 | } 361 | } 362 | } 363 | }, 364 | "patch": { 365 | "security": [ 366 | { 367 | "ApiKeyAuth": [] 368 | } 369 | ], 370 | "description": "Endpoint for update user", 371 | "consumes": [ 372 | "application/json" 373 | ], 374 | "produces": [ 375 | "application/json" 376 | ], 377 | "tags": [ 378 | "user" 379 | ], 380 | "summary": "Update user", 381 | "parameters": [ 382 | { 383 | "description": "Update user dto", 384 | "name": "body", 385 | "in": "body", 386 | "schema": { 387 | "$ref": "#/definitions/dto.UpdateUserDto" 388 | } 389 | } 390 | ], 391 | "responses": { 392 | "200": { 393 | "description": "OK" 394 | }, 395 | "400": { 396 | "description": "Bad Request", 397 | "schema": { 398 | "$ref": "#/definitions/httperr.RestErr" 399 | } 400 | }, 401 | "404": { 402 | "description": "Not Found", 403 | "schema": { 404 | "$ref": "#/definitions/httperr.RestErr" 405 | } 406 | }, 407 | "500": { 408 | "description": "Internal Server Error", 409 | "schema": { 410 | "$ref": "#/definitions/httperr.RestErr" 411 | } 412 | } 413 | } 414 | } 415 | }, 416 | "/user/password": { 417 | "get": { 418 | "security": [ 419 | { 420 | "ApiKeyAuth": [] 421 | } 422 | ], 423 | "description": "Endpoint for Update user password", 424 | "consumes": [ 425 | "application/json" 426 | ], 427 | "produces": [ 428 | "application/json" 429 | ], 430 | "tags": [ 431 | "user" 432 | ], 433 | "summary": "Update user password", 434 | "parameters": [ 435 | { 436 | "type": "string", 437 | "description": "user id", 438 | "name": "id", 439 | "in": "path", 440 | "required": true 441 | }, 442 | { 443 | "description": "Update user password dto", 444 | "name": "body", 445 | "in": "body", 446 | "required": true, 447 | "schema": { 448 | "$ref": "#/definitions/dto.UpdateUserPasswordDto" 449 | } 450 | } 451 | ], 452 | "responses": { 453 | "200": { 454 | "description": "OK" 455 | }, 456 | "400": { 457 | "description": "Bad Request", 458 | "schema": { 459 | "$ref": "#/definitions/httperr.RestErr" 460 | } 461 | }, 462 | "500": { 463 | "description": "Internal Server Error", 464 | "schema": { 465 | "$ref": "#/definitions/httperr.RestErr" 466 | } 467 | } 468 | } 469 | } 470 | } 471 | }, 472 | "definitions": { 473 | "dto.CreateCategoryDto": { 474 | "type": "object", 475 | "required": [ 476 | "title" 477 | ], 478 | "properties": { 479 | "title": { 480 | "type": "string", 481 | "maxLength": 30, 482 | "minLength": 3 483 | } 484 | } 485 | }, 486 | "dto.CreateProductDto": { 487 | "type": "object", 488 | "required": [ 489 | "categories", 490 | "description", 491 | "price", 492 | "title" 493 | ], 494 | "properties": { 495 | "categories": { 496 | "type": "array", 497 | "minItems": 1, 498 | "items": { 499 | "type": "string" 500 | } 501 | }, 502 | "description": { 503 | "type": "string", 504 | "maxLength": 500, 505 | "minLength": 3 506 | }, 507 | "price": { 508 | "type": "integer", 509 | "minimum": 1 510 | }, 511 | "title": { 512 | "type": "string", 513 | "maxLength": 40, 514 | "minLength": 3 515 | } 516 | } 517 | }, 518 | "dto.CreateUserDto": { 519 | "type": "object", 520 | "required": [ 521 | "cep", 522 | "email", 523 | "name", 524 | "password" 525 | ], 526 | "properties": { 527 | "cep": { 528 | "type": "string", 529 | "maxLength": 8, 530 | "minLength": 8 531 | }, 532 | "email": { 533 | "type": "string" 534 | }, 535 | "name": { 536 | "type": "string", 537 | "maxLength": 30, 538 | "minLength": 3 539 | }, 540 | "password": { 541 | "type": "string", 542 | "maxLength": 30, 543 | "minLength": 8 544 | } 545 | } 546 | }, 547 | "dto.FindProductDto": { 548 | "type": "object", 549 | "properties": { 550 | "categories": { 551 | "type": "array", 552 | "minItems": 1, 553 | "items": { 554 | "type": "string" 555 | } 556 | }, 557 | "search": { 558 | "type": "string", 559 | "maxLength": 40, 560 | "minLength": 2 561 | } 562 | } 563 | }, 564 | "dto.UpdateProductDto": { 565 | "type": "object", 566 | "properties": { 567 | "categories": { 568 | "type": "array", 569 | "minItems": 1, 570 | "items": { 571 | "type": "string" 572 | } 573 | }, 574 | "description": { 575 | "type": "string", 576 | "maxLength": 500, 577 | "minLength": 3 578 | }, 579 | "price": { 580 | "type": "integer", 581 | "minimum": 1 582 | }, 583 | "title": { 584 | "type": "string", 585 | "maxLength": 40, 586 | "minLength": 3 587 | } 588 | } 589 | }, 590 | "dto.UpdateUserDto": { 591 | "type": "object", 592 | "properties": { 593 | "cep": { 594 | "type": "string", 595 | "maxLength": 8, 596 | "minLength": 8 597 | }, 598 | "email": { 599 | "type": "string" 600 | }, 601 | "name": { 602 | "type": "string", 603 | "maxLength": 30, 604 | "minLength": 3 605 | } 606 | } 607 | }, 608 | "dto.UpdateUserPasswordDto": { 609 | "type": "object", 610 | "required": [ 611 | "old_password", 612 | "password" 613 | ], 614 | "properties": { 615 | "old_password": { 616 | "type": "string", 617 | "maxLength": 30, 618 | "minLength": 8 619 | }, 620 | "password": { 621 | "type": "string", 622 | "maxLength": 30, 623 | "minLength": 8 624 | } 625 | } 626 | }, 627 | "httperr.Fields": { 628 | "type": "object", 629 | "properties": { 630 | "field": { 631 | "type": "string" 632 | }, 633 | "message": { 634 | "type": "string" 635 | }, 636 | "value": {} 637 | } 638 | }, 639 | "httperr.RestErr": { 640 | "type": "object", 641 | "properties": { 642 | "code": { 643 | "type": "integer" 644 | }, 645 | "error": { 646 | "type": "string" 647 | }, 648 | "fields": { 649 | "type": "array", 650 | "items": { 651 | "$ref": "#/definitions/httperr.Fields" 652 | } 653 | }, 654 | "message": { 655 | "type": "string" 656 | } 657 | } 658 | }, 659 | "response.CategoryResponse": { 660 | "type": "object", 661 | "properties": { 662 | "id": { 663 | "type": "string" 664 | }, 665 | "title": { 666 | "type": "string" 667 | } 668 | } 669 | }, 670 | "response.ManyUsersResponse": { 671 | "type": "object", 672 | "properties": { 673 | "users": { 674 | "type": "array", 675 | "items": { 676 | "$ref": "#/definitions/response.UserResponse" 677 | } 678 | } 679 | } 680 | }, 681 | "response.ProductResponse": { 682 | "type": "object", 683 | "properties": { 684 | "categories": { 685 | "type": "array", 686 | "items": { 687 | "$ref": "#/definitions/response.CategoryResponse" 688 | } 689 | }, 690 | "created_at": { 691 | "type": "string" 692 | }, 693 | "description": { 694 | "type": "string" 695 | }, 696 | "id": { 697 | "type": "string" 698 | }, 699 | "price": { 700 | "type": "integer" 701 | }, 702 | "title": { 703 | "type": "string" 704 | } 705 | } 706 | }, 707 | "response.UserAddress": { 708 | "type": "object", 709 | "properties": { 710 | "cep": { 711 | "type": "string" 712 | }, 713 | "city": { 714 | "type": "string" 715 | }, 716 | "complement": { 717 | "type": "string" 718 | }, 719 | "street": { 720 | "type": "string" 721 | }, 722 | "uf": { 723 | "type": "string" 724 | } 725 | } 726 | }, 727 | "response.UserResponse": { 728 | "type": "object", 729 | "properties": { 730 | "address": { 731 | "$ref": "#/definitions/response.UserAddress" 732 | }, 733 | "created_at": { 734 | "type": "string" 735 | }, 736 | "email": { 737 | "type": "string" 738 | }, 739 | "id": { 740 | "type": "string" 741 | }, 742 | "name": { 743 | "type": "string" 744 | }, 745 | "updated_at": { 746 | "type": "string" 747 | } 748 | } 749 | } 750 | } 751 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | dto.CreateCategoryDto: 3 | properties: 4 | title: 5 | maxLength: 30 6 | minLength: 3 7 | type: string 8 | required: 9 | - title 10 | type: object 11 | dto.CreateProductDto: 12 | properties: 13 | categories: 14 | items: 15 | type: string 16 | minItems: 1 17 | type: array 18 | description: 19 | maxLength: 500 20 | minLength: 3 21 | type: string 22 | price: 23 | minimum: 1 24 | type: integer 25 | title: 26 | maxLength: 40 27 | minLength: 3 28 | type: string 29 | required: 30 | - categories 31 | - description 32 | - price 33 | - title 34 | type: object 35 | dto.CreateUserDto: 36 | properties: 37 | cep: 38 | maxLength: 8 39 | minLength: 8 40 | type: string 41 | email: 42 | type: string 43 | name: 44 | maxLength: 30 45 | minLength: 3 46 | type: string 47 | password: 48 | maxLength: 30 49 | minLength: 8 50 | type: string 51 | required: 52 | - cep 53 | - email 54 | - name 55 | - password 56 | type: object 57 | dto.FindProductDto: 58 | properties: 59 | categories: 60 | items: 61 | type: string 62 | minItems: 1 63 | type: array 64 | search: 65 | maxLength: 40 66 | minLength: 2 67 | type: string 68 | type: object 69 | dto.UpdateProductDto: 70 | properties: 71 | categories: 72 | items: 73 | type: string 74 | minItems: 1 75 | type: array 76 | description: 77 | maxLength: 500 78 | minLength: 3 79 | type: string 80 | price: 81 | minimum: 1 82 | type: integer 83 | title: 84 | maxLength: 40 85 | minLength: 3 86 | type: string 87 | type: object 88 | dto.UpdateUserDto: 89 | properties: 90 | cep: 91 | maxLength: 8 92 | minLength: 8 93 | type: string 94 | email: 95 | type: string 96 | name: 97 | maxLength: 30 98 | minLength: 3 99 | type: string 100 | type: object 101 | dto.UpdateUserPasswordDto: 102 | properties: 103 | old_password: 104 | maxLength: 30 105 | minLength: 8 106 | type: string 107 | password: 108 | maxLength: 30 109 | minLength: 8 110 | type: string 111 | required: 112 | - old_password 113 | - password 114 | type: object 115 | httperr.Fields: 116 | properties: 117 | field: 118 | type: string 119 | message: 120 | type: string 121 | value: {} 122 | type: object 123 | httperr.RestErr: 124 | properties: 125 | code: 126 | type: integer 127 | error: 128 | type: string 129 | fields: 130 | items: 131 | $ref: '#/definitions/httperr.Fields' 132 | type: array 133 | message: 134 | type: string 135 | type: object 136 | response.CategoryResponse: 137 | properties: 138 | id: 139 | type: string 140 | title: 141 | type: string 142 | type: object 143 | response.ManyUsersResponse: 144 | properties: 145 | users: 146 | items: 147 | $ref: '#/definitions/response.UserResponse' 148 | type: array 149 | type: object 150 | response.ProductResponse: 151 | properties: 152 | categories: 153 | items: 154 | $ref: '#/definitions/response.CategoryResponse' 155 | type: array 156 | created_at: 157 | type: string 158 | description: 159 | type: string 160 | id: 161 | type: string 162 | price: 163 | type: integer 164 | title: 165 | type: string 166 | type: object 167 | response.UserAddress: 168 | properties: 169 | cep: 170 | type: string 171 | city: 172 | type: string 173 | complement: 174 | type: string 175 | street: 176 | type: string 177 | uf: 178 | type: string 179 | type: object 180 | response.UserResponse: 181 | properties: 182 | address: 183 | $ref: '#/definitions/response.UserAddress' 184 | created_at: 185 | type: string 186 | email: 187 | type: string 188 | id: 189 | type: string 190 | name: 191 | type: string 192 | updated_at: 193 | type: string 194 | type: object 195 | info: 196 | contact: {} 197 | title: API users 198 | version: "1.0" 199 | paths: 200 | /category: 201 | post: 202 | consumes: 203 | - application/json 204 | description: Endpoint for create category 205 | parameters: 206 | - description: Create category dto 207 | in: body 208 | name: body 209 | required: true 210 | schema: 211 | $ref: '#/definitions/dto.CreateCategoryDto' 212 | produces: 213 | - application/json 214 | responses: 215 | "200": 216 | description: OK 217 | "400": 218 | description: Bad Request 219 | schema: 220 | $ref: '#/definitions/httperr.RestErr' 221 | "500": 222 | description: Internal Server Error 223 | schema: 224 | $ref: '#/definitions/httperr.RestErr' 225 | summary: Create new category 226 | tags: 227 | - category 228 | /product: 229 | get: 230 | consumes: 231 | - application/json 232 | description: Endpoint for search product 233 | parameters: 234 | - description: Search products 235 | in: body 236 | name: body 237 | required: true 238 | schema: 239 | $ref: '#/definitions/dto.FindProductDto' 240 | produces: 241 | - application/json 242 | responses: 243 | "200": 244 | description: OK 245 | schema: 246 | $ref: '#/definitions/response.ProductResponse' 247 | "400": 248 | description: Bad Request 249 | schema: 250 | $ref: '#/definitions/httperr.RestErr' 251 | "500": 252 | description: Internal Server Error 253 | schema: 254 | $ref: '#/definitions/httperr.RestErr' 255 | summary: Search products 256 | tags: 257 | - product 258 | post: 259 | consumes: 260 | - application/json 261 | description: Endpoint for create product 262 | parameters: 263 | - description: Create product dto 264 | in: body 265 | name: body 266 | required: true 267 | schema: 268 | $ref: '#/definitions/dto.CreateProductDto' 269 | produces: 270 | - application/json 271 | responses: 272 | "200": 273 | description: OK 274 | "400": 275 | description: Bad Request 276 | schema: 277 | $ref: '#/definitions/httperr.RestErr' 278 | "500": 279 | description: Internal Server Error 280 | schema: 281 | $ref: '#/definitions/httperr.RestErr' 282 | summary: Create new product 283 | tags: 284 | - product 285 | /product/{id}: 286 | delete: 287 | consumes: 288 | - application/json 289 | description: Endpoint for update product 290 | parameters: 291 | - description: product id 292 | in: path 293 | name: id 294 | required: true 295 | type: string 296 | produces: 297 | - application/json 298 | responses: 299 | "200": 300 | description: OK 301 | "400": 302 | description: Bad Request 303 | schema: 304 | $ref: '#/definitions/httperr.RestErr' 305 | "500": 306 | description: Internal Server Error 307 | schema: 308 | $ref: '#/definitions/httperr.RestErr' 309 | summary: Delete product 310 | tags: 311 | - product 312 | patch: 313 | consumes: 314 | - application/json 315 | description: Endpoint for update product 316 | parameters: 317 | - description: Update product dto 318 | in: body 319 | name: body 320 | required: true 321 | schema: 322 | $ref: '#/definitions/dto.UpdateProductDto' 323 | - description: product id 324 | in: path 325 | name: id 326 | required: true 327 | type: string 328 | produces: 329 | - application/json 330 | responses: 331 | "200": 332 | description: OK 333 | "400": 334 | description: Bad Request 335 | schema: 336 | $ref: '#/definitions/httperr.RestErr' 337 | "500": 338 | description: Internal Server Error 339 | schema: 340 | $ref: '#/definitions/httperr.RestErr' 341 | summary: Update product 342 | tags: 343 | - product 344 | /user: 345 | delete: 346 | consumes: 347 | - application/json 348 | description: delete user by id 349 | parameters: 350 | - description: user id 351 | in: path 352 | name: id 353 | required: true 354 | type: string 355 | produces: 356 | - application/json 357 | responses: 358 | "200": 359 | description: OK 360 | "400": 361 | description: Bad Request 362 | schema: 363 | $ref: '#/definitions/httperr.RestErr' 364 | "404": 365 | description: Not Found 366 | schema: 367 | $ref: '#/definitions/httperr.RestErr' 368 | "500": 369 | description: Internal Server Error 370 | schema: 371 | $ref: '#/definitions/httperr.RestErr' 372 | security: 373 | - ApiKeyAuth: [] 374 | summary: Delete user 375 | tags: 376 | - user 377 | get: 378 | consumes: 379 | - application/json 380 | description: Get many users 381 | produces: 382 | - application/json 383 | responses: 384 | "200": 385 | description: OK 386 | schema: 387 | $ref: '#/definitions/response.ManyUsersResponse' 388 | "400": 389 | description: Bad Request 390 | schema: 391 | $ref: '#/definitions/httperr.RestErr' 392 | "404": 393 | description: Not Found 394 | schema: 395 | $ref: '#/definitions/httperr.RestErr' 396 | "500": 397 | description: Internal Server Error 398 | schema: 399 | $ref: '#/definitions/httperr.RestErr' 400 | security: 401 | - ApiKeyAuth: [] 402 | summary: Get many users 403 | tags: 404 | - user 405 | patch: 406 | consumes: 407 | - application/json 408 | description: Endpoint for update user 409 | parameters: 410 | - description: Update user dto 411 | in: body 412 | name: body 413 | schema: 414 | $ref: '#/definitions/dto.UpdateUserDto' 415 | produces: 416 | - application/json 417 | responses: 418 | "200": 419 | description: OK 420 | "400": 421 | description: Bad Request 422 | schema: 423 | $ref: '#/definitions/httperr.RestErr' 424 | "404": 425 | description: Not Found 426 | schema: 427 | $ref: '#/definitions/httperr.RestErr' 428 | "500": 429 | description: Internal Server Error 430 | schema: 431 | $ref: '#/definitions/httperr.RestErr' 432 | security: 433 | - ApiKeyAuth: [] 434 | summary: Update user 435 | tags: 436 | - user 437 | post: 438 | consumes: 439 | - application/json 440 | description: Endpoint for create user 441 | parameters: 442 | - description: Create user dto 443 | in: body 444 | name: body 445 | required: true 446 | schema: 447 | $ref: '#/definitions/dto.CreateUserDto' 448 | produces: 449 | - application/json 450 | responses: 451 | "200": 452 | description: OK 453 | "400": 454 | description: Bad Request 455 | schema: 456 | $ref: '#/definitions/httperr.RestErr' 457 | "500": 458 | description: Internal Server Error 459 | schema: 460 | $ref: '#/definitions/httperr.RestErr' 461 | summary: Create new user 462 | tags: 463 | - user 464 | /user/password: 465 | get: 466 | consumes: 467 | - application/json 468 | description: Endpoint for Update user password 469 | parameters: 470 | - description: user id 471 | in: path 472 | name: id 473 | required: true 474 | type: string 475 | - description: Update user password dto 476 | in: body 477 | name: body 478 | required: true 479 | schema: 480 | $ref: '#/definitions/dto.UpdateUserPasswordDto' 481 | produces: 482 | - application/json 483 | responses: 484 | "200": 485 | description: OK 486 | "400": 487 | description: Bad Request 488 | schema: 489 | $ref: '#/definitions/httperr.RestErr' 490 | "500": 491 | description: Internal Server Error 492 | schema: 493 | $ref: '#/definitions/httperr.RestErr' 494 | security: 495 | - ApiKeyAuth: [] 496 | summary: Update user password 497 | tags: 498 | - user 499 | swagger: "2.0" 500 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wiliamvj/api-users-golang 2 | 3 | go 1.21.3 4 | 5 | require ( 6 | github.com/go-chi/chi v1.5.5 7 | github.com/go-chi/jwtauth v1.2.0 8 | github.com/go-playground/validator/v10 v10.16.0 9 | github.com/golang-jwt/jwt/v4 v4.5.0 10 | github.com/google/uuid v1.1.2 11 | github.com/lib/pq v1.10.9 12 | github.com/spf13/viper v1.17.0 13 | github.com/swaggo/http-swagger v1.3.4 14 | github.com/swaggo/swag v1.8.1 15 | golang.org/x/crypto v0.13.0 16 | ) 17 | 18 | require ( 19 | github.com/KyleBanks/depth v1.2.1 // indirect 20 | github.com/fsnotify/fsnotify v1.6.0 // indirect 21 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 22 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 23 | github.com/go-openapi/jsonreference v0.20.0 // indirect 24 | github.com/go-openapi/spec v0.20.6 // indirect 25 | github.com/go-openapi/swag v0.19.15 // indirect 26 | github.com/go-playground/locales v0.14.1 // indirect 27 | github.com/go-playground/universal-translator v0.18.1 // indirect 28 | github.com/goccy/go-json v0.3.5 // indirect 29 | github.com/hashicorp/hcl v1.0.0 // indirect 30 | github.com/josharian/intern v1.0.0 // indirect 31 | github.com/leodido/go-urn v1.2.4 // indirect 32 | github.com/lestrrat-go/backoff/v2 v2.0.7 // indirect 33 | github.com/lestrrat-go/httpcc v1.0.0 // indirect 34 | github.com/lestrrat-go/iter v1.0.0 // indirect 35 | github.com/lestrrat-go/jwx v1.1.0 // indirect 36 | github.com/lestrrat-go/option v1.0.0 // indirect 37 | github.com/magiconair/properties v1.8.7 // indirect 38 | github.com/mailru/easyjson v0.7.6 // indirect 39 | github.com/mitchellh/mapstructure v1.5.0 // indirect 40 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 41 | github.com/pkg/errors v0.9.1 // indirect 42 | github.com/sagikazarmark/locafero v0.3.0 // indirect 43 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 44 | github.com/sourcegraph/conc v0.3.0 // indirect 45 | github.com/spf13/afero v1.10.0 // indirect 46 | github.com/spf13/cast v1.5.1 // indirect 47 | github.com/spf13/pflag v1.0.5 // indirect 48 | github.com/subosito/gotenv v1.6.0 // indirect 49 | github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect 50 | go.uber.org/atomic v1.9.0 // indirect 51 | go.uber.org/multierr v1.9.0 // indirect 52 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 53 | golang.org/x/net v0.15.0 // indirect 54 | golang.org/x/sys v0.12.0 // indirect 55 | golang.org/x/text v0.13.0 // indirect 56 | golang.org/x/tools v0.13.0 // indirect 57 | gopkg.in/ini.v1 v1.67.0 // indirect 58 | gopkg.in/yaml.v2 v2.4.0 // indirect 59 | gopkg.in/yaml.v3 v3.0.1 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /http_client.http: -------------------------------------------------------------------------------- 1 | 2 | ## CreateUser 3 | 4 | POST http://localhost:8080/user HTTP/1.1 5 | content-type: application/json 6 | 7 | { 8 | "name": "John Doe", 9 | "email": "john.doe2@email.com", 10 | "password": "12345678@", 11 | "cep": "01153000" 12 | } 13 | ### 14 | 15 | ## UpdateUser 16 | PATCH http://localhost:8080/user HTTP/1.1 17 | content-type: application/json 18 | Authorization: Bearer {{token}} 19 | 20 | { 21 | "name": "John Doe", 22 | "email": "john.doe2s@email.com", 23 | "cep": "88132243" 24 | } 25 | ### 26 | 27 | ## GetUserByID 28 | GET http://localhost:8080/user HTTP/1.1 29 | content-type: application/json 30 | Authorization: Bearer {{token}} 31 | ### 32 | 33 | ## DeleteUser 34 | DELETE http://localhost:8080/user HTTP/1.1 35 | content-type: application/json 36 | Authorization: Bearer {{token}} 37 | #### 38 | 39 | ## FindManyUsers 40 | GET http://localhost:8080/user/list-all HTTP/1.1 41 | content-type: application/json 42 | Authorization: Bearer {{token}} 43 | ### 44 | 45 | ## UpdateUserPassword 46 | PATCH http://localhost:8080/user/password HTTP/1.1 47 | content-type: application/json 48 | Authorization: Bearer {{token}} 49 | 50 | { 51 | "password": "123456789@", 52 | "old_password": "12345678@" 53 | } 54 | ### 55 | 56 | ## Login 57 | # @name login 58 | POST http://localhost:8080/auth/login HTTP/1.1 59 | content-type: application/json 60 | 61 | { 62 | "email": "john.doe2@email.com", 63 | "password": "12345678@" 64 | } 65 | ### 66 | @token = {{login.response.body.access_token}} 67 | 68 | ### Categories 69 | 70 | ## CreateCategory 71 | POST http://localhost:8080/category HTTP/1.1 72 | content-type: application/json 73 | Authorization: Bearer {{token}} 74 | 75 | { 76 | "title": "Samsung" 77 | } 78 | 79 | ### Products 80 | 81 | ## CreateProduct 82 | POST http://localhost:8080/product HTTP/1.1 83 | content-type: application/json 84 | Authorization: Bearer {{token}} 85 | 86 | { 87 | "title": "Samsung", 88 | "description": "wewewe", 89 | "categories": [], 90 | "price": 39900 91 | } 92 | 93 | ### 94 | 95 | ## UpdateProduct 96 | PATCH http://localhost:8080/product/37545729-e891-40b5-946c-8e7d55bd686b HTTP/1.1 97 | content-type: application/json 98 | Authorization: Bearer {{token}} 99 | 100 | { 101 | "categories": ["07145e70-2a8e-4f71-9165-f0d450afa524"] 102 | } 103 | 104 | ### 105 | 106 | ## DeleteProduct 107 | DELETE http://localhost:8080/product/f720e1ce-cb88-4f72-a765-0250c1a525e3 HTTP/1.1 108 | content-type: application/json 109 | Authorization: Bearer {{token}} 110 | 111 | ### 112 | 113 | ## FindManyProducts 114 | GET http://localhost:8080/product HTTP/1.1 115 | content-type: application/json 116 | Authorization: Bearer {{token}} 117 | 118 | { 119 | "categories": ["ee78a1ab-c441-4c67-8b62-74fa16797ace"] 120 | } -------------------------------------------------------------------------------- /internal/common/utils/decode_jwt.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/golang-jwt/jwt/v4" 9 | "github.com/wiliamvj/api-users-golang/config/env" 10 | ) 11 | 12 | type CurrentUser struct { 13 | ID string `json:"id"` 14 | Email string `json:"email"` 15 | Name string `json:"name"` 16 | Exp int64 `json:"exp,omitempty"` 17 | jwt.RegisteredClaims 18 | } 19 | 20 | func DecodeJwt(r *http.Request) (*CurrentUser, error) { 21 | authHeader := r.Header.Get("Authorization") 22 | parts := strings.Split(authHeader, " ") 23 | if len(parts) != 2 || parts[0] != "Bearer" { 24 | return nil, errors.New("invalid authorization header") 25 | } 26 | 27 | tokenString := parts[1] 28 | key := &env.Env.JwtSecret 29 | var userClaim CurrentUser 30 | 31 | _, err := jwt.ParseWithClaims(tokenString, &userClaim, func(token *jwt.Token) (interface{}, error) { 32 | return []byte(*key), nil 33 | }) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return &userClaim, nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/database/connection.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "log/slog" 6 | 7 | _ "github.com/lib/pq" 8 | "github.com/wiliamvj/api-users-golang/config/env" 9 | ) 10 | 11 | func NewDBConnection() (*sql.DB, error) { 12 | postgresURI := env.Env.DatabaseURL 13 | db, err := sql.Open("postgres", postgresURI) 14 | if err != nil { 15 | return nil, err 16 | } 17 | err = db.Ping() 18 | if err != nil { 19 | db.Close() 20 | return nil, err 21 | } 22 | slog.Info("database connected", slog.String("package", "database")) 23 | 24 | return db, nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/database/migrations/000001_init.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | -------------------------------------------------------------------------------- /internal/database/migrations/000001_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id VARCHAR(36) NOT NULL PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL, 4 | email VARCHAR(255) NOT NULL UNIQUE, 5 | password VARCHAR(255) NOT NULL, 6 | created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | updated_at TIMESTAMP(3) NOT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /internal/database/migrations/000002_init.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS address; -------------------------------------------------------------------------------- /internal/database/migrations/000002_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE address ( 2 | id VARCHAR(36) NOT NULL PRIMARY KEY, 3 | cep VARCHAR(255) NOT NULL, 4 | ibge VARCHAR(255) NOT NULL, 5 | uf VARCHAR(255) NOT NULL, 6 | city VARCHAR(255) NOT NULL, 7 | complement VARCHAR(255) NULL, 8 | street VARCHAR(255) NOT NULL, 9 | created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | updated_at TIMESTAMP(3) NOT NULL, 11 | user_id VARCHAR(36) UNIQUE NOT NULL, 12 | 13 | FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 14 | ); 15 | -------------------------------------------------------------------------------- /internal/database/migrations/000003_init.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS product_category; 2 | DROP TABLE IF EXISTS product; 3 | DROP TABLE IF EXISTS category; 4 | -------------------------------------------------------------------------------- /internal/database/migrations/000003_init.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE category ( 2 | id VARCHAR(36) NOT NULL PRIMARY KEY, 3 | title VARCHAR(255) NOT NULL, 4 | created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | updated_at TIMESTAMP(3) NOT NULL 6 | ); 7 | 8 | CREATE TABLE product ( 9 | id VARCHAR(36) NOT NULL PRIMARY KEY, 10 | title VARCHAR(255) NOT NULL, 11 | price INTEGER NOT NULL, 12 | description TEXT NULL, 13 | created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | updated_at TIMESTAMP(3) NOT NULL 15 | ); 16 | 17 | CREATE TABLE product_category ( 18 | id VARCHAR(36) NOT NULL PRIMARY KEY, 19 | product_id VARCHAR(36) NOT NULL, 20 | category_id VARCHAR(36) NOT NULL, 21 | created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 22 | updated_at TIMESTAMP(3) NOT NULL, 23 | FOREIGN KEY (product_id) REFERENCES product(id) ON DELETE CASCADE, 24 | FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE, 25 | UNIQUE (product_id, category_id) 26 | ); 27 | -------------------------------------------------------------------------------- /internal/database/queries/categories.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateCategory :exec 2 | INSERT INTO category (id, title, created_at, updated_at) 3 | VALUES ($1, $2, $3, $4); 4 | -------------------------------------------------------------------------------- /internal/database/queries/products.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateProduct :exec 2 | INSERT INTO product (id, title, description, price, created_at, updated_at) 3 | VALUES ($1, $2, $3, $4, $5, $6); 4 | 5 | -- name: CreateProductCategory :exec 6 | INSERT INTO product_category (id, product_id, category_id, created_at, updated_at) 7 | VALUES ($1, $2, $3, $4, $5); 8 | 9 | -- name: GetCategoryByID :one 10 | SELECT EXISTS (SELECT 1 FROM category WHERE id = $1) AS category_exists; 11 | 12 | -- name: GetProductByID :one 13 | SELECT EXISTS (SELECT 1 FROM product WHERE id = $1) AS product_exists; 14 | 15 | -- name: UpdateProduct :exec 16 | UPDATE product 17 | SET 18 | title = COALESCE(sqlc.narg('title'), title), 19 | description = COALESCE(sqlc.narg('description'), description), 20 | price = COALESCE(sqlc.narg('price'), price), 21 | updated_at = $2 22 | WHERE id = $1; 23 | 24 | -- name: GetCategoriesByProductID :many 25 | SELECT pc.category_id FROM product_category pc WHERE pc.product_id = $1; 26 | 27 | -- name: DeleteProductCategory :exec 28 | DELETE FROM product_category WHERE product_id = $1 AND category_id = $2; 29 | 30 | -- name: DeleteProduct :exec 31 | DELETE FROM product WHERE id = $1; 32 | 33 | -- name: FindManyProducts :many 34 | SELECT 35 | p.id, 36 | p.title, 37 | p.description, 38 | p.price, 39 | p.created_at 40 | FROM product p 41 | JOIN product_category pc ON pc.product_id = p.id 42 | WHERE 43 | (pc.category_id = ANY(@categories::TEXT[]) OR @categories::TEXT[] IS NULL) 44 | AND ( 45 | p.title ILIKE '%' || COALESCE(@search, sqlc.narg('search'), '') || '%' 46 | OR 47 | p.description ILIKE '%' || COALESCE(@search, sqlc.narg('search'), '') || '%' 48 | ) 49 | ORDER BY p.created_at DESC; 50 | 51 | -- name: GetProductCategories :many 52 | SELECT c.id, c.title FROM category c 53 | JOIN product_category pc ON pc.category_id = c.id 54 | WHERE pc.product_id = $1; 55 | -------------------------------------------------------------------------------- /internal/database/queries/users.sql: -------------------------------------------------------------------------------- 1 | 2 | -- name: CreateUser :exec 3 | INSERT INTO users (id, name, email, password, created_at, updated_at) 4 | VALUES ($1, $2, $3, $4, $5, $6); 5 | 6 | -- name: FindUserByEmail :one 7 | SELECT u.id, u.name, u.email FROM users u WHERE u.email = $1; 8 | 9 | -- name: FindUserByID :one 10 | SELECT u.id, u.name, u.email, u.created_At, u.updated_at, a.cep, a.uf, a.city, a.complement, a.street 11 | FROM users u 12 | JOIN address a ON a.user_id = u.id 13 | WHERE u.id = $1; 14 | 15 | -- name: UpdateUser :exec 16 | UPDATE users SET 17 | name = COALESCE(sqlc.narg('name'), name), 18 | email = COALESCE(sqlc.narg('email'), email), 19 | updated_at = $2 20 | WHERE id = $1; 21 | 22 | -- name: DeleteUser :exec 23 | DELETE FROM users WHERE id = $1; 24 | 25 | -- name: FindManyUsers :many 26 | SELECT u.id, u.name, u.email, u.created_At, u.updated_at, a.cep, a.uf, a.city, a.complement, a.street 27 | FROM users u 28 | JOIN address a ON a.user_id = u.id 29 | ORDER BY u.created_at DESC; 30 | 31 | -- name: UpdatePassword :exec 32 | UPDATE users SET password = $2, updated_at = $3 WHERE id = $1; 33 | 34 | -- name: GetUserPassword :one 35 | SELECT u.password FROM users u WHERE u.id = $1; 36 | 37 | -- name: CreateUserAddress :exec 38 | INSERT INTO address (id, user_id, cep, ibge, uf, city, complement, street, created_at, updated_at) 39 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10); 40 | 41 | -- name: UpdateUserAddress :exec 42 | UPDATE address SET 43 | cep = COALESCE(sqlc.narg('cep'), cep), 44 | ibge = COALESCE(sqlc.narg('ibge'), ibge), 45 | uf = COALESCE(sqlc.narg('uf'), uf), 46 | city = COALESCE(sqlc.narg('city'), city), 47 | complement = COALESCE(sqlc.narg('complement'), complement), 48 | street = COALESCE(sqlc.narg('street'), street), 49 | updated_at = $2 50 | WHERE user_id = $1; -------------------------------------------------------------------------------- /internal/database/sqlc/categories.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.20.0 4 | // source: categories.sql 5 | 6 | package sqlc 7 | 8 | import ( 9 | "context" 10 | "time" 11 | ) 12 | 13 | const createCategory = `-- name: CreateCategory :exec 14 | INSERT INTO category (id, title, created_at, updated_at) 15 | VALUES ($1, $2, $3, $4) 16 | ` 17 | 18 | type CreateCategoryParams struct { 19 | ID string 20 | Title string 21 | CreatedAt time.Time 22 | UpdatedAt time.Time 23 | } 24 | 25 | func (q *Queries) CreateCategory(ctx context.Context, arg CreateCategoryParams) error { 26 | _, err := q.db.ExecContext(ctx, createCategory, 27 | arg.ID, 28 | arg.Title, 29 | arg.CreatedAt, 30 | arg.UpdatedAt, 31 | ) 32 | return err 33 | } 34 | -------------------------------------------------------------------------------- /internal/database/sqlc/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.20.0 4 | 5 | package sqlc 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/database/sqlc/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.20.0 4 | 5 | package sqlc 6 | 7 | import ( 8 | "database/sql" 9 | "time" 10 | ) 11 | 12 | type Address struct { 13 | ID string 14 | Cep string 15 | Ibge string 16 | Uf string 17 | City string 18 | Complement sql.NullString 19 | Street string 20 | CreatedAt time.Time 21 | UpdatedAt time.Time 22 | UserID string 23 | } 24 | 25 | type Category struct { 26 | ID string 27 | Title string 28 | CreatedAt time.Time 29 | UpdatedAt time.Time 30 | } 31 | 32 | type Product struct { 33 | ID string 34 | Title string 35 | Price int32 36 | Description sql.NullString 37 | CreatedAt time.Time 38 | UpdatedAt time.Time 39 | } 40 | 41 | type ProductCategory struct { 42 | ID string 43 | ProductID string 44 | CategoryID string 45 | CreatedAt time.Time 46 | UpdatedAt time.Time 47 | } 48 | 49 | type User struct { 50 | ID string 51 | Name string 52 | Email string 53 | Password string 54 | CreatedAt time.Time 55 | UpdatedAt time.Time 56 | } 57 | -------------------------------------------------------------------------------- /internal/database/sqlc/products.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.20.0 4 | // source: products.sql 5 | 6 | package sqlc 7 | 8 | import ( 9 | "context" 10 | "database/sql" 11 | "time" 12 | 13 | "github.com/lib/pq" 14 | ) 15 | 16 | const createProduct = `-- name: CreateProduct :exec 17 | INSERT INTO product (id, title, description, price, created_at, updated_at) 18 | VALUES ($1, $2, $3, $4, $5, $6) 19 | ` 20 | 21 | type CreateProductParams struct { 22 | ID string 23 | Title string 24 | Description sql.NullString 25 | Price int32 26 | CreatedAt time.Time 27 | UpdatedAt time.Time 28 | } 29 | 30 | func (q *Queries) CreateProduct(ctx context.Context, arg CreateProductParams) error { 31 | _, err := q.db.ExecContext(ctx, createProduct, 32 | arg.ID, 33 | arg.Title, 34 | arg.Description, 35 | arg.Price, 36 | arg.CreatedAt, 37 | arg.UpdatedAt, 38 | ) 39 | return err 40 | } 41 | 42 | const createProductCategory = `-- name: CreateProductCategory :exec 43 | INSERT INTO product_category (id, product_id, category_id, created_at, updated_at) 44 | VALUES ($1, $2, $3, $4, $5) 45 | ` 46 | 47 | type CreateProductCategoryParams struct { 48 | ID string 49 | ProductID string 50 | CategoryID string 51 | CreatedAt time.Time 52 | UpdatedAt time.Time 53 | } 54 | 55 | func (q *Queries) CreateProductCategory(ctx context.Context, arg CreateProductCategoryParams) error { 56 | _, err := q.db.ExecContext(ctx, createProductCategory, 57 | arg.ID, 58 | arg.ProductID, 59 | arg.CategoryID, 60 | arg.CreatedAt, 61 | arg.UpdatedAt, 62 | ) 63 | return err 64 | } 65 | 66 | const deleteProduct = `-- name: DeleteProduct :exec 67 | DELETE FROM product WHERE id = $1 68 | ` 69 | 70 | func (q *Queries) DeleteProduct(ctx context.Context, id string) error { 71 | _, err := q.db.ExecContext(ctx, deleteProduct, id) 72 | return err 73 | } 74 | 75 | const deleteProductCategory = `-- name: DeleteProductCategory :exec 76 | DELETE FROM product_category WHERE product_id = $1 AND category_id = $2 77 | ` 78 | 79 | type DeleteProductCategoryParams struct { 80 | ProductID string 81 | CategoryID string 82 | } 83 | 84 | func (q *Queries) DeleteProductCategory(ctx context.Context, arg DeleteProductCategoryParams) error { 85 | _, err := q.db.ExecContext(ctx, deleteProductCategory, arg.ProductID, arg.CategoryID) 86 | return err 87 | } 88 | 89 | const findManyProducts = `-- name: FindManyProducts :many 90 | SELECT 91 | p.id, 92 | p.title, 93 | p.description, 94 | p.price, 95 | p.created_at 96 | FROM product p 97 | JOIN product_category pc ON pc.product_id = p.id 98 | WHERE 99 | (pc.category_id = ANY($1::TEXT[]) OR $1::TEXT[] IS NULL) 100 | AND ( 101 | p.title ILIKE '%' || COALESCE($2, $2, '') || '%' 102 | OR 103 | p.description ILIKE '%' || COALESCE($2, $2, '') || '%' 104 | ) 105 | ORDER BY p.created_at DESC 106 | ` 107 | 108 | type FindManyProductsParams struct { 109 | Categories []string 110 | Search sql.NullString 111 | } 112 | 113 | type FindManyProductsRow struct { 114 | ID string 115 | Title string 116 | Description sql.NullString 117 | Price int32 118 | CreatedAt time.Time 119 | } 120 | 121 | func (q *Queries) FindManyProducts(ctx context.Context, arg FindManyProductsParams) ([]FindManyProductsRow, error) { 122 | rows, err := q.db.QueryContext(ctx, findManyProducts, pq.Array(arg.Categories), arg.Search) 123 | if err != nil { 124 | return nil, err 125 | } 126 | defer rows.Close() 127 | var items []FindManyProductsRow 128 | for rows.Next() { 129 | var i FindManyProductsRow 130 | if err := rows.Scan( 131 | &i.ID, 132 | &i.Title, 133 | &i.Description, 134 | &i.Price, 135 | &i.CreatedAt, 136 | ); err != nil { 137 | return nil, err 138 | } 139 | items = append(items, i) 140 | } 141 | if err := rows.Close(); err != nil { 142 | return nil, err 143 | } 144 | if err := rows.Err(); err != nil { 145 | return nil, err 146 | } 147 | return items, nil 148 | } 149 | 150 | const getCategoriesByProductID = `-- name: GetCategoriesByProductID :many 151 | SELECT pc.category_id FROM product_category pc WHERE pc.product_id = $1 152 | ` 153 | 154 | func (q *Queries) GetCategoriesByProductID(ctx context.Context, productID string) ([]string, error) { 155 | rows, err := q.db.QueryContext(ctx, getCategoriesByProductID, productID) 156 | if err != nil { 157 | return nil, err 158 | } 159 | defer rows.Close() 160 | var items []string 161 | for rows.Next() { 162 | var category_id string 163 | if err := rows.Scan(&category_id); err != nil { 164 | return nil, err 165 | } 166 | items = append(items, category_id) 167 | } 168 | if err := rows.Close(); err != nil { 169 | return nil, err 170 | } 171 | if err := rows.Err(); err != nil { 172 | return nil, err 173 | } 174 | return items, nil 175 | } 176 | 177 | const getCategoryByID = `-- name: GetCategoryByID :one 178 | SELECT EXISTS (SELECT 1 FROM category WHERE id = $1) AS category_exists 179 | ` 180 | 181 | func (q *Queries) GetCategoryByID(ctx context.Context, id string) (bool, error) { 182 | row := q.db.QueryRowContext(ctx, getCategoryByID, id) 183 | var category_exists bool 184 | err := row.Scan(&category_exists) 185 | return category_exists, err 186 | } 187 | 188 | const getProductByID = `-- name: GetProductByID :one 189 | SELECT EXISTS (SELECT 1 FROM product WHERE id = $1) AS product_exists 190 | ` 191 | 192 | func (q *Queries) GetProductByID(ctx context.Context, id string) (bool, error) { 193 | row := q.db.QueryRowContext(ctx, getProductByID, id) 194 | var product_exists bool 195 | err := row.Scan(&product_exists) 196 | return product_exists, err 197 | } 198 | 199 | const getProductCategories = `-- name: GetProductCategories :many 200 | SELECT c.id, c.title FROM category c 201 | JOIN product_category pc ON pc.category_id = c.id 202 | WHERE pc.product_id = $1 203 | ` 204 | 205 | type GetProductCategoriesRow struct { 206 | ID string 207 | Title string 208 | } 209 | 210 | func (q *Queries) GetProductCategories(ctx context.Context, productID string) ([]GetProductCategoriesRow, error) { 211 | rows, err := q.db.QueryContext(ctx, getProductCategories, productID) 212 | if err != nil { 213 | return nil, err 214 | } 215 | defer rows.Close() 216 | var items []GetProductCategoriesRow 217 | for rows.Next() { 218 | var i GetProductCategoriesRow 219 | if err := rows.Scan(&i.ID, &i.Title); err != nil { 220 | return nil, err 221 | } 222 | items = append(items, i) 223 | } 224 | if err := rows.Close(); err != nil { 225 | return nil, err 226 | } 227 | if err := rows.Err(); err != nil { 228 | return nil, err 229 | } 230 | return items, nil 231 | } 232 | 233 | const updateProduct = `-- name: UpdateProduct :exec 234 | UPDATE product 235 | SET 236 | title = COALESCE($3, title), 237 | description = COALESCE($4, description), 238 | price = COALESCE($5, price), 239 | updated_at = $2 240 | WHERE id = $1 241 | ` 242 | 243 | type UpdateProductParams struct { 244 | ID string 245 | UpdatedAt time.Time 246 | Title sql.NullString 247 | Description sql.NullString 248 | Price sql.NullInt32 249 | } 250 | 251 | func (q *Queries) UpdateProduct(ctx context.Context, arg UpdateProductParams) error { 252 | _, err := q.db.ExecContext(ctx, updateProduct, 253 | arg.ID, 254 | arg.UpdatedAt, 255 | arg.Title, 256 | arg.Description, 257 | arg.Price, 258 | ) 259 | return err 260 | } 261 | -------------------------------------------------------------------------------- /internal/database/sqlc/users.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.20.0 4 | // source: users.sql 5 | 6 | package sqlc 7 | 8 | import ( 9 | "context" 10 | "database/sql" 11 | "time" 12 | ) 13 | 14 | const createUser = `-- name: CreateUser :exec 15 | INSERT INTO users (id, name, email, password, created_at, updated_at) 16 | VALUES ($1, $2, $3, $4, $5, $6) 17 | ` 18 | 19 | type CreateUserParams struct { 20 | ID string 21 | Name string 22 | Email string 23 | Password string 24 | CreatedAt time.Time 25 | UpdatedAt time.Time 26 | } 27 | 28 | func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error { 29 | _, err := q.db.ExecContext(ctx, createUser, 30 | arg.ID, 31 | arg.Name, 32 | arg.Email, 33 | arg.Password, 34 | arg.CreatedAt, 35 | arg.UpdatedAt, 36 | ) 37 | return err 38 | } 39 | 40 | const createUserAddress = `-- name: CreateUserAddress :exec 41 | INSERT INTO address (id, user_id, cep, ibge, uf, city, complement, street, created_at, updated_at) 42 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) 43 | ` 44 | 45 | type CreateUserAddressParams struct { 46 | ID string 47 | UserID string 48 | Cep string 49 | Ibge string 50 | Uf string 51 | City string 52 | Complement sql.NullString 53 | Street string 54 | CreatedAt time.Time 55 | UpdatedAt time.Time 56 | } 57 | 58 | func (q *Queries) CreateUserAddress(ctx context.Context, arg CreateUserAddressParams) error { 59 | _, err := q.db.ExecContext(ctx, createUserAddress, 60 | arg.ID, 61 | arg.UserID, 62 | arg.Cep, 63 | arg.Ibge, 64 | arg.Uf, 65 | arg.City, 66 | arg.Complement, 67 | arg.Street, 68 | arg.CreatedAt, 69 | arg.UpdatedAt, 70 | ) 71 | return err 72 | } 73 | 74 | const deleteUser = `-- name: DeleteUser :exec 75 | DELETE FROM users WHERE id = $1 76 | ` 77 | 78 | func (q *Queries) DeleteUser(ctx context.Context, id string) error { 79 | _, err := q.db.ExecContext(ctx, deleteUser, id) 80 | return err 81 | } 82 | 83 | const findManyUsers = `-- name: FindManyUsers :many 84 | SELECT u.id, u.name, u.email, u.created_At, u.updated_at, a.cep, a.uf, a.city, a.complement, a.street 85 | FROM users u 86 | JOIN address a ON a.user_id = u.id 87 | ORDER BY u.created_at DESC 88 | ` 89 | 90 | type FindManyUsersRow struct { 91 | ID string 92 | Name string 93 | Email string 94 | CreatedAt time.Time 95 | UpdatedAt time.Time 96 | Cep string 97 | Uf string 98 | City string 99 | Complement sql.NullString 100 | Street string 101 | } 102 | 103 | func (q *Queries) FindManyUsers(ctx context.Context) ([]FindManyUsersRow, error) { 104 | rows, err := q.db.QueryContext(ctx, findManyUsers) 105 | if err != nil { 106 | return nil, err 107 | } 108 | defer rows.Close() 109 | var items []FindManyUsersRow 110 | for rows.Next() { 111 | var i FindManyUsersRow 112 | if err := rows.Scan( 113 | &i.ID, 114 | &i.Name, 115 | &i.Email, 116 | &i.CreatedAt, 117 | &i.UpdatedAt, 118 | &i.Cep, 119 | &i.Uf, 120 | &i.City, 121 | &i.Complement, 122 | &i.Street, 123 | ); err != nil { 124 | return nil, err 125 | } 126 | items = append(items, i) 127 | } 128 | if err := rows.Close(); err != nil { 129 | return nil, err 130 | } 131 | if err := rows.Err(); err != nil { 132 | return nil, err 133 | } 134 | return items, nil 135 | } 136 | 137 | const findUserByEmail = `-- name: FindUserByEmail :one 138 | SELECT u.id, u.name, u.email FROM users u WHERE u.email = $1 139 | ` 140 | 141 | type FindUserByEmailRow struct { 142 | ID string 143 | Name string 144 | Email string 145 | } 146 | 147 | func (q *Queries) FindUserByEmail(ctx context.Context, email string) (FindUserByEmailRow, error) { 148 | row := q.db.QueryRowContext(ctx, findUserByEmail, email) 149 | var i FindUserByEmailRow 150 | err := row.Scan(&i.ID, &i.Name, &i.Email) 151 | return i, err 152 | } 153 | 154 | const findUserByID = `-- name: FindUserByID :one 155 | SELECT u.id, u.name, u.email, u.created_At, u.updated_at, a.cep, a.uf, a.city, a.complement, a.street 156 | FROM users u 157 | JOIN address a ON a.user_id = u.id 158 | WHERE u.id = $1 159 | ` 160 | 161 | type FindUserByIDRow struct { 162 | ID string 163 | Name string 164 | Email string 165 | CreatedAt time.Time 166 | UpdatedAt time.Time 167 | Cep string 168 | Uf string 169 | City string 170 | Complement sql.NullString 171 | Street string 172 | } 173 | 174 | func (q *Queries) FindUserByID(ctx context.Context, id string) (FindUserByIDRow, error) { 175 | row := q.db.QueryRowContext(ctx, findUserByID, id) 176 | var i FindUserByIDRow 177 | err := row.Scan( 178 | &i.ID, 179 | &i.Name, 180 | &i.Email, 181 | &i.CreatedAt, 182 | &i.UpdatedAt, 183 | &i.Cep, 184 | &i.Uf, 185 | &i.City, 186 | &i.Complement, 187 | &i.Street, 188 | ) 189 | return i, err 190 | } 191 | 192 | const getUserPassword = `-- name: GetUserPassword :one 193 | SELECT u.password FROM users u WHERE u.id = $1 194 | ` 195 | 196 | func (q *Queries) GetUserPassword(ctx context.Context, id string) (string, error) { 197 | row := q.db.QueryRowContext(ctx, getUserPassword, id) 198 | var password string 199 | err := row.Scan(&password) 200 | return password, err 201 | } 202 | 203 | const updatePassword = `-- name: UpdatePassword :exec 204 | UPDATE users SET password = $2, updated_at = $3 WHERE id = $1 205 | ` 206 | 207 | type UpdatePasswordParams struct { 208 | ID string 209 | Password string 210 | UpdatedAt time.Time 211 | } 212 | 213 | func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error { 214 | _, err := q.db.ExecContext(ctx, updatePassword, arg.ID, arg.Password, arg.UpdatedAt) 215 | return err 216 | } 217 | 218 | const updateUser = `-- name: UpdateUser :exec 219 | UPDATE users SET 220 | name = COALESCE($3, name), 221 | email = COALESCE($4, email), 222 | updated_at = $2 223 | WHERE id = $1 224 | ` 225 | 226 | type UpdateUserParams struct { 227 | ID string 228 | UpdatedAt time.Time 229 | Name sql.NullString 230 | Email sql.NullString 231 | } 232 | 233 | func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { 234 | _, err := q.db.ExecContext(ctx, updateUser, 235 | arg.ID, 236 | arg.UpdatedAt, 237 | arg.Name, 238 | arg.Email, 239 | ) 240 | return err 241 | } 242 | 243 | const updateUserAddress = `-- name: UpdateUserAddress :exec 244 | UPDATE address SET 245 | cep = COALESCE($3, cep), 246 | ibge = COALESCE($4, ibge), 247 | uf = COALESCE($5, uf), 248 | city = COALESCE($6, city), 249 | complement = COALESCE($7, complement), 250 | street = COALESCE($8, street), 251 | updated_at = $2 252 | WHERE user_id = $1 253 | ` 254 | 255 | type UpdateUserAddressParams struct { 256 | UserID string 257 | UpdatedAt time.Time 258 | Cep sql.NullString 259 | Ibge sql.NullString 260 | Uf sql.NullString 261 | City sql.NullString 262 | Complement sql.NullString 263 | Street sql.NullString 264 | } 265 | 266 | func (q *Queries) UpdateUserAddress(ctx context.Context, arg UpdateUserAddressParams) error { 267 | _, err := q.db.ExecContext(ctx, updateUserAddress, 268 | arg.UserID, 269 | arg.UpdatedAt, 270 | arg.Cep, 271 | arg.Ibge, 272 | arg.Uf, 273 | arg.City, 274 | arg.Complement, 275 | arg.Street, 276 | ) 277 | return err 278 | } 279 | -------------------------------------------------------------------------------- /internal/dto/category_dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type CreateCategoryDto struct { 4 | Title string `json:"title" validate:"required,min=3,max=30"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/dto/product_dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type CreateProductDto struct { 4 | Title string `json:"title" validate:"required,min=3,max=40"` 5 | Price int32 `json:"price" validate:"required,min=1"` 6 | Categories []string `json:"categories" validate:"required,min=1,dive,uuid4"` 7 | Description string `json:"description" validate:"required,min=3,max=500"` 8 | } 9 | 10 | type UpdateProductDto struct { 11 | Title string `json:"title" validate:"omitempty,min=3,max=40"` 12 | Price int32 `json:"price" validate:"omitempty,min=1"` 13 | Categories []string `json:"categories" validate:"omitempty,min=1,dive,uuid4"` 14 | Description string `json:"description" validate:"omitempty,min=3,max=500"` 15 | } 16 | 17 | type FindProductDto struct { 18 | Search string `json:"search" validate:"omitempty,min=2,max=40"` 19 | Categories []string `json:"categories" validate:"omitempty,min=1,dive,uuid4"` 20 | } 21 | -------------------------------------------------------------------------------- /internal/dto/user_dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type CreateUserDto struct { 4 | Name string `json:"name" validate:"required,min=3,max=30"` 5 | Email string `json:"email" validate:"required,email"` 6 | Password string `json:"password" validate:"required,min=8,max=30,containsany=!@#$%*"` 7 | CEP string `json:"cep" validate:"required,min=8,max=8"` 8 | } 9 | 10 | type UpdateUserDto struct { 11 | Name string `json:"name" validate:"omitempty,min=3,max=30"` 12 | Email string `json:"email" validate:"omitempty,email"` 13 | CEP string `json:"cep" validate:"omitempty,min=8,max=8"` 14 | } 15 | 16 | type UpdateUserPasswordDto struct { 17 | Password string `json:"password" validate:"required,min=8,max=30,containsany=!@#$%*"` 18 | OldPassword string `json:"old_password" validate:"required,min=8,max=30,containsany=!@#$%*"` 19 | } 20 | 21 | type LoginDTO struct { 22 | Email string `json:"email" validate:"required,email"` 23 | Password string `json:"password" validate:"required,min=8,max=40"` 24 | } 25 | -------------------------------------------------------------------------------- /internal/entity/category_entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "time" 4 | 5 | type CategoryEntity struct { 6 | ID string `json:"id"` 7 | Title string `json:"title"` 8 | CreatedAt time.Time `json:"created_at"` 9 | UpdatedAt time.Time `json:"updated_at"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/entity/product_entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "time" 4 | 5 | type ProductEntity struct { 6 | ID string `json:"id"` 7 | Title string `json:"title"` 8 | Price int32 `json:"price"` 9 | Categories []string `json:"categories"` 10 | Description string `json:"description"` 11 | CreatedAt time.Time `json:"created_at"` 12 | UpdatedAt time.Time `json:"updated_at"` 13 | } 14 | 15 | type ProductCategoryEntity struct { 16 | ID string `json:"id"` 17 | ProductID string `json:"product_id"` 18 | CategoryID string `json:"category_id"` 19 | CreatedAt time.Time `json:"created_at"` 20 | UpdatedAt time.Time `json:"updated_at"` 21 | } 22 | 23 | type ProductWithCategoryEntity struct { 24 | ID string `json:"id"` 25 | Title string `json:"title"` 26 | Price int32 `json:"price"` 27 | Description string `json:"description"` 28 | Categories []CategoryEntity `json:"categories"` 29 | CreatedAt time.Time `json:"created_at"` 30 | } 31 | -------------------------------------------------------------------------------- /internal/entity/user_entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "time" 4 | 5 | type UserEntity struct { 6 | ID string `json:"id"` 7 | Name string `json:"name"` 8 | Email string `json:"email"` 9 | Password string `json:"password,omitempty"` 10 | Address UserAddress `json:"address,omitempty"` 11 | CreatedAt time.Time `json:"created_at"` 12 | UpdatedAt time.Time `json:"updated_at"` 13 | } 14 | 15 | type UserAddress struct { 16 | CEP string `json:"cep"` 17 | IBGE string `json:"ibge"` 18 | UF string `json:"uf"` 19 | City string `json:"city"` 20 | Complement string `json:"complement,omitempty"` 21 | Street string `json:"street"` 22 | } 23 | -------------------------------------------------------------------------------- /internal/handler/auth_handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | 9 | "github.com/wiliamvj/api-users-golang/internal/dto" 10 | "github.com/wiliamvj/api-users-golang/internal/handler/httperr" 11 | "github.com/wiliamvj/api-users-golang/internal/handler/validation" 12 | ) 13 | 14 | func (h *handler) Login(w http.ResponseWriter, r *http.Request) { 15 | if r.Body == http.NoBody { 16 | slog.Error("body is empty", slog.String("package", "userhandler")) 17 | w.WriteHeader(http.StatusBadRequest) 18 | msg := httperr.NewBadRequestError("body is required") 19 | json.NewEncoder(w).Encode(msg) 20 | return 21 | } 22 | var req dto.LoginDTO 23 | if r.Body != nil { 24 | err := json.NewDecoder(r.Body).Decode(&req) 25 | if err != nil { 26 | slog.Error("error to decode body", err, slog.String("package", "userhandler")) 27 | w.WriteHeader(http.StatusBadRequest) 28 | msg := httperr.NewBadRequestError("error to decode body") 29 | json.NewEncoder(w).Encode(msg) 30 | return 31 | } 32 | } 33 | httpErr := validation.ValidateHttpData(req) 34 | if httpErr != nil { 35 | slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "userhandler")) 36 | w.WriteHeader(httpErr.Code) 37 | json.NewEncoder(w).Encode(httpErr) 38 | return 39 | } 40 | token, err := h.userService.Login(r.Context(), req) 41 | if err != nil { 42 | if err.Error() == "user not found" || err.Error() == "invalid password" { 43 | w.WriteHeader(http.StatusUnauthorized) 44 | msg := httperr.NewUnauthorizedRequestError("invalid credentials") 45 | json.NewEncoder(w).Encode(msg) 46 | return 47 | } 48 | w.WriteHeader(http.StatusBadRequest) 49 | msg := httperr.NewBadRequestError(err.Error()) 50 | json.NewEncoder(w).Encode(msg) 51 | return 52 | } 53 | w.Header().Set("Content-Type", "application/json") 54 | w.WriteHeader(http.StatusOK) 55 | json.NewEncoder(w).Encode(token) 56 | } 57 | -------------------------------------------------------------------------------- /internal/handler/category_handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | 9 | "github.com/wiliamvj/api-users-golang/internal/dto" 10 | "github.com/wiliamvj/api-users-golang/internal/handler/httperr" 11 | "github.com/wiliamvj/api-users-golang/internal/handler/validation" 12 | ) 13 | 14 | // Create category 15 | // @Summary Create new category 16 | // @Description Endpoint for create category 17 | // @Tags category 18 | // @Accept json 19 | // @Produce json 20 | // @Param body body dto.CreateCategoryDto true "Create category dto" true 21 | // @Success 200 22 | // @Failure 400 {object} httperr.RestErr 23 | // @Failure 500 {object} httperr.RestErr 24 | // @Router /category [post] 25 | func (h *handler) CreateCategory(w http.ResponseWriter, r *http.Request) { 26 | var req dto.CreateCategoryDto 27 | 28 | if r.Body == http.NoBody { 29 | slog.Error("body is empty", slog.String("package", "categoryhandler")) 30 | w.WriteHeader(http.StatusBadRequest) 31 | msg := httperr.NewBadRequestError("body is required") 32 | json.NewEncoder(w).Encode(msg) 33 | return 34 | } 35 | err := json.NewDecoder(r.Body).Decode(&req) 36 | if err != nil { 37 | slog.Error("error to decode body", "err", err, slog.String("package", "categoryhandler")) 38 | w.WriteHeader(http.StatusBadRequest) 39 | msg := httperr.NewBadRequestError("error to decode body") 40 | json.NewEncoder(w).Encode(msg) 41 | return 42 | } 43 | httpErr := validation.ValidateHttpData(req) 44 | if httpErr != nil { 45 | slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "categoryhandler")) 46 | w.WriteHeader(httpErr.Code) 47 | json.NewEncoder(w).Encode(httpErr) 48 | return 49 | } 50 | err = h.categoryService.CreateCategory(r.Context(), req) 51 | if err != nil { 52 | slog.Error(fmt.Sprintf("error to create category: %v", err), slog.String("package", "categoryhandler")) 53 | w.WriteHeader(http.StatusBadRequest) 54 | } 55 | w.WriteHeader(http.StatusCreated) 56 | } 57 | -------------------------------------------------------------------------------- /internal/handler/httperr/httperr.go: -------------------------------------------------------------------------------- 1 | package httperr 2 | 3 | import "net/http" 4 | 5 | type RestErr struct { 6 | Message string `json:"message"` 7 | Err string `json:"error,omitempty"` 8 | Code int `json:"code"` 9 | Fields []Fields `json:"fields,omitempty"` 10 | } 11 | 12 | type Fields struct { 13 | Field string `json:"field"` 14 | Value interface{} `json:"value,omitempty"` 15 | Message string `json:"message"` 16 | } 17 | 18 | func (r *RestErr) Error() string { 19 | return r.Message 20 | } 21 | 22 | func NewRestErr(m, e string, c int, f []Fields) *RestErr { 23 | return &RestErr{ 24 | Message: m, 25 | Err: e, 26 | Code: c, 27 | Fields: f, 28 | } 29 | } 30 | 31 | func NewBadRequestError(message string) *RestErr { 32 | return &RestErr{ 33 | Message: message, 34 | Err: "bad_request", 35 | Code: http.StatusBadRequest, 36 | } 37 | } 38 | 39 | func NewUnauthorizedRequestError(message string) *RestErr { 40 | return &RestErr{ 41 | Message: message, 42 | Err: "unauthorized", 43 | Code: http.StatusUnauthorized, 44 | } 45 | } 46 | 47 | func NewBadRequestValidationError(m string, c []Fields) *RestErr { 48 | return &RestErr{ 49 | Message: m, 50 | Err: "bad_request", 51 | Code: http.StatusBadRequest, 52 | Fields: c, 53 | } 54 | } 55 | 56 | func NewInternalServerError(message string) *RestErr { 57 | return &RestErr{ 58 | Message: message, 59 | Err: "internal_server_error", 60 | Code: http.StatusInternalServerError, 61 | } 62 | } 63 | 64 | func NewNotFoundError(message string) *RestErr { 65 | return &RestErr{ 66 | Message: message, 67 | Err: "not_found", 68 | Code: http.StatusNotFound, 69 | } 70 | } 71 | 72 | func NewForbiddenError(message string) *RestErr { 73 | return &RestErr{ 74 | Message: message, 75 | Err: "forbidden", 76 | Code: http.StatusForbidden, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/handler/interface_handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/wiliamvj/api-users-golang/internal/service/categoryservice" 7 | "github.com/wiliamvj/api-users-golang/internal/service/productservice" 8 | "github.com/wiliamvj/api-users-golang/internal/service/userservice" 9 | ) 10 | 11 | func NewHandler(userService userservice.UserService, 12 | categoryService categoryservice.CategoryService, 13 | productservice productservice.ProductService) Handler { 14 | return &handler{ 15 | userService: userService, 16 | categoryService: categoryService, 17 | productservice: productservice, 18 | } 19 | } 20 | 21 | type handler struct { 22 | userService userservice.UserService 23 | categoryService categoryservice.CategoryService 24 | productservice productservice.ProductService 25 | } 26 | 27 | type Handler interface { 28 | CreateUser(w http.ResponseWriter, r *http.Request) 29 | UpdateUser(w http.ResponseWriter, r *http.Request) 30 | GetUserByID(w http.ResponseWriter, r *http.Request) 31 | DeleteUser(w http.ResponseWriter, r *http.Request) 32 | FindManyUsers(w http.ResponseWriter, r *http.Request) 33 | UpdateUserPassword(w http.ResponseWriter, r *http.Request) 34 | Login(w http.ResponseWriter, r *http.Request) 35 | 36 | CreateCategory(w http.ResponseWriter, r *http.Request) 37 | 38 | CreateProduct(w http.ResponseWriter, r *http.Request) 39 | UpdateProduct(w http.ResponseWriter, r *http.Request) 40 | DeleteProduct(w http.ResponseWriter, r *http.Request) 41 | FindManyProducts(w http.ResponseWriter, r *http.Request) 42 | } 43 | -------------------------------------------------------------------------------- /internal/handler/middleware/logger_middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "log/slog" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/wiliamvj/api-users-golang/internal/common/utils" 12 | ) 13 | 14 | var sensitiveKeywords = []string{"password"} 15 | 16 | func hasSensitiveData(body map[string]interface{}) bool { 17 | for key := range body { 18 | for _, keyword := range sensitiveKeywords { 19 | if value, ok := body[key].(string); ok { 20 | if strings.Contains(strings.ToLower(key), keyword) || strings.Contains(strings.ToLower(value), keyword) { 21 | return true 22 | } 23 | } 24 | } 25 | } 26 | return false 27 | } 28 | 29 | func LoggerData(next http.Handler) http.Handler { 30 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | var requestData map[string]interface{} 32 | if r.Body != http.NoBody { 33 | // copy body 34 | CopyBody, _ := io.ReadAll(r.Body) 35 | // restore body 36 | r.Body = io.NopCloser(bytes.NewBuffer(CopyBody)) 37 | if err := json.Unmarshal(CopyBody, &requestData); err != nil { 38 | slog.Error("error unmarshalling request data", err, slog.String("func", "LoggerData")) 39 | } 40 | if hasSensitiveData(requestData) { 41 | for key := range requestData { 42 | for _, keyword := range sensitiveKeywords { 43 | if value, ok := requestData[key].(string); ok { 44 | if strings.Contains(strings.ToLower(key), keyword) || strings.Contains(strings.ToLower(value), keyword) { 45 | requestData[key] = "[REDACTED]" 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } else { 52 | r.Body = http.NoBody 53 | } 54 | 55 | // get user in token 56 | var userID string 57 | var userEmail string 58 | user, err := utils.DecodeJwt(r) 59 | if err != nil { 60 | userID = "no token" 61 | userEmail = "no token" 62 | } else { 63 | userID = user.ID 64 | userEmail = user.Email 65 | } 66 | 67 | slog.Info("request_data", 68 | slog.Any("url", r.URL.Path), 69 | slog.Any("method", r.Method), 70 | slog.Any("query", r.URL.Query()), 71 | slog.Any("body", requestData), 72 | slog.Any("id", userID), 73 | slog.Any("email", userEmail), 74 | ) 75 | 76 | next.ServeHTTP(w, r) 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /internal/handler/product_handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | 9 | "github.com/go-chi/chi" 10 | "github.com/google/uuid" 11 | "github.com/wiliamvj/api-users-golang/internal/dto" 12 | "github.com/wiliamvj/api-users-golang/internal/handler/httperr" 13 | "github.com/wiliamvj/api-users-golang/internal/handler/validation" 14 | ) 15 | 16 | // Create product 17 | // @Summary Create new product 18 | // @Description Endpoint for create product 19 | // @Tags product 20 | // @Accept json 21 | // @Produce json 22 | // @Param body body dto.CreateProductDto true "Create product dto" true 23 | // @Success 200 24 | // @Failure 400 {object} httperr.RestErr 25 | // @Failure 500 {object} httperr.RestErr 26 | // @Router /product [post] 27 | func (h *handler) CreateProduct(w http.ResponseWriter, r *http.Request) { 28 | var req dto.CreateProductDto 29 | 30 | if r.Body == http.NoBody { 31 | slog.Error("body is empty", slog.String("package", "producthandler")) 32 | w.WriteHeader(http.StatusBadRequest) 33 | msg := httperr.NewBadRequestError("body is required") 34 | json.NewEncoder(w).Encode(msg) 35 | return 36 | } 37 | err := json.NewDecoder(r.Body).Decode(&req) 38 | if err != nil { 39 | slog.Error("error to decode body", "err", err, slog.String("package", "producthandler")) 40 | w.WriteHeader(http.StatusBadRequest) 41 | msg := httperr.NewBadRequestError("error to decode body") 42 | json.NewEncoder(w).Encode(msg) 43 | return 44 | } 45 | httpErr := validation.ValidateHttpData(req) 46 | if httpErr != nil { 47 | slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "producthandler")) 48 | w.WriteHeader(httpErr.Code) 49 | json.NewEncoder(w).Encode(httpErr) 50 | return 51 | } 52 | 53 | err = h.productservice.CreateProduct(r.Context(), req) 54 | if err != nil { 55 | if err.Error() == "category not found" { 56 | w.WriteHeader(http.StatusNotFound) 57 | msg := httperr.NewNotFoundError("category not found") 58 | json.NewEncoder(w).Encode(msg) 59 | return 60 | } 61 | slog.Error(fmt.Sprintf("error to create category: %v", err), slog.String("package", "categoryhandler")) 62 | w.WriteHeader(http.StatusBadRequest) 63 | } 64 | w.WriteHeader(http.StatusCreated) 65 | } 66 | 67 | // Update product 68 | // @Summary Update product 69 | // @Description Endpoint for update product 70 | // @Tags product 71 | // @Accept json 72 | // @Produce json 73 | // @Param body body dto.UpdateProductDto true "Update product dto" true 74 | // @Param id path string true "product id" 75 | // @Success 200 76 | // @Failure 400 {object} httperr.RestErr 77 | // @Failure 500 {object} httperr.RestErr 78 | // @Router /product/{id} [patch] 79 | func (h *handler) UpdateProduct(w http.ResponseWriter, r *http.Request) { 80 | var req dto.UpdateProductDto 81 | 82 | productID := chi.URLParam(r, "id") 83 | if productID == "" { 84 | slog.Error("product id is required", slog.String("package", "producthandler")) 85 | w.WriteHeader(http.StatusBadRequest) 86 | msg := httperr.NewBadRequestError("product id is required") 87 | json.NewEncoder(w).Encode(msg) 88 | return 89 | } 90 | _, err := uuid.Parse(productID) 91 | if err != nil { 92 | slog.Error(fmt.Sprintf("error to parse product id: %v", err), slog.String("package", "producthandler")) 93 | w.WriteHeader(http.StatusBadRequest) 94 | msg := httperr.NewBadRequestError("invalid product id") 95 | json.NewEncoder(w).Encode(msg) 96 | return 97 | } 98 | if r.Body == http.NoBody { 99 | slog.Error("body is empty", slog.String("package", "producthandler")) 100 | w.WriteHeader(http.StatusBadRequest) 101 | msg := httperr.NewBadRequestError("body is required") 102 | json.NewEncoder(w).Encode(msg) 103 | return 104 | } 105 | err = json.NewDecoder(r.Body).Decode(&req) 106 | if err != nil { 107 | slog.Error("error to decode body", "err", err, slog.String("package", "producthandler")) 108 | w.WriteHeader(http.StatusBadRequest) 109 | msg := httperr.NewBadRequestError("error to decode body") 110 | json.NewEncoder(w).Encode(msg) 111 | return 112 | } 113 | httpErr := validation.ValidateHttpData(req) 114 | if httpErr != nil { 115 | slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "producthandler")) 116 | w.WriteHeader(httpErr.Code) 117 | json.NewEncoder(w).Encode(httpErr) 118 | return 119 | } 120 | err = h.productservice.UpdateProduct(r.Context(), productID, req) 121 | if err != nil { 122 | if err.Error() == "product not found" { 123 | w.WriteHeader(http.StatusNotFound) 124 | msg := httperr.NewNotFoundError("product not found") 125 | json.NewEncoder(w).Encode(msg) 126 | return 127 | } 128 | if err.Error() == "category not found" { 129 | w.WriteHeader(http.StatusNotFound) 130 | msg := httperr.NewNotFoundError("category not found") 131 | json.NewEncoder(w).Encode(msg) 132 | return 133 | } 134 | slog.Error(fmt.Sprintf("error to update category: %v", err), slog.String("package", "categoryhandler")) 135 | w.WriteHeader(http.StatusBadRequest) 136 | } 137 | w.WriteHeader(http.StatusOK) 138 | } 139 | 140 | // Delete product 141 | // @Summary Delete product 142 | // @Description Endpoint for update product 143 | // @Tags product 144 | // @Accept json 145 | // @Produce json 146 | // @Param id path string true "product id" 147 | // @Success 200 148 | // @Failure 400 {object} httperr.RestErr 149 | // @Failure 500 {object} httperr.RestErr 150 | // @Router /product/{id} [delete] 151 | func (h *handler) DeleteProduct(w http.ResponseWriter, r *http.Request) { 152 | productID := chi.URLParam(r, "id") 153 | if productID == "" { 154 | slog.Error("product id is required", slog.String("package", "producthandler")) 155 | w.WriteHeader(http.StatusBadRequest) 156 | msg := httperr.NewBadRequestError("product id is required") 157 | json.NewEncoder(w).Encode(msg) 158 | return 159 | } 160 | _, err := uuid.Parse(productID) 161 | if err != nil { 162 | slog.Error(fmt.Sprintf("error to parse product id: %v", err), slog.String("package", "producthandler")) 163 | w.WriteHeader(http.StatusBadRequest) 164 | msg := httperr.NewBadRequestError("invalid product id") 165 | json.NewEncoder(w).Encode(msg) 166 | return 167 | } 168 | err = h.productservice.DeleteProduct(r.Context(), productID) 169 | if err != nil { 170 | if err.Error() == "product not found" { 171 | w.WriteHeader(http.StatusNotFound) 172 | msg := httperr.NewNotFoundError("product not found") 173 | json.NewEncoder(w).Encode(msg) 174 | return 175 | } 176 | slog.Error(fmt.Sprintf("error to delete category: %v", err), slog.String("package", "categoryhandler")) 177 | w.WriteHeader(http.StatusBadRequest) 178 | } 179 | w.WriteHeader(http.StatusOK) 180 | } 181 | 182 | // Search products 183 | // @Summary Search products 184 | // @Description Endpoint for search product 185 | // @Tags product 186 | // @Accept json 187 | // @Produce json 188 | // @Param body body dto.FindProductDto true "Search products" true 189 | // @Success 200 {object} response.ProductResponse 190 | // @Failure 400 {object} httperr.RestErr 191 | // @Failure 500 {object} httperr.RestErr 192 | // @Router /product [get] 193 | func (h *handler) FindManyProducts(w http.ResponseWriter, r *http.Request) { 194 | var req dto.FindProductDto 195 | 196 | err := json.NewDecoder(r.Body).Decode(&req) 197 | if err != nil { 198 | slog.Error("error to decode body", "err", err, slog.String("package", "producthandler")) 199 | w.WriteHeader(http.StatusBadRequest) 200 | msg := httperr.NewBadRequestError("error to decode body") 201 | json.NewEncoder(w).Encode(msg) 202 | return 203 | } 204 | httpErr := validation.ValidateHttpData(req) 205 | if httpErr != nil { 206 | slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "producthandler")) 207 | w.WriteHeader(httpErr.Code) 208 | json.NewEncoder(w).Encode(httpErr) 209 | return 210 | } 211 | products, err := h.productservice.FindManyProducts(r.Context(), req) 212 | if err != nil { 213 | slog.Error(fmt.Sprintf("error to find many products: %v", err), slog.String("package", "producthandler")) 214 | w.WriteHeader(http.StatusBadRequest) 215 | } 216 | w.WriteHeader(http.StatusOK) 217 | json.NewEncoder(w).Encode(products) 218 | } 219 | -------------------------------------------------------------------------------- /internal/handler/response/category_response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | type CategoryResponse struct { 4 | ID string `json:"id"` 5 | Title string `json:"title"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/handler/response/product_response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type ProductResponse struct { 8 | ID string `json:"id"` 9 | Title string `json:"title"` 10 | Price int32 `json:"price"` 11 | Description string `json:"description,omitempty"` 12 | Categories []CategoryResponse `json:"categories"` 13 | CreatedAt time.Time `json:"created_at"` 14 | } 15 | -------------------------------------------------------------------------------- /internal/handler/response/user_response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import "time" 4 | 5 | type UserResponse struct { 6 | ID string `json:"id"` 7 | Name string `json:"name"` 8 | Email string `json:"email"` 9 | Address UserAddress `json:"address"` 10 | CreatedAt time.Time `json:"created_at"` 11 | UpdatedAt time.Time `json:"updated_at"` 12 | } 13 | 14 | type UserAddress struct { 15 | CEP string `json:"cep"` 16 | UF string `json:"uf"` 17 | City string `json:"city"` 18 | Complement string `json:"complement,omitempty"` 19 | Street string `json:"street"` 20 | } 21 | 22 | type ManyUsersResponse struct { 23 | Users []UserResponse `json:"users"` 24 | } 25 | 26 | type UserAuthToken struct { 27 | AccessToken string `json:"access_token"` 28 | } 29 | -------------------------------------------------------------------------------- /internal/handler/routes/docs_route.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/go-chi/chi" 5 | httpSwagger "github.com/swaggo/http-swagger" 6 | "github.com/wiliamvj/api-users-golang/docs/custom" 7 | ) 8 | 9 | var ( 10 | docsURL = "http://localhost:8080/docs/doc.json" 11 | ) 12 | 13 | // @title API users 14 | // @version 1.0 15 | // @in header 16 | // @name Authorization 17 | func InitDocsRoutes(r chi.Router) { 18 | r.Get("/docs/*", httpSwagger.Handler(httpSwagger.URL(docsURL), 19 | httpSwagger.AfterScript(custom.CustomJS), 20 | httpSwagger.DocExpansion("none"), 21 | httpSwagger.UIConfig(map[string]string{ 22 | "defaultModelsExpandDepth": `"-1"`, 23 | }), 24 | )) 25 | } 26 | -------------------------------------------------------------------------------- /internal/handler/routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/go-chi/chi" 5 | "github.com/go-chi/jwtauth" 6 | "github.com/wiliamvj/api-users-golang/config/env" 7 | "github.com/wiliamvj/api-users-golang/internal/handler" 8 | "github.com/wiliamvj/api-users-golang/internal/handler/middleware" 9 | ) 10 | 11 | func InitRoutes(router chi.Router, h handler.Handler) { 12 | router.Use(middleware.LoggerData) 13 | 14 | router.Post("/user", h.CreateUser) 15 | router.Route("/", func(r chi.Router) { 16 | r.Use(jwtauth.Verifier(env.Env.TokenAuth)) 17 | r.Use(jwtauth.Authenticator) 18 | 19 | //user routes 20 | r.Patch("/user", h.UpdateUser) 21 | r.Get("/user", h.GetUserByID) 22 | r.Delete("/user", h.DeleteUser) 23 | r.Get("/user/list-all", h.FindManyUsers) 24 | r.Patch("/user/password", h.UpdateUserPassword) 25 | 26 | // categories routes 27 | r.Post("/category", h.CreateCategory) 28 | 29 | // products routes 30 | r.Post("/product", h.CreateProduct) 31 | r.Patch("/product/{id}", h.UpdateProduct) 32 | r.Delete("/product/{id}", h.DeleteProduct) 33 | r.Get("/product", h.FindManyProducts) 34 | }) 35 | router.Route("/auth", func(r chi.Router) { 36 | r.Post("/login", h.Login) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /internal/handler/user_handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | 9 | "github.com/wiliamvj/api-users-golang/internal/common/utils" 10 | "github.com/wiliamvj/api-users-golang/internal/dto" 11 | "github.com/wiliamvj/api-users-golang/internal/handler/httperr" 12 | "github.com/wiliamvj/api-users-golang/internal/handler/validation" 13 | ) 14 | 15 | // Create user 16 | // @Summary Create new user 17 | // @Description Endpoint for create user 18 | // @Tags user 19 | // @Accept json 20 | // @Produce json 21 | // @Param body body dto.CreateUserDto true "Create user dto" true 22 | // @Success 200 23 | // @Failure 400 {object} httperr.RestErr 24 | // @Failure 500 {object} httperr.RestErr 25 | // @Router /user [post] 26 | func (h *handler) CreateUser(w http.ResponseWriter, r *http.Request) { 27 | var req dto.CreateUserDto 28 | 29 | if r.Body == http.NoBody { 30 | slog.Error("body is empty", slog.String("package", "userhandler")) 31 | w.WriteHeader(http.StatusBadRequest) 32 | msg := httperr.NewBadRequestError("body is required") 33 | json.NewEncoder(w).Encode(msg) 34 | return 35 | } 36 | err := json.NewDecoder(r.Body).Decode(&req) 37 | if err != nil { 38 | slog.Error("error to decode body", "err", err, slog.String("package", "userhandler")) 39 | w.WriteHeader(http.StatusBadRequest) 40 | msg := httperr.NewBadRequestError("error to decode body") 41 | json.NewEncoder(w).Encode(msg) 42 | return 43 | } 44 | httpErr := validation.ValidateHttpData(req) 45 | if httpErr != nil { 46 | slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "userhandler")) 47 | w.WriteHeader(httpErr.Code) 48 | json.NewEncoder(w).Encode(httpErr) 49 | return 50 | } 51 | err = h.userService.CreateUser(r.Context(), req) 52 | if err != nil { 53 | slog.Error(fmt.Sprintf("error to create user: %v", err), slog.String("package", "userhandler")) 54 | if err.Error() == "cep not found" { 55 | w.WriteHeader(http.StatusNotFound) 56 | msg := httperr.NewNotFoundError("cep not found") 57 | json.NewEncoder(w).Encode(msg) 58 | return 59 | } 60 | if err.Error() == "user already exists" { 61 | w.WriteHeader(http.StatusBadRequest) 62 | msg := httperr.NewBadRequestError("user already exists") 63 | json.NewEncoder(w).Encode(msg) 64 | return 65 | } 66 | w.WriteHeader(http.StatusInternalServerError) 67 | msg := httperr.NewBadRequestError("error to create user") 68 | json.NewEncoder(w).Encode(msg) 69 | return 70 | } 71 | } 72 | 73 | // Update user 74 | // @Summary Update user 75 | // @Description Endpoint for update user 76 | // @Tags user 77 | // @Security ApiKeyAuth 78 | // @Accept json 79 | // @Produce json 80 | // @Param body body dto.UpdateUserDto false "Update user dto" true 81 | // @Success 200 82 | // @Failure 400 {object} httperr.RestErr 83 | // @Failure 404 {object} httperr.RestErr 84 | // @Failure 500 {object} httperr.RestErr 85 | // @Router /user [patch] 86 | func (h *handler) UpdateUser(w http.ResponseWriter, r *http.Request) { 87 | var req dto.UpdateUserDto 88 | 89 | user, err := utils.DecodeJwt(r) 90 | if err != nil { 91 | slog.Error("error to decode jwt", slog.String("package", "userhandler")) 92 | w.WriteHeader(http.StatusBadRequest) 93 | msg := httperr.NewBadRequestError("error to decode jwt") 94 | json.NewEncoder(w).Encode(msg) 95 | return 96 | } 97 | if r.Body == http.NoBody { 98 | slog.Error("body is empty", slog.String("package", "userhandler")) 99 | w.WriteHeader(http.StatusBadRequest) 100 | msg := httperr.NewBadRequestError("body is required") 101 | json.NewEncoder(w).Encode(msg) 102 | return 103 | } 104 | err = json.NewDecoder(r.Body).Decode(&req) 105 | if err != nil { 106 | slog.Error("error to decode body", "err", err, slog.String("package", "userhandler")) 107 | w.WriteHeader(http.StatusBadRequest) 108 | msg := httperr.NewBadRequestError("error to decode body") 109 | json.NewEncoder(w).Encode(msg) 110 | return 111 | } 112 | httpErr := validation.ValidateHttpData(req) 113 | if httpErr != nil { 114 | slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "userhandler")) 115 | w.WriteHeader(httpErr.Code) 116 | json.NewEncoder(w).Encode(httpErr) 117 | return 118 | } 119 | err = h.userService.UpdateUser(r.Context(), req, user.ID) 120 | if err != nil { 121 | slog.Error(fmt.Sprintf("error to update user: %v", err), slog.String("package", "userhandler")) 122 | if err.Error() == "user not found" { 123 | w.WriteHeader(http.StatusNotFound) 124 | msg := httperr.NewNotFoundError("user not found") 125 | json.NewEncoder(w).Encode(msg) 126 | return 127 | } 128 | if err.Error() == "cep not found" { 129 | w.WriteHeader(http.StatusNotFound) 130 | msg := httperr.NewNotFoundError("cep not found") 131 | json.NewEncoder(w).Encode(msg) 132 | return 133 | } 134 | if err.Error() == "user already exists" { 135 | w.WriteHeader(http.StatusBadRequest) 136 | msg := httperr.NewBadRequestError("user already exists with this email") 137 | json.NewEncoder(w).Encode(msg) 138 | return 139 | } 140 | w.WriteHeader(http.StatusBadRequest) 141 | json.NewEncoder(w).Encode(err) 142 | return 143 | } 144 | } 145 | 146 | // User details 147 | // @Summary User details 148 | // @Description Get user by id 149 | // @Tags user 150 | // @Security ApiKeyAuth 151 | // @Accept json 152 | // @Produce json 153 | // @Param id path string true "user id" 154 | // @Success 200 {object} response.UserResponse 155 | // @Failure 400 {object} httperr.RestErr 156 | // @Failure 404 {object} httperr.RestErr 157 | // @Failure 500 {object} httperr.RestErr 158 | // @Router /user [get] 159 | func (h *handler) GetUserByID(w http.ResponseWriter, r *http.Request) { 160 | user, err := utils.DecodeJwt(r) 161 | if err != nil { 162 | slog.Error("error to decode jwt", slog.String("package", "userhandler")) 163 | w.WriteHeader(http.StatusBadRequest) 164 | msg := httperr.NewBadRequestError("error to decode jwt") 165 | json.NewEncoder(w).Encode(msg) 166 | return 167 | } 168 | res, err := h.userService.GetUserByID(r.Context(), user.ID) 169 | if err != nil { 170 | slog.Error(fmt.Sprintf("error to get user: %v", err), slog.String("package", "userhandler")) 171 | if err.Error() == "user not found" { 172 | w.WriteHeader(http.StatusNotFound) 173 | msg := httperr.NewNotFoundError("user not found") 174 | json.NewEncoder(w).Encode(msg) 175 | return 176 | } 177 | w.WriteHeader(http.StatusInternalServerError) 178 | msg := httperr.NewBadRequestError("error to get user") 179 | json.NewEncoder(w).Encode(msg) 180 | return 181 | } 182 | w.Header().Set("Content-Type", "application/json") 183 | w.WriteHeader(http.StatusOK) 184 | json.NewEncoder(w).Encode(res) 185 | } 186 | 187 | // Delete user 188 | // @Summary Delete user 189 | // @Description delete user by id 190 | // @Tags user 191 | // @Security ApiKeyAuth 192 | // @Accept json 193 | // @Produce json 194 | // @Param id path string true "user id" 195 | // @Success 200 196 | // @Failure 400 {object} httperr.RestErr 197 | // @Failure 404 {object} httperr.RestErr 198 | // @Failure 500 {object} httperr.RestErr 199 | // @Router /user [delete] 200 | func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) { 201 | user, err := utils.DecodeJwt(r) 202 | if err != nil { 203 | slog.Error("error to decode jwt", slog.String("package", "userhandler")) 204 | w.WriteHeader(http.StatusBadRequest) 205 | msg := httperr.NewBadRequestError("error to decode jwt") 206 | json.NewEncoder(w).Encode(msg) 207 | return 208 | } 209 | err = h.userService.DeleteUser(r.Context(), user.ID) 210 | if err != nil { 211 | slog.Error(fmt.Sprintf("error to delete user: %v", err), slog.String("package", "userhandler")) 212 | if err.Error() == "user not found" { 213 | w.WriteHeader(http.StatusNotFound) 214 | msg := httperr.NewNotFoundError("user not found") 215 | json.NewEncoder(w).Encode(msg) 216 | return 217 | } 218 | w.WriteHeader(http.StatusInternalServerError) 219 | msg := httperr.NewBadRequestError("error to delete user") 220 | json.NewEncoder(w).Encode(msg) 221 | return 222 | } 223 | w.WriteHeader(http.StatusNoContent) 224 | } 225 | 226 | // Get many user 227 | // @Summary Get many users 228 | // @Description Get many users 229 | // @Tags user 230 | // @Security ApiKeyAuth 231 | // @Accept json 232 | // @Produce json 233 | // @Success 200 {object} response.ManyUsersResponse 234 | // @Failure 400 {object} httperr.RestErr 235 | // @Failure 404 {object} httperr.RestErr 236 | // @Failure 500 {object} httperr.RestErr 237 | // @Router /user [get] 238 | func (h *handler) FindManyUsers(w http.ResponseWriter, r *http.Request) { 239 | res, err := h.userService.FindManyUsers(r.Context()) 240 | if err != nil { 241 | slog.Error(fmt.Sprintf("error to find many users: %v", err), slog.String("package", "userhandler")) 242 | w.WriteHeader(http.StatusInternalServerError) 243 | msg := httperr.NewBadRequestError("error to find many users") 244 | json.NewEncoder(w).Encode(msg) 245 | return 246 | } 247 | w.Header().Set("Content-Type", "application/json") 248 | w.WriteHeader(http.StatusOK) 249 | json.NewEncoder(w).Encode(res) 250 | } 251 | 252 | // Update user password 253 | // @Summary Update user password 254 | // @Description Endpoint for Update user password 255 | // @Tags user 256 | // @Security ApiKeyAuth 257 | // @Accept json 258 | // @Produce json 259 | // @Param id path string true "user id" 260 | // @Param body body dto.UpdateUserPasswordDto true "Update user password dto" true 261 | // @Success 200 262 | // @Failure 400 {object} httperr.RestErr 263 | // @Failure 500 {object} httperr.RestErr 264 | // @Router /user/password [get] 265 | func (h *handler) UpdateUserPassword(w http.ResponseWriter, r *http.Request) { 266 | var req dto.UpdateUserPasswordDto 267 | 268 | user, err := utils.DecodeJwt(r) 269 | if err != nil { 270 | slog.Error("error to decode jwt", slog.String("package", "userhandler")) 271 | w.WriteHeader(http.StatusBadRequest) 272 | msg := httperr.NewBadRequestError("error to decode jwt") 273 | json.NewEncoder(w).Encode(msg) 274 | return 275 | } 276 | if r.Body == http.NoBody { 277 | slog.Error("body is empty", slog.String("package", "userhandler")) 278 | w.WriteHeader(http.StatusBadRequest) 279 | msg := httperr.NewBadRequestError("body is required") 280 | json.NewEncoder(w).Encode(msg) 281 | return 282 | } 283 | err = json.NewDecoder(r.Body).Decode(&req) 284 | if err != nil { 285 | slog.Error("error to decode body", "err", err, slog.String("package", "userhandler")) 286 | w.WriteHeader(http.StatusBadRequest) 287 | msg := httperr.NewBadRequestError("error to decode body") 288 | json.NewEncoder(w).Encode(msg) 289 | return 290 | } 291 | httpErr := validation.ValidateHttpData(req) 292 | if httpErr != nil { 293 | slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "userhandler")) 294 | w.WriteHeader(httpErr.Code) 295 | json.NewEncoder(w).Encode(httpErr) 296 | return 297 | } 298 | err = h.userService.UpdateUserPassword(r.Context(), &req, user.ID) 299 | if err != nil { 300 | slog.Error(fmt.Sprintf("error to update user password: %v", err), slog.String("package", "userhandler")) 301 | if err.Error() == "user not found" { 302 | w.WriteHeader(http.StatusNotFound) 303 | msg := httperr.NewNotFoundError("user not found") 304 | json.NewEncoder(w).Encode(msg) 305 | return 306 | } 307 | w.WriteHeader(http.StatusInternalServerError) 308 | msg := httperr.NewBadRequestError("error to update user password") 309 | json.NewEncoder(w).Encode(msg) 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /internal/handler/validation/http_validation.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/go-playground/validator/v10" 9 | "github.com/wiliamvj/api-users-golang/internal/handler/httperr" 10 | ) 11 | 12 | func ValidateHttpData(d interface{}) *httperr.RestErr { 13 | val := validator.New(validator.WithRequiredStructEnabled()) 14 | 15 | // extract json tag name 16 | val.RegisterTagNameFunc(func(fld reflect.StructField) string { 17 | name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] 18 | if name == "-" { 19 | return "" 20 | } 21 | return name 22 | }) 23 | 24 | if err := val.Struct(d); err != nil { 25 | var errorsCauses []httperr.Fields 26 | 27 | for _, e := range err.(validator.ValidationErrors) { 28 | cause := httperr.Fields{} 29 | fieldName := e.Field() 30 | 31 | switch e.Tag() { 32 | case "required": 33 | cause.Message = fmt.Sprintf("%s is required", fieldName) 34 | cause.Field = fieldName 35 | cause.Value = e.Value() 36 | case "uuid4": 37 | cause.Message = fmt.Sprintf("%s is not a valid uuid", fieldName) 38 | cause.Field = fieldName 39 | cause.Value = e.Value() 40 | case "boolean": 41 | cause.Message = fmt.Sprintf("%s is not a valid boolean", fieldName) 42 | cause.Field = fieldName 43 | cause.Value = e.Value() 44 | case "min": 45 | cause.Message = fmt.Sprintf("%s must be greater than %s", fieldName, e.Param()) 46 | cause.Field = fieldName 47 | cause.Value = e.Value() 48 | case "max": 49 | cause.Message = fmt.Sprintf("%s must be less than %s", fieldName, e.Param()) 50 | cause.Field = fieldName 51 | cause.Value = e.Value() 52 | case "email": 53 | cause.Message = fmt.Sprintf("%s is not a valid email", fieldName) 54 | cause.Field = fieldName 55 | cause.Value = e.Value() 56 | case "containsany": 57 | cause.Message = fmt.Sprintf("%s must contain at least one of the following characters: !@#$%%*", fieldName) 58 | cause.Field = fieldName 59 | cause.Value = e.Value() 60 | case "dive": 61 | cause.Message = fmt.Sprintf("%s is not a valid array", fieldName) 62 | cause.Field = fieldName 63 | cause.Value = e.Value() 64 | case "gt": 65 | cause.Message = fmt.Sprintf("%s must have at least %s elements", fieldName, e.Param()) 66 | cause.Field = fieldName 67 | cause.Value = e.Value() 68 | default: 69 | cause.Message = "invalid field" 70 | cause.Field = fieldName 71 | cause.Value = e.Value() 72 | } 73 | 74 | errorsCauses = append(errorsCauses, cause) 75 | } 76 | 77 | return httperr.NewBadRequestValidationError("some fields are invalid", errorsCauses) 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/repository/categoryrepository/category_interface_repository.go: -------------------------------------------------------------------------------- 1 | package categoryrepository 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/wiliamvj/api-users-golang/internal/database/sqlc" 8 | "github.com/wiliamvj/api-users-golang/internal/entity" 9 | ) 10 | 11 | func NewCategoryRepository(db *sql.DB, q *sqlc.Queries) CategoryRepository { 12 | return &repository{ 13 | db, 14 | q, 15 | } 16 | } 17 | 18 | type repository struct { 19 | db *sql.DB 20 | queries *sqlc.Queries 21 | } 22 | 23 | type CategoryRepository interface { 24 | CreateCategory(ctx context.Context, c *entity.CategoryEntity) error 25 | } 26 | -------------------------------------------------------------------------------- /internal/repository/categoryrepository/category_repository.go: -------------------------------------------------------------------------------- 1 | package categoryrepository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wiliamvj/api-users-golang/internal/database/sqlc" 7 | "github.com/wiliamvj/api-users-golang/internal/entity" 8 | ) 9 | 10 | func (r *repository) CreateCategory(ctx context.Context, c *entity.CategoryEntity) error { 11 | err := r.queries.CreateCategory(ctx, sqlc.CreateCategoryParams{ 12 | ID: c.ID, 13 | Title: c.Title, 14 | CreatedAt: c.CreatedAt, 15 | UpdatedAt: c.UpdatedAt, 16 | }) 17 | if err != nil { 18 | return err 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/repository/productrepository/product_interface_repository.go: -------------------------------------------------------------------------------- 1 | package productrepository 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/wiliamvj/api-users-golang/internal/database/sqlc" 8 | "github.com/wiliamvj/api-users-golang/internal/dto" 9 | "github.com/wiliamvj/api-users-golang/internal/entity" 10 | ) 11 | 12 | func NewProductRepository(db *sql.DB, q *sqlc.Queries) ProductRepository { 13 | return &repository{ 14 | db, 15 | q, 16 | } 17 | } 18 | 19 | type repository struct { 20 | db *sql.DB 21 | queries *sqlc.Queries 22 | } 23 | 24 | type ProductRepository interface { 25 | CreateProduct(ctx context.Context, p *entity.ProductEntity, c []entity.ProductCategoryEntity) error 26 | GetCategoryByID(ctx context.Context, id string) (bool, error) 27 | GetProductByID(ctx context.Context, id string) (bool, error) 28 | UpdateProduct(ctx context.Context, p *entity.ProductEntity, c []entity.ProductCategoryEntity) error 29 | GetCategoriesByProductID(ctx context.Context, id string) ([]string, error) 30 | DeleteProductCategory(ctx context.Context, productID, categoryID string) error 31 | DeleteProduct(ctx context.Context, id string) error 32 | FindManyProducts(ctx context.Context, d dto.FindProductDto) ([]entity.ProductWithCategoryEntity, error) 33 | } 34 | -------------------------------------------------------------------------------- /internal/repository/productrepository/product_repository.go: -------------------------------------------------------------------------------- 1 | package productrepository 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "log/slog" 7 | 8 | "github.com/wiliamvj/api-users-golang/internal/database/sqlc" 9 | "github.com/wiliamvj/api-users-golang/internal/dto" 10 | "github.com/wiliamvj/api-users-golang/internal/entity" 11 | "github.com/wiliamvj/api-users-golang/internal/repository/transaction" 12 | ) 13 | 14 | func (r *repository) CreateProduct(ctx context.Context, p *entity.ProductEntity, c []entity.ProductCategoryEntity) error { 15 | err := transaction.Run(ctx, r.db, func(q *sqlc.Queries) error { 16 | var err error 17 | err = q.CreateProduct(ctx, sqlc.CreateProductParams{ 18 | ID: p.ID, 19 | Title: p.Title, 20 | Price: p.Price, 21 | Description: sql.NullString{String: p.Description, Valid: p.Description != ""}, 22 | CreatedAt: p.CreatedAt, 23 | UpdatedAt: p.UpdatedAt, 24 | }) 25 | if err != nil { 26 | return err 27 | } 28 | for _, category := range c { 29 | err = q.CreateProductCategory(ctx, sqlc.CreateProductCategoryParams{ 30 | ID: category.ID, 31 | ProductID: p.ID, 32 | CategoryID: category.CategoryID, 33 | CreatedAt: category.CreatedAt, 34 | UpdatedAt: category.UpdatedAt, 35 | }) 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | return nil 41 | }) 42 | if err != nil { 43 | slog.Error("error to create product, roll back applied", "err", err) 44 | return err 45 | } 46 | return nil 47 | } 48 | 49 | func (r *repository) GetCategoryByID(ctx context.Context, id string) (bool, error) { 50 | exists, err := r.queries.GetCategoryByID(ctx, id) 51 | if err != nil || err == sql.ErrNoRows { 52 | return false, err 53 | } 54 | return exists, nil 55 | } 56 | 57 | func (r *repository) GetProductByID(ctx context.Context, id string) (bool, error) { 58 | exists, err := r.queries.GetProductByID(ctx, id) 59 | if err != nil || err == sql.ErrNoRows { 60 | return false, err 61 | } 62 | return exists, nil 63 | } 64 | 65 | func (r *repository) UpdateProduct(ctx context.Context, p *entity.ProductEntity, c []entity.ProductCategoryEntity) error { 66 | err := transaction.Run(ctx, r.db, func(q *sqlc.Queries) error { 67 | var err error 68 | err = q.UpdateProduct(ctx, sqlc.UpdateProductParams{ 69 | ID: p.ID, 70 | Title: sql.NullString{String: p.Title, Valid: p.Title != ""}, 71 | Price: sql.NullInt32{Int32: p.Price, Valid: p.Price != 0}, 72 | Description: sql.NullString{String: p.Description, Valid: p.Description != ""}, 73 | UpdatedAt: p.UpdatedAt, 74 | }) 75 | if err != nil { 76 | return err 77 | } 78 | for _, category := range c { 79 | err = q.CreateProductCategory(ctx, sqlc.CreateProductCategoryParams{ 80 | ID: category.ID, 81 | ProductID: p.ID, 82 | CategoryID: category.CategoryID, 83 | CreatedAt: category.CreatedAt, 84 | UpdatedAt: category.UpdatedAt, 85 | }) 86 | if err != nil { 87 | return err 88 | } 89 | } 90 | return nil 91 | }) 92 | if err != nil { 93 | slog.Error("error to update product, roll back applied", "err", err) 94 | return err 95 | } 96 | return nil 97 | } 98 | 99 | func (r *repository) GetCategoriesByProductID(ctx context.Context, id string) ([]string, error) { 100 | categories, err := r.queries.GetCategoriesByProductID(ctx, id) 101 | if err != nil { 102 | return nil, err 103 | } 104 | return categories, nil 105 | } 106 | 107 | func (r *repository) DeleteProductCategory(ctx context.Context, productID, categoryID string) error { 108 | err := r.queries.DeleteProductCategory(ctx, sqlc.DeleteProductCategoryParams{ 109 | ProductID: productID, 110 | CategoryID: categoryID, 111 | }) 112 | if err != nil { 113 | return err 114 | } 115 | return nil 116 | } 117 | 118 | func (r *repository) DeleteProduct(ctx context.Context, id string) error { 119 | err := r.queries.DeleteProduct(ctx, id) 120 | if err != nil { 121 | return err 122 | } 123 | return nil 124 | } 125 | 126 | func (r *repository) FindManyProducts(ctx context.Context, d dto.FindProductDto) ([]entity.ProductWithCategoryEntity, error) { 127 | products, err := r.queries.FindManyProducts(ctx, sqlc.FindManyProductsParams{ 128 | Categories: d.Categories, 129 | Search: sql.NullString{String: d.Search, Valid: d.Search != ""}, 130 | }) 131 | if err != nil { 132 | return nil, err 133 | } 134 | var response []entity.ProductWithCategoryEntity 135 | for _, p := range products { 136 | var category []entity.CategoryEntity 137 | categories, err := r.queries.GetProductCategories(ctx, p.ID) 138 | if err != nil { 139 | return nil, err 140 | } 141 | for _, c := range categories { 142 | category = append(category, entity.CategoryEntity{ 143 | ID: c.ID, 144 | Title: c.Title, 145 | }) 146 | } 147 | response = append(response, entity.ProductWithCategoryEntity{ 148 | ID: p.ID, 149 | Title: p.Title, 150 | Description: p.Description.String, 151 | Price: p.Price, 152 | Categories: category, 153 | CreatedAt: p.CreatedAt, 154 | }) 155 | } 156 | return response, nil 157 | } 158 | -------------------------------------------------------------------------------- /internal/repository/transaction/run_transaction.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | 8 | "github.com/wiliamvj/api-users-golang/internal/database/sqlc" 9 | ) 10 | 11 | func Run(ctx context.Context, c *sql.DB, fn func(*sqlc.Queries) error) error { 12 | tx, err := c.BeginTx(ctx, nil) 13 | if err != nil { 14 | return err 15 | } 16 | q := sqlc.New(tx) 17 | err = fn(q) 18 | if err != nil { 19 | if errRb := tx.Rollback(); errRb != nil { 20 | return fmt.Errorf("error on rollback: %v, original error: %w", errRb, err) 21 | } 22 | return err 23 | } 24 | return tx.Commit() 25 | } 26 | -------------------------------------------------------------------------------- /internal/repository/userrepository/user_interface_repository.go: -------------------------------------------------------------------------------- 1 | package userrepository 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/wiliamvj/api-users-golang/internal/database/sqlc" 8 | "github.com/wiliamvj/api-users-golang/internal/entity" 9 | ) 10 | 11 | func NewUserRepository(db *sql.DB, q *sqlc.Queries) UserRepository { 12 | return &repository{ 13 | db, 14 | q, 15 | } 16 | } 17 | 18 | type repository struct { 19 | db *sql.DB 20 | queries *sqlc.Queries 21 | } 22 | 23 | type UserRepository interface { 24 | CreateUser(ctx context.Context, u *entity.UserEntity) error 25 | FindUserByEmail(ctx context.Context, email string) (*entity.UserEntity, error) 26 | FindUserByID(ctx context.Context, id string) (*entity.UserEntity, error) 27 | UpdateUser(ctx context.Context, u *entity.UserEntity) error 28 | DeleteUser(ctx context.Context, id string) error 29 | FindManyUsers(ctx context.Context) ([]entity.UserEntity, error) 30 | UpdatePassword(ctx context.Context, pass, id string) error 31 | GetUserPassword(ctx context.Context, id string) (string, error) 32 | } 33 | -------------------------------------------------------------------------------- /internal/repository/userrepository/user_repository.go: -------------------------------------------------------------------------------- 1 | package userrepository 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "log/slog" 7 | "time" 8 | 9 | "github.com/google/uuid" 10 | "github.com/wiliamvj/api-users-golang/internal/database/sqlc" 11 | "github.com/wiliamvj/api-users-golang/internal/entity" 12 | transaction "github.com/wiliamvj/api-users-golang/internal/repository/transaction" 13 | ) 14 | 15 | func (r *repository) CreateUser(ctx context.Context, u *entity.UserEntity) error { 16 | err := transaction.Run(ctx, r.db, func(q *sqlc.Queries) error { 17 | var err error 18 | err = q.CreateUser(ctx, sqlc.CreateUserParams{ 19 | ID: u.ID, 20 | Name: u.Name, 21 | Email: u.Email, 22 | Password: u.Password, 23 | CreatedAt: u.CreatedAt, 24 | UpdatedAt: u.UpdatedAt, 25 | }) 26 | if err != nil { 27 | return err 28 | } 29 | err = q.CreateUserAddress(ctx, sqlc.CreateUserAddressParams{ 30 | ID: uuid.New().String(), 31 | UserID: u.ID, 32 | Cep: u.Address.CEP, 33 | Ibge: u.Address.IBGE, 34 | Uf: u.Address.UF, 35 | City: u.Address.City, 36 | Complement: sql.NullString{String: u.Address.Complement, Valid: u.Address.Complement != ""}, 37 | Street: u.Address.Street, 38 | CreatedAt: time.Now(), 39 | UpdatedAt: time.Now(), 40 | }) 41 | if err != nil { 42 | return err 43 | } 44 | return nil 45 | }) 46 | if err != nil { 47 | slog.Error("error to create user and address, roll back applied", "err", err) 48 | return err 49 | } 50 | return nil 51 | } 52 | 53 | func (r *repository) FindUserByEmail(ctx context.Context, email string) (*entity.UserEntity, error) { 54 | user, err := r.queries.FindUserByEmail(ctx, email) 55 | if err != nil { 56 | return nil, err 57 | } 58 | userEntity := entity.UserEntity{ 59 | ID: user.ID, 60 | Name: user.Name, 61 | Email: user.Email, 62 | } 63 | return &userEntity, nil 64 | } 65 | 66 | func (r *repository) FindUserByID(ctx context.Context, id string) (*entity.UserEntity, error) { 67 | user, err := r.queries.FindUserByID(ctx, id) 68 | if err != nil { 69 | return nil, err 70 | } 71 | userEntity := entity.UserEntity{ 72 | ID: user.ID, 73 | Name: user.Name, 74 | Email: user.Email, 75 | Address: entity.UserAddress{ 76 | CEP: user.Cep, 77 | UF: user.Uf, 78 | City: user.City, 79 | Complement: user.Complement.String, 80 | Street: user.Street, 81 | }, 82 | CreatedAt: user.CreatedAt, 83 | UpdatedAt: user.UpdatedAt, 84 | } 85 | return &userEntity, nil 86 | } 87 | 88 | func (r *repository) UpdateUser(ctx context.Context, u *entity.UserEntity) error { 89 | err := transaction.Run(ctx, r.db, func(q *sqlc.Queries) error { 90 | var err error 91 | err = r.queries.UpdateUser(ctx, sqlc.UpdateUserParams{ 92 | ID: u.ID, 93 | Name: sql.NullString{String: u.Name, Valid: u.Name != ""}, 94 | Email: sql.NullString{String: u.Email, Valid: u.Email != ""}, 95 | UpdatedAt: u.UpdatedAt, 96 | }) 97 | if err != nil { 98 | return err 99 | } 100 | err = r.queries.UpdateUserAddress(ctx, sqlc.UpdateUserAddressParams{ 101 | UserID: u.ID, 102 | Cep: sql.NullString{String: u.Address.CEP, Valid: u.Address.CEP != ""}, 103 | Ibge: sql.NullString{String: u.Address.IBGE, Valid: u.Address.IBGE != ""}, 104 | Uf: sql.NullString{String: u.Address.UF, Valid: u.Address.UF != ""}, 105 | City: sql.NullString{String: u.Address.City, Valid: u.Address.City != ""}, 106 | Complement: sql.NullString{String: u.Address.Complement, Valid: u.Address.Complement != ""}, 107 | Street: sql.NullString{String: u.Address.Street, Valid: u.Address.Street != ""}, 108 | UpdatedAt: time.Now(), 109 | }) 110 | if err != nil { 111 | return err 112 | } 113 | return nil 114 | }) 115 | if err != nil { 116 | slog.Error("error to update user and address, roll back applied", "err", err) 117 | return err 118 | } 119 | return nil 120 | } 121 | 122 | func (r *repository) DeleteUser(ctx context.Context, id string) error { 123 | err := r.queries.DeleteUser(ctx, id) 124 | if err != nil { 125 | return err 126 | } 127 | return nil 128 | } 129 | 130 | func (r *repository) FindManyUsers(ctx context.Context) ([]entity.UserEntity, error) { 131 | users, err := r.queries.FindManyUsers(ctx) 132 | if err != nil { 133 | return nil, err 134 | } 135 | var usersEntity []entity.UserEntity 136 | for _, user := range users { 137 | userEntity := entity.UserEntity{ 138 | ID: user.ID, 139 | Name: user.Name, 140 | Email: user.Email, 141 | Address: entity.UserAddress{ 142 | CEP: user.Cep, 143 | UF: user.Uf, 144 | City: user.City, 145 | Street: user.Street, 146 | Complement: user.Complement.String, 147 | }, 148 | CreatedAt: user.CreatedAt, 149 | UpdatedAt: user.UpdatedAt, 150 | } 151 | usersEntity = append(usersEntity, userEntity) 152 | } 153 | return usersEntity, nil 154 | } 155 | 156 | func (r *repository) UpdatePassword(ctx context.Context, pass, id string) error { 157 | err := r.queries.UpdatePassword(ctx, sqlc.UpdatePasswordParams{ 158 | ID: id, 159 | Password: pass, 160 | UpdatedAt: time.Now(), 161 | }) 162 | if err != nil { 163 | return err 164 | } 165 | return nil 166 | } 167 | 168 | func (r *repository) GetUserPassword(ctx context.Context, id string) (string, error) { 169 | pass, err := r.queries.GetUserPassword(ctx, id) 170 | if err != nil { 171 | return "", err 172 | } 173 | return pass, nil 174 | } 175 | -------------------------------------------------------------------------------- /internal/service/categoryservice/category_interface_service.go: -------------------------------------------------------------------------------- 1 | package categoryservice 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wiliamvj/api-users-golang/internal/dto" 7 | "github.com/wiliamvj/api-users-golang/internal/repository/categoryrepository" 8 | ) 9 | 10 | func NewCategoryService(repo categoryrepository.CategoryRepository) CategoryService { 11 | return &service{ 12 | repo, 13 | } 14 | } 15 | 16 | type service struct { 17 | repo categoryrepository.CategoryRepository 18 | } 19 | 20 | type CategoryService interface { 21 | CreateCategory(ctx context.Context, u dto.CreateCategoryDto) error 22 | } 23 | -------------------------------------------------------------------------------- /internal/service/categoryservice/category_service.go: -------------------------------------------------------------------------------- 1 | package categoryservice 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | "github.com/wiliamvj/api-users-golang/internal/dto" 10 | "github.com/wiliamvj/api-users-golang/internal/entity" 11 | ) 12 | 13 | func (s *service) CreateCategory(ctx context.Context, u dto.CreateCategoryDto) error { 14 | categoryEntity := entity.CategoryEntity{ 15 | ID: uuid.New().String(), 16 | Title: u.Title, 17 | CreatedAt: time.Now(), 18 | UpdatedAt: time.Now(), 19 | } 20 | err := s.repo.CreateCategory(ctx, &categoryEntity) 21 | if err != nil { 22 | return errors.New("error to create category") 23 | } 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/service/productservice/product_interface_service.go: -------------------------------------------------------------------------------- 1 | package productservice 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wiliamvj/api-users-golang/internal/dto" 7 | "github.com/wiliamvj/api-users-golang/internal/handler/response" 8 | "github.com/wiliamvj/api-users-golang/internal/repository/productrepository" 9 | ) 10 | 11 | func NewProductService(repo productrepository.ProductRepository) ProductService { 12 | return &service{ 13 | repo, 14 | } 15 | } 16 | 17 | type service struct { 18 | repo productrepository.ProductRepository 19 | } 20 | 21 | type ProductService interface { 22 | CreateProduct(ctx context.Context, u dto.CreateProductDto) error 23 | UpdateProduct(ctx context.Context, id string, u dto.UpdateProductDto) error 24 | DeleteProduct(ctx context.Context, id string) error 25 | FindManyProducts(ctx context.Context, d dto.FindProductDto) ([]response.ProductResponse, error) 26 | } 27 | -------------------------------------------------------------------------------- /internal/service/productservice/product_service.go: -------------------------------------------------------------------------------- 1 | package productservice 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log/slog" 7 | "time" 8 | 9 | "github.com/google/uuid" 10 | "github.com/wiliamvj/api-users-golang/internal/dto" 11 | "github.com/wiliamvj/api-users-golang/internal/entity" 12 | "github.com/wiliamvj/api-users-golang/internal/handler/response" 13 | ) 14 | 15 | func (s *service) CreateProduct(ctx context.Context, u dto.CreateProductDto) error { 16 | productId := uuid.New().String() 17 | productEntity := entity.ProductEntity{ 18 | ID: productId, 19 | Title: u.Title, 20 | Price: u.Price, 21 | Categories: u.Categories, 22 | Description: u.Description, 23 | CreatedAt: time.Now(), 24 | UpdatedAt: time.Now(), 25 | } 26 | var categories []entity.ProductCategoryEntity 27 | for _, categoryID := range u.Categories { 28 | exists, err := s.repo.GetCategoryByID(ctx, categoryID) 29 | if err != nil || !exists { 30 | slog.Error("category not found", slog.String("category_id", categoryID), slog.String("package", "productservice")) 31 | return errors.New("category not found") 32 | } 33 | categories = append(categories, entity.ProductCategoryEntity{ 34 | ID: uuid.New().String(), 35 | ProductID: productId, 36 | CategoryID: categoryID, 37 | CreatedAt: time.Now(), 38 | UpdatedAt: time.Now(), 39 | }) 40 | } 41 | err := s.repo.CreateProduct(ctx, &productEntity, categories) 42 | if err != nil { 43 | return err 44 | } 45 | return nil 46 | } 47 | 48 | func (s *service) UpdateProduct(ctx context.Context, id string, u dto.UpdateProductDto) error { 49 | exists, err := s.repo.GetProductByID(ctx, id) 50 | if err != nil || !exists { 51 | slog.Error("product not found", slog.String("product_id", id), slog.String("package", "productservice")) 52 | return errors.New("product not found") 53 | } 54 | // validate categories if they exist 55 | var categories []entity.ProductCategoryEntity 56 | if len(u.Categories) > 0 { 57 | for _, categoryID := range u.Categories { 58 | exists, err := s.repo.GetCategoryByID(ctx, categoryID) 59 | if err != nil || !exists { 60 | slog.Error("category not found", slog.String("category_id", categoryID), slog.String("package", "productservice")) 61 | return errors.New("category not found") 62 | } 63 | } 64 | 65 | // search for all categories of the product 66 | productCategories, err := s.repo.GetCategoriesByProductID(ctx, id) 67 | if err != nil { 68 | return errors.New("error getting categories by product id") 69 | } 70 | // remove all categories that are not in u.Categories 71 | for _, productCategory := range productCategories { 72 | found := false 73 | for _, categoryID := range u.Categories { 74 | if productCategory == categoryID { 75 | found = true 76 | break 77 | } 78 | } 79 | // if not found, then we can delete it 80 | if !found { 81 | err = s.repo.DeleteProductCategory(ctx, id, productCategory) 82 | if err != nil { 83 | return errors.New("error deleting product category") 84 | } 85 | } 86 | } 87 | 88 | for _, categoryID := range u.Categories { 89 | found := false 90 | for _, productCategory := range productCategories { 91 | if productCategory == categoryID { 92 | found = true 93 | break 94 | } 95 | } 96 | if !found { 97 | categories = append(categories, entity.ProductCategoryEntity{ 98 | ID: uuid.New().String(), 99 | ProductID: id, 100 | CategoryID: categoryID, 101 | CreatedAt: time.Now(), 102 | UpdatedAt: time.Now(), 103 | }) 104 | } 105 | } 106 | } 107 | productEntity := entity.ProductEntity{ 108 | ID: id, 109 | Title: u.Title, 110 | Price: u.Price, 111 | Description: u.Description, 112 | Categories: u.Categories, 113 | UpdatedAt: time.Now(), 114 | } 115 | err = s.repo.UpdateProduct(ctx, &productEntity, categories) 116 | if err != nil { 117 | return err 118 | } 119 | return nil 120 | } 121 | 122 | func (s *service) DeleteProduct(ctx context.Context, id string) error { 123 | exists, err := s.repo.GetProductByID(ctx, id) 124 | if err != nil || !exists { 125 | slog.Error("product not found", slog.String("product_id", id), slog.String("package", "productservice")) 126 | return errors.New("product not found") 127 | } 128 | err = s.repo.DeleteProduct(ctx, id) 129 | if err != nil { 130 | return err 131 | } 132 | return nil 133 | } 134 | 135 | func (s *service) FindManyProducts(ctx context.Context, d dto.FindProductDto) ([]response.ProductResponse, error) { 136 | products, err := s.repo.FindManyProducts(ctx, d) 137 | if err != nil { 138 | return nil, err 139 | } 140 | var productsResponse []response.ProductResponse 141 | for _, p := range products { 142 | var categories []response.CategoryResponse 143 | for _, c := range p.Categories { 144 | categories = append(categories, response.CategoryResponse{ 145 | ID: c.ID, 146 | Title: c.Title, 147 | }) 148 | } 149 | productsResponse = append(productsResponse, response.ProductResponse{ 150 | ID: p.ID, 151 | Title: p.Title, 152 | Description: p.Description, 153 | Price: p.Price, 154 | Categories: categories, 155 | CreatedAt: p.CreatedAt, 156 | }) 157 | } 158 | if len(productsResponse) == 0 { 159 | return []response.ProductResponse{}, nil 160 | } 161 | return productsResponse, nil 162 | } 163 | -------------------------------------------------------------------------------- /internal/service/userservice/auth_service.go: -------------------------------------------------------------------------------- 1 | package userservice 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log/slog" 7 | "time" 8 | 9 | "github.com/wiliamvj/api-users-golang/config/env" 10 | "github.com/wiliamvj/api-users-golang/internal/dto" 11 | "github.com/wiliamvj/api-users-golang/internal/handler/response" 12 | "golang.org/x/crypto/bcrypt" 13 | ) 14 | 15 | func (s *service) Login(ctx context.Context, u dto.LoginDTO) (*response.UserAuthToken, error) { 16 | user, err := s.repo.FindUserByEmail(ctx, u.Email) 17 | if err != nil { 18 | slog.Error("error to search user by email", "err", err, slog.String("package", "userservice")) 19 | return nil, errors.New("error to search user password") 20 | } 21 | if user == nil { 22 | slog.Error("user not found", slog.String("package", "userservice")) 23 | return nil, errors.New("user not found") 24 | } 25 | pass, err := s.repo.GetUserPassword(ctx, user.ID) 26 | if err != nil { 27 | slog.Error("error to search user password", "err", err, slog.String("package", "userservice")) 28 | return nil, errors.New("error to search user password") 29 | } 30 | // compare password with password in database 31 | err = bcrypt.CompareHashAndPassword([]byte(pass), []byte(u.Password)) 32 | if err != nil { 33 | slog.Error("invalid password", slog.String("package", "service_user")) 34 | return nil, errors.New("invalid password") 35 | } 36 | _, token, _ := env.Env.TokenAuth.Encode(map[string]interface{}{ 37 | "id": user.ID, 38 | "email": u.Email, 39 | "name": user.Name, 40 | "exp": time.Now().Add(time.Second * time.Duration(env.Env.JwtExpiresIn)).Unix(), 41 | }) 42 | userAuthToken := response.UserAuthToken{ 43 | AccessToken: token, 44 | } 45 | return &userAuthToken, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/service/userservice/user_interface_service.go: -------------------------------------------------------------------------------- 1 | package userservice 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/wiliamvj/api-users-golang/internal/dto" 7 | "github.com/wiliamvj/api-users-golang/internal/handler/response" 8 | "github.com/wiliamvj/api-users-golang/internal/repository/userrepository" 9 | ) 10 | 11 | func NewUserService(repo userrepository.UserRepository) UserService { 12 | return &service{ 13 | repo, 14 | } 15 | } 16 | 17 | type service struct { 18 | repo userrepository.UserRepository 19 | } 20 | 21 | type UserService interface { 22 | CreateUser(ctx context.Context, u dto.CreateUserDto) error 23 | UpdateUser(ctx context.Context, u dto.UpdateUserDto, id string) error 24 | GetUserByID(ctx context.Context, id string) (*response.UserResponse, error) 25 | DeleteUser(ctx context.Context, id string) error 26 | FindManyUsers(ctx context.Context) (*response.ManyUsersResponse, error) 27 | UpdateUserPassword(ctx context.Context, u *dto.UpdateUserPasswordDto, id string) error 28 | Login(ctx context.Context, u dto.LoginDTO) (*response.UserAuthToken, error) 29 | } 30 | -------------------------------------------------------------------------------- /internal/service/userservice/user_service.go: -------------------------------------------------------------------------------- 1 | package userservice 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "log/slog" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | "github.com/wiliamvj/api-users-golang/api/viacep" 12 | "github.com/wiliamvj/api-users-golang/internal/dto" 13 | "github.com/wiliamvj/api-users-golang/internal/entity" 14 | "github.com/wiliamvj/api-users-golang/internal/handler/response" 15 | "golang.org/x/crypto/bcrypt" 16 | ) 17 | 18 | func (s *service) CreateUser(ctx context.Context, u dto.CreateUserDto) error { 19 | userExists, err := s.repo.FindUserByEmail(ctx, u.Email) 20 | if err != nil { 21 | if err != sql.ErrNoRows { 22 | slog.Error("error to search user by email", "err", err, slog.String("package", "userservice")) 23 | return err 24 | } 25 | } 26 | if userExists != nil { 27 | slog.Error("user already exists", slog.String("package", "userservice")) 28 | return errors.New("user already exists") 29 | } 30 | passwordEncrypted, err := bcrypt.GenerateFromPassword([]byte(u.Password), 12) 31 | if err != nil { 32 | slog.Error("error to encrypt password", "err", err, slog.String("package", "userservice")) 33 | return errors.New("error to encrypt password") 34 | } 35 | cep, err := viacep.GetCep(u.CEP) 36 | if err != nil { 37 | slog.Error("error to get cep", "err", err, slog.String("package", "userservice")) 38 | return err 39 | } 40 | newUser := entity.UserEntity{ 41 | ID: uuid.New().String(), 42 | Name: u.Name, 43 | Email: u.Email, 44 | Password: string(passwordEncrypted), 45 | Address: entity.UserAddress{ 46 | CEP: cep.CEP, 47 | IBGE: cep.IBGE, 48 | UF: cep.UF, 49 | City: cep.Localidade, 50 | Complement: cep.Complemento, 51 | Street: cep.Logradouro, 52 | }, 53 | CreatedAt: time.Now(), 54 | UpdatedAt: time.Now(), 55 | } 56 | err = s.repo.CreateUser(ctx, &newUser) 57 | if err != nil { 58 | slog.Error("error to create user", "err", err, slog.String("package", "userservice")) 59 | return err 60 | } 61 | return nil 62 | } 63 | 64 | func (s *service) UpdateUser(ctx context.Context, u dto.UpdateUserDto, id string) error { 65 | _, err := s.repo.FindUserByID(ctx, id) 66 | if err != nil { 67 | if err == sql.ErrNoRows { 68 | slog.Error("user not found", slog.String("package", "userservice")) 69 | return errors.New("user not found") 70 | } 71 | slog.Error("error to search user by id", "err", err, slog.String("package", "userservice")) 72 | return err 73 | } 74 | var updateUser entity.UserEntity 75 | if u.Email != "" { 76 | userExists, err := s.repo.FindUserByEmail(ctx, u.Email) 77 | if err != nil { 78 | if err != sql.ErrNoRows { 79 | slog.Error("error to search user by email", "err", err, slog.String("package", "userservice")) 80 | return errors.New("error to search user by email") 81 | } 82 | } 83 | if userExists != nil { 84 | slog.Error("user already exists", slog.String("package", "userservice")) 85 | return errors.New("user already exists") 86 | } 87 | updateUser.Email = u.Email 88 | } 89 | if u.CEP != "" { 90 | cep, err := viacep.GetCep(u.CEP) 91 | if err != nil { 92 | slog.Error("error to get cep", "err", err, slog.String("package", "userservice")) 93 | return err 94 | } 95 | updateUser.Address = entity.UserAddress{ 96 | CEP: cep.CEP, 97 | IBGE: cep.IBGE, 98 | UF: cep.UF, 99 | City: cep.Localidade, 100 | Complement: cep.Complemento, 101 | Street: cep.Logradouro, 102 | } 103 | } 104 | updateUser.ID = id 105 | updateUser.Name = u.Name 106 | updateUser.UpdatedAt = time.Now() 107 | err = s.repo.UpdateUser(ctx, &updateUser) 108 | if err != nil { 109 | slog.Error("error to update user", "err", err, slog.String("package", "userservice")) 110 | return err 111 | } 112 | return nil 113 | } 114 | 115 | func (s *service) GetUserByID(ctx context.Context, id string) (*response.UserResponse, error) { 116 | userExists, err := s.repo.FindUserByID(ctx, id) 117 | if err != nil { 118 | slog.Error("error to search user by id", "err", err, slog.String("package", "userservice")) 119 | return nil, err 120 | } 121 | if userExists == nil { 122 | slog.Error("user not found", slog.String("package", "userservice")) 123 | return nil, errors.New("user not found") 124 | } 125 | user := response.UserResponse{ 126 | ID: userExists.ID, 127 | Name: userExists.Name, 128 | Email: userExists.Email, 129 | Address: response.UserAddress{ 130 | CEP: userExists.Address.CEP, 131 | UF: userExists.Address.UF, 132 | City: userExists.Address.City, 133 | Complement: userExists.Address.Complement, 134 | Street: userExists.Address.Street, 135 | }, 136 | CreatedAt: userExists.CreatedAt, 137 | UpdatedAt: userExists.UpdatedAt, 138 | } 139 | return &user, nil 140 | } 141 | 142 | func (s *service) DeleteUser(ctx context.Context, id string) error { 143 | userExists, err := s.repo.FindUserByID(ctx, id) 144 | if err != nil { 145 | slog.Error("error to search user by id", "err", err, slog.String("package", "userservice")) 146 | return err 147 | } 148 | if userExists == nil { 149 | slog.Error("user not found", slog.String("package", "userservice")) 150 | return errors.New("user not found") 151 | } 152 | err = s.repo.DeleteUser(ctx, id) 153 | if err != nil { 154 | slog.Error("error to delete user", "err", err, slog.String("package", "userservice")) 155 | return err 156 | } 157 | return nil 158 | } 159 | 160 | func (s *service) FindManyUsers(ctx context.Context) (*response.ManyUsersResponse, error) { 161 | findManyUsers, err := s.repo.FindManyUsers(ctx) 162 | if err != nil { 163 | slog.Error("error to find many users", "err", err, slog.String("package", "userservice")) 164 | return nil, err 165 | } 166 | users := response.ManyUsersResponse{} 167 | for _, user := range findManyUsers { 168 | userResponse := response.UserResponse{ 169 | ID: user.ID, 170 | Name: user.Name, 171 | Email: user.Email, 172 | Address: response.UserAddress{ 173 | CEP: user.Address.CEP, 174 | UF: user.Address.UF, 175 | City: user.Address.City, 176 | Complement: user.Address.Complement, 177 | Street: user.Address.Street, 178 | }, 179 | CreatedAt: user.CreatedAt, 180 | UpdatedAt: user.UpdatedAt, 181 | } 182 | users.Users = append(users.Users, userResponse) 183 | } 184 | return &users, nil 185 | } 186 | 187 | func (s *service) UpdateUserPassword(ctx context.Context, u *dto.UpdateUserPasswordDto, id string) error { 188 | userExists, err := s.repo.FindUserByID(ctx, id) 189 | if err != nil { 190 | slog.Error("error to search user by id", "err", err, slog.String("package", "userservice")) 191 | return err 192 | } 193 | if userExists == nil { 194 | slog.Error("user not found", slog.String("package", "userservice")) 195 | return errors.New("user not found") 196 | } 197 | oldPass, err := s.repo.GetUserPassword(ctx, id) 198 | if err != nil { 199 | slog.Error("error to get user password", "err", err, slog.String("package", "userservice")) 200 | return err 201 | } 202 | // compare passwords 203 | err = bcrypt.CompareHashAndPassword([]byte(oldPass), []byte(u.OldPassword)) 204 | if err != nil { 205 | slog.Error("invalid password", slog.String("package", "userservice")) 206 | return errors.New("invalid password") 207 | } 208 | // compare new password with password in database 209 | err = bcrypt.CompareHashAndPassword([]byte(oldPass), []byte(u.Password)) 210 | if err == nil { 211 | slog.Error("new password is equal to old password", slog.String("package", "userservice")) 212 | return errors.New("new password is equal to old password") 213 | } 214 | passwordEncrypted, err := bcrypt.GenerateFromPassword([]byte(u.Password), 12) 215 | if err != nil { 216 | slog.Error("error to encrypt password", "err", err, slog.String("package", "userservice")) 217 | return errors.New("error to encrypt password") 218 | } 219 | err = s.repo.UpdatePassword(ctx, string(passwordEncrypted), id) 220 | if err != nil { 221 | slog.Error("error to update password", "err", err, slog.String("package", "userservice")) 222 | return err 223 | } 224 | return nil 225 | } 226 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | 3 | create_migration: 4 | migrate create -ext=sql -dir=internal/database/migrations -seq init 5 | 6 | migrate_up: 7 | migrate -path=internal/database/migrations -database "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?sslmode=disable" -verbose up 8 | 9 | migrate_down: 10 | migrate -path=internal/database/migrations -database "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?sslmode=disable" -verbose down 11 | 12 | .PHONY: create_migration migrate_up migrate_down 13 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Project example for api users 2 | 3 | view post part 1 [here](https://wiliamvj.com/posts/api-golang-parte-1) 4 | 5 | view post part 2 [here](https://wiliamvj.com/posts/api-golang-parte-2) 6 | 7 | view post part 3 [here](https://wiliamvj.com/posts/api-golang-parte-3) 8 | 9 | view post part 4 [here](https://wiliamvj.com/posts/api-golang-parte-4) 10 | 11 | view post part 5 [here](https://wiliamvj.com/posts/api-golang-parte-5) 12 | 13 | view post part 6 [here](https://wiliamvj.com/posts/api-golang-parte-6) 14 | 15 | view post part 7 [here](https://wiliamvj.com/posts/api-golang-parte-7) 16 | 17 | run project: 18 | ```bash 19 | go run cmd/webserver/main.go 20 | ``` 21 | 22 | generate sqcl files: 23 | ```bash 24 | sqlc generate 25 | ``` 26 | 27 | create new migration: 28 | ```bash 29 | make create_migration 30 | ``` 31 | 32 | run migrations up: 33 | ```bash 34 | make migrate_up 35 | ``` 36 | 37 | run migrations down: 38 | ```bash 39 | make migrate_down 40 | ``` 41 | -------------------------------------------------------------------------------- /sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - schema: "internal/database/migrations" 4 | queries: "internal/database/queries" 5 | engine: "postgresql" 6 | gen: 7 | go: 8 | package: "sqlc" 9 | out: "internal/database/sqlc" 10 | --------------------------------------------------------------------------------