Repository: StarpTech/go-web Branch: master Commit: 80bf6b777a6b Files: 56 Total size: 57.6 KB Directory structure: gitextract_pi_oupt8/ ├── .editorconfig ├── .github/ │ └── workflows/ │ ├── go.yml │ └── goreleaser.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── app.json ├── config/ │ └── config.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── internal/ │ ├── cache/ │ │ └── cache.go │ ├── context/ │ │ └── app_context.go │ ├── controller/ │ │ ├── healthcheck.go │ │ ├── healthcheck_test.go │ │ ├── init_test.go │ │ ├── metric_test.go │ │ ├── user-list.go │ │ ├── user.go │ │ └── user_test.go │ ├── core/ │ │ ├── cache_store.go │ │ ├── error_handler.go │ │ ├── errors/ │ │ │ └── boom.go │ │ ├── middleware/ │ │ │ └── app_context.go │ │ ├── router.go │ │ ├── server.go │ │ ├── template.go │ │ ├── user_store.go │ │ └── validator.go │ ├── i18n/ │ │ └── i18n.go │ ├── models/ │ │ ├── models.go │ │ └── user.go │ └── store/ │ ├── cache.go │ └── user.go ├── locales/ │ └── en/ │ └── default.po ├── main.go ├── scripts/ │ └── create.db.sql └── web/ ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── README.md ├── package.json ├── src/ │ ├── app/ │ │ ├── app.js │ │ ├── components/ │ │ │ ├── header.js │ │ │ └── like-button.js │ │ ├── index.js │ │ └── styles.scss │ └── global/ │ ├── app.js │ ├── global.html │ ├── index.js │ └── styles.scss └── templates/ ├── layouts/ │ └── base.html └── pages/ ├── user-list.html └── user.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ ; http://editorconfig.org/ root = true [*] insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true indent_style = space indent_size = 2 [{Makefile,go.mod,go.sum,*.go}] indent_style = tab indent_size = 8 ================================================ FILE: .github/workflows/go.yml ================================================ name: CI on: pull_request: paths-ignore: - "**.md" - "docs/**" jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Set up Go 1.14 uses: actions/setup-go@v1 with: go-version: 1.14 id: go - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Install tools run: make setup - name: Test run: make ci ================================================ FILE: .github/workflows/goreleaser.yml ================================================ name: Release with goreleaser on: push: branches: - "!*" tags: - "v*.*.*" jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Unshallow run: git fetch --prune --unshallow - name: Set up Go uses: actions/setup-go@v1 with: go-version: 1.13.x - name: Run GoReleaser uses: goreleaser/goreleaser-action@v1 with: version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. vendor .netrc .vscode .vs .tern-project .DS_Store .idea .cgo_ldflags tmp .eslintcache # dependencies web/node_modules web/.pnp web/.pnp.js # testing web/coverage # misc .DS_Store .env* /.sass-cache /connect.lock /coverage/* /test-results/* /libpeerconnection.log npm-debug.log testem.log /typings /vendor # dist dist # debug npm-debug.log* yarn-debug.log* yarn-error.log* # make artifacts /bin # vendored files /vendor # test outputs /test-results.xml junit-results cypress/screenshots cypress/videos coverage.txt # gcloud utils cloud_sql_proxy .now # tools air air.log # test data cockroach-data bin ================================================ FILE: CONTRIBUTING.md ================================================ ## Setup your machine `go-web` is written in [Go](https://golang.org/). Prerequisites: - `make` - [Go 1.14+](https://golang.org/doc/install) - [Docker](https://www.docker.com/) - `gpg` (probably already installed on your system) Clone `goweb` anywhere: ```sh $ git clone git@github.com:StarpTech/go-web.git ``` Install the build and lint dependencies: ```sh $ make setup ``` A good way of making sure everything is all right is running the test suite: ```sh $ make test ``` ## Test your change You can create a branch for your changes and try to build from the source as you go: ```sh $ make ``` When you are satisfied with the changes, we suggest you run: ```sh $ make ci ``` Which runs all the linters and tests. ## Create a commit Commit messages should be well formatted, and to make that "standardized", we are using Conventional Commits. You can follow the documentation on [their website](https://www.conventionalcommits.org). ## Submit a pull request Push your branch to your `go-web` fork and open a pull request against the master branch. ## Deployment Tag a new release and push it to origin. This will trigger the Github CI to deploy the commit. ``` git tag v1.0.0 git push origin v1.0.0 ``` ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Dustin Deus 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 ================================================ MODULE = $(shell env GO111MODULE=on $(GO) list -m) DATE ?= $(shell date +%FT%T%z) VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || \ cat $(CURDIR)/.version 2> /dev/null || echo v0) PKGS = $(or $(PKG),$(shell env GO111MODULE=on $(GO) list ./...)) TESTPKGS = $(shell env GO111MODULE=on $(GO) list -f \ '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' \ $(PKGS)) BIN = $(CURDIR)/bin GO = go TIMEOUT = 15 V = 0 Q = $(if $(filter 1,$V),,@) M = $(shell printf "\033[34;1m▶\033[0m") export GO111MODULE=on export GOPROXY=https://proxy.golang.org,direct .PHONY: all all: fmt lint | $(BIN) ; $(info $(M) building executable…) @ ## Build program binary $Q $(GO) build \ -tags release \ -ldflags '-X $(MODULE)/cmd.Version=$(VERSION) -X $(MODULE)/cmd.BuildDate=$(DATE)' \ -o $(BIN)/$(basename $(MODULE)) main.go # Tools # Install all the build and lint dependencies setup: # Install by default to .bin curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- v1.24.0 go mod tidy .PHONY: setup $(BIN): @mkdir -p $@ $(BIN)/%: | $(BIN) ; $(info $(M) building $(PACKAGE)…) $Q tmp=$$(mktemp -d); \ env GO111MODULE=off GOPATH=$$tmp GOBIN=$(BIN) $(GO) get $(PACKAGE) \ || ret=$$?; \ rm -rf $$tmp ; exit $$ret # Tests # Run all the tests test: LC_ALL=C go test $(TEST_OPTIONS) -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m .PHONY: test .PHONY: cover cover: ; $(info $(M) running coverage…) @ # Run all the tests and opens the coverage report go tool cover -html=coverage.txt .PHONY: cover .PHONY: fmt fmt: ; $(info $(M) running gofmt…) @ ## Run gofmt on all source files $Q $(GO) fmt $(PKGS) .PHONY: lint lint: ; $(info $(M) running lint…) @ ## Run gofmt on all source files ./bin/golangci-lint run ./... .PHONY: ci ci: all test; $(info $(M) running all the tests and code checks…) @ ## Run all the tests and code checks # UI .PHONY: ui ui: ; @ ## Run frontend development server cd ui && yarn run dev .PHONY: build-ui build-ui: ; @ ## Build frontend production build cd ui && yarn run build # API .PHONY: start start-api: ; @ ## Start api go run main.go # Misc .PHONY: clean clean: ; $(info $(M) cleaning…) @ ## Cleanup everything @rm -rf $(BIN) @rm -rf test/tests.* test/coverage.* .PHONY: help help: @grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' .PHONY: version version: @echo $(VERSION) ================================================ FILE: README.md ================================================ ![big-gopher](big-gopher.png) [![License MIT](https://img.shields.io/badge/License-MIT-blue.svg)](http://opensource.org/licenses/MIT) [![Build Status](https://github.com/StarpTech/go-web/workflows/Go/badge.svg)](https://github.com/StarpTech/go-web/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/StarpTech/go-web)](https://goreportcard.com/report/github.com/StarpTech/go-web) # Go-Web Modern Web Application with Golang "Keep it simple, stupid" # Stack ## Backend - HTTP Middleware [Echo](https://echo.labstack.com/) - ORM library [gorm](https://github.com/jinzhu/gorm) - Configuration [env](https://github.com/caarlos0/env) - Load ENV variables from .env file [godotenv](https://github.com/joho/godotenv) - Payload validation [validator](https://github.com/go-playground/validator) - Cache [Redis](https://github.com/go-redis/redis) - Localization [gotext](https://github.com/leonelquinteros/gotext) - Database [CockroachDB](https://github.com/cockroachdb/cockroach) - Releasing [goreleaser](https://github.com/goreleaser/goreleaser) ## Frontend - Server side templating [Go Templates](https://golang.org/pkg/text/template/) - Module Bundler [Parcel bundler](https://github.com/parcel-bundler/parcel) - Javascript UI library [React](https://github.com/facebook/react) # Getting Started ## Project structure Follows https://github.com/golang-standards/project-layout ## Building From Source This project requires Go +1.13 and Go module support. To build the project run: ``` make ``` ## Bootstrap infrastructure and run application This project requires docker and docker compose to run the required services. 1. To run the services: ``` docker-compose up ``` 2. To create database ``` docker run --network="host" -it cockroachdb/cockroach:v19.2.1 sql --insecure -e "$(cat ./scripts/create.db.sql)" ``` 3. Build [web application](ui/README.md) 4. Start server ``` go run main.go ``` 5. Navigate to users list [page](http://127.0.0.1/users) ## CI and Static Analysis ### CI All pull requests will run through CI, which is currently hosted by Github-CI. Community contributors should be able to see the outcome of this process by looking at the checks on their PR. Please fix any issues to ensure a prompt review from members of the team. ### Static Analysis This project uses the following static analysis tools. Failure during the running of any of these tools results in a failed build. Generally, code must be adjusted to satisfy these tools, though there are exceptions. - [go vet](https://golang.org/cmd/vet/) checks for Go code that should be considered incorrect. - [go fmt](https://golang.org/cmd/gofmt/) checks that Go code is correctly formatted. - [golangci-lint](https://github.com/golangci/golangci-lintt) checks for things like: unused code, code that can be simplified, code that is incorrect and code that will have performance issues. - [go mod tidy](https://tip.golang.org/cmd/go/#hdr-Add_missing_and_remove_unused_modules) ensures that the source code and go.mod agree. # Releasing When a new tag is pushed, the version is released with [goreleaser](https://github.com/goreleaser/goreleaser). ``` $ git tag -a v0.1.0 -m "First release" $ git push origin v0.1.0 # => want to release v0.1.0 ``` # Tooling - IDE plugin [vscode-go](https://github.com/Microsoft/vscode-go) - Administration of cockroachdb [DBeaver](https://dbeaver.io/) - REST client [Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en) - Go testing in the browser [go-convey](https://github.com/smartystreets/goconvey) - Benchmarking [bombardier](http://github.com/codesenberg/bombardier) # Documentation ``` $ godoc github.com/starptech/go-web/pkg/controller $ godoc -http=:6060 ``` Visit localhost:6060 and search for `go-web` # Benchmarking ``` $ bombardier -c 10 -n 10000 http://localhost:8080/users ``` # Cockroachdb Cluster overview http://localhost:8111/ ## Deploy on Heroku [![Heroku Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/StarpTech/go-web) # Further reading - http://www.alexedwards.net/blog/organising-database-access - https://12factor.net/ - https://dev.otto.de/2015/09/30/on-monoliths-and-microservices/ ================================================ FILE: app.json ================================================ { "name": "go-web", "description": "Modern Web Application with Golang", "website": "https://github.com/StarpTech/go-web", "repository": "https://github.com/StarpTech/go-web", "logo": "https://github.com/StarpTech/go-web/raw/master/big-gopher.png", "success_url": "/", "keywords": [ "starter-kit", "golang", "frontend", "api" ], "addons": [ "heroku-postgresql", "heroku-redis" ], "env": {} } ================================================ FILE: config/config.go ================================================ package config import ( "log" "github.com/caarlos0/env" "github.com/joho/godotenv" ) type Configuration struct { Address string `env:"ADDRESS" envDefault:":8080"` Dialect string `env:"DIALECT,required" envDefault:"postgres"` AssetsBuildDir string `env:"ASSETS_BUILD_DIR"` TemplateDir string `env:"TPL_DIR"` LayoutDir string `env:"LAYOUT_DIR"` RedisAddr string `env:"REDIS_ADDR" envDefault:":6379"` RedisPwd string `env:"REDIS_PWD"` ConnectionString string `env:"CONNECTION_STRING,required"` IsProduction bool `env:"PRODUCTION"` GrayLogAddr string `env:"GRAYLOG_ADDR"` RequestLogger bool `env:"REQUEST_LOGGER"` LocaleDir string `env:"LOCALE_DIR" envDefault:"locales"` Lang string `env:"LANG" envDefault:"en_US"` LangDomain string `env:"LANG_DOMAIN" envDefault:"default"` JwtSecret string `env:"JWT_SECRET,required"` } func NewConfig(files ...string) (*Configuration, error) { err := godotenv.Load(files...) if err != nil { log.Printf("No .env file could be found %q\n", files) } cfg := Configuration{} err = env.Parse(&cfg) if err != nil { return nil, err } return &cfg, nil } ================================================ FILE: docker-compose.yml ================================================ version: "3" services: roach1: container_name: roach1 image: cockroachdb/cockroach:v19.2.1 command: start --insecure ports: - "26257:26257" - "8111:8080" volumes: - ./cockroach-data/roach1:/cockroach/cockroach-data networks: roachnet: aliases: - roach1 roach2: container_name: roach2 image: cockroachdb/cockroach:v19.2.1 command: start --insecure --join=roach1 volumes: - ./cockroach-data/roach2:/cockroach/cockroach-data depends_on: - roach1 networks: roachnet: aliases: - roach2 roach3: container_name: roach3 image: cockroachdb/cockroach:v19.2.1 command: start --insecure --join=roach1 volumes: - ./cockroach-data/roach3:/cockroach/cockroach-data depends_on: - roach1 networks: roachnet: aliases: - roach3 redis: ports: - "6379:6379" image: "redis:alpine" networks: roachnet: driver: bridge ================================================ FILE: go.mod ================================================ module github.com/starptech/go-web go 1.13 require ( github.com/caarlos0/env v3.5.0+incompatible github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect github.com/go-playground/locales v0.12.1 // indirect github.com/go-playground/universal-translator v0.16.0 // indirect github.com/go-redis/redis v6.15.5+incompatible github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/golang/protobuf v1.3.1 // indirect github.com/jinzhu/gorm v1.9.12 github.com/jinzhu/now v1.1.1 // indirect github.com/joho/godotenv v1.3.0 github.com/labstack/echo/v4 v4.1.14 github.com/labstack/gommon v0.3.0 github.com/leodido/go-urn v1.1.0 // indirect github.com/mattn/go-isatty v0.0.12 // indirect github.com/mattn/go-sqlite3 v2.0.2+incompatible // indirect github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect github.com/onsi/ginkgo v1.11.0 // indirect github.com/onsi/gomega v1.8.1 // indirect github.com/prometheus/client_golang v0.9.1 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect github.com/prometheus/common v0.2.0 // indirect github.com/prometheus/procfs v0.0.0-20190322151404-55ae3d9d5573 // indirect github.com/stretchr/testify v1.4.0 golang.org/x/crypto v0.0.0-20200117160349-530e935923ad // indirect golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa // indirect golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v9 v9.31.0 gopkg.in/leonelquinteros/gotext.v1 v1.3.1 ) ================================================ FILE: go.sum ================================================ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 h1:OGNva6WhsKst5OZf7eZOklDztV3hwtTHovdrLHV+MsA= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-redis/redis v6.15.5+incompatible h1:pLky8I0rgiblWfa8C1EV7fPEUv0aH6vKRaYHc/YRHVk= github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= 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.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/labstack/echo/v4 v4.1.14 h1:h8XP66UfB3tUm+L3QPw7tmwAu3pJaA/nyfHPCcz46ic= github.com/labstack/echo/v4 v4.1.14/go.mod h1:Q5KZ1vD3V5FEzjM79hjwVrC3ABr7F5IdM23bXQMRDGg= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.2+incompatible h1:qzw9c2GNT8UFrgWNDhCTqRqYUSmu/Dav/9Z58LGpk7U= github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190322151404-55ae3d9d5573 h1:gAuD3LIrjkoOOPLlhGlZWZXztrQII9a9kT6HS5jFtSY= github.com/prometheus/procfs v0.0.0-20190322151404-55ae3d9d5573/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg= golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= ================================================ FILE: internal/cache/cache.go ================================================ package cache import ( "log" "github.com/go-redis/redis" "github.com/starptech/go-web/config" ) func NewCache(config *config.Configuration) *redis.Client { client := redis.NewClient(&redis.Options{ Addr: config.RedisAddr, Password: config.RedisPwd, DB: 0, // use default DB }) pong, err := client.Ping().Result() if err != nil || pong == "" { log.Fatalf("redis cache: got no PONG back %q", err) } return client } ================================================ FILE: internal/context/app_context.go ================================================ package context import ( "github.com/labstack/echo/v4" "github.com/starptech/go-web/config" "github.com/starptech/go-web/internal/i18n" "github.com/starptech/go-web/internal/store" ) // AppContext is the new context in the request / response cycle // We can use the db store, cache and central configuration type AppContext struct { echo.Context UserStore store.User Cache store.Cache Config *config.Configuration Loc i18n.I18ner } ================================================ FILE: internal/controller/healthcheck.go ================================================ package controller import ( "net/http" "github.com/labstack/echo/v4" "github.com/starptech/go-web/internal/context" ) type Healthcheck struct{} type healthcheckReport struct { Health string `json:"health"` Details map[string]bool `json:"details"` } // GetHealthcheck returns the current functional state of the application func (ctrl Healthcheck) GetHealthcheck(c echo.Context) error { cc := c.(*context.AppContext) m := healthcheckReport{Health: "OK"} dbCheck := cc.UserStore.Ping() cacheCheck := cc.Cache.Ping() if dbCheck != nil { m.Health = "NOT" m.Details["db"] = false } if cacheCheck != nil { m.Health = "NOT" m.Details["cache"] = false } return c.JSON(http.StatusOK, m) } ================================================ FILE: internal/controller/healthcheck_test.go ================================================ package controller import ( "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestHealthcheck(t *testing.T) { req := httptest.NewRequest(echo.GET, "/.well-known/health-check", nil) rec := httptest.NewRecorder() e.server.Echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) } ================================================ FILE: internal/controller/init_test.go ================================================ package controller import ( "os" "testing" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/starptech/go-web/config" "github.com/starptech/go-web/internal/core" "github.com/starptech/go-web/internal/models" ) var e struct { config *config.Configuration logger *log.Logger server *core.Server testUser *models.User } func TestMain(m *testing.M) { e.config = &config.Configuration{ ConnectionString: "host=localhost user=gorm dbname=gorm sslmode=disable password=mypassword", TemplateDir: "../templates/*.html", LayoutDir: "../templates/layouts/*.html", Dialect: "postgres", RedisAddr: ":6379", } e.server = core.NewServer(e.config) setup() code := m.Run() tearDown() os.Exit(code) } func setup() { userCtrl := &User{} healthCtrl := &Healthcheck{} g := e.server.Echo.Group("/api") g.GET("/users/:id", userCtrl.GetUserJSON) u := e.server.Echo.Group("/users") u.GET("/:id", userCtrl.GetUser) e.server.Echo.GET("/.well-known/health-check", healthCtrl.GetHealthcheck) e.server.Echo.GET("/.well-known/metrics", echo.WrapHandler(promhttp.Handler())) // test data user := models.User{Name: "Peter"} mr := e.server.GetModelRegistry() err := mr.Register(user) if err != nil { e.server.Echo.Logger.Fatal(err) } mr.AutoMigrateAll() mr.Save(&user) e.testUser = &user } func tearDown() { e.server.GetModelRegistry().AutoDropAll() } ================================================ FILE: internal/controller/metric_test.go ================================================ package controller import ( "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestMetric(t *testing.T) { req := httptest.NewRequest(echo.GET, "/.well-known/metrics", nil) rec := httptest.NewRecorder() e.server.Echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) } ================================================ FILE: internal/controller/user-list.go ================================================ package controller import ( "net/http" "github.com/labstack/echo/v4" "github.com/starptech/go-web/internal/context" "github.com/starptech/go-web/internal/core/errors" "github.com/starptech/go-web/internal/models" ) type ( UserList struct{} UserListViewModel struct { Users []UserViewModel } ) func (ctrl UserList) GetUsers(c echo.Context) error { cc := c.(*context.AppContext) users := []models.User{} err := cc.UserStore.Find(&users) if err != nil { b := errors.NewBoom(errors.UserNotFound, errors.ErrorText(errors.UserNotFound), err) c.Logger().Error(err) return c.JSON(http.StatusNotFound, b) } viewModel := UserListViewModel{ Users: make([]UserViewModel, len(users)), } for index, user := range users { viewModel.Users[index] = UserViewModel{ Name: user.Name, ID: user.ID, } } return c.Render(http.StatusOK, "user-list.html", viewModel) } ================================================ FILE: internal/controller/user.go ================================================ package controller import ( "net/http" "github.com/labstack/echo/v4" "github.com/starptech/go-web/internal/context" "github.com/starptech/go-web/internal/core/errors" "github.com/starptech/go-web/internal/models" ) type ( User struct{} UserViewModel struct { Name string ID string } ) func (ctrl User) GetUser(c echo.Context) error { cc := c.(*context.AppContext) userID := c.Param("id") user := models.User{ID: userID} err := cc.UserStore.First(&user) if err != nil { b := errors.NewBoom(errors.UserNotFound, errors.ErrorText(errors.UserNotFound), err) c.Logger().Error(err) return c.JSON(http.StatusNotFound, b) } vm := UserViewModel{ Name: user.Name, ID: user.ID, } return c.Render(http.StatusOK, "user.html", vm) } func (ctrl User) GetUserJSON(c echo.Context) error { cc := c.(*context.AppContext) userID := c.Param("id") user := models.User{ID: userID} err := cc.UserStore.First(&user) if err != nil { b := errors.NewBoom(errors.UserNotFound, errors.ErrorText(errors.UserNotFound), err) c.Logger().Error(err) return c.JSON(http.StatusNotFound, b) } vm := UserViewModel{ Name: user.Name, ID: user.ID, } return c.JSON(http.StatusOK, vm) } ================================================ FILE: internal/controller/user_test.go ================================================ package controller import ( "net/http" "net/http/httptest" "testing" "github.com/labstack/echo/v4" "github.com/starptech/go-web/internal/context" "github.com/starptech/go-web/internal/core/middleware" "github.com/starptech/go-web/internal/models" "github.com/stretchr/testify/assert" ) type UserFakeStore struct{} func (s *UserFakeStore) First(m *models.User) error { return nil } func (s *UserFakeStore) Find(m *[]models.User) error { return nil } func (s *UserFakeStore) Create(m *models.User) error { return nil } func (s *UserFakeStore) Ping() error { return nil } func TestUserPage(t *testing.T) { req := httptest.NewRequest(echo.GET, "/users/"+e.testUser.ID, nil) rec := httptest.NewRecorder() e.server.Echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) } func TestUnitGetUserJson(t *testing.T) { s := echo.New() g := s.Group("/api") req := httptest.NewRequest(echo.GET, "/api/users/"+e.testUser.ID, nil) rec := httptest.NewRecorder() userCtrl := &User{} cc := &context.AppContext{ Config: e.config, UserStore: &UserFakeStore{}, } s.Use(middleware.AppContext(cc)) g.GET("/users/:id", userCtrl.GetUserJSON) s.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) } ================================================ FILE: internal/core/cache_store.go ================================================ package core import ( "time" "github.com/go-redis/redis" ) // CacheStore simple redis implementation type CacheStore struct { Cache *redis.Client } func (s *CacheStore) Ping() error { return s.Cache.Ping().Err() } func (s *CacheStore) Get(key string) (string, error) { return s.Cache.Get(key).Result() } func (s *CacheStore) Set(key string, value interface{}, exp time.Duration) (string, error) { return s.Cache.Set(key, value, exp).Result() } ================================================ FILE: internal/core/error_handler.go ================================================ package core import ( "net/http" "github.com/labstack/echo/v4" "github.com/starptech/go-web/internal/core/errors" ) func HTTPErrorHandler(err error, c echo.Context) { c.Logger().Error(err) code := http.StatusInternalServerError switch v := err.(type) { case *echo.HTTPError: err := c.JSON(v.Code, v) if err != nil { c.Logger().Error("error handler: json encoding", err) } default: e := errors.NewBoom(errors.InternalError, "Bad implementation", nil) err := c.JSON(code, e) if err != nil { c.Logger().Error("error handler: json encoding", err) } } } ================================================ FILE: internal/core/errors/boom.go ================================================ package errors const ( InternalError = "internalError" UserNotFound = "userNotFound" InvalidBindingModel = "invalidBindingModel" EntityCreationError = "entityCreationError" ) var errorMessage = map[string]string{ "internalError": "an internal error occured", "userNotFound": "user could not be found", "invalidBindingModel": "model could not be bound", "EntityCreationError": "could not create entity", } // Booms can contain multiple boom errors type Booms struct { Errors []Boom `json:"errors"` } func (b *Booms) Add(e Boom) { b.Errors = append(b.Errors, e) } func NewBooms() Booms { return Booms{} } // boom represent the basic structure of an json error type Boom struct { Code string `json:"code"` Message string `json:"message"` Details interface{} `json:"details"` } func NewBoom(code, msg string, details interface{}) Boom { return Boom{Code: code, Message: msg, Details: details} } func ErrorText(code string) string { return errorMessage[code] } ================================================ FILE: internal/core/middleware/app_context.go ================================================ package middleware import ( "github.com/labstack/echo/v4" "github.com/starptech/go-web/internal/context" ) func AppContext(cc *context.AppContext) echo.MiddlewareFunc { return func(h echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { cc.Context = c return h(cc) } } } ================================================ FILE: internal/core/router.go ================================================ package core import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/starptech/go-web/internal/context" mid "github.com/starptech/go-web/internal/core/middleware" "github.com/starptech/go-web/internal/i18n" v "gopkg.in/go-playground/validator.v9" ) func NewRouter(server *Server) *echo.Echo { config := server.config e := echo.New() e.Validator = &Validator{validator: v.New()} cc := context.AppContext{ Cache: &CacheStore{Cache: server.cache}, Config: config, UserStore: &UserStore{DB: server.db}, Loc: i18n.New(), } e.Use(mid.AppContext(&cc)) if config.RequestLogger { e.Use(middleware.Logger()) // request logger } e.Use(middleware.Recover()) // panic errors are thrown e.Use(middleware.BodyLimit("5M")) // limit body payload to 5MB e.Use(middleware.Secure()) // provide protection against injection attacks e.Use(middleware.RequestID()) // generate unique requestId e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"*"}, AllowMethods: []string{echo.GET, echo.HEAD, echo.PUT, echo.PATCH, echo.POST, echo.DELETE}, })) // add custom error formating e.HTTPErrorHandler = HTTPErrorHandler // Add html templates with go template syntax renderer := newTemplateRenderer(config.LayoutDir, config.TemplateDir) e.Renderer = renderer return e } ================================================ FILE: internal/core/server.go ================================================ package core import ( "context" "log" "os" "os/signal" "time" "github.com/go-redis/redis" "github.com/jinzhu/gorm" "github.com/labstack/echo/v4" "github.com/starptech/go-web/config" "github.com/starptech/go-web/internal/cache" "github.com/starptech/go-web/internal/i18n" "github.com/starptech/go-web/internal/models" ) type Server struct { Echo *echo.Echo // HTTP middleware config *config.Configuration // Configuration db *gorm.DB // Database connection cache *redis.Client // Redis cache connection modelRegistry *models.Model // Model registry for migration } // NewServer will create a new instance of the application func NewServer(config *config.Configuration) *Server { server := &Server{} server.config = config i18n.Configure(config.LocaleDir, config.Lang, config.LangDomain) server.modelRegistry = models.NewModel() err := server.modelRegistry.OpenWithConfig(config) if err != nil { log.Fatalf("gorm: could not connect to db %q", err) } server.cache = cache.NewCache(config) server.db = server.modelRegistry.DB server.Echo = NewRouter(server) return server } // GetDB returns gorm (ORM) func (s *Server) GetDB() *gorm.DB { return s.db } // GetCache returns the current redis client func (s *Server) GetCache() *redis.Client { return s.cache } // GetConfig return the current app configuration func (s *Server) GetConfig() *config.Configuration { return s.config } // GetModelRegistry returns the model registry func (s *Server) GetModelRegistry() *models.Model { return s.modelRegistry } // Start the http server func (s *Server) Start(addr string) error { return s.Echo.Start(addr) } // ServeStaticFiles serve static files for development purpose func (s *Server) ServeStaticFiles() { s.Echo.Static("/assets", s.config.AssetsBuildDir) } // GracefulShutdown Wait for interrupt signal // to gracefully shutdown the server with a timeout of 5 seconds. func (s *Server) GracefulShutdown() { quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) <-quit ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // close cache if s.cache != nil { cErr := s.cache.Close() if cErr != nil { s.Echo.Logger.Fatal(cErr) } } // close database connection if s.db != nil { dErr := s.db.Close() if dErr != nil { s.Echo.Logger.Fatal(dErr) } } // shutdown http server if err := s.Echo.Shutdown(ctx); err != nil { s.Echo.Logger.Fatal(err) } } ================================================ FILE: internal/core/template.go ================================================ package core import ( "fmt" "html/template" "io" "log" "path/filepath" "github.com/labstack/echo/v4" "github.com/starptech/go-web/internal/i18n" ) var mainTmpl = `{{define "main" }} {{ template "base" . }} {{ end }}` type templateRenderer struct { templates map[string]*template.Template } // NewTemplateRenderer creates a new setup to render layout based go templates func newTemplateRenderer(layoutsDir, templatesDir string) *templateRenderer { r := &templateRenderer{} r.templates = make(map[string]*template.Template) r.Load(layoutsDir, templatesDir) return r } func (t *templateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { tmpl, ok := t.templates[name] if !ok { c.Logger().Fatalf("the template %s does not exist", name) return fmt.Errorf("the template %s does not exist", name) } return tmpl.ExecuteTemplate(w, "base", data) } func (t *templateRenderer) Load(layoutsDir, templatesDir string) { layouts, err := filepath.Glob(layoutsDir) if err != nil { log.Fatal(err) } includes, err := filepath.Glob(templatesDir) if err != nil { log.Fatal(err) } funcMap := template.FuncMap{ "Loc": i18n.Get, } mainTemplate := template.New("main") mainTemplate.Funcs(funcMap) mainTemplate, err = mainTemplate.Parse(mainTmpl) if err != nil { log.Fatal(err) } for _, file := range includes { fileName := filepath.Base(file) files := append(layouts, file) t.templates[fileName], err = mainTemplate.Clone() if err != nil { log.Fatal(err) } t.templates[fileName] = template.Must(t.templates[fileName].ParseFiles(files...)) } } ================================================ FILE: internal/core/user_store.go ================================================ package core import ( "github.com/jinzhu/gorm" "github.com/starptech/go-web/internal/models" ) // UserStore implements the UserStore interface type UserStore struct { DB *gorm.DB } func (s *UserStore) First(m *models.User) error { return s.DB.First(m).Error } func (s *UserStore) Create(m *models.User) error { return s.DB.Create(m).Error } func (s *UserStore) Find(m *[]models.User) error { return s.DB.Find(m).Error } func (s *UserStore) Ping() error { return s.DB.DB().Ping() } ================================================ FILE: internal/core/validator.go ================================================ package core import ( validator "gopkg.in/go-playground/validator.v9" ) type Validator struct { validator *validator.Validate } func (v *Validator) Validate(i interface{}) error { return v.validator.Struct(i) } ================================================ FILE: internal/i18n/i18n.go ================================================ package i18n import gotext "gopkg.in/leonelquinteros/gotext.v1" type I18ner interface { Get(string, ...interface{}) string } type I18n struct{} func New() *I18n { return &I18n{} } func Configure(lib, lang, dom string) { gotext.Configure(lib, lang, dom) } func (i *I18n) Get(str string, vars ...interface{}) string { return gotext.Get(str, vars...) } func Get(str string, vars ...interface{}) string { return gotext.Get(str, vars...) } ================================================ FILE: internal/models/models.go ================================================ package models import ( "errors" "reflect" "strings" "time" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/postgres" "github.com/starptech/go-web/config" ) // Model facilitate database interactions type Model struct { models map[string]reflect.Value isOpen bool *gorm.DB } // NewModel returns a new Model without opening database connection func NewModel() *Model { return &Model{ models: make(map[string]reflect.Value), } } // IsOpen returns true if the Model has already established connection // to the database func (m *Model) IsOpen() bool { return m.isOpen } // OpenWithConfig opens database connection with the settings found in cfg func (m *Model) OpenWithConfig(cfg *config.Configuration) error { db, err := gorm.Open(cfg.Dialect, cfg.ConnectionString) if err != nil { return err } // https://github.com/go-sql-driver/mysql/issues/461 db.DB().SetConnMaxLifetime(time.Minute * 5) db.DB().SetMaxIdleConns(0) db.DB().SetMaxOpenConns(20) m.DB = db m.isOpen = true return nil } // Register adds the values to the models registry func (m *Model) Register(values ...interface{}) error { // do not work on them.models first, this is like an insurance policy // whenever we encounter any error in the values nothing goes into the registry models := make(map[string]reflect.Value) if len(values) > 0 { for _, val := range values { rVal := reflect.ValueOf(val) if rVal.Kind() == reflect.Ptr { rVal = rVal.Elem() } switch rVal.Kind() { case reflect.Struct: models[getTypeName(rVal.Type())] = reflect.New(rVal.Type()) default: return errors.New("models must be structs") } } } for k, v := range models { m.models[k] = v } return nil } // AutoMigrateAll runs migrations for all the registered models func (m *Model) AutoMigrateAll() { for _, v := range m.models { m.AutoMigrate(v.Interface()) } } // AutoDropAll drops all tables of all registered models func (m *Model) AutoDropAll() { for _, v := range m.models { m.DropTableIfExists(v.Interface()) } } func getTypeName(typ reflect.Type) string { if typ.Name() != "" { return typ.Name() } split := strings.Split(typ.String(), ".") return split[len(split)-1] } ================================================ FILE: internal/models/user.go ================================================ package models import "time" type User struct { ID string `gorm:"type:uuid;primary_key;default:gen_random_uuid()"` Name string `sql:"type:varchar(30)"` CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time } ================================================ FILE: internal/store/cache.go ================================================ package store import "time" type Cache interface { Ping() error Get(string) (string, error) Set(string, interface{}, time.Duration) (string, error) } ================================================ FILE: internal/store/user.go ================================================ package store import "github.com/starptech/go-web/internal/models" type User interface { First(m *models.User) error Find(m *[]models.User) error Create(m *models.User) error Ping() error } ================================================ FILE: locales/en/default.po ================================================ # msgid "" # msgstr "" # Initial comment # Headers below "Language: en\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" # Some comment msgid "My text" msgstr "Translated text" # More comments msgid "Another string" msgstr "" ================================================ FILE: main.go ================================================ package main import ( "log" "github.com/labstack/echo/v4" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/starptech/go-web/config" "github.com/starptech/go-web/internal/controller" "github.com/starptech/go-web/internal/core" "github.com/starptech/go-web/internal/models" ) func main() { config, err := config.NewConfig() if err != nil { log.Fatalf("%+v\n", err) } // create server server := core.NewServer(config) // serve files for dev server.ServeStaticFiles() userCtrl := &controller.User{} userListCtrl := &controller.UserList{} healthCtrl := &controller.Healthcheck{} // api endpoints g := server.Echo.Group("/api") g.GET("/users/:id", userCtrl.GetUserJSON) // pages u := server.Echo.Group("/users") u.GET("", userListCtrl.GetUsers) u.GET("/:id", userCtrl.GetUser) // metric / health endpoint according to RFC 5785 server.Echo.GET("/.well-known/health-check", healthCtrl.GetHealthcheck) server.Echo.GET("/.well-known/metrics", echo.WrapHandler(promhttp.Handler())) // migration for dev user := models.User{Name: "Peter"} mr := server.GetModelRegistry() err = mr.Register(user) if err != nil { server.Echo.Logger.Fatal(err) } mr.AutoMigrateAll() mr.Create(&user) // Start server go func() { if err := server.Start(config.Address); err != nil { server.Echo.Logger.Info("shutting down the server") } }() server.GracefulShutdown() } ================================================ FILE: scripts/create.db.sql ================================================ CREATE USER IF NOT EXISTS goweb; CREATE DATABASE goweb; GRANT ALL ON DATABASE goweb TO goweb; ================================================ FILE: web/.babelrc ================================================ { "presets": [ [ "@babel/preset-env", { "modules": false } ], "@babel/preset-react" ] } ================================================ FILE: web/.eslintrc.json ================================================ { "rules": {}, "env": { "es6": true, "browser": true }, "parserOptions": { "ecmaVersion": 2018, "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "extends": [ "eslint:recommended", "plugin:prettier/recommended" ], "globals": { "Atomics": "readonly", "SharedArrayBuffer": "readonly" }, "plugins": [ "react" ] } ================================================ FILE: web/.gitignore ================================================ .cache/ coverage/ dist/* !dist/index.html node_modules/ *.log # OS generated files .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db ================================================ FILE: web/README.md ================================================ # Go Web Web application for the api ## Building and running on localhost First install dependencies: ```sh yarn install ``` To run in hot module reloading mode: ```sh yarn start ``` To create a production build: ```sh yarn run build-prod ``` ## Development Run the server and start the bundler in watch mode ```sh yarn start go run main.go ``` ================================================ FILE: web/package.json ================================================ { "name": "empty-project", "version": "1.0.0", "description": "", "main": "index.js", "keywords": [], "author": "", "license": "ISC", "scripts": { "clean": "rm -rf dist", "start": "parcel watch src/app/index.js --public-url /assets/dist", "build-prod": "yarn clean && parcel build src/app/index.js --public-url /assets/dist" }, "dependencies": { "react": "^16.12.0", "react-dom": "^16.12.0" }, "devDependencies": { "@babel/core": "^7.8.3", "@babel/preset-env": "^7.8.3", "@babel/preset-react": "^7.8.3", "bootstrap": "^4.4.1", "eslint": "^6.8.0", "eslint-config-prettier": "^6.9.0", "eslint-plugin-prettier": "^3.1.2", "parcel-bundler": "^1.12.4", "prettier": "^1.19.1", "sass": "^1.25.1-test.1" } } ================================================ FILE: web/src/app/app.js ================================================ import React from "react"; export default function App({ message }) { return (

