[
  {
    "path": ".air.toml",
    "content": "# Config file for [Air](https://github.com/air-verse/air) in TOML format\n\n# Working directory\n# . or absolute path, please note that the directories following must be under root.\nroot = \".\"\ntmp_dir = \"tmp\"\n\n[build]\n# Add additional arguments when running binary (bin/full_bin).\nargs_bin = []\n# Binary file yields from `cmd`. change binary to `main.exe` if you using windows\nbin = \"./tmp/main\"\n# Just plain old shell command. You could use `make` as well. change binary to `main.exe` if you using windows\ncmd = \"go build -race -o ./tmp/main ./src\"\n# It's not necessary to trigger build each time file changes if it's too frequent.\ndelay = 1000 # ms\n# Ignore these filename extensions or directories.\nexclude_dir = [\"assets\", \"tmp\", \"vendor\"]\n# Exclude files.\nexclude_file = []\n# Exclude specific regular expressions.\nexclude_regex = [\"_test\\\\.go\"]\n# Exclude unchanged files.\nexclude_unchanged = false\n# Follow symlink for directories\nfollow_symlink = false\n# Customize binary, can setup environment variables when run your app.\nfull_bin = \"\"\n# Watch these directories if you specified.\ninclude_dir = []\n# Watch these filename extensions.\ninclude_ext = [\"go\", \"tpl\", \"tmpl\", \"env\"]\n# Watch these files.\ninclude_file = []\n# Delay after sending Interrupt signal\nkill_delay = \"0s\"\n# This log file places in your tmp_dir.\nlog = \"build-errors.log\"\n# Poll files for changes instead of using fsnotify.\npoll = false\n# Poll interval (defaults to the minimum interval of 500ms).\npoll_interval = 0 # ms\n# Array of commands to run after ^C\npost_cmd = []\n# Array of commands to run before each build\npre_cmd = []\n# Rerun binary or not\nrerun = false\n# Delay after each execution\nrerun_delay = 500\n# Send Interrupt signal before killing process (windows does not support this feature)\nsend_interrupt = false\n# Stop running old binary when build errors occur.\nstop_on_error = false\n\n[color]\n# Customize each part's color. If no color found, use the raw app log.\napp = \"\"\nbuild = \"yellow\"\nmain = \"magenta\"\nrunner = \"green\"\nwatcher = \"cyan\"\n\n[log]\n# Only show main log (silences watcher, build, runner)\nmain_only = false\n# Show log time\ntime = false\n\n[misc]\n# Delete tmp directory on exit\nclean_on_exit = true\n\n# Enable live-reloading on the browser.\n[proxy]\napp_port = 0\nenabled = false\nproxy_port = 0\n\n[screen]\nclear_on_rebuild = true\nkeep_scroll = true\n"
  },
  {
    "path": ".env.example",
    "content": "# server configuration\n# Env value : prod || dev\nAPP_ENV=dev\nAPP_HOST=0.0.0.0\nAPP_PORT=3000\nAPP_URL=http://localhost:3000\n\n# database configuration\nDB_HOST=postgresdb\nDB_USER=postgres\nDB_PASSWORD=thisisasamplepassword\nDB_NAME=fiberdb\nDB_PORT=5432\n\n# JWT\n# JWT secret key\nJWT_SECRET=thisisasamplesecret\n# Number of minutes after which an access token expires\nJWT_ACCESS_EXP_MINUTES=30\n# Number of days after which a refresh token expires\nJWT_REFRESH_EXP_DAYS=30\n# Number of minutes after which a reset password token expires\nJWT_RESET_PASSWORD_EXP_MINUTES=10\n# Number of minutes after which a verify email token expires\nJWT_VERIFY_EMAIL_EXP_MINUTES=10\n\n# SMTP configuration options for the email service\nSMTP_HOST=email-server\nSMTP_PORT=587\nSMTP_USERNAME=email-server-username\nSMTP_PASSWORD=email-server-password\nEMAIL_FROM=support@yourapp.com\n\n# OAuth2 configuration\nGOOGLE_CLIENT_ID=yourapps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=thisisasamplesecret\nREDIRECT_URL=http://localhost:3000/v1/auth/google-callback\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  push:\n    branches: [\"main\"]\n  pull_request:\n    branches: [\"main\"]\n\nenv:\n  APP_ENV: dev\n  APP_HOST: 0.0.0.0\n  APP_PORT: 3000\n  DB_HOST: localhost\n  DB_USER: postgres\n  DB_PASSWORD: thisisasamplepassword\n  DB_NAME: fiberdb\n  DB_PORT: 5432\n\njobs:\n  GoFiber:\n    runs-on: ubuntu-latest\n\n    services:\n      postgresdb:\n        image: postgres:alpine\n        env:\n          POSTGRES_USER: postgres\n          POSTGRES_PASSWORD: thisisasamplepassword\n          POSTGRES_DB: fiberdb\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd \"pg_isready -U postgres\"\n          --health-interval 10s\n          --health-timeout 20s\n          --health-retries 10\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: \"1.25\"\n\n      - name: Wait for PostgreSQL to be ready\n        run: |\n          until pg_isready -h localhost -U postgres -d fiberdb; do\n            echo \"Waiting for PostgreSQL...\"\n            sleep 5\n          done\n\n      - name: Install dependencies\n        run: go mod tidy\n\n      - name: Build Go application\n        run: CGO_ENABLED=0 GOOS=linux go build src/main.go\n        env:\n          APP_ENV: ${{ env.APP_ENV }}\n          APP_HOST: ${{ env.APP_HOST }}\n          APP_PORT: ${{ env.APP_PORT }}\n          DB_HOST: ${{ env.DB_HOST }}\n          DB_USER: ${{ env.DB_USER }}\n          DB_PASSWORD: ${{ env.DB_PASSWORD }}\n          DB_NAME: ${{ env.DB_NAME }}\n          DB_PORT: ${{ env.DB_PORT }}\n"
  },
  {
    "path": ".github/workflows/linter.yml",
    "content": "name: Linter\n\non:\n  push:\n    branches: [\"main\"]\n  pull_request:\n    branches: [\"main\"]\n\njobs:\n  Golint:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v4\n\n      - name: Run Golint\n        uses: reviewdog/action-golangci-lint@v2\n        with:\n          golangci_lint_flags: \"--config=.golangci.yml --tests=false\"\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  push:\n    branches: [\"main\"]\n  pull_request:\n    branches: [\"main\"]\n\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v4\n\n      - name: Install Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: \"1.25\"\n\n      - name: Install dependencies\n        run: go mod tidy\n\n      - name: Run Unit Test\n        run: go test ./test/unit/... -v -race\n"
  },
  {
    "path": ".gitignore",
    "content": "# Environment varibales\n.env*\n!.env*.example\n\n# Temporary\ntmp/\n\nlint.txt\nmain\nbin/golangci-lint"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nlinters:\n  default: none\n  enable:\n    - asasalint\n    - asciicheck\n    - bidichk\n    - bodyclose\n    - canonicalheader\n    - cyclop\n    - dupl\n    - durationcheck\n    - errcheck\n    - errname\n    - errorlint\n    - exhaustive\n    - fatcontext\n    - forbidigo\n    - funlen\n    - gocheckcompilerdirectives\n    - gochecksumtype\n    - gocognit\n    - goconst\n    - gocritic\n    - gocyclo\n    - gomoddirectives\n    - gomodguard\n    - goprintffuncname\n    - gosec\n    - govet\n    - ineffassign\n    - intrange\n    - lll\n    - loggercheck\n    - makezero\n    - mirror\n    - musttag\n    - nakedret\n    - nestif\n    - nilerr\n    - nilnil\n    - noctx\n    - nolintlint\n    - nonamedreturns\n    - nosprintfhostport\n    - perfsprint\n    - predeclared\n    - promlinter\n    - protogetter\n    - reassign\n    - revive\n    - rowserrcheck\n    - sloglint\n    - spancheck\n    - sqlclosecheck\n    - staticcheck\n    - testableexamples\n    - testpackage\n    - tparallel\n    - unconvert\n    - unparam\n    - unused\n    - usestdlibvars\n    - wastedassign\n    - whitespace\n  settings:\n    cyclop:\n      max-complexity: 30\n      package-average: 10\n    errcheck:\n      check-type-assertions: true\n    exhaustive:\n      check:\n        - switch\n        - map\n    exhaustruct:\n      exclude:\n        - ^net/http.Client$\n        - ^net/http.Cookie$\n        - ^net/http.Request$\n        - ^net/http.Response$\n        - ^net/http.Server$\n        - ^net/http.Transport$\n        - ^net/url.URL$\n        - ^os/exec.Cmd$\n        - ^reflect.StructField$\n        - ^github.com/Shopify/sarama.Config$\n        - ^github.com/Shopify/sarama.ProducerMessage$\n        - ^github.com/mitchellh/mapstructure.DecoderConfig$\n        - ^github.com/prometheus/client_golang/.+Opts$\n        - ^github.com/spf13/cobra.Command$\n        - ^github.com/spf13/cobra.CompletionOptions$\n        - ^github.com/stretchr/testify/mock.Mock$\n        - ^github.com/testcontainers/testcontainers-go.+Request$\n        - ^github.com/testcontainers/testcontainers-go.FromDockerfile$\n        - ^golang.org/x/tools/go/analysis.Analyzer$\n        - ^google.golang.org/protobuf/.+Options$\n        - ^gopkg.in/yaml.v3.Node$\n    funlen:\n      lines: 100\n      statements: 50\n      ignore-comments: true\n    gocognit:\n      min-complexity: 20\n    gocritic:\n      settings:\n        captLocal:\n          paramsOnly: false\n        underef:\n          skipRecvDeref: false\n    gomodguard:\n      blocked:\n        modules:\n          - github.com/golang/protobuf:\n              recommendations:\n                - google.golang.org/protobuf\n              reason: see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules\n          - github.com/satori/go.uuid:\n              recommendations:\n                - github.com/google/uuid\n              reason: satori's package is not maintained\n          - github.com/gofrs/uuid:\n              recommendations:\n                - github.com/gofrs/uuid/v5\n              reason: gofrs' package was not go module before v5\n    govet:\n      disable:\n        - fieldalignment\n      enable-all: true\n      settings:\n        shadow:\n          strict: true\n    inamedparam:\n      skip-single-param: true\n    mnd:\n      ignored-functions:\n        - args.Error\n        - flag.Arg\n        - flag.Duration.*\n        - flag.Float.*\n        - flag.Int.*\n        - flag.Uint.*\n        - os.Chmod\n        - os.Mkdir.*\n        - os.OpenFile\n        - os.WriteFile\n        - prometheus.ExponentialBuckets.*\n        - prometheus.LinearBuckets\n    nakedret:\n      max-func-lines: 0\n    nolintlint:\n      require-explanation: true\n      require-specific: true\n      allow-no-explanation:\n        - funlen\n        - gocognit\n        - lll\n    perfsprint:\n      strconcat: false\n    rowserrcheck:\n      packages:\n        - github.com/jmoiron/sqlx\n    sloglint:\n      no-global: all\n      context: scope\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    rules:\n      - linters:\n          - godot\n        source: (noinspection|TODO)\n      - linters:\n          - gocritic\n        source: //noinspection\n      - linters:\n          - lll\n        path: example\\.go\n      - linters:\n          - bodyclose\n          - dupl\n          - funlen\n          - goconst\n          - gosec\n          - lll\n          - noctx\n          - testpackage\n          - wrapcheck\n        path: _test\\.go\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\nissues:\n  max-same-issues: 50\nformatters:\n  enable:\n    - goimports\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\ngdindra13@gmail.com.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nFirst off, thank you so much for taking the time to contribute. All contributions are more than welcome!\n\n## How can I contribute?\n\nIf you have an awesome new feature that you want to implement or you found a bug that you would like to fix, here are some instructions to guide you through the process:\n\n- **Fork the repo**\n- **Clone the repo** and set it up (check out the [manual installation](https://github.com/indrayyana/go-fiber-boilerplate#manual-installation) section in README.md)\n- **Implement** the necessary changes\n- **Create tests** to keep the code coverage high\n- **Send a pull request**\n\n## Guidelines\n\n### Git commit messages\n\nFollow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for clear and structured commit messages, here are the key guidelines:\n\n- Limit the subject line to 80 characters\n- Capitalize the first letter of the subject line\n- Use the present tense (\"Add feature\" instead of \"Added feature\")\n- Separate the subject from the body with a blank line\n\n### Coding style guide\n\nWe are using [golangci-lint](https://golangci-lint.run) to ensure consistent coding standards in this project.\n\nPlease make sure that the code you are pushing conforms to the style guides mentioned above.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.25 AS build\n\nWORKDIR /app\nCOPY . .\nRUN go clean --modcache\nRUN go mod tidy\nRUN CGO_ENABLED=0 GOOS=linux go build src/main.go\n\nFROM alpine:latest\n\nRUN apk add --no-cache curl tzdata\n\nWORKDIR /root\nCOPY --from=build /app/main .\nCOPY --from=build /app/.env .\n\nEXPOSE 3000\nCMD [\"./main\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 I Gede Indra Adnyana\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "Makefile",
    "content": "include .env\nexport $(shell sed 's/=.*//' .env)\n\nstart:\n\t@go run src/main.go\nlint:\n\t@golangci-lint run\ntests:\n\t@go test -v ./test/...\ntests-%:\n\t@go test -v ./test/... -run=$(shell echo $* | sed 's/_/./g')\ntestsum:\n\t@cd test && gotestsum --format testname\nswagger:\n\t@cd src && swag init\nmigration-%:\n\t@migrate create -ext sql -dir src/database/migrations create-table-$(subst :,_,$*)\nmigrate-up:\n\t@migrate -database \"postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable\" -path src/database/migrations up\nmigrate-down:\n\t@migrate -database \"postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable\" -path src/database/migrations down\nmigrate-docker-up:\n\t@docker run -v ./src/database/migrations:/migrations --network go-fiber-boilerplate_go-network migrate/migrate -path=/migrations/ -database postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable up\nmigrate-docker-down:\n\t@docker run -v ./src/database/migrations:/migrations --network go-fiber-boilerplate_go-network migrate/migrate -path=/migrations/ -database postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable down -all\ndocker:\n\t@chmod -R 755 ./src/database/init\n\t@docker compose up --build\ndocker-test:\n\t@docker compose up -d && make tests\ndocker-down:\n\t@docker compose down --rmi all --volumes --remove-orphans\ndocker-cache:\n\t@docker builder prune -f"
  },
  {
    "path": "README.md",
    "content": "# RESTful API Go Fiber Boilerplate\n\n![Go Version](https://img.shields.io/badge/Go-1.22+-00ADD8?style=flat&logo=go)\n[![Go Report Card](https://goreportcard.com/badge/github.com/indravscode/go-fiber-boilerplate)](https://goreportcard.com/report/github.com/indravscode/go-fiber-boilerplate)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)\n![Repository size](https://img.shields.io/github/repo-size/indravscode/go-fiber-boilerplate?color=56BEB8)\n![Build](https://github.com/indravscode/go-fiber-boilerplate/workflows/Build/badge.svg)\n![Test](https://github.com/indravscode/go-fiber-boilerplate/workflows/Test/badge.svg)\n![Linter](https://github.com/indravscode/go-fiber-boilerplate/workflows/Linter/badge.svg)\n\nA boilerplate/starter project for quickly building RESTful APIs using Go, Fiber, and PostgreSQL. Inspired by the Express boilerplate.\n\nThe app comes with many built-in features, such as authentication using JWT and Google OAuth2, request validation, unit and integration tests, docker support, API documentation, pagination, etc. For more details, check the features list below.\n\n## Quick Start\n\nTo create a project, simply run:\n\n```bash\ngo mod init <project-name>\n```\n\n## Manual Installation\n\nIf you would still prefer to do the installation manually, follow these steps:\n\nClone the repo:\n\n```bash\ngit clone --depth 1 https://github.com/indravscode/go-fiber-boilerplate.git\ncd go-fiber-boilerplate\nrm -rf ./.git\n```\n\nInstall the dependencies:\n\n```bash\ngo mod tidy\n```\n\nSet the environment variables:\n\n```bash\ncp .env.example .env\n\n# open .env and modify the environment variables (if needed)\n```\n\n## Table of Contents\n\n- [Features](#features)\n- [Commands](#commands)\n- [Environment Variables](#environment-variables)\n- [Project Structure](#project-structure)\n- [API Documentation](#api-documentation)\n- [Error Handling](#error-handling)\n- [Validation](#validation)\n- [Authentication](#authentication)\n- [Authorization](#authorization)\n- [Logging](#logging)\n- [Linting](#linting)\n- [Contributing](#contributing)\n\n## Features\n\n- **SQL database**: [PostgreSQL](https://www.postgresql.org) Object Relation Mapping using [Gorm](https://gorm.io)\n- **Database migrations**: with [golang-migrate](https://github.com/golang-migrate/migrate)\n- **Validation**: request data validation using [Package validator](https://github.com/go-playground/validator)\n- **Logging**: using [Logrus](https://github.com/sirupsen/logrus) and [Fiber-Logger](https://docs.gofiber.io/api/middleware/logger)\n- **Testing**: unit and integration tests using [Testify](https://github.com/stretchr/testify) and formatted test output using [gotestsum](https://github.com/gotestyourself/gotestsum)\n- **Error handling**: centralized error handling mechanism\n- **API documentation**: with [Swag](https://github.com/swaggo/swag) and [Swagger](https://github.com/gofiber/swagger)\n- **Sending email**: using [Gomail](https://github.com/go-gomail/gomail)\n- **Environment variables**: using [Viper](https://github.com/spf13/viper)\n- **Security**: set security HTTP headers using [Fiber-Helmet](https://docs.gofiber.io/api/middleware/helmet)\n- **CORS**: Cross-Origin Resource-Sharing enabled using [Fiber-CORS](https://docs.gofiber.io/api/middleware/cors)\n- **Compression**: gzip compression with [Fiber-Compress](https://docs.gofiber.io/api/middleware/compress)\n- **Docker support**\n- **Linting**: with [golangci-lint](https://golangci-lint.run)\n\n## Commands\n\nRunning locally:\n\n```bash\nmake start\n```\n\nOr running with live reload:\n\n```bash\nair\n```\n\n> [!NOTE]\n> Make sure you have `Air` installed.\\\n> See 👉 [How to install Air](https://github.com/air-verse/air)\n\nTesting:\n\n```bash\n# run all tests\nmake tests\n\n# run all tests with gotestsum format\nmake testsum\n\n# run test for the selected function name\nmake tests-TestUserModel\n```\n\n> [!IMPORTANT]\n> Tests use a **separate test database**.\n>\n> By default, the test database name is defined in:\n> `test/init.go`\n>\n> ```go\n> DB = database.Connect(\"localhost\", \"testdb\")\n> ```\n>\n> Make sure the test database (`testdb`) **already exists** and all required\n> tables (`users`, `tokens`, etc.) have been migrated before running the test commands.\n\nDocker:\n\n```bash\n# run docker container\nmake docker\n\n# run all tests in a docker container\nmake docker-test\n```\n\nLinting:\n\n```bash\n# run lint\nmake lint\n```\n\nSwagger:\n\n```bash\n# generate the swagger documentation\nmake swagger\n```\n\nMigration:\n\n```bash\n# Create migration\nmake migration-<table-name>\n\n# Example for table users\nmake migration-users\n```\n\n```bash\n# run migration up in local\nmake migrate-up\n\n# run migration down in local\nmake migrate-down\n\n# run migration up in docker container\nmake migrate-docker-up\n\n# run migration down all in docker container\nmake migrate-docker-down\n```\n\n## Environment Variables\n\nThe environment variables can be found and modified in the `.env` file. They come with these default values:\n\n```bash\n# server configuration\n# Env value : prod || dev\nAPP_ENV=dev\nAPP_HOST=0.0.0.0\nAPP_PORT=3000\n\n# database configuration\nDB_HOST=postgresdb\nDB_USER=postgres\nDB_PASSWORD=thisisasamplepassword\nDB_NAME=fiberdb\nDB_PORT=5432\n\n# JWT\n# JWT secret key\nJWT_SECRET=thisisasamplesecret\n# Number of minutes after which an access token expires\nJWT_ACCESS_EXP_MINUTES=30\n# Number of days after which a refresh token expires\nJWT_REFRESH_EXP_DAYS=30\n# Number of minutes after which a reset password token expires\nJWT_RESET_PASSWORD_EXP_MINUTES=10\n# Number of minutes after which a verify email token expires\nJWT_VERIFY_EMAIL_EXP_MINUTES=10\n\n# SMTP configuration options for the email service\nSMTP_HOST=email-server\nSMTP_PORT=587\nSMTP_USERNAME=email-server-username\nSMTP_PASSWORD=email-server-password\nEMAIL_FROM=support@yourapp.com\n\n# OAuth2 configuration\nGOOGLE_CLIENT_ID=yourapps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=thisisasamplesecret\nREDIRECT_URL=http://localhost:3000/v1/auth/google-callback\n```\n\n## Project Structure\n\n```\nsrc\\\n |--config\\         # Environment variables and configuration related things\n |--controller\\     # Route controllers (controller layer)\n |--database\\       # Database connection & migrations\n |--docs\\           # Swagger files\n |--middleware\\     # Custom fiber middlewares\n |--model\\          # Postgres models (data layer)\n |--response\\       # Response models\n |--router\\         # Routes\n |--service\\        # Business logic (service layer)\n |--utils\\          # Utility classes and functions\n |--validation\\     # Request data validation schemas\n |--main.go         # Fiber app\n```\n\n## API Documentation\n\nTo view the list of available APIs and their specifications, run the server and go to `http://localhost:3000/v1/docs` in your browser.\n\n![Auth](https://indravscode.github.io/assets/images/swagger1.png)\n![User](https://indravscode.github.io/assets/images/swagger2.png)\n\nThis documentation page is automatically generated using the [Swag](https://github.com/swaggo/swag) definitions written as comments in the controller files.\n\nSee 👉 [Declarative Comments Format.](https://github.com/swaggo/swag#declarative-comments-format)\n\n## API Endpoints\n\nList of available routes:\n\n**Auth routes**:\\\n`POST /v1/auth/register` - register\\\n`POST /v1/auth/login` - login\\\n`POST /v1/auth/logout` - logout\\\n`POST /v1/auth/refresh-tokens` - refresh auth tokens\\\n`POST /v1/auth/forgot-password` - send reset password email\\\n`POST /v1/auth/reset-password` - reset password\\\n`POST /v1/auth/send-verification-email` - send verification email\\\n`POST /v1/auth/verify-email` - verify email\\\n`GET /v1/auth/google` - login with google account\n\n**User routes**:\\\n`POST /v1/users` - create a user\\\n`GET /v1/users` - get all users\\\n`GET /v1/users/:userId` - get user\\\n`PATCH /v1/users/:userId` - update user\\\n`DELETE /v1/users/:userId` - delete user\n\n## Error Handling\n\nThe app includes a custom error handling mechanism, which can be found in the `src/utils/error.go` file.\n\nIt also utilizes the `Fiber-Recover` middleware to gracefully recover from any panic that might occur in the handler stack, preventing the app from crashing unexpectedly.\n\nThe error handling process sends an error response in the following format:\n\n```json\n{\n  \"code\": 404,\n  \"status\": \"error\",\n  \"message\": \"Not found\"\n}\n```\n\nFiber provides a custom error struct using `fiber.NewError()`, where you can specify a response code and a message. This error can then be returned from any part of your code, and Fiber's `ErrorHandler` will automatically catch it.\n\nFor example, if you are trying to retrieve a user from the database but the user is not found, and you want to return a 404 error, the code might look like this:\n\n```go\nfunc (s *userService) GetUserByID(c *fiber.Ctx, id string) {\n\tuser := new(model.User)\n\n\terr := s.DB.WithContext(c.Context()).First(user, \"id = ?\", id).Error\n\n\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\treturn fiber.NewError(fiber.StatusNotFound, \"User not found\")\n\t}\n}\n```\n\n## Validation\n\nRequest data is validated using [Package validator](https://github.com/go-playground/validator). Check the [documentation](https://pkg.go.dev/github.com/go-playground/validator/v10) for more details on how to write validations.\n\nThe validation schemas are defined in the `src/validation` directory and are used within the services by passing them to the validation logic. In this example, the CreateUser method in the userService uses the `validation.CreateUser` schema to validate incoming request data before processing it. The validation is handled by the `Validate.Struct` method, which checks the request data against the schema.\n\n```go\nimport (\n\t\"app/src/model\"\n\t\"app/src/validation\"\n\n\t\"github.com/gofiber/fiber/v2\"\n)\n\nfunc (s *userService) CreateUser(c *fiber.Ctx, req validation.CreateUser) (*model.User, error) {\n\tif err := s.Validate.Struct(&req); err != nil {\n\t\treturn nil, err\n\t}\n}\n```\n\n## Authentication\n\nTo require authentication for certain routes, you can use the `Auth` middleware.\n\n```go\nimport (\n\t\"app/src/controllers\"\n\tm \"app/src/middleware\"\n\t\"app/src/services\"\n\n\t\"github.com/gofiber/fiber/v2\"\n)\n\nfunc SetupRoutes(app *fiber.App, u services.UserService, t services.TokenService) {\n  userController := controllers.NewUserController(u, t)\n\tapp.Post(\"/users\", m.Auth(u), userController.CreateUser)\n}\n```\n\nThese routes require a valid JWT access token in the Authorization request header using the Bearer schema. If the request does not contain a valid access token, an Unauthorized (401) error is thrown.\n\n**Generating Access Tokens**:\n\nAn access token can be generated by making a successful call to the register (`POST /v1/auth/register`) or login (`POST /v1/auth/login`) endpoints. The response of these endpoints also contains refresh tokens (explained below).\n\nAn access token is valid for 30 minutes. You can modify this expiration time by changing the `JWT_ACCESS_EXP_MINUTES` environment variable in the .env file.\n\n**Refreshing Access Tokens**:\n\nAfter the access token expires, a new access token can be generated, by making a call to the refresh token endpoint (`POST /v1/auth/refresh-tokens`) and sending along a valid refresh token in the request body. This call returns a new access token and a new refresh token.\n\nA refresh token is valid for 30 days. You can modify this expiration time by changing the `JWT_REFRESH_EXP_DAYS` environment variable in the .env file.\n\n## Authorization\n\nThe `Auth` middleware can also be used to require certain rights/permissions to access a route.\n\n```go\nimport (\n\t\"app/src/controllers\"\n\tm \"app/src/middleware\"\n\t\"app/src/services\"\n\n\t\"github.com/gofiber/fiber/v2\"\n)\n\nfunc SetupRoutes(app *fiber.App, u services.UserService, t services.TokenService) {\n  userController := controllers.NewUserController(u, t)\n\tapp.Post(\"/users\", m.Auth(u, \"manageUsers\"), userController.CreateUser)\n}\n```\n\nIn the example above, an authenticated user can access this route only if that user has the `manageUsers` permission.\n\nThe permissions are role-based. You can view the permissions/rights of each role in the `src/config/roles.go` file.\n\nIf the user making the request does not have the required permissions to access this route, a Forbidden (403) error is thrown.\n\n## Logging\n\nImport the logger from `src/utils/logrus.go`. It is using the [Logrus](https://github.com/sirupsen/logrus) logging library.\n\nLogging should be done according to the following severity levels (ascending order from most important to least important):\n\n```go\nimport \"app/src/utils\"\n\nutils.Log.Panic('message') // Calls panic() after logging\nutils.Log.Fatal('message'); // Calls os.Exit(1) after logging\nutils.Log.Error('message');\nutils.Log.Warn('message');\nutils.Log.Info('message');\nutils.Log.Debug('message');\nutils.Log.Trace('message');\n```\n\n> [!NOTE]\n> API request information (request url, response code, timestamp, etc.) are also automatically logged (using [Fiber-Logger](https://docs.gofiber.io/api/middleware/logger)).\n\n## Linting\n\nLinting is done using [golangci-lint](https://golangci-lint.run)\n\nSee 👉 [How to install golangci-lint](https://golangci-lint.run/welcome/install)\n\nTo modify the golangci-lint configuration, update the `.golangci.yml` file.\n\n## Contributing\n\nContributions are more than welcome! Please check out the [contributing guide](CONTRIBUTING.md).\n\nIf you find this boilerplate useful, consider giving it a star! ⭐\n\n## Inspirations\n\n- [hagopj13/node-express-boilerplate](https://github.com/hagopj13/node-express-boilerplate)\n- [khannedy/golang-clean-architecture](https://github.com/khannedy/golang-clean-architecture)\n- [zexoverz/express-prisma-template](https://github.com/zexoverz/express-prisma-template)\n\n## License\n\n[MIT](LICENSE)\n\n## Contributors\n\n[![Contributors](https://contrib.rocks/image?c=6&repo=indravscode/go-fiber-boilerplate)](https://github.com/indravscode/go-fiber-boilerplate/graphs/contributors)\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  adminer:\n    image: adminer\n    restart: always\n    ports:\n      - 8080:8080\n    networks:\n      - go-network\n\n  postgresdb:\n    image: postgres:alpine\n    restart: always\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U ${DB_USER}\"]\n      timeout: 20s\n      retries: 10\n    ports:\n      - ${DB_PORT}:5432\n    environment:\n      - POSTGRES_USER=${DB_USER}\n      - POSTGRES_PASSWORD=${DB_PASSWORD}\n      - POSTGRES_DB=${DB_NAME}\n    volumes:\n      - dbdata:/var/lib/postgresql/data\n      - ./src/database/init:/docker-entrypoint-initdb.d\n    networks:\n      - go-network\n\n  go-app:\n    build: .\n    image: go-app\n    ports:\n      - ${APP_PORT}:3000\n    depends_on:\n      postgresdb:\n        condition: service_healthy\n    volumes:\n      - .:/usr/src/go-app\n    restart: on-failure\n    env_file:\n      - .env\n    networks:\n      - go-network\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"${APP_URL}/v1/health-check\"]\n      interval: 40s\n      timeout: 30s\n      retries: 3\n      start_period: 30s\n\nvolumes:\n  dbdata:\n\nnetworks:\n  go-network:\n    driver: bridge\n"
  },
  {
    "path": "go.mod",
    "content": "module app\n\ngo 1.24.0\n\nrequire (\n\tgithub.com/bytedance/sonic v1.14.2\n\tgithub.com/go-playground/validator/v10 v10.29.0\n\tgithub.com/gofiber/contrib/jwt v1.1.2\n\tgithub.com/gofiber/fiber/v2 v2.52.10\n\tgithub.com/gofiber/swagger v1.1.1\n\tgithub.com/golang-jwt/jwt/v5 v5.3.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgithub.com/spf13/viper v1.21.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/swaggo/swag v1.16.6\n\tgolang.org/x/crypto v0.46.0\n\tgolang.org/x/oauth2 v0.34.0\n\tgopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df\n\tgorm.io/driver/postgres v1.6.0\n\tgorm.io/gorm v1.31.1\n)\n\nrequire (\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tgithub.com/KyleBanks/depth v1.2.1 // indirect\n\tgithub.com/MicahParks/keyfunc/v2 v2.1.0 // indirect\n\tgithub.com/andybalholm/brotli v1.2.0 // indirect\n\tgithub.com/bytedance/gopkg v0.1.3 // indirect\n\tgithub.com/bytedance/sonic/loader v0.4.0 // indirect\n\tgithub.com/clipperhouse/stringish v0.1.1 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.3.0 // indirect\n\tgithub.com/cloudwego/base64x v0.1.6 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.12 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.22.4 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.4 // indirect\n\tgithub.com/go-openapi/spec v0.22.2 // indirect\n\tgithub.com/go-openapi/swag/conv v0.25.4 // indirect\n\tgithub.com/go-openapi/swag/jsonname v0.25.4 // indirect\n\tgithub.com/go-openapi/swag/jsonutils v0.25.4 // indirect\n\tgithub.com/go-openapi/swag/loading v0.25.4 // indirect\n\tgithub.com/go-openapi/swag/stringutils v0.25.4 // indirect\n\tgithub.com/go-openapi/swag/typeutils v0.25.4 // indirect\n\tgithub.com/go-openapi/swag/yamlutils v0.25.4 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.7.6 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.2 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/klauspost/compress v1.18.2 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.19 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/sagikazarmark/locafero v0.12.0 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/swaggo/files/v2 v2.0.2 // indirect\n\tgithub.com/tinylib/msgp v1.6.1 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.68.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/arch v0.23.0 // indirect\n\tgolang.org/x/mod v0.31.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.39.0 // indirect\n\tgolang.org/x/text v0.32.0 // indirect\n\tgolang.org/x/tools v0.40.0 // indirect\n\tgopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ngithub.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=\ngithub.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=\ngithub.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=\ngithub.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=\ngithub.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=\ngithub.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=\ngithub.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=\ngithub.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=\ngithub.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=\ngithub.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=\ngithub.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=\ngithub.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=\ngithub.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=\ngithub.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=\ngithub.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=\ngithub.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=\ngithub.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=\ngithub.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=\ngithub.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=\ngithub.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=\ngithub.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=\ngithub.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc=\ngithub.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=\ngithub.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=\ngithub.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=\ngithub.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=\ngithub.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=\ngithub.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=\ngithub.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=\ngithub.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=\ngithub.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=\ngithub.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=\ngithub.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=\ngithub.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=\ngithub.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=\ngithub.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=\ngithub.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=\ngithub.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=\ngithub.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=\ngithub.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk=\ngithub.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4=\ngithub.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=\ngithub.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gofiber/contrib/jwt v1.1.2 h1:GmWnOqT4A15EkA8IPXwSpvNUXZR4u5SMj+geBmyLAjs=\ngithub.com/gofiber/contrib/jwt v1.1.2/go.mod h1:CpIwrkUQ3Q6IP8y9n3f0wP9bOnSKx39EDp2fBVgMFVk=\ngithub.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=\ngithub.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=\ngithub.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA=\ngithub.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw=\ngithub.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=\ngithub.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=\ngithub.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=\ngithub.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=\ngithub.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=\ngithub.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=\ngithub.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=\ngithub.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=\ngithub.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=\ngithub.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=\ngithub.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=\ngithub.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=\ngithub.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=\ngithub.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=\ngolang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=\ngolang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=\ngolang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=\ngolang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=\ngolang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=\ngolang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=\ngolang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=\ngolang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=\ngolang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=\ngolang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=\ngolang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=\ngopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=\ngopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=\ngopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=\ngorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=\ngorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=\ngorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=\n"
  },
  {
    "path": "src/config/config.go",
    "content": "package config\n\nimport (\n\t\"app/src/utils\"\n\n\t\"github.com/spf13/viper\"\n)\n\nvar (\n\tIsProd              bool\n\tAppHost             string\n\tAppPort             int\n\tDBHost              string\n\tDBUser              string\n\tDBPassword          string\n\tDBName              string\n\tDBPort              int\n\tJWTSecret           string\n\tJWTAccessExp        int\n\tJWTRefreshExp       int\n\tJWTResetPasswordExp int\n\tJWTVerifyEmailExp   int\n\tSMTPHost            string\n\tSMTPPort            int\n\tSMTPUsername        string\n\tSMTPPassword        string\n\tEmailFrom           string\n\tGoogleClientID      string\n\tGoogleClientSecret  string\n\tRedirectURL         string\n)\n\nfunc init() {\n\tloadConfig()\n\n\t// server configuration\n\tIsProd = viper.GetString(\"APP_ENV\") == \"prod\"\n\tAppHost = viper.GetString(\"APP_HOST\")\n\tAppPort = viper.GetInt(\"APP_PORT\")\n\n\t// database configuration\n\tDBHost = viper.GetString(\"DB_HOST\")\n\tDBUser = viper.GetString(\"DB_USER\")\n\tDBPassword = viper.GetString(\"DB_PASSWORD\")\n\tDBName = viper.GetString(\"DB_NAME\")\n\tDBPort = viper.GetInt(\"DB_PORT\")\n\n\t// jwt configuration\n\tJWTSecret = viper.GetString(\"JWT_SECRET\")\n\tJWTAccessExp = viper.GetInt(\"JWT_ACCESS_EXP_MINUTES\")\n\tJWTRefreshExp = viper.GetInt(\"JWT_REFRESH_EXP_DAYS\")\n\tJWTResetPasswordExp = viper.GetInt(\"JWT_RESET_PASSWORD_EXP_MINUTES\")\n\tJWTVerifyEmailExp = viper.GetInt(\"JWT_VERIFY_EMAIL_EXP_MINUTES\")\n\n\t// SMTP configuration\n\tSMTPHost = viper.GetString(\"SMTP_HOST\")\n\tSMTPPort = viper.GetInt(\"SMTP_PORT\")\n\tSMTPUsername = viper.GetString(\"SMTP_USERNAME\")\n\tSMTPPassword = viper.GetString(\"SMTP_PASSWORD\")\n\tEmailFrom = viper.GetString(\"EMAIL_FROM\")\n\n\t// oauth2 configuration\n\tGoogleClientID = viper.GetString(\"GOOGLE_CLIENT_ID\")\n\tGoogleClientSecret = viper.GetString(\"GOOGLE_CLIENT_SECRET\")\n\tRedirectURL = viper.GetString(\"REDIRECT_URL\")\n}\n\nfunc loadConfig() {\n\tconfigPaths := []string{\n\t\t\"./\",     // For app\n\t\t\"../../\", // For test folder\n\t}\n\n\tfor _, path := range configPaths {\n\t\tviper.SetConfigFile(path + \".env\")\n\n\t\tif err := viper.ReadInConfig(); err == nil {\n\t\t\tutils.Log.Infof(\"Config file loaded from %s\", path)\n\t\t\treturn\n\t\t}\n\t}\n\n\tutils.Log.Error(\"Failed to load any config file\")\n}\n"
  },
  {
    "path": "src/config/fiber.go",
    "content": "package config\n\nimport (\n\t\"app/src/utils\"\n\n\t\"github.com/bytedance/sonic\"\n\t\"github.com/gofiber/fiber/v2\"\n)\n\nfunc FiberConfig() fiber.Config {\n\treturn fiber.Config{\n\t\tPrefork:       IsProd,\n\t\tCaseSensitive: true,\n\t\tServerHeader:  \"Fiber\",\n\t\tAppName:       \"Fiber API\",\n\t\tErrorHandler:  utils.ErrorHandler,\n\t\tJSONEncoder:   sonic.Marshal,\n\t\tJSONDecoder:   sonic.Unmarshal,\n\t}\n}\n"
  },
  {
    "path": "src/config/oauth2.go",
    "content": "package config\n\nimport (\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/google\"\n)\n\ntype Config struct {\n\tGoogleLoginConfig oauth2.Config\n}\n\nvar AppConfig Config\n\nfunc GoogleConfig() oauth2.Config {\n\tAppConfig.GoogleLoginConfig = oauth2.Config{\n\t\tRedirectURL:  RedirectURL,\n\t\tClientID:     GoogleClientID,\n\t\tClientSecret: GoogleClientSecret,\n\t\tScopes: []string{\n\t\t\t\"https://www.googleapis.com/auth/userinfo.email\",\n\t\t\t\"https://www.googleapis.com/auth/userinfo.profile\",\n\t\t},\n\t\tEndpoint: google.Endpoint,\n\t}\n\n\treturn AppConfig.GoogleLoginConfig\n}\n"
  },
  {
    "path": "src/config/roles.go",
    "content": "package config\n\nvar allRoles = map[string][]string{\n\t\"user\":  {},\n\t\"admin\": {\"getUsers\", \"manageUsers\"},\n}\n\nvar Roles = getKeys(allRoles)\nvar RoleRights = allRoles\n\nfunc getKeys(m map[string][]string) []string {\n\tkeys := make([]string, 0, len(m))\n\tfor k := range m {\n\t\tkeys = append(keys, k)\n\t}\n\treturn keys\n}\n"
  },
  {
    "path": "src/config/tokens.go",
    "content": "package config\n\nconst (\n\tTokenTypeAccess        = \"access\"\n\tTokenTypeRefresh       = \"refresh\"\n\tTokenTypeResetPassword = \"resetPassword\"\n\tTokenTypeVerifyEmail   = \"verifyEmail\"\n)\n"
  },
  {
    "path": "src/controller/auth_controller.go",
    "content": "package controller\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/model\"\n\t\"app/src/response\"\n\t\"app/src/service\"\n\t\"app/src/validation\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/google/uuid\"\n)\n\ntype AuthController struct {\n\tAuthService  service.AuthService\n\tUserService  service.UserService\n\tTokenService service.TokenService\n\tEmailService service.EmailService\n}\n\nfunc NewAuthController(\n\tauthService service.AuthService, userService service.UserService,\n\ttokenService service.TokenService, emailService service.EmailService,\n) *AuthController {\n\treturn &AuthController{\n\t\tAuthService:  authService,\n\t\tUserService:  userService,\n\t\tTokenService: tokenService,\n\t\tEmailService: emailService,\n\t}\n}\n\n// @Tags         Auth\n// @Summary      Register as user\n// @Accept       json\n// @Produce      json\n// @Param        request  body  validation.Register  true  \"Request body\"\n// @Router       /auth/register [post]\n// @Success      201  {object}  example.RegisterResponse\n// @Failure      409  {object}  example.DuplicateEmail  \"Email already taken\"\nfunc (a *AuthController) Register(c *fiber.Ctx) error {\n\treq := new(validation.Register)\n\n\tif err := c.BodyParser(req); err != nil {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid request body\")\n\t}\n\n\tuser, err := a.AuthService.Register(c, req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttokens, err := a.TokenService.GenerateAuthTokens(c, user)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusCreated).\n\t\tJSON(response.SuccessWithTokens{\n\t\t\tCode:    fiber.StatusCreated,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"Register successfully\",\n\t\t\tUser:    *user,\n\t\t\tTokens:  *tokens,\n\t\t})\n}\n\n// @Tags         Auth\n// @Summary      Login\n// @Accept       json\n// @Produce      json\n// @Param        request  body  validation.Login  true  \"Request body\"\n// @Router       /auth/login [post]\n// @Success      200  {object}  example.LoginResponse\n// @Failure      401  {object}  example.FailedLogin  \"Invalid email or password\"\nfunc (a *AuthController) Login(c *fiber.Ctx) error {\n\treq := new(validation.Login)\n\n\tif err := c.BodyParser(req); err != nil {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid request body\")\n\t}\n\n\tuser, err := a.AuthService.Login(c, req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttokens, err := a.TokenService.GenerateAuthTokens(c, user)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.SuccessWithTokens{\n\t\t\tCode:    fiber.StatusOK,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"Login successfully\",\n\t\t\tUser:    *user,\n\t\t\tTokens:  *tokens,\n\t\t})\n}\n\n// @Tags         Auth\n// @Summary      Logout\n// @Accept       json\n// @Produce      json\n// @Param        request  body  example.RefreshToken  true  \"Request body\"\n// @Router       /auth/logout [post]\n// @Success      200  {object}  example.LogoutResponse\n// @Failure      404  {object}  example.NotFound  \"Not found\"\nfunc (a *AuthController) Logout(c *fiber.Ctx) error {\n\treq := new(validation.Logout)\n\n\tif err := c.BodyParser(req); err != nil {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid request body\")\n\t}\n\n\tif err := a.AuthService.Logout(c, req); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.Common{\n\t\t\tCode:    fiber.StatusOK,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"Logout successfully\",\n\t\t})\n}\n\n// @Tags         Auth\n// @Summary      Refresh auth tokens\n// @Accept       json\n// @Produce      json\n// @Param        request  body  example.RefreshToken  true  \"Request body\"\n// @Router       /auth/refresh-tokens [post]\n// @Success      200  {object}  example.RefreshTokenResponse\n// @Failure      401  {object}  example.Unauthorized  \"Unauthorized\"\nfunc (a *AuthController) RefreshTokens(c *fiber.Ctx) error {\n\treq := new(validation.RefreshToken)\n\n\tif err := c.BodyParser(req); err != nil {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid request body\")\n\t}\n\n\ttokens, err := a.AuthService.RefreshAuth(c, req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.RefreshToken{\n\t\t\tCode:   fiber.StatusOK,\n\t\t\tStatus: \"success\",\n\t\t\tTokens: *tokens,\n\t\t})\n}\n\n// @Tags         Auth\n// @Summary      Forgot password\n// @Description  An email will be sent to reset password.\n// @Accept       json\n// @Produce      json\n// @Param        request  body  validation.ForgotPassword  true  \"Request body\"\n// @Router       /auth/forgot-password [post]\n// @Success      200  {object}  example.ForgotPasswordResponse\n// @Failure      404  {object}  example.NotFound  \"Not found\"\nfunc (a *AuthController) ForgotPassword(c *fiber.Ctx) error {\n\treq := new(validation.ForgotPassword)\n\n\tif err := c.BodyParser(req); err != nil {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid request body\")\n\t}\n\n\tresetPasswordToken, err := a.TokenService.GenerateResetPasswordToken(c, req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif errEmail := a.EmailService.SendResetPasswordEmail(req.Email, resetPasswordToken); errEmail != nil {\n\t\treturn errEmail\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.Common{\n\t\t\tCode:    fiber.StatusOK,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"A password reset link has been sent to your email address.\",\n\t\t})\n}\n\n// @Tags         Auth\n// @Summary      Reset password\n// @Accept       json\n// @Produce      json\n// @Param        token   query  string  true  \"The reset password token\"\n// @Param        request  body  validation.UpdatePassOrVerify  true  \"Request body\"\n// @Router       /auth/reset-password [post]\n// @Success      200  {object}  example.ResetPasswordResponse\n// @Failure      401  {object}  example.FailedResetPassword  \"Password reset failed\"\nfunc (a *AuthController) ResetPassword(c *fiber.Ctx) error {\n\treq := new(validation.UpdatePassOrVerify)\n\tquery := &validation.Token{\n\t\tToken: c.Query(\"token\"),\n\t}\n\n\tif err := c.BodyParser(req); err != nil {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid request body\")\n\t}\n\n\tif err := a.AuthService.ResetPassword(c, query, req); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.Common{\n\t\t\tCode:    fiber.StatusOK,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"Update password successfully\",\n\t\t})\n}\n\n// @Tags         Auth\n// @Summary      Send verification email\n// @Description  An email will be sent to verify email.\n// @Security BearerAuth\n// @Produce      json\n// @Router       /auth/send-verification-email [post]\n// @Success      200  {object}  example.SendVerificationEmailResponse\n// @Failure      401  {object}  example.Unauthorized  \"Unauthorized\"\nfunc (a *AuthController) SendVerificationEmail(c *fiber.Ctx) error {\n\tuser, _ := c.Locals(\"user\").(*model.User)\n\n\tverifyEmailToken, err := a.TokenService.GenerateVerifyEmailToken(c, user)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif errEmail := a.EmailService.SendVerificationEmail(user.Email, *verifyEmailToken); errEmail != nil {\n\t\treturn errEmail\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.Common{\n\t\t\tCode:    fiber.StatusOK,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"Please check your email for a link to verify your account\",\n\t\t})\n}\n\n// @Tags         Auth\n// @Summary      Verify email\n// @Produce      json\n// @Param        token   query  string  true  \"The verify email token\"\n// @Router       /auth/verify-email [post]\n// @Success      200  {object}  example.VerifyEmailResponse\n// @Failure      401  {object}  example.FailedVerifyEmail  \"Verify email failed\"\nfunc (a *AuthController) VerifyEmail(c *fiber.Ctx) error {\n\tquery := &validation.Token{\n\t\tToken: c.Query(\"token\"),\n\t}\n\n\tif err := a.AuthService.VerifyEmail(c, query); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.Common{\n\t\t\tCode:    fiber.StatusOK,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"Verify email successfully\",\n\t\t})\n}\n\n// @Tags         Auth\n// @Summary      Login with google\n// @Description  This route initiates the Google OAuth2 login flow. Please try this in your browser.\n// @Router       /auth/google [get]\n// @Success      200  {object}  example.GoogleLoginResponse\nfunc (a *AuthController) GoogleLogin(c *fiber.Ctx) error {\n\t// Generate a random state\n\tstate := uuid.New().String()\n\n\tc.Cookie(&fiber.Cookie{\n\t\tName:   \"oauth_state\",\n\t\tValue:  state,\n\t\tMaxAge: 30,\n\t})\n\n\turl := config.AppConfig.GoogleLoginConfig.AuthCodeURL(state)\n\n\treturn c.Status(fiber.StatusSeeOther).Redirect(url)\n}\n\nfunc (a *AuthController) GoogleCallback(c *fiber.Ctx) error {\n\tstate := c.Query(\"state\")\n\tstoredState := c.Cookies(\"oauth_state\")\n\n\tif state != storedState {\n\t\treturn fiber.NewError(fiber.StatusUnauthorized, \"States don't Match!\")\n\t}\n\n\tcode := c.Query(\"code\")\n\tgooglecon := config.GoogleConfig()\n\n\ttoken, err := googlecon.Exchange(context.Background(), code)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq, err := http.NewRequestWithContext(\n\t\tc.Context(), http.MethodGet,\n\t\t\"https://www.googleapis.com/oauth2/v2/userinfo?access_token=\"+token.AccessToken,\n\t\tnil,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tuserData, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgoogleUser := new(validation.GoogleLogin)\n\tif errJSON := json.Unmarshal(userData, googleUser); errJSON != nil {\n\t\treturn errJSON\n\t}\n\n\tuser, err := a.UserService.CreateGoogleUser(c, googleUser)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttokens, err := a.TokenService.GenerateAuthTokens(c, user)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.SuccessWithTokens{\n\t\t\tCode:    fiber.StatusOK,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"Login successfully\",\n\t\t\tUser:    *user,\n\t\t\tTokens:  *tokens,\n\t\t})\n\n\t// TODO: replace this url with the link to the oauth google success page of your front-end app\n\t// googleLoginURL := fmt.Sprintf(\"http://link-to-app/google/success?access_token=%s&refresh_token=%s\",\n\t// \ttokens.Access.Token, tokens.Refresh.Token)\n\n\t// return c.Status(fiber.StatusSeeOther).Redirect(googleLoginURL)\n}\n"
  },
  {
    "path": "src/controller/health_check_controller.go",
    "content": "package controller\n\nimport (\n\t\"app/src/response\"\n\t\"app/src/service\"\n\n\t\"github.com/gofiber/fiber/v2\"\n)\n\ntype HealthCheckController struct {\n\tHealthCheckService service.HealthCheckService\n}\n\nfunc NewHealthCheckController(healthCheckService service.HealthCheckService) *HealthCheckController {\n\treturn &HealthCheckController{\n\t\tHealthCheckService: healthCheckService,\n\t}\n}\n\nfunc (h *HealthCheckController) addServiceStatus(\n\tserviceList *[]response.HealthCheck, name string, isUp bool, message *string,\n) {\n\tstatus := \"Up\"\n\n\tif !isUp {\n\t\tstatus = \"Down\"\n\t}\n\n\t*serviceList = append(*serviceList, response.HealthCheck{\n\t\tName:    name,\n\t\tStatus:  status,\n\t\tIsUp:    isUp,\n\t\tMessage: message,\n\t})\n}\n\n// @Tags Health\n// @Summary Health Check\n// @Description Check the status of services and database connections\n// @Accept json\n// @Produce json\n// @Success 200 {object} example.HealthCheckResponse\n// @Failure 500 {object} example.HealthCheckResponseError\n// @Router /health-check [get]\nfunc (h *HealthCheckController) Check(c *fiber.Ctx) error {\n\tisHealthy := true\n\tvar serviceList []response.HealthCheck\n\n\t// Check the database connection\n\tif err := h.HealthCheckService.GormCheck(); err != nil {\n\t\tisHealthy = false\n\t\terrMsg := err.Error()\n\t\th.addServiceStatus(&serviceList, \"Postgre\", false, &errMsg)\n\t} else {\n\t\th.addServiceStatus(&serviceList, \"Postgre\", true, nil)\n\t}\n\n\tif err := h.HealthCheckService.MemoryHeapCheck(); err != nil {\n\t\tisHealthy = false\n\t\terrMsg := err.Error()\n\t\th.addServiceStatus(&serviceList, \"Memory\", false, &errMsg)\n\t} else {\n\t\th.addServiceStatus(&serviceList, \"Memory\", true, nil)\n\t}\n\n\t// Return the response based on health check result\n\tstatusCode := fiber.StatusOK\n\tstatus := \"success\"\n\n\tif !isHealthy {\n\t\tstatusCode = fiber.StatusInternalServerError\n\t\tstatus = \"error\"\n\t}\n\n\treturn c.Status(statusCode).JSON(response.HealthCheckResponse{\n\t\tStatus:    status,\n\t\tMessage:   \"Health check completed\",\n\t\tCode:      statusCode,\n\t\tIsHealthy: isHealthy,\n\t\tResult:    serviceList,\n\t})\n}\n"
  },
  {
    "path": "src/controller/user_controller.go",
    "content": "package controller\n\nimport (\n\t\"app/src/model\"\n\t\"app/src/response\"\n\t\"app/src/service\"\n\t\"app/src/validation\"\n\t\"math\"\n\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/google/uuid\"\n)\n\ntype UserController struct {\n\tUserService  service.UserService\n\tTokenService service.TokenService\n}\n\nfunc NewUserController(userService service.UserService, tokenService service.TokenService) *UserController {\n\treturn &UserController{\n\t\tUserService:  userService,\n\t\tTokenService: tokenService,\n\t}\n}\n\n// @Tags         Users\n// @Summary      Get all users\n// @Description  Only admins can retrieve all users.\n// @Security BearerAuth\n// @Produce      json\n// @Param        page     query     int     false   \"Page number\"  default(1)\n// @Param        limit    query     int     false   \"Maximum number of users\"    default(10)\n// @Param        search   query     string  false  \"Search by name or email or role\"\n// @Router       /users [get]\n// @Success      200  {object}  example.GetAllUserResponse\n// @Failure      401  {object}  example.Unauthorized  \"Unauthorized\"\n// @Failure      403  {object}  example.Forbidden  \"Forbidden\"\nfunc (u *UserController) GetUsers(c *fiber.Ctx) error {\n\tquery := &validation.QueryUser{\n\t\tPage:   c.QueryInt(\"page\", 1),\n\t\tLimit:  c.QueryInt(\"limit\", 10),\n\t\tSearch: c.Query(\"search\", \"\"),\n\t}\n\n\tusers, totalResults, err := u.UserService.GetUsers(c, query)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.SuccessWithPaginate[model.User]{\n\t\t\tCode:         fiber.StatusOK,\n\t\t\tStatus:       \"success\",\n\t\t\tMessage:      \"Get all users successfully\",\n\t\t\tResults:      users,\n\t\t\tPage:         query.Page,\n\t\t\tLimit:        query.Limit,\n\t\t\tTotalPages:   int64(math.Ceil(float64(totalResults) / float64(query.Limit))),\n\t\t\tTotalResults: totalResults,\n\t\t})\n}\n\n// @Tags         Users\n// @Summary      Get a user\n// @Description  Logged in users can fetch only their own user information. Only admins can fetch other users.\n// @Security BearerAuth\n// @Produce      json\n// @Param        id  path  string  true  \"User id\"\n// @Router       /users/{id} [get]\n// @Success      200  {object}  example.GetUserResponse\n// @Failure      401  {object}  example.Unauthorized  \"Unauthorized\"\n// @Failure      403  {object}  example.Forbidden  \"Forbidden\"\n// @Failure      404  {object}  example.NotFound  \"Not found\"\nfunc (u *UserController) GetUserByID(c *fiber.Ctx) error {\n\tuserID := c.Params(\"userId\")\n\n\tif _, err := uuid.Parse(userID); err != nil {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid user ID\")\n\t}\n\n\tuser, err := u.UserService.GetUserByID(c, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.SuccessWithUser{\n\t\t\tCode:    fiber.StatusOK,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"Get user successfully\",\n\t\t\tUser:    *user,\n\t\t})\n}\n\n// @Tags         Users\n// @Summary      Create a user\n// @Description  Only admins can create other users.\n// @Security BearerAuth\n// @Produce      json\n// @Param        request  body  validation.CreateUser  true  \"Request body\"\n// @Router       /users [post]\n// @Success      201  {object}  example.CreateUserResponse\n// @Failure      401  {object}  example.Unauthorized  \"Unauthorized\"\n// @Failure      403  {object}  example.Forbidden  \"Forbidden\"\n// @Failure      409  {object}  example.DuplicateEmail  \"Email already taken\"\nfunc (u *UserController) CreateUser(c *fiber.Ctx) error {\n\treq := new(validation.CreateUser)\n\n\tif err := c.BodyParser(req); err != nil {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid request body\")\n\t}\n\n\tuser, err := u.UserService.CreateUser(c, req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusCreated).\n\t\tJSON(response.SuccessWithUser{\n\t\t\tCode:    fiber.StatusCreated,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"Create user successfully\",\n\t\t\tUser:    *user,\n\t\t})\n}\n\n// @Tags         Users\n// @Summary      Update a user\n// @Description  Logged in users can only update their own information. Only admins can update other users.\n// @Security BearerAuth\n// @Produce      json\n// @Param        id  path  string  true  \"User id\"\n// @Param        request  body  validation.UpdateUser  true  \"Request body\"\n// @Router       /users/{id} [patch]\n// @Success      200  {object}  example.UpdateUserResponse\n// @Failure      401  {object}  example.Unauthorized  \"Unauthorized\"\n// @Failure      403  {object}  example.Forbidden  \"Forbidden\"\n// @Failure      404  {object}  example.NotFound  \"Not found\"\n// @Failure      409  {object}  example.DuplicateEmail  \"Email already taken\"\nfunc (u *UserController) UpdateUser(c *fiber.Ctx) error {\n\treq := new(validation.UpdateUser)\n\tuserID := c.Params(\"userId\")\n\n\tif _, err := uuid.Parse(userID); err != nil {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid user ID\")\n\t}\n\n\tif err := c.BodyParser(req); err != nil {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid request body\")\n\t}\n\n\tuser, err := u.UserService.UpdateUser(c, req, userID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.SuccessWithUser{\n\t\t\tCode:    fiber.StatusOK,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"Update user successfully\",\n\t\t\tUser:    *user,\n\t\t})\n}\n\n// @Tags         Users\n// @Summary      Delete a user\n// @Description  Logged in users can delete only themselves. Only admins can delete other users.\n// @Security BearerAuth\n// @Produce      json\n// @Param        id  path  string  true  \"User id\"\n// @Router       /users/{id} [delete]\n// @Success      200  {object}  example.DeleteUserResponse\n// @Failure      401  {object}  example.Unauthorized  \"Unauthorized\"\n// @Failure      403  {object}  example.Forbidden  \"Forbidden\"\n// @Failure      404  {object}  example.NotFound  \"Not found\"\nfunc (u *UserController) DeleteUser(c *fiber.Ctx) error {\n\tuserID := c.Params(\"userId\")\n\n\tif _, err := uuid.Parse(userID); err != nil {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid user ID\")\n\t}\n\n\tif err := u.TokenService.DeleteAllToken(c, userID); err != nil {\n\t\treturn err\n\t}\n\n\tif err := u.UserService.DeleteUser(c, userID); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Status(fiber.StatusOK).\n\t\tJSON(response.Common{\n\t\t\tCode:    fiber.StatusOK,\n\t\t\tStatus:  \"success\",\n\t\t\tMessage: \"Delete user successfully\",\n\t\t})\n}\n"
  },
  {
    "path": "src/database/database.go",
    "content": "package database\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/utils\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/logger\"\n)\n\nfunc Connect(dbHost, dbName string) *gorm.DB {\n\tdsn := fmt.Sprintf(\n\t\t\"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai\",\n\t\tdbHost, config.DBUser, config.DBPassword, dbName, config.DBPort,\n\t)\n\n\tdb, err := gorm.Open(postgres.Open(dsn), &gorm.Config{\n\t\tLogger:                 logger.Default.LogMode(logger.Info),\n\t\tSkipDefaultTransaction: true,\n\t\tPrepareStmt:            true,\n\t\tTranslateError:         true,\n\t})\n\tif err != nil {\n\t\tutils.Log.Errorf(\"Failed to connect to database: %+v\", err)\n\t}\n\n\tsqlDB, errDB := db.DB()\n\tif errDB != nil {\n\t\tutils.Log.Errorf(\"Failed to connect to database: %+v\", errDB)\n\t}\n\n\t// Config connection pooling\n\tsqlDB.SetMaxIdleConns(10)\n\tsqlDB.SetMaxOpenConns(100)\n\tsqlDB.SetConnMaxLifetime(60 * time.Minute)\n\n\treturn db\n}\n"
  },
  {
    "path": "src/database/init/init.sql",
    "content": "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\nCREATE DATABASE testdb;\n"
  },
  {
    "path": "src/database/migrations/20240929085103_create-table-users.down.sql",
    "content": "DROP TABLE IF EXISTS users;"
  },
  {
    "path": "src/database/migrations/20240929085103_create-table-users.up.sql",
    "content": "CREATE TABLE users(\n    id              UUID            PRIMARY KEY DEFAULT uuid_generate_v4(),\n    name            VARCHAR(255)    NOT NULL,\n    email           VARCHAR(255)    NOT NULL UNIQUE,\n    password        VARCHAR(255)    NOT NULL,\n    role            VARCHAR(255)    NOT NULL,\n    verified_email  BOOLEAN         DEFAULT FALSE  NOT NULL,\n    created_at      TIMESTAMP       DEFAULT CURRENT_TIMESTAMP  NOT NULL,\n    updated_at      TIMESTAMP       DEFAULT CURRENT_TIMESTAMP  NOT NULL\n);\n"
  },
  {
    "path": "src/database/migrations/20240929085107_create-table-tokens.down.sql",
    "content": "DROP TABLE IF EXISTS tokens;"
  },
  {
    "path": "src/database/migrations/20240929085107_create-table-tokens.up.sql",
    "content": "CREATE TABLE tokens(\n    id              UUID            PRIMARY KEY DEFAULT uuid_generate_v4(),\n    token           VARCHAR(255)    NOT NULL,\n    user_id         UUID            NOT NULL,\n    type            VARCHAR(255)    NOT NULL,\n    expires         TIMESTAMP       DEFAULT CURRENT_TIMESTAMP  NOT NULL,\n    created_at      TIMESTAMP       DEFAULT CURRENT_TIMESTAMP  NOT NULL,\n    updated_at      TIMESTAMP       DEFAULT CURRENT_TIMESTAMP  NOT NULL,\n    CONSTRAINT fk_user\n        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE\n);\n"
  },
  {
    "path": "src/docs/docs.go",
    "content": "// Package docs Code generated by swaggo/swag. DO NOT EDIT\npackage docs\n\nimport \"github.com/swaggo/swag\"\n\nconst docTemplate = `{\n    \"schemes\": {{ marshal .Schemes }},\n    \"swagger\": \"2.0\",\n    \"info\": {\n        \"description\": \"{{escape .Description}}\",\n        \"title\": \"{{.Title}}\",\n        \"contact\": {},\n        \"license\": {\n            \"name\": \"MIT\",\n            \"url\": \"https://github.com/indrayyana/go-fiber-boilerplate/blob/main/LICENSE\"\n        },\n        \"version\": \"{{.Version}}\"\n    },\n    \"host\": \"{{.Host}}\",\n    \"basePath\": \"{{.BasePath}}\",\n    \"paths\": {\n        \"/auth/forgot-password\": {\n            \"post\": {\n                \"description\": \"An email will be sent to reset password.\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Forgot password\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.ForgotPassword\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.ForgotPasswordResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.NotFound\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/google\": {\n            \"get\": {\n                \"description\": \"This route initiates the Google OAuth2 login flow. Please try this in your browser.\",\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Login with google\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.GoogleLoginResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/login\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Login\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.Login\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.LoginResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Invalid email or password\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.FailedLogin\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/logout\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Logout\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.RefreshToken\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.LogoutResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.NotFound\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/refresh-tokens\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Refresh auth tokens\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.RefreshToken\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.RefreshTokenResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/register\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Register as user\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.Register\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"201\": {\n                        \"description\": \"Created\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.RegisterResponse\"\n                        }\n                    },\n                    \"409\": {\n                        \"description\": \"Email already taken\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.DuplicateEmail\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/reset-password\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Reset password\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"The reset password token\",\n                        \"name\": \"token\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.UpdatePassOrVerify\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.ResetPasswordResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Password reset failed\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.FailedResetPassword\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/send-verification-email\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"An email will be sent to verify email.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Send verification email\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.SendVerificationEmailResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/verify-email\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Verify email\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"The verify email token\",\n                        \"name\": \"token\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.VerifyEmailResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Verify email failed\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.FailedVerifyEmail\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/health-check\": {\n            \"get\": {\n                \"description\": \"Check the status of services and database connections\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Health\"\n                ],\n                \"summary\": \"Health Check\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.HealthCheckResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.HealthCheckResponseError\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/users\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"Only admins can retrieve all users.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Users\"\n                ],\n                \"summary\": \"Get all users\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"default\": 1,\n                        \"description\": \"Page number\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"default\": 10,\n                        \"description\": \"Maximum number of users\",\n                        \"name\": \"limit\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Search by name or email or role\",\n                        \"name\": \"search\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.GetAllUserResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    },\n                    \"403\": {\n                        \"description\": \"Forbidden\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Forbidden\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"Only admins can create other users.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Users\"\n                ],\n                \"summary\": \"Create a user\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.CreateUser\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"201\": {\n                        \"description\": \"Created\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.CreateUserResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    },\n                    \"403\": {\n                        \"description\": \"Forbidden\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Forbidden\"\n                        }\n                    },\n                    \"409\": {\n                        \"description\": \"Email already taken\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.DuplicateEmail\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/users/{id}\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"Logged in users can fetch only their own user information. Only admins can fetch other users.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Users\"\n                ],\n                \"summary\": \"Get a user\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User id\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.GetUserResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    },\n                    \"403\": {\n                        \"description\": \"Forbidden\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Forbidden\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.NotFound\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"Logged in users can delete only themselves. Only admins can delete other users.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Users\"\n                ],\n                \"summary\": \"Delete a user\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User id\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.DeleteUserResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    },\n                    \"403\": {\n                        \"description\": \"Forbidden\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Forbidden\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.NotFound\"\n                        }\n                    }\n                }\n            },\n            \"patch\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"Logged in users can only update their own information. Only admins can update other users.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Users\"\n                ],\n                \"summary\": \"Update a user\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User id\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.UpdateUser\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.UpdateUserResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    },\n                    \"403\": {\n                        \"description\": \"Forbidden\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Forbidden\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.NotFound\"\n                        }\n                    },\n                    \"409\": {\n                        \"description\": \"Email already taken\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.DuplicateEmail\"\n                        }\n                    }\n                }\n            }\n        }\n    },\n    \"definitions\": {\n        \"example.CreateUserResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 201\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Create user successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.User\"\n                }\n            }\n        },\n        \"example.DeleteUserResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Delete user successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.DuplicateEmail\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 409\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Email already taken\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.FailedLogin\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 401\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Invalid email or password\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.FailedResetPassword\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 401\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Password reset failed\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.FailedVerifyEmail\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 401\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Verify email failed\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.Forbidden\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 403\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"You don't have permission to access this resource\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.ForgotPasswordResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"A password reset link has been sent to your email address.\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.GetAllUserResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"limit\": {\n                    \"type\": \"integer\",\n                    \"example\": 10\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Get all users successfully\"\n                },\n                \"page\": {\n                    \"type\": \"integer\",\n                    \"example\": 1\n                },\n                \"results\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/example.User\"\n                    }\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"total_pages\": {\n                    \"type\": \"integer\",\n                    \"example\": 1\n                },\n                \"total_results\": {\n                    \"type\": \"integer\",\n                    \"example\": 1\n                }\n            }\n        },\n        \"example.GetUserResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Get user successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.User\"\n                }\n            }\n        },\n        \"example.GoogleLoginResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Login successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"tokens\": {\n                    \"$ref\": \"#/definitions/example.Tokens\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.GoogleUser\"\n                }\n            }\n        },\n        \"example.GoogleUser\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"example\": \"fake@example.com\"\n                },\n                \"id\": {\n                    \"type\": \"string\",\n                    \"example\": \"e088d183-9eea-4a11-8d5d-74d7ec91bdf5\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"fake name\"\n                },\n                \"role\": {\n                    \"type\": \"string\",\n                    \"example\": \"user\"\n                },\n                \"verified_email\": {\n                    \"type\": \"boolean\",\n                    \"example\": true\n                }\n            }\n        },\n        \"example.HealthCheck\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"is_up\": {\n                    \"type\": \"boolean\",\n                    \"example\": true\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"Postgre\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"Up\"\n                }\n            }\n        },\n        \"example.HealthCheckError\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"is_up\": {\n                    \"type\": \"boolean\",\n                    \"example\": false\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"failed to connect to 'host=localhost user=postgres database=wrongdb': server error (FATAL: database \\\"wrongdb\\\" does not exist (SQLSTATE 3D000))\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"Postgre\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"Down\"\n                }\n            }\n        },\n        \"example.HealthCheckResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"is_healthy\": {\n                    \"type\": \"boolean\",\n                    \"example\": true\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Health check completed\"\n                },\n                \"result\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/example.HealthCheck\"\n                    }\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.HealthCheckResponseError\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 500\n                },\n                \"is_healthy\": {\n                    \"type\": \"boolean\",\n                    \"example\": false\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Health check completed\"\n                },\n                \"result\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/example.HealthCheckError\"\n                    }\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.LoginResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Login successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"tokens\": {\n                    \"$ref\": \"#/definitions/example.Tokens\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.User\"\n                }\n            }\n        },\n        \"example.LogoutResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Logout successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.NotFound\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 404\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Not found\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.RefreshToken\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"refresh_token\": {\n                    \"type\": \"string\",\n                    \"example\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg\"\n                }\n            }\n        },\n        \"example.RefreshTokenResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"tokens\": {\n                    \"$ref\": \"#/definitions/example.Tokens\"\n                }\n            }\n        },\n        \"example.RegisterResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 201\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Register successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"tokens\": {\n                    \"$ref\": \"#/definitions/example.Tokens\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.User\"\n                }\n            }\n        },\n        \"example.ResetPasswordResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Update password successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.SendVerificationEmailResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Please check your email for a link to verify your account\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.TokenExpires\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"expires\": {\n                    \"type\": \"string\",\n                    \"example\": \"2024-10-07T11:56:46.618180553Z\"\n                },\n                \"token\": {\n                    \"type\": \"string\",\n                    \"example\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg\"\n                }\n            }\n        },\n        \"example.Tokens\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access\": {\n                    \"$ref\": \"#/definitions/example.TokenExpires\"\n                },\n                \"refresh\": {\n                    \"$ref\": \"#/definitions/example.TokenExpires\"\n                }\n            }\n        },\n        \"example.Unauthorized\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 401\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Please authenticate\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.UpdateUserResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Update user successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.User\"\n                }\n            }\n        },\n        \"example.User\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"example\": \"fake@example.com\"\n                },\n                \"id\": {\n                    \"type\": \"string\",\n                    \"example\": \"e088d183-9eea-4a11-8d5d-74d7ec91bdf5\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"fake name\"\n                },\n                \"role\": {\n                    \"type\": \"string\",\n                    \"example\": \"user\"\n                },\n                \"verified_email\": {\n                    \"type\": \"boolean\",\n                    \"example\": false\n                }\n            }\n        },\n        \"example.VerifyEmailResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Verify email successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"validation.CreateUser\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"email\",\n                \"name\",\n                \"password\",\n                \"role\"\n            ],\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake@example.com\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake name\"\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 20,\n                    \"minLength\": 8,\n                    \"example\": \"password1\"\n                },\n                \"role\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"enum\": [\n                        \"user\",\n                        \"admin\"\n                    ],\n                    \"example\": \"user\"\n                }\n            }\n        },\n        \"validation.ForgotPassword\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"email\"\n            ],\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake@example.com\"\n                }\n            }\n        },\n        \"validation.Login\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"email\",\n                \"password\"\n            ],\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake@example.com\"\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 20,\n                    \"minLength\": 8,\n                    \"example\": \"password1\"\n                }\n            }\n        },\n        \"validation.Register\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"email\",\n                \"name\",\n                \"password\"\n            ],\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake@example.com\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake name\"\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 20,\n                    \"minLength\": 8,\n                    \"example\": \"password1\"\n                }\n            }\n        },\n        \"validation.UpdatePassOrVerify\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 20,\n                    \"minLength\": 8,\n                    \"example\": \"password1\"\n                }\n            }\n        },\n        \"validation.UpdateUser\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake@example.com\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake name\"\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 20,\n                    \"minLength\": 8,\n                    \"example\": \"password1\"\n                }\n            }\n        }\n    },\n    \"securityDefinitions\": {\n        \"BearerAuth\": {\n            \"description\": \"Example Value: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n            \"type\": \"apiKey\",\n            \"name\": \"Authorization\",\n            \"in\": \"header\"\n        }\n    }\n}`\n\n// SwaggerInfo holds exported Swagger Info so clients can modify it\nvar SwaggerInfo = &swag.Spec{\n\tVersion:          \"1.3.1\",\n\tHost:             \"localhost:3000\",\n\tBasePath:         \"/v1\",\n\tSchemes:          []string{},\n\tTitle:            \"go-fiber-boilerplate API documentation\",\n\tDescription:      \"\",\n\tInfoInstanceName: \"swagger\",\n\tSwaggerTemplate:  docTemplate,\n\tLeftDelim:        \"{{\",\n\tRightDelim:       \"}}\",\n}\n\nfunc init() {\n\tswag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)\n}\n"
  },
  {
    "path": "src/docs/swagger.json",
    "content": "{\n    \"swagger\": \"2.0\",\n    \"info\": {\n        \"title\": \"go-fiber-boilerplate API documentation\",\n        \"contact\": {},\n        \"license\": {\n            \"name\": \"MIT\",\n            \"url\": \"https://github.com/indrayyana/go-fiber-boilerplate/blob/main/LICENSE\"\n        },\n        \"version\": \"1.3.1\"\n    },\n    \"host\": \"localhost:3000\",\n    \"basePath\": \"/v1\",\n    \"paths\": {\n        \"/auth/forgot-password\": {\n            \"post\": {\n                \"description\": \"An email will be sent to reset password.\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Forgot password\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.ForgotPassword\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.ForgotPasswordResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.NotFound\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/google\": {\n            \"get\": {\n                \"description\": \"This route initiates the Google OAuth2 login flow. Please try this in your browser.\",\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Login with google\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.GoogleLoginResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/login\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Login\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.Login\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.LoginResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Invalid email or password\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.FailedLogin\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/logout\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Logout\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.RefreshToken\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.LogoutResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.NotFound\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/refresh-tokens\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Refresh auth tokens\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.RefreshToken\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.RefreshTokenResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/register\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Register as user\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.Register\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"201\": {\n                        \"description\": \"Created\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.RegisterResponse\"\n                        }\n                    },\n                    \"409\": {\n                        \"description\": \"Email already taken\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.DuplicateEmail\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/reset-password\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Reset password\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"The reset password token\",\n                        \"name\": \"token\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.UpdatePassOrVerify\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.ResetPasswordResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Password reset failed\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.FailedResetPassword\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/send-verification-email\": {\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"An email will be sent to verify email.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Send verification email\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.SendVerificationEmailResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/auth/verify-email\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Auth\"\n                ],\n                \"summary\": \"Verify email\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"The verify email token\",\n                        \"name\": \"token\",\n                        \"in\": \"query\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.VerifyEmailResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Verify email failed\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.FailedVerifyEmail\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/health-check\": {\n            \"get\": {\n                \"description\": \"Check the status of services and database connections\",\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Health\"\n                ],\n                \"summary\": \"Health Check\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.HealthCheckResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.HealthCheckResponseError\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/users\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"Only admins can retrieve all users.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Users\"\n                ],\n                \"summary\": \"Get all users\",\n                \"parameters\": [\n                    {\n                        \"type\": \"integer\",\n                        \"default\": 1,\n                        \"description\": \"Page number\",\n                        \"name\": \"page\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"integer\",\n                        \"default\": 10,\n                        \"description\": \"Maximum number of users\",\n                        \"name\": \"limit\",\n                        \"in\": \"query\"\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Search by name or email or role\",\n                        \"name\": \"search\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.GetAllUserResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    },\n                    \"403\": {\n                        \"description\": \"Forbidden\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Forbidden\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"Only admins can create other users.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Users\"\n                ],\n                \"summary\": \"Create a user\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.CreateUser\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"201\": {\n                        \"description\": \"Created\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.CreateUserResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    },\n                    \"403\": {\n                        \"description\": \"Forbidden\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Forbidden\"\n                        }\n                    },\n                    \"409\": {\n                        \"description\": \"Email already taken\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.DuplicateEmail\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/users/{id}\": {\n            \"get\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"Logged in users can fetch only their own user information. Only admins can fetch other users.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Users\"\n                ],\n                \"summary\": \"Get a user\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User id\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.GetUserResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    },\n                    \"403\": {\n                        \"description\": \"Forbidden\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Forbidden\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.NotFound\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"Logged in users can delete only themselves. Only admins can delete other users.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Users\"\n                ],\n                \"summary\": \"Delete a user\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User id\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.DeleteUserResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    },\n                    \"403\": {\n                        \"description\": \"Forbidden\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Forbidden\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.NotFound\"\n                        }\n                    }\n                }\n            },\n            \"patch\": {\n                \"security\": [\n                    {\n                        \"BearerAuth\": []\n                    }\n                ],\n                \"description\": \"Logged in users can only update their own information. Only admins can update other users.\",\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Users\"\n                ],\n                \"summary\": \"Update a user\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User id\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Request body\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/validation.UpdateUser\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.UpdateUserResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Unauthorized\"\n                        }\n                    },\n                    \"403\": {\n                        \"description\": \"Forbidden\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.Forbidden\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.NotFound\"\n                        }\n                    },\n                    \"409\": {\n                        \"description\": \"Email already taken\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/example.DuplicateEmail\"\n                        }\n                    }\n                }\n            }\n        }\n    },\n    \"definitions\": {\n        \"example.CreateUserResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 201\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Create user successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.User\"\n                }\n            }\n        },\n        \"example.DeleteUserResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Delete user successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.DuplicateEmail\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 409\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Email already taken\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.FailedLogin\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 401\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Invalid email or password\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.FailedResetPassword\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 401\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Password reset failed\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.FailedVerifyEmail\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 401\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Verify email failed\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.Forbidden\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 403\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"You don't have permission to access this resource\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.ForgotPasswordResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"A password reset link has been sent to your email address.\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.GetAllUserResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"limit\": {\n                    \"type\": \"integer\",\n                    \"example\": 10\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Get all users successfully\"\n                },\n                \"page\": {\n                    \"type\": \"integer\",\n                    \"example\": 1\n                },\n                \"results\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/example.User\"\n                    }\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"total_pages\": {\n                    \"type\": \"integer\",\n                    \"example\": 1\n                },\n                \"total_results\": {\n                    \"type\": \"integer\",\n                    \"example\": 1\n                }\n            }\n        },\n        \"example.GetUserResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Get user successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.User\"\n                }\n            }\n        },\n        \"example.GoogleLoginResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Login successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"tokens\": {\n                    \"$ref\": \"#/definitions/example.Tokens\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.GoogleUser\"\n                }\n            }\n        },\n        \"example.GoogleUser\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"example\": \"fake@example.com\"\n                },\n                \"id\": {\n                    \"type\": \"string\",\n                    \"example\": \"e088d183-9eea-4a11-8d5d-74d7ec91bdf5\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"fake name\"\n                },\n                \"role\": {\n                    \"type\": \"string\",\n                    \"example\": \"user\"\n                },\n                \"verified_email\": {\n                    \"type\": \"boolean\",\n                    \"example\": true\n                }\n            }\n        },\n        \"example.HealthCheck\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"is_up\": {\n                    \"type\": \"boolean\",\n                    \"example\": true\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"Postgre\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"Up\"\n                }\n            }\n        },\n        \"example.HealthCheckError\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"is_up\": {\n                    \"type\": \"boolean\",\n                    \"example\": false\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"failed to connect to 'host=localhost user=postgres database=wrongdb': server error (FATAL: database \\\"wrongdb\\\" does not exist (SQLSTATE 3D000))\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"Postgre\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"Down\"\n                }\n            }\n        },\n        \"example.HealthCheckResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"is_healthy\": {\n                    \"type\": \"boolean\",\n                    \"example\": true\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Health check completed\"\n                },\n                \"result\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/example.HealthCheck\"\n                    }\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.HealthCheckResponseError\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 500\n                },\n                \"is_healthy\": {\n                    \"type\": \"boolean\",\n                    \"example\": false\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Health check completed\"\n                },\n                \"result\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/example.HealthCheckError\"\n                    }\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.LoginResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Login successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"tokens\": {\n                    \"$ref\": \"#/definitions/example.Tokens\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.User\"\n                }\n            }\n        },\n        \"example.LogoutResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Logout successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.NotFound\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 404\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Not found\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.RefreshToken\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"refresh_token\": {\n                    \"type\": \"string\",\n                    \"example\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg\"\n                }\n            }\n        },\n        \"example.RefreshTokenResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"tokens\": {\n                    \"$ref\": \"#/definitions/example.Tokens\"\n                }\n            }\n        },\n        \"example.RegisterResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 201\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Register successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"tokens\": {\n                    \"$ref\": \"#/definitions/example.Tokens\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.User\"\n                }\n            }\n        },\n        \"example.ResetPasswordResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Update password successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.SendVerificationEmailResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Please check your email for a link to verify your account\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"example.TokenExpires\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"expires\": {\n                    \"type\": \"string\",\n                    \"example\": \"2024-10-07T11:56:46.618180553Z\"\n                },\n                \"token\": {\n                    \"type\": \"string\",\n                    \"example\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg\"\n                }\n            }\n        },\n        \"example.Tokens\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"access\": {\n                    \"$ref\": \"#/definitions/example.TokenExpires\"\n                },\n                \"refresh\": {\n                    \"$ref\": \"#/definitions/example.TokenExpires\"\n                }\n            }\n        },\n        \"example.Unauthorized\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 401\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Please authenticate\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"error\"\n                }\n            }\n        },\n        \"example.UpdateUserResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Update user successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/example.User\"\n                }\n            }\n        },\n        \"example.User\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"example\": \"fake@example.com\"\n                },\n                \"id\": {\n                    \"type\": \"string\",\n                    \"example\": \"e088d183-9eea-4a11-8d5d-74d7ec91bdf5\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"fake name\"\n                },\n                \"role\": {\n                    \"type\": \"string\",\n                    \"example\": \"user\"\n                },\n                \"verified_email\": {\n                    \"type\": \"boolean\",\n                    \"example\": false\n                }\n            }\n        },\n        \"example.VerifyEmailResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"code\": {\n                    \"type\": \"integer\",\n                    \"example\": 200\n                },\n                \"message\": {\n                    \"type\": \"string\",\n                    \"example\": \"Verify email successfully\"\n                },\n                \"status\": {\n                    \"type\": \"string\",\n                    \"example\": \"success\"\n                }\n            }\n        },\n        \"validation.CreateUser\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"email\",\n                \"name\",\n                \"password\",\n                \"role\"\n            ],\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake@example.com\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake name\"\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 20,\n                    \"minLength\": 8,\n                    \"example\": \"password1\"\n                },\n                \"role\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"enum\": [\n                        \"user\",\n                        \"admin\"\n                    ],\n                    \"example\": \"user\"\n                }\n            }\n        },\n        \"validation.ForgotPassword\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"email\"\n            ],\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake@example.com\"\n                }\n            }\n        },\n        \"validation.Login\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"email\",\n                \"password\"\n            ],\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake@example.com\"\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 20,\n                    \"minLength\": 8,\n                    \"example\": \"password1\"\n                }\n            }\n        },\n        \"validation.Register\": {\n            \"type\": \"object\",\n            \"required\": [\n                \"email\",\n                \"name\",\n                \"password\"\n            ],\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake@example.com\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake name\"\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 20,\n                    \"minLength\": 8,\n                    \"example\": \"password1\"\n                }\n            }\n        },\n        \"validation.UpdatePassOrVerify\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 20,\n                    \"minLength\": 8,\n                    \"example\": \"password1\"\n                }\n            }\n        },\n        \"validation.UpdateUser\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake@example.com\"\n                },\n                \"name\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 50,\n                    \"example\": \"fake name\"\n                },\n                \"password\": {\n                    \"type\": \"string\",\n                    \"maxLength\": 20,\n                    \"minLength\": 8,\n                    \"example\": \"password1\"\n                }\n            }\n        }\n    },\n    \"securityDefinitions\": {\n        \"BearerAuth\": {\n            \"description\": \"Example Value: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n            \"type\": \"apiKey\",\n            \"name\": \"Authorization\",\n            \"in\": \"header\"\n        }\n    }\n}"
  },
  {
    "path": "src/docs/swagger.yaml",
    "content": "basePath: /v1\ndefinitions:\n  example.CreateUserResponse:\n    properties:\n      code:\n        example: 201\n        type: integer\n      message:\n        example: Create user successfully\n        type: string\n      status:\n        example: success\n        type: string\n      user:\n        $ref: '#/definitions/example.User'\n    type: object\n  example.DeleteUserResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      message:\n        example: Delete user successfully\n        type: string\n      status:\n        example: success\n        type: string\n    type: object\n  example.DuplicateEmail:\n    properties:\n      code:\n        example: 409\n        type: integer\n      message:\n        example: Email already taken\n        type: string\n      status:\n        example: error\n        type: string\n    type: object\n  example.FailedLogin:\n    properties:\n      code:\n        example: 401\n        type: integer\n      message:\n        example: Invalid email or password\n        type: string\n      status:\n        example: error\n        type: string\n    type: object\n  example.FailedResetPassword:\n    properties:\n      code:\n        example: 401\n        type: integer\n      message:\n        example: Password reset failed\n        type: string\n      status:\n        example: error\n        type: string\n    type: object\n  example.FailedVerifyEmail:\n    properties:\n      code:\n        example: 401\n        type: integer\n      message:\n        example: Verify email failed\n        type: string\n      status:\n        example: error\n        type: string\n    type: object\n  example.Forbidden:\n    properties:\n      code:\n        example: 403\n        type: integer\n      message:\n        example: You don't have permission to access this resource\n        type: string\n      status:\n        example: error\n        type: string\n    type: object\n  example.ForgotPasswordResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      message:\n        example: A password reset link has been sent to your email address.\n        type: string\n      status:\n        example: success\n        type: string\n    type: object\n  example.GetAllUserResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      limit:\n        example: 10\n        type: integer\n      message:\n        example: Get all users successfully\n        type: string\n      page:\n        example: 1\n        type: integer\n      results:\n        items:\n          $ref: '#/definitions/example.User'\n        type: array\n      status:\n        example: success\n        type: string\n      total_pages:\n        example: 1\n        type: integer\n      total_results:\n        example: 1\n        type: integer\n    type: object\n  example.GetUserResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      message:\n        example: Get user successfully\n        type: string\n      status:\n        example: success\n        type: string\n      user:\n        $ref: '#/definitions/example.User'\n    type: object\n  example.GoogleLoginResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      message:\n        example: Login successfully\n        type: string\n      status:\n        example: success\n        type: string\n      tokens:\n        $ref: '#/definitions/example.Tokens'\n      user:\n        $ref: '#/definitions/example.GoogleUser'\n    type: object\n  example.GoogleUser:\n    properties:\n      email:\n        example: fake@example.com\n        type: string\n      id:\n        example: e088d183-9eea-4a11-8d5d-74d7ec91bdf5\n        type: string\n      name:\n        example: fake name\n        type: string\n      role:\n        example: user\n        type: string\n      verified_email:\n        example: true\n        type: boolean\n    type: object\n  example.HealthCheck:\n    properties:\n      is_up:\n        example: true\n        type: boolean\n      name:\n        example: Postgre\n        type: string\n      status:\n        example: Up\n        type: string\n    type: object\n  example.HealthCheckError:\n    properties:\n      is_up:\n        example: false\n        type: boolean\n      message:\n        example: 'failed to connect to ''host=localhost user=postgres database=wrongdb'':\n          server error (FATAL: database \"wrongdb\" does not exist (SQLSTATE 3D000))'\n        type: string\n      name:\n        example: Postgre\n        type: string\n      status:\n        example: Down\n        type: string\n    type: object\n  example.HealthCheckResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      is_healthy:\n        example: true\n        type: boolean\n      message:\n        example: Health check completed\n        type: string\n      result:\n        items:\n          $ref: '#/definitions/example.HealthCheck'\n        type: array\n      status:\n        example: success\n        type: string\n    type: object\n  example.HealthCheckResponseError:\n    properties:\n      code:\n        example: 500\n        type: integer\n      is_healthy:\n        example: false\n        type: boolean\n      message:\n        example: Health check completed\n        type: string\n      result:\n        items:\n          $ref: '#/definitions/example.HealthCheckError'\n        type: array\n      status:\n        example: error\n        type: string\n    type: object\n  example.LoginResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      message:\n        example: Login successfully\n        type: string\n      status:\n        example: success\n        type: string\n      tokens:\n        $ref: '#/definitions/example.Tokens'\n      user:\n        $ref: '#/definitions/example.User'\n    type: object\n  example.LogoutResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      message:\n        example: Logout successfully\n        type: string\n      status:\n        example: success\n        type: string\n    type: object\n  example.NotFound:\n    properties:\n      code:\n        example: 404\n        type: integer\n      message:\n        example: Not found\n        type: string\n      status:\n        example: error\n        type: string\n    type: object\n  example.RefreshToken:\n    properties:\n      refresh_token:\n        example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg\n        type: string\n    type: object\n  example.RefreshTokenResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      status:\n        example: success\n        type: string\n      tokens:\n        $ref: '#/definitions/example.Tokens'\n    type: object\n  example.RegisterResponse:\n    properties:\n      code:\n        example: 201\n        type: integer\n      message:\n        example: Register successfully\n        type: string\n      status:\n        example: success\n        type: string\n      tokens:\n        $ref: '#/definitions/example.Tokens'\n      user:\n        $ref: '#/definitions/example.User'\n    type: object\n  example.ResetPasswordResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      message:\n        example: Update password successfully\n        type: string\n      status:\n        example: success\n        type: string\n    type: object\n  example.SendVerificationEmailResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      message:\n        example: Please check your email for a link to verify your account\n        type: string\n      status:\n        example: success\n        type: string\n    type: object\n  example.TokenExpires:\n    properties:\n      expires:\n        example: \"2024-10-07T11:56:46.618180553Z\"\n        type: string\n      token:\n        example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg\n        type: string\n    type: object\n  example.Tokens:\n    properties:\n      access:\n        $ref: '#/definitions/example.TokenExpires'\n      refresh:\n        $ref: '#/definitions/example.TokenExpires'\n    type: object\n  example.Unauthorized:\n    properties:\n      code:\n        example: 401\n        type: integer\n      message:\n        example: Please authenticate\n        type: string\n      status:\n        example: error\n        type: string\n    type: object\n  example.UpdateUserResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      message:\n        example: Update user successfully\n        type: string\n      status:\n        example: success\n        type: string\n      user:\n        $ref: '#/definitions/example.User'\n    type: object\n  example.User:\n    properties:\n      email:\n        example: fake@example.com\n        type: string\n      id:\n        example: e088d183-9eea-4a11-8d5d-74d7ec91bdf5\n        type: string\n      name:\n        example: fake name\n        type: string\n      role:\n        example: user\n        type: string\n      verified_email:\n        example: false\n        type: boolean\n    type: object\n  example.VerifyEmailResponse:\n    properties:\n      code:\n        example: 200\n        type: integer\n      message:\n        example: Verify email successfully\n        type: string\n      status:\n        example: success\n        type: string\n    type: object\n  validation.CreateUser:\n    properties:\n      email:\n        example: fake@example.com\n        maxLength: 50\n        type: string\n      name:\n        example: fake name\n        maxLength: 50\n        type: string\n      password:\n        example: password1\n        maxLength: 20\n        minLength: 8\n        type: string\n      role:\n        enum:\n        - user\n        - admin\n        example: user\n        maxLength: 50\n        type: string\n    required:\n    - email\n    - name\n    - password\n    - role\n    type: object\n  validation.ForgotPassword:\n    properties:\n      email:\n        example: fake@example.com\n        maxLength: 50\n        type: string\n    required:\n    - email\n    type: object\n  validation.Login:\n    properties:\n      email:\n        example: fake@example.com\n        maxLength: 50\n        type: string\n      password:\n        example: password1\n        maxLength: 20\n        minLength: 8\n        type: string\n    required:\n    - email\n    - password\n    type: object\n  validation.Register:\n    properties:\n      email:\n        example: fake@example.com\n        maxLength: 50\n        type: string\n      name:\n        example: fake name\n        maxLength: 50\n        type: string\n      password:\n        example: password1\n        maxLength: 20\n        minLength: 8\n        type: string\n    required:\n    - email\n    - name\n    - password\n    type: object\n  validation.UpdatePassOrVerify:\n    properties:\n      password:\n        example: password1\n        maxLength: 20\n        minLength: 8\n        type: string\n    type: object\n  validation.UpdateUser:\n    properties:\n      email:\n        example: fake@example.com\n        maxLength: 50\n        type: string\n      name:\n        example: fake name\n        maxLength: 50\n        type: string\n      password:\n        example: password1\n        maxLength: 20\n        minLength: 8\n        type: string\n    type: object\nhost: localhost:3000\ninfo:\n  contact: {}\n  license:\n    name: MIT\n    url: https://github.com/indrayyana/go-fiber-boilerplate/blob/main/LICENSE\n  title: go-fiber-boilerplate API documentation\n  version: 1.3.1\npaths:\n  /auth/forgot-password:\n    post:\n      consumes:\n      - application/json\n      description: An email will be sent to reset password.\n      parameters:\n      - description: Request body\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/validation.ForgotPassword'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.ForgotPasswordResponse'\n        \"404\":\n          description: Not found\n          schema:\n            $ref: '#/definitions/example.NotFound'\n      summary: Forgot password\n      tags:\n      - Auth\n  /auth/google:\n    get:\n      description: This route initiates the Google OAuth2 login flow. Please try this\n        in your browser.\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.GoogleLoginResponse'\n      summary: Login with google\n      tags:\n      - Auth\n  /auth/login:\n    post:\n      consumes:\n      - application/json\n      parameters:\n      - description: Request body\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/validation.Login'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.LoginResponse'\n        \"401\":\n          description: Invalid email or password\n          schema:\n            $ref: '#/definitions/example.FailedLogin'\n      summary: Login\n      tags:\n      - Auth\n  /auth/logout:\n    post:\n      consumes:\n      - application/json\n      parameters:\n      - description: Request body\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/example.RefreshToken'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.LogoutResponse'\n        \"404\":\n          description: Not found\n          schema:\n            $ref: '#/definitions/example.NotFound'\n      summary: Logout\n      tags:\n      - Auth\n  /auth/refresh-tokens:\n    post:\n      consumes:\n      - application/json\n      parameters:\n      - description: Request body\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/example.RefreshToken'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.RefreshTokenResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/example.Unauthorized'\n      summary: Refresh auth tokens\n      tags:\n      - Auth\n  /auth/register:\n    post:\n      consumes:\n      - application/json\n      parameters:\n      - description: Request body\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/validation.Register'\n      produces:\n      - application/json\n      responses:\n        \"201\":\n          description: Created\n          schema:\n            $ref: '#/definitions/example.RegisterResponse'\n        \"409\":\n          description: Email already taken\n          schema:\n            $ref: '#/definitions/example.DuplicateEmail'\n      summary: Register as user\n      tags:\n      - Auth\n  /auth/reset-password:\n    post:\n      consumes:\n      - application/json\n      parameters:\n      - description: The reset password token\n        in: query\n        name: token\n        required: true\n        type: string\n      - description: Request body\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/validation.UpdatePassOrVerify'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.ResetPasswordResponse'\n        \"401\":\n          description: Password reset failed\n          schema:\n            $ref: '#/definitions/example.FailedResetPassword'\n      summary: Reset password\n      tags:\n      - Auth\n  /auth/send-verification-email:\n    post:\n      description: An email will be sent to verify email.\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.SendVerificationEmailResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/example.Unauthorized'\n      security:\n      - BearerAuth: []\n      summary: Send verification email\n      tags:\n      - Auth\n  /auth/verify-email:\n    post:\n      parameters:\n      - description: The verify email token\n        in: query\n        name: token\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.VerifyEmailResponse'\n        \"401\":\n          description: Verify email failed\n          schema:\n            $ref: '#/definitions/example.FailedVerifyEmail'\n      summary: Verify email\n      tags:\n      - Auth\n  /health-check:\n    get:\n      consumes:\n      - application/json\n      description: Check the status of services and database connections\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.HealthCheckResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/example.HealthCheckResponseError'\n      summary: Health Check\n      tags:\n      - Health\n  /users:\n    get:\n      description: Only admins can retrieve all users.\n      parameters:\n      - default: 1\n        description: Page number\n        in: query\n        name: page\n        type: integer\n      - default: 10\n        description: Maximum number of users\n        in: query\n        name: limit\n        type: integer\n      - description: Search by name or email or role\n        in: query\n        name: search\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.GetAllUserResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/example.Unauthorized'\n        \"403\":\n          description: Forbidden\n          schema:\n            $ref: '#/definitions/example.Forbidden'\n      security:\n      - BearerAuth: []\n      summary: Get all users\n      tags:\n      - Users\n    post:\n      description: Only admins can create other users.\n      parameters:\n      - description: Request body\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/validation.CreateUser'\n      produces:\n      - application/json\n      responses:\n        \"201\":\n          description: Created\n          schema:\n            $ref: '#/definitions/example.CreateUserResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/example.Unauthorized'\n        \"403\":\n          description: Forbidden\n          schema:\n            $ref: '#/definitions/example.Forbidden'\n        \"409\":\n          description: Email already taken\n          schema:\n            $ref: '#/definitions/example.DuplicateEmail'\n      security:\n      - BearerAuth: []\n      summary: Create a user\n      tags:\n      - Users\n  /users/{id}:\n    delete:\n      description: Logged in users can delete only themselves. Only admins can delete\n        other users.\n      parameters:\n      - description: User id\n        in: path\n        name: id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.DeleteUserResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/example.Unauthorized'\n        \"403\":\n          description: Forbidden\n          schema:\n            $ref: '#/definitions/example.Forbidden'\n        \"404\":\n          description: Not found\n          schema:\n            $ref: '#/definitions/example.NotFound'\n      security:\n      - BearerAuth: []\n      summary: Delete a user\n      tags:\n      - Users\n    get:\n      description: Logged in users can fetch only their own user information. Only\n        admins can fetch other users.\n      parameters:\n      - description: User id\n        in: path\n        name: id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.GetUserResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/example.Unauthorized'\n        \"403\":\n          description: Forbidden\n          schema:\n            $ref: '#/definitions/example.Forbidden'\n        \"404\":\n          description: Not found\n          schema:\n            $ref: '#/definitions/example.NotFound'\n      security:\n      - BearerAuth: []\n      summary: Get a user\n      tags:\n      - Users\n    patch:\n      description: Logged in users can only update their own information. Only admins\n        can update other users.\n      parameters:\n      - description: User id\n        in: path\n        name: id\n        required: true\n        type: string\n      - description: Request body\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/validation.UpdateUser'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/example.UpdateUserResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/example.Unauthorized'\n        \"403\":\n          description: Forbidden\n          schema:\n            $ref: '#/definitions/example.Forbidden'\n        \"404\":\n          description: Not found\n          schema:\n            $ref: '#/definitions/example.NotFound'\n        \"409\":\n          description: Email already taken\n          schema:\n            $ref: '#/definitions/example.DuplicateEmail'\n      security:\n      - BearerAuth: []\n      summary: Update a user\n      tags:\n      - Users\nsecurityDefinitions:\n  BearerAuth:\n    description: 'Example Value: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'\n    in: header\n    name: Authorization\n    type: apiKey\nswagger: \"2.0\"\n"
  },
  {
    "path": "src/main.go",
    "content": "package main\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/database\"\n\t\"app/src/middleware\"\n\t\"app/src/router\"\n\t\"app/src/utils\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/gofiber/fiber/v2/middleware/compress\"\n\t\"github.com/gofiber/fiber/v2/middleware/cors\"\n\t\"github.com/gofiber/fiber/v2/middleware/helmet\"\n\t\"gorm.io/gorm\"\n)\n\n// @title go-fiber-boilerplate API documentation\n// @version 1.3.1\n// @license.name MIT\n// @license.url https://github.com/indrayyana/go-fiber-boilerplate/blob/main/LICENSE\n// @host localhost:3000\n// @BasePath /v1\n// @securityDefinitions.apikey BearerAuth\n// @in header\n// @name Authorization\n// @description Example Value: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\nfunc main() {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tapp := setupFiberApp()\n\tdb := setupDatabase()\n\tdefer closeDatabase(db)\n\tsetupRoutes(app, db)\n\n\taddress := fmt.Sprintf(\"%s:%d\", config.AppHost, config.AppPort)\n\n\t// Start server and handle graceful shutdown\n\tserverErrors := make(chan error, 1)\n\tgo startServer(app, address, serverErrors)\n\thandleGracefulShutdown(ctx, app, serverErrors)\n}\n\nfunc setupFiberApp() *fiber.App {\n\tapp := fiber.New(config.FiberConfig())\n\n\t// Middleware setup\n\tapp.Use(\"/v1/auth\", middleware.LimiterConfig())\n\tapp.Use(middleware.LoggerConfig())\n\tapp.Use(helmet.New())\n\tapp.Use(compress.New())\n\tapp.Use(cors.New())\n\tapp.Use(middleware.RecoverConfig())\n\n\treturn app\n}\n\nfunc setupDatabase() *gorm.DB {\n\tdb := database.Connect(config.DBHost, config.DBName)\n\t// Add any additional database setup if needed\n\treturn db\n}\n\nfunc setupRoutes(app *fiber.App, db *gorm.DB) {\n\trouter.Routes(app, db)\n\tapp.Use(utils.NotFoundHandler)\n}\n\nfunc startServer(app *fiber.App, address string, errs chan<- error) {\n\tif err := app.Listen(address); err != nil {\n\t\terrs <- fmt.Errorf(\"error starting server: %w\", err)\n\t}\n}\n\nfunc closeDatabase(db *gorm.DB) {\n\tsqlDB, errDB := db.DB()\n\tif errDB != nil {\n\t\tutils.Log.Errorf(\"Error getting database instance: %v\", errDB)\n\t\treturn\n\t}\n\n\tif err := sqlDB.Close(); err != nil {\n\t\tutils.Log.Errorf(\"Error closing database connection: %v\", err)\n\t} else {\n\t\tutils.Log.Info(\"Database connection closed successfully\")\n\t}\n}\n\nfunc handleGracefulShutdown(ctx context.Context, app *fiber.App, serverErrors <-chan error) {\n\tquit := make(chan os.Signal, 1)\n\tsignal.Notify(quit, os.Interrupt, syscall.SIGTERM)\n\n\tselect {\n\tcase err := <-serverErrors:\n\t\tutils.Log.Fatalf(\"Server error: %v\", err)\n\tcase <-quit:\n\t\tutils.Log.Info(\"Shutting down server...\")\n\t\tif err := app.Shutdown(); err != nil {\n\t\t\tutils.Log.Fatalf(\"Error during server shutdown: %v\", err)\n\t\t}\n\tcase <-ctx.Done():\n\t\tutils.Log.Info(\"Server exiting due to context cancellation\")\n\t}\n\n\tutils.Log.Info(\"Server exited\")\n}\n"
  },
  {
    "path": "src/middleware/auth.go",
    "content": "package middleware\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/service\"\n\t\"app/src/utils\"\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v2\"\n)\n\nfunc Auth(userService service.UserService, requiredRights ...string) fiber.Handler {\n\treturn func(c *fiber.Ctx) error {\n\t\tauthHeader := c.Get(\"Authorization\")\n\t\ttoken := strings.TrimSpace(strings.TrimPrefix(authHeader, \"Bearer \"))\n\n\t\tif token == \"\" {\n\t\t\treturn fiber.NewError(fiber.StatusUnauthorized, \"Please authenticate\")\n\t\t}\n\n\t\tuserID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess)\n\t\tif err != nil {\n\t\t\treturn fiber.NewError(fiber.StatusUnauthorized, \"Please authenticate\")\n\t\t}\n\n\t\tuser, err := userService.GetUserByID(c, userID)\n\t\tif err != nil || user == nil {\n\t\t\treturn fiber.NewError(fiber.StatusUnauthorized, \"Please authenticate\")\n\t\t}\n\n\t\tc.Locals(\"user\", user)\n\n\t\tif len(requiredRights) > 0 {\n\t\t\tuserRights, hasRights := config.RoleRights[user.Role]\n\t\t\tif (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params(\"userId\") != userID {\n\t\t\t\treturn fiber.NewError(fiber.StatusForbidden, \"You don't have permission to access this resource\")\n\t\t\t}\n\t\t}\n\n\t\treturn c.Next()\n\t}\n}\n\nfunc hasAllRights(userRights, requiredRights []string) bool {\n\trightSet := make(map[string]struct{}, len(userRights))\n\tfor _, right := range userRights {\n\t\trightSet[right] = struct{}{}\n\t}\n\n\tfor _, right := range requiredRights {\n\t\tif _, exists := rightSet[right]; !exists {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "src/middleware/jwt.go",
    "content": "package middleware\n\nimport (\n\tjwtware \"github.com/gofiber/contrib/jwt\"\n\t\"github.com/gofiber/fiber/v2\"\n)\n\nfunc JwtConfig() fiber.Handler {\n\treturn jwtware.New(jwtware.Config{\n\t\tSigningKey: jwtware.SigningKey{Key: []byte(\"secret\")},\n\t})\n}\n"
  },
  {
    "path": "src/middleware/limiter.go",
    "content": "package middleware\n\nimport (\n\t\"app/src/response\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/gofiber/fiber/v2/middleware/limiter\"\n)\n\nfunc LimiterConfig() fiber.Handler {\n\treturn limiter.New(limiter.Config{\n\t\tMax:        20,\n\t\tExpiration: 15 * time.Minute,\n\t\tLimitReached: func(c *fiber.Ctx) error {\n\t\t\treturn c.Status(fiber.StatusTooManyRequests).\n\t\t\t\tJSON(response.Common{\n\t\t\t\t\tCode:    fiber.StatusTooManyRequests,\n\t\t\t\t\tStatus:  \"error\",\n\t\t\t\t\tMessage: \"Too many requests, please try again later\",\n\t\t\t\t})\n\t\t},\n\t\tSkipSuccessfulRequests: true,\n\t})\n}\n"
  },
  {
    "path": "src/middleware/logger.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/gofiber/fiber/v2/middleware/logger\"\n)\n\nfunc LoggerConfig() fiber.Handler {\n\treturn logger.New(logger.Config{\n\t\tFormat:     \"${time} ${method} ${status} ${path} in ${latency}\\n\",\n\t\tTimeFormat: \"15:04:05.00\",\n\t})\n}\n"
  },
  {
    "path": "src/middleware/recover.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/gofiber/fiber/v2/middleware/recover\"\n)\n\nfunc RecoverConfig() fiber.Handler {\n\treturn recover.New(recover.Config{\n\t\tEnableStackTrace: true,\n\t})\n}\n"
  },
  {
    "path": "src/model/token_model.go",
    "content": "package model\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"gorm.io/gorm\"\n)\n\ntype Token struct {\n\tID        uuid.UUID `gorm:\"primaryKey;not null\"`\n\tToken     string    `gorm:\"not null\"`\n\tUserID    uuid.UUID `gorm:\"not null\"`\n\tType      string    `gorm:\"not null\"`\n\tExpires   time.Time `gorm:\"not null\"`\n\tCreatedAt time.Time `gorm:\"autoCreateTime:milli\"`\n\tUpdatedAt time.Time `gorm:\"autoCreateTime:milli;autoUpdateTime:milli\"`\n\tUser      *User     `gorm:\"foreignKey:user_id;references:id\"`\n}\n\nfunc (token *Token) BeforeCreate(_ *gorm.DB) error {\n\ttoken.ID = uuid.New()\n\treturn nil\n}\n"
  },
  {
    "path": "src/model/user_model.go",
    "content": "package model\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"gorm.io/gorm\"\n)\n\ntype User struct {\n\tID            uuid.UUID `gorm:\"primaryKey;not null\" json:\"id\"`\n\tName          string    `gorm:\"not null\" json:\"name\"`\n\tEmail         string    `gorm:\"uniqueIndex;not null\" json:\"email\"`\n\tPassword      string    `gorm:\"not null\" json:\"-\"`\n\tRole          string    `gorm:\"default:user;not null\" json:\"role\"`\n\tVerifiedEmail bool      `gorm:\"default:false;not null\" json:\"verified_email\"`\n\tCreatedAt     time.Time `gorm:\"autoCreateTime:milli\" json:\"-\"`\n\tUpdatedAt     time.Time `gorm:\"autoCreateTime:milli;autoUpdateTime:milli\" json:\"-\"`\n\tToken         []Token   `gorm:\"foreignKey:user_id;references:id\" json:\"-\"`\n}\n\nfunc (user *User) BeforeCreate(_ *gorm.DB) error {\n\tuser.ID = uuid.New() // Generate UUID before create\n\treturn nil\n}\n"
  },
  {
    "path": "src/response/auth_response.go",
    "content": "package response\n\nimport \"time\"\n\ntype Tokens struct {\n\tAccess  TokenExpires `json:\"access\"`\n\tRefresh TokenExpires `json:\"refresh\"`\n}\n\ntype TokenExpires struct {\n\tToken   string    `json:\"token\"`\n\tExpires time.Time `json:\"expires\"`\n}\n\ntype RefreshToken struct {\n\tCode   int    `json:\"code\"`\n\tStatus string `json:\"status\"`\n\tTokens Tokens `json:\"tokens\"`\n}\n"
  },
  {
    "path": "src/response/error_response.go",
    "content": "package response\n\nimport (\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc Error(c *fiber.Ctx, statusCode int, message string, details interface{}) error {\n\tvar errRes error\n\tif details != nil {\n\t\terrRes = c.Status(statusCode).JSON(ErrorDetails{\n\t\t\tCode:    statusCode,\n\t\t\tStatus:  \"error\",\n\t\t\tMessage: message,\n\t\t\tErrors:  details,\n\t\t})\n\t} else {\n\t\terrRes = c.Status(statusCode).JSON(Common{\n\t\t\tCode:    statusCode,\n\t\t\tStatus:  \"error\",\n\t\t\tMessage: message,\n\t\t})\n\t}\n\n\tif errRes != nil {\n\t\tlogrus.Errorf(\"Failed to send error response : %+v\", errRes)\n\t}\n\n\treturn errRes\n}\n"
  },
  {
    "path": "src/response/example/error_example.go",
    "content": "package example\n\ntype Unauthorized struct {\n\tCode    int    `json:\"code\" example:\"401\"`\n\tStatus  string `json:\"status\" example:\"error\"`\n\tMessage string `json:\"message\" example:\"Please authenticate\"`\n}\n\ntype FailedLogin struct {\n\tCode    int    `json:\"code\" example:\"401\"`\n\tStatus  string `json:\"status\" example:\"error\"`\n\tMessage string `json:\"message\" example:\"Invalid email or password\"`\n}\n\ntype FailedResetPassword struct {\n\tCode    int    `json:\"code\" example:\"401\"`\n\tStatus  string `json:\"status\" example:\"error\"`\n\tMessage string `json:\"message\" example:\"Password reset failed\"`\n}\n\ntype FailedVerifyEmail struct {\n\tCode    int    `json:\"code\" example:\"401\"`\n\tStatus  string `json:\"status\" example:\"error\"`\n\tMessage string `json:\"message\" example:\"Verify email failed\"`\n}\n\ntype Forbidden struct {\n\tCode    int    `json:\"code\" example:\"403\"`\n\tStatus  string `json:\"status\" example:\"error\"`\n\tMessage string `json:\"message\" example:\"You don't have permission to access this resource\"`\n}\n\ntype NotFound struct {\n\tCode    int    `json:\"code\" example:\"404\"`\n\tStatus  string `json:\"status\" example:\"error\"`\n\tMessage string `json:\"message\" example:\"Not found\"`\n}\n\ntype DuplicateEmail struct {\n\tCode    int    `json:\"code\" example:\"409\"`\n\tStatus  string `json:\"status\" example:\"error\"`\n\tMessage string `json:\"message\" example:\"Email already taken\"`\n}\n"
  },
  {
    "path": "src/response/example/example.go",
    "content": "package example\n\ntype RegisterResponse struct {\n\tCode    int    `json:\"code\" example:\"201\"`\n\tStatus  string `json:\"status\" example:\"success\"`\n\tMessage string `json:\"message\" example:\"Register successfully\"`\n\tUser    User   `json:\"user\"`\n\tTokens  Tokens `json:\"tokens\"`\n}\n\ntype LoginResponse struct {\n\tCode    int    `json:\"code\" example:\"200\"`\n\tStatus  string `json:\"status\" example:\"success\"`\n\tMessage string `json:\"message\" example:\"Login successfully\"`\n\tUser    User   `json:\"user\"`\n\tTokens  Tokens `json:\"tokens\"`\n}\n\ntype GoogleLoginResponse struct {\n\tCode    int        `json:\"code\" example:\"200\"`\n\tStatus  string     `json:\"status\" example:\"success\"`\n\tMessage string     `json:\"message\" example:\"Login successfully\"`\n\tUser    GoogleUser `json:\"user\"`\n\tTokens  Tokens     `json:\"tokens\"`\n}\n\ntype LogoutResponse struct {\n\tCode    int    `json:\"code\" example:\"200\"`\n\tStatus  string `json:\"status\" example:\"success\"`\n\tMessage string `json:\"message\" example:\"Logout successfully\"`\n}\n\ntype RefreshTokenResponse struct {\n\tCode   int    `json:\"code\" example:\"200\"`\n\tStatus string `json:\"status\" example:\"success\"`\n\tTokens Tokens `json:\"tokens\"`\n}\n\ntype ForgotPasswordResponse struct {\n\tCode    int    `json:\"code\" example:\"200\"`\n\tStatus  string `json:\"status\" example:\"success\"`\n\tMessage string `json:\"message\" example:\"A password reset link has been sent to your email address.\"`\n}\n\ntype ResetPasswordResponse struct {\n\tCode    int    `json:\"code\" example:\"200\"`\n\tStatus  string `json:\"status\" example:\"success\"`\n\tMessage string `json:\"message\" example:\"Update password successfully\"`\n}\n\ntype SendVerificationEmailResponse struct {\n\tCode    int    `json:\"code\" example:\"200\"`\n\tStatus  string `json:\"status\" example:\"success\"`\n\tMessage string `json:\"message\" example:\"Please check your email for a link to verify your account\"`\n}\n\ntype VerifyEmailResponse struct {\n\tCode    int    `json:\"code\" example:\"200\"`\n\tStatus  string `json:\"status\" example:\"success\"`\n\tMessage string `json:\"message\" example:\"Verify email successfully\"`\n}\n\ntype GetAllUserResponse struct {\n\tCode         int    `json:\"code\" example:\"200\"`\n\tStatus       string `json:\"status\" example:\"success\"`\n\tMessage      string `json:\"message\" example:\"Get all users successfully\"`\n\tResults      []User `json:\"results\"`\n\tPage         int    `json:\"page\" example:\"1\"`\n\tLimit        int    `json:\"limit\" example:\"10\"`\n\tTotalPages   int64  `json:\"total_pages\" example:\"1\"`\n\tTotalResults int64  `json:\"total_results\" example:\"1\"`\n}\n\ntype GetUserResponse struct {\n\tCode    int    `json:\"code\" example:\"200\"`\n\tStatus  string `json:\"status\" example:\"success\"`\n\tMessage string `json:\"message\" example:\"Get user successfully\"`\n\tUser    User   `json:\"user\"`\n}\n\ntype CreateUserResponse struct {\n\tCode    int    `json:\"code\" example:\"201\"`\n\tStatus  string `json:\"status\" example:\"success\"`\n\tMessage string `json:\"message\" example:\"Create user successfully\"`\n\tUser    User   `json:\"user\"`\n}\n\ntype UpdateUserResponse struct {\n\tCode    int    `json:\"code\" example:\"200\"`\n\tStatus  string `json:\"status\" example:\"success\"`\n\tMessage string `json:\"message\" example:\"Update user successfully\"`\n\tUser    User   `json:\"user\"`\n}\n\ntype DeleteUserResponse struct {\n\tCode    int    `json:\"code\" example:\"200\"`\n\tStatus  string `json:\"status\" example:\"success\"`\n\tMessage string `json:\"message\" example:\"Delete user successfully\"`\n}\n"
  },
  {
    "path": "src/response/example/health_check_example.go",
    "content": "package example\n\ntype HealthCheck struct {\n\tName   string `json:\"name\" example:\"Postgre\"`\n\tStatus string `json:\"status\" example:\"Up\"`\n\tIsUp   bool   `json:\"is_up\" example:\"true\"`\n}\n\ntype HealthCheckResponse struct {\n\tCode      int           `json:\"code\" example:\"200\"`\n\tStatus    string        `json:\"status\" example:\"success\"`\n\tMessage   string        `json:\"message\" example:\"Health check completed\"`\n\tIsHealthy bool          `json:\"is_healthy\" example:\"true\"`\n\tResult    []HealthCheck `json:\"result\"`\n}\n\ntype HealthCheckError struct {\n\tName    string  `json:\"name\" example:\"Postgre\"`\n\tStatus  string  `json:\"status\" example:\"Down\"`\n\tIsUp    bool    `json:\"is_up\" example:\"false\"`\n\tMessage *string `json:\"message,omitempty\" example:\"failed to connect to 'host=localhost user=postgres database=wrongdb': server error (FATAL: database \\\"wrongdb\\\" does not exist (SQLSTATE 3D000))\"`\n}\n\ntype HealthCheckResponseError struct {\n\tCode      int                `json:\"code\" example:\"500\"`\n\tStatus    string             `json:\"status\" example:\"error\"`\n\tMessage   string             `json:\"message\" example:\"Health check completed\"`\n\tIsHealthy bool               `json:\"is_healthy\" example:\"false\"`\n\tResult    []HealthCheckError `json:\"result\"`\n}\n"
  },
  {
    "path": "src/response/example/token_example.go",
    "content": "package example\n\nimport \"time\"\n\ntype Tokens struct {\n\tAccess  TokenExpires `json:\"access\"`\n\tRefresh TokenExpires `json:\"refresh\"`\n}\n\ntype TokenExpires struct {\n\tToken   string    `json:\"token\" example:\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg\"`\n\tExpires time.Time `json:\"expires\" example:\"2024-10-07T11:56:46.618180553Z\"`\n}\n\ntype RefreshToken struct {\n\tRefreshToken string `json:\"refresh_token\" example:\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg\"`\n}\n"
  },
  {
    "path": "src/response/example/user_example.go",
    "content": "package example\n\nimport \"github.com/google/uuid\"\n\ntype User struct {\n\tID            uuid.UUID `json:\"id\" example:\"e088d183-9eea-4a11-8d5d-74d7ec91bdf5\"`\n\tName          string    `json:\"name\" example:\"fake name\"`\n\tEmail         string    `json:\"email\" example:\"fake@example.com\"`\n\tRole          string    `json:\"role\" example:\"user\"`\n\tVerifiedEmail bool      `json:\"verified_email\" example:\"false\"`\n}\n\ntype GoogleUser struct {\n\tID            uuid.UUID `json:\"id\" example:\"e088d183-9eea-4a11-8d5d-74d7ec91bdf5\"`\n\tName          string    `json:\"name\" example:\"fake name\"`\n\tEmail         string    `json:\"email\" example:\"fake@example.com\"`\n\tRole          string    `json:\"role\" example:\"user\"`\n\tVerifiedEmail bool      `json:\"verified_email\" example:\"true\"`\n}\n"
  },
  {
    "path": "src/response/health_check_response.go",
    "content": "package response\n\ntype HealthCheck struct {\n\tName    string  `json:\"name\"`\n\tStatus  string  `json:\"status\"`\n\tIsUp    bool    `json:\"is_up\"`\n\tMessage *string `json:\"message,omitempty\"`\n}\n\ntype HealthCheckResponse struct {\n\tCode      int           `json:\"code\"`\n\tStatus    string        `json:\"status\"`\n\tMessage   string        `json:\"message\"`\n\tIsHealthy bool          `json:\"is_healthy\"`\n\tResult    []HealthCheck `json:\"result\"`\n}\n"
  },
  {
    "path": "src/response/response.go",
    "content": "package response\n\nimport \"app/src/model\"\n\ntype Common struct {\n\tCode    int    `json:\"code\"`\n\tStatus  string `json:\"status\"`\n\tMessage string `json:\"message\"`\n}\n\ntype SuccessWithUser struct {\n\tCode    int        `json:\"code\"`\n\tStatus  string     `json:\"status\"`\n\tMessage string     `json:\"message\"`\n\tUser    model.User `json:\"user\"`\n}\n\ntype SuccessWithTokens struct {\n\tCode    int        `json:\"code\"`\n\tStatus  string     `json:\"status\"`\n\tMessage string     `json:\"message\"`\n\tUser    model.User `json:\"user\"`\n\tTokens  Tokens     `json:\"tokens\"`\n}\n\ntype SuccessWithPaginate[T any] struct {\n\tCode         int    `json:\"code\"`\n\tStatus       string `json:\"status\"`\n\tMessage      string `json:\"message\"`\n\tResults      []T    `json:\"results\"`\n\tPage         int    `json:\"page\"`\n\tLimit        int    `json:\"limit\"`\n\tTotalPages   int64  `json:\"total_pages\"`\n\tTotalResults int64  `json:\"total_results\"`\n}\n\ntype ErrorDetails struct {\n\tCode    int         `json:\"code\"`\n\tStatus  string      `json:\"status\"`\n\tMessage string      `json:\"message\"`\n\tErrors  interface{} `json:\"errors\"`\n}\n"
  },
  {
    "path": "src/response/user_response.go",
    "content": "package response\n\nimport \"github.com/google/uuid\"\n\ntype CreateUser struct {\n\tName            string `json:\"name\"`\n\tEmail           string `json:\"email\"`\n\tRole            string `json:\"role\"`\n\tIsEmailVerified bool   `json:\"is_email_verified\"`\n}\n\ntype GetUsers struct {\n\tID              uuid.UUID `json:\"id\"`\n\tName            string    `json:\"name\"`\n\tEmail           string    `json:\"email\"`\n\tRole            string    `json:\"role\"`\n\tIsEmailVerified bool      `json:\"is_email_verified\"`\n}\n"
  },
  {
    "path": "src/router/auth_route.go",
    "content": "package router\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/controller\"\n\tm \"app/src/middleware\"\n\t\"app/src/service\"\n\n\t\"github.com/gofiber/fiber/v2\"\n)\n\nfunc AuthRoutes(\n\tv1 fiber.Router, a service.AuthService, u service.UserService,\n\tt service.TokenService, e service.EmailService,\n) {\n\tauthController := controller.NewAuthController(a, u, t, e)\n\tconfig.GoogleConfig()\n\n\tauth := v1.Group(\"/auth\")\n\n\tauth.Post(\"/register\", authController.Register)\n\tauth.Post(\"/login\", authController.Login)\n\tauth.Post(\"/logout\", authController.Logout)\n\tauth.Post(\"/refresh-tokens\", authController.RefreshTokens)\n\tauth.Post(\"/forgot-password\", authController.ForgotPassword)\n\tauth.Post(\"/reset-password\", authController.ResetPassword)\n\tauth.Post(\"/send-verification-email\", m.Auth(u), authController.SendVerificationEmail)\n\tauth.Post(\"/verify-email\", authController.VerifyEmail)\n\tauth.Get(\"/google\", authController.GoogleLogin)\n\tauth.Get(\"/google-callback\", authController.GoogleCallback)\n}\n"
  },
  {
    "path": "src/router/docs_route.go",
    "content": "package router\n\nimport (\n\t// initialize the Swagger documentation\n\t_ \"app/src/docs\"\n\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/gofiber/swagger\"\n)\n\nfunc DocsRoutes(v1 fiber.Router) {\n\tdocs := v1.Group(\"/docs\")\n\n\tdocs.Get(\"/*\", swagger.HandlerDefault)\n}\n"
  },
  {
    "path": "src/router/health_check_route.go",
    "content": "package router\n\nimport (\n\t\"app/src/controller\"\n\t\"app/src/service\"\n\n\t\"github.com/gofiber/fiber/v2\"\n)\n\nfunc HealthCheckRoutes(v1 fiber.Router, h service.HealthCheckService) {\n\thealthCheckController := controller.NewHealthCheckController(h)\n\n\thealthCheck := v1.Group(\"/health-check\")\n\thealthCheck.Get(\"/\", healthCheckController.Check)\n}\n"
  },
  {
    "path": "src/router/router.go",
    "content": "package router\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/service\"\n\t\"app/src/validation\"\n\n\t\"github.com/gofiber/fiber/v2\"\n\t\"gorm.io/gorm\"\n)\n\nfunc Routes(app *fiber.App, db *gorm.DB) {\n\tvalidate := validation.Validator()\n\n\thealthCheckService := service.NewHealthCheckService(db)\n\temailService := service.NewEmailService()\n\tuserService := service.NewUserService(db, validate)\n\ttokenService := service.NewTokenService(db, validate, userService)\n\tauthService := service.NewAuthService(db, validate, userService, tokenService)\n\n\tv1 := app.Group(\"/v1\")\n\n\tHealthCheckRoutes(v1, healthCheckService)\n\tAuthRoutes(v1, authService, userService, tokenService, emailService)\n\tUserRoutes(v1, userService, tokenService)\n\t// TODO: add another routes here...\n\n\tif !config.IsProd {\n\t\tDocsRoutes(v1)\n\t}\n}\n"
  },
  {
    "path": "src/router/user_route.go",
    "content": "package router\n\nimport (\n\t\"app/src/controller\"\n\tm \"app/src/middleware\"\n\t\"app/src/service\"\n\n\t\"github.com/gofiber/fiber/v2\"\n)\n\nfunc UserRoutes(v1 fiber.Router, u service.UserService, t service.TokenService) {\n\tuserController := controller.NewUserController(u, t)\n\n\tuser := v1.Group(\"/users\")\n\n\tuser.Get(\"/\", m.Auth(u, \"getUsers\"), userController.GetUsers)\n\tuser.Post(\"/\", m.Auth(u, \"manageUsers\"), userController.CreateUser)\n\tuser.Get(\"/:userId\", m.Auth(u, \"getUsers\"), userController.GetUserByID)\n\tuser.Patch(\"/:userId\", m.Auth(u, \"manageUsers\"), userController.UpdateUser)\n\tuser.Delete(\"/:userId\", m.Auth(u, \"manageUsers\"), userController.DeleteUser)\n}\n"
  },
  {
    "path": "src/service/auth_service.go",
    "content": "package service\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/model\"\n\t\"app/src/response\"\n\t\"app/src/utils\"\n\t\"app/src/validation\"\n\t\"errors\"\n\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/sirupsen/logrus\"\n\t\"gorm.io/gorm\"\n)\n\ntype AuthService interface {\n\tRegister(c *fiber.Ctx, req *validation.Register) (*model.User, error)\n\tLogin(c *fiber.Ctx, req *validation.Login) (*model.User, error)\n\tLogout(c *fiber.Ctx, req *validation.Logout) error\n\tRefreshAuth(c *fiber.Ctx, req *validation.RefreshToken) (*response.Tokens, error)\n\tResetPassword(c *fiber.Ctx, query *validation.Token, req *validation.UpdatePassOrVerify) error\n\tVerifyEmail(c *fiber.Ctx, query *validation.Token) error\n}\n\ntype authService struct {\n\tLog          *logrus.Logger\n\tDB           *gorm.DB\n\tValidate     *validator.Validate\n\tUserService  UserService\n\tTokenService TokenService\n}\n\nfunc NewAuthService(\n\tdb *gorm.DB, validate *validator.Validate, userService UserService, tokenService TokenService,\n) AuthService {\n\treturn &authService{\n\t\tLog:          utils.Log,\n\t\tDB:           db,\n\t\tValidate:     validate,\n\t\tUserService:  userService,\n\t\tTokenService: tokenService,\n\t}\n}\n\nfunc (s *authService) Register(c *fiber.Ctx, req *validation.Register) (*model.User, error) {\n\tif err := s.Validate.Struct(req); err != nil {\n\t\treturn nil, err\n\t}\n\n\thashedPassword, err := utils.HashPassword(req.Password)\n\tif err != nil {\n\t\ts.Log.Errorf(\"Failed hash password: %+v\", err)\n\t\treturn nil, err\n\t}\n\n\tuser := &model.User{\n\t\tName:     req.Name,\n\t\tEmail:    req.Email,\n\t\tPassword: hashedPassword,\n\t}\n\n\tresult := s.DB.WithContext(c.Context()).Create(user)\n\tif errors.Is(result.Error, gorm.ErrDuplicatedKey) {\n\t\treturn nil, fiber.NewError(fiber.StatusConflict, \"Email already taken\")\n\t}\n\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed create user: %+v\", result.Error)\n\t}\n\n\treturn user, result.Error\n}\n\nfunc (s *authService) Login(c *fiber.Ctx, req *validation.Login) (*model.User, error) {\n\tif err := s.Validate.Struct(req); err != nil {\n\t\treturn nil, err\n\t}\n\n\tuser, err := s.UserService.GetUserByEmail(c, req.Email)\n\tif err != nil {\n\t\treturn nil, fiber.NewError(fiber.StatusUnauthorized, \"Invalid email or password\")\n\t}\n\n\tif !utils.CheckPasswordHash(req.Password, user.Password) {\n\t\treturn nil, fiber.NewError(fiber.StatusUnauthorized, \"Invalid email or password\")\n\t}\n\n\treturn user, nil\n}\n\nfunc (s *authService) Logout(c *fiber.Ctx, req *validation.Logout) error {\n\tif err := s.Validate.Struct(req); err != nil {\n\t\treturn err\n\t}\n\n\ttoken, err := s.TokenService.GetTokenByUserID(c, req.RefreshToken)\n\tif err != nil {\n\t\treturn fiber.NewError(fiber.StatusNotFound, \"Token not found\")\n\t}\n\n\terr = s.TokenService.DeleteToken(c, config.TokenTypeRefresh, token.UserID.String())\n\n\treturn err\n}\n\nfunc (s *authService) RefreshAuth(c *fiber.Ctx, req *validation.RefreshToken) (*response.Tokens, error) {\n\tif err := s.Validate.Struct(req); err != nil {\n\t\treturn nil, err\n\t}\n\n\ttoken, err := s.TokenService.GetTokenByUserID(c, req.RefreshToken)\n\tif err != nil {\n\t\treturn nil, fiber.NewError(fiber.StatusUnauthorized, \"Please authenticate\")\n\t}\n\n\tuser, err := s.UserService.GetUserByID(c, token.UserID.String())\n\tif err != nil {\n\t\treturn nil, fiber.NewError(fiber.StatusUnauthorized, \"Please authenticate\")\n\t}\n\n\tnewTokens, err := s.TokenService.GenerateAuthTokens(c, user)\n\tif err != nil {\n\t\treturn nil, fiber.ErrInternalServerError\n\t}\n\n\treturn newTokens, err\n}\n\nfunc (s *authService) ResetPassword(c *fiber.Ctx, query *validation.Token, req *validation.UpdatePassOrVerify) error {\n\tif err := s.Validate.Struct(query); err != nil {\n\t\treturn err\n\t}\n\n\tuserID, err := utils.VerifyToken(query.Token, config.JWTSecret, config.TokenTypeResetPassword)\n\tif err != nil {\n\t\treturn fiber.NewError(fiber.StatusUnauthorized, \"Invalid Token\")\n\t}\n\n\tuser, err := s.UserService.GetUserByID(c, userID)\n\tif err != nil {\n\t\treturn fiber.NewError(fiber.StatusUnauthorized, \"Password reset failed\")\n\t}\n\n\tif errUpdate := s.UserService.UpdatePassOrVerify(c, req, user.ID.String()); errUpdate != nil {\n\t\treturn errUpdate\n\t}\n\n\tif errToken := s.TokenService.DeleteToken(c, config.TokenTypeResetPassword, user.ID.String()); errToken != nil {\n\t\treturn errToken\n\t}\n\n\treturn nil\n}\n\nfunc (s *authService) VerifyEmail(c *fiber.Ctx, query *validation.Token) error {\n\tif err := s.Validate.Struct(query); err != nil {\n\t\treturn err\n\t}\n\n\tuserID, err := utils.VerifyToken(query.Token, config.JWTSecret, config.TokenTypeVerifyEmail)\n\tif err != nil {\n\t\treturn fiber.NewError(fiber.StatusUnauthorized, \"Invalid Token\")\n\t}\n\n\tuser, err := s.UserService.GetUserByID(c, userID)\n\tif err != nil {\n\t\treturn fiber.NewError(fiber.StatusUnauthorized, \"Verify email failed\")\n\t}\n\n\tif errToken := s.TokenService.DeleteToken(c, config.TokenTypeVerifyEmail, user.ID.String()); errToken != nil {\n\t\treturn errToken\n\t}\n\n\tupdateBody := &validation.UpdatePassOrVerify{\n\t\tVerifiedEmail: true,\n\t}\n\n\tif errUpdate := s.UserService.UpdatePassOrVerify(c, updateBody, user.ID.String()); errUpdate != nil {\n\t\treturn errUpdate\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/service/email_service.go",
    "content": "package service\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/utils\"\n\t\"fmt\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"gopkg.in/gomail.v2\"\n)\n\ntype EmailService interface {\n\tSendEmail(to, subject, body string) error\n\tSendResetPasswordEmail(to, token string) error\n\tSendVerificationEmail(to, token string) error\n}\n\ntype emailService struct {\n\tLog    *logrus.Logger\n\tDialer *gomail.Dialer\n}\n\nfunc NewEmailService() EmailService {\n\treturn &emailService{\n\t\tLog: utils.Log,\n\t\tDialer: gomail.NewDialer(\n\t\t\tconfig.SMTPHost,\n\t\t\tconfig.SMTPPort,\n\t\t\tconfig.SMTPUsername,\n\t\t\tconfig.SMTPPassword,\n\t\t),\n\t}\n}\n\nfunc (s *emailService) SendEmail(to, subject, body string) error {\n\tmailer := gomail.NewMessage()\n\tmailer.SetHeader(\"From\", config.EmailFrom)\n\tmailer.SetHeader(\"To\", to)\n\tmailer.SetHeader(\"Subject\", subject)\n\tmailer.SetBody(\"text/plain\", body)\n\n\tif err := s.Dialer.DialAndSend(mailer); err != nil {\n\t\ts.Log.Errorf(\"Failed to send email: %v\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *emailService) SendResetPasswordEmail(to, token string) error {\n\tsubject := \"Reset password\"\n\n\t// TODO: replace this url with the link to the reset password page of your front-end app\n\tresetPasswordURL := fmt.Sprintf(\"http://link-to-app/reset-password?token=%s\", token)\n\tbody := fmt.Sprintf(`Dear user,\n\nTo reset your password, click on this link: %s\n\nIf you did not request any password resets, then ignore this email.`, resetPasswordURL)\n\treturn s.SendEmail(to, subject, body)\n}\n\nfunc (s *emailService) SendVerificationEmail(to, token string) error {\n\tsubject := \"Email Verification\"\n\n\t// TODO: replace this url with the link to the email verification page of your front-end app\n\tverificationEmailURL := fmt.Sprintf(\"http://link-to-app/verify-email?token=%s\", token)\n\tbody := fmt.Sprintf(`Dear user,\n\nTo verify your email, click on this link: %s\n\nIf you did not create an account, then ignore this email.`, verificationEmailURL)\n\treturn s.SendEmail(to, subject, body)\n}\n"
  },
  {
    "path": "src/service/health_check_service.go",
    "content": "package service\n\nimport (\n\t\"app/src/utils\"\n\t\"errors\"\n\t\"runtime\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"gorm.io/gorm\"\n)\n\ntype HealthCheckService interface {\n\tGormCheck() error\n\tMemoryHeapCheck() error\n}\n\ntype healthCheckService struct {\n\tLog *logrus.Logger\n\tDB  *gorm.DB\n}\n\nfunc NewHealthCheckService(db *gorm.DB) HealthCheckService {\n\treturn &healthCheckService{\n\t\tLog: utils.Log,\n\t\tDB:  db,\n\t}\n}\n\nfunc (s *healthCheckService) GormCheck() error {\n\tsqlDB, errDB := s.DB.DB()\n\tif errDB != nil {\n\t\ts.Log.Errorf(\"failed to access the database connection pool: %v\", errDB)\n\t\treturn errDB\n\t}\n\n\tif err := sqlDB.Ping(); err != nil {\n\t\ts.Log.Errorf(\"failed to ping the database: %v\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// MemoryHeapCheck checks if heap memory usage exceeds a threshold\nfunc (s *healthCheckService) MemoryHeapCheck() error {\n\tvar memStats runtime.MemStats\n\truntime.ReadMemStats(&memStats) // Collect memory statistics\n\n\theapAlloc := memStats.HeapAlloc            // Heap memory currently allocated\n\theapThreshold := uint64(300 * 1024 * 1024) // Example threshold: 300 MB\n\n\ts.Log.Infof(\"Heap Memory Allocation: %v bytes\", heapAlloc)\n\n\t// If the heap allocation exceeds the threshold, return an error\n\tif heapAlloc > heapThreshold {\n\t\ts.Log.Errorf(\"Heap memory usage exceeds threshold: %v bytes\", heapAlloc)\n\t\treturn errors.New(\"heap memory usage too high\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/service/token_service.go",
    "content": "package service\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/model\"\n\tres \"app/src/response\"\n\t\"app/src/utils\"\n\t\"app/src/validation\"\n\t\"time\"\n\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/golang-jwt/jwt/v5\"\n\t\"github.com/google/uuid\"\n\t\"github.com/sirupsen/logrus\"\n\t\"gorm.io/gorm\"\n)\n\ntype TokenService interface {\n\tGenerateToken(userID string, expires time.Time, tokenType string) (string, error)\n\tSaveToken(c *fiber.Ctx, token, userID, tokenType string, expires time.Time) error\n\tDeleteToken(c *fiber.Ctx, tokenType string, userID string) error\n\tDeleteAllToken(c *fiber.Ctx, userID string) error\n\tGetTokenByUserID(c *fiber.Ctx, tokenStr string) (*model.Token, error)\n\tGenerateAuthTokens(c *fiber.Ctx, user *model.User) (*res.Tokens, error)\n\tGenerateResetPasswordToken(c *fiber.Ctx, req *validation.ForgotPassword) (string, error)\n\tGenerateVerifyEmailToken(c *fiber.Ctx, user *model.User) (*string, error)\n}\n\ntype tokenService struct {\n\tLog         *logrus.Logger\n\tDB          *gorm.DB\n\tValidate    *validator.Validate\n\tUserService UserService\n}\n\nfunc NewTokenService(db *gorm.DB, validate *validator.Validate, userService UserService) TokenService {\n\treturn &tokenService{\n\t\tLog:         utils.Log,\n\t\tDB:          db,\n\t\tValidate:    validate,\n\t\tUserService: userService,\n\t}\n}\n\nfunc (s *tokenService) GenerateToken(userID string, expires time.Time, tokenType string) (string, error) {\n\tclaims := jwt.MapClaims{\n\t\t\"sub\":  userID,\n\t\t\"iat\":  time.Now().Unix(),\n\t\t\"exp\":  expires.Unix(),\n\t\t\"type\": tokenType,\n\t}\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n\n\treturn token.SignedString([]byte(config.JWTSecret))\n}\n\nfunc (s *tokenService) SaveToken(c *fiber.Ctx, token, userID, tokenType string, expires time.Time) error {\n\tif err := s.DeleteToken(c, tokenType, userID); err != nil {\n\t\treturn err\n\t}\n\n\ttokenDoc := &model.Token{\n\t\tToken:   token,\n\t\tUserID:  uuid.MustParse(userID),\n\t\tType:    tokenType,\n\t\tExpires: expires,\n\t}\n\n\tresult := s.DB.WithContext(c.Context()).Create(tokenDoc)\n\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed save token: %+v\", result.Error)\n\t}\n\n\treturn result.Error\n}\n\nfunc (s *tokenService) DeleteToken(c *fiber.Ctx, tokenType string, userID string) error {\n\ttokenDoc := new(model.Token)\n\n\tresult := s.DB.WithContext(c.Context()).\n\t\tWhere(\"type = ? AND user_id = ?\", tokenType, userID).\n\t\tDelete(tokenDoc)\n\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed to delete token: %+v\", result.Error)\n\t}\n\n\treturn result.Error\n}\n\nfunc (s *tokenService) DeleteAllToken(c *fiber.Ctx, userID string) error {\n\ttokenDoc := new(model.Token)\n\n\tresult := s.DB.WithContext(c.Context()).Where(\"user_id = ?\", userID).Delete(tokenDoc)\n\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed to delete all token: %+v\", result.Error)\n\t}\n\n\treturn result.Error\n}\n\nfunc (s *tokenService) GetTokenByUserID(c *fiber.Ctx, tokenStr string) (*model.Token, error) {\n\tuserID, err := utils.VerifyToken(tokenStr, config.JWTSecret, config.TokenTypeRefresh)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttokenDoc := new(model.Token)\n\n\tresult := s.DB.WithContext(c.Context()).\n\t\tWhere(\"token = ? AND user_id = ?\", tokenStr, userID).\n\t\tFirst(tokenDoc)\n\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed get token by user id: %+v\", err)\n\t\treturn nil, result.Error\n\t}\n\n\treturn tokenDoc, nil\n}\n\nfunc (s *tokenService) GenerateAuthTokens(c *fiber.Ctx, user *model.User) (*res.Tokens, error) {\n\taccessTokenExpires := time.Now().UTC().Add(time.Minute * time.Duration(config.JWTAccessExp))\n\taccessToken, err := s.GenerateToken(user.ID.String(), accessTokenExpires, config.TokenTypeAccess)\n\tif err != nil {\n\t\ts.Log.Errorf(\"Failed generate token: %+v\", err)\n\t\treturn nil, err\n\t}\n\n\trefreshTokenExpires := time.Now().UTC().Add(time.Hour * 24 * time.Duration(config.JWTRefreshExp))\n\trefreshToken, err := s.GenerateToken(user.ID.String(), refreshTokenExpires, config.TokenTypeRefresh)\n\tif err != nil {\n\t\ts.Log.Errorf(\"Failed generate token: %+v\", err)\n\t\treturn nil, err\n\t}\n\n\tif err = s.SaveToken(c, refreshToken, user.ID.String(), config.TokenTypeRefresh, refreshTokenExpires); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &res.Tokens{\n\t\tAccess: res.TokenExpires{\n\t\t\tToken:   accessToken,\n\t\t\tExpires: accessTokenExpires,\n\t\t},\n\t\tRefresh: res.TokenExpires{\n\t\t\tToken:   refreshToken,\n\t\t\tExpires: refreshTokenExpires,\n\t\t},\n\t}, nil\n}\n\nfunc (s *tokenService) GenerateResetPasswordToken(c *fiber.Ctx, req *validation.ForgotPassword) (string, error) {\n\tif err := s.Validate.Struct(req); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tuser, err := s.UserService.GetUserByEmail(c, req.Email)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\texpires := time.Now().UTC().Add(time.Minute * time.Duration(config.JWTResetPasswordExp))\n\tresetPasswordToken, err := s.GenerateToken(user.ID.String(), expires, config.TokenTypeResetPassword)\n\tif err != nil {\n\t\ts.Log.Errorf(\"Failed generate token: %+v\", err)\n\t\treturn \"\", err\n\t}\n\n\tif err = s.SaveToken(c, resetPasswordToken, user.ID.String(), config.TokenTypeResetPassword, expires); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn resetPasswordToken, nil\n}\n\nfunc (s *tokenService) GenerateVerifyEmailToken(c *fiber.Ctx, user *model.User) (*string, error) {\n\texpires := time.Now().UTC().Add(time.Minute * time.Duration(config.JWTVerifyEmailExp))\n\tverifyEmailToken, err := s.GenerateToken(user.ID.String(), expires, config.TokenTypeVerifyEmail)\n\tif err != nil {\n\t\ts.Log.Errorf(\"Failed generate token: %+v\", err)\n\t\treturn nil, err\n\t}\n\n\tif err = s.SaveToken(c, verifyEmailToken, user.ID.String(), config.TokenTypeVerifyEmail, expires); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &verifyEmailToken, nil\n}\n"
  },
  {
    "path": "src/service/user_service.go",
    "content": "package service\n\nimport (\n\t\"app/src/model\"\n\t\"app/src/utils\"\n\t\"app/src/validation\"\n\t\"errors\"\n\n\t\"github.com/go-playground/validator/v10\"\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/sirupsen/logrus\"\n\t\"gorm.io/gorm\"\n)\n\ntype UserService interface {\n\tGetUsers(c *fiber.Ctx, params *validation.QueryUser) ([]model.User, int64, error)\n\tGetUserByID(c *fiber.Ctx, id string) (*model.User, error)\n\tGetUserByEmail(c *fiber.Ctx, email string) (*model.User, error)\n\tCreateUser(c *fiber.Ctx, req *validation.CreateUser) (*model.User, error)\n\tUpdatePassOrVerify(c *fiber.Ctx, req *validation.UpdatePassOrVerify, id string) error\n\tUpdateUser(c *fiber.Ctx, req *validation.UpdateUser, id string) (*model.User, error)\n\tDeleteUser(c *fiber.Ctx, id string) error\n\tCreateGoogleUser(c *fiber.Ctx, req *validation.GoogleLogin) (*model.User, error)\n}\n\ntype userService struct {\n\tLog      *logrus.Logger\n\tDB       *gorm.DB\n\tValidate *validator.Validate\n}\n\nfunc NewUserService(db *gorm.DB, validate *validator.Validate) UserService {\n\treturn &userService{\n\t\tLog:      utils.Log,\n\t\tDB:       db,\n\t\tValidate: validate,\n\t}\n}\n\nfunc (s *userService) GetUsers(c *fiber.Ctx, params *validation.QueryUser) ([]model.User, int64, error) {\n\tvar users []model.User\n\tvar totalResults int64\n\n\tif err := s.Validate.Struct(params); err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\toffset := (params.Page - 1) * params.Limit\n\tquery := s.DB.WithContext(c.Context()).Order(\"created_at asc\")\n\n\tif search := params.Search; search != \"\" {\n\t\tquery = query.Where(\"name LIKE ? OR email LIKE ? OR role LIKE ?\",\n\t\t\t\"%\"+search+\"%\", \"%\"+search+\"%\", \"%\"+search+\"%\")\n\t}\n\n\tresult := query.Find(&users).Count(&totalResults)\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed to search users: %+v\", result.Error)\n\t\treturn nil, 0, result.Error\n\t}\n\n\tresult = query.Limit(params.Limit).Offset(offset).Find(&users)\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed to get all users: %+v\", result.Error)\n\t\treturn nil, 0, result.Error\n\t}\n\n\treturn users, totalResults, result.Error\n}\n\nfunc (s *userService) GetUserByID(c *fiber.Ctx, id string) (*model.User, error) {\n\tuser := new(model.User)\n\n\tresult := s.DB.WithContext(c.Context()).First(user, \"id = ?\", id)\n\n\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\treturn nil, fiber.NewError(fiber.StatusNotFound, \"User not found\")\n\t}\n\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed get user by id: %+v\", result.Error)\n\t}\n\n\treturn user, result.Error\n}\n\nfunc (s *userService) GetUserByEmail(c *fiber.Ctx, email string) (*model.User, error) {\n\tuser := new(model.User)\n\n\tresult := s.DB.WithContext(c.Context()).Where(\"email = ?\", email).First(user)\n\n\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\treturn nil, fiber.NewError(fiber.StatusNotFound, \"User not found\")\n\t}\n\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed get user by email: %+v\", result.Error)\n\t}\n\n\treturn user, result.Error\n}\n\nfunc (s *userService) CreateUser(c *fiber.Ctx, req *validation.CreateUser) (*model.User, error) {\n\tif err := s.Validate.Struct(req); err != nil {\n\t\treturn nil, err\n\t}\n\n\thashedPassword, err := utils.HashPassword(req.Password)\n\tif err != nil {\n\t\ts.Log.Errorf(\"Failed hash password: %+v\", err)\n\t\treturn nil, err\n\t}\n\n\tuser := &model.User{\n\t\tName:     req.Name,\n\t\tEmail:    req.Email,\n\t\tPassword: hashedPassword,\n\t\tRole:     req.Role,\n\t}\n\n\tresult := s.DB.WithContext(c.Context()).Create(user)\n\n\tif errors.Is(result.Error, gorm.ErrDuplicatedKey) {\n\t\treturn nil, fiber.NewError(fiber.StatusConflict, \"Email is already in use\")\n\t}\n\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed to create user: %+v\", result.Error)\n\t}\n\n\treturn user, result.Error\n}\n\nfunc (s *userService) UpdateUser(c *fiber.Ctx, req *validation.UpdateUser, id string) (*model.User, error) {\n\tif err := s.Validate.Struct(req); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif req.Email == \"\" && req.Name == \"\" && req.Password == \"\" {\n\t\treturn nil, fiber.NewError(fiber.StatusBadRequest, \"Invalid Request\")\n\t}\n\n\tif req.Password != \"\" {\n\t\thashedPassword, err := utils.HashPassword(req.Password)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treq.Password = hashedPassword\n\t}\n\n\tupdateBody := &model.User{\n\t\tName:     req.Name,\n\t\tPassword: req.Password,\n\t\tEmail:    req.Email,\n\t}\n\n\tresult := s.DB.WithContext(c.Context()).Where(\"id = ?\", id).Updates(updateBody)\n\n\tif errors.Is(result.Error, gorm.ErrDuplicatedKey) {\n\t\treturn nil, fiber.NewError(fiber.StatusConflict, \"Email is already in use\")\n\t}\n\n\tif result.RowsAffected == 0 {\n\t\treturn nil, fiber.NewError(fiber.StatusNotFound, \"User not found\")\n\t}\n\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed to update user: %+v\", result.Error)\n\t}\n\n\tuser, err := s.GetUserByID(c, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn user, result.Error\n}\n\nfunc (s *userService) UpdatePassOrVerify(c *fiber.Ctx, req *validation.UpdatePassOrVerify, id string) error {\n\tif err := s.Validate.Struct(req); err != nil {\n\t\treturn err\n\t}\n\n\tif req.Password == \"\" && !req.VerifiedEmail {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"Invalid Request\")\n\t}\n\n\tif req.Password != \"\" {\n\t\thashedPassword, err := utils.HashPassword(req.Password)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq.Password = hashedPassword\n\t}\n\n\tupdateBody := &model.User{\n\t\tPassword:      req.Password,\n\t\tVerifiedEmail: req.VerifiedEmail,\n\t}\n\n\tresult := s.DB.WithContext(c.Context()).Where(\"id = ?\", id).Updates(updateBody)\n\n\tif result.RowsAffected == 0 {\n\t\treturn fiber.NewError(fiber.StatusNotFound, \"User not found\")\n\t}\n\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed to update user password or verifiedEmail: %+v\", result.Error)\n\t}\n\n\treturn result.Error\n}\n\nfunc (s *userService) DeleteUser(c *fiber.Ctx, id string) error {\n\tuser := new(model.User)\n\n\tresult := s.DB.WithContext(c.Context()).Delete(user, \"id = ?\", id)\n\n\tif result.RowsAffected == 0 {\n\t\treturn fiber.NewError(fiber.StatusNotFound, \"User not found\")\n\t}\n\n\tif result.Error != nil {\n\t\ts.Log.Errorf(\"Failed to delete user: %+v\", result.Error)\n\t}\n\n\treturn result.Error\n}\n\nfunc (s *userService) CreateGoogleUser(c *fiber.Ctx, req *validation.GoogleLogin) (*model.User, error) {\n\tif err := s.Validate.Struct(req); err != nil {\n\t\treturn nil, err\n\t}\n\n\tuserFromDB, err := s.GetUserByEmail(c, req.Email)\n\tif err != nil {\n\t\tif err.Error() == \"User not found\" {\n\t\t\tuser := &model.User{\n\t\t\t\tName:          req.Name,\n\t\t\t\tEmail:         req.Email,\n\t\t\t\tVerifiedEmail: req.VerifiedEmail,\n\t\t\t}\n\n\t\t\tif createErr := s.DB.WithContext(c.Context()).Create(user).Error; createErr != nil {\n\t\t\t\ts.Log.Errorf(\"Failed to create user: %+v\", createErr)\n\t\t\t\treturn nil, createErr\n\t\t\t}\n\n\t\t\treturn user, nil\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\tuserFromDB.VerifiedEmail = req.VerifiedEmail\n\tif updateErr := s.DB.WithContext(c.Context()).Save(userFromDB).Error; updateErr != nil {\n\t\ts.Log.Errorf(\"Failed to update user: %+v\", updateErr)\n\t\treturn nil, updateErr\n\t}\n\n\treturn userFromDB, nil\n}\n"
  },
  {
    "path": "src/utils/bcrypt.go",
    "content": "package utils\n\nimport \"golang.org/x/crypto/bcrypt\"\n\nfunc HashPassword(password string) (string, error) {\n\tbytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)\n\treturn string(bytes), err\n}\n\nfunc CheckPasswordHash(password, hash string) bool {\n\terr := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))\n\treturn err == nil\n}\n"
  },
  {
    "path": "src/utils/error.go",
    "content": "package utils\n\nimport (\n\t\"app/src/response\"\n\t\"app/src/validation\"\n\t\"errors\"\n\n\t\"github.com/gofiber/fiber/v2\"\n)\n\nfunc ErrorHandler(c *fiber.Ctx, err error) error {\n\tif errorsMap := validation.CustomErrorMessages(err); len(errorsMap) > 0 {\n\t\treturn response.Error(c, fiber.StatusBadRequest, \"Bad Request\", errorsMap)\n\t}\n\n\tvar fiberErr *fiber.Error\n\tif errors.As(err, &fiberErr) {\n\t\treturn response.Error(c, fiberErr.Code, fiberErr.Message, nil)\n\t}\n\n\treturn response.Error(c, fiber.StatusInternalServerError, \"Internal Server Error\", nil)\n}\n\nfunc NotFoundHandler(c *fiber.Ctx) error {\n\treturn response.Error(c, fiber.StatusNotFound, \"Endpoint Not Found\", nil)\n}\n"
  },
  {
    "path": "src/utils/logrus.go",
    "content": "package utils\n\nimport (\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\ntype CustomFormatter struct {\n\tlogrus.TextFormatter\n}\n\nvar Log *logrus.Logger\n\nfunc init() {\n\tLog = logrus.New()\n\n\t// Set logger to use the custom text formatter\n\tLog.SetFormatter(&CustomFormatter{\n\t\tTextFormatter: logrus.TextFormatter{\n\t\t\tTimestampFormat: \"15:04:05.000\",\n\t\t\tFullTimestamp:   true,\n\t\t\tForceColors:     true,\n\t\t},\n\t})\n\n\tLog.SetOutput(os.Stdout)\n}\n"
  },
  {
    "path": "src/utils/verify.go",
    "content": "//revive:disable:var-naming\npackage utils\n\nimport (\n\t\"errors\"\n\n\t\"github.com/golang-jwt/jwt/v5\"\n)\n\nfunc VerifyToken(tokenStr, secret, tokenType string) (string, error) {\n\ttoken, err := jwt.Parse(tokenStr, func(_ *jwt.Token) (interface{}, error) {\n\t\treturn []byte(secret), nil\n\t})\n\n\tif err != nil || !token.Valid {\n\t\treturn \"\", err\n\t}\n\n\tclaims, ok := token.Claims.(jwt.MapClaims)\n\tif !ok {\n\t\treturn \"\", errors.New(\"invalid token claims\")\n\t}\n\n\tjwtType, ok := claims[\"type\"].(string)\n\tif !ok || jwtType != tokenType {\n\t\treturn \"\", errors.New(\"invalid token type\")\n\t}\n\n\tuserID, ok := claims[\"sub\"].(string)\n\tif !ok {\n\t\treturn \"\", errors.New(\"invalid token sub\")\n\t}\n\n\treturn userID, nil\n}\n"
  },
  {
    "path": "src/validation/auth_validation.go",
    "content": "package validation\n\ntype Register struct {\n\tName     string `json:\"name\" validate:\"required,max=50\" example:\"fake name\"`\n\tEmail    string `json:\"email\" validate:\"required,email,max=50\" example:\"fake@example.com\"`\n\tPassword string `json:\"password\" validate:\"required,min=8,max=20,password\" example:\"password1\"`\n}\n\ntype Login struct {\n\tEmail    string `json:\"email\" validate:\"required,email,max=50\" example:\"fake@example.com\"`\n\tPassword string `json:\"password\" validate:\"required,min=8,max=20,password\" example:\"password1\"`\n}\n\ntype GoogleLogin struct {\n\tName          string `json:\"name\" validate:\"required,max=50\"`\n\tEmail         string `json:\"email\" validate:\"required,email,max=50\"`\n\tVerifiedEmail bool   `json:\"verified_email\" validate:\"required\"`\n}\n\ntype Logout struct {\n\tRefreshToken string `json:\"refresh_token\" validate:\"required,max=255\"`\n}\n\ntype RefreshToken struct {\n\tRefreshToken string `json:\"refresh_token\" validate:\"required,max=255\"`\n}\n\ntype ForgotPassword struct {\n\tEmail string `json:\"email\" validate:\"required,email,max=50\" example:\"fake@example.com\"`\n}\n\ntype Token struct {\n\tToken string `json:\"token\" validate:\"required,max=255\"`\n}\n"
  },
  {
    "path": "src/validation/custom_validation.go",
    "content": "package validation\n\nimport (\n\t\"regexp\"\n\n\t\"github.com/go-playground/validator/v10\"\n)\n\nfunc Password(field validator.FieldLevel) bool {\n\tvalue, ok := field.Field().Interface().(string)\n\tif ok {\n\t\thasDigit := regexp.MustCompile(`[0-9]`).MatchString(value)\n\t\thasLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(value)\n\n\t\tif !hasDigit || !hasLetter {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "src/validation/user_validation.go",
    "content": "package validation\n\ntype CreateUser struct {\n\tName     string `json:\"name\" validate:\"required,max=50\" example:\"fake name\"`\n\tEmail    string `json:\"email\" validate:\"required,email,max=50\" example:\"fake@example.com\"`\n\tPassword string `json:\"password\" validate:\"required,min=8,max=20,password\" example:\"password1\"`\n\tRole     string `json:\"role\" validate:\"required,oneof=user admin,max=50\" example:\"user\"`\n}\n\ntype UpdateUser struct {\n\tName     string `json:\"name,omitempty\" validate:\"omitempty,max=50\" example:\"fake name\"`\n\tEmail    string `json:\"email\" validate:\"omitempty,email,max=50\" example:\"fake@example.com\"`\n\tPassword string `json:\"password,omitempty\" validate:\"omitempty,min=8,max=20,password\" example:\"password1\"`\n}\n\ntype UpdatePassOrVerify struct {\n\tPassword      string `json:\"password,omitempty\" validate:\"omitempty,min=8,max=20,password\" example:\"password1\"`\n\tVerifiedEmail bool   `json:\"verified_email\" swaggerignore:\"true\" validate:\"omitempty,boolean\"`\n}\n\ntype QueryUser struct {\n\tPage   int    `validate:\"omitempty,number,max=50\"`\n\tLimit  int    `validate:\"omitempty,number,max=50\"`\n\tSearch string `validate:\"omitempty,max=50\"`\n}\n"
  },
  {
    "path": "src/validation/validation.go",
    "content": "package validation\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/go-playground/validator/v10\"\n)\n\nvar customMessages = map[string]string{\n\t\"required\": \"Field %s must be filled\",\n\t\"email\":    \"Invalid email address for field %s\",\n\t\"min\":      \"Field %s must have a minimum length of %s characters\",\n\t\"max\":      \"Field %s must have a maximum length of %s characters\",\n\t\"len\":      \"Field %s must be exactly %s characters long\",\n\t\"number\":   \"Field %s must be a number\",\n\t\"positive\": \"Field %s must be a positive number\",\n\t\"alphanum\": \"Field %s must contain only alphanumeric characters\",\n\t\"oneof\":    \"Invalid value for field %s\",\n\t\"password\": \"Field %s must contain at least 1 letter and 1 number\",\n}\n\nfunc CustomErrorMessages(err error) map[string]string {\n\tvar validationErrors validator.ValidationErrors\n\tif errors.As(err, &validationErrors) {\n\t\treturn generateErrorMessages(validationErrors)\n\t}\n\treturn nil\n}\n\nfunc generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string {\n\terrorsMap := make(map[string]string)\n\tfor _, err := range validationErrors {\n\t\tfieldName := err.StructNamespace()\n\t\ttag := err.Tag()\n\n\t\tcustomMessage := customMessages[tag]\n\t\tif customMessage != \"\" {\n\t\t\terrorsMap[fieldName] = formatErrorMessage(customMessage, err, tag)\n\t\t} else {\n\t\t\terrorsMap[fieldName] = defaultErrorMessage(err)\n\t\t}\n\t}\n\treturn errorsMap\n}\n\nfunc formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {\n\tif tag == \"min\" || tag == \"max\" || tag == \"len\" {\n\t\treturn fmt.Sprintf(customMessage, err.Field(), err.Param())\n\t}\n\treturn fmt.Sprintf(customMessage, err.Field())\n}\n\nfunc defaultErrorMessage(err validator.FieldError) string {\n\treturn fmt.Sprintf(\"Field validation for '%s' failed on the '%s' tag\", err.Field(), err.Tag())\n}\n\nfunc Validator() *validator.Validate {\n\tvalidate := validator.New()\n\n\tif err := validate.RegisterValidation(\"password\", Password); err != nil {\n\t\treturn nil\n\t}\n\n\treturn validate\n}\n"
  },
  {
    "path": "test/fixture/token_fixture.go",
    "content": "package fixture\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/model\"\n\t\"app/test/helper\"\n\t\"time\"\n)\n\nvar ExpiresAccessToken = time.Now().UTC().Add(time.Minute * time.Duration(config.JWTAccessExp))\nvar ExpiresRefreshToken = time.Now().UTC().Add(time.Hour * 24 * time.Duration(config.JWTRefreshExp))\nvar ExpiresResetPasswordToken = time.Now().UTC().Add(time.Minute * time.Duration(config.JWTResetPasswordExp))\nvar ExpiresVerifyEmailToken = time.Now().UTC().Add(time.Minute * time.Duration(config.JWTVerifyEmailExp))\n\nfunc AccessToken(user *model.User) (string, error) {\n\taccessToken, err := helper.GenerateToken(user.ID.String(), ExpiresAccessToken, config.TokenTypeAccess)\n\tif err != nil {\n\t\treturn accessToken, err\n\t}\n\treturn accessToken, nil\n}\n\nfunc RefreshToken(user *model.User) (string, error) {\n\trefreshToken, err := helper.GenerateToken(user.ID.String(), ExpiresRefreshToken, config.TokenTypeRefresh)\n\tif err != nil {\n\t\treturn refreshToken, err\n\t}\n\treturn refreshToken, nil\n}\n\nfunc ResetPasswordToken(user *model.User) (string, error) {\n\tresetPasswordToken, err := helper.GenerateToken(\n\t\tuser.ID.String(), ExpiresResetPasswordToken, config.TokenTypeResetPassword,\n\t)\n\tif err != nil {\n\t\treturn resetPasswordToken, err\n\t}\n\treturn resetPasswordToken, nil\n}\n\nfunc VerifyEmailToken(user *model.User) (string, error) {\n\tverifyEmailToken, err := helper.GenerateToken(user.ID.String(), ExpiresVerifyEmailToken, config.TokenTypeVerifyEmail)\n\tif err != nil {\n\t\treturn verifyEmailToken, err\n\t}\n\treturn verifyEmailToken, nil\n}\n"
  },
  {
    "path": "test/fixture/user_fixture.go",
    "content": "package fixture\n\nimport (\n\t\"app/src/model\"\n\n\t\"github.com/google/uuid\"\n)\n\nvar UserOne = &model.User{\n\tID:            uuid.New(),\n\tName:          \"Test1\",\n\tEmail:         \"test1@gmail.com\",\n\tPassword:      \"password1\",\n\tRole:          \"user\",\n\tVerifiedEmail: false,\n}\n\nvar UserTwo = &model.User{\n\tID:            uuid.New(),\n\tName:          \"Test2\",\n\tEmail:         \"test2@gmail.com\",\n\tPassword:      \"password1\",\n\tRole:          \"user\",\n\tVerifiedEmail: false,\n}\n\nvar Admin = &model.User{\n\tID:            uuid.New(),\n\tName:          \"Admin\",\n\tEmail:         \"admin@gmail.com\",\n\tPassword:      \"password1\",\n\tRole:          \"admin\",\n\tVerifiedEmail: false,\n}\n"
  },
  {
    "path": "test/helper/helper.go",
    "content": "package helper\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/model\"\n\t\"app/src/utils\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/golang-jwt/jwt/v5\"\n\t\"github.com/google/uuid\"\n\t\"github.com/sirupsen/logrus\"\n\t\"gorm.io/gorm\"\n)\n\nfunc ClearAll(db *gorm.DB) {\n\tClearToken(db)\n\tClearUsers(db)\n}\n\nfunc ClearUsers(db *gorm.DB) {\n\terr := db.Where(\"id is not null\").Delete(&model.User{}).Error\n\tif err != nil {\n\t\tlogrus.Fatalf(\"Failed clear user data : %+v\", err)\n\t}\n}\n\nfunc ClearToken(db *gorm.DB) {\n\terr := db.Where(\"id is not null\").Delete(&model.Token{}).Error\n\tif err != nil {\n\t\tlogrus.Fatalf(\"Failed clear user token : %+v\", err)\n\t}\n}\n\nfunc CreateUser(db *gorm.DB, email, password, name string) {\n\thashedPassword, err := utils.HashPassword(password)\n\tif err != nil {\n\t\tlogrus.Errorf(\"Failed hashed password : %+v\", err)\n\t}\n\n\tuser := &model.User{\n\t\tEmail:    email,\n\t\tPassword: hashedPassword,\n\t\tName:     name,\n\t}\n\n\terr = db.Create(user).Error\n\tif err != nil {\n\t\tlogrus.Errorf(\"Failed create user : %+v\", err)\n\t}\n}\n\nfunc InsertUser(db *gorm.DB, users ...*model.User) {\n\tnow := time.Now()\n\n\tfor i, user := range users {\n\t\thashedPassword, err := utils.HashPassword(user.Password)\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"Failed to hash password: %+v\", err)\n\t\t\tcontinue\n\t\t}\n\t\tuser.Password = hashedPassword\n\t\tuser.CreatedAt = now.Add(time.Duration(i) * time.Second)\n\n\t\tif errDB := db.Create(user).Error; errDB != nil {\n\t\t\tlogrus.Errorf(\"Failed to create user: %+v\", errDB)\n\t\t}\n\t}\n}\n\nfunc SaveToken(db *gorm.DB, token, userID, tokenType string, expires time.Time) error {\n\tif err := DeleteToken(db, tokenType, userID); err != nil {\n\t\treturn err\n\t}\n\n\ttokenDoc := &model.Token{\n\t\tToken:   token,\n\t\tUserID:  uuid.MustParse(userID),\n\t\tType:    tokenType,\n\t\tExpires: expires,\n\t}\n\n\tresult := db.Create(tokenDoc)\n\n\treturn result.Error\n}\n\nfunc DeleteToken(db *gorm.DB, tokenType, userID string) error {\n\ttokenDoc := new(model.Token)\n\n\tresult := db.Where(\"type = ? AND user_id = ?\", tokenType, userID).Delete(tokenDoc)\n\n\treturn result.Error\n}\n\nfunc GenerateToken(\n\tuserID string, expires time.Time, tokenType string,\n) (string, error) {\n\tclaims := jwt.MapClaims{\n\t\t\"sub\":  userID,\n\t\t\"iat\":  time.Now().Unix(),\n\t\t\"exp\":  expires.Unix(),\n\t\t\"type\": tokenType,\n\t}\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n\n\treturn token.SignedString([]byte(config.JWTSecret))\n}\n\nfunc GenerateInvalidToken(\n\tuserID string, expires time.Time, tokenType string,\n) (string, error) {\n\tclaims := jwt.MapClaims{\n\t\t\"sub\":  userID,\n\t\t\"iat\":  time.Now().Unix(),\n\t\t\"exp\":  expires.Unix(),\n\t\t\"type\": tokenType,\n\t}\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n\n\treturn token.SignedString([]byte(\"invalidSecret\"))\n}\n\nfunc GetTokenByUserID(db *gorm.DB, tokenStr string) (*model.Token, error) {\n\tuserID, err := utils.VerifyToken(tokenStr, config.JWTSecret, config.TokenTypeRefresh)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttokenDoc := new(model.Token)\n\tresult := db.Where(\"token = ? AND user_id = ?\", tokenStr, userID).\n\t\tFirst(tokenDoc)\n\n\tif result.Error != nil {\n\t\treturn nil, result.Error\n\t}\n\n\treturn tokenDoc, nil\n}\n\nfunc GetTokenByType(db *gorm.DB, userID string, tokenType string) (*model.Token, error) {\n\ttokenDoc := new(model.Token)\n\tresult := db.Where(\"type = ? AND user_id = ?\", tokenType, userID).\n\t\tFirst(tokenDoc)\n\n\tif result.Error != nil {\n\t\treturn nil, result.Error\n\t}\n\n\treturn tokenDoc, nil\n}\n\nfunc GetUserByID(db *gorm.DB, id string) (*model.User, error) {\n\tuser := new(model.User)\n\n\tresult := db.First(user, \"id = ?\", id)\n\n\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\treturn nil, result.Error\n\t}\n\n\tif result.Error != nil {\n\t\tlogrus.Errorf(\"Failed get user by id: %+v\", result.Error)\n\t}\n\n\treturn user, result.Error\n}\n"
  },
  {
    "path": "test/init.go",
    "content": "package test\n\nimport (\n\t\"app/src/database\"\n\t\"app/src/router\"\n\t\"app/src/utils\"\n\n\t\"github.com/gofiber/fiber/v2\"\n\t\"gorm.io/gorm\"\n)\n\nvar App = fiber.New(fiber.Config{\n\tCaseSensitive: true,\n\tErrorHandler:  utils.ErrorHandler,\n})\nvar DB *gorm.DB\nvar Log = utils.Log\n\nfunc init() {\n\t// TODO: You can modify host and database configuration for tests\n\tDB = database.Connect(\"localhost\", \"testdb\")\n\trouter.Routes(App, DB)\n\tApp.Use(utils.NotFoundHandler)\n}\n"
  },
  {
    "path": "test/integration/auth_test.go",
    "content": "package integration\n\nimport (\n\t\"app/src/config\"\n\t\"app/src/response\"\n\t\"app/src/utils\"\n\t\"app/src/validation\"\n\t\"app/test\"\n\t\"app/test/fixture\"\n\t\"app/test/helper\"\n\t_ \"embed\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAuthRoutes(t *testing.T) {\n\tt.Run(\"POST /v1/auth/register\", func(t *testing.T) {\n\t\tvar requestBody = validation.Register{\n\t\t\tName:     \"Test\",\n\t\t\tEmail:    \"test@gmail.com\",\n\t\t\tPassword: \"password1\",\n\t\t}\n\n\t\tt.Run(\"should return 201 and successfully register user if request data is ok\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\tbodyJSON, err := json.Marshal(requestBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/register\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tmsTimeout := 2000\n\t\t\tapiResponse, err := test.App.Test(request, msTimeout)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := new(response.SuccessWithTokens)\n\n\t\t\terr = json.Unmarshal(bytes, responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusCreated, apiResponse.StatusCode)\n\t\t\tassert.Equal(t, \"success\", responseBody.Status)\n\t\t\tassert.NotContains(t, string(bytes), \"password\")\n\t\t\tassert.NotNil(t, responseBody.User.ID)\n\t\t\tassert.Equal(t, requestBody.Name, responseBody.User.Name)\n\t\t\tassert.Equal(t, requestBody.Email, responseBody.User.Email)\n\t\t\tassert.Equal(t, \"user\", responseBody.User.Role)\n\t\t\tassert.Equal(t, false, responseBody.User.VerifiedEmail)\n\t\t\tassert.NotNil(t, responseBody.Tokens.Access.Token)\n\t\t\tassert.NotNil(t, responseBody.Tokens.Refresh.Token)\n\n\t\t\tuser, err := helper.GetUserByID(test.DB, responseBody.User.ID.String())\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.NotNil(t, user)\n\t\t\tassert.NotEqual(t, user.Password, requestBody.Password)\n\t\t\tassert.Equal(t, user.Name, requestBody.Name)\n\t\t\tassert.Equal(t, user.Email, requestBody.Email)\n\t\t\tassert.Equal(t, user.Role, \"user\")\n\t\t\tassert.Equal(t, user.VerifiedEmail, false)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if email is invalid\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\trequestBody.Email = \"invalidEmail\"\n\n\t\t\tbodyJSON, err := json.Marshal(requestBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/register\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 409 error if email is already used\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.CreateUser(test.DB, \"test@gmail.com\", \"test1234\", \"Test\")\n\t\t\trequestBody.Email = \"test@gmail.com\"\n\n\t\t\tbodyJSON, err := json.Marshal(requestBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/register\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusConflict, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if password length is less than 8 characters\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\trequestBody.Password = \"passwo1\"\n\n\t\t\tbodyJSON, err := json.Marshal(requestBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/register\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if password does not contain both letters and numbers\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\trequestBody.Password = \"password\"\n\n\t\t\tbodyJSON, err := json.Marshal(requestBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/register\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\n\t\t\trequestBody.Password = \"11111111\"\n\n\t\t\tbodyJSON, err = json.Marshal(requestBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest = httptest.NewRequest(http.MethodPost, \"/v1/auth/register\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err = test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\t})\n\tt.Run(\"POST /v1/auth/login\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 and login user if email and password match\", func(t *testing.T) {\n\t\t\thelper.CreateUser(test.DB, \"test@gmail.com\", \"test1234\", \"Test User\")\n\t\t\tloginCredentials := &validation.Login{\n\t\t\t\tEmail:    \"test@gmail.com\",\n\t\t\t\tPassword: \"test1234\",\n\t\t\t}\n\n\t\t\tbodyJSON, err := json.Marshal(loginCredentials)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/login\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := new(response.SuccessWithTokens)\n\n\t\t\terr = json.Unmarshal(bytes, responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t\tassert.Equal(t, \"success\", responseBody.Status)\n\t\t\tassert.NotNil(t, responseBody.User.ID)\n\t\t\tassert.Equal(t, \"Test User\", responseBody.User.Name)\n\t\t\tassert.Equal(t, \"test@gmail.com\", responseBody.User.Email)\n\t\t\tassert.Equal(t, \"user\", responseBody.User.Role)\n\t\t\tassert.Equal(t, false, responseBody.User.VerifiedEmail)\n\t\t\tassert.NotNil(t, responseBody.Tokens.Access.Token)\n\t\t\tassert.NotNil(t, responseBody.Tokens.Refresh.Token)\n\t\t})\n\n\t\tt.Run(\"should return 401 error if there are no users with that email\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\tloginCredentials := &validation.Login{\n\t\t\t\tEmail:    \"nonexistent@gmail.com\",\n\t\t\t\tPassword: \"test1234\",\n\t\t\t}\n\n\t\t\tbodyJSON, err := json.Marshal(loginCredentials)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/login\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := make(map[string]interface{})\n\n\t\t\terr = json.Unmarshal(bytes, &responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t\tassert.Equal(t, \"error\", responseBody[\"status\"])\n\t\t\tassert.Equal(t, \"Invalid email or password\", responseBody[\"message\"])\n\t\t})\n\n\t\tt.Run(\"should return 401 error if password is wrong\", func(t *testing.T) {\n\t\t\thelper.CreateUser(test.DB, \"test@gmail.com\", \"test1234\", \"Test User\")\n\t\t\tloginCredentials := &validation.Login{\n\t\t\t\tEmail:    \"test@gmail.com\",\n\t\t\t\tPassword: \"wrongPassword1\",\n\t\t\t}\n\n\t\t\tbodyJSON, err := json.Marshal(loginCredentials)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/login\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := make(map[string]interface{})\n\n\t\t\terr = json.Unmarshal(bytes, &responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t\tassert.Equal(t, \"error\", responseBody[\"status\"])\n\t\t\tassert.Equal(t, \"Invalid email or password\", responseBody[\"message\"])\n\t\t})\n\t})\n\tt.Run(\"POST /v1/auth/logout\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 if refresh token is valid\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trefreshToken, err := fixture.RefreshToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\terr = helper.SaveToken(test.DB, refreshToken, fixture.UserOne.ID.String(), config.TokenTypeRefresh, fixture.ExpiresRefreshToken)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken})\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/logout\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\n\t\t\tdbRefreshTokenDoc, _ := helper.GetTokenByUserID(test.DB, refreshToken)\n\t\t\tassert.Nil(t, dbRefreshTokenDoc)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if refresh token is missing from request body\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/logout\", nil)\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 404 error if refresh token is not found in the database\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trefreshToken, err := fixture.RefreshToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken})\n\t\t\tassert.Nil(t, err)\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/logout\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiresponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\t\t\tassert.Equal(t, http.StatusNotFound, apiresponse.StatusCode)\n\t\t})\n\t})\n\tt.Run(\"POST /v1/auth/refresh-tokens\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 and new auth tokens if refresh token is valid\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trefreshToken, err := fixture.RefreshToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\terr = helper.SaveToken(test.DB, refreshToken, fixture.UserOne.ID.String(), config.TokenTypeRefresh, fixture.ExpiresRefreshToken)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken})\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/refresh-tokens\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := new(response.RefreshToken)\n\n\t\t\terr = json.Unmarshal(bytes, responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t\tassert.NotNil(t, responseBody.Tokens.Access.Token)\n\t\t\tassert.NotNil(t, responseBody.Tokens.Refresh.Token)\n\n\t\t\tdbRefreshTokenDoc, err := helper.GetTokenByUserID(test.DB, responseBody.Tokens.Refresh.Token)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, dbRefreshTokenDoc.UserID, fixture.UserOne.ID)\n\t\t\tassert.Equal(t, dbRefreshTokenDoc.Type, config.TokenTypeRefresh)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if refresh token is missing from request body\", func(t *testing.T) {\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/refresh-tokens\", nil)\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 401 error if refresh token is signed using an invalid secret\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trefreshToken, err := helper.GenerateInvalidToken(fixture.UserOne.ID.String(), fixture.ExpiresRefreshToken, config.TokenTypeRefresh)\n\t\t\tassert.Nil(t, err)\n\n\t\t\terr = helper.SaveToken(test.DB, refreshToken, fixture.UserOne.ID.String(), config.TokenTypeRefresh, fixture.ExpiresRefreshToken)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken})\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/refresh-tokens\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 401 error if refresh token is not found in the database\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trefreshToken, err := fixture.RefreshToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken})\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/refresh-tokens\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 401 error if refresh token is expired\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\texpires := time.Now().Add(time.Second * 1)\n\t\t\trefreshToken, err := helper.GenerateToken(fixture.UserOne.ID.String(), expires, config.TokenTypeRefresh)\n\t\t\tassert.Nil(t, err)\n\n\t\t\terr = helper.SaveToken(test.DB, refreshToken, fixture.UserOne.ID.String(), config.TokenTypeRefresh, fixture.ExpiresRefreshToken)\n\t\t\tassert.Nil(t, err)\n\n\t\t\ttime.Sleep(2 * time.Second)\n\n\t\t\tbodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken})\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/refresh-tokens\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t})\n\t})\n\tt.Run(\"POST /v1/auth/forgot-password\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 and send reset password email to the user\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trequestBody := validation.ForgotPassword{\n\t\t\t\tEmail: fixture.UserOne.Email,\n\t\t\t}\n\n\t\t\tbodyJSON, err := json.Marshal(requestBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/forgot-password\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tmsTimeout := 10000\n\t\t\tapiResponse, err := test.App.Test(request, msTimeout)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\n\t\t\tdbVerifyEmailTokenDoc, _ := helper.GetTokenByType(test.DB, fixture.UserOne.ID.String(), config.TokenTypeResetPassword)\n\t\t\tassert.NotNil(t, dbVerifyEmailTokenDoc)\n\t\t})\n\n\t\tt.Run(\"should return 400 if email is missing\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/forgot-password\", nil)\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 404 if email does not belong to any user\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\n\t\t\trequestBody := validation.ForgotPassword{\n\t\t\t\tEmail: fixture.UserOne.Email,\n\t\t\t}\n\n\t\t\tbodyJSON, err := json.Marshal(requestBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/forgot-password\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusNotFound, apiResponse.StatusCode)\n\t\t})\n\t})\n\tt.Run(\"POST /v1/auth/reset-password\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 and reset the password\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\tresetPasswordToken, err := fixture.ResetPasswordToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\terr = helper.SaveToken(test.DB, resetPasswordToken, fixture.UserOne.ID.String(), config.TokenTypeResetPassword, fixture.ExpiresResetPasswordToken)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequestBody := validation.UpdatePassOrVerify{\n\t\t\t\tPassword: \"password2\",\n\t\t\t}\n\n\t\t\tbodyJSON, err := json.Marshal(requestBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/reset-password?token=\"+resetPasswordToken, strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\n\t\t\tuser, err := helper.GetUserByID(test.DB, fixture.UserOne.ID.String())\n\t\t\tassert.Nil(t, err)\n\n\t\t\tisPasswordMatch := utils.CheckPasswordHash(\"password2\", user.Password)\n\t\t\tassert.True(t, isPasswordMatch)\n\n\t\t\tdbResetPasswordTokenDoc, _ := helper.GetTokenByUserID(test.DB, resetPasswordToken)\n\t\t\tassert.Nil(t, dbResetPasswordTokenDoc)\n\t\t})\n\n\t\tt.Run(\"should return 400 if reset password token is missing\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trequestBody := validation.UpdatePassOrVerify{\n\t\t\t\tPassword: \"password2\",\n\t\t\t}\n\n\t\t\tbodyJSON, err := json.Marshal(requestBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/reset-password\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 401 if reset password token is expired\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\texpires := time.Now().Add(time.Second * 1)\n\t\t\tresetPasswordToken, err := helper.GenerateToken(fixture.UserOne.ID.String(), expires, config.TokenTypeResetPassword)\n\t\t\tassert.Nil(t, err)\n\n\t\t\terr = helper.SaveToken(test.DB, resetPasswordToken, fixture.UserOne.ID.String(), config.TokenTypeResetPassword, fixture.ExpiresResetPasswordToken)\n\t\t\tassert.Nil(t, err)\n\n\t\t\ttime.Sleep(2 * time.Second)\n\n\t\t\trequestBody := validation.UpdatePassOrVerify{\n\t\t\t\tPassword: \"password2\",\n\t\t\t}\n\n\t\t\tbodyJSON, err := json.Marshal(requestBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/reset-password?token=\"+resetPasswordToken, strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 if password is missing or invalid\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\tresetPasswordToken, err := fixture.ResetPasswordToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\terr = helper.SaveToken(test.DB, resetPasswordToken, fixture.UserOne.ID.String(), config.TokenTypeResetPassword, fixture.ExpiresResetPasswordToken)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/reset-password?token=\"+resetPasswordToken, nil)\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\n\t\t\tbodyJSON, err := json.Marshal(validation.UpdatePassOrVerify{Password: \"short1\"})\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest = httptest.NewRequest(http.MethodPost, \"/v1/auth/reset-password?token=\"+resetPasswordToken, strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err = test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\n\t\t\tbodyJSON, err = json.Marshal(validation.UpdatePassOrVerify{Password: \"password\"})\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest = httptest.NewRequest(http.MethodPost, \"/v1/auth/reset-password?token=\"+resetPasswordToken, strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err = test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\n\t\t\tbodyJSON, err = json.Marshal(validation.UpdatePassOrVerify{Password: \"11111111\"})\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest = httptest.NewRequest(http.MethodPost, \"/v1/auth/reset-password?token=\"+resetPasswordToken, strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err = test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\t})\n\tt.Run(\"POST /v1/auth/send-verification-email\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 and send verification email to the user\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/send-verification-email\", nil)\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tmsTimeout := 10000\n\t\t\tapiResponse, err := test.App.Test(request, msTimeout)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\n\t\t\tdbVerifyEmailTokenDoc, _ := helper.GetTokenByType(test.DB, fixture.UserOne.ID.String(), config.TokenTypeVerifyEmail)\n\t\t\tassert.NotNil(t, dbVerifyEmailTokenDoc)\n\t\t})\n\n\t\tt.Run(\"should return 401 error if access token is missing\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/send-verification-email\", nil)\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t})\n\t})\n\tt.Run(\"POST /v1/auth/verify-email\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 and verify the email\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\tverifyEmailToken, err := fixture.VerifyEmailToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\terr = helper.SaveToken(test.DB, verifyEmailToken, fixture.UserOne.ID.String(), config.TokenTypeVerifyEmail, fixture.ExpiresVerifyEmailToken)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/verify-email?token=\"+verifyEmailToken, nil)\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\n\t\t\tuser, err := helper.GetUserByID(test.DB, fixture.UserOne.ID.String())\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.True(t, user.VerifiedEmail)\n\n\t\t\tdbVerifyEmailTokenDoc, _ := helper.GetTokenByUserID(test.DB, verifyEmailToken)\n\t\t\tassert.Nil(t, dbVerifyEmailTokenDoc)\n\t\t})\n\n\t\tt.Run(\"should return 400 if verify email token is missing\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/verify-email\", nil)\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 401 if verify email token is expired\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\texpires := time.Now().Add(time.Second * 1)\n\t\t\tverifyEmailToken, err := helper.GenerateToken(fixture.UserOne.ID.String(), expires, config.TokenTypeVerifyEmail)\n\t\t\tassert.Nil(t, err)\n\n\t\t\terr = helper.SaveToken(test.DB, verifyEmailToken, fixture.UserOne.ID.String(), config.TokenTypeVerifyEmail, fixture.ExpiresVerifyEmailToken)\n\t\t\tassert.Nil(t, err)\n\n\t\t\ttime.Sleep(2 * time.Second)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/auth/verify-email?token=\"+verifyEmailToken, nil)\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t})\n\t})\n}\n\nfunc TestAuthMiddleware(t *testing.T) {\n\tt.Run(\"should call next with no errors if access token is valid\", func(t *testing.T) {\n\t\thelper.ClearAll(test.DB)\n\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\ttoken, err := fixture.AccessToken(fixture.UserOne)\n\t\tassert.Nil(t, err)\n\n\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users\", nil)\n\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+token)\n\n\t\tuserID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess)\n\t\tassert.Nil(t, err)\n\n\t\tassert.Equal(t, fixture.UserOne.ID.String(), userID)\n\t})\n\n\tt.Run(\"should call next with unauthorized error if access token is not found in header\", func(t *testing.T) {\n\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users\", nil)\n\t\tapiResponse, err := test.App.Test(request)\n\t\tassert.Nil(t, err)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t})\n\n\tt.Run(\"should call next with unauthorized error if access token is not a valid jwt token\", func(t *testing.T) {\n\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users\", nil)\n\t\trequest.Header.Set(\"Authorization\", \"Bearer randomToken\")\n\n\t\tapiResponse, err := test.App.Test(request)\n\t\tassert.Nil(t, err)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t})\n\n\tt.Run(\"should call next with unauthorized error if the token is not an access token\", func(t *testing.T) {\n\t\thelper.ClearAll(test.DB)\n\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\trefreshToken, err := fixture.RefreshToken(fixture.UserOne)\n\t\tassert.Nil(t, err)\n\n\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users\", nil)\n\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+refreshToken)\n\n\t\tapiResponse, err := test.App.Test(request)\n\t\tassert.Nil(t, err)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t})\n\n\tt.Run(\"should call next with unauthorized error if access token is generated with an invalid secret\", func(t *testing.T) {\n\t\thelper.ClearAll(test.DB)\n\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\taccessToken, err := helper.GenerateInvalidToken(fixture.UserOne.ID.String(), fixture.ExpiresAccessToken, config.TokenTypeAccess)\n\t\tassert.Nil(t, err)\n\n\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users\", nil)\n\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+accessToken)\n\n\t\tapiResponse, err := test.App.Test(request)\n\t\tassert.Nil(t, err)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t})\n\n\tt.Run(\"should call next with unauthorized error if access token is expired\", func(t *testing.T) {\n\t\thelper.ClearAll(test.DB)\n\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\texpires := time.Now().Add(time.Second * 1)\n\t\taccessToken, err := helper.GenerateToken(fixture.UserOne.ID.String(), expires, config.TokenTypeAccess)\n\t\tassert.Nil(t, err)\n\n\t\ttime.Sleep(2 * time.Second)\n\n\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users\", nil)\n\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+accessToken)\n\n\t\tapiResponse, err := test.App.Test(request)\n\t\tassert.Nil(t, err)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t})\n\n\tt.Run(\"should call next with unauthorized error if user is not found\", func(t *testing.T) {\n\t\thelper.ClearAll(test.DB)\n\n\t\taccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\tassert.Nil(t, err)\n\n\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users\", nil)\n\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+accessToken)\n\n\t\tapiResponse, err := test.App.Test(request)\n\t\tassert.Nil(t, err)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t})\n\n\tt.Run(\"should call next with forbidden error if user does not have required rights and userId is not in params\", func(t *testing.T) {\n\t\thelper.ClearAll(test.DB)\n\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\taccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\tassert.Nil(t, err)\n\n\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users\", nil)\n\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+accessToken)\n\n\t\tapiResponse, err := test.App.Test(request)\n\t\tassert.Nil(t, err)\n\n\t\tassert.Equal(t, http.StatusForbidden, apiResponse.StatusCode)\n\t})\n\n\tt.Run(\"should call next with no errors if user does not have required rights but userId is in params\", func(t *testing.T) {\n\t\thelper.ClearAll(test.DB)\n\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\taccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\tassert.Nil(t, err)\n\n\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users/\"+fixture.UserOne.ID.String(), nil)\n\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+accessToken)\n\n\t\tapiResponse, err := test.App.Test(request)\n\t\tassert.Nil(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t})\n\n\tt.Run(\"should call next with no errors if user has required rights\", func(t *testing.T) {\n\t\thelper.ClearAll(test.DB)\n\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.Admin)\n\n\t\taccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\tassert.Nil(t, err)\n\n\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users/\"+fixture.UserOne.ID.String(), nil)\n\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+accessToken)\n\n\t\tapiResponse, err := test.App.Test(request)\n\t\tassert.Nil(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t})\n}\n"
  },
  {
    "path": "test/integration/health_check_test.go",
    "content": "package integration\n\nimport (\n\t\"app/src/response\"\n\t\"app/test\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHealthCheckRoutes(t *testing.T) {\n\tt.Run(\"GET /v1/health-check\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 and success response if request is ok\", func(t *testing.T) {\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/health-check\", nil)\n\n\t\t\tmsTimeout := 2000\n\t\t\tapiResponse, err := test.App.Test(request, msTimeout)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := new(response.HealthCheckResponse)\n\n\t\t\terr = json.Unmarshal(bytes, responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t\tassert.Equal(t, http.StatusOK, responseBody.Code)\n\t\t\tassert.Equal(t, \"success\", responseBody.Status)\n\t\t\tassert.Equal(t, \"Health check completed\", responseBody.Message)\n\t\t\tassert.Equal(t, true, responseBody.IsHealthy)\n\t\t\tassert.Equal(t, []response.HealthCheck{\n\t\t\t\t{\n\t\t\t\t\tName:   \"Postgre\",\n\t\t\t\t\tStatus: \"Up\",\n\t\t\t\t\tIsUp:   true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:   \"Memory\",\n\t\t\t\t\tStatus: \"Up\",\n\t\t\t\t\tIsUp:   true,\n\t\t\t\t},\n\t\t\t}, responseBody.Result)\n\t\t})\n\n\t\t// t.Run(\"should return 500 and error response if request failed\", func(t *testing.T) {\n\t\t// \trequest := httptest.NewRequest(http.MethodGet, \"/v1/health-check\", nil)\n\n\t\t// \tmsTimeout := 2000\n\t\t// \tapiResponse, err := test.App.Test(request, msTimeout)\n\t\t// \tassert.Nil(t, err)\n\n\t\t// \tassert.Equal(t, http.StatusInternalServerError, apiResponse.StatusCode)\n\n\t\t// \tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t// \tassert.Nil(t, err)\n\n\t\t// \tresponseBody := new(response.HealthCheckResponse)\n\n\t\t// \terr = json.Unmarshal(bytes, responseBody)\n\t\t// \tassert.Nil(t, err)\n\n\t\t// \tassert.Equal(t, http.StatusInternalServerError, apiResponse.StatusCode)\n\t\t// \tassert.Equal(t, http.StatusInternalServerError, responseBody.Code)\n\t\t// \tassert.Equal(t, \"error\", responseBody.Status)\n\t\t// \tassert.Equal(t, \"Health check completed\", responseBody.Message)\n\t\t// \tassert.Equal(t, false, responseBody.IsHealthy)\n\t\t// \tassert.Equal(t, []response.HealthCheck{\n\t\t// \t\t{\n\t\t// \t\t\tName:   \"Postgre\",\n\t\t// \t\t\tStatus: \"Down\",\n\t\t// \t\t\tIsUp:   false,\n\t\t// \t\t},\n\t\t// \t}, responseBody.Result)\n\t\t// })\n\t})\n}\n"
  },
  {
    "path": "test/integration/user_test.go",
    "content": "package integration\n\nimport (\n\t\"app/src/model\"\n\t\"app/src/response\"\n\t\"app/src/validation\"\n\t\"app/test\"\n\t\"app/test/fixture\"\n\t\"app/test/helper\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUserRoutes(t *testing.T) {\n\tt.Run(\"POST /v1/users\", func(t *testing.T) {\n\t\tvar newUser = validation.CreateUser{\n\t\t\tName:     \"Test\",\n\t\t\tEmail:    \"test@gmail.com\",\n\t\t\tPassword: \"password1\",\n\t\t\tRole:     \"user\",\n\t\t}\n\n\t\tt.Run(\"should return 201 and successfully create new user if data is ok\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(newUser)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/users\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := new(response.SuccessWithUser)\n\n\t\t\terr = json.Unmarshal(bytes, responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusCreated, apiResponse.StatusCode)\n\t\t\tassert.Equal(t, \"success\", responseBody.Status)\n\t\t\tassert.NotContains(t, string(bytes), \"password\")\n\t\t\tassert.NotNil(t, responseBody.User.ID)\n\t\t\tassert.Equal(t, newUser.Name, responseBody.User.Name)\n\t\t\tassert.Equal(t, newUser.Email, responseBody.User.Email)\n\t\t\tassert.Equal(t, \"user\", responseBody.User.Role)\n\t\t\tassert.Equal(t, false, responseBody.User.VerifiedEmail)\n\n\t\t\tuser, err := helper.GetUserByID(test.DB, responseBody.User.ID.String())\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.NotNil(t, user)\n\t\t\tassert.NotEqual(t, user.Password, newUser.Password)\n\t\t\tassert.Equal(t, user.Name, newUser.Name)\n\t\t\tassert.Equal(t, user.Email, newUser.Email)\n\t\t\tassert.Equal(t, user.Role, newUser.Role)\n\t\t\tassert.Equal(t, false, user.VerifiedEmail)\n\t\t})\n\n\t\tt.Run(\"should be able to create an admin as well\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\tnewUser.Role = \"admin\"\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(newUser)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/users\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := new(response.SuccessWithUser)\n\n\t\t\terr = json.Unmarshal(bytes, responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusCreated, apiResponse.StatusCode)\n\t\t\tassert.Equal(t, responseBody.User.Role, \"admin\")\n\n\t\t\tuser, err := helper.GetUserByID(test.DB, responseBody.User.ID.String())\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, user.Role, \"admin\")\n\t\t})\n\n\t\tt.Run(\"should return 401 error if access token is missing\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\n\t\t\tbodyJSON, err := json.Marshal(newUser)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/users\", strings.NewReader(string(bodyJSON)))\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 403 error if logged in user is not admin\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(newUser)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/users\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusForbidden, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if email is invalid\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\t\t\tnewUser.Email = \"invalidEmail\"\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(newUser)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/users\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 409 error if email is already used\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin, fixture.UserOne)\n\t\t\tnewUser.Email = fixture.UserOne.Email\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(newUser)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/users\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusConflict, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if password length is less than 8 characters\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\t\t\tnewUser.Password = \"passwo1\"\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(newUser)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/users\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if password does not contain both letters and numbers\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\t\t\tnewUser.Password = \"password\"\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(newUser)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/users\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\n\t\t\tnewUser.Password = \"1111111\"\n\n\t\t\tbodyJSON, err = json.Marshal(newUser)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest = httptest.NewRequest(http.MethodPost, \"/v1/users\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err = test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if role is neither user nor admin\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\t\t\tnewUser.Role = \"invalid\"\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(newUser)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/users\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if role is neither user or admin\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\t\t\tnewUser.Role = \"invalid\"\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(newUser)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPost, \"/v1/users\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\t})\n\n\tt.Run(\"GET /v1/users\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 and apply the default query options\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo, fixture.Admin)\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users\", nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := new(response.SuccessWithPaginate[model.User])\n\n\t\t\terr = json.Unmarshal(bytes, responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t\tassert.Equal(t, 1, responseBody.Page)\n\t\t\tassert.Equal(t, 10, responseBody.Limit)\n\t\t\tassert.Equal(t, int64(1), responseBody.TotalPages)\n\t\t\tassert.Equal(t, int64(3), responseBody.TotalResults)\n\n\t\t\tassert.Len(t, responseBody.Results, 3)\n\t\t\tassert.Equal(t, fixture.UserOne.ID, responseBody.Results[0].ID)\n\t\t\tassert.Equal(t, fixture.UserOne.Name, responseBody.Results[0].Name)\n\t\t\tassert.Equal(t, fixture.UserOne.Email, responseBody.Results[0].Email)\n\t\t\tassert.Equal(t, fixture.UserOne.Role, responseBody.Results[0].Role)\n\t\t\tassert.Equal(t, fixture.UserOne.VerifiedEmail, responseBody.Results[0].VerifiedEmail)\n\t\t})\n\n\t\tt.Run(\"should return 401 if access token is missing\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo, fixture.Admin)\n\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users\", nil)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 403 if a non-admin is trying to access all users\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo, fixture.Admin)\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users\", nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusForbidden, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should limit returned array if limit param is specified\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo, fixture.Admin)\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users?limit=2\", nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := new(response.SuccessWithPaginate[model.User])\n\n\t\t\terr = json.Unmarshal(bytes, responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t\tassert.Equal(t, 1, responseBody.Page)\n\t\t\tassert.Equal(t, 2, responseBody.Limit)\n\t\t\tassert.Equal(t, int64(2), responseBody.TotalPages)\n\t\t\tassert.Equal(t, int64(3), responseBody.TotalResults)\n\n\t\t\tassert.Len(t, responseBody.Results, 2)\n\t\t\tassert.Equal(t, fixture.UserOne.ID, responseBody.Results[0].ID)\n\t\t\tassert.Equal(t, fixture.UserTwo.ID, responseBody.Results[1].ID)\n\t\t})\n\n\t\tt.Run(\"should return the correct page if page and limit params are specified\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo, fixture.Admin)\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users?page=2&limit=2\", nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := new(response.SuccessWithPaginate[model.User])\n\n\t\t\terr = json.Unmarshal(bytes, responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t\tassert.Equal(t, 2, responseBody.Page)\n\t\t\tassert.Equal(t, 2, responseBody.Limit)\n\t\t\tassert.Equal(t, int64(2), responseBody.TotalPages)\n\t\t\tassert.Equal(t, int64(3), responseBody.TotalResults)\n\n\t\t\tassert.Len(t, responseBody.Results, 1)\n\t\t\tassert.Equal(t, fixture.Admin.ID, responseBody.Results[0].ID)\n\t\t})\n\t})\n\n\tt.Run(\"GET /v1/users/:userId\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 and the user object if data is ok\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users/\"+fixture.UserOne.ID.String(), nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := new(response.SuccessWithUser)\n\t\t\terr = json.Unmarshal(bytes, responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t\tassert.NotContains(t, string(bytes), \"password\")\n\t\t\tassert.Equal(t, responseBody.User.ID, fixture.UserOne.ID)\n\t\t\tassert.Equal(t, responseBody.User.Email, fixture.UserOne.Email)\n\t\t\tassert.Equal(t, responseBody.User.Name, fixture.UserOne.Name)\n\t\t\tassert.Equal(t, responseBody.User.Role, fixture.UserOne.Role)\n\t\t\tassert.Equal(t, responseBody.User.VerifiedEmail, fixture.UserOne.VerifiedEmail)\n\t\t})\n\n\t\tt.Run(\"should return 401 error if access token is missing\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users/\"+fixture.UserOne.ID.String(), nil)\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 403 error if user is trying to get another user\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo)\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users/\"+fixture.UserTwo.ID.String(), nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusForbidden, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 200 and the user object if admin is trying to get another user\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.Admin)\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users/\"+fixture.UserOne.ID.String(), nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if userId is not a valid postgres id\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users/invalidId\", nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 404 error if user is not found\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodGet, \"/v1/users/\"+fixture.UserOne.ID.String(), nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusNotFound, apiResponse.StatusCode)\n\t\t})\n\t})\n\n\tt.Run(\"DELETE /v1/users/:userId\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 if data is ok\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodDelete, \"/v1/users/\"+fixture.UserOne.ID.String(), nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\n\t\t\tuser, _ := helper.GetUserByID(test.DB, fixture.UserOne.ID.String())\n\t\t\tassert.Nil(t, user)\n\t\t})\n\n\t\tt.Run(\"should return 401 error if access token is missing\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\n\t\t\trequest := httptest.NewRequest(http.MethodDelete, \"/v1/users/\"+fixture.UserOne.ID.String(), nil)\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 403 error if user is trying to delete another user\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo)\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodDelete, \"/v1/users/\"+fixture.UserTwo.ID.String(), nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusForbidden, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 200 if admin is trying to delete another user\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.Admin)\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodDelete, \"/v1/users/\"+fixture.UserOne.ID.String(), nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if userId is not a valid postgres id\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodDelete, \"/v1/users/invalidId\", nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 404 error if user already is not found\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodDelete, \"/v1/users/\"+fixture.UserOne.ID.String(), nil)\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusNotFound, apiResponse.StatusCode)\n\t\t})\n\t})\n\n\tt.Run(\"PATCH /v1/users/:userId\", func(t *testing.T) {\n\t\tt.Run(\"should return 200 and successfully update user if data is ok\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\t\t\tupdateBody := validation.UpdateUser{\n\t\t\t\tName:     \"Golang\",\n\t\t\t\tEmail:    \"golang@gmail.com\",\n\t\t\t\tPassword: \"newPassword1\",\n\t\t\t}\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPatch, \"/v1/users/\"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbytes, err := io.ReadAll(apiResponse.Body)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tresponseBody := new(response.SuccessWithUser)\n\n\t\t\terr = json.Unmarshal(bytes, responseBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t\tassert.Equal(t, \"success\", responseBody.Status)\n\t\t\tassert.NotContains(t, string(bytes), \"password\")\n\t\t\tassert.Equal(t, fixture.UserOne.ID, responseBody.User.ID)\n\t\t\tassert.Equal(t, updateBody.Name, responseBody.User.Name)\n\t\t\tassert.Equal(t, updateBody.Email, responseBody.User.Email)\n\t\t\tassert.Equal(t, \"user\", responseBody.User.Role)\n\t\t\tassert.Equal(t, false, responseBody.User.VerifiedEmail)\n\n\t\t\tuser, err := helper.GetUserByID(test.DB, responseBody.User.ID.String())\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.NotNil(t, user)\n\t\t\tassert.NotEqual(t, user.Password, updateBody.Password)\n\t\t\tassert.Equal(t, user.Name, updateBody.Name)\n\t\t\tassert.Equal(t, user.Email, updateBody.Email)\n\t\t\tassert.Equal(t, user.Role, \"user\")\n\t\t})\n\n\t\tt.Run(\"should return 401 error if access token is missing\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\t\t\tupdateBody := validation.UpdateUser{\n\t\t\t\tName: \"Golang\",\n\t\t\t}\n\n\t\t\tbodyJSON, err := json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPatch, \"/v1/users/\"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 403 if user is updating another user\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo)\n\t\t\tupdateBody := validation.UpdateUser{\n\t\t\t\tName: \"Golang\",\n\t\t\t}\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPatch, \"/v1/users/\"+fixture.UserTwo.ID.String(), strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusForbidden, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 200 and successfully update user if admin is updating another user\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.Admin)\n\t\t\tupdateBody := validation.UpdateUser{\n\t\t\t\tName: \"Golang\",\n\t\t\t}\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPatch, \"/v1/users/\"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 404 if admin is updating another user that is not found\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\t\t\tupdateBody := validation.UpdateUser{\n\t\t\t\tName: \"Golang\",\n\t\t\t}\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPatch, \"/v1/users/\"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusNotFound, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 error if userId is not a valid postgres id\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.Admin)\n\t\t\tupdateBody := validation.UpdateUser{\n\t\t\t\tName: \"Golang\",\n\t\t\t}\n\n\t\t\tadminAccessToken, err := fixture.AccessToken(fixture.Admin)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPatch, \"/v1/users/invalidId\", strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+adminAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 if email is invalid\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\t\t\tupdateBody := validation.UpdateUser{\n\t\t\t\tEmail: \"invalidEmail\",\n\t\t\t}\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPatch, \"/v1/users/\"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 409 if email is already taken\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo)\n\t\t\tupdateBody := validation.UpdateUser{\n\t\t\t\tEmail: fixture.UserTwo.Email,\n\t\t\t}\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPatch, \"/v1/users/\"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusConflict, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should not return 400 if email is my email\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\t\t\tupdateBody := validation.UpdateUser{\n\t\t\t\tEmail: fixture.UserOne.Email,\n\t\t\t}\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPatch, \"/v1/users/\"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusOK, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 if password length is less than 8 characters\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\t\t\tupdateBody := validation.UpdateUser{\n\t\t\t\tPassword: \"passwo1\",\n\t\t\t}\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPatch, \"/v1/users/\"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\n\t\tt.Run(\"should return 400 if password does not contain both letters and numbers\", func(t *testing.T) {\n\t\t\thelper.ClearAll(test.DB)\n\t\t\thelper.InsertUser(test.DB, fixture.UserOne)\n\t\t\tupdateBody := validation.UpdateUser{\n\t\t\t\tPassword: \"password\",\n\t\t\t}\n\n\t\t\tuserOneAccessToken, err := fixture.AccessToken(fixture.UserOne)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tbodyJSON, err := json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest := httptest.NewRequest(http.MethodPatch, \"/v1/users/\"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err := test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\n\t\t\tupdateBody.Password = \"11111111\"\n\n\t\t\tbodyJSON, err = json.Marshal(updateBody)\n\t\t\tassert.Nil(t, err)\n\n\t\t\trequest = httptest.NewRequest(http.MethodPatch, \"/v1/users/\"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON)))\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\trequest.Header.Set(\"Accept\", \"application/json\")\n\t\t\trequest.Header.Set(\"Authorization\", \"Bearer \"+userOneAccessToken)\n\n\t\t\tapiResponse, err = test.App.Test(request)\n\t\t\tassert.Nil(t, err)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode)\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "test/unit/model/user_model_test.go",
    "content": "package model_test\n\nimport (\n\t\"app/src/model\"\n\t\"app/src/validation\"\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar validate = validation.Validator()\n\nfunc TestUserModel(t *testing.T) {\n\tt.Run(\"Create user validation\", func(t *testing.T) {\n\t\tvar newUser = validation.CreateUser{\n\t\t\tName:     \"John Doe\",\n\t\t\tEmail:    \"johndoe@gmail.com\",\n\t\t\tPassword: \"password1\",\n\t\t\tRole:     \"user\",\n\t\t}\n\n\t\tt.Run(\"should correctly validate a valid user\", func(t *testing.T) {\n\t\t\terr := validate.Struct(newUser)\n\t\t\tassert.NoError(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if email is invalid\", func(t *testing.T) {\n\t\t\tnewUser.Email = \"invalidEmail\"\n\t\t\terr := validate.Struct(newUser)\n\t\t\tassert.Error(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if password length is less than 8 characters\", func(t *testing.T) {\n\t\t\tnewUser.Password = \"passwo1\"\n\t\t\terr := validate.Struct(newUser)\n\t\t\tassert.Error(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if password does not contain numbers\", func(t *testing.T) {\n\t\t\tnewUser.Password = \"password\"\n\t\t\terr := validate.Struct(newUser)\n\t\t\tassert.Error(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if password does not contain letters\", func(t *testing.T) {\n\t\t\tnewUser.Password = \"11111111\"\n\t\t\terr := validate.Struct(newUser)\n\t\t\tassert.Error(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if role is unknown\", func(t *testing.T) {\n\t\t\tnewUser.Role = \"invalid\"\n\t\t\terr := validate.Struct(newUser)\n\t\t\tassert.Error(t, err)\n\t\t})\n\t})\n\n\tt.Run(\"Update user validation\", func(t *testing.T) {\n\t\tvar updateUser = validation.UpdateUser{\n\t\t\tName:     \"John Doe\",\n\t\t\tEmail:    \"johndoe@gmail.com\",\n\t\t\tPassword: \"password1\",\n\t\t}\n\n\t\tt.Run(\"should correctly validate a valid user\", func(t *testing.T) {\n\t\t\terr := validate.Struct(updateUser)\n\t\t\tassert.NoError(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if email is invalid\", func(t *testing.T) {\n\t\t\tupdateUser.Email = \"invalidEmail\"\n\t\t\terr := validate.Struct(updateUser)\n\t\t\tassert.Error(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if password length is less than 8 characters\", func(t *testing.T) {\n\t\t\tupdateUser.Password = \"passwo1\"\n\t\t\terr := validate.Struct(updateUser)\n\t\t\tassert.Error(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if password does not contain numbers\", func(t *testing.T) {\n\t\t\tupdateUser.Password = \"password\"\n\t\t\terr := validate.Struct(updateUser)\n\t\t\tassert.Error(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if password does not contain letters\", func(t *testing.T) {\n\t\t\tupdateUser.Password = \"11111111\"\n\t\t\terr := validate.Struct(updateUser)\n\t\t\tassert.Error(t, err)\n\t\t})\n\t})\n\n\tt.Run(\"Update user password validation\", func(t *testing.T) {\n\t\tvar newPassword = validation.UpdatePassOrVerify{\n\t\t\tPassword: \"password1\",\n\t\t}\n\n\t\tt.Run(\"should correctly validate a valid user password\", func(t *testing.T) {\n\t\t\terr := validate.Struct(newPassword)\n\t\t\tassert.NoError(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if password length is less than 8 characters\", func(t *testing.T) {\n\t\t\tnewPassword.Password = \"passwo1\"\n\t\t\terr := validate.Struct(newPassword)\n\t\t\tassert.Error(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if password does not contain numbers\", func(t *testing.T) {\n\t\t\tnewPassword.Password = \"password\"\n\t\t\terr := validate.Struct(newPassword)\n\t\t\tassert.Error(t, err)\n\t\t})\n\n\t\tt.Run(\"should throw a validation error if password does not contain letters\", func(t *testing.T) {\n\t\t\tnewPassword.Password = \"11111111\"\n\t\t\terr := validate.Struct(newPassword)\n\t\t\tassert.Error(t, err)\n\t\t})\n\t})\n\n\tt.Run(\"User toJSON()\", func(t *testing.T) {\n\t\tt.Run(\"should not return user password when toJSON is called\", func(t *testing.T) {\n\t\t\tuser := &model.User{\n\t\t\t\tName:     \"John Doe\",\n\t\t\t\tEmail:    \"johndoe@gmail.com\",\n\t\t\t\tPassword: \"password1\",\n\t\t\t\tRole:     \"user\",\n\t\t\t}\n\n\t\t\tbytes, _ := json.Marshal(user)\n\t\t\tassert.NotContains(t, string(bytes), \"password\")\n\t\t})\n\t})\n}\n"
  }
]