Repository: indrayyana/go-fiber-boilerplate Branch: main Commit: 0be2195024a3 Files: 77 Total size: 308.1 KB Directory structure: gitextract_mzrohxsq/ ├── .air.toml ├── .env.example ├── .github/ │ └── workflows/ │ ├── build.yml │ ├── linter.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── go.mod ├── go.sum ├── src/ │ ├── config/ │ │ ├── config.go │ │ ├── fiber.go │ │ ├── oauth2.go │ │ ├── roles.go │ │ └── tokens.go │ ├── controller/ │ │ ├── auth_controller.go │ │ ├── health_check_controller.go │ │ └── user_controller.go │ ├── database/ │ │ ├── database.go │ │ ├── init/ │ │ │ └── init.sql │ │ └── migrations/ │ │ ├── 20240929085103_create-table-users.down.sql │ │ ├── 20240929085103_create-table-users.up.sql │ │ ├── 20240929085107_create-table-tokens.down.sql │ │ └── 20240929085107_create-table-tokens.up.sql │ ├── docs/ │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ ├── main.go │ ├── middleware/ │ │ ├── auth.go │ │ ├── jwt.go │ │ ├── limiter.go │ │ ├── logger.go │ │ └── recover.go │ ├── model/ │ │ ├── token_model.go │ │ └── user_model.go │ ├── response/ │ │ ├── auth_response.go │ │ ├── error_response.go │ │ ├── example/ │ │ │ ├── error_example.go │ │ │ ├── example.go │ │ │ ├── health_check_example.go │ │ │ ├── token_example.go │ │ │ └── user_example.go │ │ ├── health_check_response.go │ │ ├── response.go │ │ └── user_response.go │ ├── router/ │ │ ├── auth_route.go │ │ ├── docs_route.go │ │ ├── health_check_route.go │ │ ├── router.go │ │ └── user_route.go │ ├── service/ │ │ ├── auth_service.go │ │ ├── email_service.go │ │ ├── health_check_service.go │ │ ├── token_service.go │ │ └── user_service.go │ ├── utils/ │ │ ├── bcrypt.go │ │ ├── error.go │ │ ├── logrus.go │ │ └── verify.go │ └── validation/ │ ├── auth_validation.go │ ├── custom_validation.go │ ├── user_validation.go │ └── validation.go └── test/ ├── fixture/ │ ├── token_fixture.go │ └── user_fixture.go ├── helper/ │ └── helper.go ├── init.go ├── integration/ │ ├── auth_test.go │ ├── health_check_test.go │ └── user_test.go └── unit/ └── model/ └── user_model_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .air.toml ================================================ # Config file for [Air](https://github.com/air-verse/air) in TOML format # Working directory # . or absolute path, please note that the directories following must be under root. root = "." tmp_dir = "tmp" [build] # Add additional arguments when running binary (bin/full_bin). args_bin = [] # Binary file yields from `cmd`. change binary to `main.exe` if you using windows bin = "./tmp/main" # Just plain old shell command. You could use `make` as well. change binary to `main.exe` if you using windows cmd = "go build -race -o ./tmp/main ./src" # It's not necessary to trigger build each time file changes if it's too frequent. delay = 1000 # ms # Ignore these filename extensions or directories. exclude_dir = ["assets", "tmp", "vendor"] # Exclude files. exclude_file = [] # Exclude specific regular expressions. exclude_regex = ["_test\\.go"] # Exclude unchanged files. exclude_unchanged = false # Follow symlink for directories follow_symlink = false # Customize binary, can setup environment variables when run your app. full_bin = "" # Watch these directories if you specified. include_dir = [] # Watch these filename extensions. include_ext = ["go", "tpl", "tmpl", "env"] # Watch these files. include_file = [] # Delay after sending Interrupt signal kill_delay = "0s" # This log file places in your tmp_dir. log = "build-errors.log" # Poll files for changes instead of using fsnotify. poll = false # Poll interval (defaults to the minimum interval of 500ms). poll_interval = 0 # ms # Array of commands to run after ^C post_cmd = [] # Array of commands to run before each build pre_cmd = [] # Rerun binary or not rerun = false # Delay after each execution rerun_delay = 500 # Send Interrupt signal before killing process (windows does not support this feature) send_interrupt = false # Stop running old binary when build errors occur. stop_on_error = false [color] # Customize each part's color. If no color found, use the raw app log. app = "" build = "yellow" main = "magenta" runner = "green" watcher = "cyan" [log] # Only show main log (silences watcher, build, runner) main_only = false # Show log time time = false [misc] # Delete tmp directory on exit clean_on_exit = true # Enable live-reloading on the browser. [proxy] app_port = 0 enabled = false proxy_port = 0 [screen] clear_on_rebuild = true keep_scroll = true ================================================ FILE: .env.example ================================================ # server configuration # Env value : prod || dev APP_ENV=dev APP_HOST=0.0.0.0 APP_PORT=3000 APP_URL=http://localhost:3000 # database configuration DB_HOST=postgresdb DB_USER=postgres DB_PASSWORD=thisisasamplepassword DB_NAME=fiberdb DB_PORT=5432 # JWT # JWT secret key JWT_SECRET=thisisasamplesecret # Number of minutes after which an access token expires JWT_ACCESS_EXP_MINUTES=30 # Number of days after which a refresh token expires JWT_REFRESH_EXP_DAYS=30 # Number of minutes after which a reset password token expires JWT_RESET_PASSWORD_EXP_MINUTES=10 # Number of minutes after which a verify email token expires JWT_VERIFY_EMAIL_EXP_MINUTES=10 # SMTP configuration options for the email service SMTP_HOST=email-server SMTP_PORT=587 SMTP_USERNAME=email-server-username SMTP_PASSWORD=email-server-password EMAIL_FROM=support@yourapp.com # OAuth2 configuration GOOGLE_CLIENT_ID=yourapps.googleusercontent.com GOOGLE_CLIENT_SECRET=thisisasamplesecret REDIRECT_URL=http://localhost:3000/v1/auth/google-callback ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: ["main"] pull_request: branches: ["main"] env: APP_ENV: dev APP_HOST: 0.0.0.0 APP_PORT: 3000 DB_HOST: localhost DB_USER: postgres DB_PASSWORD: thisisasamplepassword DB_NAME: fiberdb DB_PORT: 5432 jobs: GoFiber: runs-on: ubuntu-latest services: postgresdb: image: postgres:alpine env: POSTGRES_USER: postgres POSTGRES_PASSWORD: thisisasamplepassword POSTGRES_DB: fiberdb ports: - 5432:5432 options: >- --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 20s --health-retries 10 steps: - uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v4 with: go-version: "1.25" - name: Wait for PostgreSQL to be ready run: | until pg_isready -h localhost -U postgres -d fiberdb; do echo "Waiting for PostgreSQL..." sleep 5 done - name: Install dependencies run: go mod tidy - name: Build Go application run: CGO_ENABLED=0 GOOS=linux go build src/main.go env: APP_ENV: ${{ env.APP_ENV }} APP_HOST: ${{ env.APP_HOST }} APP_PORT: ${{ env.APP_PORT }} DB_HOST: ${{ env.DB_HOST }} DB_USER: ${{ env.DB_USER }} DB_PASSWORD: ${{ env.DB_PASSWORD }} DB_NAME: ${{ env.DB_NAME }} DB_PORT: ${{ env.DB_PORT }} ================================================ FILE: .github/workflows/linter.yml ================================================ name: Linter on: push: branches: ["main"] pull_request: branches: ["main"] jobs: Golint: runs-on: ubuntu-latest steps: - name: Fetch Repository uses: actions/checkout@v4 - name: Run Golint uses: reviewdog/action-golangci-lint@v2 with: golangci_lint_flags: "--config=.golangci.yml --tests=false" ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: branches: ["main"] pull_request: branches: ["main"] jobs: Tests: runs-on: ubuntu-latest steps: - name: Fetch Repository uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v4 with: go-version: "1.25" - name: Install dependencies run: go mod tidy - name: Run Unit Test run: go test ./test/unit/... -v -race ================================================ FILE: .gitignore ================================================ # Environment varibales .env* !.env*.example # Temporary tmp/ lint.txt main bin/golangci-lint ================================================ FILE: .golangci.yml ================================================ version: "2" linters: default: none enable: - asasalint - asciicheck - bidichk - bodyclose - canonicalheader - cyclop - dupl - durationcheck - errcheck - errname - errorlint - exhaustive - fatcontext - forbidigo - funlen - gocheckcompilerdirectives - gochecksumtype - gocognit - goconst - gocritic - gocyclo - gomoddirectives - gomodguard - goprintffuncname - gosec - govet - ineffassign - intrange - lll - loggercheck - makezero - mirror - musttag - nakedret - nestif - nilerr - nilnil - noctx - nolintlint - nonamedreturns - nosprintfhostport - perfsprint - predeclared - promlinter - protogetter - reassign - revive - rowserrcheck - sloglint - spancheck - sqlclosecheck - staticcheck - testableexamples - testpackage - tparallel - unconvert - unparam - unused - usestdlibvars - wastedassign - whitespace settings: cyclop: max-complexity: 30 package-average: 10 errcheck: check-type-assertions: true exhaustive: check: - switch - map exhaustruct: exclude: - ^net/http.Client$ - ^net/http.Cookie$ - ^net/http.Request$ - ^net/http.Response$ - ^net/http.Server$ - ^net/http.Transport$ - ^net/url.URL$ - ^os/exec.Cmd$ - ^reflect.StructField$ - ^github.com/Shopify/sarama.Config$ - ^github.com/Shopify/sarama.ProducerMessage$ - ^github.com/mitchellh/mapstructure.DecoderConfig$ - ^github.com/prometheus/client_golang/.+Opts$ - ^github.com/spf13/cobra.Command$ - ^github.com/spf13/cobra.CompletionOptions$ - ^github.com/stretchr/testify/mock.Mock$ - ^github.com/testcontainers/testcontainers-go.+Request$ - ^github.com/testcontainers/testcontainers-go.FromDockerfile$ - ^golang.org/x/tools/go/analysis.Analyzer$ - ^google.golang.org/protobuf/.+Options$ - ^gopkg.in/yaml.v3.Node$ funlen: lines: 100 statements: 50 ignore-comments: true gocognit: min-complexity: 20 gocritic: settings: captLocal: paramsOnly: false underef: skipRecvDeref: false gomodguard: blocked: modules: - github.com/golang/protobuf: recommendations: - google.golang.org/protobuf reason: see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules - github.com/satori/go.uuid: recommendations: - github.com/google/uuid reason: satori's package is not maintained - github.com/gofrs/uuid: recommendations: - github.com/gofrs/uuid/v5 reason: gofrs' package was not go module before v5 govet: disable: - fieldalignment enable-all: true settings: shadow: strict: true inamedparam: skip-single-param: true mnd: ignored-functions: - args.Error - flag.Arg - flag.Duration.* - flag.Float.* - flag.Int.* - flag.Uint.* - os.Chmod - os.Mkdir.* - os.OpenFile - os.WriteFile - prometheus.ExponentialBuckets.* - prometheus.LinearBuckets nakedret: max-func-lines: 0 nolintlint: require-explanation: true require-specific: true allow-no-explanation: - funlen - gocognit - lll perfsprint: strconcat: false rowserrcheck: packages: - github.com/jmoiron/sqlx sloglint: no-global: all context: scope exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling rules: - linters: - godot source: (noinspection|TODO) - linters: - gocritic source: //noinspection - linters: - lll path: example\.go - linters: - bodyclose - dupl - funlen - goconst - gosec - lll - noctx - testpackage - wrapcheck path: _test\.go paths: - third_party$ - builtin$ - examples$ issues: max-same-issues: 50 formatters: enable: - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at gdindra13@gmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing First off, thank you so much for taking the time to contribute. All contributions are more than welcome! ## How can I contribute? If 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: - **Fork the repo** - **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) - **Implement** the necessary changes - **Create tests** to keep the code coverage high - **Send a pull request** ## Guidelines ### Git commit messages Follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for clear and structured commit messages, here are the key guidelines: - Limit the subject line to 80 characters - Capitalize the first letter of the subject line - Use the present tense ("Add feature" instead of "Added feature") - Separate the subject from the body with a blank line ### Coding style guide We are using [golangci-lint](https://golangci-lint.run) to ensure consistent coding standards in this project. Please make sure that the code you are pushing conforms to the style guides mentioned above. ================================================ FILE: Dockerfile ================================================ FROM golang:1.25 AS build WORKDIR /app COPY . . RUN go clean --modcache RUN go mod tidy RUN CGO_ENABLED=0 GOOS=linux go build src/main.go FROM alpine:latest RUN apk add --no-cache curl tzdata WORKDIR /root COPY --from=build /app/main . COPY --from=build /app/.env . EXPOSE 3000 CMD ["./main"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 I Gede Indra Adnyana Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ include .env export $(shell sed 's/=.*//' .env) start: @go run src/main.go lint: @golangci-lint run tests: @go test -v ./test/... tests-%: @go test -v ./test/... -run=$(shell echo $* | sed 's/_/./g') testsum: @cd test && gotestsum --format testname swagger: @cd src && swag init migration-%: @migrate create -ext sql -dir src/database/migrations create-table-$(subst :,_,$*) migrate-up: @migrate -database "postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable" -path src/database/migrations up migrate-down: @migrate -database "postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable" -path src/database/migrations down migrate-docker-up: @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 migrate-docker-down: @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 docker: @chmod -R 755 ./src/database/init @docker compose up --build docker-test: @docker compose up -d && make tests docker-down: @docker compose down --rmi all --volumes --remove-orphans docker-cache: @docker builder prune -f ================================================ FILE: README.md ================================================ # RESTful API Go Fiber Boilerplate ![Go Version](https://img.shields.io/badge/Go-1.22+-00ADD8?style=flat&logo=go) [![Go Report Card](https://goreportcard.com/badge/github.com/indravscode/go-fiber-boilerplate)](https://goreportcard.com/report/github.com/indravscode/go-fiber-boilerplate) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) ![Repository size](https://img.shields.io/github/repo-size/indravscode/go-fiber-boilerplate?color=56BEB8) ![Build](https://github.com/indravscode/go-fiber-boilerplate/workflows/Build/badge.svg) ![Test](https://github.com/indravscode/go-fiber-boilerplate/workflows/Test/badge.svg) ![Linter](https://github.com/indravscode/go-fiber-boilerplate/workflows/Linter/badge.svg) A boilerplate/starter project for quickly building RESTful APIs using Go, Fiber, and PostgreSQL. Inspired by the Express boilerplate. The 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. ## Quick Start To create a project, simply run: ```bash go mod init ``` ## Manual Installation If you would still prefer to do the installation manually, follow these steps: Clone the repo: ```bash git clone --depth 1 https://github.com/indravscode/go-fiber-boilerplate.git cd go-fiber-boilerplate rm -rf ./.git ``` Install the dependencies: ```bash go mod tidy ``` Set the environment variables: ```bash cp .env.example .env # open .env and modify the environment variables (if needed) ``` ## Table of Contents - [Features](#features) - [Commands](#commands) - [Environment Variables](#environment-variables) - [Project Structure](#project-structure) - [API Documentation](#api-documentation) - [Error Handling](#error-handling) - [Validation](#validation) - [Authentication](#authentication) - [Authorization](#authorization) - [Logging](#logging) - [Linting](#linting) - [Contributing](#contributing) ## Features - **SQL database**: [PostgreSQL](https://www.postgresql.org) Object Relation Mapping using [Gorm](https://gorm.io) - **Database migrations**: with [golang-migrate](https://github.com/golang-migrate/migrate) - **Validation**: request data validation using [Package validator](https://github.com/go-playground/validator) - **Logging**: using [Logrus](https://github.com/sirupsen/logrus) and [Fiber-Logger](https://docs.gofiber.io/api/middleware/logger) - **Testing**: unit and integration tests using [Testify](https://github.com/stretchr/testify) and formatted test output using [gotestsum](https://github.com/gotestyourself/gotestsum) - **Error handling**: centralized error handling mechanism - **API documentation**: with [Swag](https://github.com/swaggo/swag) and [Swagger](https://github.com/gofiber/swagger) - **Sending email**: using [Gomail](https://github.com/go-gomail/gomail) - **Environment variables**: using [Viper](https://github.com/spf13/viper) - **Security**: set security HTTP headers using [Fiber-Helmet](https://docs.gofiber.io/api/middleware/helmet) - **CORS**: Cross-Origin Resource-Sharing enabled using [Fiber-CORS](https://docs.gofiber.io/api/middleware/cors) - **Compression**: gzip compression with [Fiber-Compress](https://docs.gofiber.io/api/middleware/compress) - **Docker support** - **Linting**: with [golangci-lint](https://golangci-lint.run) ## Commands Running locally: ```bash make start ``` Or running with live reload: ```bash air ``` > [!NOTE] > Make sure you have `Air` installed.\ > See 👉 [How to install Air](https://github.com/air-verse/air) Testing: ```bash # run all tests make tests # run all tests with gotestsum format make testsum # run test for the selected function name make tests-TestUserModel ``` > [!IMPORTANT] > Tests use a **separate test database**. > > By default, the test database name is defined in: > `test/init.go` > > ```go > DB = database.Connect("localhost", "testdb") > ``` > > Make sure the test database (`testdb`) **already exists** and all required > tables (`users`, `tokens`, etc.) have been migrated before running the test commands. Docker: ```bash # run docker container make docker # run all tests in a docker container make docker-test ``` Linting: ```bash # run lint make lint ``` Swagger: ```bash # generate the swagger documentation make swagger ``` Migration: ```bash # Create migration make migration- # Example for table users make migration-users ``` ```bash # run migration up in local make migrate-up # run migration down in local make migrate-down # run migration up in docker container make migrate-docker-up # run migration down all in docker container make migrate-docker-down ``` ## Environment Variables The environment variables can be found and modified in the `.env` file. They come with these default values: ```bash # server configuration # Env value : prod || dev APP_ENV=dev APP_HOST=0.0.0.0 APP_PORT=3000 # database configuration DB_HOST=postgresdb DB_USER=postgres DB_PASSWORD=thisisasamplepassword DB_NAME=fiberdb DB_PORT=5432 # JWT # JWT secret key JWT_SECRET=thisisasamplesecret # Number of minutes after which an access token expires JWT_ACCESS_EXP_MINUTES=30 # Number of days after which a refresh token expires JWT_REFRESH_EXP_DAYS=30 # Number of minutes after which a reset password token expires JWT_RESET_PASSWORD_EXP_MINUTES=10 # Number of minutes after which a verify email token expires JWT_VERIFY_EMAIL_EXP_MINUTES=10 # SMTP configuration options for the email service SMTP_HOST=email-server SMTP_PORT=587 SMTP_USERNAME=email-server-username SMTP_PASSWORD=email-server-password EMAIL_FROM=support@yourapp.com # OAuth2 configuration GOOGLE_CLIENT_ID=yourapps.googleusercontent.com GOOGLE_CLIENT_SECRET=thisisasamplesecret REDIRECT_URL=http://localhost:3000/v1/auth/google-callback ``` ## Project Structure ``` src\ |--config\ # Environment variables and configuration related things |--controller\ # Route controllers (controller layer) |--database\ # Database connection & migrations |--docs\ # Swagger files |--middleware\ # Custom fiber middlewares |--model\ # Postgres models (data layer) |--response\ # Response models |--router\ # Routes |--service\ # Business logic (service layer) |--utils\ # Utility classes and functions |--validation\ # Request data validation schemas |--main.go # Fiber app ``` ## API Documentation To view the list of available APIs and their specifications, run the server and go to `http://localhost:3000/v1/docs` in your browser. ![Auth](https://indravscode.github.io/assets/images/swagger1.png) ![User](https://indravscode.github.io/assets/images/swagger2.png) This documentation page is automatically generated using the [Swag](https://github.com/swaggo/swag) definitions written as comments in the controller files. See 👉 [Declarative Comments Format.](https://github.com/swaggo/swag#declarative-comments-format) ## API Endpoints List of available routes: **Auth routes**:\ `POST /v1/auth/register` - register\ `POST /v1/auth/login` - login\ `POST /v1/auth/logout` - logout\ `POST /v1/auth/refresh-tokens` - refresh auth tokens\ `POST /v1/auth/forgot-password` - send reset password email\ `POST /v1/auth/reset-password` - reset password\ `POST /v1/auth/send-verification-email` - send verification email\ `POST /v1/auth/verify-email` - verify email\ `GET /v1/auth/google` - login with google account **User routes**:\ `POST /v1/users` - create a user\ `GET /v1/users` - get all users\ `GET /v1/users/:userId` - get user\ `PATCH /v1/users/:userId` - update user\ `DELETE /v1/users/:userId` - delete user ## Error Handling The app includes a custom error handling mechanism, which can be found in the `src/utils/error.go` file. It 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. The error handling process sends an error response in the following format: ```json { "code": 404, "status": "error", "message": "Not found" } ``` Fiber 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. For 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: ```go func (s *userService) GetUserByID(c *fiber.Ctx, id string) { user := new(model.User) err := s.DB.WithContext(c.Context()).First(user, "id = ?", id).Error if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "User not found") } } ``` ## Validation Request 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. The 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. ```go import ( "app/src/model" "app/src/validation" "github.com/gofiber/fiber/v2" ) func (s *userService) CreateUser(c *fiber.Ctx, req validation.CreateUser) (*model.User, error) { if err := s.Validate.Struct(&req); err != nil { return nil, err } } ``` ## Authentication To require authentication for certain routes, you can use the `Auth` middleware. ```go import ( "app/src/controllers" m "app/src/middleware" "app/src/services" "github.com/gofiber/fiber/v2" ) func SetupRoutes(app *fiber.App, u services.UserService, t services.TokenService) { userController := controllers.NewUserController(u, t) app.Post("/users", m.Auth(u), userController.CreateUser) } ``` These 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. **Generating Access Tokens**: An 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). An 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. **Refreshing Access Tokens**: After 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. A 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. ## Authorization The `Auth` middleware can also be used to require certain rights/permissions to access a route. ```go import ( "app/src/controllers" m "app/src/middleware" "app/src/services" "github.com/gofiber/fiber/v2" ) func SetupRoutes(app *fiber.App, u services.UserService, t services.TokenService) { userController := controllers.NewUserController(u, t) app.Post("/users", m.Auth(u, "manageUsers"), userController.CreateUser) } ``` In the example above, an authenticated user can access this route only if that user has the `manageUsers` permission. The permissions are role-based. You can view the permissions/rights of each role in the `src/config/roles.go` file. If the user making the request does not have the required permissions to access this route, a Forbidden (403) error is thrown. ## Logging Import the logger from `src/utils/logrus.go`. It is using the [Logrus](https://github.com/sirupsen/logrus) logging library. Logging should be done according to the following severity levels (ascending order from most important to least important): ```go import "app/src/utils" utils.Log.Panic('message') // Calls panic() after logging utils.Log.Fatal('message'); // Calls os.Exit(1) after logging utils.Log.Error('message'); utils.Log.Warn('message'); utils.Log.Info('message'); utils.Log.Debug('message'); utils.Log.Trace('message'); ``` > [!NOTE] > API request information (request url, response code, timestamp, etc.) are also automatically logged (using [Fiber-Logger](https://docs.gofiber.io/api/middleware/logger)). ## Linting Linting is done using [golangci-lint](https://golangci-lint.run) See 👉 [How to install golangci-lint](https://golangci-lint.run/welcome/install) To modify the golangci-lint configuration, update the `.golangci.yml` file. ## Contributing Contributions are more than welcome! Please check out the [contributing guide](CONTRIBUTING.md). If you find this boilerplate useful, consider giving it a star! ⭐ ## Inspirations - [hagopj13/node-express-boilerplate](https://github.com/hagopj13/node-express-boilerplate) - [khannedy/golang-clean-architecture](https://github.com/khannedy/golang-clean-architecture) - [zexoverz/express-prisma-template](https://github.com/zexoverz/express-prisma-template) ## License [MIT](LICENSE) ## Contributors [![Contributors](https://contrib.rocks/image?c=6&repo=indravscode/go-fiber-boilerplate)](https://github.com/indravscode/go-fiber-boilerplate/graphs/contributors) ================================================ FILE: docker-compose.yml ================================================ services: adminer: image: adminer restart: always ports: - 8080:8080 networks: - go-network postgresdb: image: postgres:alpine restart: always healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] timeout: 20s retries: 10 ports: - ${DB_PORT}:5432 environment: - POSTGRES_USER=${DB_USER} - POSTGRES_PASSWORD=${DB_PASSWORD} - POSTGRES_DB=${DB_NAME} volumes: - dbdata:/var/lib/postgresql/data - ./src/database/init:/docker-entrypoint-initdb.d networks: - go-network go-app: build: . image: go-app ports: - ${APP_PORT}:3000 depends_on: postgresdb: condition: service_healthy volumes: - .:/usr/src/go-app restart: on-failure env_file: - .env networks: - go-network healthcheck: test: ["CMD", "curl", "-f", "${APP_URL}/v1/health-check"] interval: 40s timeout: 30s retries: 3 start_period: 30s volumes: dbdata: networks: go-network: driver: bridge ================================================ FILE: go.mod ================================================ module app go 1.24.0 require ( github.com/bytedance/sonic v1.14.2 github.com/go-playground/validator/v10 v10.29.0 github.com/gofiber/contrib/jwt v1.1.2 github.com/gofiber/fiber/v2 v2.52.10 github.com/gofiber/swagger v1.1.1 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/swaggo/swag v1.16.6 golang.org/x/crypto v0.46.0 golang.org/x/oauth2 v0.34.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.31.1 ) require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect github.com/go-openapi/jsonreference v0.21.4 // indirect github.com/go-openapi/spec v0.22.2 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/go-openapi/swag/jsonutils v0.25.4 // indirect github.com/go-openapi/swag/loading v0.25.4 // indirect github.com/go-openapi/swag/stringutils v0.25.4 // indirect github.com/go-openapi/swag/typeutils v0.25.4 // indirect github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.6 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/swaggo/files/v2 v2.0.2 // indirect github.com/tinylib/msgp v1.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.68.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/tools v0.40.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= github.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc= github.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk= github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gofiber/contrib/jwt v1.1.2 h1:GmWnOqT4A15EkA8IPXwSpvNUXZR4u5SMj+geBmyLAjs= github.com/gofiber/contrib/jwt v1.1.2/go.mod h1:CpIwrkUQ3Q6IP8y9n3f0wP9bOnSKx39EDp2fBVgMFVk= github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY= github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA= github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok= github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= ================================================ FILE: src/config/config.go ================================================ package config import ( "app/src/utils" "github.com/spf13/viper" ) var ( IsProd bool AppHost string AppPort int DBHost string DBUser string DBPassword string DBName string DBPort int JWTSecret string JWTAccessExp int JWTRefreshExp int JWTResetPasswordExp int JWTVerifyEmailExp int SMTPHost string SMTPPort int SMTPUsername string SMTPPassword string EmailFrom string GoogleClientID string GoogleClientSecret string RedirectURL string ) func init() { loadConfig() // server configuration IsProd = viper.GetString("APP_ENV") == "prod" AppHost = viper.GetString("APP_HOST") AppPort = viper.GetInt("APP_PORT") // database configuration DBHost = viper.GetString("DB_HOST") DBUser = viper.GetString("DB_USER") DBPassword = viper.GetString("DB_PASSWORD") DBName = viper.GetString("DB_NAME") DBPort = viper.GetInt("DB_PORT") // jwt configuration JWTSecret = viper.GetString("JWT_SECRET") JWTAccessExp = viper.GetInt("JWT_ACCESS_EXP_MINUTES") JWTRefreshExp = viper.GetInt("JWT_REFRESH_EXP_DAYS") JWTResetPasswordExp = viper.GetInt("JWT_RESET_PASSWORD_EXP_MINUTES") JWTVerifyEmailExp = viper.GetInt("JWT_VERIFY_EMAIL_EXP_MINUTES") // SMTP configuration SMTPHost = viper.GetString("SMTP_HOST") SMTPPort = viper.GetInt("SMTP_PORT") SMTPUsername = viper.GetString("SMTP_USERNAME") SMTPPassword = viper.GetString("SMTP_PASSWORD") EmailFrom = viper.GetString("EMAIL_FROM") // oauth2 configuration GoogleClientID = viper.GetString("GOOGLE_CLIENT_ID") GoogleClientSecret = viper.GetString("GOOGLE_CLIENT_SECRET") RedirectURL = viper.GetString("REDIRECT_URL") } func loadConfig() { configPaths := []string{ "./", // For app "../../", // For test folder } for _, path := range configPaths { viper.SetConfigFile(path + ".env") if err := viper.ReadInConfig(); err == nil { utils.Log.Infof("Config file loaded from %s", path) return } } utils.Log.Error("Failed to load any config file") } ================================================ FILE: src/config/fiber.go ================================================ package config import ( "app/src/utils" "github.com/bytedance/sonic" "github.com/gofiber/fiber/v2" ) func FiberConfig() fiber.Config { return fiber.Config{ Prefork: IsProd, CaseSensitive: true, ServerHeader: "Fiber", AppName: "Fiber API", ErrorHandler: utils.ErrorHandler, JSONEncoder: sonic.Marshal, JSONDecoder: sonic.Unmarshal, } } ================================================ FILE: src/config/oauth2.go ================================================ package config import ( "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) type Config struct { GoogleLoginConfig oauth2.Config } var AppConfig Config func GoogleConfig() oauth2.Config { AppConfig.GoogleLoginConfig = oauth2.Config{ RedirectURL: RedirectURL, ClientID: GoogleClientID, ClientSecret: GoogleClientSecret, Scopes: []string{ "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", }, Endpoint: google.Endpoint, } return AppConfig.GoogleLoginConfig } ================================================ FILE: src/config/roles.go ================================================ package config var allRoles = map[string][]string{ "user": {}, "admin": {"getUsers", "manageUsers"}, } var Roles = getKeys(allRoles) var RoleRights = allRoles func getKeys(m map[string][]string) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys } ================================================ FILE: src/config/tokens.go ================================================ package config const ( TokenTypeAccess = "access" TokenTypeRefresh = "refresh" TokenTypeResetPassword = "resetPassword" TokenTypeVerifyEmail = "verifyEmail" ) ================================================ FILE: src/controller/auth_controller.go ================================================ package controller import ( "app/src/config" "app/src/model" "app/src/response" "app/src/service" "app/src/validation" "context" "encoding/json" "io" "net/http" "github.com/gofiber/fiber/v2" "github.com/google/uuid" ) type AuthController struct { AuthService service.AuthService UserService service.UserService TokenService service.TokenService EmailService service.EmailService } func NewAuthController( authService service.AuthService, userService service.UserService, tokenService service.TokenService, emailService service.EmailService, ) *AuthController { return &AuthController{ AuthService: authService, UserService: userService, TokenService: tokenService, EmailService: emailService, } } // @Tags Auth // @Summary Register as user // @Accept json // @Produce json // @Param request body validation.Register true "Request body" // @Router /auth/register [post] // @Success 201 {object} example.RegisterResponse // @Failure 409 {object} example.DuplicateEmail "Email already taken" func (a *AuthController) Register(c *fiber.Ctx) error { req := new(validation.Register) if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } user, err := a.AuthService.Register(c, req) if err != nil { return err } tokens, err := a.TokenService.GenerateAuthTokens(c, user) if err != nil { return err } return c.Status(fiber.StatusCreated). JSON(response.SuccessWithTokens{ Code: fiber.StatusCreated, Status: "success", Message: "Register successfully", User: *user, Tokens: *tokens, }) } // @Tags Auth // @Summary Login // @Accept json // @Produce json // @Param request body validation.Login true "Request body" // @Router /auth/login [post] // @Success 200 {object} example.LoginResponse // @Failure 401 {object} example.FailedLogin "Invalid email or password" func (a *AuthController) Login(c *fiber.Ctx) error { req := new(validation.Login) if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } user, err := a.AuthService.Login(c, req) if err != nil { return err } tokens, err := a.TokenService.GenerateAuthTokens(c, user) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.SuccessWithTokens{ Code: fiber.StatusOK, Status: "success", Message: "Login successfully", User: *user, Tokens: *tokens, }) } // @Tags Auth // @Summary Logout // @Accept json // @Produce json // @Param request body example.RefreshToken true "Request body" // @Router /auth/logout [post] // @Success 200 {object} example.LogoutResponse // @Failure 404 {object} example.NotFound "Not found" func (a *AuthController) Logout(c *fiber.Ctx) error { req := new(validation.Logout) if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if err := a.AuthService.Logout(c, req); err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Common{ Code: fiber.StatusOK, Status: "success", Message: "Logout successfully", }) } // @Tags Auth // @Summary Refresh auth tokens // @Accept json // @Produce json // @Param request body example.RefreshToken true "Request body" // @Router /auth/refresh-tokens [post] // @Success 200 {object} example.RefreshTokenResponse // @Failure 401 {object} example.Unauthorized "Unauthorized" func (a *AuthController) RefreshTokens(c *fiber.Ctx) error { req := new(validation.RefreshToken) if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } tokens, err := a.AuthService.RefreshAuth(c, req) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.RefreshToken{ Code: fiber.StatusOK, Status: "success", Tokens: *tokens, }) } // @Tags Auth // @Summary Forgot password // @Description An email will be sent to reset password. // @Accept json // @Produce json // @Param request body validation.ForgotPassword true "Request body" // @Router /auth/forgot-password [post] // @Success 200 {object} example.ForgotPasswordResponse // @Failure 404 {object} example.NotFound "Not found" func (a *AuthController) ForgotPassword(c *fiber.Ctx) error { req := new(validation.ForgotPassword) if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } resetPasswordToken, err := a.TokenService.GenerateResetPasswordToken(c, req) if err != nil { return err } if errEmail := a.EmailService.SendResetPasswordEmail(req.Email, resetPasswordToken); errEmail != nil { return errEmail } return c.Status(fiber.StatusOK). JSON(response.Common{ Code: fiber.StatusOK, Status: "success", Message: "A password reset link has been sent to your email address.", }) } // @Tags Auth // @Summary Reset password // @Accept json // @Produce json // @Param token query string true "The reset password token" // @Param request body validation.UpdatePassOrVerify true "Request body" // @Router /auth/reset-password [post] // @Success 200 {object} example.ResetPasswordResponse // @Failure 401 {object} example.FailedResetPassword "Password reset failed" func (a *AuthController) ResetPassword(c *fiber.Ctx) error { req := new(validation.UpdatePassOrVerify) query := &validation.Token{ Token: c.Query("token"), } if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if err := a.AuthService.ResetPassword(c, query, req); err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Common{ Code: fiber.StatusOK, Status: "success", Message: "Update password successfully", }) } // @Tags Auth // @Summary Send verification email // @Description An email will be sent to verify email. // @Security BearerAuth // @Produce json // @Router /auth/send-verification-email [post] // @Success 200 {object} example.SendVerificationEmailResponse // @Failure 401 {object} example.Unauthorized "Unauthorized" func (a *AuthController) SendVerificationEmail(c *fiber.Ctx) error { user, _ := c.Locals("user").(*model.User) verifyEmailToken, err := a.TokenService.GenerateVerifyEmailToken(c, user) if err != nil { return err } if errEmail := a.EmailService.SendVerificationEmail(user.Email, *verifyEmailToken); errEmail != nil { return errEmail } return c.Status(fiber.StatusOK). JSON(response.Common{ Code: fiber.StatusOK, Status: "success", Message: "Please check your email for a link to verify your account", }) } // @Tags Auth // @Summary Verify email // @Produce json // @Param token query string true "The verify email token" // @Router /auth/verify-email [post] // @Success 200 {object} example.VerifyEmailResponse // @Failure 401 {object} example.FailedVerifyEmail "Verify email failed" func (a *AuthController) VerifyEmail(c *fiber.Ctx) error { query := &validation.Token{ Token: c.Query("token"), } if err := a.AuthService.VerifyEmail(c, query); err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Common{ Code: fiber.StatusOK, Status: "success", Message: "Verify email successfully", }) } // @Tags Auth // @Summary Login with google // @Description This route initiates the Google OAuth2 login flow. Please try this in your browser. // @Router /auth/google [get] // @Success 200 {object} example.GoogleLoginResponse func (a *AuthController) GoogleLogin(c *fiber.Ctx) error { // Generate a random state state := uuid.New().String() c.Cookie(&fiber.Cookie{ Name: "oauth_state", Value: state, MaxAge: 30, }) url := config.AppConfig.GoogleLoginConfig.AuthCodeURL(state) return c.Status(fiber.StatusSeeOther).Redirect(url) } func (a *AuthController) GoogleCallback(c *fiber.Ctx) error { state := c.Query("state") storedState := c.Cookies("oauth_state") if state != storedState { return fiber.NewError(fiber.StatusUnauthorized, "States don't Match!") } code := c.Query("code") googlecon := config.GoogleConfig() token, err := googlecon.Exchange(context.Background(), code) if err != nil { return err } req, err := http.NewRequestWithContext( c.Context(), http.MethodGet, "https://www.googleapis.com/oauth2/v2/userinfo?access_token="+token.AccessToken, nil, ) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() userData, err := io.ReadAll(resp.Body) if err != nil { return err } googleUser := new(validation.GoogleLogin) if errJSON := json.Unmarshal(userData, googleUser); errJSON != nil { return errJSON } user, err := a.UserService.CreateGoogleUser(c, googleUser) if err != nil { return err } tokens, err := a.TokenService.GenerateAuthTokens(c, user) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.SuccessWithTokens{ Code: fiber.StatusOK, Status: "success", Message: "Login successfully", User: *user, Tokens: *tokens, }) // TODO: replace this url with the link to the oauth google success page of your front-end app // googleLoginURL := fmt.Sprintf("http://link-to-app/google/success?access_token=%s&refresh_token=%s", // tokens.Access.Token, tokens.Refresh.Token) // return c.Status(fiber.StatusSeeOther).Redirect(googleLoginURL) } ================================================ FILE: src/controller/health_check_controller.go ================================================ package controller import ( "app/src/response" "app/src/service" "github.com/gofiber/fiber/v2" ) type HealthCheckController struct { HealthCheckService service.HealthCheckService } func NewHealthCheckController(healthCheckService service.HealthCheckService) *HealthCheckController { return &HealthCheckController{ HealthCheckService: healthCheckService, } } func (h *HealthCheckController) addServiceStatus( serviceList *[]response.HealthCheck, name string, isUp bool, message *string, ) { status := "Up" if !isUp { status = "Down" } *serviceList = append(*serviceList, response.HealthCheck{ Name: name, Status: status, IsUp: isUp, Message: message, }) } // @Tags Health // @Summary Health Check // @Description Check the status of services and database connections // @Accept json // @Produce json // @Success 200 {object} example.HealthCheckResponse // @Failure 500 {object} example.HealthCheckResponseError // @Router /health-check [get] func (h *HealthCheckController) Check(c *fiber.Ctx) error { isHealthy := true var serviceList []response.HealthCheck // Check the database connection if err := h.HealthCheckService.GormCheck(); err != nil { isHealthy = false errMsg := err.Error() h.addServiceStatus(&serviceList, "Postgre", false, &errMsg) } else { h.addServiceStatus(&serviceList, "Postgre", true, nil) } if err := h.HealthCheckService.MemoryHeapCheck(); err != nil { isHealthy = false errMsg := err.Error() h.addServiceStatus(&serviceList, "Memory", false, &errMsg) } else { h.addServiceStatus(&serviceList, "Memory", true, nil) } // Return the response based on health check result statusCode := fiber.StatusOK status := "success" if !isHealthy { statusCode = fiber.StatusInternalServerError status = "error" } return c.Status(statusCode).JSON(response.HealthCheckResponse{ Status: status, Message: "Health check completed", Code: statusCode, IsHealthy: isHealthy, Result: serviceList, }) } ================================================ FILE: src/controller/user_controller.go ================================================ package controller import ( "app/src/model" "app/src/response" "app/src/service" "app/src/validation" "math" "github.com/gofiber/fiber/v2" "github.com/google/uuid" ) type UserController struct { UserService service.UserService TokenService service.TokenService } func NewUserController(userService service.UserService, tokenService service.TokenService) *UserController { return &UserController{ UserService: userService, TokenService: tokenService, } } // @Tags Users // @Summary Get all users // @Description Only admins can retrieve all users. // @Security BearerAuth // @Produce json // @Param page query int false "Page number" default(1) // @Param limit query int false "Maximum number of users" default(10) // @Param search query string false "Search by name or email or role" // @Router /users [get] // @Success 200 {object} example.GetAllUserResponse // @Failure 401 {object} example.Unauthorized "Unauthorized" // @Failure 403 {object} example.Forbidden "Forbidden" func (u *UserController) GetUsers(c *fiber.Ctx) error { query := &validation.QueryUser{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), Search: c.Query("search", ""), } users, totalResults, err := u.UserService.GetUsers(c, query) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[model.User]{ Code: fiber.StatusOK, Status: "success", Message: "Get all users successfully", Results: users, Page: query.Page, Limit: query.Limit, TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }) } // @Tags Users // @Summary Get a user // @Description Logged in users can fetch only their own user information. Only admins can fetch other users. // @Security BearerAuth // @Produce json // @Param id path string true "User id" // @Router /users/{id} [get] // @Success 200 {object} example.GetUserResponse // @Failure 401 {object} example.Unauthorized "Unauthorized" // @Failure 403 {object} example.Forbidden "Forbidden" // @Failure 404 {object} example.NotFound "Not found" func (u *UserController) GetUserByID(c *fiber.Ctx) error { userID := c.Params("userId") if _, err := uuid.Parse(userID); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid user ID") } user, err := u.UserService.GetUserByID(c, userID) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.SuccessWithUser{ Code: fiber.StatusOK, Status: "success", Message: "Get user successfully", User: *user, }) } // @Tags Users // @Summary Create a user // @Description Only admins can create other users. // @Security BearerAuth // @Produce json // @Param request body validation.CreateUser true "Request body" // @Router /users [post] // @Success 201 {object} example.CreateUserResponse // @Failure 401 {object} example.Unauthorized "Unauthorized" // @Failure 403 {object} example.Forbidden "Forbidden" // @Failure 409 {object} example.DuplicateEmail "Email already taken" func (u *UserController) CreateUser(c *fiber.Ctx) error { req := new(validation.CreateUser) if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } user, err := u.UserService.CreateUser(c, req) if err != nil { return err } return c.Status(fiber.StatusCreated). JSON(response.SuccessWithUser{ Code: fiber.StatusCreated, Status: "success", Message: "Create user successfully", User: *user, }) } // @Tags Users // @Summary Update a user // @Description Logged in users can only update their own information. Only admins can update other users. // @Security BearerAuth // @Produce json // @Param id path string true "User id" // @Param request body validation.UpdateUser true "Request body" // @Router /users/{id} [patch] // @Success 200 {object} example.UpdateUserResponse // @Failure 401 {object} example.Unauthorized "Unauthorized" // @Failure 403 {object} example.Forbidden "Forbidden" // @Failure 404 {object} example.NotFound "Not found" // @Failure 409 {object} example.DuplicateEmail "Email already taken" func (u *UserController) UpdateUser(c *fiber.Ctx) error { req := new(validation.UpdateUser) userID := c.Params("userId") if _, err := uuid.Parse(userID); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid user ID") } if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } user, err := u.UserService.UpdateUser(c, req, userID) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.SuccessWithUser{ Code: fiber.StatusOK, Status: "success", Message: "Update user successfully", User: *user, }) } // @Tags Users // @Summary Delete a user // @Description Logged in users can delete only themselves. Only admins can delete other users. // @Security BearerAuth // @Produce json // @Param id path string true "User id" // @Router /users/{id} [delete] // @Success 200 {object} example.DeleteUserResponse // @Failure 401 {object} example.Unauthorized "Unauthorized" // @Failure 403 {object} example.Forbidden "Forbidden" // @Failure 404 {object} example.NotFound "Not found" func (u *UserController) DeleteUser(c *fiber.Ctx) error { userID := c.Params("userId") if _, err := uuid.Parse(userID); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid user ID") } if err := u.TokenService.DeleteAllToken(c, userID); err != nil { return err } if err := u.UserService.DeleteUser(c, userID); err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Common{ Code: fiber.StatusOK, Status: "success", Message: "Delete user successfully", }) } ================================================ FILE: src/database/database.go ================================================ package database import ( "app/src/config" "app/src/utils" "fmt" "time" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) func Connect(dbHost, dbName string) *gorm.DB { dsn := fmt.Sprintf( "host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", dbHost, config.DBUser, config.DBPassword, dbName, config.DBPort, ) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Info), SkipDefaultTransaction: true, PrepareStmt: true, TranslateError: true, }) if err != nil { utils.Log.Errorf("Failed to connect to database: %+v", err) } sqlDB, errDB := db.DB() if errDB != nil { utils.Log.Errorf("Failed to connect to database: %+v", errDB) } // Config connection pooling sqlDB.SetMaxIdleConns(10) sqlDB.SetMaxOpenConns(100) sqlDB.SetConnMaxLifetime(60 * time.Minute) return db } ================================================ FILE: src/database/init/init.sql ================================================ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE DATABASE testdb; ================================================ FILE: src/database/migrations/20240929085103_create-table-users.down.sql ================================================ DROP TABLE IF EXISTS users; ================================================ FILE: src/database/migrations/20240929085103_create-table-users.up.sql ================================================ CREATE TABLE users( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, role VARCHAR(255) NOT NULL, verified_email BOOLEAN DEFAULT FALSE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL ); ================================================ FILE: src/database/migrations/20240929085107_create-table-tokens.down.sql ================================================ DROP TABLE IF EXISTS tokens; ================================================ FILE: src/database/migrations/20240929085107_create-table-tokens.up.sql ================================================ CREATE TABLE tokens( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), token VARCHAR(255) NOT NULL, user_id UUID NOT NULL, type VARCHAR(255) NOT NULL, expires TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); ================================================ FILE: src/docs/docs.go ================================================ // Package docs Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", "contact": {}, "license": { "name": "MIT", "url": "https://github.com/indrayyana/go-fiber-boilerplate/blob/main/LICENSE" }, "version": "{{.Version}}" }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { "/auth/forgot-password": { "post": { "description": "An email will be sent to reset password.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Forgot password", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.ForgotPassword" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.ForgotPasswordResponse" } }, "404": { "description": "Not found", "schema": { "$ref": "#/definitions/example.NotFound" } } } } }, "/auth/google": { "get": { "description": "This route initiates the Google OAuth2 login flow. Please try this in your browser.", "tags": [ "Auth" ], "summary": "Login with google", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.GoogleLoginResponse" } } } } }, "/auth/login": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Login", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.Login" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.LoginResponse" } }, "401": { "description": "Invalid email or password", "schema": { "$ref": "#/definitions/example.FailedLogin" } } } } }, "/auth/logout": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Logout", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.RefreshToken" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.LogoutResponse" } }, "404": { "description": "Not found", "schema": { "$ref": "#/definitions/example.NotFound" } } } } }, "/auth/refresh-tokens": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Refresh auth tokens", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.RefreshToken" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.RefreshTokenResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } } } } }, "/auth/register": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Register as user", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.Register" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/example.RegisterResponse" } }, "409": { "description": "Email already taken", "schema": { "$ref": "#/definitions/example.DuplicateEmail" } } } } }, "/auth/reset-password": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Reset password", "parameters": [ { "type": "string", "description": "The reset password token", "name": "token", "in": "query", "required": true }, { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.UpdatePassOrVerify" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.ResetPasswordResponse" } }, "401": { "description": "Password reset failed", "schema": { "$ref": "#/definitions/example.FailedResetPassword" } } } } }, "/auth/send-verification-email": { "post": { "security": [ { "BearerAuth": [] } ], "description": "An email will be sent to verify email.", "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Send verification email", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.SendVerificationEmailResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } } } } }, "/auth/verify-email": { "post": { "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Verify email", "parameters": [ { "type": "string", "description": "The verify email token", "name": "token", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.VerifyEmailResponse" } }, "401": { "description": "Verify email failed", "schema": { "$ref": "#/definitions/example.FailedVerifyEmail" } } } } }, "/health-check": { "get": { "description": "Check the status of services and database connections", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Health" ], "summary": "Health Check", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.HealthCheckResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/example.HealthCheckResponseError" } } } } }, "/users": { "get": { "security": [ { "BearerAuth": [] } ], "description": "Only admins can retrieve all users.", "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Get all users", "parameters": [ { "type": "integer", "default": 1, "description": "Page number", "name": "page", "in": "query" }, { "type": "integer", "default": 10, "description": "Maximum number of users", "name": "limit", "in": "query" }, { "type": "string", "description": "Search by name or email or role", "name": "search", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.GetAllUserResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/example.Forbidden" } } } }, "post": { "security": [ { "BearerAuth": [] } ], "description": "Only admins can create other users.", "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Create a user", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.CreateUser" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/example.CreateUserResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/example.Forbidden" } }, "409": { "description": "Email already taken", "schema": { "$ref": "#/definitions/example.DuplicateEmail" } } } } }, "/users/{id}": { "get": { "security": [ { "BearerAuth": [] } ], "description": "Logged in users can fetch only their own user information. Only admins can fetch other users.", "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Get a user", "parameters": [ { "type": "string", "description": "User id", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.GetUserResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/example.Forbidden" } }, "404": { "description": "Not found", "schema": { "$ref": "#/definitions/example.NotFound" } } } }, "delete": { "security": [ { "BearerAuth": [] } ], "description": "Logged in users can delete only themselves. Only admins can delete other users.", "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Delete a user", "parameters": [ { "type": "string", "description": "User id", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.DeleteUserResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/example.Forbidden" } }, "404": { "description": "Not found", "schema": { "$ref": "#/definitions/example.NotFound" } } } }, "patch": { "security": [ { "BearerAuth": [] } ], "description": "Logged in users can only update their own information. Only admins can update other users.", "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Update a user", "parameters": [ { "type": "string", "description": "User id", "name": "id", "in": "path", "required": true }, { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.UpdateUser" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.UpdateUserResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/example.Forbidden" } }, "404": { "description": "Not found", "schema": { "$ref": "#/definitions/example.NotFound" } }, "409": { "description": "Email already taken", "schema": { "$ref": "#/definitions/example.DuplicateEmail" } } } } } }, "definitions": { "example.CreateUserResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 201 }, "message": { "type": "string", "example": "Create user successfully" }, "status": { "type": "string", "example": "success" }, "user": { "$ref": "#/definitions/example.User" } } }, "example.DeleteUserResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Delete user successfully" }, "status": { "type": "string", "example": "success" } } }, "example.DuplicateEmail": { "type": "object", "properties": { "code": { "type": "integer", "example": 409 }, "message": { "type": "string", "example": "Email already taken" }, "status": { "type": "string", "example": "error" } } }, "example.FailedLogin": { "type": "object", "properties": { "code": { "type": "integer", "example": 401 }, "message": { "type": "string", "example": "Invalid email or password" }, "status": { "type": "string", "example": "error" } } }, "example.FailedResetPassword": { "type": "object", "properties": { "code": { "type": "integer", "example": 401 }, "message": { "type": "string", "example": "Password reset failed" }, "status": { "type": "string", "example": "error" } } }, "example.FailedVerifyEmail": { "type": "object", "properties": { "code": { "type": "integer", "example": 401 }, "message": { "type": "string", "example": "Verify email failed" }, "status": { "type": "string", "example": "error" } } }, "example.Forbidden": { "type": "object", "properties": { "code": { "type": "integer", "example": 403 }, "message": { "type": "string", "example": "You don't have permission to access this resource" }, "status": { "type": "string", "example": "error" } } }, "example.ForgotPasswordResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "A password reset link has been sent to your email address." }, "status": { "type": "string", "example": "success" } } }, "example.GetAllUserResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "limit": { "type": "integer", "example": 10 }, "message": { "type": "string", "example": "Get all users successfully" }, "page": { "type": "integer", "example": 1 }, "results": { "type": "array", "items": { "$ref": "#/definitions/example.User" } }, "status": { "type": "string", "example": "success" }, "total_pages": { "type": "integer", "example": 1 }, "total_results": { "type": "integer", "example": 1 } } }, "example.GetUserResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Get user successfully" }, "status": { "type": "string", "example": "success" }, "user": { "$ref": "#/definitions/example.User" } } }, "example.GoogleLoginResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Login successfully" }, "status": { "type": "string", "example": "success" }, "tokens": { "$ref": "#/definitions/example.Tokens" }, "user": { "$ref": "#/definitions/example.GoogleUser" } } }, "example.GoogleUser": { "type": "object", "properties": { "email": { "type": "string", "example": "fake@example.com" }, "id": { "type": "string", "example": "e088d183-9eea-4a11-8d5d-74d7ec91bdf5" }, "name": { "type": "string", "example": "fake name" }, "role": { "type": "string", "example": "user" }, "verified_email": { "type": "boolean", "example": true } } }, "example.HealthCheck": { "type": "object", "properties": { "is_up": { "type": "boolean", "example": true }, "name": { "type": "string", "example": "Postgre" }, "status": { "type": "string", "example": "Up" } } }, "example.HealthCheckError": { "type": "object", "properties": { "is_up": { "type": "boolean", "example": false }, "message": { "type": "string", "example": "failed to connect to 'host=localhost user=postgres database=wrongdb': server error (FATAL: database \"wrongdb\" does not exist (SQLSTATE 3D000))" }, "name": { "type": "string", "example": "Postgre" }, "status": { "type": "string", "example": "Down" } } }, "example.HealthCheckResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "is_healthy": { "type": "boolean", "example": true }, "message": { "type": "string", "example": "Health check completed" }, "result": { "type": "array", "items": { "$ref": "#/definitions/example.HealthCheck" } }, "status": { "type": "string", "example": "success" } } }, "example.HealthCheckResponseError": { "type": "object", "properties": { "code": { "type": "integer", "example": 500 }, "is_healthy": { "type": "boolean", "example": false }, "message": { "type": "string", "example": "Health check completed" }, "result": { "type": "array", "items": { "$ref": "#/definitions/example.HealthCheckError" } }, "status": { "type": "string", "example": "error" } } }, "example.LoginResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Login successfully" }, "status": { "type": "string", "example": "success" }, "tokens": { "$ref": "#/definitions/example.Tokens" }, "user": { "$ref": "#/definitions/example.User" } } }, "example.LogoutResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Logout successfully" }, "status": { "type": "string", "example": "success" } } }, "example.NotFound": { "type": "object", "properties": { "code": { "type": "integer", "example": 404 }, "message": { "type": "string", "example": "Not found" }, "status": { "type": "string", "example": "error" } } }, "example.RefreshToken": { "type": "object", "properties": { "refresh_token": { "type": "string", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg" } } }, "example.RefreshTokenResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "status": { "type": "string", "example": "success" }, "tokens": { "$ref": "#/definitions/example.Tokens" } } }, "example.RegisterResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 201 }, "message": { "type": "string", "example": "Register successfully" }, "status": { "type": "string", "example": "success" }, "tokens": { "$ref": "#/definitions/example.Tokens" }, "user": { "$ref": "#/definitions/example.User" } } }, "example.ResetPasswordResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Update password successfully" }, "status": { "type": "string", "example": "success" } } }, "example.SendVerificationEmailResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Please check your email for a link to verify your account" }, "status": { "type": "string", "example": "success" } } }, "example.TokenExpires": { "type": "object", "properties": { "expires": { "type": "string", "example": "2024-10-07T11:56:46.618180553Z" }, "token": { "type": "string", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg" } } }, "example.Tokens": { "type": "object", "properties": { "access": { "$ref": "#/definitions/example.TokenExpires" }, "refresh": { "$ref": "#/definitions/example.TokenExpires" } } }, "example.Unauthorized": { "type": "object", "properties": { "code": { "type": "integer", "example": 401 }, "message": { "type": "string", "example": "Please authenticate" }, "status": { "type": "string", "example": "error" } } }, "example.UpdateUserResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Update user successfully" }, "status": { "type": "string", "example": "success" }, "user": { "$ref": "#/definitions/example.User" } } }, "example.User": { "type": "object", "properties": { "email": { "type": "string", "example": "fake@example.com" }, "id": { "type": "string", "example": "e088d183-9eea-4a11-8d5d-74d7ec91bdf5" }, "name": { "type": "string", "example": "fake name" }, "role": { "type": "string", "example": "user" }, "verified_email": { "type": "boolean", "example": false } } }, "example.VerifyEmailResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Verify email successfully" }, "status": { "type": "string", "example": "success" } } }, "validation.CreateUser": { "type": "object", "required": [ "email", "name", "password", "role" ], "properties": { "email": { "type": "string", "maxLength": 50, "example": "fake@example.com" }, "name": { "type": "string", "maxLength": 50, "example": "fake name" }, "password": { "type": "string", "maxLength": 20, "minLength": 8, "example": "password1" }, "role": { "type": "string", "maxLength": 50, "enum": [ "user", "admin" ], "example": "user" } } }, "validation.ForgotPassword": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "maxLength": 50, "example": "fake@example.com" } } }, "validation.Login": { "type": "object", "required": [ "email", "password" ], "properties": { "email": { "type": "string", "maxLength": 50, "example": "fake@example.com" }, "password": { "type": "string", "maxLength": 20, "minLength": 8, "example": "password1" } } }, "validation.Register": { "type": "object", "required": [ "email", "name", "password" ], "properties": { "email": { "type": "string", "maxLength": 50, "example": "fake@example.com" }, "name": { "type": "string", "maxLength": 50, "example": "fake name" }, "password": { "type": "string", "maxLength": 20, "minLength": 8, "example": "password1" } } }, "validation.UpdatePassOrVerify": { "type": "object", "properties": { "password": { "type": "string", "maxLength": 20, "minLength": 8, "example": "password1" } } }, "validation.UpdateUser": { "type": "object", "properties": { "email": { "type": "string", "maxLength": 50, "example": "fake@example.com" }, "name": { "type": "string", "maxLength": 50, "example": "fake name" }, "password": { "type": "string", "maxLength": 20, "minLength": 8, "example": "password1" } } } }, "securityDefinitions": { "BearerAuth": { "description": "Example Value: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "type": "apiKey", "name": "Authorization", "in": "header" } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.3.1", Host: "localhost:3000", BasePath: "/v1", Schemes: []string{}, Title: "go-fiber-boilerplate API documentation", Description: "", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", RightDelim: "}}", } func init() { swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) } ================================================ FILE: src/docs/swagger.json ================================================ { "swagger": "2.0", "info": { "title": "go-fiber-boilerplate API documentation", "contact": {}, "license": { "name": "MIT", "url": "https://github.com/indrayyana/go-fiber-boilerplate/blob/main/LICENSE" }, "version": "1.3.1" }, "host": "localhost:3000", "basePath": "/v1", "paths": { "/auth/forgot-password": { "post": { "description": "An email will be sent to reset password.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Forgot password", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.ForgotPassword" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.ForgotPasswordResponse" } }, "404": { "description": "Not found", "schema": { "$ref": "#/definitions/example.NotFound" } } } } }, "/auth/google": { "get": { "description": "This route initiates the Google OAuth2 login flow. Please try this in your browser.", "tags": [ "Auth" ], "summary": "Login with google", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.GoogleLoginResponse" } } } } }, "/auth/login": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Login", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.Login" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.LoginResponse" } }, "401": { "description": "Invalid email or password", "schema": { "$ref": "#/definitions/example.FailedLogin" } } } } }, "/auth/logout": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Logout", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.RefreshToken" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.LogoutResponse" } }, "404": { "description": "Not found", "schema": { "$ref": "#/definitions/example.NotFound" } } } } }, "/auth/refresh-tokens": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Refresh auth tokens", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/example.RefreshToken" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.RefreshTokenResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } } } } }, "/auth/register": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Register as user", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.Register" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/example.RegisterResponse" } }, "409": { "description": "Email already taken", "schema": { "$ref": "#/definitions/example.DuplicateEmail" } } } } }, "/auth/reset-password": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Reset password", "parameters": [ { "type": "string", "description": "The reset password token", "name": "token", "in": "query", "required": true }, { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.UpdatePassOrVerify" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.ResetPasswordResponse" } }, "401": { "description": "Password reset failed", "schema": { "$ref": "#/definitions/example.FailedResetPassword" } } } } }, "/auth/send-verification-email": { "post": { "security": [ { "BearerAuth": [] } ], "description": "An email will be sent to verify email.", "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Send verification email", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.SendVerificationEmailResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } } } } }, "/auth/verify-email": { "post": { "produces": [ "application/json" ], "tags": [ "Auth" ], "summary": "Verify email", "parameters": [ { "type": "string", "description": "The verify email token", "name": "token", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.VerifyEmailResponse" } }, "401": { "description": "Verify email failed", "schema": { "$ref": "#/definitions/example.FailedVerifyEmail" } } } } }, "/health-check": { "get": { "description": "Check the status of services and database connections", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Health" ], "summary": "Health Check", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.HealthCheckResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/example.HealthCheckResponseError" } } } } }, "/users": { "get": { "security": [ { "BearerAuth": [] } ], "description": "Only admins can retrieve all users.", "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Get all users", "parameters": [ { "type": "integer", "default": 1, "description": "Page number", "name": "page", "in": "query" }, { "type": "integer", "default": 10, "description": "Maximum number of users", "name": "limit", "in": "query" }, { "type": "string", "description": "Search by name or email or role", "name": "search", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.GetAllUserResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/example.Forbidden" } } } }, "post": { "security": [ { "BearerAuth": [] } ], "description": "Only admins can create other users.", "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Create a user", "parameters": [ { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.CreateUser" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/example.CreateUserResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/example.Forbidden" } }, "409": { "description": "Email already taken", "schema": { "$ref": "#/definitions/example.DuplicateEmail" } } } } }, "/users/{id}": { "get": { "security": [ { "BearerAuth": [] } ], "description": "Logged in users can fetch only their own user information. Only admins can fetch other users.", "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Get a user", "parameters": [ { "type": "string", "description": "User id", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.GetUserResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/example.Forbidden" } }, "404": { "description": "Not found", "schema": { "$ref": "#/definitions/example.NotFound" } } } }, "delete": { "security": [ { "BearerAuth": [] } ], "description": "Logged in users can delete only themselves. Only admins can delete other users.", "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Delete a user", "parameters": [ { "type": "string", "description": "User id", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.DeleteUserResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/example.Forbidden" } }, "404": { "description": "Not found", "schema": { "$ref": "#/definitions/example.NotFound" } } } }, "patch": { "security": [ { "BearerAuth": [] } ], "description": "Logged in users can only update their own information. Only admins can update other users.", "produces": [ "application/json" ], "tags": [ "Users" ], "summary": "Update a user", "parameters": [ { "type": "string", "description": "User id", "name": "id", "in": "path", "required": true }, { "description": "Request body", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/validation.UpdateUser" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/example.UpdateUserResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/example.Unauthorized" } }, "403": { "description": "Forbidden", "schema": { "$ref": "#/definitions/example.Forbidden" } }, "404": { "description": "Not found", "schema": { "$ref": "#/definitions/example.NotFound" } }, "409": { "description": "Email already taken", "schema": { "$ref": "#/definitions/example.DuplicateEmail" } } } } } }, "definitions": { "example.CreateUserResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 201 }, "message": { "type": "string", "example": "Create user successfully" }, "status": { "type": "string", "example": "success" }, "user": { "$ref": "#/definitions/example.User" } } }, "example.DeleteUserResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Delete user successfully" }, "status": { "type": "string", "example": "success" } } }, "example.DuplicateEmail": { "type": "object", "properties": { "code": { "type": "integer", "example": 409 }, "message": { "type": "string", "example": "Email already taken" }, "status": { "type": "string", "example": "error" } } }, "example.FailedLogin": { "type": "object", "properties": { "code": { "type": "integer", "example": 401 }, "message": { "type": "string", "example": "Invalid email or password" }, "status": { "type": "string", "example": "error" } } }, "example.FailedResetPassword": { "type": "object", "properties": { "code": { "type": "integer", "example": 401 }, "message": { "type": "string", "example": "Password reset failed" }, "status": { "type": "string", "example": "error" } } }, "example.FailedVerifyEmail": { "type": "object", "properties": { "code": { "type": "integer", "example": 401 }, "message": { "type": "string", "example": "Verify email failed" }, "status": { "type": "string", "example": "error" } } }, "example.Forbidden": { "type": "object", "properties": { "code": { "type": "integer", "example": 403 }, "message": { "type": "string", "example": "You don't have permission to access this resource" }, "status": { "type": "string", "example": "error" } } }, "example.ForgotPasswordResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "A password reset link has been sent to your email address." }, "status": { "type": "string", "example": "success" } } }, "example.GetAllUserResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "limit": { "type": "integer", "example": 10 }, "message": { "type": "string", "example": "Get all users successfully" }, "page": { "type": "integer", "example": 1 }, "results": { "type": "array", "items": { "$ref": "#/definitions/example.User" } }, "status": { "type": "string", "example": "success" }, "total_pages": { "type": "integer", "example": 1 }, "total_results": { "type": "integer", "example": 1 } } }, "example.GetUserResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Get user successfully" }, "status": { "type": "string", "example": "success" }, "user": { "$ref": "#/definitions/example.User" } } }, "example.GoogleLoginResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Login successfully" }, "status": { "type": "string", "example": "success" }, "tokens": { "$ref": "#/definitions/example.Tokens" }, "user": { "$ref": "#/definitions/example.GoogleUser" } } }, "example.GoogleUser": { "type": "object", "properties": { "email": { "type": "string", "example": "fake@example.com" }, "id": { "type": "string", "example": "e088d183-9eea-4a11-8d5d-74d7ec91bdf5" }, "name": { "type": "string", "example": "fake name" }, "role": { "type": "string", "example": "user" }, "verified_email": { "type": "boolean", "example": true } } }, "example.HealthCheck": { "type": "object", "properties": { "is_up": { "type": "boolean", "example": true }, "name": { "type": "string", "example": "Postgre" }, "status": { "type": "string", "example": "Up" } } }, "example.HealthCheckError": { "type": "object", "properties": { "is_up": { "type": "boolean", "example": false }, "message": { "type": "string", "example": "failed to connect to 'host=localhost user=postgres database=wrongdb': server error (FATAL: database \"wrongdb\" does not exist (SQLSTATE 3D000))" }, "name": { "type": "string", "example": "Postgre" }, "status": { "type": "string", "example": "Down" } } }, "example.HealthCheckResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "is_healthy": { "type": "boolean", "example": true }, "message": { "type": "string", "example": "Health check completed" }, "result": { "type": "array", "items": { "$ref": "#/definitions/example.HealthCheck" } }, "status": { "type": "string", "example": "success" } } }, "example.HealthCheckResponseError": { "type": "object", "properties": { "code": { "type": "integer", "example": 500 }, "is_healthy": { "type": "boolean", "example": false }, "message": { "type": "string", "example": "Health check completed" }, "result": { "type": "array", "items": { "$ref": "#/definitions/example.HealthCheckError" } }, "status": { "type": "string", "example": "error" } } }, "example.LoginResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Login successfully" }, "status": { "type": "string", "example": "success" }, "tokens": { "$ref": "#/definitions/example.Tokens" }, "user": { "$ref": "#/definitions/example.User" } } }, "example.LogoutResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Logout successfully" }, "status": { "type": "string", "example": "success" } } }, "example.NotFound": { "type": "object", "properties": { "code": { "type": "integer", "example": 404 }, "message": { "type": "string", "example": "Not found" }, "status": { "type": "string", "example": "error" } } }, "example.RefreshToken": { "type": "object", "properties": { "refresh_token": { "type": "string", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg" } } }, "example.RefreshTokenResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "status": { "type": "string", "example": "success" }, "tokens": { "$ref": "#/definitions/example.Tokens" } } }, "example.RegisterResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 201 }, "message": { "type": "string", "example": "Register successfully" }, "status": { "type": "string", "example": "success" }, "tokens": { "$ref": "#/definitions/example.Tokens" }, "user": { "$ref": "#/definitions/example.User" } } }, "example.ResetPasswordResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Update password successfully" }, "status": { "type": "string", "example": "success" } } }, "example.SendVerificationEmailResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Please check your email for a link to verify your account" }, "status": { "type": "string", "example": "success" } } }, "example.TokenExpires": { "type": "object", "properties": { "expires": { "type": "string", "example": "2024-10-07T11:56:46.618180553Z" }, "token": { "type": "string", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg" } } }, "example.Tokens": { "type": "object", "properties": { "access": { "$ref": "#/definitions/example.TokenExpires" }, "refresh": { "$ref": "#/definitions/example.TokenExpires" } } }, "example.Unauthorized": { "type": "object", "properties": { "code": { "type": "integer", "example": 401 }, "message": { "type": "string", "example": "Please authenticate" }, "status": { "type": "string", "example": "error" } } }, "example.UpdateUserResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Update user successfully" }, "status": { "type": "string", "example": "success" }, "user": { "$ref": "#/definitions/example.User" } } }, "example.User": { "type": "object", "properties": { "email": { "type": "string", "example": "fake@example.com" }, "id": { "type": "string", "example": "e088d183-9eea-4a11-8d5d-74d7ec91bdf5" }, "name": { "type": "string", "example": "fake name" }, "role": { "type": "string", "example": "user" }, "verified_email": { "type": "boolean", "example": false } } }, "example.VerifyEmailResponse": { "type": "object", "properties": { "code": { "type": "integer", "example": 200 }, "message": { "type": "string", "example": "Verify email successfully" }, "status": { "type": "string", "example": "success" } } }, "validation.CreateUser": { "type": "object", "required": [ "email", "name", "password", "role" ], "properties": { "email": { "type": "string", "maxLength": 50, "example": "fake@example.com" }, "name": { "type": "string", "maxLength": 50, "example": "fake name" }, "password": { "type": "string", "maxLength": 20, "minLength": 8, "example": "password1" }, "role": { "type": "string", "maxLength": 50, "enum": [ "user", "admin" ], "example": "user" } } }, "validation.ForgotPassword": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "maxLength": 50, "example": "fake@example.com" } } }, "validation.Login": { "type": "object", "required": [ "email", "password" ], "properties": { "email": { "type": "string", "maxLength": 50, "example": "fake@example.com" }, "password": { "type": "string", "maxLength": 20, "minLength": 8, "example": "password1" } } }, "validation.Register": { "type": "object", "required": [ "email", "name", "password" ], "properties": { "email": { "type": "string", "maxLength": 50, "example": "fake@example.com" }, "name": { "type": "string", "maxLength": 50, "example": "fake name" }, "password": { "type": "string", "maxLength": 20, "minLength": 8, "example": "password1" } } }, "validation.UpdatePassOrVerify": { "type": "object", "properties": { "password": { "type": "string", "maxLength": 20, "minLength": 8, "example": "password1" } } }, "validation.UpdateUser": { "type": "object", "properties": { "email": { "type": "string", "maxLength": 50, "example": "fake@example.com" }, "name": { "type": "string", "maxLength": 50, "example": "fake name" }, "password": { "type": "string", "maxLength": 20, "minLength": 8, "example": "password1" } } } }, "securityDefinitions": { "BearerAuth": { "description": "Example Value: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "type": "apiKey", "name": "Authorization", "in": "header" } } } ================================================ FILE: src/docs/swagger.yaml ================================================ basePath: /v1 definitions: example.CreateUserResponse: properties: code: example: 201 type: integer message: example: Create user successfully type: string status: example: success type: string user: $ref: '#/definitions/example.User' type: object example.DeleteUserResponse: properties: code: example: 200 type: integer message: example: Delete user successfully type: string status: example: success type: string type: object example.DuplicateEmail: properties: code: example: 409 type: integer message: example: Email already taken type: string status: example: error type: string type: object example.FailedLogin: properties: code: example: 401 type: integer message: example: Invalid email or password type: string status: example: error type: string type: object example.FailedResetPassword: properties: code: example: 401 type: integer message: example: Password reset failed type: string status: example: error type: string type: object example.FailedVerifyEmail: properties: code: example: 401 type: integer message: example: Verify email failed type: string status: example: error type: string type: object example.Forbidden: properties: code: example: 403 type: integer message: example: You don't have permission to access this resource type: string status: example: error type: string type: object example.ForgotPasswordResponse: properties: code: example: 200 type: integer message: example: A password reset link has been sent to your email address. type: string status: example: success type: string type: object example.GetAllUserResponse: properties: code: example: 200 type: integer limit: example: 10 type: integer message: example: Get all users successfully type: string page: example: 1 type: integer results: items: $ref: '#/definitions/example.User' type: array status: example: success type: string total_pages: example: 1 type: integer total_results: example: 1 type: integer type: object example.GetUserResponse: properties: code: example: 200 type: integer message: example: Get user successfully type: string status: example: success type: string user: $ref: '#/definitions/example.User' type: object example.GoogleLoginResponse: properties: code: example: 200 type: integer message: example: Login successfully type: string status: example: success type: string tokens: $ref: '#/definitions/example.Tokens' user: $ref: '#/definitions/example.GoogleUser' type: object example.GoogleUser: properties: email: example: fake@example.com type: string id: example: e088d183-9eea-4a11-8d5d-74d7ec91bdf5 type: string name: example: fake name type: string role: example: user type: string verified_email: example: true type: boolean type: object example.HealthCheck: properties: is_up: example: true type: boolean name: example: Postgre type: string status: example: Up type: string type: object example.HealthCheckError: properties: is_up: example: false type: boolean message: example: 'failed to connect to ''host=localhost user=postgres database=wrongdb'': server error (FATAL: database "wrongdb" does not exist (SQLSTATE 3D000))' type: string name: example: Postgre type: string status: example: Down type: string type: object example.HealthCheckResponse: properties: code: example: 200 type: integer is_healthy: example: true type: boolean message: example: Health check completed type: string result: items: $ref: '#/definitions/example.HealthCheck' type: array status: example: success type: string type: object example.HealthCheckResponseError: properties: code: example: 500 type: integer is_healthy: example: false type: boolean message: example: Health check completed type: string result: items: $ref: '#/definitions/example.HealthCheckError' type: array status: example: error type: string type: object example.LoginResponse: properties: code: example: 200 type: integer message: example: Login successfully type: string status: example: success type: string tokens: $ref: '#/definitions/example.Tokens' user: $ref: '#/definitions/example.User' type: object example.LogoutResponse: properties: code: example: 200 type: integer message: example: Logout successfully type: string status: example: success type: string type: object example.NotFound: properties: code: example: 404 type: integer message: example: Not found type: string status: example: error type: string type: object example.RefreshToken: properties: refresh_token: example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg type: string type: object example.RefreshTokenResponse: properties: code: example: 200 type: integer status: example: success type: string tokens: $ref: '#/definitions/example.Tokens' type: object example.RegisterResponse: properties: code: example: 201 type: integer message: example: Register successfully type: string status: example: success type: string tokens: $ref: '#/definitions/example.Tokens' user: $ref: '#/definitions/example.User' type: object example.ResetPasswordResponse: properties: code: example: 200 type: integer message: example: Update password successfully type: string status: example: success type: string type: object example.SendVerificationEmailResponse: properties: code: example: 200 type: integer message: example: Please check your email for a link to verify your account type: string status: example: success type: string type: object example.TokenExpires: properties: expires: example: "2024-10-07T11:56:46.618180553Z" type: string token: example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg type: string type: object example.Tokens: properties: access: $ref: '#/definitions/example.TokenExpires' refresh: $ref: '#/definitions/example.TokenExpires' type: object example.Unauthorized: properties: code: example: 401 type: integer message: example: Please authenticate type: string status: example: error type: string type: object example.UpdateUserResponse: properties: code: example: 200 type: integer message: example: Update user successfully type: string status: example: success type: string user: $ref: '#/definitions/example.User' type: object example.User: properties: email: example: fake@example.com type: string id: example: e088d183-9eea-4a11-8d5d-74d7ec91bdf5 type: string name: example: fake name type: string role: example: user type: string verified_email: example: false type: boolean type: object example.VerifyEmailResponse: properties: code: example: 200 type: integer message: example: Verify email successfully type: string status: example: success type: string type: object validation.CreateUser: properties: email: example: fake@example.com maxLength: 50 type: string name: example: fake name maxLength: 50 type: string password: example: password1 maxLength: 20 minLength: 8 type: string role: enum: - user - admin example: user maxLength: 50 type: string required: - email - name - password - role type: object validation.ForgotPassword: properties: email: example: fake@example.com maxLength: 50 type: string required: - email type: object validation.Login: properties: email: example: fake@example.com maxLength: 50 type: string password: example: password1 maxLength: 20 minLength: 8 type: string required: - email - password type: object validation.Register: properties: email: example: fake@example.com maxLength: 50 type: string name: example: fake name maxLength: 50 type: string password: example: password1 maxLength: 20 minLength: 8 type: string required: - email - name - password type: object validation.UpdatePassOrVerify: properties: password: example: password1 maxLength: 20 minLength: 8 type: string type: object validation.UpdateUser: properties: email: example: fake@example.com maxLength: 50 type: string name: example: fake name maxLength: 50 type: string password: example: password1 maxLength: 20 minLength: 8 type: string type: object host: localhost:3000 info: contact: {} license: name: MIT url: https://github.com/indrayyana/go-fiber-boilerplate/blob/main/LICENSE title: go-fiber-boilerplate API documentation version: 1.3.1 paths: /auth/forgot-password: post: consumes: - application/json description: An email will be sent to reset password. parameters: - description: Request body in: body name: request required: true schema: $ref: '#/definitions/validation.ForgotPassword' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.ForgotPasswordResponse' "404": description: Not found schema: $ref: '#/definitions/example.NotFound' summary: Forgot password tags: - Auth /auth/google: get: description: This route initiates the Google OAuth2 login flow. Please try this in your browser. responses: "200": description: OK schema: $ref: '#/definitions/example.GoogleLoginResponse' summary: Login with google tags: - Auth /auth/login: post: consumes: - application/json parameters: - description: Request body in: body name: request required: true schema: $ref: '#/definitions/validation.Login' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.LoginResponse' "401": description: Invalid email or password schema: $ref: '#/definitions/example.FailedLogin' summary: Login tags: - Auth /auth/logout: post: consumes: - application/json parameters: - description: Request body in: body name: request required: true schema: $ref: '#/definitions/example.RefreshToken' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.LogoutResponse' "404": description: Not found schema: $ref: '#/definitions/example.NotFound' summary: Logout tags: - Auth /auth/refresh-tokens: post: consumes: - application/json parameters: - description: Request body in: body name: request required: true schema: $ref: '#/definitions/example.RefreshToken' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.RefreshTokenResponse' "401": description: Unauthorized schema: $ref: '#/definitions/example.Unauthorized' summary: Refresh auth tokens tags: - Auth /auth/register: post: consumes: - application/json parameters: - description: Request body in: body name: request required: true schema: $ref: '#/definitions/validation.Register' produces: - application/json responses: "201": description: Created schema: $ref: '#/definitions/example.RegisterResponse' "409": description: Email already taken schema: $ref: '#/definitions/example.DuplicateEmail' summary: Register as user tags: - Auth /auth/reset-password: post: consumes: - application/json parameters: - description: The reset password token in: query name: token required: true type: string - description: Request body in: body name: request required: true schema: $ref: '#/definitions/validation.UpdatePassOrVerify' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.ResetPasswordResponse' "401": description: Password reset failed schema: $ref: '#/definitions/example.FailedResetPassword' summary: Reset password tags: - Auth /auth/send-verification-email: post: description: An email will be sent to verify email. produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.SendVerificationEmailResponse' "401": description: Unauthorized schema: $ref: '#/definitions/example.Unauthorized' security: - BearerAuth: [] summary: Send verification email tags: - Auth /auth/verify-email: post: parameters: - description: The verify email token in: query name: token required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.VerifyEmailResponse' "401": description: Verify email failed schema: $ref: '#/definitions/example.FailedVerifyEmail' summary: Verify email tags: - Auth /health-check: get: consumes: - application/json description: Check the status of services and database connections produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.HealthCheckResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/example.HealthCheckResponseError' summary: Health Check tags: - Health /users: get: description: Only admins can retrieve all users. parameters: - default: 1 description: Page number in: query name: page type: integer - default: 10 description: Maximum number of users in: query name: limit type: integer - description: Search by name or email or role in: query name: search type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.GetAllUserResponse' "401": description: Unauthorized schema: $ref: '#/definitions/example.Unauthorized' "403": description: Forbidden schema: $ref: '#/definitions/example.Forbidden' security: - BearerAuth: [] summary: Get all users tags: - Users post: description: Only admins can create other users. parameters: - description: Request body in: body name: request required: true schema: $ref: '#/definitions/validation.CreateUser' produces: - application/json responses: "201": description: Created schema: $ref: '#/definitions/example.CreateUserResponse' "401": description: Unauthorized schema: $ref: '#/definitions/example.Unauthorized' "403": description: Forbidden schema: $ref: '#/definitions/example.Forbidden' "409": description: Email already taken schema: $ref: '#/definitions/example.DuplicateEmail' security: - BearerAuth: [] summary: Create a user tags: - Users /users/{id}: delete: description: Logged in users can delete only themselves. Only admins can delete other users. parameters: - description: User id in: path name: id required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.DeleteUserResponse' "401": description: Unauthorized schema: $ref: '#/definitions/example.Unauthorized' "403": description: Forbidden schema: $ref: '#/definitions/example.Forbidden' "404": description: Not found schema: $ref: '#/definitions/example.NotFound' security: - BearerAuth: [] summary: Delete a user tags: - Users get: description: Logged in users can fetch only their own user information. Only admins can fetch other users. parameters: - description: User id in: path name: id required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.GetUserResponse' "401": description: Unauthorized schema: $ref: '#/definitions/example.Unauthorized' "403": description: Forbidden schema: $ref: '#/definitions/example.Forbidden' "404": description: Not found schema: $ref: '#/definitions/example.NotFound' security: - BearerAuth: [] summary: Get a user tags: - Users patch: description: Logged in users can only update their own information. Only admins can update other users. parameters: - description: User id in: path name: id required: true type: string - description: Request body in: body name: request required: true schema: $ref: '#/definitions/validation.UpdateUser' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/example.UpdateUserResponse' "401": description: Unauthorized schema: $ref: '#/definitions/example.Unauthorized' "403": description: Forbidden schema: $ref: '#/definitions/example.Forbidden' "404": description: Not found schema: $ref: '#/definitions/example.NotFound' "409": description: Email already taken schema: $ref: '#/definitions/example.DuplicateEmail' security: - BearerAuth: [] summary: Update a user tags: - Users securityDefinitions: BearerAuth: description: 'Example Value: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' in: header name: Authorization type: apiKey swagger: "2.0" ================================================ FILE: src/main.go ================================================ package main import ( "app/src/config" "app/src/database" "app/src/middleware" "app/src/router" "app/src/utils" "context" "fmt" "os" "os/signal" "syscall" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/compress" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/helmet" "gorm.io/gorm" ) // @title go-fiber-boilerplate API documentation // @version 1.3.1 // @license.name MIT // @license.url https://github.com/indrayyana/go-fiber-boilerplate/blob/main/LICENSE // @host localhost:3000 // @BasePath /v1 // @securityDefinitions.apikey BearerAuth // @in header // @name Authorization // @description Example Value: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() app := setupFiberApp() db := setupDatabase() defer closeDatabase(db) setupRoutes(app, db) address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort) // Start server and handle graceful shutdown serverErrors := make(chan error, 1) go startServer(app, address, serverErrors) handleGracefulShutdown(ctx, app, serverErrors) } func setupFiberApp() *fiber.App { app := fiber.New(config.FiberConfig()) // Middleware setup app.Use("/v1/auth", middleware.LimiterConfig()) app.Use(middleware.LoggerConfig()) app.Use(helmet.New()) app.Use(compress.New()) app.Use(cors.New()) app.Use(middleware.RecoverConfig()) return app } func setupDatabase() *gorm.DB { db := database.Connect(config.DBHost, config.DBName) // Add any additional database setup if needed return db } func setupRoutes(app *fiber.App, db *gorm.DB) { router.Routes(app, db) app.Use(utils.NotFoundHandler) } func startServer(app *fiber.App, address string, errs chan<- error) { if err := app.Listen(address); err != nil { errs <- fmt.Errorf("error starting server: %w", err) } } func closeDatabase(db *gorm.DB) { sqlDB, errDB := db.DB() if errDB != nil { utils.Log.Errorf("Error getting database instance: %v", errDB) return } if err := sqlDB.Close(); err != nil { utils.Log.Errorf("Error closing database connection: %v", err) } else { utils.Log.Info("Database connection closed successfully") } } func handleGracefulShutdown(ctx context.Context, app *fiber.App, serverErrors <-chan error) { quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) select { case err := <-serverErrors: utils.Log.Fatalf("Server error: %v", err) case <-quit: utils.Log.Info("Shutting down server...") if err := app.Shutdown(); err != nil { utils.Log.Fatalf("Error during server shutdown: %v", err) } case <-ctx.Done(): utils.Log.Info("Server exiting due to context cancellation") } utils.Log.Info("Server exited") } ================================================ FILE: src/middleware/auth.go ================================================ package middleware import ( "app/src/config" "app/src/service" "app/src/utils" "strings" "github.com/gofiber/fiber/v2" ) func Auth(userService service.UserService, requiredRights ...string) fiber.Handler { return func(c *fiber.Ctx) error { authHeader := c.Get("Authorization") token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")) if token == "" { return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") } userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess) if err != nil { return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") } user, err := userService.GetUserByID(c, userID) if err != nil || user == nil { return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") } c.Locals("user", user) if len(requiredRights) > 0 { userRights, hasRights := config.RoleRights[user.Role] if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID { return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource") } } return c.Next() } } func hasAllRights(userRights, requiredRights []string) bool { rightSet := make(map[string]struct{}, len(userRights)) for _, right := range userRights { rightSet[right] = struct{}{} } for _, right := range requiredRights { if _, exists := rightSet[right]; !exists { return false } } return true } ================================================ FILE: src/middleware/jwt.go ================================================ package middleware import ( jwtware "github.com/gofiber/contrib/jwt" "github.com/gofiber/fiber/v2" ) func JwtConfig() fiber.Handler { return jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte("secret")}, }) } ================================================ FILE: src/middleware/limiter.go ================================================ package middleware import ( "app/src/response" "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/limiter" ) func LimiterConfig() fiber.Handler { return limiter.New(limiter.Config{ Max: 20, Expiration: 15 * time.Minute, LimitReached: func(c *fiber.Ctx) error { return c.Status(fiber.StatusTooManyRequests). JSON(response.Common{ Code: fiber.StatusTooManyRequests, Status: "error", Message: "Too many requests, please try again later", }) }, SkipSuccessfulRequests: true, }) } ================================================ FILE: src/middleware/logger.go ================================================ package middleware import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/logger" ) func LoggerConfig() fiber.Handler { return logger.New(logger.Config{ Format: "${time} ${method} ${status} ${path} in ${latency}\n", TimeFormat: "15:04:05.00", }) } ================================================ FILE: src/middleware/recover.go ================================================ package middleware import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/recover" ) func RecoverConfig() fiber.Handler { return recover.New(recover.Config{ EnableStackTrace: true, }) } ================================================ FILE: src/model/token_model.go ================================================ package model import ( "time" "github.com/google/uuid" "gorm.io/gorm" ) type Token struct { ID uuid.UUID `gorm:"primaryKey;not null"` Token string `gorm:"not null"` UserID uuid.UUID `gorm:"not null"` Type string `gorm:"not null"` Expires time.Time `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime:milli"` UpdatedAt time.Time `gorm:"autoCreateTime:milli;autoUpdateTime:milli"` User *User `gorm:"foreignKey:user_id;references:id"` } func (token *Token) BeforeCreate(_ *gorm.DB) error { token.ID = uuid.New() return nil } ================================================ FILE: src/model/user_model.go ================================================ package model import ( "time" "github.com/google/uuid" "gorm.io/gorm" ) type User struct { ID uuid.UUID `gorm:"primaryKey;not null" json:"id"` Name string `gorm:"not null" json:"name"` Email string `gorm:"uniqueIndex;not null" json:"email"` Password string `gorm:"not null" json:"-"` Role string `gorm:"default:user;not null" json:"role"` VerifiedEmail bool `gorm:"default:false;not null" json:"verified_email"` CreatedAt time.Time `gorm:"autoCreateTime:milli" json:"-"` UpdatedAt time.Time `gorm:"autoCreateTime:milli;autoUpdateTime:milli" json:"-"` Token []Token `gorm:"foreignKey:user_id;references:id" json:"-"` } func (user *User) BeforeCreate(_ *gorm.DB) error { user.ID = uuid.New() // Generate UUID before create return nil } ================================================ FILE: src/response/auth_response.go ================================================ package response import "time" type Tokens struct { Access TokenExpires `json:"access"` Refresh TokenExpires `json:"refresh"` } type TokenExpires struct { Token string `json:"token"` Expires time.Time `json:"expires"` } type RefreshToken struct { Code int `json:"code"` Status string `json:"status"` Tokens Tokens `json:"tokens"` } ================================================ FILE: src/response/error_response.go ================================================ package response import ( "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" ) func Error(c *fiber.Ctx, statusCode int, message string, details interface{}) error { var errRes error if details != nil { errRes = c.Status(statusCode).JSON(ErrorDetails{ Code: statusCode, Status: "error", Message: message, Errors: details, }) } else { errRes = c.Status(statusCode).JSON(Common{ Code: statusCode, Status: "error", Message: message, }) } if errRes != nil { logrus.Errorf("Failed to send error response : %+v", errRes) } return errRes } ================================================ FILE: src/response/example/error_example.go ================================================ package example type Unauthorized struct { Code int `json:"code" example:"401"` Status string `json:"status" example:"error"` Message string `json:"message" example:"Please authenticate"` } type FailedLogin struct { Code int `json:"code" example:"401"` Status string `json:"status" example:"error"` Message string `json:"message" example:"Invalid email or password"` } type FailedResetPassword struct { Code int `json:"code" example:"401"` Status string `json:"status" example:"error"` Message string `json:"message" example:"Password reset failed"` } type FailedVerifyEmail struct { Code int `json:"code" example:"401"` Status string `json:"status" example:"error"` Message string `json:"message" example:"Verify email failed"` } type Forbidden struct { Code int `json:"code" example:"403"` Status string `json:"status" example:"error"` Message string `json:"message" example:"You don't have permission to access this resource"` } type NotFound struct { Code int `json:"code" example:"404"` Status string `json:"status" example:"error"` Message string `json:"message" example:"Not found"` } type DuplicateEmail struct { Code int `json:"code" example:"409"` Status string `json:"status" example:"error"` Message string `json:"message" example:"Email already taken"` } ================================================ FILE: src/response/example/example.go ================================================ package example type RegisterResponse struct { Code int `json:"code" example:"201"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Register successfully"` User User `json:"user"` Tokens Tokens `json:"tokens"` } type LoginResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Login successfully"` User User `json:"user"` Tokens Tokens `json:"tokens"` } type GoogleLoginResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Login successfully"` User GoogleUser `json:"user"` Tokens Tokens `json:"tokens"` } type LogoutResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Logout successfully"` } type RefreshTokenResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Tokens Tokens `json:"tokens"` } type ForgotPasswordResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"A password reset link has been sent to your email address."` } type ResetPasswordResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Update password successfully"` } type SendVerificationEmailResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Please check your email for a link to verify your account"` } type VerifyEmailResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Verify email successfully"` } type GetAllUserResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Get all users successfully"` Results []User `json:"results"` Page int `json:"page" example:"1"` Limit int `json:"limit" example:"10"` TotalPages int64 `json:"total_pages" example:"1"` TotalResults int64 `json:"total_results" example:"1"` } type GetUserResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Get user successfully"` User User `json:"user"` } type CreateUserResponse struct { Code int `json:"code" example:"201"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Create user successfully"` User User `json:"user"` } type UpdateUserResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Update user successfully"` User User `json:"user"` } type DeleteUserResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Delete user successfully"` } ================================================ FILE: src/response/example/health_check_example.go ================================================ package example type HealthCheck struct { Name string `json:"name" example:"Postgre"` Status string `json:"status" example:"Up"` IsUp bool `json:"is_up" example:"true"` } type HealthCheckResponse struct { Code int `json:"code" example:"200"` Status string `json:"status" example:"success"` Message string `json:"message" example:"Health check completed"` IsHealthy bool `json:"is_healthy" example:"true"` Result []HealthCheck `json:"result"` } type HealthCheckError struct { Name string `json:"name" example:"Postgre"` Status string `json:"status" example:"Down"` IsUp bool `json:"is_up" example:"false"` Message *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))"` } type HealthCheckResponseError struct { Code int `json:"code" example:"500"` Status string `json:"status" example:"error"` Message string `json:"message" example:"Health check completed"` IsHealthy bool `json:"is_healthy" example:"false"` Result []HealthCheckError `json:"result"` } ================================================ FILE: src/response/example/token_example.go ================================================ package example import "time" type Tokens struct { Access TokenExpires `json:"access"` Refresh TokenExpires `json:"refresh"` } type TokenExpires struct { Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg"` Expires time.Time `json:"expires" example:"2024-10-07T11:56:46.618180553Z"` } type RefreshToken struct { RefreshToken string `json:"refresh_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZWJhYzUzNDk1NGI1NDEzOTgwNmMxMTIiLCJpYXQiOjE1ODkyOTg0ODQsImV4cCI6MTU4OTMwMDI4NH0.m1U63blB0MLej_WfB7yC2FTMnCziif9X8yzwDEfJXAg"` } ================================================ FILE: src/response/example/user_example.go ================================================ package example import "github.com/google/uuid" type User struct { ID uuid.UUID `json:"id" example:"e088d183-9eea-4a11-8d5d-74d7ec91bdf5"` Name string `json:"name" example:"fake name"` Email string `json:"email" example:"fake@example.com"` Role string `json:"role" example:"user"` VerifiedEmail bool `json:"verified_email" example:"false"` } type GoogleUser struct { ID uuid.UUID `json:"id" example:"e088d183-9eea-4a11-8d5d-74d7ec91bdf5"` Name string `json:"name" example:"fake name"` Email string `json:"email" example:"fake@example.com"` Role string `json:"role" example:"user"` VerifiedEmail bool `json:"verified_email" example:"true"` } ================================================ FILE: src/response/health_check_response.go ================================================ package response type HealthCheck struct { Name string `json:"name"` Status string `json:"status"` IsUp bool `json:"is_up"` Message *string `json:"message,omitempty"` } type HealthCheckResponse struct { Code int `json:"code"` Status string `json:"status"` Message string `json:"message"` IsHealthy bool `json:"is_healthy"` Result []HealthCheck `json:"result"` } ================================================ FILE: src/response/response.go ================================================ package response import "app/src/model" type Common struct { Code int `json:"code"` Status string `json:"status"` Message string `json:"message"` } type SuccessWithUser struct { Code int `json:"code"` Status string `json:"status"` Message string `json:"message"` User model.User `json:"user"` } type SuccessWithTokens struct { Code int `json:"code"` Status string `json:"status"` Message string `json:"message"` User model.User `json:"user"` Tokens Tokens `json:"tokens"` } type SuccessWithPaginate[T any] struct { Code int `json:"code"` Status string `json:"status"` Message string `json:"message"` Results []T `json:"results"` Page int `json:"page"` Limit int `json:"limit"` TotalPages int64 `json:"total_pages"` TotalResults int64 `json:"total_results"` } type ErrorDetails struct { Code int `json:"code"` Status string `json:"status"` Message string `json:"message"` Errors interface{} `json:"errors"` } ================================================ FILE: src/response/user_response.go ================================================ package response import "github.com/google/uuid" type CreateUser struct { Name string `json:"name"` Email string `json:"email"` Role string `json:"role"` IsEmailVerified bool `json:"is_email_verified"` } type GetUsers struct { ID uuid.UUID `json:"id"` Name string `json:"name"` Email string `json:"email"` Role string `json:"role"` IsEmailVerified bool `json:"is_email_verified"` } ================================================ FILE: src/router/auth_route.go ================================================ package router import ( "app/src/config" "app/src/controller" m "app/src/middleware" "app/src/service" "github.com/gofiber/fiber/v2" ) func AuthRoutes( v1 fiber.Router, a service.AuthService, u service.UserService, t service.TokenService, e service.EmailService, ) { authController := controller.NewAuthController(a, u, t, e) config.GoogleConfig() auth := v1.Group("/auth") auth.Post("/register", authController.Register) auth.Post("/login", authController.Login) auth.Post("/logout", authController.Logout) auth.Post("/refresh-tokens", authController.RefreshTokens) auth.Post("/forgot-password", authController.ForgotPassword) auth.Post("/reset-password", authController.ResetPassword) auth.Post("/send-verification-email", m.Auth(u), authController.SendVerificationEmail) auth.Post("/verify-email", authController.VerifyEmail) auth.Get("/google", authController.GoogleLogin) auth.Get("/google-callback", authController.GoogleCallback) } ================================================ FILE: src/router/docs_route.go ================================================ package router import ( // initialize the Swagger documentation _ "app/src/docs" "github.com/gofiber/fiber/v2" "github.com/gofiber/swagger" ) func DocsRoutes(v1 fiber.Router) { docs := v1.Group("/docs") docs.Get("/*", swagger.HandlerDefault) } ================================================ FILE: src/router/health_check_route.go ================================================ package router import ( "app/src/controller" "app/src/service" "github.com/gofiber/fiber/v2" ) func HealthCheckRoutes(v1 fiber.Router, h service.HealthCheckService) { healthCheckController := controller.NewHealthCheckController(h) healthCheck := v1.Group("/health-check") healthCheck.Get("/", healthCheckController.Check) } ================================================ FILE: src/router/router.go ================================================ package router import ( "app/src/config" "app/src/service" "app/src/validation" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) func Routes(app *fiber.App, db *gorm.DB) { validate := validation.Validator() healthCheckService := service.NewHealthCheckService(db) emailService := service.NewEmailService() userService := service.NewUserService(db, validate) tokenService := service.NewTokenService(db, validate, userService) authService := service.NewAuthService(db, validate, userService, tokenService) v1 := app.Group("/v1") HealthCheckRoutes(v1, healthCheckService) AuthRoutes(v1, authService, userService, tokenService, emailService) UserRoutes(v1, userService, tokenService) // TODO: add another routes here... if !config.IsProd { DocsRoutes(v1) } } ================================================ FILE: src/router/user_route.go ================================================ package router import ( "app/src/controller" m "app/src/middleware" "app/src/service" "github.com/gofiber/fiber/v2" ) func UserRoutes(v1 fiber.Router, u service.UserService, t service.TokenService) { userController := controller.NewUserController(u, t) user := v1.Group("/users") user.Get("/", m.Auth(u, "getUsers"), userController.GetUsers) user.Post("/", m.Auth(u, "manageUsers"), userController.CreateUser) user.Get("/:userId", m.Auth(u, "getUsers"), userController.GetUserByID) user.Patch("/:userId", m.Auth(u, "manageUsers"), userController.UpdateUser) user.Delete("/:userId", m.Auth(u, "manageUsers"), userController.DeleteUser) } ================================================ FILE: src/service/auth_service.go ================================================ package service import ( "app/src/config" "app/src/model" "app/src/response" "app/src/utils" "app/src/validation" "errors" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type AuthService interface { Register(c *fiber.Ctx, req *validation.Register) (*model.User, error) Login(c *fiber.Ctx, req *validation.Login) (*model.User, error) Logout(c *fiber.Ctx, req *validation.Logout) error RefreshAuth(c *fiber.Ctx, req *validation.RefreshToken) (*response.Tokens, error) ResetPassword(c *fiber.Ctx, query *validation.Token, req *validation.UpdatePassOrVerify) error VerifyEmail(c *fiber.Ctx, query *validation.Token) error } type authService struct { Log *logrus.Logger DB *gorm.DB Validate *validator.Validate UserService UserService TokenService TokenService } func NewAuthService( db *gorm.DB, validate *validator.Validate, userService UserService, tokenService TokenService, ) AuthService { return &authService{ Log: utils.Log, DB: db, Validate: validate, UserService: userService, TokenService: tokenService, } } func (s *authService) Register(c *fiber.Ctx, req *validation.Register) (*model.User, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } hashedPassword, err := utils.HashPassword(req.Password) if err != nil { s.Log.Errorf("Failed hash password: %+v", err) return nil, err } user := &model.User{ Name: req.Name, Email: req.Email, Password: hashedPassword, } result := s.DB.WithContext(c.Context()).Create(user) if errors.Is(result.Error, gorm.ErrDuplicatedKey) { return nil, fiber.NewError(fiber.StatusConflict, "Email already taken") } if result.Error != nil { s.Log.Errorf("Failed create user: %+v", result.Error) } return user, result.Error } func (s *authService) Login(c *fiber.Ctx, req *validation.Login) (*model.User, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } user, err := s.UserService.GetUserByEmail(c, req.Email) if err != nil { return nil, fiber.NewError(fiber.StatusUnauthorized, "Invalid email or password") } if !utils.CheckPasswordHash(req.Password, user.Password) { return nil, fiber.NewError(fiber.StatusUnauthorized, "Invalid email or password") } return user, nil } func (s *authService) Logout(c *fiber.Ctx, req *validation.Logout) error { if err := s.Validate.Struct(req); err != nil { return err } token, err := s.TokenService.GetTokenByUserID(c, req.RefreshToken) if err != nil { return fiber.NewError(fiber.StatusNotFound, "Token not found") } err = s.TokenService.DeleteToken(c, config.TokenTypeRefresh, token.UserID.String()) return err } func (s *authService) RefreshAuth(c *fiber.Ctx, req *validation.RefreshToken) (*response.Tokens, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } token, err := s.TokenService.GetTokenByUserID(c, req.RefreshToken) if err != nil { return nil, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") } user, err := s.UserService.GetUserByID(c, token.UserID.String()) if err != nil { return nil, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") } newTokens, err := s.TokenService.GenerateAuthTokens(c, user) if err != nil { return nil, fiber.ErrInternalServerError } return newTokens, err } func (s *authService) ResetPassword(c *fiber.Ctx, query *validation.Token, req *validation.UpdatePassOrVerify) error { if err := s.Validate.Struct(query); err != nil { return err } userID, err := utils.VerifyToken(query.Token, config.JWTSecret, config.TokenTypeResetPassword) if err != nil { return fiber.NewError(fiber.StatusUnauthorized, "Invalid Token") } user, err := s.UserService.GetUserByID(c, userID) if err != nil { return fiber.NewError(fiber.StatusUnauthorized, "Password reset failed") } if errUpdate := s.UserService.UpdatePassOrVerify(c, req, user.ID.String()); errUpdate != nil { return errUpdate } if errToken := s.TokenService.DeleteToken(c, config.TokenTypeResetPassword, user.ID.String()); errToken != nil { return errToken } return nil } func (s *authService) VerifyEmail(c *fiber.Ctx, query *validation.Token) error { if err := s.Validate.Struct(query); err != nil { return err } userID, err := utils.VerifyToken(query.Token, config.JWTSecret, config.TokenTypeVerifyEmail) if err != nil { return fiber.NewError(fiber.StatusUnauthorized, "Invalid Token") } user, err := s.UserService.GetUserByID(c, userID) if err != nil { return fiber.NewError(fiber.StatusUnauthorized, "Verify email failed") } if errToken := s.TokenService.DeleteToken(c, config.TokenTypeVerifyEmail, user.ID.String()); errToken != nil { return errToken } updateBody := &validation.UpdatePassOrVerify{ VerifiedEmail: true, } if errUpdate := s.UserService.UpdatePassOrVerify(c, updateBody, user.ID.String()); errUpdate != nil { return errUpdate } return nil } ================================================ FILE: src/service/email_service.go ================================================ package service import ( "app/src/config" "app/src/utils" "fmt" "github.com/sirupsen/logrus" "gopkg.in/gomail.v2" ) type EmailService interface { SendEmail(to, subject, body string) error SendResetPasswordEmail(to, token string) error SendVerificationEmail(to, token string) error } type emailService struct { Log *logrus.Logger Dialer *gomail.Dialer } func NewEmailService() EmailService { return &emailService{ Log: utils.Log, Dialer: gomail.NewDialer( config.SMTPHost, config.SMTPPort, config.SMTPUsername, config.SMTPPassword, ), } } func (s *emailService) SendEmail(to, subject, body string) error { mailer := gomail.NewMessage() mailer.SetHeader("From", config.EmailFrom) mailer.SetHeader("To", to) mailer.SetHeader("Subject", subject) mailer.SetBody("text/plain", body) if err := s.Dialer.DialAndSend(mailer); err != nil { s.Log.Errorf("Failed to send email: %v", err) return err } return nil } func (s *emailService) SendResetPasswordEmail(to, token string) error { subject := "Reset password" // TODO: replace this url with the link to the reset password page of your front-end app resetPasswordURL := fmt.Sprintf("http://link-to-app/reset-password?token=%s", token) body := fmt.Sprintf(`Dear user, To reset your password, click on this link: %s If you did not request any password resets, then ignore this email.`, resetPasswordURL) return s.SendEmail(to, subject, body) } func (s *emailService) SendVerificationEmail(to, token string) error { subject := "Email Verification" // TODO: replace this url with the link to the email verification page of your front-end app verificationEmailURL := fmt.Sprintf("http://link-to-app/verify-email?token=%s", token) body := fmt.Sprintf(`Dear user, To verify your email, click on this link: %s If you did not create an account, then ignore this email.`, verificationEmailURL) return s.SendEmail(to, subject, body) } ================================================ FILE: src/service/health_check_service.go ================================================ package service import ( "app/src/utils" "errors" "runtime" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type HealthCheckService interface { GormCheck() error MemoryHeapCheck() error } type healthCheckService struct { Log *logrus.Logger DB *gorm.DB } func NewHealthCheckService(db *gorm.DB) HealthCheckService { return &healthCheckService{ Log: utils.Log, DB: db, } } func (s *healthCheckService) GormCheck() error { sqlDB, errDB := s.DB.DB() if errDB != nil { s.Log.Errorf("failed to access the database connection pool: %v", errDB) return errDB } if err := sqlDB.Ping(); err != nil { s.Log.Errorf("failed to ping the database: %v", err) return err } return nil } // MemoryHeapCheck checks if heap memory usage exceeds a threshold func (s *healthCheckService) MemoryHeapCheck() error { var memStats runtime.MemStats runtime.ReadMemStats(&memStats) // Collect memory statistics heapAlloc := memStats.HeapAlloc // Heap memory currently allocated heapThreshold := uint64(300 * 1024 * 1024) // Example threshold: 300 MB s.Log.Infof("Heap Memory Allocation: %v bytes", heapAlloc) // If the heap allocation exceeds the threshold, return an error if heapAlloc > heapThreshold { s.Log.Errorf("Heap memory usage exceeds threshold: %v bytes", heapAlloc) return errors.New("heap memory usage too high") } return nil } ================================================ FILE: src/service/token_service.go ================================================ package service import ( "app/src/config" "app/src/model" res "app/src/response" "app/src/utils" "app/src/validation" "time" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type TokenService interface { GenerateToken(userID string, expires time.Time, tokenType string) (string, error) SaveToken(c *fiber.Ctx, token, userID, tokenType string, expires time.Time) error DeleteToken(c *fiber.Ctx, tokenType string, userID string) error DeleteAllToken(c *fiber.Ctx, userID string) error GetTokenByUserID(c *fiber.Ctx, tokenStr string) (*model.Token, error) GenerateAuthTokens(c *fiber.Ctx, user *model.User) (*res.Tokens, error) GenerateResetPasswordToken(c *fiber.Ctx, req *validation.ForgotPassword) (string, error) GenerateVerifyEmailToken(c *fiber.Ctx, user *model.User) (*string, error) } type tokenService struct { Log *logrus.Logger DB *gorm.DB Validate *validator.Validate UserService UserService } func NewTokenService(db *gorm.DB, validate *validator.Validate, userService UserService) TokenService { return &tokenService{ Log: utils.Log, DB: db, Validate: validate, UserService: userService, } } func (s *tokenService) GenerateToken(userID string, expires time.Time, tokenType string) (string, error) { claims := jwt.MapClaims{ "sub": userID, "iat": time.Now().Unix(), "exp": expires.Unix(), "type": tokenType, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(config.JWTSecret)) } func (s *tokenService) SaveToken(c *fiber.Ctx, token, userID, tokenType string, expires time.Time) error { if err := s.DeleteToken(c, tokenType, userID); err != nil { return err } tokenDoc := &model.Token{ Token: token, UserID: uuid.MustParse(userID), Type: tokenType, Expires: expires, } result := s.DB.WithContext(c.Context()).Create(tokenDoc) if result.Error != nil { s.Log.Errorf("Failed save token: %+v", result.Error) } return result.Error } func (s *tokenService) DeleteToken(c *fiber.Ctx, tokenType string, userID string) error { tokenDoc := new(model.Token) result := s.DB.WithContext(c.Context()). Where("type = ? AND user_id = ?", tokenType, userID). Delete(tokenDoc) if result.Error != nil { s.Log.Errorf("Failed to delete token: %+v", result.Error) } return result.Error } func (s *tokenService) DeleteAllToken(c *fiber.Ctx, userID string) error { tokenDoc := new(model.Token) result := s.DB.WithContext(c.Context()).Where("user_id = ?", userID).Delete(tokenDoc) if result.Error != nil { s.Log.Errorf("Failed to delete all token: %+v", result.Error) } return result.Error } func (s *tokenService) GetTokenByUserID(c *fiber.Ctx, tokenStr string) (*model.Token, error) { userID, err := utils.VerifyToken(tokenStr, config.JWTSecret, config.TokenTypeRefresh) if err != nil { return nil, err } tokenDoc := new(model.Token) result := s.DB.WithContext(c.Context()). Where("token = ? AND user_id = ?", tokenStr, userID). First(tokenDoc) if result.Error != nil { s.Log.Errorf("Failed get token by user id: %+v", err) return nil, result.Error } return tokenDoc, nil } func (s *tokenService) GenerateAuthTokens(c *fiber.Ctx, user *model.User) (*res.Tokens, error) { accessTokenExpires := time.Now().UTC().Add(time.Minute * time.Duration(config.JWTAccessExp)) accessToken, err := s.GenerateToken(user.ID.String(), accessTokenExpires, config.TokenTypeAccess) if err != nil { s.Log.Errorf("Failed generate token: %+v", err) return nil, err } refreshTokenExpires := time.Now().UTC().Add(time.Hour * 24 * time.Duration(config.JWTRefreshExp)) refreshToken, err := s.GenerateToken(user.ID.String(), refreshTokenExpires, config.TokenTypeRefresh) if err != nil { s.Log.Errorf("Failed generate token: %+v", err) return nil, err } if err = s.SaveToken(c, refreshToken, user.ID.String(), config.TokenTypeRefresh, refreshTokenExpires); err != nil { return nil, err } return &res.Tokens{ Access: res.TokenExpires{ Token: accessToken, Expires: accessTokenExpires, }, Refresh: res.TokenExpires{ Token: refreshToken, Expires: refreshTokenExpires, }, }, nil } func (s *tokenService) GenerateResetPasswordToken(c *fiber.Ctx, req *validation.ForgotPassword) (string, error) { if err := s.Validate.Struct(req); err != nil { return "", err } user, err := s.UserService.GetUserByEmail(c, req.Email) if err != nil { return "", err } expires := time.Now().UTC().Add(time.Minute * time.Duration(config.JWTResetPasswordExp)) resetPasswordToken, err := s.GenerateToken(user.ID.String(), expires, config.TokenTypeResetPassword) if err != nil { s.Log.Errorf("Failed generate token: %+v", err) return "", err } if err = s.SaveToken(c, resetPasswordToken, user.ID.String(), config.TokenTypeResetPassword, expires); err != nil { return "", err } return resetPasswordToken, nil } func (s *tokenService) GenerateVerifyEmailToken(c *fiber.Ctx, user *model.User) (*string, error) { expires := time.Now().UTC().Add(time.Minute * time.Duration(config.JWTVerifyEmailExp)) verifyEmailToken, err := s.GenerateToken(user.ID.String(), expires, config.TokenTypeVerifyEmail) if err != nil { s.Log.Errorf("Failed generate token: %+v", err) return nil, err } if err = s.SaveToken(c, verifyEmailToken, user.ID.String(), config.TokenTypeVerifyEmail, expires); err != nil { return nil, err } return &verifyEmailToken, nil } ================================================ FILE: src/service/user_service.go ================================================ package service import ( "app/src/model" "app/src/utils" "app/src/validation" "errors" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type UserService interface { GetUsers(c *fiber.Ctx, params *validation.QueryUser) ([]model.User, int64, error) GetUserByID(c *fiber.Ctx, id string) (*model.User, error) GetUserByEmail(c *fiber.Ctx, email string) (*model.User, error) CreateUser(c *fiber.Ctx, req *validation.CreateUser) (*model.User, error) UpdatePassOrVerify(c *fiber.Ctx, req *validation.UpdatePassOrVerify, id string) error UpdateUser(c *fiber.Ctx, req *validation.UpdateUser, id string) (*model.User, error) DeleteUser(c *fiber.Ctx, id string) error CreateGoogleUser(c *fiber.Ctx, req *validation.GoogleLogin) (*model.User, error) } type userService struct { Log *logrus.Logger DB *gorm.DB Validate *validator.Validate } func NewUserService(db *gorm.DB, validate *validator.Validate) UserService { return &userService{ Log: utils.Log, DB: db, Validate: validate, } } func (s *userService) GetUsers(c *fiber.Ctx, params *validation.QueryUser) ([]model.User, int64, error) { var users []model.User var totalResults int64 if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit query := s.DB.WithContext(c.Context()).Order("created_at asc") if search := params.Search; search != "" { query = query.Where("name LIKE ? OR email LIKE ? OR role LIKE ?", "%"+search+"%", "%"+search+"%", "%"+search+"%") } result := query.Find(&users).Count(&totalResults) if result.Error != nil { s.Log.Errorf("Failed to search users: %+v", result.Error) return nil, 0, result.Error } result = query.Limit(params.Limit).Offset(offset).Find(&users) if result.Error != nil { s.Log.Errorf("Failed to get all users: %+v", result.Error) return nil, 0, result.Error } return users, totalResults, result.Error } func (s *userService) GetUserByID(c *fiber.Ctx, id string) (*model.User, error) { user := new(model.User) result := s.DB.WithContext(c.Context()).First(user, "id = ?", id) if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "User not found") } if result.Error != nil { s.Log.Errorf("Failed get user by id: %+v", result.Error) } return user, result.Error } func (s *userService) GetUserByEmail(c *fiber.Ctx, email string) (*model.User, error) { user := new(model.User) result := s.DB.WithContext(c.Context()).Where("email = ?", email).First(user) if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "User not found") } if result.Error != nil { s.Log.Errorf("Failed get user by email: %+v", result.Error) } return user, result.Error } func (s *userService) CreateUser(c *fiber.Ctx, req *validation.CreateUser) (*model.User, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } hashedPassword, err := utils.HashPassword(req.Password) if err != nil { s.Log.Errorf("Failed hash password: %+v", err) return nil, err } user := &model.User{ Name: req.Name, Email: req.Email, Password: hashedPassword, Role: req.Role, } result := s.DB.WithContext(c.Context()).Create(user) if errors.Is(result.Error, gorm.ErrDuplicatedKey) { return nil, fiber.NewError(fiber.StatusConflict, "Email is already in use") } if result.Error != nil { s.Log.Errorf("Failed to create user: %+v", result.Error) } return user, result.Error } func (s *userService) UpdateUser(c *fiber.Ctx, req *validation.UpdateUser, id string) (*model.User, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } if req.Email == "" && req.Name == "" && req.Password == "" { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid Request") } if req.Password != "" { hashedPassword, err := utils.HashPassword(req.Password) if err != nil { return nil, err } req.Password = hashedPassword } updateBody := &model.User{ Name: req.Name, Password: req.Password, Email: req.Email, } result := s.DB.WithContext(c.Context()).Where("id = ?", id).Updates(updateBody) if errors.Is(result.Error, gorm.ErrDuplicatedKey) { return nil, fiber.NewError(fiber.StatusConflict, "Email is already in use") } if result.RowsAffected == 0 { return nil, fiber.NewError(fiber.StatusNotFound, "User not found") } if result.Error != nil { s.Log.Errorf("Failed to update user: %+v", result.Error) } user, err := s.GetUserByID(c, id) if err != nil { return nil, err } return user, result.Error } func (s *userService) UpdatePassOrVerify(c *fiber.Ctx, req *validation.UpdatePassOrVerify, id string) error { if err := s.Validate.Struct(req); err != nil { return err } if req.Password == "" && !req.VerifiedEmail { return fiber.NewError(fiber.StatusBadRequest, "Invalid Request") } if req.Password != "" { hashedPassword, err := utils.HashPassword(req.Password) if err != nil { return err } req.Password = hashedPassword } updateBody := &model.User{ Password: req.Password, VerifiedEmail: req.VerifiedEmail, } result := s.DB.WithContext(c.Context()).Where("id = ?", id).Updates(updateBody) if result.RowsAffected == 0 { return fiber.NewError(fiber.StatusNotFound, "User not found") } if result.Error != nil { s.Log.Errorf("Failed to update user password or verifiedEmail: %+v", result.Error) } return result.Error } func (s *userService) DeleteUser(c *fiber.Ctx, id string) error { user := new(model.User) result := s.DB.WithContext(c.Context()).Delete(user, "id = ?", id) if result.RowsAffected == 0 { return fiber.NewError(fiber.StatusNotFound, "User not found") } if result.Error != nil { s.Log.Errorf("Failed to delete user: %+v", result.Error) } return result.Error } func (s *userService) CreateGoogleUser(c *fiber.Ctx, req *validation.GoogleLogin) (*model.User, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } userFromDB, err := s.GetUserByEmail(c, req.Email) if err != nil { if err.Error() == "User not found" { user := &model.User{ Name: req.Name, Email: req.Email, VerifiedEmail: req.VerifiedEmail, } if createErr := s.DB.WithContext(c.Context()).Create(user).Error; createErr != nil { s.Log.Errorf("Failed to create user: %+v", createErr) return nil, createErr } return user, nil } return nil, err } userFromDB.VerifiedEmail = req.VerifiedEmail if updateErr := s.DB.WithContext(c.Context()).Save(userFromDB).Error; updateErr != nil { s.Log.Errorf("Failed to update user: %+v", updateErr) return nil, updateErr } return userFromDB, nil } ================================================ FILE: src/utils/bcrypt.go ================================================ package utils import "golang.org/x/crypto/bcrypt" func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(bytes), err } func CheckPasswordHash(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } ================================================ FILE: src/utils/error.go ================================================ package utils import ( "app/src/response" "app/src/validation" "errors" "github.com/gofiber/fiber/v2" ) func ErrorHandler(c *fiber.Ctx, err error) error { if errorsMap := validation.CustomErrorMessages(err); len(errorsMap) > 0 { return response.Error(c, fiber.StatusBadRequest, "Bad Request", errorsMap) } var fiberErr *fiber.Error if errors.As(err, &fiberErr) { return response.Error(c, fiberErr.Code, fiberErr.Message, nil) } return response.Error(c, fiber.StatusInternalServerError, "Internal Server Error", nil) } func NotFoundHandler(c *fiber.Ctx) error { return response.Error(c, fiber.StatusNotFound, "Endpoint Not Found", nil) } ================================================ FILE: src/utils/logrus.go ================================================ package utils import ( "os" "github.com/sirupsen/logrus" ) type CustomFormatter struct { logrus.TextFormatter } var Log *logrus.Logger func init() { Log = logrus.New() // Set logger to use the custom text formatter Log.SetFormatter(&CustomFormatter{ TextFormatter: logrus.TextFormatter{ TimestampFormat: "15:04:05.000", FullTimestamp: true, ForceColors: true, }, }) Log.SetOutput(os.Stdout) } ================================================ FILE: src/utils/verify.go ================================================ //revive:disable:var-naming package utils import ( "errors" "github.com/golang-jwt/jwt/v5" ) func VerifyToken(tokenStr, secret, tokenType string) (string, error) { token, err := jwt.Parse(tokenStr, func(_ *jwt.Token) (interface{}, error) { return []byte(secret), nil }) if err != nil || !token.Valid { return "", err } claims, ok := token.Claims.(jwt.MapClaims) if !ok { return "", errors.New("invalid token claims") } jwtType, ok := claims["type"].(string) if !ok || jwtType != tokenType { return "", errors.New("invalid token type") } userID, ok := claims["sub"].(string) if !ok { return "", errors.New("invalid token sub") } return userID, nil } ================================================ FILE: src/validation/auth_validation.go ================================================ package validation type Register struct { Name string `json:"name" validate:"required,max=50" example:"fake name"` Email string `json:"email" validate:"required,email,max=50" example:"fake@example.com"` Password string `json:"password" validate:"required,min=8,max=20,password" example:"password1"` } type Login struct { Email string `json:"email" validate:"required,email,max=50" example:"fake@example.com"` Password string `json:"password" validate:"required,min=8,max=20,password" example:"password1"` } type GoogleLogin struct { Name string `json:"name" validate:"required,max=50"` Email string `json:"email" validate:"required,email,max=50"` VerifiedEmail bool `json:"verified_email" validate:"required"` } type Logout struct { RefreshToken string `json:"refresh_token" validate:"required,max=255"` } type RefreshToken struct { RefreshToken string `json:"refresh_token" validate:"required,max=255"` } type ForgotPassword struct { Email string `json:"email" validate:"required,email,max=50" example:"fake@example.com"` } type Token struct { Token string `json:"token" validate:"required,max=255"` } ================================================ FILE: src/validation/custom_validation.go ================================================ package validation import ( "regexp" "github.com/go-playground/validator/v10" ) func Password(field validator.FieldLevel) bool { value, ok := field.Field().Interface().(string) if ok { hasDigit := regexp.MustCompile(`[0-9]`).MatchString(value) hasLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(value) if !hasDigit || !hasLetter { return false } } return true } ================================================ FILE: src/validation/user_validation.go ================================================ package validation type CreateUser struct { Name string `json:"name" validate:"required,max=50" example:"fake name"` Email string `json:"email" validate:"required,email,max=50" example:"fake@example.com"` Password string `json:"password" validate:"required,min=8,max=20,password" example:"password1"` Role string `json:"role" validate:"required,oneof=user admin,max=50" example:"user"` } type UpdateUser struct { Name string `json:"name,omitempty" validate:"omitempty,max=50" example:"fake name"` Email string `json:"email" validate:"omitempty,email,max=50" example:"fake@example.com"` Password string `json:"password,omitempty" validate:"omitempty,min=8,max=20,password" example:"password1"` } type UpdatePassOrVerify struct { Password string `json:"password,omitempty" validate:"omitempty,min=8,max=20,password" example:"password1"` VerifiedEmail bool `json:"verified_email" swaggerignore:"true" validate:"omitempty,boolean"` } type QueryUser struct { Page int `validate:"omitempty,number,max=50"` Limit int `validate:"omitempty,number,max=50"` Search string `validate:"omitempty,max=50"` } ================================================ FILE: src/validation/validation.go ================================================ package validation import ( "errors" "fmt" "github.com/go-playground/validator/v10" ) var customMessages = map[string]string{ "required": "Field %s must be filled", "email": "Invalid email address for field %s", "min": "Field %s must have a minimum length of %s characters", "max": "Field %s must have a maximum length of %s characters", "len": "Field %s must be exactly %s characters long", "number": "Field %s must be a number", "positive": "Field %s must be a positive number", "alphanum": "Field %s must contain only alphanumeric characters", "oneof": "Invalid value for field %s", "password": "Field %s must contain at least 1 letter and 1 number", } func CustomErrorMessages(err error) map[string]string { var validationErrors validator.ValidationErrors if errors.As(err, &validationErrors) { return generateErrorMessages(validationErrors) } return nil } func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string { errorsMap := make(map[string]string) for _, err := range validationErrors { fieldName := err.StructNamespace() tag := err.Tag() customMessage := customMessages[tag] if customMessage != "" { errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag) } else { errorsMap[fieldName] = defaultErrorMessage(err) } } return errorsMap } func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string { if tag == "min" || tag == "max" || tag == "len" { return fmt.Sprintf(customMessage, err.Field(), err.Param()) } return fmt.Sprintf(customMessage, err.Field()) } func defaultErrorMessage(err validator.FieldError) string { return fmt.Sprintf("Field validation for '%s' failed on the '%s' tag", err.Field(), err.Tag()) } func Validator() *validator.Validate { validate := validator.New() if err := validate.RegisterValidation("password", Password); err != nil { return nil } return validate } ================================================ FILE: test/fixture/token_fixture.go ================================================ package fixture import ( "app/src/config" "app/src/model" "app/test/helper" "time" ) var ExpiresAccessToken = time.Now().UTC().Add(time.Minute * time.Duration(config.JWTAccessExp)) var ExpiresRefreshToken = time.Now().UTC().Add(time.Hour * 24 * time.Duration(config.JWTRefreshExp)) var ExpiresResetPasswordToken = time.Now().UTC().Add(time.Minute * time.Duration(config.JWTResetPasswordExp)) var ExpiresVerifyEmailToken = time.Now().UTC().Add(time.Minute * time.Duration(config.JWTVerifyEmailExp)) func AccessToken(user *model.User) (string, error) { accessToken, err := helper.GenerateToken(user.ID.String(), ExpiresAccessToken, config.TokenTypeAccess) if err != nil { return accessToken, err } return accessToken, nil } func RefreshToken(user *model.User) (string, error) { refreshToken, err := helper.GenerateToken(user.ID.String(), ExpiresRefreshToken, config.TokenTypeRefresh) if err != nil { return refreshToken, err } return refreshToken, nil } func ResetPasswordToken(user *model.User) (string, error) { resetPasswordToken, err := helper.GenerateToken( user.ID.String(), ExpiresResetPasswordToken, config.TokenTypeResetPassword, ) if err != nil { return resetPasswordToken, err } return resetPasswordToken, nil } func VerifyEmailToken(user *model.User) (string, error) { verifyEmailToken, err := helper.GenerateToken(user.ID.String(), ExpiresVerifyEmailToken, config.TokenTypeVerifyEmail) if err != nil { return verifyEmailToken, err } return verifyEmailToken, nil } ================================================ FILE: test/fixture/user_fixture.go ================================================ package fixture import ( "app/src/model" "github.com/google/uuid" ) var UserOne = &model.User{ ID: uuid.New(), Name: "Test1", Email: "test1@gmail.com", Password: "password1", Role: "user", VerifiedEmail: false, } var UserTwo = &model.User{ ID: uuid.New(), Name: "Test2", Email: "test2@gmail.com", Password: "password1", Role: "user", VerifiedEmail: false, } var Admin = &model.User{ ID: uuid.New(), Name: "Admin", Email: "admin@gmail.com", Password: "password1", Role: "admin", VerifiedEmail: false, } ================================================ FILE: test/helper/helper.go ================================================ package helper import ( "app/src/config" "app/src/model" "app/src/utils" "errors" "time" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/sirupsen/logrus" "gorm.io/gorm" ) func ClearAll(db *gorm.DB) { ClearToken(db) ClearUsers(db) } func ClearUsers(db *gorm.DB) { err := db.Where("id is not null").Delete(&model.User{}).Error if err != nil { logrus.Fatalf("Failed clear user data : %+v", err) } } func ClearToken(db *gorm.DB) { err := db.Where("id is not null").Delete(&model.Token{}).Error if err != nil { logrus.Fatalf("Failed clear user token : %+v", err) } } func CreateUser(db *gorm.DB, email, password, name string) { hashedPassword, err := utils.HashPassword(password) if err != nil { logrus.Errorf("Failed hashed password : %+v", err) } user := &model.User{ Email: email, Password: hashedPassword, Name: name, } err = db.Create(user).Error if err != nil { logrus.Errorf("Failed create user : %+v", err) } } func InsertUser(db *gorm.DB, users ...*model.User) { now := time.Now() for i, user := range users { hashedPassword, err := utils.HashPassword(user.Password) if err != nil { logrus.Errorf("Failed to hash password: %+v", err) continue } user.Password = hashedPassword user.CreatedAt = now.Add(time.Duration(i) * time.Second) if errDB := db.Create(user).Error; errDB != nil { logrus.Errorf("Failed to create user: %+v", errDB) } } } func SaveToken(db *gorm.DB, token, userID, tokenType string, expires time.Time) error { if err := DeleteToken(db, tokenType, userID); err != nil { return err } tokenDoc := &model.Token{ Token: token, UserID: uuid.MustParse(userID), Type: tokenType, Expires: expires, } result := db.Create(tokenDoc) return result.Error } func DeleteToken(db *gorm.DB, tokenType, userID string) error { tokenDoc := new(model.Token) result := db.Where("type = ? AND user_id = ?", tokenType, userID).Delete(tokenDoc) return result.Error } func GenerateToken( userID string, expires time.Time, tokenType string, ) (string, error) { claims := jwt.MapClaims{ "sub": userID, "iat": time.Now().Unix(), "exp": expires.Unix(), "type": tokenType, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(config.JWTSecret)) } func GenerateInvalidToken( userID string, expires time.Time, tokenType string, ) (string, error) { claims := jwt.MapClaims{ "sub": userID, "iat": time.Now().Unix(), "exp": expires.Unix(), "type": tokenType, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte("invalidSecret")) } func GetTokenByUserID(db *gorm.DB, tokenStr string) (*model.Token, error) { userID, err := utils.VerifyToken(tokenStr, config.JWTSecret, config.TokenTypeRefresh) if err != nil { return nil, err } tokenDoc := new(model.Token) result := db.Where("token = ? AND user_id = ?", tokenStr, userID). First(tokenDoc) if result.Error != nil { return nil, result.Error } return tokenDoc, nil } func GetTokenByType(db *gorm.DB, userID string, tokenType string) (*model.Token, error) { tokenDoc := new(model.Token) result := db.Where("type = ? AND user_id = ?", tokenType, userID). First(tokenDoc) if result.Error != nil { return nil, result.Error } return tokenDoc, nil } func GetUserByID(db *gorm.DB, id string) (*model.User, error) { user := new(model.User) result := db.First(user, "id = ?", id) if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, result.Error } if result.Error != nil { logrus.Errorf("Failed get user by id: %+v", result.Error) } return user, result.Error } ================================================ FILE: test/init.go ================================================ package test import ( "app/src/database" "app/src/router" "app/src/utils" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) var App = fiber.New(fiber.Config{ CaseSensitive: true, ErrorHandler: utils.ErrorHandler, }) var DB *gorm.DB var Log = utils.Log func init() { // TODO: You can modify host and database configuration for tests DB = database.Connect("localhost", "testdb") router.Routes(App, DB) App.Use(utils.NotFoundHandler) } ================================================ FILE: test/integration/auth_test.go ================================================ package integration import ( "app/src/config" "app/src/response" "app/src/utils" "app/src/validation" "app/test" "app/test/fixture" "app/test/helper" _ "embed" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/stretchr/testify/assert" ) func TestAuthRoutes(t *testing.T) { t.Run("POST /v1/auth/register", func(t *testing.T) { var requestBody = validation.Register{ Name: "Test", Email: "test@gmail.com", Password: "password1", } t.Run("should return 201 and successfully register user if request data is ok", func(t *testing.T) { helper.ClearAll(test.DB) bodyJSON, err := json.Marshal(requestBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/register", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") msTimeout := 2000 apiResponse, err := test.App.Test(request, msTimeout) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := new(response.SuccessWithTokens) err = json.Unmarshal(bytes, responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusCreated, apiResponse.StatusCode) assert.Equal(t, "success", responseBody.Status) assert.NotContains(t, string(bytes), "password") assert.NotNil(t, responseBody.User.ID) assert.Equal(t, requestBody.Name, responseBody.User.Name) assert.Equal(t, requestBody.Email, responseBody.User.Email) assert.Equal(t, "user", responseBody.User.Role) assert.Equal(t, false, responseBody.User.VerifiedEmail) assert.NotNil(t, responseBody.Tokens.Access.Token) assert.NotNil(t, responseBody.Tokens.Refresh.Token) user, err := helper.GetUserByID(test.DB, responseBody.User.ID.String()) assert.Nil(t, err) assert.NotNil(t, user) assert.NotEqual(t, user.Password, requestBody.Password) assert.Equal(t, user.Name, requestBody.Name) assert.Equal(t, user.Email, requestBody.Email) assert.Equal(t, user.Role, "user") assert.Equal(t, user.VerifiedEmail, false) }) t.Run("should return 400 error if email is invalid", func(t *testing.T) { helper.ClearAll(test.DB) requestBody.Email = "invalidEmail" bodyJSON, err := json.Marshal(requestBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/register", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 409 error if email is already used", func(t *testing.T) { helper.ClearAll(test.DB) helper.CreateUser(test.DB, "test@gmail.com", "test1234", "Test") requestBody.Email = "test@gmail.com" bodyJSON, err := json.Marshal(requestBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/register", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusConflict, apiResponse.StatusCode) }) t.Run("should return 400 error if password length is less than 8 characters", func(t *testing.T) { helper.ClearAll(test.DB) requestBody.Password = "passwo1" bodyJSON, err := json.Marshal(requestBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/register", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 400 error if password does not contain both letters and numbers", func(t *testing.T) { helper.ClearAll(test.DB) requestBody.Password = "password" bodyJSON, err := json.Marshal(requestBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/register", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) requestBody.Password = "11111111" bodyJSON, err = json.Marshal(requestBody) assert.Nil(t, err) request = httptest.NewRequest(http.MethodPost, "/v1/auth/register", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err = test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) }) t.Run("POST /v1/auth/login", func(t *testing.T) { t.Run("should return 200 and login user if email and password match", func(t *testing.T) { helper.CreateUser(test.DB, "test@gmail.com", "test1234", "Test User") loginCredentials := &validation.Login{ Email: "test@gmail.com", Password: "test1234", } bodyJSON, err := json.Marshal(loginCredentials) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/login", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := new(response.SuccessWithTokens) err = json.Unmarshal(bytes, responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) assert.Equal(t, "success", responseBody.Status) assert.NotNil(t, responseBody.User.ID) assert.Equal(t, "Test User", responseBody.User.Name) assert.Equal(t, "test@gmail.com", responseBody.User.Email) assert.Equal(t, "user", responseBody.User.Role) assert.Equal(t, false, responseBody.User.VerifiedEmail) assert.NotNil(t, responseBody.Tokens.Access.Token) assert.NotNil(t, responseBody.Tokens.Refresh.Token) }) t.Run("should return 401 error if there are no users with that email", func(t *testing.T) { helper.ClearAll(test.DB) loginCredentials := &validation.Login{ Email: "nonexistent@gmail.com", Password: "test1234", } bodyJSON, err := json.Marshal(loginCredentials) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/login", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := make(map[string]interface{}) err = json.Unmarshal(bytes, &responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) assert.Equal(t, "error", responseBody["status"]) assert.Equal(t, "Invalid email or password", responseBody["message"]) }) t.Run("should return 401 error if password is wrong", func(t *testing.T) { helper.CreateUser(test.DB, "test@gmail.com", "test1234", "Test User") loginCredentials := &validation.Login{ Email: "test@gmail.com", Password: "wrongPassword1", } bodyJSON, err := json.Marshal(loginCredentials) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/login", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := make(map[string]interface{}) err = json.Unmarshal(bytes, &responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) assert.Equal(t, "error", responseBody["status"]) assert.Equal(t, "Invalid email or password", responseBody["message"]) }) }) t.Run("POST /v1/auth/logout", func(t *testing.T) { t.Run("should return 200 if refresh token is valid", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) refreshToken, err := fixture.RefreshToken(fixture.UserOne) assert.Nil(t, err) err = helper.SaveToken(test.DB, refreshToken, fixture.UserOne.ID.String(), config.TokenTypeRefresh, fixture.ExpiresRefreshToken) assert.Nil(t, err) bodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken}) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/logout", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) dbRefreshTokenDoc, _ := helper.GetTokenByUserID(test.DB, refreshToken) assert.Nil(t, dbRefreshTokenDoc) }) t.Run("should return 400 error if refresh token is missing from request body", func(t *testing.T) { helper.ClearAll(test.DB) request := httptest.NewRequest(http.MethodPost, "/v1/auth/logout", nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 404 error if refresh token is not found in the database", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) refreshToken, err := fixture.RefreshToken(fixture.UserOne) assert.Nil(t, err) bodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken}) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/logout", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiresponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusNotFound, apiresponse.StatusCode) }) }) t.Run("POST /v1/auth/refresh-tokens", func(t *testing.T) { t.Run("should return 200 and new auth tokens if refresh token is valid", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) refreshToken, err := fixture.RefreshToken(fixture.UserOne) assert.Nil(t, err) err = helper.SaveToken(test.DB, refreshToken, fixture.UserOne.ID.String(), config.TokenTypeRefresh, fixture.ExpiresRefreshToken) assert.Nil(t, err) bodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken}) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/refresh-tokens", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := new(response.RefreshToken) err = json.Unmarshal(bytes, responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) assert.NotNil(t, responseBody.Tokens.Access.Token) assert.NotNil(t, responseBody.Tokens.Refresh.Token) dbRefreshTokenDoc, err := helper.GetTokenByUserID(test.DB, responseBody.Tokens.Refresh.Token) assert.Nil(t, err) assert.Equal(t, dbRefreshTokenDoc.UserID, fixture.UserOne.ID) assert.Equal(t, dbRefreshTokenDoc.Type, config.TokenTypeRefresh) }) t.Run("should return 400 error if refresh token is missing from request body", func(t *testing.T) { request := httptest.NewRequest(http.MethodPost, "/v1/auth/refresh-tokens", nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 401 error if refresh token is signed using an invalid secret", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) refreshToken, err := helper.GenerateInvalidToken(fixture.UserOne.ID.String(), fixture.ExpiresRefreshToken, config.TokenTypeRefresh) assert.Nil(t, err) err = helper.SaveToken(test.DB, refreshToken, fixture.UserOne.ID.String(), config.TokenTypeRefresh, fixture.ExpiresRefreshToken) assert.Nil(t, err) bodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken}) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/refresh-tokens", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should return 401 error if refresh token is not found in the database", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) refreshToken, err := fixture.RefreshToken(fixture.UserOne) assert.Nil(t, err) bodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken}) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/refresh-tokens", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should return 401 error if refresh token is expired", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) expires := time.Now().Add(time.Second * 1) refreshToken, err := helper.GenerateToken(fixture.UserOne.ID.String(), expires, config.TokenTypeRefresh) assert.Nil(t, err) err = helper.SaveToken(test.DB, refreshToken, fixture.UserOne.ID.String(), config.TokenTypeRefresh, fixture.ExpiresRefreshToken) assert.Nil(t, err) time.Sleep(2 * time.Second) bodyJSON, err := json.Marshal(validation.RefreshToken{RefreshToken: refreshToken}) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/refresh-tokens", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) }) t.Run("POST /v1/auth/forgot-password", func(t *testing.T) { t.Run("should return 200 and send reset password email to the user", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) requestBody := validation.ForgotPassword{ Email: fixture.UserOne.Email, } bodyJSON, err := json.Marshal(requestBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/forgot-password", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") msTimeout := 10000 apiResponse, err := test.App.Test(request, msTimeout) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) dbVerifyEmailTokenDoc, _ := helper.GetTokenByType(test.DB, fixture.UserOne.ID.String(), config.TokenTypeResetPassword) assert.NotNil(t, dbVerifyEmailTokenDoc) }) t.Run("should return 400 if email is missing", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) request := httptest.NewRequest(http.MethodPost, "/v1/auth/forgot-password", nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 404 if email does not belong to any user", func(t *testing.T) { helper.ClearAll(test.DB) requestBody := validation.ForgotPassword{ Email: fixture.UserOne.Email, } bodyJSON, err := json.Marshal(requestBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/forgot-password", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusNotFound, apiResponse.StatusCode) }) }) t.Run("POST /v1/auth/reset-password", func(t *testing.T) { t.Run("should return 200 and reset the password", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) resetPasswordToken, err := fixture.ResetPasswordToken(fixture.UserOne) assert.Nil(t, err) err = helper.SaveToken(test.DB, resetPasswordToken, fixture.UserOne.ID.String(), config.TokenTypeResetPassword, fixture.ExpiresResetPasswordToken) assert.Nil(t, err) requestBody := validation.UpdatePassOrVerify{ Password: "password2", } bodyJSON, err := json.Marshal(requestBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/reset-password?token="+resetPasswordToken, strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) user, err := helper.GetUserByID(test.DB, fixture.UserOne.ID.String()) assert.Nil(t, err) isPasswordMatch := utils.CheckPasswordHash("password2", user.Password) assert.True(t, isPasswordMatch) dbResetPasswordTokenDoc, _ := helper.GetTokenByUserID(test.DB, resetPasswordToken) assert.Nil(t, dbResetPasswordTokenDoc) }) t.Run("should return 400 if reset password token is missing", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) requestBody := validation.UpdatePassOrVerify{ Password: "password2", } bodyJSON, err := json.Marshal(requestBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/reset-password", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 401 if reset password token is expired", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) expires := time.Now().Add(time.Second * 1) resetPasswordToken, err := helper.GenerateToken(fixture.UserOne.ID.String(), expires, config.TokenTypeResetPassword) assert.Nil(t, err) err = helper.SaveToken(test.DB, resetPasswordToken, fixture.UserOne.ID.String(), config.TokenTypeResetPassword, fixture.ExpiresResetPasswordToken) assert.Nil(t, err) time.Sleep(2 * time.Second) requestBody := validation.UpdatePassOrVerify{ Password: "password2", } bodyJSON, err := json.Marshal(requestBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/reset-password?token="+resetPasswordToken, strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should return 400 if password is missing or invalid", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) resetPasswordToken, err := fixture.ResetPasswordToken(fixture.UserOne) assert.Nil(t, err) err = helper.SaveToken(test.DB, resetPasswordToken, fixture.UserOne.ID.String(), config.TokenTypeResetPassword, fixture.ExpiresResetPasswordToken) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/reset-password?token="+resetPasswordToken, nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) bodyJSON, err := json.Marshal(validation.UpdatePassOrVerify{Password: "short1"}) assert.Nil(t, err) request = httptest.NewRequest(http.MethodPost, "/v1/auth/reset-password?token="+resetPasswordToken, strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err = test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) bodyJSON, err = json.Marshal(validation.UpdatePassOrVerify{Password: "password"}) assert.Nil(t, err) request = httptest.NewRequest(http.MethodPost, "/v1/auth/reset-password?token="+resetPasswordToken, strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err = test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) bodyJSON, err = json.Marshal(validation.UpdatePassOrVerify{Password: "11111111"}) assert.Nil(t, err) request = httptest.NewRequest(http.MethodPost, "/v1/auth/reset-password?token="+resetPasswordToken, strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err = test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) }) t.Run("POST /v1/auth/send-verification-email", func(t *testing.T) { t.Run("should return 200 and send verification email to the user", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/send-verification-email", nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+userOneAccessToken) msTimeout := 10000 apiResponse, err := test.App.Test(request, msTimeout) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) dbVerifyEmailTokenDoc, _ := helper.GetTokenByType(test.DB, fixture.UserOne.ID.String(), config.TokenTypeVerifyEmail) assert.NotNil(t, dbVerifyEmailTokenDoc) }) t.Run("should return 401 error if access token is missing", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) request := httptest.NewRequest(http.MethodPost, "/v1/auth/send-verification-email", nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) }) t.Run("POST /v1/auth/verify-email", func(t *testing.T) { t.Run("should return 200 and verify the email", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) verifyEmailToken, err := fixture.VerifyEmailToken(fixture.UserOne) assert.Nil(t, err) err = helper.SaveToken(test.DB, verifyEmailToken, fixture.UserOne.ID.String(), config.TokenTypeVerifyEmail, fixture.ExpiresVerifyEmailToken) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/auth/verify-email?token="+verifyEmailToken, nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) user, err := helper.GetUserByID(test.DB, fixture.UserOne.ID.String()) assert.Nil(t, err) assert.True(t, user.VerifiedEmail) dbVerifyEmailTokenDoc, _ := helper.GetTokenByUserID(test.DB, verifyEmailToken) assert.Nil(t, dbVerifyEmailTokenDoc) }) t.Run("should return 400 if verify email token is missing", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) request := httptest.NewRequest(http.MethodPost, "/v1/auth/verify-email", nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 401 if verify email token is expired", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) expires := time.Now().Add(time.Second * 1) verifyEmailToken, err := helper.GenerateToken(fixture.UserOne.ID.String(), expires, config.TokenTypeVerifyEmail) assert.Nil(t, err) err = helper.SaveToken(test.DB, verifyEmailToken, fixture.UserOne.ID.String(), config.TokenTypeVerifyEmail, fixture.ExpiresVerifyEmailToken) assert.Nil(t, err) time.Sleep(2 * time.Second) request := httptest.NewRequest(http.MethodPost, "/v1/auth/verify-email?token="+verifyEmailToken, nil) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) }) } func TestAuthMiddleware(t *testing.T) { t.Run("should call next with no errors if access token is valid", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) token, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users", nil) request.Header.Set("Authorization", "Bearer "+token) userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess) assert.Nil(t, err) assert.Equal(t, fixture.UserOne.ID.String(), userID) }) t.Run("should call next with unauthorized error if access token is not found in header", func(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/v1/users", nil) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should call next with unauthorized error if access token is not a valid jwt token", func(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/v1/users", nil) request.Header.Set("Authorization", "Bearer randomToken") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should call next with unauthorized error if the token is not an access token", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) refreshToken, err := fixture.RefreshToken(fixture.UserOne) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users", nil) request.Header.Set("Authorization", "Bearer "+refreshToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should call next with unauthorized error if access token is generated with an invalid secret", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) accessToken, err := helper.GenerateInvalidToken(fixture.UserOne.ID.String(), fixture.ExpiresAccessToken, config.TokenTypeAccess) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users", nil) request.Header.Set("Authorization", "Bearer "+accessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should call next with unauthorized error if access token is expired", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) expires := time.Now().Add(time.Second * 1) accessToken, err := helper.GenerateToken(fixture.UserOne.ID.String(), expires, config.TokenTypeAccess) assert.Nil(t, err) time.Sleep(2 * time.Second) request := httptest.NewRequest(http.MethodGet, "/v1/users", nil) request.Header.Set("Authorization", "Bearer "+accessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should call next with unauthorized error if user is not found", func(t *testing.T) { helper.ClearAll(test.DB) accessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users", nil) request.Header.Set("Authorization", "Bearer "+accessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should call next with forbidden error if user does not have required rights and userId is not in params", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) accessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users", nil) request.Header.Set("Authorization", "Bearer "+accessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusForbidden, apiResponse.StatusCode) }) t.Run("should call next with no errors if user does not have required rights but userId is in params", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) accessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users/"+fixture.UserOne.ID.String(), nil) request.Header.Set("Authorization", "Bearer "+accessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) }) t.Run("should call next with no errors if user has required rights", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.Admin) accessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users/"+fixture.UserOne.ID.String(), nil) request.Header.Set("Authorization", "Bearer "+accessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) }) } ================================================ FILE: test/integration/health_check_test.go ================================================ package integration import ( "app/src/response" "app/test" "encoding/json" "io" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func TestHealthCheckRoutes(t *testing.T) { t.Run("GET /v1/health-check", func(t *testing.T) { t.Run("should return 200 and success response if request is ok", func(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/v1/health-check", nil) msTimeout := 2000 apiResponse, err := test.App.Test(request, msTimeout) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := new(response.HealthCheckResponse) err = json.Unmarshal(bytes, responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) assert.Equal(t, http.StatusOK, responseBody.Code) assert.Equal(t, "success", responseBody.Status) assert.Equal(t, "Health check completed", responseBody.Message) assert.Equal(t, true, responseBody.IsHealthy) assert.Equal(t, []response.HealthCheck{ { Name: "Postgre", Status: "Up", IsUp: true, }, { Name: "Memory", Status: "Up", IsUp: true, }, }, responseBody.Result) }) // t.Run("should return 500 and error response if request failed", func(t *testing.T) { // request := httptest.NewRequest(http.MethodGet, "/v1/health-check", nil) // msTimeout := 2000 // apiResponse, err := test.App.Test(request, msTimeout) // assert.Nil(t, err) // assert.Equal(t, http.StatusInternalServerError, apiResponse.StatusCode) // bytes, err := io.ReadAll(apiResponse.Body) // assert.Nil(t, err) // responseBody := new(response.HealthCheckResponse) // err = json.Unmarshal(bytes, responseBody) // assert.Nil(t, err) // assert.Equal(t, http.StatusInternalServerError, apiResponse.StatusCode) // assert.Equal(t, http.StatusInternalServerError, responseBody.Code) // assert.Equal(t, "error", responseBody.Status) // assert.Equal(t, "Health check completed", responseBody.Message) // assert.Equal(t, false, responseBody.IsHealthy) // assert.Equal(t, []response.HealthCheck{ // { // Name: "Postgre", // Status: "Down", // IsUp: false, // }, // }, responseBody.Result) // }) }) } ================================================ FILE: test/integration/user_test.go ================================================ package integration import ( "app/src/model" "app/src/response" "app/src/validation" "app/test" "app/test/fixture" "app/test/helper" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestUserRoutes(t *testing.T) { t.Run("POST /v1/users", func(t *testing.T) { var newUser = validation.CreateUser{ Name: "Test", Email: "test@gmail.com", Password: "password1", Role: "user", } t.Run("should return 201 and successfully create new user if data is ok", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) bodyJSON, err := json.Marshal(newUser) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/users", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := new(response.SuccessWithUser) err = json.Unmarshal(bytes, responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusCreated, apiResponse.StatusCode) assert.Equal(t, "success", responseBody.Status) assert.NotContains(t, string(bytes), "password") assert.NotNil(t, responseBody.User.ID) assert.Equal(t, newUser.Name, responseBody.User.Name) assert.Equal(t, newUser.Email, responseBody.User.Email) assert.Equal(t, "user", responseBody.User.Role) assert.Equal(t, false, responseBody.User.VerifiedEmail) user, err := helper.GetUserByID(test.DB, responseBody.User.ID.String()) assert.Nil(t, err) assert.NotNil(t, user) assert.NotEqual(t, user.Password, newUser.Password) assert.Equal(t, user.Name, newUser.Name) assert.Equal(t, user.Email, newUser.Email) assert.Equal(t, user.Role, newUser.Role) assert.Equal(t, false, user.VerifiedEmail) }) t.Run("should be able to create an admin as well", func(t *testing.T) { helper.ClearAll(test.DB) newUser.Role = "admin" helper.InsertUser(test.DB, fixture.Admin) adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) bodyJSON, err := json.Marshal(newUser) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/users", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := new(response.SuccessWithUser) err = json.Unmarshal(bytes, responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusCreated, apiResponse.StatusCode) assert.Equal(t, responseBody.User.Role, "admin") user, err := helper.GetUserByID(test.DB, responseBody.User.ID.String()) assert.Nil(t, err) assert.Equal(t, user.Role, "admin") }) t.Run("should return 401 error if access token is missing", func(t *testing.T) { helper.ClearAll(test.DB) bodyJSON, err := json.Marshal(newUser) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/users", strings.NewReader(string(bodyJSON))) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should return 403 error if logged in user is not admin", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) bodyJSON, err := json.Marshal(newUser) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/users", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusForbidden, apiResponse.StatusCode) }) t.Run("should return 400 error if email is invalid", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) newUser.Email = "invalidEmail" adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) bodyJSON, err := json.Marshal(newUser) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/users", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 409 error if email is already used", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin, fixture.UserOne) newUser.Email = fixture.UserOne.Email adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) bodyJSON, err := json.Marshal(newUser) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/users", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusConflict, apiResponse.StatusCode) }) t.Run("should return 400 error if password length is less than 8 characters", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) newUser.Password = "passwo1" adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) bodyJSON, err := json.Marshal(newUser) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/users", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 400 error if password does not contain both letters and numbers", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) newUser.Password = "password" adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) bodyJSON, err := json.Marshal(newUser) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/users", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) newUser.Password = "1111111" bodyJSON, err = json.Marshal(newUser) assert.Nil(t, err) request = httptest.NewRequest(http.MethodPost, "/v1/users", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err = test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 400 error if role is neither user nor admin", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) newUser.Role = "invalid" adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) bodyJSON, err := json.Marshal(newUser) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/users", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 400 error if role is neither user or admin", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) newUser.Role = "invalid" adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) bodyJSON, err := json.Marshal(newUser) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPost, "/v1/users", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) }) t.Run("GET /v1/users", func(t *testing.T) { t.Run("should return 200 and apply the default query options", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo, fixture.Admin) adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users", nil) request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := new(response.SuccessWithPaginate[model.User]) err = json.Unmarshal(bytes, responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) assert.Equal(t, 1, responseBody.Page) assert.Equal(t, 10, responseBody.Limit) assert.Equal(t, int64(1), responseBody.TotalPages) assert.Equal(t, int64(3), responseBody.TotalResults) assert.Len(t, responseBody.Results, 3) assert.Equal(t, fixture.UserOne.ID, responseBody.Results[0].ID) assert.Equal(t, fixture.UserOne.Name, responseBody.Results[0].Name) assert.Equal(t, fixture.UserOne.Email, responseBody.Results[0].Email) assert.Equal(t, fixture.UserOne.Role, responseBody.Results[0].Role) assert.Equal(t, fixture.UserOne.VerifiedEmail, responseBody.Results[0].VerifiedEmail) }) t.Run("should return 401 if access token is missing", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo, fixture.Admin) request := httptest.NewRequest(http.MethodGet, "/v1/users", nil) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should return 403 if a non-admin is trying to access all users", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo, fixture.Admin) userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users", nil) request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusForbidden, apiResponse.StatusCode) }) t.Run("should limit returned array if limit param is specified", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo, fixture.Admin) adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users?limit=2", nil) request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := new(response.SuccessWithPaginate[model.User]) err = json.Unmarshal(bytes, responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) assert.Equal(t, 1, responseBody.Page) assert.Equal(t, 2, responseBody.Limit) assert.Equal(t, int64(2), responseBody.TotalPages) assert.Equal(t, int64(3), responseBody.TotalResults) assert.Len(t, responseBody.Results, 2) assert.Equal(t, fixture.UserOne.ID, responseBody.Results[0].ID) assert.Equal(t, fixture.UserTwo.ID, responseBody.Results[1].ID) }) t.Run("should return the correct page if page and limit params are specified", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo, fixture.Admin) adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users?page=2&limit=2", nil) request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := new(response.SuccessWithPaginate[model.User]) err = json.Unmarshal(bytes, responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) assert.Equal(t, 2, responseBody.Page) assert.Equal(t, 2, responseBody.Limit) assert.Equal(t, int64(2), responseBody.TotalPages) assert.Equal(t, int64(3), responseBody.TotalResults) assert.Len(t, responseBody.Results, 1) assert.Equal(t, fixture.Admin.ID, responseBody.Results[0].ID) }) }) t.Run("GET /v1/users/:userId", func(t *testing.T) { t.Run("should return 200 and the user object if data is ok", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users/"+fixture.UserOne.ID.String(), nil) request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := new(response.SuccessWithUser) err = json.Unmarshal(bytes, responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) assert.NotContains(t, string(bytes), "password") assert.Equal(t, responseBody.User.ID, fixture.UserOne.ID) assert.Equal(t, responseBody.User.Email, fixture.UserOne.Email) assert.Equal(t, responseBody.User.Name, fixture.UserOne.Name) assert.Equal(t, responseBody.User.Role, fixture.UserOne.Role) assert.Equal(t, responseBody.User.VerifiedEmail, fixture.UserOne.VerifiedEmail) }) t.Run("should return 401 error if access token is missing", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) request := httptest.NewRequest(http.MethodGet, "/v1/users/"+fixture.UserOne.ID.String(), nil) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should return 403 error if user is trying to get another user", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo) userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users/"+fixture.UserTwo.ID.String(), nil) request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusForbidden, apiResponse.StatusCode) }) t.Run("should return 200 and the user object if admin is trying to get another user", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.Admin) adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users/"+fixture.UserOne.ID.String(), nil) request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) }) t.Run("should return 400 error if userId is not a valid postgres id", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users/invalidId", nil) request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 404 error if user is not found", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) request := httptest.NewRequest(http.MethodGet, "/v1/users/"+fixture.UserOne.ID.String(), nil) request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusNotFound, apiResponse.StatusCode) }) }) t.Run("DELETE /v1/users/:userId", func(t *testing.T) { t.Run("should return 200 if data is ok", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) request := httptest.NewRequest(http.MethodDelete, "/v1/users/"+fixture.UserOne.ID.String(), nil) request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) user, _ := helper.GetUserByID(test.DB, fixture.UserOne.ID.String()) assert.Nil(t, user) }) t.Run("should return 401 error if access token is missing", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) request := httptest.NewRequest(http.MethodDelete, "/v1/users/"+fixture.UserOne.ID.String(), nil) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should return 403 error if user is trying to delete another user", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo) userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) request := httptest.NewRequest(http.MethodDelete, "/v1/users/"+fixture.UserTwo.ID.String(), nil) request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusForbidden, apiResponse.StatusCode) }) t.Run("should return 200 if admin is trying to delete another user", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.Admin) adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) request := httptest.NewRequest(http.MethodDelete, "/v1/users/"+fixture.UserOne.ID.String(), nil) request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) }) t.Run("should return 400 error if userId is not a valid postgres id", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) request := httptest.NewRequest(http.MethodDelete, "/v1/users/invalidId", nil) request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 404 error if user already is not found", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) request := httptest.NewRequest(http.MethodDelete, "/v1/users/"+fixture.UserOne.ID.String(), nil) request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusNotFound, apiResponse.StatusCode) }) }) t.Run("PATCH /v1/users/:userId", func(t *testing.T) { t.Run("should return 200 and successfully update user if data is ok", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) updateBody := validation.UpdateUser{ Name: "Golang", Email: "golang@gmail.com", Password: "newPassword1", } userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) bodyJSON, err := json.Marshal(updateBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPatch, "/v1/users/"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) bytes, err := io.ReadAll(apiResponse.Body) assert.Nil(t, err) responseBody := new(response.SuccessWithUser) err = json.Unmarshal(bytes, responseBody) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) assert.Equal(t, "success", responseBody.Status) assert.NotContains(t, string(bytes), "password") assert.Equal(t, fixture.UserOne.ID, responseBody.User.ID) assert.Equal(t, updateBody.Name, responseBody.User.Name) assert.Equal(t, updateBody.Email, responseBody.User.Email) assert.Equal(t, "user", responseBody.User.Role) assert.Equal(t, false, responseBody.User.VerifiedEmail) user, err := helper.GetUserByID(test.DB, responseBody.User.ID.String()) assert.Nil(t, err) assert.NotNil(t, user) assert.NotEqual(t, user.Password, updateBody.Password) assert.Equal(t, user.Name, updateBody.Name) assert.Equal(t, user.Email, updateBody.Email) assert.Equal(t, user.Role, "user") }) t.Run("should return 401 error if access token is missing", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) updateBody := validation.UpdateUser{ Name: "Golang", } bodyJSON, err := json.Marshal(updateBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPatch, "/v1/users/"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusUnauthorized, apiResponse.StatusCode) }) t.Run("should return 403 if user is updating another user", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo) updateBody := validation.UpdateUser{ Name: "Golang", } userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) bodyJSON, err := json.Marshal(updateBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPatch, "/v1/users/"+fixture.UserTwo.ID.String(), strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusForbidden, apiResponse.StatusCode) }) t.Run("should return 200 and successfully update user if admin is updating another user", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.Admin) updateBody := validation.UpdateUser{ Name: "Golang", } adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) bodyJSON, err := json.Marshal(updateBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPatch, "/v1/users/"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) }) t.Run("should return 404 if admin is updating another user that is not found", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) updateBody := validation.UpdateUser{ Name: "Golang", } adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) bodyJSON, err := json.Marshal(updateBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPatch, "/v1/users/"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusNotFound, apiResponse.StatusCode) }) t.Run("should return 400 error if userId is not a valid postgres id", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.Admin) updateBody := validation.UpdateUser{ Name: "Golang", } adminAccessToken, err := fixture.AccessToken(fixture.Admin) assert.Nil(t, err) bodyJSON, err := json.Marshal(updateBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPatch, "/v1/users/invalidId", strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+adminAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 400 if email is invalid", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) updateBody := validation.UpdateUser{ Email: "invalidEmail", } userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) bodyJSON, err := json.Marshal(updateBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPatch, "/v1/users/"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 409 if email is already taken", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne, fixture.UserTwo) updateBody := validation.UpdateUser{ Email: fixture.UserTwo.Email, } userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) bodyJSON, err := json.Marshal(updateBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPatch, "/v1/users/"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusConflict, apiResponse.StatusCode) }) t.Run("should not return 400 if email is my email", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) updateBody := validation.UpdateUser{ Email: fixture.UserOne.Email, } userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) bodyJSON, err := json.Marshal(updateBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPatch, "/v1/users/"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusOK, apiResponse.StatusCode) }) t.Run("should return 400 if password length is less than 8 characters", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) updateBody := validation.UpdateUser{ Password: "passwo1", } userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) bodyJSON, err := json.Marshal(updateBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPatch, "/v1/users/"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) t.Run("should return 400 if password does not contain both letters and numbers", func(t *testing.T) { helper.ClearAll(test.DB) helper.InsertUser(test.DB, fixture.UserOne) updateBody := validation.UpdateUser{ Password: "password", } userOneAccessToken, err := fixture.AccessToken(fixture.UserOne) assert.Nil(t, err) bodyJSON, err := json.Marshal(updateBody) assert.Nil(t, err) request := httptest.NewRequest(http.MethodPatch, "/v1/users/"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err := test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) updateBody.Password = "11111111" bodyJSON, err = json.Marshal(updateBody) assert.Nil(t, err) request = httptest.NewRequest(http.MethodPatch, "/v1/users/"+fixture.UserOne.ID.String(), strings.NewReader(string(bodyJSON))) request.Header.Set("Content-Type", "application/json") request.Header.Set("Accept", "application/json") request.Header.Set("Authorization", "Bearer "+userOneAccessToken) apiResponse, err = test.App.Test(request) assert.Nil(t, err) assert.Equal(t, http.StatusBadRequest, apiResponse.StatusCode) }) }) } ================================================ FILE: test/unit/model/user_model_test.go ================================================ package model_test import ( "app/src/model" "app/src/validation" "encoding/json" "testing" "github.com/stretchr/testify/assert" ) var validate = validation.Validator() func TestUserModel(t *testing.T) { t.Run("Create user validation", func(t *testing.T) { var newUser = validation.CreateUser{ Name: "John Doe", Email: "johndoe@gmail.com", Password: "password1", Role: "user", } t.Run("should correctly validate a valid user", func(t *testing.T) { err := validate.Struct(newUser) assert.NoError(t, err) }) t.Run("should throw a validation error if email is invalid", func(t *testing.T) { newUser.Email = "invalidEmail" err := validate.Struct(newUser) assert.Error(t, err) }) t.Run("should throw a validation error if password length is less than 8 characters", func(t *testing.T) { newUser.Password = "passwo1" err := validate.Struct(newUser) assert.Error(t, err) }) t.Run("should throw a validation error if password does not contain numbers", func(t *testing.T) { newUser.Password = "password" err := validate.Struct(newUser) assert.Error(t, err) }) t.Run("should throw a validation error if password does not contain letters", func(t *testing.T) { newUser.Password = "11111111" err := validate.Struct(newUser) assert.Error(t, err) }) t.Run("should throw a validation error if role is unknown", func(t *testing.T) { newUser.Role = "invalid" err := validate.Struct(newUser) assert.Error(t, err) }) }) t.Run("Update user validation", func(t *testing.T) { var updateUser = validation.UpdateUser{ Name: "John Doe", Email: "johndoe@gmail.com", Password: "password1", } t.Run("should correctly validate a valid user", func(t *testing.T) { err := validate.Struct(updateUser) assert.NoError(t, err) }) t.Run("should throw a validation error if email is invalid", func(t *testing.T) { updateUser.Email = "invalidEmail" err := validate.Struct(updateUser) assert.Error(t, err) }) t.Run("should throw a validation error if password length is less than 8 characters", func(t *testing.T) { updateUser.Password = "passwo1" err := validate.Struct(updateUser) assert.Error(t, err) }) t.Run("should throw a validation error if password does not contain numbers", func(t *testing.T) { updateUser.Password = "password" err := validate.Struct(updateUser) assert.Error(t, err) }) t.Run("should throw a validation error if password does not contain letters", func(t *testing.T) { updateUser.Password = "11111111" err := validate.Struct(updateUser) assert.Error(t, err) }) }) t.Run("Update user password validation", func(t *testing.T) { var newPassword = validation.UpdatePassOrVerify{ Password: "password1", } t.Run("should correctly validate a valid user password", func(t *testing.T) { err := validate.Struct(newPassword) assert.NoError(t, err) }) t.Run("should throw a validation error if password length is less than 8 characters", func(t *testing.T) { newPassword.Password = "passwo1" err := validate.Struct(newPassword) assert.Error(t, err) }) t.Run("should throw a validation error if password does not contain numbers", func(t *testing.T) { newPassword.Password = "password" err := validate.Struct(newPassword) assert.Error(t, err) }) t.Run("should throw a validation error if password does not contain letters", func(t *testing.T) { newPassword.Password = "11111111" err := validate.Struct(newPassword) assert.Error(t, err) }) }) t.Run("User toJSON()", func(t *testing.T) { t.Run("should not return user password when toJSON is called", func(t *testing.T) { user := &model.User{ Name: "John Doe", Email: "johndoe@gmail.com", Password: "password1", Role: "user", } bytes, _ := json.Marshal(user) assert.NotContains(t, string(bytes), "password") }) }) }