{message}

); } ================================================ FILE: web/src/app/components/header.js ================================================ import React from "react"; export default function Header({ message }) { return (

{message}

Welcome to modern web development with Go

); } ================================================ FILE: web/src/app/components/like-button.js ================================================ import React, { useState } from "react"; export default function LikeButton({ id }) { const [likes, setLikes] = useState(() => parseInt(id)); return ( ); } ================================================ FILE: web/src/app/index.js ================================================ import React from "react"; import ReactDOM from "react-dom"; import App from "./app"; import LikeButton from "./components/like-button"; import Header from "./components/header"; import "./styles.scss"; var mountNode = document.getElementById("app"); ReactDOM.render(, mountNode); document.querySelectorAll(".like-button-component").forEach(domContainer => { ReactDOM.render(, domContainer); }); document.querySelectorAll(".header-component").forEach(domContainer => { ReactDOM.render(
, domContainer); }); ================================================ FILE: web/src/app/styles.scss ================================================ @import "~bootstrap/scss/bootstrap"; header { padding: 156px 0 100px; } section { padding: 50px 0; } .bd-highlight { background-color: rgb(237, 253, 255); border: 1px solid rgb(106, 214, 227); } ================================================ FILE: web/src/global/app.js ================================================ import React from "react"; export default function App({ message }) { return (

{message}

); } ================================================ FILE: web/src/global/global.html ================================================ ================================================ FILE: web/src/global/index.js ================================================ import React from "react"; import ReactDOM from "react-dom"; import App from "./app"; import "./styles.scss"; var mountNode = document.getElementById("app"); ReactDOM.render( , mountNode ); ================================================ FILE: web/src/global/styles.scss ================================================ @import "~bootstrap/scss/bootstrap"; ================================================ FILE: web/templates/layouts/base.html ================================================ {{ define "base" }} {{ block "title" . }} {{end}} {{ template "header" . }}
{{ template "content" . }}

Copyright © Your Website 2019

{{ end }} ================================================ FILE: web/templates/pages/user-list.html ================================================ {{define "title"}}User list{{end}} {{define "header"}}
{{end}} {{define "content"}}
{{range .Users}}
{{.Name}}: Details
{{end}}
{{end}} ================================================ FILE: web/templates/pages/user.html ================================================ {{define "title"}}User {{.Name}}{{end}} {{define "header"}}
{{end}} {{define "content"}}
ID: {{.ID}}
Name: {{.Name}}
Translated content: {{ Loc "My text" }}
Mount server rendered html as react component:
{{end}}