├── .appveyor.yml ├── .dockerignore ├── .drone.jsonnet ├── .drone.yml ├── .env.example ├── .gitignore ├── .graphqlconfig ├── .revive.toml ├── LICENSE ├── Makefile ├── README.md ├── api ├── 404.go ├── common.go ├── errors.go ├── favicon.go ├── healthz.go ├── index.go ├── shorten.go └── url.go ├── assets ├── ab0x.yaml ├── assets.go ├── dist │ ├── favicon.ico │ └── firebase │ │ └── .gitkeep └── generate.go ├── cmd ├── ggz-redirect │ ├── health.go │ ├── main.go │ └── server.go └── ggz-server │ ├── health.go │ ├── mail.go │ ├── main.go │ ├── server.go │ └── setup.go ├── configs └── prometheus.yml ├── docker-compose.yml ├── docker ├── ggz-redirect │ ├── Dockerfile.linux.amd64 │ ├── Dockerfile.linux.arm │ ├── Dockerfile.linux.arm64 │ ├── Dockerfile.windows.amd64 │ └── manifest.tmpl └── ggz-server │ ├── Dockerfile.linux.amd64 │ ├── Dockerfile.linux.arm │ ├── Dockerfile.linux.arm64 │ ├── Dockerfile.windows.amd64 │ └── manifest.tmpl ├── go.mod ├── go.sum ├── integrations └── container_test.go ├── pipeline.libsonnet └── pkg ├── config └── config.go ├── errors ├── errors.go └── errors_test.go ├── fixtures ├── shorten.yml └── user.yml ├── helper ├── jwt.go ├── qrcode.go └── validator.go ├── middleware ├── auth │ ├── auth.go │ ├── auth0 │ │ └── auth0.go │ └── firebase │ │ └── firebase.go └── header │ └── header.go ├── model ├── errors.go ├── main_test.go ├── migrate.go ├── models.go ├── models_sqlite.go ├── models_test.go ├── shorten.go ├── test_fixtures.go ├── token.go ├── unit_tests.go ├── user.go └── user_test.go ├── module ├── base │ ├── base.go │ └── base_test.go ├── loader │ ├── cache.go │ ├── lru │ │ └── cache.go │ └── memory │ │ └── cache.go ├── mailer │ ├── mailer.go │ ├── ses.go │ └── smtp.go ├── meta │ └── meta.go ├── metrics │ └── collector.go ├── socket │ └── socket.go └── storage │ ├── disk │ ├── disk.go │ └── disk_test.go │ ├── minio │ └── minio.go │ └── storage.go ├── router ├── graphql.go ├── metrics.go └── routes │ ├── main_test.go │ ├── routes.go │ └── routes_test.go ├── schema ├── errors.go ├── main_test.go ├── schema.go ├── unit_tests.go ├── url.go ├── url_test.go └── user.go └── version └── version.go /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | image: 'Visual Studio 2017' 3 | platform: x64 4 | 5 | clone_folder: 'c:\gopath\src\github.com\go-ggz\ggz' 6 | max_jobs: 1 7 | 8 | environment: 9 | GOPATH: c:\gopath 10 | docker_username: 11 | secure: em/TNLUXxG19O/HvbvfJuQ== 12 | docker_password: 13 | secure: Yo9FJJqihaNz5q8T4Jz8tQ== 14 | GO111MODULE: on 15 | GOVERSION: 1.13.3 16 | 17 | branches: 18 | only: 19 | - master 20 | 21 | install: 22 | - go version 23 | - go env 24 | - ps: | 25 | docker version 26 | go version 27 | - ps: | 28 | $env:Path = "c:\gopath\bin;$env:Path" 29 | 30 | build_script: 31 | - ps: | 32 | if ( $env:APPVEYOR_REPO_TAG -eq 'false' ) { 33 | $version = $env:APPVEYOR_REPO_COMMIT 34 | $buildDate = $env:APPVEYOR_REPO_COMMIT_TIMESTAMP 35 | } else { 36 | $version = $env:APPVEYOR_REPO_TAG_NAME 37 | $buildDate = $env:APPVEYOR_REPO_COMMIT_TIMESTAMP 38 | } 39 | go get -u github.com/UnnoTed/fileb0x 40 | go generate ./assets/... 41 | go build -ldflags "-X github.com/go-ggz/ggz/pkg/version.Version=$version -X github.com/go-ggz/ggz/pkg/version.BuildDate=$buildDate" -a -o release/ggz-server.exe ./cmd/ggz-server 42 | go build -ldflags "-X github.com/go-ggz/ggz/pkg/version.Version=$version -X github.com/go-ggz/ggz/pkg/version.BuildDate=$buildDate" -a -o release/ggz-redirect.exe ./cmd/ggz-redirect 43 | 44 | docker pull microsoft/nanoserver:10.0.14393.1884 45 | docker build -f docker/ggz-server/Dockerfile.windows.amd64 -t goggz/ggz-server:windows-amd64 . 46 | docker build -f docker/ggz-redirect/Dockerfile.windows.amd64 -t goggz/ggz-redirect:windows-amd64 . 47 | 48 | test_script: 49 | - ps: | 50 | docker run --rm goggz/ggz-server:windows-amd64 --version 51 | docker run --rm goggz/ggz-redirect:windows-amd64 --version 52 | 53 | deploy_script: 54 | - ps: | 55 | $ErrorActionPreference = 'Stop'; 56 | if ( $env:APPVEYOR_PULL_REQUEST_NUMBER ) { 57 | Write-Host Nothing to deploy. 58 | } else { 59 | echo $env:DOCKER_PASSWORD | docker login --username $env:DOCKER_USERNAME --password-stdin 60 | if ( $env:APPVEYOR_REPO_TAG -eq 'true' ) { 61 | $major,$minor,$patch = $env:APPVEYOR_REPO_TAG_NAME.split('.') 62 | 63 | docker tag goggz/ggz-server:windows-amd64 goggz/ggz:$major.$minor.$patch-windows-amd64 64 | docker push goggz/ggz-server:$major.$minor.$patch-windows-amd64 65 | docker tag goggz/ggz-redirect:windows-amd64 goggz/ggz:$major.$minor.$patch-windows-amd64 66 | docker push goggz/ggz-redirect:$major.$minor.$patch-windows-amd64 67 | 68 | docker tag goggz/ggz-server:windows-amd64 goggz/ggz:$major.$minor-windows-amd64 69 | docker push goggz/ggz-server:$major.$minor-windows-amd64 70 | docker tag goggz/ggz-redirect:windows-amd64 goggz/ggz:$major.$minor-windows-amd64 71 | docker push goggz/ggz-redirect:$major.$minor-windows-amd64 72 | 73 | docker tag goggz/ggz-server:windows-amd64 goggz/ggz:$major-windows-amd64 74 | docker push goggz/ggz-server:$major-windows-amd64 75 | docker tag goggz/ggz-redirect:windows-amd64 goggz/ggz:$major-windows-amd64 76 | docker push goggz/ggz-redirect:$major-windows-amd64 77 | } else { 78 | if ( $env:APPVEYOR_REPO_BRANCH -eq 'master' ) { 79 | docker push goggz/ggz-server:windows-amd64 80 | docker push goggz/ggz-redirect:windows-amd64 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !release/ 3 | -------------------------------------------------------------------------------- /.drone.jsonnet: -------------------------------------------------------------------------------- 1 | local pipeline = import 'pipeline.libsonnet'; 2 | local ggzServer = 'ggz-server'; 3 | local ggzRedirect = 'ggz-redirect'; 4 | [ 5 | pipeline.test, 6 | pipeline.build(ggzServer, 'linux', 'amd64', true), 7 | pipeline.build(ggzServer, 'linux', 'arm64', true), 8 | pipeline.build(ggzServer, 'linux', 'arm', true), 9 | pipeline.build(ggzRedirect, 'linux', 'amd64', true), 10 | pipeline.build(ggzRedirect, 'linux', 'arm64', true), 11 | pipeline.build(ggzRedirect, 'linux', 'arm', true), 12 | pipeline.release, 13 | pipeline.notifications(ggzServer, depends_on=[ 14 | ggzServer + '-linux-amd64', 15 | ggzServer + '-linux-arm64', 16 | ggzServer + '-linux-arm', 17 | 'release-binary', 18 | ]), 19 | pipeline.notifications(ggzRedirect, depends_on=[ 20 | ggzRedirect + '-linux-amd64', 21 | ggzRedirect + '-linux-arm64', 22 | ggzRedirect + '-linux-arm', 23 | 'release-binary', 24 | ]), 25 | ] 26 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | name: testing 4 | 5 | platform: 6 | os: linux 7 | arch: amd64 8 | 9 | steps: 10 | - name: generate 11 | pull: always 12 | image: golang:1.13 13 | commands: 14 | - make generate 15 | volumes: 16 | - name: gopath 17 | path: /go 18 | 19 | - name: vet 20 | pull: always 21 | image: golang:1.13 22 | commands: 23 | - make vet 24 | volumes: 25 | - name: gopath 26 | path: /go 27 | 28 | - name: lint 29 | pull: always 30 | image: golang:1.13 31 | commands: 32 | - make lint 33 | volumes: 34 | - name: gopath 35 | path: /go 36 | 37 | - name: misspell 38 | pull: always 39 | image: golang:1.13 40 | commands: 41 | - make misspell-check 42 | volumes: 43 | - name: gopath 44 | path: /go 45 | 46 | - name: embedmd 47 | pull: always 48 | image: golang:1.13 49 | commands: 50 | - make embedmd 51 | volumes: 52 | - name: gopath 53 | path: /go 54 | 55 | - name: test 56 | pull: always 57 | image: golang:1.13 58 | commands: 59 | - make test 60 | volumes: 61 | - name: gopath 62 | path: /go 63 | 64 | - name: codecov 65 | pull: always 66 | image: robertstettner/drone-codecov 67 | settings: 68 | token: 69 | from_secret: codecov_token 70 | 71 | volumes: 72 | - name: gopath 73 | temp: {} 74 | 75 | --- 76 | kind: pipeline 77 | name: ggz-server-linux-amd64 78 | 79 | platform: 80 | os: linux 81 | arch: amd64 82 | 83 | steps: 84 | - name: build-push 85 | pull: always 86 | image: golang:1.13 87 | commands: 88 | - make generate 89 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_COMMIT_SHA:0:8} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/amd64/ggz-server ./cmd/ggz-server 90 | environment: 91 | CGO_ENABLED: 1 92 | when: 93 | event: 94 | exclude: 95 | - tag 96 | 97 | - name: build-tag 98 | pull: always 99 | image: golang:1.13 100 | commands: 101 | - make generate 102 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_TAG##v} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/amd64/ggz-server ./cmd/ggz-server 103 | environment: 104 | CGO_ENABLED: 1 105 | when: 106 | event: 107 | - tag 108 | 109 | - name: executable 110 | pull: always 111 | image: golang:1.13 112 | commands: 113 | - ./release/linux/amd64/ggz-server --help 114 | 115 | - name: dryrun 116 | pull: always 117 | image: plugins/docker:linux-amd64 118 | settings: 119 | cache_from: goggz/ggz-server 120 | dockerfile: docker/ggz-server/Dockerfile.linux.amd64 121 | dry_run: true 122 | repo: goggz/ggz-server 123 | tags: linux-amd64 124 | when: 125 | event: 126 | - pull_request 127 | 128 | - name: publish 129 | pull: always 130 | image: plugins/docker:linux-amd64 131 | settings: 132 | auto_tag: true 133 | auto_tag_suffix: linux-amd64 134 | cache_from: goggz/ggz-server 135 | daemon_off: false 136 | dockerfile: docker/ggz-server/Dockerfile.linux.amd64 137 | password: 138 | from_secret: docker_password 139 | repo: goggz/ggz-server 140 | username: 141 | from_secret: docker_username 142 | when: 143 | event: 144 | exclude: 145 | - pull_request 146 | 147 | trigger: 148 | ref: 149 | - refs/heads/master 150 | - refs/pull/** 151 | - refs/tags/** 152 | 153 | depends_on: 154 | - testing 155 | 156 | --- 157 | kind: pipeline 158 | name: ggz-server-linux-arm64 159 | 160 | platform: 161 | os: linux 162 | arch: arm64 163 | 164 | steps: 165 | - name: build-push 166 | pull: always 167 | image: golang:1.13 168 | commands: 169 | - make generate 170 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_COMMIT_SHA:0:8} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/arm64/ggz-server ./cmd/ggz-server 171 | environment: 172 | CGO_ENABLED: 1 173 | when: 174 | event: 175 | exclude: 176 | - tag 177 | 178 | - name: build-tag 179 | pull: always 180 | image: golang:1.13 181 | commands: 182 | - make generate 183 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_TAG##v} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/arm64/ggz-server ./cmd/ggz-server 184 | environment: 185 | CGO_ENABLED: 1 186 | when: 187 | event: 188 | - tag 189 | 190 | - name: executable 191 | pull: always 192 | image: golang:1.13 193 | commands: 194 | - ./release/linux/arm64/ggz-server --help 195 | 196 | - name: dryrun 197 | pull: always 198 | image: plugins/docker:linux-arm64 199 | settings: 200 | cache_from: goggz/ggz-server 201 | dockerfile: docker/ggz-server/Dockerfile.linux.arm64 202 | dry_run: true 203 | repo: goggz/ggz-server 204 | tags: linux-arm64 205 | when: 206 | event: 207 | - pull_request 208 | 209 | - name: publish 210 | pull: always 211 | image: plugins/docker:linux-arm64 212 | settings: 213 | auto_tag: true 214 | auto_tag_suffix: linux-arm64 215 | cache_from: goggz/ggz-server 216 | daemon_off: false 217 | dockerfile: docker/ggz-server/Dockerfile.linux.arm64 218 | password: 219 | from_secret: docker_password 220 | repo: goggz/ggz-server 221 | username: 222 | from_secret: docker_username 223 | when: 224 | event: 225 | exclude: 226 | - pull_request 227 | 228 | trigger: 229 | ref: 230 | - refs/heads/master 231 | - refs/pull/** 232 | - refs/tags/** 233 | 234 | depends_on: 235 | - testing 236 | 237 | --- 238 | kind: pipeline 239 | name: ggz-server-linux-arm 240 | 241 | platform: 242 | os: linux 243 | arch: arm 244 | 245 | steps: 246 | - name: build-push 247 | pull: always 248 | image: golang:1.13 249 | commands: 250 | - make generate 251 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_COMMIT_SHA:0:8} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/arm/ggz-server ./cmd/ggz-server 252 | environment: 253 | CGO_ENABLED: 1 254 | when: 255 | event: 256 | exclude: 257 | - tag 258 | 259 | - name: build-tag 260 | pull: always 261 | image: golang:1.13 262 | commands: 263 | - make generate 264 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_TAG##v} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/arm/ggz-server ./cmd/ggz-server 265 | environment: 266 | CGO_ENABLED: 1 267 | when: 268 | event: 269 | - tag 270 | 271 | - name: executable 272 | pull: always 273 | image: golang:1.13 274 | commands: 275 | - ./release/linux/arm/ggz-server --help 276 | 277 | - name: dryrun 278 | pull: always 279 | image: plugins/docker:linux-arm 280 | settings: 281 | cache_from: goggz/ggz-server 282 | dockerfile: docker/ggz-server/Dockerfile.linux.arm 283 | dry_run: true 284 | repo: goggz/ggz-server 285 | tags: linux-arm 286 | when: 287 | event: 288 | - pull_request 289 | 290 | - name: publish 291 | pull: always 292 | image: plugins/docker:linux-arm 293 | settings: 294 | auto_tag: true 295 | auto_tag_suffix: linux-arm 296 | cache_from: goggz/ggz-server 297 | daemon_off: false 298 | dockerfile: docker/ggz-server/Dockerfile.linux.arm 299 | password: 300 | from_secret: docker_password 301 | repo: goggz/ggz-server 302 | username: 303 | from_secret: docker_username 304 | when: 305 | event: 306 | exclude: 307 | - pull_request 308 | 309 | trigger: 310 | ref: 311 | - refs/heads/master 312 | - refs/pull/** 313 | - refs/tags/** 314 | 315 | depends_on: 316 | - testing 317 | 318 | --- 319 | kind: pipeline 320 | name: ggz-redirect-linux-amd64 321 | 322 | platform: 323 | os: linux 324 | arch: amd64 325 | 326 | steps: 327 | - name: build-push 328 | pull: always 329 | image: golang:1.13 330 | commands: 331 | - make generate 332 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_COMMIT_SHA:0:8} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/amd64/ggz-redirect ./cmd/ggz-redirect 333 | environment: 334 | CGO_ENABLED: 1 335 | when: 336 | event: 337 | exclude: 338 | - tag 339 | 340 | - name: build-tag 341 | pull: always 342 | image: golang:1.13 343 | commands: 344 | - make generate 345 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_TAG##v} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/amd64/ggz-redirect ./cmd/ggz-redirect 346 | environment: 347 | CGO_ENABLED: 1 348 | when: 349 | event: 350 | - tag 351 | 352 | - name: executable 353 | pull: always 354 | image: golang:1.13 355 | commands: 356 | - ./release/linux/amd64/ggz-redirect --help 357 | 358 | - name: dryrun 359 | pull: always 360 | image: plugins/docker:linux-amd64 361 | settings: 362 | cache_from: goggz/ggz-redirect 363 | dockerfile: docker/ggz-redirect/Dockerfile.linux.amd64 364 | dry_run: true 365 | repo: goggz/ggz-redirect 366 | tags: linux-amd64 367 | when: 368 | event: 369 | - pull_request 370 | 371 | - name: publish 372 | pull: always 373 | image: plugins/docker:linux-amd64 374 | settings: 375 | auto_tag: true 376 | auto_tag_suffix: linux-amd64 377 | cache_from: goggz/ggz-redirect 378 | daemon_off: false 379 | dockerfile: docker/ggz-redirect/Dockerfile.linux.amd64 380 | password: 381 | from_secret: docker_password 382 | repo: goggz/ggz-redirect 383 | username: 384 | from_secret: docker_username 385 | when: 386 | event: 387 | exclude: 388 | - pull_request 389 | 390 | trigger: 391 | ref: 392 | - refs/heads/master 393 | - refs/pull/** 394 | - refs/tags/** 395 | 396 | depends_on: 397 | - testing 398 | 399 | --- 400 | kind: pipeline 401 | name: ggz-redirect-linux-arm64 402 | 403 | platform: 404 | os: linux 405 | arch: arm64 406 | 407 | steps: 408 | - name: build-push 409 | pull: always 410 | image: golang:1.13 411 | commands: 412 | - make generate 413 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_COMMIT_SHA:0:8} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/arm64/ggz-redirect ./cmd/ggz-redirect 414 | environment: 415 | CGO_ENABLED: 1 416 | when: 417 | event: 418 | exclude: 419 | - tag 420 | 421 | - name: build-tag 422 | pull: always 423 | image: golang:1.13 424 | commands: 425 | - make generate 426 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_TAG##v} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/arm64/ggz-redirect ./cmd/ggz-redirect 427 | environment: 428 | CGO_ENABLED: 1 429 | when: 430 | event: 431 | - tag 432 | 433 | - name: executable 434 | pull: always 435 | image: golang:1.13 436 | commands: 437 | - ./release/linux/arm64/ggz-redirect --help 438 | 439 | - name: dryrun 440 | pull: always 441 | image: plugins/docker:linux-arm64 442 | settings: 443 | cache_from: goggz/ggz-redirect 444 | dockerfile: docker/ggz-redirect/Dockerfile.linux.arm64 445 | dry_run: true 446 | repo: goggz/ggz-redirect 447 | tags: linux-arm64 448 | when: 449 | event: 450 | - pull_request 451 | 452 | - name: publish 453 | pull: always 454 | image: plugins/docker:linux-arm64 455 | settings: 456 | auto_tag: true 457 | auto_tag_suffix: linux-arm64 458 | cache_from: goggz/ggz-redirect 459 | daemon_off: false 460 | dockerfile: docker/ggz-redirect/Dockerfile.linux.arm64 461 | password: 462 | from_secret: docker_password 463 | repo: goggz/ggz-redirect 464 | username: 465 | from_secret: docker_username 466 | when: 467 | event: 468 | exclude: 469 | - pull_request 470 | 471 | trigger: 472 | ref: 473 | - refs/heads/master 474 | - refs/pull/** 475 | - refs/tags/** 476 | 477 | depends_on: 478 | - testing 479 | 480 | --- 481 | kind: pipeline 482 | name: ggz-redirect-linux-arm 483 | 484 | platform: 485 | os: linux 486 | arch: arm 487 | 488 | steps: 489 | - name: build-push 490 | pull: always 491 | image: golang:1.13 492 | commands: 493 | - make generate 494 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_COMMIT_SHA:0:8} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/arm/ggz-redirect ./cmd/ggz-redirect 495 | environment: 496 | CGO_ENABLED: 1 497 | when: 498 | event: 499 | exclude: 500 | - tag 501 | 502 | - name: build-tag 503 | pull: always 504 | image: golang:1.13 505 | commands: 506 | - make generate 507 | - go build -v -tags 'sqlite sqlite_unlock_notify' -ldflags "-extldflags -static -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_TAG##v} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/linux/arm/ggz-redirect ./cmd/ggz-redirect 508 | environment: 509 | CGO_ENABLED: 1 510 | when: 511 | event: 512 | - tag 513 | 514 | - name: executable 515 | pull: always 516 | image: golang:1.13 517 | commands: 518 | - ./release/linux/arm/ggz-redirect --help 519 | 520 | - name: dryrun 521 | pull: always 522 | image: plugins/docker:linux-arm 523 | settings: 524 | cache_from: goggz/ggz-redirect 525 | dockerfile: docker/ggz-redirect/Dockerfile.linux.arm 526 | dry_run: true 527 | repo: goggz/ggz-redirect 528 | tags: linux-arm 529 | when: 530 | event: 531 | - pull_request 532 | 533 | - name: publish 534 | pull: always 535 | image: plugins/docker:linux-arm 536 | settings: 537 | auto_tag: true 538 | auto_tag_suffix: linux-arm 539 | cache_from: goggz/ggz-redirect 540 | daemon_off: false 541 | dockerfile: docker/ggz-redirect/Dockerfile.linux.arm 542 | password: 543 | from_secret: docker_password 544 | repo: goggz/ggz-redirect 545 | username: 546 | from_secret: docker_username 547 | when: 548 | event: 549 | exclude: 550 | - pull_request 551 | 552 | trigger: 553 | ref: 554 | - refs/heads/master 555 | - refs/pull/** 556 | - refs/tags/** 557 | 558 | depends_on: 559 | - testing 560 | 561 | --- 562 | kind: pipeline 563 | name: release-binary 564 | 565 | platform: 566 | os: linux 567 | arch: amd64 568 | 569 | steps: 570 | - name: generate 571 | pull: always 572 | image: golang:1.13 573 | commands: 574 | - make generate 575 | volumes: 576 | - name: gopath 577 | path: /go 578 | 579 | - name: build-all-binary 580 | pull: always 581 | image: golang:1.13 582 | commands: 583 | - make release 584 | volumes: 585 | - name: gopath 586 | path: /go 587 | when: 588 | event: 589 | - tag 590 | 591 | - name: deploy-all-binary 592 | pull: always 593 | image: plugins/github-release 594 | settings: 595 | api_key: 596 | from_secret: github_release_api_key 597 | files: 598 | - dist/release/* 599 | when: 600 | event: 601 | - tag 602 | 603 | trigger: 604 | ref: 605 | - refs/tags/** 606 | 607 | depends_on: 608 | - testing 609 | 610 | --- 611 | kind: pipeline 612 | name: ggz-server-notifications 613 | 614 | platform: 615 | os: linux 616 | arch: amd64 617 | 618 | steps: 619 | - name: manifest 620 | pull: always 621 | image: plugins/manifest 622 | settings: 623 | ignore_missing: true 624 | password: 625 | from_secret: docker_password 626 | spec: docker/ggz-server/manifest.tmpl 627 | username: 628 | from_secret: docker_username 629 | 630 | trigger: 631 | ref: 632 | - refs/heads/master 633 | - refs/tags/** 634 | 635 | depends_on: 636 | - ggz-server-linux-amd64 637 | - ggz-server-linux-arm64 638 | - ggz-server-linux-arm 639 | - release-binary 640 | 641 | --- 642 | kind: pipeline 643 | name: ggz-redirect-notifications 644 | 645 | platform: 646 | os: linux 647 | arch: amd64 648 | 649 | steps: 650 | - name: manifest 651 | pull: always 652 | image: plugins/manifest 653 | settings: 654 | ignore_missing: true 655 | password: 656 | from_secret: docker_password 657 | spec: docker/ggz-redirect/manifest.tmpl 658 | username: 659 | from_secret: docker_username 660 | 661 | trigger: 662 | ref: 663 | - refs/heads/master 664 | - refs/tags/** 665 | 666 | depends_on: 667 | - ggz-redirect-linux-amd64 668 | - ggz-redirect-linux-arm64 669 | - ggz-redirect-linux-arm 670 | - release-binary 671 | 672 | ... 673 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GGZ_DB_DRIVER=mysql 2 | GGZ_DB_USERNAME=root 3 | GGZ_DB_PASSWORD=123456 4 | GGZ_DB_NAME=ggz 5 | GGZ_DB_HOST=127.0.0.1:3307 6 | GGZ_SERVER_ADDR=:8080 7 | GGZ_SHORTEN_SERVER_ADDR=:8081 8 | GGZ_DEBUG=true 9 | GGZ_SERVER_HOST=http://localhost:8080 10 | GGZ_SERVER_SHORTEN_HOST=http://localhost:8081 11 | GGZ_STORAGE_DRIVER=disk 12 | GGZ_MINIO_ACCESS_ID=xxxxxxxx 13 | GGZ_MINIO_SECRET_KEY=xxxxxxxx 14 | GGZ_MINIO_ENDPOINT=s3.example.com 15 | GGZ_MINIO_BUCKET=qrcode 16 | GGZ_MINIO_SSL=true 17 | GGZ_AUTH0_PEM_PATH=pem/dev.pem 18 | GGZ_AUTH0_DEBUG=true 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | yarn-error.log 13 | .yarn-cache 14 | .env 15 | .assets 16 | bin 17 | *.tar.gz 18 | rev 19 | data 20 | storage 21 | !pkg/module/storage 22 | .cover 23 | release/ 24 | coverage.txt 25 | assets/ab0x.go 26 | assets/dist/firebase/serviceAccountKey.json 27 | vendor 28 | dist 29 | -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "schemaPath": "schema.graphql", 3 | "extensions": { 4 | "endpoints": { 5 | "dev": { 6 | "url": "http://localhost:8080/graphql" 7 | }, 8 | "prod": { 9 | "url": "https://api.ggz.tw/graphql" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 1 5 | warningCode = 1 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.package-comments] 20 | [rule.range] 21 | [rule.receiver-naming] 22 | [rule.time-naming] 23 | [rule.unexported-return] 24 | [rule.indent-error-flow] 25 | [rule.errorf] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) <2019> 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DIST := dist 2 | SERVICE ?= ggz-server 3 | 4 | DOCKER_ACCOUNT := goggz 5 | DOCKER_IMAGE := $(SERVICE) 6 | GOFMT ?= gofmt "-s" 7 | SHASUM ?= shasum -a 256 8 | GO ?= go 9 | TARGETS ?= linux darwin windows 10 | ARCHS ?= amd64 386 11 | BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") 12 | PACKAGES ?= $(shell $(GO) list ./... | grep -v integrations) 13 | GOFILES := $(shell find . -name "*.go" -type f) 14 | TAGS ?= sqlite sqlite_unlock_notify json1 15 | 16 | ifneq ($(shell uname), Darwin) 17 | EXTLDFLAGS = -extldflags "-static" $(null) 18 | else 19 | EXTLDFLAGS = 20 | endif 21 | 22 | ifneq ($(DRONE_TAG),) 23 | VERSION ?= $(subst v,,$(DRONE_TAG)) 24 | else 25 | VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//') 26 | endif 27 | 28 | LDFLAGS ?= -X github.com/go-ggz/ggz/pkg/version.Version=$(VERSION) -X github.com/go-ggz/ggz/pkg/version.BuildDate=$(BUILD_DATE) 29 | 30 | all: build 31 | 32 | .PHONY: tar 33 | tar: 34 | tar -zcvf release.tar.gz bin Dockerfile Makefile 35 | 36 | .PHONY: check_image 37 | check_image: 38 | if [ "$(shell docker ps -aq -f name=$(SERVICE))" ]; then \ 39 | docker rm -f $(SERVICE); \ 40 | fi 41 | 42 | .PHONY: dev 43 | dev: build_image check_image 44 | docker run -d --name $(DOCKER_IMAGE) --env-file env/env.$@ --net host -p 3003:3003 --restart always $(DOCKER_ACCOUNT)/$(DOCKER_IMAGE) 45 | 46 | .PHONY: prod 47 | prod: build_image check_image 48 | docker run -d --name $(DOCKER_IMAGE) --env-file env/env.$@ --net host -p 3003:3003 --restart always $(DOCKER_ACCOUNT)/$(DOCKER_IMAGE) 49 | 50 | .PHONY: generate 51 | generate: 52 | @which fileb0x > /dev/null; if [ $$? -ne 0 ]; then \ 53 | $(GO) get -u github.com/UnnoTed/fileb0x; \ 54 | fi 55 | $(GO) generate $(PACKAGES) 56 | 57 | .PHONY: vendor 58 | vendor: 59 | GO111MODULE=on $(GO) mod tidy && GO111MODULE=on $(GO) mod vendor 60 | 61 | .PHONY: fmt 62 | fmt: 63 | $(GOFMT) -w $(GOFILES) 64 | 65 | .PHONY: fmt-check 66 | fmt-check: 67 | @diff=$$($(GOFMT) -d $(GOFILES)); \ 68 | if [ -n "$$diff" ]; then \ 69 | echo "Please run 'make fmt' and commit the result:"; \ 70 | echo "$${diff}"; \ 71 | exit 1; \ 72 | fi; 73 | 74 | embedmd: 75 | @hash embedmd > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 76 | $(GO) get -u github.com/campoy/embedmd; \ 77 | fi 78 | embedmd -d *.md 79 | 80 | vet: 81 | $(GO) vet $(PACKAGES) 82 | 83 | .PHONY: lint 84 | lint: 85 | @hash revive > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 86 | $(GO) get -u github.com/mgechev/revive; \ 87 | fi 88 | revive -config .revive.toml ./... || exit 1 89 | 90 | .PHONY: golangci-lint 91 | golangci-lint: 92 | @hash golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 93 | export BINARY="golangci-lint"; \ 94 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.18.0; \ 95 | fi 96 | golangci-lint run --deadline=3m 97 | 98 | install: $(GOFILES) 99 | $(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' 100 | 101 | build: $(SERVICE) 102 | 103 | $(SERVICE): $(GOFILES) 104 | $(GO) build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o bin/$@ ./cmd/$(SERVICE) 105 | 106 | .PHONY: misspell-check 107 | misspell-check: 108 | @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 109 | $(GO) get -u github.com/client9/misspell/cmd/misspell; \ 110 | fi 111 | misspell -error $(GOFILES) 112 | 113 | .PHONY: misspell 114 | misspell: 115 | @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 116 | $(GO) get -u github.com/client9/misspell/cmd/misspell; \ 117 | fi 118 | misspell -w $(GOFILES) 119 | 120 | upx: 121 | @hash upx > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 122 | echo "Missing upx command"; \ 123 | exit 1; \ 124 | fi 125 | upx -o bin/$(SERVICE)-small bin/$(SERVICE) 126 | mv bin/$(SERVICE)-small bin/$(SERVICE) 127 | 128 | .PHONY: test 129 | test: fmt-check 130 | @$(GO) test -v -cover -tags '$(TAGS)' -coverprofile coverage.txt $(PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1 131 | 132 | release: release-dirs release-build release-copy release-compress release-check 133 | 134 | release-dirs: 135 | mkdir -p $(DIST)/binaries $(DIST)/release 136 | 137 | release-build: 138 | @hash gox > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 139 | $(GO) get -u github.com/mitchellh/gox; \ 140 | fi 141 | gox -os="$(TARGETS)" -arch="$(ARCHS)" -tags="$(TAGS)" -ldflags="$(EXTLDFLAGS)-s -w $(LDFLAGS)" -output="$(DIST)/binaries/$(SERVICE)-$(VERSION)-{{.OS}}-{{.Arch}}" ./cmd/$(SERVICE)/... 142 | 143 | .PHONY: release-compress 144 | release-compress: 145 | @hash gxz > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 146 | $(GO) get -u github.com/ulikunitz/xz/cmd/gxz; \ 147 | fi 148 | cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && gxz -k -9 $${file}; done; 149 | 150 | release-copy: 151 | $(foreach file,$(wildcard $(DIST)/binaries/$(SERVICE)-*),cp $(file) $(DIST)/release/$(notdir $(file));) 152 | 153 | release-check: 154 | cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "checksumming $${file}" && $(SHASUM) `echo $${file} | sed 's/^..//'` > $${file}.sha256; done; 155 | 156 | build_linux_amd64: 157 | GOOS=linux GOARCH=amd64 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/amd64/$(DOCKER_IMAGE) ./cmd/$(SERVICE) 158 | 159 | build_linux_i386: 160 | GOOS=linux GOARCH=386 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/i386/$(DOCKER_IMAGE) ./cmd/$(SERVICE) 161 | 162 | build_linux_arm64: 163 | GOOS=linux GOARCH=arm64 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm64/$(DOCKER_IMAGE) ./cmd/$(SERVICE) 164 | 165 | build_linux_arm: 166 | GOOS=linux GOARCH=arm GOARM=7 $(GO) build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o release/linux/arm/$(DOCKER_IMAGE) ./cmd/$(SERVICE) 167 | 168 | build_image: 169 | docker build -t $(DOCKER_ACCOUNT)/$(DOCKER_IMAGE) -f Dockerfile . 170 | 171 | docker_release: build_image 172 | 173 | clean_dist: 174 | rm -rf bin release assets/ab0x.go 175 | 176 | clean: clean_dist 177 | $(GO) clean -modcache -cache -x -i ./... 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ggz 2 | 3 | [![GoDoc](https://godoc.org/github.com/go-ggz/ggz?status.svg)](https://godoc.org/github.com/go-ggz/ggz) 4 | [![Build Status](https://cloud.drone.io/api/badges/go-ggz/ggz/status.svg)](https://cloud.drone.io/go-ggz/ggz) 5 | [![Build status](https://ci.appveyor.com/api/projects/status/prjvsklt3io5nuhn/branch/master?svg=true)](https://ci.appveyor.com/project/appleboy/ggz/branch/master) 6 | [![codecov](https://codecov.io/gh/go-ggz/ggz/branch/master/graph/badge.svg)](https://codecov.io/gh/go-ggz/ggz) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-ggz/ggz)](https://goreportcard.com/report/github.com/go-ggz/ggz) 8 | [![codebeat badge](https://codebeat.co/badges/6fc8d61a-17c1-446d-a895-2dc6a8d1c16c)](https://codebeat.co/projects/github-com-go-ggz-ggz-master) 9 | [![Docker Pulls](https://img.shields.io/docker/pulls/goggz/ggz-server.svg)](https://hub.docker.com/r/goggz/ggz-server/) 10 | [![Get your own image badge on microbadger.com](https://images.microbadger.com/badges/image/goggz/ggz-server.svg)](https://microbadger.com/images/goggz/ggz-server "Get your own image badge on microbadger.com") 11 | 12 | An URL shortener service written in Golang. 13 | 14 | ## Features 15 | 16 | * Support [MySQL](https://www.mysql.com/), [Postgres](https://www.postgresql.org/) or [SQLite](https://www.sqlite.org/) Database. 17 | * Support [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) or [GraphQL](http://graphql.org/) API. 18 | * Support [Auth0](https://auth0.com/) or [Firebase](https://firebase.google.com/) Single Sign On (default is `auth0`). 19 | * Support expose [prometheus](https://prometheus.io/) metrics and database data like count of registerd users. 20 | * Support install TLS certificates from [Let's Encrypt](https://letsencrypt.org/) automatically. 21 | * Support [QR Code](https://en.wikipedia.org/wiki/QR_code) Generator from shorten URL. 22 | * Support local disk storage or [Minio Object Storage](https://minio.io/). 23 | * Support linux and windows container, see [Docker Hub](https://hub.docker.com/r/goggz/ggz/tags/). 24 | * Support integrate with [Grafana](https://grafana.com/) service. 25 | 26 | ## Requirement 27 | 28 | Go version: `1.13` 29 | 30 | ## Start app using docker-compose 31 | 32 | See the `docker-compose.yml` 33 | 34 | ```yml 35 | version: '3' 36 | 37 | services: 38 | ggz: 39 | image: goggz/ggz 40 | restart: always 41 | ports: 42 | - 8080:8080 43 | - 8081:8081 44 | environment: 45 | - GGZ_DB_DRIVER=sqlite3 46 | - GGZ_SERVER_HOST=http://localhost:8080 47 | - GGZ_SERVER_SHORTEN_HOST=http://localhost:8081 48 | - GGZ_AUTH0_PEM_PATH=test.pem 49 | ``` 50 | 51 | ## Stargazers over time 52 | 53 | [![Stargazers over time](https://starcharts.herokuapp.com/go-ggz/ggz.svg)](https://starcharts.herokuapp.com/go-ggz/ggz) 54 | -------------------------------------------------------------------------------- /api/404.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // NotFound represents the 404 page. 10 | func NotFound(c *gin.Context) { 11 | c.JSON( 12 | http.StatusNotFound, 13 | gin.H{ 14 | "code": http.StatusNotFound, 15 | "error": "PAGE NOT FOUND", 16 | }, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /api/common.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/rs/zerolog/log" 6 | ) 7 | 8 | func errorJSON(c *gin.Context, code int, err InnError) { 9 | log.Error().Err(err).Msg("json error") 10 | 11 | c.AbortWithStatusJSON( 12 | code, 13 | gin.H{ 14 | "code": err.Code, 15 | "error": err.Error(), 16 | }, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /api/errors.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var ( 8 | errBadRequest = InnError{Code: 101, Message: "Bad Input Request"} 9 | errSlugNotMatch = InnError{Code: 102, Message: "Slug Not Match"} 10 | errSlugNotFound = InnError{Code: 103, Message: "Slug Not Found"} 11 | errNotLogin = InnError{Code: 104, Message: "user not login"} 12 | 13 | // Internal Server Error 14 | errInternalServer = InnError{Code: 500, Message: "Internal Server Error"} 15 | ) 16 | 17 | // InnError is an error implementation that includes a time and message. 18 | type InnError struct { 19 | Code int 20 | Message string 21 | } 22 | 23 | func (e InnError) Error() string { 24 | return fmt.Sprintf("Error Code: %d, Error Message: %s", e.Code, e.Message) 25 | } 26 | 27 | // IsInnError is check error type 28 | func IsInnError(err error) bool { 29 | _, ok := err.(InnError) 30 | return ok 31 | } 32 | -------------------------------------------------------------------------------- /api/favicon.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/go-ggz/ui/dist" 11 | ) 12 | 13 | // Favicon represents the favicon. 14 | func Favicon(c *gin.Context) { 15 | file, _ := dist.ReadFile("favicon.ico") 16 | etag := fmt.Sprintf("%x", md5.Sum(file)) 17 | c.Header("ETag", etag) 18 | c.Header("Cache-Control", "max-age=0") 19 | 20 | if match := c.GetHeader("If-None-Match"); match != "" { 21 | if strings.Contains(match, etag) { 22 | c.Status(http.StatusNotModified) 23 | return 24 | } 25 | } 26 | 27 | c.Data( 28 | http.StatusOK, 29 | "image/x-icon", 30 | file, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /api/healthz.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // Heartbeat for check server status 10 | func Heartbeat(c *gin.Context) { 11 | c.AbortWithStatus(http.StatusOK) 12 | c.String(http.StatusOK, "ok") 13 | } 14 | -------------------------------------------------------------------------------- /api/index.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/go-ggz/ui/dist" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | // Index represents the index page. 15 | func Index(c *gin.Context) { 16 | file, _ := dist.ReadFile("index.html") 17 | etag := fmt.Sprintf("%x", md5.Sum(file)) 18 | c.Header("ETag", etag) 19 | c.Header("Cache-Control", "no-cache") 20 | 21 | if match := c.GetHeader("If-None-Match"); match != "" { 22 | if strings.Contains(match, etag) { 23 | c.Status(http.StatusNotModified) 24 | return 25 | } 26 | } 27 | 28 | c.Data(http.StatusOK, "text/html; charset=utf-8", file) 29 | } 30 | -------------------------------------------------------------------------------- /api/shorten.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "regexp" 6 | 7 | "github.com/go-ggz/ggz/pkg/config" 8 | "github.com/go-ggz/ggz/pkg/helper" 9 | "github.com/go-ggz/ggz/pkg/model" 10 | "github.com/go-ggz/ggz/pkg/router" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/rs/zerolog/log" 14 | ) 15 | 16 | var shortenPattern = regexp.MustCompile(`^[a-zA-Z0-9]+$`) 17 | 18 | // ShortenedIndex index page. 19 | func ShortenedIndex(c *gin.Context) { 20 | c.Redirect(http.StatusMovedPermanently, config.Server.Host) 21 | } 22 | 23 | // FormURL URL Struct 24 | type FormURL struct { 25 | URL string `json:"url,omitempty" binding:"required,url"` 26 | } 27 | 28 | // CreateShortenURL create shorten url 29 | func CreateShortenURL(c *gin.Context) { 30 | var data FormURL 31 | if err := c.ShouldBindJSON(&data); err != nil { 32 | errorJSON(c, http.StatusBadRequest, errBadRequest) 33 | return 34 | } 35 | 36 | row, err := model.GetShortenFromURL(data.URL) 37 | 38 | if model.IsErrURLExist(err) { 39 | c.JSON( 40 | http.StatusOK, 41 | row, 42 | ) 43 | return 44 | } 45 | 46 | if err != nil { 47 | errorJSON(c, http.StatusInternalServerError, errInternalServer) 48 | return 49 | } 50 | 51 | user := helper.GetUserDataFromModel(c.Request.Context()) 52 | 53 | if user == nil { 54 | errorJSON(c, http.StatusUnauthorized, errNotLogin) 55 | return 56 | } 57 | 58 | row, err = model.CreateShorten(data.URL, config.Server.ShortenSize, user) 59 | 60 | if err != nil { 61 | errorJSON(c, http.StatusInternalServerError, errInternalServer) 62 | return 63 | } 64 | 65 | // upload QRCode image. 66 | go func(slug string) { 67 | if err := helper.QRCodeGenerator(slug); err != nil { 68 | log.Error().Err(err).Msg("QRCode Generator fail") 69 | } 70 | }(row.Slug) 71 | 72 | c.JSON( 73 | http.StatusOK, 74 | row, 75 | ) 76 | } 77 | 78 | // FetchShortenedURL show URL content 79 | func FetchShortenedURL(c *gin.Context) { 80 | slug := c.Param("slug") 81 | 82 | if !shortenPattern.MatchString(slug) { 83 | errorJSON(c, http.StatusBadRequest, errSlugNotMatch) 84 | return 85 | } 86 | 87 | row, err := model.GetShortenBySlug(slug) 88 | if err != nil { 89 | errorJSON(c, http.StatusInternalServerError, errInternalServer) 90 | return 91 | } 92 | 93 | if model.IsErrShortenNotExist(err) { 94 | errorJSON(c, http.StatusNotFound, errSlugNotFound) 95 | return 96 | } 97 | 98 | c.JSON( 99 | http.StatusOK, 100 | row, 101 | ) 102 | } 103 | 104 | // RedirectURL redirect shorten text to origin URL. 105 | func RedirectURL(c *gin.Context) { 106 | slug := c.Param("slug") 107 | 108 | if !shortenPattern.MatchString(slug) { 109 | errorJSON(c, http.StatusBadRequest, errSlugNotMatch) 110 | return 111 | } 112 | 113 | if slug == "healthz" { 114 | Heartbeat(c) 115 | return 116 | } else if slug == "metrics" { 117 | if config.Metrics.Enabled { 118 | router.Metrics(config.Metrics.Token)(c) 119 | } 120 | return 121 | } 122 | 123 | row, err := model.GetShortenBySlug(slug) 124 | if err != nil { 125 | errorJSON(c, http.StatusInternalServerError, errInternalServer) 126 | return 127 | } 128 | 129 | if model.IsErrShortenNotExist(err) { 130 | errorJSON(c, http.StatusNotFound, errSlugNotFound) 131 | return 132 | } 133 | 134 | err = row.UpdateHits(slug) 135 | if err != nil { 136 | errorJSON(c, http.StatusNotFound, errInternalServer) 137 | return 138 | } 139 | 140 | c.Redirect(http.StatusMovedPermanently, row.URL) 141 | } 142 | -------------------------------------------------------------------------------- /api/url.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-ggz/ggz/pkg/module/meta" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | // URLMeta for fetch metadata from URL 13 | func URLMeta(c *gin.Context) { 14 | var data FormURL 15 | if err := c.ShouldBindJSON(&data); err != nil { 16 | errorJSON(c, http.StatusBadRequest, errBadRequest) 17 | return 18 | } 19 | 20 | metaData, err := meta.FetchData(data.URL) 21 | log.Info().Msgf("%#v", metaData) 22 | 23 | if err != nil { 24 | errorJSON(c, http.StatusInternalServerError, errInternalServer) 25 | return 26 | } 27 | 28 | c.JSON( 29 | http.StatusOK, 30 | metaData, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /assets/ab0x.yaml: -------------------------------------------------------------------------------- 1 | pkg: "assets" 2 | dest: "." 3 | fmt: true 4 | 5 | compression: 6 | compress: true 7 | 8 | custom: 9 | - files: 10 | - "./dist/" 11 | base: "dist" 12 | prefix: "" 13 | exclude: 14 | - "*/firebase/.gitkeep" 15 | -------------------------------------------------------------------------------- /assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "path" 10 | "strings" 11 | "time" 12 | 13 | "github.com/go-ggz/ggz/pkg/config" 14 | "github.com/go-ggz/ui/dist" 15 | 16 | "github.com/appleboy/com/file" 17 | "github.com/gin-gonic/gin" 18 | "github.com/rs/zerolog/log" 19 | ) 20 | 21 | var fileServer = http.FileServer(dist.HTTP) 22 | 23 | // Load initializes the static files. 24 | func Load() http.FileSystem { 25 | return ChainedFS{} 26 | } 27 | 28 | // ChainedFS is a simple HTTP filesystem including custom path. 29 | type ChainedFS struct { 30 | } 31 | 32 | // Open just implements the HTTP filesystem interface. 33 | func (c ChainedFS) Open(origPath string) (http.File, error) { 34 | if config.Server.Assets != "" { 35 | if file.IsDir(config.Server.Assets) { 36 | customPath := path.Join(config.Server.Assets, origPath) 37 | 38 | if file.IsFile(customPath) { 39 | f, err := os.Open(customPath) 40 | 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return f, nil 46 | } 47 | } else { 48 | log.Warn().Msg("Custom assets directory doesn't exist") 49 | } 50 | } 51 | 52 | f, err := dist.FS.OpenFile(dist.CTX, origPath, os.O_RDONLY, 0644) 53 | 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return f, nil 59 | } 60 | 61 | // ReadSource is adapTed from ioutil 62 | func ReadSource(origPath string) (content []byte, err error) { 63 | content, err = ReadFile(origPath) 64 | 65 | if err != nil { 66 | log.Warn().Err(err).Msgf("Failed to read builtin %s file.", origPath) 67 | } 68 | 69 | if config.Server.Assets != "" && file.IsDir(config.Server.Assets) { 70 | origPath = path.Join( 71 | config.Server.Assets, 72 | origPath, 73 | ) 74 | 75 | if file.IsFile(origPath) { 76 | content, err = ioutil.ReadFile(origPath) 77 | 78 | if err != nil { 79 | log.Warn().Err(err).Msgf("Failed to read custom %s file", origPath) 80 | } 81 | } 82 | } 83 | 84 | return content, err 85 | } 86 | 87 | // ViewHandler support dist handler from UI 88 | func ViewHandler() gin.HandlerFunc { 89 | fileServer := http.FileServer(dist.HTTP) 90 | data := []byte(time.Now().String()) 91 | etag := fmt.Sprintf("%x", md5.Sum(data)) 92 | 93 | return func(c *gin.Context) { 94 | c.Header("Cache-Control", "public, max-age=31536000") 95 | c.Header("ETag", etag) 96 | 97 | if match := c.GetHeader("If-None-Match"); match != "" { 98 | if strings.Contains(match, etag) { 99 | c.Status(http.StatusNotModified) 100 | return 101 | } 102 | } 103 | 104 | fileServer.ServeHTTP(c.Writer, c.Request) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /assets/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-ggz/ggz/46af19ffdbe9b3d5316cfd4131eb3e0849297f17/assets/dist/favicon.ico -------------------------------------------------------------------------------- /assets/dist/firebase/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-ggz/ggz/46af19ffdbe9b3d5316cfd4131eb3e0849297f17/assets/dist/firebase/.gitkeep -------------------------------------------------------------------------------- /assets/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-present The GGZ Authors. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package assets 6 | 7 | //go:generate fileb0x ab0x.yaml 8 | -------------------------------------------------------------------------------- /cmd/ggz-redirect/health.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-ggz/ggz/pkg/config" 8 | 9 | "github.com/rs/zerolog/log" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | func healthAction() cli.ActionFunc { 14 | return func(c *cli.Context) error { 15 | resp, err := http.Get("http://localhost" + config.Server.Addr + "/healthz") 16 | if err != nil { 17 | log.Error(). 18 | Err(err). 19 | Msg("failed to request health check") 20 | return err 21 | } 22 | defer resp.Body.Close() 23 | if resp.StatusCode != http.StatusOK { 24 | log.Error(). 25 | Int("code", resp.StatusCode). 26 | Msg("health seems to be in bad state") 27 | return fmt.Errorf("server returned non-200 status code") 28 | } 29 | return nil 30 | } 31 | } 32 | 33 | func healthFlags() []cli.Flag { 34 | return []cli.Flag{ 35 | &cli.StringFlag{ 36 | Name: "addr", 37 | Value: defaultHostAddr, 38 | Usage: "Address to bind the server", 39 | EnvVars: []string{"GGZ_SERVER_ADDR"}, 40 | Destination: &config.Server.Addr, 41 | }, 42 | } 43 | } 44 | 45 | // Health provides the sub-command to perform a health check. 46 | func Health() *cli.Command { 47 | return &cli.Command{ 48 | Name: "health", 49 | Usage: "perform health checks", 50 | Flags: healthFlags(), 51 | Action: healthAction(), 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cmd/ggz-redirect/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "time" 7 | 8 | "github.com/go-ggz/ggz/pkg/config" 9 | "github.com/go-ggz/ggz/pkg/version" 10 | 11 | "github.com/joho/godotenv" 12 | _ "github.com/joho/godotenv/autoload" 13 | "github.com/rs/zerolog" 14 | "github.com/rs/zerolog/log" 15 | "github.com/urfave/cli/v2" 16 | ) 17 | 18 | func setupLogging() { 19 | switch strings.ToLower(config.Logs.Level) { 20 | case "panic": 21 | zerolog.SetGlobalLevel(zerolog.PanicLevel) 22 | case "fatal": 23 | zerolog.SetGlobalLevel(zerolog.FatalLevel) 24 | case "error": 25 | zerolog.SetGlobalLevel(zerolog.ErrorLevel) 26 | case "warn": 27 | zerolog.SetGlobalLevel(zerolog.WarnLevel) 28 | case "info": 29 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 30 | case "debug": 31 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 32 | default: 33 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 34 | } 35 | 36 | if config.Logs.Pretty { 37 | log.Logger = log.Output( 38 | zerolog.ConsoleWriter{ 39 | Out: os.Stderr, 40 | NoColor: !config.Logs.Color, 41 | }, 42 | ) 43 | } 44 | } 45 | 46 | func main() { 47 | if env := os.Getenv("GGZ_ENV_FILE"); env != "" { 48 | if err := godotenv.Load(env); err != nil { 49 | log.Fatal().Err(err).Msg("Cannot start load config from env") 50 | } 51 | } 52 | 53 | app := &cli.App{ 54 | Name: "gzz redirect", 55 | Usage: "redirect service", 56 | Copyright: "Copyright (c) 2018 Bo-Yi Wu", 57 | Version: version.PrintCLIVersion(), 58 | Compiled: time.Now(), 59 | Authors: []*cli.Author{ 60 | { 61 | Name: "Bo-Yi Wu", 62 | Email: "appleboy.tw@gmail.com", 63 | }, 64 | }, 65 | 66 | Flags: []cli.Flag{ 67 | &cli.BoolFlag{ 68 | Name: "debug", 69 | Value: true, 70 | Usage: "Activate debug information", 71 | EnvVars: []string{"GGZ_SERVER_DEBUG"}, 72 | Destination: &config.Server.Debug, 73 | }, 74 | &cli.BoolFlag{ 75 | Name: "log-color", 76 | Value: true, 77 | Usage: "enable colored logging", 78 | EnvVars: []string{"GGZ_LOGS_COLOR"}, 79 | Destination: &config.Logs.Color, 80 | }, 81 | &cli.BoolFlag{ 82 | Name: "log-pretty", 83 | Value: true, 84 | Usage: "enable pretty logging", 85 | EnvVars: []string{"GGZ_LOGS_PRETTY"}, 86 | Destination: &config.Logs.Pretty, 87 | }, 88 | &cli.StringFlag{ 89 | Name: "log-level", 90 | Value: "info", 91 | Usage: "set logging level", 92 | EnvVars: []string{"GGZ_LOGS_LEVEL"}, 93 | Destination: &config.Logs.Level, 94 | }, 95 | }, 96 | 97 | Before: func(c *cli.Context) error { 98 | setupLogging() 99 | 100 | return nil 101 | }, 102 | 103 | Commands: []*cli.Command{ 104 | Server(), 105 | Health(), 106 | }, 107 | } 108 | 109 | cli.HelpFlag = &cli.BoolFlag{ 110 | Name: "help", 111 | Aliases: []string{"h"}, 112 | Usage: "Show the help, so what you see now", 113 | } 114 | 115 | cli.VersionFlag = &cli.BoolFlag{ 116 | Name: "version", 117 | Aliases: []string{"v"}, 118 | Usage: "Print the current version of that tool", 119 | } 120 | 121 | if err := app.Run(os.Args); err != nil { 122 | os.Exit(1) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /cmd/ggz-redirect/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "os/signal" 11 | "path" 12 | "strings" 13 | "syscall" 14 | "time" 15 | 16 | "github.com/go-ggz/ggz/pkg/config" 17 | "github.com/go-ggz/ggz/pkg/router/routes" 18 | 19 | "github.com/rs/zerolog/log" 20 | "github.com/urfave/cli/v2" 21 | "golang.org/x/crypto/acme/autocert" 22 | "golang.org/x/sync/errgroup" 23 | ) 24 | 25 | var ( 26 | defaultHostAddr = ":8081" 27 | ) 28 | 29 | // Server provides the sub-command to start the API server. 30 | func Server() *cli.Command { 31 | return &cli.Command{ 32 | Name: "server", 33 | Usage: "Start the gzz service", 34 | Flags: []cli.Flag{ 35 | &cli.StringFlag{ 36 | Name: "assets", 37 | Value: "", 38 | Usage: "Path to custom assets and templates", 39 | EnvVars: []string{"GGZ_SERVER_ASSETS"}, 40 | Destination: &config.Server.Assets, 41 | }, 42 | &cli.StringFlag{ 43 | Name: "db-driver", 44 | Value: "sqlite3", 45 | Usage: "Database driver selection", 46 | EnvVars: []string{"GGZ_DB_DRIVER"}, 47 | Destination: &config.Database.Driver, 48 | }, 49 | &cli.StringFlag{ 50 | Name: "db-name", 51 | Value: "ggz", 52 | Usage: "Name for database connection", 53 | EnvVars: []string{"GGZ_DB_NAME"}, 54 | Destination: &config.Database.Name, 55 | }, 56 | &cli.StringFlag{ 57 | Name: "db-username", 58 | Value: "root", 59 | Usage: "Username for database connection", 60 | EnvVars: []string{"GGZ_DB_USERNAME"}, 61 | Destination: &config.Database.Username, 62 | }, 63 | &cli.StringFlag{ 64 | Name: "db-password", 65 | Value: "root", 66 | Usage: "Password for database connection", 67 | EnvVars: []string{"GGZ_DB_PASSWORD"}, 68 | Destination: &config.Database.Password, 69 | }, 70 | &cli.StringFlag{ 71 | Name: "db-host", 72 | Value: "localhost:3306", 73 | Usage: "Host for database connection", 74 | EnvVars: []string{"GGZ_DB_HOST"}, 75 | Destination: &config.Database.Host, 76 | }, 77 | &cli.StringFlag{ 78 | Name: "path", 79 | Value: "data/db/ggz.db", 80 | Usage: "sqlite path", 81 | EnvVars: []string{"GGZ_SQLITE_PATH"}, 82 | Destination: &config.Database.Path, 83 | }, 84 | &cli.StringFlag{ 85 | Name: "host", 86 | Value: "http://localhost:8080", 87 | Usage: "External access to server", 88 | EnvVars: []string{"GGZ_SERVER_HOST"}, 89 | Destination: &config.Server.Host, 90 | }, 91 | &cli.StringFlag{ 92 | Name: "addr", 93 | Value: defaultHostAddr, 94 | Usage: "Address to bind the server", 95 | EnvVars: []string{"GGZ_SERVER_ADDR"}, 96 | Destination: &config.Server.Addr, 97 | }, 98 | &cli.StringFlag{ 99 | Name: "root", 100 | Value: "/", 101 | Usage: "Root folder of the app", 102 | EnvVars: []string{"GGZ_SERVER_ROOT"}, 103 | Destination: &config.Server.Root, 104 | }, 105 | &cli.BoolFlag{ 106 | Name: "pprof", 107 | Value: false, 108 | Usage: "Enable pprof debugging server", 109 | EnvVars: []string{"GGZ_SERVER_PPROF"}, 110 | Destination: &config.Server.Pprof, 111 | }, 112 | &cli.StringFlag{ 113 | Name: "cert", 114 | Value: "", 115 | Usage: "Path to SSL cert", 116 | EnvVars: []string{"GGZ_SERVER_CERT"}, 117 | Destination: &config.Server.Cert, 118 | }, 119 | &cli.StringFlag{ 120 | Name: "key", 121 | Value: "", 122 | Usage: "Path to SSL key", 123 | EnvVars: []string{"GGZ_SERVER_KEY"}, 124 | Destination: &config.Server.Key, 125 | }, 126 | &cli.BoolFlag{ 127 | Name: "letsencrypt", 128 | Value: false, 129 | Usage: "Enable Let's Encrypt SSL", 130 | EnvVars: []string{"GGZ_SERVER_LETSENCRYPT"}, 131 | Destination: &config.Server.LetsEncrypt, 132 | }, 133 | &cli.BoolFlag{ 134 | Name: "strict-curves", 135 | Value: false, 136 | Usage: "Use strict SSL curves", 137 | EnvVars: []string{"GGZ_STRICT_CURVES"}, 138 | Destination: &config.Server.StrictCurves, 139 | }, 140 | &cli.BoolFlag{ 141 | Name: "strict-ciphers", 142 | Value: false, 143 | Usage: "Use strict SSL ciphers", 144 | EnvVars: []string{"GGZ_STRICT_CIPHERS"}, 145 | Destination: &config.Server.StrictCiphers, 146 | }, 147 | &cli.DurationFlag{ 148 | Name: "expire", 149 | Value: time.Hour * 24, 150 | Usage: "Session expire duration", 151 | EnvVars: []string{"GGZ_SESSION_EXPIRE"}, 152 | Destination: &config.Session.Expire, 153 | }, 154 | &cli.StringSliceFlag{ 155 | Name: "admin-user", 156 | Value: &cli.StringSlice{}, 157 | Usage: "Enforce user as an admin", 158 | EnvVars: []string{"GGZ_ADMIN_USERS"}, 159 | }, 160 | &cli.BoolFlag{ 161 | Name: "admin-create", 162 | Value: true, 163 | Usage: "Create an initial admin user", 164 | EnvVars: []string{"GGZ_ADMIN_CREATE"}, 165 | Destination: &config.Admin.Create, 166 | }, 167 | &cli.StringFlag{ 168 | Name: "token", 169 | Value: "", 170 | Usage: "Header token", 171 | EnvVars: []string{"GGZ_TOKEN"}, 172 | Destination: &config.Server.Token, 173 | }, 174 | &cli.IntFlag{ 175 | Name: "timeout", 176 | Value: 500, 177 | Usage: "sqlite database timeout", 178 | EnvVars: []string{"GGZ_SQLITE_TIMEOUT"}, 179 | Destination: &config.Database.TimeOut, 180 | }, 181 | &cli.StringFlag{ 182 | Name: "mode", 183 | Value: "", 184 | Usage: "databas ssl mode", 185 | EnvVars: []string{"GGZ_SSL_MODE"}, 186 | Destination: &config.Database.SSLMode, 187 | }, 188 | &cli.StringFlag{ 189 | Name: "shorten-host", 190 | Value: "http://localhost:8081", 191 | Usage: "shorten-host", 192 | EnvVars: []string{"GGZ_SERVER_SHORTEN_HOST"}, 193 | Destination: &config.Server.ShortenHost, 194 | }, 195 | &cli.IntFlag{ 196 | Name: "shorten-size", 197 | Value: 5, 198 | Usage: "shorten-size", 199 | EnvVars: []string{"GGZ_SERVER_SHORTEN_SIZE"}, 200 | Destination: &config.Server.ShortenSize, 201 | }, 202 | &cli.StringFlag{ 203 | Name: "storage-driver", 204 | Value: "disk", 205 | Usage: "Storage driver selection", 206 | EnvVars: []string{"GGZ_STORAGE_DRIVER"}, 207 | Destination: &config.Storage.Driver, 208 | }, 209 | &cli.StringFlag{ 210 | Name: "storage-path", 211 | Value: "storage/", 212 | Usage: "Folder for storing uploads", 213 | EnvVars: []string{"GGZ_STORAGE_PATH"}, 214 | Destination: &config.Storage.Path, 215 | }, 216 | &cli.BoolFlag{ 217 | Name: "qrcode-enable", 218 | Usage: "qrcode module enable", 219 | EnvVars: []string{"GGZ_QRCODE_ENABLE"}, 220 | Destination: &config.QRCode.Enable, 221 | }, 222 | &cli.StringFlag{ 223 | Name: "qrcode-bucket", 224 | Value: "qrcode", 225 | Usage: "qrcode bucket name", 226 | EnvVars: []string{"GGZ_QRCODE_BUCKET"}, 227 | Destination: &config.QRCode.Bucket, 228 | }, 229 | &cli.StringFlag{ 230 | Name: "minio-access-id", 231 | Value: "", 232 | Usage: "minio-access-id", 233 | EnvVars: []string{"GGZ_MINIO_ACCESS_ID"}, 234 | Destination: &config.Minio.AccessID, 235 | }, 236 | &cli.StringFlag{ 237 | Name: "minio-secret-key", 238 | Value: "", 239 | Usage: "minio-secret-key", 240 | EnvVars: []string{"GGZ_MINIO_SECRET_KEY"}, 241 | Destination: &config.Minio.SecretKey, 242 | }, 243 | &cli.StringFlag{ 244 | Name: "minio-endpoint", 245 | Value: "", 246 | Usage: "minio-endpoint", 247 | EnvVars: []string{"GGZ_MINIO_ENDPOINT"}, 248 | Destination: &config.Minio.EndPoint, 249 | }, 250 | &cli.BoolFlag{ 251 | Name: "minio-ssl", 252 | Usage: "minio-ssl", 253 | EnvVars: []string{"GGZ_MINIO_SSL"}, 254 | Destination: &config.Minio.SSL, 255 | }, 256 | &cli.StringFlag{ 257 | Name: "minio-bucket", 258 | Value: "qrcode", 259 | Usage: "minio-bucket", 260 | EnvVars: []string{"GGZ_MINIO_BUCKET"}, 261 | Destination: &config.Minio.Bucket, 262 | }, 263 | &cli.StringFlag{ 264 | Name: "minio-region", 265 | Value: "us-east-1", 266 | Usage: "minio-region", 267 | EnvVars: []string{"GGZ_MINIO_REGION"}, 268 | Destination: &config.Minio.Region, 269 | }, 270 | &cli.StringFlag{ 271 | Name: "auth0-pem-path", 272 | Usage: "Auth0 Pem file path", 273 | EnvVars: []string{"GGZ_AUTH0_PEM_PATH"}, 274 | Destination: &config.Auth0.PemPath, 275 | }, 276 | &cli.BoolFlag{ 277 | Name: "auth0-debug", 278 | Usage: "Auth0 debug", 279 | EnvVars: []string{"GGZ_AUTH0_DEBUG"}, 280 | Destination: &config.Auth0.Debug, 281 | }, 282 | &cli.StringFlag{ 283 | Name: "auth0-key-name", 284 | Usage: "Auth0 key content", 285 | EnvVars: []string{"GGZ_AUTH0_Key"}, 286 | Destination: &config.Auth0.Key, 287 | }, 288 | &cli.StringFlag{ 289 | Name: "cache-driver", 290 | Value: "default", 291 | Usage: "Cache driver selection", 292 | EnvVars: []string{"GGZ_CACHE_DRIVER"}, 293 | Destination: &config.Cache.Driver, 294 | }, 295 | &cli.IntFlag{ 296 | Name: "cache-expire-time", 297 | Value: 15, 298 | Usage: "cache expire time (minutes)", 299 | EnvVars: []string{"GGZ_CACHE_EXPIRE"}, 300 | Destination: &config.Cache.Expire, 301 | }, 302 | &cli.StringFlag{ 303 | Name: "cache-prefix-name", 304 | Value: "ggz", 305 | Usage: "prefix name of key", 306 | EnvVars: []string{"GGZ_CACHE_PREFIX_NAME"}, 307 | Destination: &config.Cache.Prefix, 308 | }, 309 | &cli.StringFlag{ 310 | Name: "metrics-auth-token", 311 | EnvVars: []string{"GGZ_METRICS_TOKEN"}, 312 | Usage: "token to secure prometheus metrics endpoint", 313 | Destination: &config.Metrics.Token, 314 | }, 315 | &cli.BoolFlag{ 316 | Name: "metrics-enabled", 317 | EnvVars: []string{"GGZ_METRICS_ENABLED"}, 318 | Usage: "enable prometheus metrics", 319 | Destination: &config.Metrics.Enabled, 320 | }, 321 | }, 322 | Before: func(c *cli.Context) error { 323 | if len(c.StringSlice("admin-user")) > 0 { 324 | // StringSliceFlag doesn't support Destination 325 | config.Admin.Users = c.StringSlice("admin-user") 326 | } 327 | 328 | return nil 329 | }, 330 | Action: func(c *cli.Context) error { 331 | idleConnsClosed := make(chan struct{}) 332 | 333 | // load global script 334 | log.Info().Msg("Initial module engine.") 335 | routes.GlobalInit() 336 | 337 | server := &http.Server{ 338 | Addr: config.Server.Addr, 339 | Handler: routes.LoadRedirct(), 340 | ReadTimeout: 5 * time.Second, 341 | WriteTimeout: 10 * time.Second, 342 | } 343 | 344 | go func(srv *http.Server) { 345 | sigint := make(chan os.Signal, 1) 346 | 347 | // interrupt signal sent from terminal 348 | signal.Notify(sigint, os.Interrupt) 349 | // sigterm signal sent from kubernetes 350 | signal.Notify(sigint, syscall.SIGTERM) 351 | 352 | <-sigint 353 | 354 | log.Info().Msg("received an interrupt signal, shut down the server.") 355 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 356 | defer cancel() 357 | // We received an interrupt signal, shut down. 358 | if err := srv.Shutdown(ctx); err != nil { 359 | // Error from closing listeners, or context timeout: 360 | log.Error().Err(err).Msg("HTTP server Shutdown") 361 | } 362 | close(idleConnsClosed) 363 | }(server) 364 | 365 | if config.Server.LetsEncrypt || (config.Server.Cert != "" && config.Server.Key != "") { 366 | cfg := &tls.Config{ 367 | PreferServerCipherSuites: true, 368 | MinVersion: tls.VersionTLS12, 369 | } 370 | 371 | if config.Server.StrictCurves { 372 | cfg.CurvePreferences = []tls.CurveID{ 373 | tls.CurveP521, 374 | tls.CurveP384, 375 | tls.CurveP256, 376 | } 377 | } 378 | 379 | if config.Server.StrictCiphers { 380 | cfg.CipherSuites = []uint16{ 381 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 382 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 383 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 384 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 385 | } 386 | } 387 | 388 | if config.Server.LetsEncrypt { 389 | if config.Server.Addr != defaultHostAddr { 390 | log.Fatal().Msg("With Let's Encrypt bind port have been overwritten!") 391 | } 392 | 393 | parsed, err := url.Parse(config.Server.Host) 394 | 395 | if err != nil { 396 | log.Fatal().Err(err).Msg("Failed to parse host name.") 397 | } 398 | 399 | certManager := &autocert.Manager{ 400 | Prompt: autocert.AcceptTOS, 401 | HostPolicy: autocert.HostWhitelist(parsed.Host), 402 | Cache: autocert.DirCache(path.Join(config.Server.Storage, "certs")), 403 | } 404 | 405 | cfg.GetCertificate = certManager.GetCertificate 406 | 407 | var ( 408 | g errgroup.Group 409 | ) 410 | 411 | splitAddr := strings.SplitN(config.Server.Addr, ":", 2) 412 | log.Info().Msgf("Starting on %s:80 and %s:443", splitAddr[0], splitAddr[0]) 413 | 414 | g.Go(func() error { 415 | return http.ListenAndServe( 416 | fmt.Sprintf("%s:80", splitAddr[0]), 417 | certManager.HTTPHandler(http.HandlerFunc(redirect)), 418 | ) 419 | }) 420 | 421 | g.Go(func() error { 422 | server.Addr = fmt.Sprintf("%s:443", splitAddr[0]) 423 | server.TLSConfig = cfg 424 | return startServer(server) 425 | }) 426 | 427 | if err := g.Wait(); err != nil { 428 | log.Fatal().Err(err) 429 | } 430 | } else { 431 | cert, err := tls.LoadX509KeyPair( 432 | config.Server.Cert, 433 | config.Server.Key, 434 | ) 435 | 436 | if err != nil { 437 | log.Fatal().Err(err).Msg("Failed to load SSL certificates.") 438 | } 439 | 440 | cfg.Certificates = []tls.Certificate{ 441 | cert, 442 | } 443 | 444 | // Add TLS config 445 | server.TLSConfig = cfg 446 | 447 | if err := startServer(server); err != nil { 448 | log.Fatal().Err(err) 449 | } 450 | } 451 | } else { 452 | var ( 453 | g errgroup.Group 454 | ) 455 | 456 | g.Go(func() error { 457 | log.Info().Msgf("Starting redirect server on %s", config.Server.Addr) 458 | return startServer(server) 459 | }) 460 | 461 | if err := g.Wait(); err != nil { 462 | log.Fatal().Err(err) 463 | } 464 | } 465 | 466 | <-idleConnsClosed 467 | 468 | return nil 469 | }, 470 | } 471 | } 472 | 473 | func redirect(w http.ResponseWriter, req *http.Request) { 474 | target := "https://" + req.Host + req.URL.Path 475 | 476 | if len(req.URL.RawQuery) > 0 { 477 | target += "?" + req.URL.RawQuery 478 | } 479 | 480 | log.Printf("Redirecting to %s", target) 481 | http.Redirect(w, req, target, http.StatusTemporaryRedirect) 482 | } 483 | 484 | func startServer(s *http.Server) error { 485 | if s.TLSConfig == nil { 486 | return s.ListenAndServe() 487 | } 488 | return s.ListenAndServeTLS("", "") 489 | } 490 | -------------------------------------------------------------------------------- /cmd/ggz-server/health.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-ggz/ggz/pkg/config" 8 | 9 | "github.com/rs/zerolog/log" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | func healthAction() cli.ActionFunc { 14 | return func(c *cli.Context) error { 15 | resp, err := http.Get("http://localhost" + config.Server.Addr + "/healthz") 16 | if err != nil { 17 | log.Error(). 18 | Err(err). 19 | Msg("failed to request health check") 20 | return err 21 | } 22 | defer resp.Body.Close() 23 | if resp.StatusCode != http.StatusOK { 24 | log.Error(). 25 | Int("code", resp.StatusCode). 26 | Msg("health seems to be in bad state") 27 | return fmt.Errorf("server returned non-200 status code") 28 | } 29 | return nil 30 | } 31 | } 32 | 33 | func healthFlags() []cli.Flag { 34 | return []cli.Flag{ 35 | &cli.StringFlag{ 36 | Name: "addr", 37 | Value: defaultHostAddr, 38 | Usage: "Address to bind the server", 39 | EnvVars: []string{"GGZ_SERVER_ADDR"}, 40 | Destination: &config.Server.Addr, 41 | }, 42 | } 43 | } 44 | 45 | // Health provides the sub-command to perform a health check. 46 | func Health() *cli.Command { 47 | return &cli.Command{ 48 | Name: "health", 49 | Usage: "perform health checks", 50 | Flags: healthFlags(), 51 | Action: healthAction(), 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cmd/ggz-server/mail.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/go-ggz/ggz/pkg/config" 8 | "github.com/go-ggz/ggz/pkg/module/mailer" 9 | 10 | "github.com/aws/aws-sdk-go/service/ses" 11 | "github.com/rs/zerolog/log" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | // Mail provides the sub-command to send email. 16 | func Mail() *cli.Command { 17 | return &cli.Command{ 18 | Name: "email", 19 | Usage: "send test email", 20 | Flags: []cli.Flag{ 21 | &cli.StringFlag{ 22 | Name: "aws-access-id", 23 | Usage: "aws access key", 24 | EnvVars: []string{"AWS_ACCESS_KEY_ID"}, 25 | Destination: &config.AWS.AccessID, 26 | }, 27 | &cli.StringFlag{ 28 | Name: "aws-secret-key", 29 | Usage: "aws secret key", 30 | EnvVars: []string{"AWS_SECRET_ACCESS_KEY"}, 31 | Destination: &config.AWS.SecretKey, 32 | }, 33 | &cli.StringFlag{ 34 | Name: "mail-driver", 35 | Usage: "mail driver", 36 | Value: "ses", 37 | EnvVars: []string{"GGZ_MAIL_DRIVER"}, 38 | Destination: &config.MailService.Driver, 39 | }, 40 | &cli.StringFlag{ 41 | Name: "smtp-host", 42 | Usage: "smtp host", 43 | EnvVars: []string{"GGZ_SMTP_HOST"}, 44 | Destination: &config.SMTP.Host, 45 | }, 46 | &cli.StringFlag{ 47 | Name: "smtp-port", 48 | Usage: "smtp port", 49 | EnvVars: []string{"GGZ_SMTP_PORT"}, 50 | Destination: &config.SMTP.Port, 51 | }, 52 | &cli.StringFlag{ 53 | Name: "smtp-username", 54 | Usage: "smtp username", 55 | EnvVars: []string{"GGZ_SMTP_USERNAME"}, 56 | Destination: &config.SMTP.Username, 57 | }, 58 | &cli.StringFlag{ 59 | Name: "smtp-password", 60 | Usage: "smtp password", 61 | EnvVars: []string{"GGZ_SMTP_PASSWORD"}, 62 | Destination: &config.SMTP.Password, 63 | }, 64 | &cli.IntFlag{ 65 | Name: "email-count", 66 | Usage: "send email count", 67 | Value: 1, 68 | }, 69 | }, 70 | Action: func(c *cli.Context) error { 71 | // initial mailer service 72 | if _, err := mailer.NewEngine(mailer.Config{ 73 | Driver: "ses", 74 | }); err != nil { 75 | log.Fatal().Err(err).Msgf("failed to initial mailer") 76 | } 77 | 78 | count := c.Int("email-count") 79 | wg := sync.WaitGroup{} 80 | 81 | for i := 0; i < count; i++ { 82 | wg.Add(1) 83 | go func(k int, wg *sync.WaitGroup) { 84 | resp, err := mailer.Client. 85 | From("Bo-Yi Wu", "appleboy.tw@gmail.com"). 86 | Subject(fmt.Sprintf("[Go 語言] 測試電子郵件系統 [%d]", (k + 1))). 87 | To("appleboy.tw@gmail.com"). 88 | Body("

繁體中文 Amazon SES Test Email (AWS SDK for Go)

This email was sent with " + 89 | "Amazon SES using the " + 90 | "AWS SDK for Go.

"). 91 | Send() 92 | 93 | if err != nil { 94 | log.Fatal().Err(err).Msgf("failed to send email") 95 | } 96 | 97 | log.Info().Msgf("Send the Email completely [%d]", (k + 1)) 98 | if v, ok := resp.(*ses.SendEmailOutput); ok { 99 | log.Info().Msgf("Message ID: %s", *v.MessageId) 100 | } 101 | wg.Done() 102 | }(i, &wg) 103 | } 104 | 105 | wg.Wait() 106 | 107 | return nil 108 | }, 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /cmd/ggz-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/go-ggz/ggz/pkg/config" 8 | "github.com/go-ggz/ggz/pkg/version" 9 | 10 | "github.com/joho/godotenv" 11 | _ "github.com/joho/godotenv/autoload" 12 | "github.com/rs/zerolog/log" 13 | "github.com/urfave/cli/v2" 14 | ) 15 | 16 | func authorList() []*cli.Author { 17 | return []*cli.Author{ 18 | { 19 | Name: "Bo-Yi Wu", 20 | Email: "appleboy.tw@gmail.com", 21 | }, 22 | } 23 | } 24 | 25 | func globalFlags() []cli.Flag { 26 | return []cli.Flag{ 27 | &cli.BoolFlag{ 28 | Name: "debug", 29 | Value: true, 30 | Usage: "Activate debug information", 31 | EnvVars: []string{"GGZ_SERVER_DEBUG"}, 32 | Destination: &config.Server.Debug, 33 | }, 34 | &cli.BoolFlag{ 35 | Name: "log-color", 36 | Value: true, 37 | Usage: "enable colored logging", 38 | EnvVars: []string{"GGZ_LOGS_COLOR"}, 39 | Destination: &config.Logs.Color, 40 | }, 41 | &cli.BoolFlag{ 42 | Name: "log-pretty", 43 | Value: true, 44 | Usage: "enable pretty logging", 45 | EnvVars: []string{"GGZ_LOGS_PRETTY"}, 46 | Destination: &config.Logs.Pretty, 47 | }, 48 | &cli.StringFlag{ 49 | Name: "log-level", 50 | Value: "info", 51 | Usage: "set logging level", 52 | EnvVars: []string{"GGZ_LOGS_LEVEL"}, 53 | Destination: &config.Logs.Level, 54 | }, 55 | } 56 | } 57 | 58 | func globalCommands() []*cli.Command { 59 | return []*cli.Command{ 60 | Server(), 61 | Health(), 62 | Mail(), 63 | } 64 | } 65 | 66 | func globalBefore() cli.BeforeFunc { 67 | return func(c *cli.Context) error { 68 | setupLogger() 69 | return nil 70 | } 71 | } 72 | 73 | func main() { 74 | if env := os.Getenv("GGZ_ENV_FILE"); env != "" { 75 | if err := godotenv.Load(env); err != nil { 76 | log.Fatal().Err(err).Msg("can't load env file") 77 | } 78 | } 79 | 80 | app := &cli.App{ 81 | Name: "gzz server", 82 | Usage: "shorten url service", 83 | Copyright: "Copyright (c) 2019 Bo-Yi Wu", 84 | Version: version.PrintCLIVersion(), 85 | Compiled: time.Now(), 86 | Authors: authorList(), 87 | Flags: globalFlags(), 88 | Commands: globalCommands(), 89 | Before: globalBefore(), 90 | } 91 | 92 | cli.HelpFlag = &cli.BoolFlag{ 93 | Name: "help", 94 | Aliases: []string{"h"}, 95 | Usage: "Show the help, so what you see now", 96 | } 97 | 98 | cli.VersionFlag = &cli.BoolFlag{ 99 | Name: "version", 100 | Aliases: []string{"v"}, 101 | Usage: "Print the current version of that tool", 102 | } 103 | 104 | if err := app.Run(os.Args); err != nil { 105 | log.Fatal().Err(err).Msg("can't run app") 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cmd/ggz-server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "os/signal" 11 | "path" 12 | "strings" 13 | "syscall" 14 | "time" 15 | 16 | "github.com/go-ggz/ggz/pkg/config" 17 | "github.com/go-ggz/ggz/pkg/router/routes" 18 | 19 | "github.com/graphql-go/graphql" 20 | "github.com/rs/zerolog/log" 21 | "github.com/urfave/cli/v2" 22 | "golang.org/x/crypto/acme/autocert" 23 | "golang.org/x/sync/errgroup" 24 | ) 25 | 26 | var ( 27 | defaultHostAddr = ":8080" 28 | ) 29 | 30 | // Server provides the sub-command to start the API server. 31 | func Server() *cli.Command { 32 | return &cli.Command{ 33 | Name: "server", 34 | Usage: "Start the gzz service", 35 | Flags: []cli.Flag{ 36 | &cli.StringFlag{ 37 | Name: "assets", 38 | Value: "", 39 | Usage: "Path to custom assets and templates", 40 | EnvVars: []string{"GGZ_SERVER_ASSETS"}, 41 | Destination: &config.Server.Assets, 42 | }, 43 | &cli.StringFlag{ 44 | Name: "db-driver", 45 | Value: "sqlite3", 46 | Usage: "Database driver selection", 47 | EnvVars: []string{"GGZ_DB_DRIVER"}, 48 | Destination: &config.Database.Driver, 49 | }, 50 | &cli.StringFlag{ 51 | Name: "db-name", 52 | Value: "ggz", 53 | Usage: "Name for database connection", 54 | EnvVars: []string{"GGZ_DB_NAME"}, 55 | Destination: &config.Database.Name, 56 | }, 57 | &cli.StringFlag{ 58 | Name: "db-username", 59 | Value: "root", 60 | Usage: "Username for database connection", 61 | EnvVars: []string{"GGZ_DB_USERNAME"}, 62 | Destination: &config.Database.Username, 63 | }, 64 | &cli.StringFlag{ 65 | Name: "db-password", 66 | Value: "root", 67 | Usage: "Password for database connection", 68 | EnvVars: []string{"GGZ_DB_PASSWORD"}, 69 | Destination: &config.Database.Password, 70 | }, 71 | &cli.StringFlag{ 72 | Name: "db-host", 73 | Value: "localhost:3306", 74 | Usage: "Host for database connection", 75 | EnvVars: []string{"GGZ_DB_HOST"}, 76 | Destination: &config.Database.Host, 77 | }, 78 | &cli.StringFlag{ 79 | Name: "path", 80 | Value: "data/db/ggz.db", 81 | Usage: "sqlite path", 82 | EnvVars: []string{"GGZ_SQLITE_PATH"}, 83 | Destination: &config.Database.Path, 84 | }, 85 | &cli.StringFlag{ 86 | Name: "host", 87 | Value: "http://localhost:8080", 88 | Usage: "External access to server", 89 | EnvVars: []string{"GGZ_SERVER_HOST"}, 90 | Destination: &config.Server.Host, 91 | }, 92 | &cli.StringFlag{ 93 | Name: "addr", 94 | Value: defaultHostAddr, 95 | Usage: "Address to bind the server", 96 | EnvVars: []string{"GGZ_SERVER_ADDR"}, 97 | Destination: &config.Server.Addr, 98 | }, 99 | &cli.StringFlag{ 100 | Name: "root", 101 | Value: "/", 102 | Usage: "Root folder of the app", 103 | EnvVars: []string{"GGZ_SERVER_ROOT"}, 104 | Destination: &config.Server.Root, 105 | }, 106 | &cli.BoolFlag{ 107 | Name: "pprof", 108 | Value: false, 109 | Usage: "Enable pprof debugging server", 110 | EnvVars: []string{"GGZ_SERVER_PPROF"}, 111 | Destination: &config.Server.Pprof, 112 | }, 113 | &cli.BoolFlag{ 114 | Name: "graphiql", 115 | Value: false, 116 | Usage: "Enable graphiql interface", 117 | EnvVars: []string{"GOBENTO_SERVER_GRAPHIQL"}, 118 | Destination: &config.Server.GraphiQL, 119 | }, 120 | &cli.StringFlag{ 121 | Name: "cert", 122 | Value: "", 123 | Usage: "Path to SSL cert", 124 | EnvVars: []string{"GGZ_SERVER_CERT"}, 125 | Destination: &config.Server.Cert, 126 | }, 127 | &cli.StringFlag{ 128 | Name: "key", 129 | Value: "", 130 | Usage: "Path to SSL key", 131 | EnvVars: []string{"GGZ_SERVER_KEY"}, 132 | Destination: &config.Server.Key, 133 | }, 134 | &cli.BoolFlag{ 135 | Name: "letsencrypt", 136 | Value: false, 137 | Usage: "Enable Let's Encrypt SSL", 138 | EnvVars: []string{"GGZ_SERVER_LETSENCRYPT"}, 139 | Destination: &config.Server.LetsEncrypt, 140 | }, 141 | &cli.BoolFlag{ 142 | Name: "strict-curves", 143 | Value: false, 144 | Usage: "Use strict SSL curves", 145 | EnvVars: []string{"GGZ_STRICT_CURVES"}, 146 | Destination: &config.Server.StrictCurves, 147 | }, 148 | &cli.BoolFlag{ 149 | Name: "strict-ciphers", 150 | Value: false, 151 | Usage: "Use strict SSL ciphers", 152 | EnvVars: []string{"GGZ_STRICT_CIPHERS"}, 153 | Destination: &config.Server.StrictCiphers, 154 | }, 155 | &cli.DurationFlag{ 156 | Name: "expire", 157 | Value: time.Hour * 24, 158 | Usage: "Session expire duration", 159 | EnvVars: []string{"GGZ_SESSION_EXPIRE"}, 160 | Destination: &config.Session.Expire, 161 | }, 162 | &cli.StringSliceFlag{ 163 | Name: "admin-user", 164 | Value: &cli.StringSlice{}, 165 | Usage: "Enforce user as an admin", 166 | EnvVars: []string{"GGZ_ADMIN_USERS"}, 167 | }, 168 | &cli.BoolFlag{ 169 | Name: "admin-create", 170 | Value: true, 171 | Usage: "Create an initial admin user", 172 | EnvVars: []string{"GGZ_ADMIN_CREATE"}, 173 | Destination: &config.Admin.Create, 174 | }, 175 | &cli.StringFlag{ 176 | Name: "token", 177 | Value: "", 178 | Usage: "Header token", 179 | EnvVars: []string{"GGZ_TOKEN"}, 180 | Destination: &config.Server.Token, 181 | }, 182 | &cli.IntFlag{ 183 | Name: "timeout", 184 | Value: 500, 185 | Usage: "sqlite database timeout", 186 | EnvVars: []string{"GGZ_SQLITE_TIMEOUT"}, 187 | Destination: &config.Database.TimeOut, 188 | }, 189 | &cli.StringFlag{ 190 | Name: "mode", 191 | Value: "", 192 | Usage: "databas ssl mode", 193 | EnvVars: []string{"GGZ_SSL_MODE"}, 194 | Destination: &config.Database.SSLMode, 195 | }, 196 | &cli.StringFlag{ 197 | Name: "shorten-host", 198 | Value: "http://localhost:8081", 199 | Usage: "shorten-host", 200 | EnvVars: []string{"GGZ_SERVER_SHORTEN_HOST"}, 201 | Destination: &config.Server.ShortenHost, 202 | }, 203 | &cli.IntFlag{ 204 | Name: "shorten-size", 205 | Value: 5, 206 | Usage: "shorten-size", 207 | EnvVars: []string{"GGZ_SERVER_SHORTEN_SIZE"}, 208 | Destination: &config.Server.ShortenSize, 209 | }, 210 | &cli.StringFlag{ 211 | Name: "storage-driver", 212 | Value: "disk", 213 | Usage: "Storage driver selection", 214 | EnvVars: []string{"GGZ_STORAGE_DRIVER"}, 215 | Destination: &config.Storage.Driver, 216 | }, 217 | &cli.StringFlag{ 218 | Name: "storage-path", 219 | Value: "storage/", 220 | Usage: "Folder for storing uploads", 221 | EnvVars: []string{"GGZ_STORAGE_PATH"}, 222 | Destination: &config.Storage.Path, 223 | }, 224 | &cli.BoolFlag{ 225 | Name: "qrcode-enable", 226 | Usage: "qrcode module enable", 227 | EnvVars: []string{"GGZ_QRCODE_ENABLE"}, 228 | Destination: &config.QRCode.Enable, 229 | }, 230 | &cli.StringFlag{ 231 | Name: "qrcode-bucket", 232 | Value: "qrcode", 233 | Usage: "qrcode bucket name", 234 | EnvVars: []string{"GGZ_QRCODE_BUCKET"}, 235 | Destination: &config.QRCode.Bucket, 236 | }, 237 | &cli.StringFlag{ 238 | Name: "minio-access-id", 239 | Value: "", 240 | Usage: "minio-access-id", 241 | EnvVars: []string{"GGZ_MINIO_ACCESS_ID"}, 242 | Destination: &config.Minio.AccessID, 243 | }, 244 | &cli.StringFlag{ 245 | Name: "minio-secret-key", 246 | Value: "", 247 | Usage: "minio-secret-key", 248 | EnvVars: []string{"GGZ_MINIO_SECRET_KEY"}, 249 | Destination: &config.Minio.SecretKey, 250 | }, 251 | &cli.StringFlag{ 252 | Name: "minio-endpoint", 253 | Value: "", 254 | Usage: "minio-endpoint", 255 | EnvVars: []string{"GGZ_MINIO_ENDPOINT"}, 256 | Destination: &config.Minio.EndPoint, 257 | }, 258 | &cli.BoolFlag{ 259 | Name: "minio-ssl", 260 | Usage: "minio-ssl", 261 | EnvVars: []string{"GGZ_MINIO_SSL"}, 262 | Destination: &config.Minio.SSL, 263 | }, 264 | &cli.StringFlag{ 265 | Name: "minio-bucket", 266 | Value: "qrcode", 267 | Usage: "minio-bucket", 268 | EnvVars: []string{"GGZ_MINIO_BUCKET"}, 269 | Destination: &config.Minio.Bucket, 270 | }, 271 | &cli.StringFlag{ 272 | Name: "minio-region", 273 | Value: "us-east-1", 274 | Usage: "minio-region", 275 | EnvVars: []string{"GGZ_MINIO_REGION"}, 276 | Destination: &config.Minio.Region, 277 | }, 278 | &cli.StringFlag{ 279 | Name: "auth0-pem-path", 280 | Usage: "Auth0 Pem file path", 281 | EnvVars: []string{"GGZ_AUTH0_PEM_PATH"}, 282 | Destination: &config.Auth0.PemPath, 283 | }, 284 | &cli.BoolFlag{ 285 | Name: "auth0-debug", 286 | Usage: "Auth0 debug", 287 | EnvVars: []string{"GGZ_AUTH0_DEBUG"}, 288 | Destination: &config.Auth0.Debug, 289 | }, 290 | &cli.StringFlag{ 291 | Name: "auth0-key-name", 292 | Usage: "Auth0 key content", 293 | EnvVars: []string{"GGZ_AUTH0_Key"}, 294 | Destination: &config.Auth0.Key, 295 | }, 296 | &cli.StringFlag{ 297 | Name: "cache-driver", 298 | Value: "default", 299 | Usage: "Cache driver selection", 300 | EnvVars: []string{"GGZ_CACHE_DRIVER"}, 301 | Destination: &config.Cache.Driver, 302 | }, 303 | &cli.IntFlag{ 304 | Name: "cache-expire-time", 305 | Value: 15, 306 | Usage: "cache expire time (minutes)", 307 | EnvVars: []string{"GGZ_CACHE_EXPIRE"}, 308 | Destination: &config.Cache.Expire, 309 | }, 310 | &cli.StringFlag{ 311 | Name: "cache-prefix-name", 312 | Value: "ggz", 313 | Usage: "prefix name of key", 314 | EnvVars: []string{"GGZ_CACHE_PREFIX_NAME"}, 315 | Destination: &config.Cache.Prefix, 316 | }, 317 | &cli.StringFlag{ 318 | Name: "metrics-auth-token", 319 | EnvVars: []string{"GGZ_METRICS_TOKEN"}, 320 | Usage: "token to secure prometheus metrics endpoint", 321 | Destination: &config.Metrics.Token, 322 | }, 323 | &cli.BoolFlag{ 324 | Name: "metrics-enabled", 325 | EnvVars: []string{"GGZ_METRICS_ENABLED"}, 326 | Usage: "enable prometheus metrics", 327 | Destination: &config.Metrics.Enabled, 328 | }, 329 | &cli.StringFlag{ 330 | Name: "auth-driver", 331 | EnvVars: []string{"GGZ_AUTH_DRIVER"}, 332 | Usage: "auth driver", 333 | Value: "auth0", 334 | Destination: &config.Auth.Driver, 335 | }, 336 | }, 337 | Before: func(c *cli.Context) error { 338 | if len(c.StringSlice("admin-user")) > 0 { 339 | // StringSliceFlag doesn't support Destination 340 | config.Admin.Users = c.StringSlice("admin-user") 341 | } 342 | 343 | return nil 344 | }, 345 | Action: func(c *cli.Context) error { 346 | idleConnsClosed := make(chan struct{}) 347 | 348 | // load global script 349 | log.Info().Msg("Initial module engine.") 350 | routes.GlobalInit() 351 | 352 | server := &http.Server{ 353 | Addr: config.Server.Addr, 354 | Handler: routes.Load(), 355 | ReadTimeout: 5 * time.Second, 356 | WriteTimeout: 10 * time.Second, 357 | } 358 | 359 | go func(srv *http.Server) { 360 | sigint := make(chan os.Signal, 1) 361 | 362 | // interrupt signal sent from terminal 363 | signal.Notify(sigint, os.Interrupt) 364 | // sigterm signal sent from kubernetes 365 | signal.Notify(sigint, syscall.SIGINT, syscall.SIGTERM) 366 | defer signal.Stop(sigint) 367 | 368 | <-sigint 369 | 370 | log.Info().Msg("received an interrupt signal, shut down the server.") 371 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 372 | defer cancel() 373 | // We received an interrupt signal, shut down. 374 | if err := srv.Shutdown(ctx); err != nil { 375 | // Error from closing listeners, or context timeout: 376 | log.Error().Err(err).Msg("HTTP server Shutdown") 377 | } 378 | close(idleConnsClosed) 379 | }(server) 380 | 381 | if !config.Server.Debug { 382 | graphql.SchemaMetaFieldDef.Resolve = func(p graphql.ResolveParams) (interface{}, error) { 383 | return nil, nil 384 | } 385 | graphql.TypeMetaFieldDef.Resolve = func(p graphql.ResolveParams) (interface{}, error) { 386 | return nil, nil 387 | } 388 | } 389 | 390 | if config.Server.LetsEncrypt || (config.Server.Cert != "" && config.Server.Key != "") { 391 | cfg := &tls.Config{ 392 | PreferServerCipherSuites: true, 393 | MinVersion: tls.VersionTLS12, 394 | } 395 | 396 | if config.Server.StrictCurves { 397 | cfg.CurvePreferences = []tls.CurveID{ 398 | tls.CurveP521, 399 | tls.CurveP384, 400 | tls.CurveP256, 401 | } 402 | } 403 | 404 | if config.Server.StrictCiphers { 405 | cfg.CipherSuites = []uint16{ 406 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 407 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 408 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 409 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 410 | } 411 | } 412 | 413 | if config.Server.LetsEncrypt { 414 | if config.Server.Addr != defaultHostAddr { 415 | log.Fatal().Msg("With Let's Encrypt bind port have been overwritten!") 416 | } 417 | 418 | parsed, err := url.Parse(config.Server.Host) 419 | 420 | if err != nil { 421 | log.Fatal().Err(err).Msg("Failed to parse host name.") 422 | } 423 | 424 | certManager := &autocert.Manager{ 425 | Prompt: autocert.AcceptTOS, 426 | HostPolicy: autocert.HostWhitelist(parsed.Host), 427 | Cache: autocert.DirCache(path.Join(config.Server.Storage, "certs")), 428 | } 429 | 430 | cfg.GetCertificate = certManager.GetCertificate 431 | 432 | var ( 433 | g errgroup.Group 434 | ) 435 | 436 | splitAddr := strings.SplitN(config.Server.Addr, ":", 2) 437 | log.Info().Msgf("Starting on %s:80 and %s:443", splitAddr[0], splitAddr[0]) 438 | 439 | g.Go(func() error { 440 | return http.ListenAndServe( 441 | fmt.Sprintf("%s:80", splitAddr[0]), 442 | certManager.HTTPHandler(http.HandlerFunc(redirect)), 443 | ) 444 | }) 445 | 446 | g.Go(func() error { 447 | server.Addr = fmt.Sprintf("%s:443", splitAddr[0]) 448 | server.TLSConfig = cfg 449 | return startServer(server) 450 | }) 451 | 452 | if err := g.Wait(); err != nil { 453 | log.Fatal().Err(err) 454 | } 455 | } else { 456 | cert, err := tls.LoadX509KeyPair( 457 | config.Server.Cert, 458 | config.Server.Key, 459 | ) 460 | 461 | if err != nil { 462 | log.Fatal().Err(err).Msg("Failed to load SSL certificates.") 463 | } 464 | 465 | cfg.Certificates = []tls.Certificate{ 466 | cert, 467 | } 468 | 469 | // Add TLS config 470 | server.TLSConfig = cfg 471 | 472 | if err := startServer(server); err != nil { 473 | log.Fatal().Err(err) 474 | } 475 | } 476 | } else { 477 | var ( 478 | g errgroup.Group 479 | ) 480 | 481 | g.Go(func() error { 482 | log.Info().Msgf("Starting shorten server on %s", config.Server.Addr) 483 | return startServer(server) 484 | }) 485 | 486 | if err := g.Wait(); err != nil { 487 | log.Fatal().Err(err) 488 | } 489 | } 490 | 491 | <-idleConnsClosed 492 | 493 | return nil 494 | }, 495 | } 496 | } 497 | 498 | func redirect(w http.ResponseWriter, req *http.Request) { 499 | target := "https://" + req.Host + req.URL.Path 500 | 501 | if len(req.URL.RawQuery) > 0 { 502 | target += "?" + req.URL.RawQuery 503 | } 504 | 505 | log.Printf("Redirecting to %s", target) 506 | http.Redirect(w, req, target, http.StatusTemporaryRedirect) 507 | } 508 | 509 | func startServer(s *http.Server) error { 510 | if s.TLSConfig == nil { 511 | return s.ListenAndServe() 512 | } 513 | return s.ListenAndServeTLS("", "") 514 | } 515 | -------------------------------------------------------------------------------- /cmd/ggz-server/setup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/go-ggz/ggz/pkg/config" 8 | 9 | "github.com/rs/zerolog" 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | func setupLogger() { 14 | switch strings.ToLower(config.Logs.Level) { 15 | case "panic": 16 | zerolog.SetGlobalLevel(zerolog.PanicLevel) 17 | case "fatal": 18 | zerolog.SetGlobalLevel(zerolog.FatalLevel) 19 | case "error": 20 | zerolog.SetGlobalLevel(zerolog.ErrorLevel) 21 | case "warn": 22 | zerolog.SetGlobalLevel(zerolog.WarnLevel) 23 | case "info": 24 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 25 | case "debug": 26 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 27 | default: 28 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 29 | } 30 | 31 | if config.Logs.Pretty { 32 | log.Logger = log.Output( 33 | zerolog.ConsoleWriter{ 34 | Out: os.Stderr, 35 | NoColor: !config.Logs.Color, 36 | }, 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /configs/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 5s 3 | external_labels: 4 | monitor: 'my-monitor' 5 | scrape_configs: 6 | - job_name: 'prometheus' 7 | static_configs: 8 | - targets: ['localhost:9090'] 9 | - job_name: 'ggz-server' 10 | static_configs: 11 | - targets: ['ggz-server:8080'] 12 | bearer_token: 'test-prometheus-token' 13 | 14 | - job_name: 'ggz-redirect' 15 | static_configs: 16 | - targets: ['ggz-redirect:8081'] 17 | bearer_token: 'test-prometheus-token' 18 | 19 | - job_name: 'node resources' 20 | scrape_interval: 10s 21 | static_configs: 22 | - targets: 23 | - 'node-exporter:9100' 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | ggz-server: 5 | image: goggz/ggz-server 6 | restart: always 7 | ports: 8 | - 8080:8080 9 | environment: 10 | - GGZ_METRICS_TOKEN=test-prometheus-token 11 | - GGZ_METRICS_ENABLED=true 12 | 13 | ggz-redirect: 14 | image: goggz/ggz-redirect 15 | restart: always 16 | ports: 17 | - 8081:8081 18 | environment: 19 | - GGZ_PROMETHEUS_AUTH_TOKEN=test-prometheus-token 20 | 21 | prometheus: 22 | image: prom/prometheus 23 | volumes: 24 | - ./configs/prometheus.yml:/etc/prometheus/prometheus.yml 25 | - prometheus-data:/prometheus 26 | command: 27 | - '--config.file=/etc/prometheus/prometheus.yml' 28 | ports: 29 | - '9090:9090' 30 | 31 | node-exporter: 32 | image: prom/node-exporter 33 | ports: 34 | - '9100:9100' 35 | 36 | grafana: 37 | image: grafana/grafana 38 | volumes: 39 | - grafana-data:/var/lib/grafana 40 | environment: 41 | - GF_SECURITY_ADMIN_PASSWORD=pass 42 | depends_on: 43 | - prometheus 44 | ports: 45 | - '3000:3000' 46 | 47 | # db: 48 | # image: mysql 49 | # restart: always 50 | # volumes: 51 | # - mysql-data:/var/lib/mysql 52 | # environment: 53 | # MYSQL_USER: ggz 54 | # MYSQL_PASSWORD: example 55 | # MYSQL_DATABASE: ggz 56 | # MYSQL_ROOT_PASSWORD: example 57 | 58 | # minio: 59 | # image: minio/minio 60 | # restart: always 61 | # ports: 62 | # - "9000:9000" 63 | # volumes: 64 | # - minio-data:/data 65 | # environment: 66 | # MINIO_ACCESS_KEY: minio123456 67 | # MINIO_SECRET_KEY: minio1234567890 68 | # command: server /data 69 | 70 | volumes: 71 | prometheus-data: {} 72 | grafana-data: {} 73 | # minio-data: 74 | # mysql-data: 75 | -------------------------------------------------------------------------------- /docker/ggz-redirect/Dockerfile.linux.amd64: -------------------------------------------------------------------------------- 1 | FROM plugins/base:linux-amd64 2 | 3 | LABEL maintainer="Bo-Yi Wu " \ 4 | org.label-schema.name="ggz-redirect" \ 5 | org.label-schema.vendor="Bo-Yi Wu" \ 6 | org.label-schema.schema-version="1.0" 7 | 8 | EXPOSE 8081 9 | 10 | COPY release/linux/amd64/ggz-redirect /bin/ 11 | 12 | HEALTHCHECK --start-period=2s --interval=10s --timeout=5s \ 13 | CMD ["/bin/ggz-redirect", "health"] 14 | 15 | ENTRYPOINT ["/bin/ggz-redirect"] 16 | CMD ["server"] 17 | -------------------------------------------------------------------------------- /docker/ggz-redirect/Dockerfile.linux.arm: -------------------------------------------------------------------------------- 1 | FROM plugins/base:linux-arm 2 | 3 | LABEL maintainer="Bo-Yi Wu " \ 4 | org.label-schema.name="ggz-redirect" \ 5 | org.label-schema.vendor="Bo-Yi Wu" \ 6 | org.label-schema.schema-version="1.0" 7 | 8 | EXPOSE 8080 9 | 10 | COPY release/linux/arm/ggz-redirect /bin/ 11 | 12 | HEALTHCHECK --start-period=2s --interval=10s --timeout=5s \ 13 | CMD ["/bin/ggz-redirect", "health"] 14 | 15 | ENTRYPOINT ["/bin/ggz-redirect"] 16 | CMD ["server"] 17 | -------------------------------------------------------------------------------- /docker/ggz-redirect/Dockerfile.linux.arm64: -------------------------------------------------------------------------------- 1 | FROM plugins/base:linux-arm64 2 | 3 | LABEL maintainer="Bo-Yi Wu " \ 4 | org.label-schema.name="ggz-redirect" \ 5 | org.label-schema.vendor="Bo-Yi Wu" \ 6 | org.label-schema.schema-version="1.0" 7 | 8 | EXPOSE 8080 9 | 10 | COPY release/linux/arm64/ggz-redirect /bin/ 11 | 12 | HEALTHCHECK --start-period=2s --interval=10s --timeout=5s \ 13 | CMD ["/bin/ggz-redirect", "health"] 14 | 15 | ENTRYPOINT ["/bin/ggz-redirect"] 16 | CMD ["server"] 17 | -------------------------------------------------------------------------------- /docker/ggz-redirect/Dockerfile.windows.amd64: -------------------------------------------------------------------------------- 1 | FROM microsoft/nanoserver:10.0.14393.1884 2 | 3 | LABEL maintainer="Bo-Yi Wu " \ 4 | org.label-schema.name="ggz-redirect" \ 5 | org.label-schema.vendor="Bo-Yi Wu" \ 6 | org.label-schema.schema-version="1.0" 7 | 8 | EXPOSE 8080 9 | 10 | COPY release/ggz-redirect.exe C:/bin/ggz-redirect.exe 11 | 12 | ENTRYPOINT [ "C:\\bin\\ggz-redirect.exe" ] 13 | -------------------------------------------------------------------------------- /docker/ggz-redirect/manifest.tmpl: -------------------------------------------------------------------------------- 1 | image: goggz/ggz-redirect:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} 2 | {{#if build.tags}} 3 | tags: 4 | {{#each build.tags}} 5 | - {{this}} 6 | {{/each}} 7 | {{/if}} 8 | manifests: 9 | - 10 | image: goggz/ggz-redirect:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 11 | platform: 12 | architecture: amd64 13 | os: linux 14 | - 15 | image: goggz/ggz-redirect:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 16 | platform: 17 | architecture: arm64 18 | os: linux 19 | variant: v8 20 | - 21 | image: goggz/ggz-redirect:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm 22 | platform: 23 | architecture: arm 24 | os: linux 25 | variant: v7 26 | -------------------------------------------------------------------------------- /docker/ggz-server/Dockerfile.linux.amd64: -------------------------------------------------------------------------------- 1 | FROM plugins/base:linux-amd64 2 | 3 | LABEL maintainer="Bo-Yi Wu " \ 4 | org.label-schema.name="ggz-server" \ 5 | org.label-schema.vendor="Bo-Yi Wu" \ 6 | org.label-schema.schema-version="1.0" 7 | 8 | EXPOSE 8080 9 | 10 | COPY release/linux/amd64/ggz-server /bin/ 11 | 12 | HEALTHCHECK --start-period=2s --interval=10s --timeout=5s \ 13 | CMD ["/bin/ggz-server", "health"] 14 | 15 | ENTRYPOINT ["/bin/ggz-server"] 16 | CMD ["server"] 17 | -------------------------------------------------------------------------------- /docker/ggz-server/Dockerfile.linux.arm: -------------------------------------------------------------------------------- 1 | FROM plugins/base:linux-arm 2 | 3 | LABEL maintainer="Bo-Yi Wu " \ 4 | org.label-schema.name="ggz-server" \ 5 | org.label-schema.vendor="Bo-Yi Wu" \ 6 | org.label-schema.schema-version="1.0" 7 | 8 | EXPOSE 8080 9 | 10 | COPY release/linux/arm/ggz-server /bin/ 11 | 12 | HEALTHCHECK --start-period=2s --interval=10s --timeout=5s \ 13 | CMD ["/bin/ggz-server", "health"] 14 | 15 | ENTRYPOINT ["/bin/ggz-server"] 16 | CMD ["server"] 17 | -------------------------------------------------------------------------------- /docker/ggz-server/Dockerfile.linux.arm64: -------------------------------------------------------------------------------- 1 | FROM plugins/base:linux-arm64 2 | 3 | LABEL maintainer="Bo-Yi Wu " \ 4 | org.label-schema.name="ggz-server" \ 5 | org.label-schema.vendor="Bo-Yi Wu" \ 6 | org.label-schema.schema-version="1.0" 7 | 8 | EXPOSE 8080 9 | 10 | COPY release/linux/arm64/ggz-server /bin/ 11 | 12 | HEALTHCHECK --start-period=2s --interval=10s --timeout=5s \ 13 | CMD ["/bin/ggz-server", "health"] 14 | 15 | ENTRYPOINT ["/bin/ggz-server"] 16 | CMD ["server"] 17 | -------------------------------------------------------------------------------- /docker/ggz-server/Dockerfile.windows.amd64: -------------------------------------------------------------------------------- 1 | FROM microsoft/nanoserver:10.0.14393.1884 2 | 3 | LABEL maintainer="Bo-Yi Wu " \ 4 | org.label-schema.name="ggz-server" \ 5 | org.label-schema.vendor="Bo-Yi Wu" \ 6 | org.label-schema.schema-version="1.0" 7 | 8 | EXPOSE 8080 9 | 10 | COPY release/ggz-server.exe C:/bin/ggz-server.exe 11 | 12 | ENTRYPOINT [ "C:\\bin\\ggz-server.exe" ] 13 | -------------------------------------------------------------------------------- /docker/ggz-server/manifest.tmpl: -------------------------------------------------------------------------------- 1 | image: goggz/ggz-server:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} 2 | {{#if build.tags}} 3 | tags: 4 | {{#each build.tags}} 5 | - {{this}} 6 | {{/each}} 7 | {{/if}} 8 | manifests: 9 | - 10 | image: goggz/ggz-server:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 11 | platform: 12 | architecture: amd64 13 | os: linux 14 | - 15 | image: goggz/ggz-server:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 16 | platform: 17 | architecture: arm64 18 | os: linux 19 | variant: v8 20 | - 21 | image: goggz/ggz-server:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm 22 | platform: 23 | architecture: arm 24 | os: linux 25 | variant: v7 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-ggz/ggz 2 | 3 | go 1.12 4 | 5 | require ( 6 | firebase.google.com/go v3.8.0+incompatible 7 | github.com/appleboy/com v0.0.1 8 | github.com/appleboy/gofight/v2 v2.1.2 9 | github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7 10 | github.com/aws/aws-sdk-go v1.19.41 11 | github.com/codegangsta/negroni v1.0.0 // indirect 12 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 13 | github.com/gin-contrib/gzip v0.0.1 14 | github.com/gin-contrib/logger v0.0.1 15 | github.com/gin-contrib/pprof v1.2.0 16 | github.com/gin-gonic/gin v1.5.0 17 | github.com/go-ggz/ui v0.0.1 18 | github.com/go-ini/ini v1.42.0 // indirect 19 | github.com/go-sql-driver/mysql v1.4.1 20 | github.com/google/go-cmp v0.3.0 // indirect 21 | github.com/googollee/go-socket.io v1.4.1 22 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect 23 | github.com/gorilla/mux v1.7.2 // indirect 24 | github.com/graphql-go/graphql v0.7.9 25 | github.com/graphql-go/handler v0.2.3 26 | github.com/hashicorp/golang-lru v0.5.1 27 | github.com/joho/godotenv v1.3.0 28 | github.com/keighl/metabolize v0.0.0-20150915210303-97ab655d4034 29 | github.com/kelseyhightower/envconfig v1.4.0 30 | github.com/lib/pq v1.1.1 31 | github.com/mattn/go-oci8 v0.0.0-20190320171441-14ba190cf52d // indirect 32 | github.com/mattn/go-sqlite3 v1.10.0 33 | github.com/minio/minio-go v6.0.14+incompatible 34 | github.com/mitchellh/go-homedir v1.1.0 // indirect 35 | github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect 36 | github.com/opentracing/opentracing-go v1.1.0 // indirect 37 | github.com/patrickmn/go-cache v2.1.0+incompatible 38 | github.com/prometheus/client_golang v0.9.3 39 | github.com/rs/zerolog v1.14.3 40 | github.com/satori/go.uuid v1.2.0 41 | github.com/scorredoira/email v0.0.0-20190509221456-365bb6a9fa0c 42 | github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 43 | github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect 44 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect 45 | github.com/stretchr/testify v1.4.0 46 | github.com/testcontainers/testcontainers-go v0.0.4 47 | github.com/urfave/cli/v2 v2.0.0 48 | golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 49 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 50 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 51 | golang.org/x/text v0.3.2 // indirect 52 | google.golang.org/api v0.5.0 53 | gopkg.in/ini.v1 v1.42.0 // indirect 54 | gopkg.in/nicksrandall/dataloader.v5 v5.0.0 55 | gopkg.in/testfixtures.v2 v2.5.3 56 | xorm.io/core v0.7.2 57 | xorm.io/xorm v0.8.0 58 | ) 59 | 60 | replace github.com/airking05/termui v2.2.0+incompatible => github.com/airking05/termui v0.0.0-20180528121417-a42394913439 61 | -------------------------------------------------------------------------------- /integrations/container_test.go: -------------------------------------------------------------------------------- 1 | package integrations 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/testcontainers/testcontainers-go" 10 | "github.com/testcontainers/testcontainers-go/wait" 11 | ) 12 | 13 | func TestGGZServer(t *testing.T) { 14 | ctx := context.Background() 15 | req := testcontainers.ContainerRequest{ 16 | Image: "goggz/ggz-server", 17 | ExposedPorts: []string{"8080/tcp"}, 18 | WaitingFor: wait.ForLog("Starting shorten server on :8080"), 19 | } 20 | ggzServer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 21 | ContainerRequest: req, 22 | Started: true, 23 | }) 24 | 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | // At the end of the test remove the container 30 | defer ggzServer.Terminate(ctx) 31 | // Retrieve the container IP 32 | ip, err := ggzServer.Host(ctx) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | // Retrieve the port mapped to port 8080 38 | port, err := ggzServer.MappedPort(ctx, "8080") 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | resp, err := http.Get(fmt.Sprintf("http://%s:%s/", ip, port.Port())) 43 | 44 | if resp.StatusCode != http.StatusOK { 45 | t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pipeline.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | test:: { 3 | kind: 'pipeline', 4 | name: 'testing', 5 | platform: { 6 | os: 'linux', 7 | arch: 'amd64', 8 | }, 9 | steps: [ 10 | { 11 | name: 'generate', 12 | image: 'golang:1.13', 13 | pull: 'always', 14 | commands: [ 15 | 'make generate', 16 | ], 17 | volumes: [ 18 | { 19 | name: 'gopath', 20 | path: '/go', 21 | }, 22 | ], 23 | }, 24 | { 25 | name: 'vet', 26 | image: 'golang:1.13', 27 | pull: 'always', 28 | commands: [ 29 | 'make vet', 30 | ], 31 | volumes: [ 32 | { 33 | name: 'gopath', 34 | path: '/go', 35 | }, 36 | ], 37 | }, 38 | { 39 | name: 'lint', 40 | image: 'golang:1.13', 41 | pull: 'always', 42 | commands: [ 43 | 'make lint', 44 | ], 45 | volumes: [ 46 | { 47 | name: 'gopath', 48 | path: '/go', 49 | }, 50 | ], 51 | }, 52 | { 53 | name: 'misspell', 54 | image: 'golang:1.13', 55 | pull: 'always', 56 | commands: [ 57 | 'make misspell-check', 58 | ], 59 | volumes: [ 60 | { 61 | name: 'gopath', 62 | path: '/go', 63 | }, 64 | ], 65 | }, 66 | { 67 | name: 'embedmd', 68 | image: 'golang:1.13', 69 | pull: 'always', 70 | commands: [ 71 | 'make embedmd', 72 | ], 73 | volumes: [ 74 | { 75 | name: 'gopath', 76 | path: '/go', 77 | }, 78 | ], 79 | }, 80 | { 81 | name: 'test', 82 | image: 'golang:1.13', 83 | pull: 'always', 84 | commands: [ 85 | 'make test', 86 | ], 87 | volumes: [ 88 | { 89 | name: 'gopath', 90 | path: '/go', 91 | }, 92 | ], 93 | }, 94 | { 95 | name: 'codecov', 96 | image: 'robertstettner/drone-codecov', 97 | pull: 'always', 98 | settings: { 99 | token: { 'from_secret': 'codecov_token' }, 100 | }, 101 | }, 102 | ], 103 | volumes: [ 104 | { 105 | name: 'gopath', 106 | temp: {}, 107 | }, 108 | ], 109 | }, 110 | 111 | build(name, os='linux', arch='amd64', cgo=false):: 112 | local build_sqlite = if cgo then "-tags 'sqlite sqlite_unlock_notify'" else ""; 113 | local build_static = if cgo then "-extldflags -static" else ""; 114 | { 115 | kind: 'pipeline', 116 | name: name + '-' + os + '-' + arch, 117 | platform: { 118 | os: os, 119 | arch: arch, 120 | }, 121 | steps: [ 122 | { 123 | name: 'build-push', 124 | image: 'golang:1.13', 125 | pull: 'always', 126 | environment: { 127 | CGO_ENABLED: if cgo then "1" else "0", 128 | }, 129 | commands: [ 130 | 'make generate', 131 | 'go build -v '+ build_sqlite +' -ldflags "'+ build_static +' -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_COMMIT_SHA:0:8} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/' + os + '/' + arch + '/' + name + ' ./cmd/' + name, 132 | ], 133 | when: { 134 | event: { 135 | exclude: [ 'tag' ], 136 | }, 137 | }, 138 | }, 139 | { 140 | name: 'build-tag', 141 | image: 'golang:1.13', 142 | pull: 'always', 143 | environment: { 144 | CGO_ENABLED: if cgo then "1" else "0", 145 | }, 146 | commands: [ 147 | 'make generate', 148 | 'go build -v '+ build_sqlite +' -ldflags "'+ build_static +' -X github.com/go-ggz/ggz/pkg/version.Version=${DRONE_TAG##v} -X github.com/go-ggz/ggz/pkg/version.BuildDate=`date -u +%Y-%m-%dT%H:%M:%SZ`" -a -o release/' + os + '/' + arch + '/' + name + ' ./cmd/' + name, 149 | ], 150 | when: { 151 | event: [ 'tag' ], 152 | }, 153 | }, 154 | { 155 | name: 'executable', 156 | image: 'golang:1.13', 157 | pull: 'always', 158 | commands: [ 159 | './release/' + os + '/' + arch + '/' + name + ' --help', 160 | ], 161 | }, 162 | { 163 | name: 'dryrun', 164 | image: 'plugins/docker:' + os + '-' + arch, 165 | pull: 'always', 166 | settings: { 167 | daemon_off: false, 168 | dry_run: true, 169 | tags: os + '-' + arch, 170 | dockerfile: 'docker/' + name + '/Dockerfile.' + os + '.' + arch, 171 | repo: 'goggz/' + name, 172 | cache_from: 'goggz/' + name, 173 | }, 174 | when: { 175 | event: [ 'pull_request' ], 176 | }, 177 | }, 178 | { 179 | name: 'publish', 180 | image: 'plugins/docker:' + os + '-' + arch, 181 | pull: 'always', 182 | settings: { 183 | daemon_off: 'false', 184 | auto_tag: true, 185 | auto_tag_suffix: os + '-' + arch, 186 | dockerfile: 'docker/' + name + '/Dockerfile.' + os + '.' + arch, 187 | repo: 'goggz/' + name, 188 | cache_from: 'goggz/' + name, 189 | username: { 'from_secret': 'docker_username' }, 190 | password: { 'from_secret': 'docker_password' }, 191 | }, 192 | when: { 193 | event: { 194 | exclude: [ 'pull_request' ], 195 | }, 196 | }, 197 | }, 198 | ], 199 | depends_on: [ 200 | 'testing', 201 | ], 202 | trigger: { 203 | ref: [ 204 | 'refs/heads/master', 205 | 'refs/pull/**', 206 | 'refs/tags/**', 207 | ], 208 | }, 209 | }, 210 | 211 | release:: { 212 | kind: 'pipeline', 213 | name: 'release-binary', 214 | platform: { 215 | os: 'linux', 216 | arch: 'amd64', 217 | }, 218 | steps: [ 219 | { 220 | name: 'generate', 221 | image: 'golang:1.13', 222 | pull: 'always', 223 | commands: [ 224 | 'make generate', 225 | ], 226 | volumes: [ 227 | { 228 | name: 'gopath', 229 | path: '/go', 230 | }, 231 | ], 232 | }, 233 | { 234 | name: 'build-all-binary', 235 | image: 'golang:1.13', 236 | pull: 'always', 237 | commands: [ 238 | 'make release' 239 | ], 240 | when: { 241 | event: [ 'tag' ], 242 | }, 243 | volumes: [ 244 | { 245 | name: 'gopath', 246 | path: '/go', 247 | }, 248 | ], 249 | }, 250 | { 251 | name: 'deploy-all-binary', 252 | image: 'plugins/github-release', 253 | pull: 'always', 254 | settings: { 255 | files: [ 'dist/release/*' ], 256 | api_key: { 'from_secret': 'github_release_api_key' }, 257 | }, 258 | when: { 259 | event: [ 'tag' ], 260 | }, 261 | }, 262 | ], 263 | depends_on: [ 264 | 'testing', 265 | ], 266 | trigger: { 267 | ref: [ 268 | 'refs/tags/**', 269 | ], 270 | }, 271 | }, 272 | 273 | notifications(name, os='linux', arch='amd64', depends_on=[]):: { 274 | kind: 'pipeline', 275 | name: name + '-notifications', 276 | platform: { 277 | os: os, 278 | arch: arch, 279 | }, 280 | steps: [ 281 | { 282 | name: 'manifest', 283 | image: 'plugins/manifest', 284 | pull: 'always', 285 | settings: { 286 | username: { from_secret: 'docker_username' }, 287 | password: { from_secret: 'docker_password' }, 288 | spec: 'docker/' + name + '/manifest.tmpl', 289 | ignore_missing: true, 290 | }, 291 | }, 292 | ], 293 | depends_on: depends_on, 294 | trigger: { 295 | ref: [ 296 | 'refs/heads/master', 297 | 'refs/tags/**', 298 | ], 299 | }, 300 | }, 301 | 302 | signature(key):: { 303 | kind: 'signature', 304 | hmac: key, 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type database struct { 8 | Driver string 9 | Username string 10 | Password string 11 | Name string 12 | Host string 13 | SSLMode string 14 | Path string 15 | TimeOut int 16 | } 17 | 18 | type server struct { 19 | Host string 20 | Addr string 21 | Cert string 22 | Key string 23 | Root string 24 | Storage string 25 | Assets string 26 | LetsEncrypt bool 27 | StrictCurves bool 28 | StrictCiphers bool 29 | Pprof bool 30 | Token string 31 | ShortenHost string 32 | ShortenSize int 33 | Cache string 34 | Debug bool `default:"true"` 35 | GraphiQL bool `default:"false"` 36 | } 37 | 38 | type storage struct { 39 | Driver string 40 | Path string 41 | } 42 | 43 | type admin struct { 44 | Users []string 45 | Create bool 46 | } 47 | 48 | type cache struct { 49 | Driver string 50 | Expire int 51 | Prefix string 52 | } 53 | 54 | type session struct { 55 | Expire time.Duration 56 | } 57 | 58 | type qrcode struct { 59 | Enable bool 60 | Bucket string 61 | } 62 | 63 | type s3 struct { 64 | AccessID string 65 | SecretKey string 66 | EndPoint string 67 | SSL bool 68 | Bucket string 69 | Region string 70 | } 71 | 72 | type auth0 struct { 73 | Key string 74 | PemPath string 75 | Debug bool 76 | } 77 | 78 | type auth struct { 79 | Driver string 80 | } 81 | 82 | type metrics struct { 83 | Token string 84 | Enabled bool 85 | } 86 | 87 | type logs struct { 88 | Color bool 89 | Debug bool 90 | Pretty bool 91 | Level string `default:"debug"` 92 | } 93 | 94 | // ContextKey for context package 95 | type ContextKey string 96 | 97 | func (c ContextKey) String() string { 98 | return "user context key " + string(c) 99 | } 100 | 101 | type mailService struct { 102 | Driver string 103 | } 104 | 105 | type smtp struct { 106 | Host string 107 | Port string 108 | Username string 109 | Password string 110 | } 111 | 112 | type aws struct { 113 | AccessID string 114 | SecretKey string 115 | } 116 | 117 | var ( 118 | // Database represents the current database connection details. 119 | Database = &database{} 120 | 121 | // Server represents the informations about the server bindings. 122 | Server = &server{} 123 | 124 | // Admin represents the informations about the admin config. 125 | Admin = &admin{} 126 | 127 | // Session represents the informations about the session handling. 128 | Session = &session{} 129 | 130 | // Storage represents the informations about the storage bindings. 131 | Storage = &storage{} 132 | 133 | // QRCode represents the informations about the qrcode settings. 134 | QRCode = &qrcode{} 135 | 136 | // Minio represents the informations about the Minio server. 137 | Minio = &s3{} 138 | 139 | // Auth0 token information 140 | Auth0 = &auth0{} 141 | 142 | // Auth driver 143 | Auth = &auth{} 144 | 145 | // ContextKeyUser for user 146 | ContextKeyUser = ContextKey("user") 147 | 148 | // Cache for redis, lur or memory cache 149 | Cache = &cache{} 150 | 151 | // Metrics config 152 | Metrics = &metrics{} 153 | 154 | // Logs for zerolog 155 | Logs = &logs{} 156 | 157 | // AWS config 158 | AWS = &aws{} 159 | 160 | // MailService mail setting 161 | MailService = &mailService{} 162 | 163 | // SMTP email setting 164 | SMTP = &smtp{} 165 | ) 166 | -------------------------------------------------------------------------------- /pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/rs/zerolog/log" 8 | ) 9 | 10 | // Error applicational 11 | type Error struct { 12 | Type Type 13 | Message string 14 | cause error 15 | } 16 | 17 | // Error message 18 | func (e *Error) Error() string { 19 | return e.Message 20 | } 21 | 22 | // Cause of the original error 23 | func (e *Error) Cause() string { 24 | if e.cause != nil { 25 | return e.cause.Error() 26 | } 27 | 28 | return "" 29 | } 30 | 31 | // Extensions for graphQL extension 32 | func (e *Error) Extensions() map[string]interface{} { 33 | if e.cause != nil { 34 | log.Error().Err(e.cause).Msg("graphql error report") 35 | } 36 | 37 | return map[string]interface{}{ 38 | "code": e.Type.Code(), 39 | "type": e.Type, 40 | } 41 | } 42 | 43 | // Type defines the type of an error 44 | type Type string 45 | 46 | const ( 47 | // Internal error 48 | Internal Type = "internal" 49 | // NotFound error means that a specific item does not exist 50 | NotFound Type = "not_found" 51 | // BadRequest error 52 | BadRequest Type = "bad_request" 53 | // Validation error 54 | Validation Type = "validation" 55 | // AlreadyExists error 56 | AlreadyExists Type = "already_exists" 57 | // Unauthorized error 58 | Unauthorized Type = "unauthorized" 59 | ) 60 | 61 | func (t Type) String() string { 62 | switch t { 63 | case Internal: 64 | return "Internal Error" 65 | case NotFound: 66 | return "Item not found" 67 | case BadRequest: 68 | return "BadRequest error" 69 | case Validation: 70 | return "Validation error" 71 | case AlreadyExists: 72 | return "Item already exists" 73 | case Unauthorized: 74 | return "Unauthorized error" 75 | } 76 | 77 | return "Unknown error" 78 | } 79 | 80 | // Code http error code 81 | func (t Type) Code() int { 82 | switch t { 83 | case Internal: 84 | return http.StatusInternalServerError 85 | case NotFound: 86 | return http.StatusNotFound 87 | case BadRequest: 88 | return http.StatusBadRequest 89 | case Validation: 90 | return http.StatusForbidden 91 | case AlreadyExists: 92 | return http.StatusBadRequest 93 | case Unauthorized: 94 | return http.StatusUnauthorized 95 | } 96 | 97 | return http.StatusInternalServerError 98 | } 99 | 100 | // New creates a new error 101 | func New(t Type, msg string, err error) error { 102 | return &Error{ 103 | Type: t, 104 | Message: msg, 105 | cause: err, 106 | } 107 | } 108 | 109 | // EValidation creates an error of type Validationn 110 | func EValidation(msg string, err error, arg ...interface{}) error { 111 | return New(Validation, fmt.Sprintf(msg, arg...), err) 112 | } 113 | 114 | // ENotExists creates an error of type NotExist 115 | func ENotExists(msg string, err error, arg ...interface{}) error { 116 | return New(NotFound, fmt.Sprintf(msg, arg...), err) 117 | } 118 | 119 | // EBadRequest creates an error of type BadRequest 120 | func EBadRequest(msg string, err error, arg ...interface{}) error { 121 | return New(BadRequest, fmt.Sprintf(msg, arg...), err) 122 | } 123 | 124 | // EAlreadyExists creates an error of type AlreadyExists 125 | func EAlreadyExists(msg string, err error, arg ...interface{}) error { 126 | return New(AlreadyExists, fmt.Sprintf(msg, arg...), err) 127 | } 128 | 129 | // EInternal creates an error of type Internal 130 | func EInternal(msg string, err error, arg ...interface{}) error { 131 | return New(Internal, fmt.Sprintf(msg, arg...), err) 132 | } 133 | 134 | // ENotFound creates an error of type NotFound 135 | func ENotFound(msg string, err error, arg ...interface{}) error { 136 | return New(NotFound, fmt.Sprintf(msg, arg...), err) 137 | } 138 | 139 | // EUnauthorized creates an error of type Unauthorized 140 | func EUnauthorized(msg string, err error, arg ...interface{}) error { 141 | return New(Unauthorized, fmt.Sprintf(msg, arg...), err) 142 | } 143 | 144 | // Is method checks if an error is of a specific type 145 | func Is(t Type, err error) bool { 146 | e, ok := err.(*Error) 147 | 148 | return ok && e.Type == t 149 | } 150 | -------------------------------------------------------------------------------- /pkg/errors/errors_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestIs(t *testing.T) { 12 | type args struct { 13 | t Type 14 | err error 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want bool 20 | }{ 21 | { 22 | name: "Internal check", 23 | args: args{Internal, EInternal("Internal error", nil)}, 24 | want: true, 25 | }, 26 | { 27 | name: "NotFound check", 28 | args: args{NotFound, ENotFound("NotFound error", nil)}, 29 | want: true, 30 | }, 31 | { 32 | name: "BadRequest check", 33 | args: args{BadRequest, EBadRequest("BadRequest error", nil)}, 34 | want: true, 35 | }, 36 | { 37 | name: "Validation check", 38 | args: args{Validation, EValidation("Validation error", nil)}, 39 | want: true, 40 | }, 41 | { 42 | name: "EAlreadyExists check", 43 | args: args{AlreadyExists, EAlreadyExists("EAlreadyExists error", nil)}, 44 | want: true, 45 | }, 46 | { 47 | name: "EUnauthorized check", 48 | args: args{Unauthorized, EUnauthorized("EUnauthorized error", nil)}, 49 | want: true, 50 | }, 51 | { 52 | name: "new error check", 53 | args: args{Internal, New(Internal, "error", nil)}, 54 | want: true, 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | if got := Is(tt.args.t, tt.args.err); got != tt.want { 60 | t.Errorf("Is() = %v, want %v", got, tt.want) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | func TestErrorType(t *testing.T) { 67 | t.Run("EValidation", func(t *testing.T) { 68 | err := EValidation("not validation", errors.New("test")) 69 | assert.Equal(t, "not validation", err.Error()) 70 | assert.Equal(t, "test", err.(*Error).Cause()) 71 | assert.Equal(t, http.StatusForbidden, err.(*Error).Type.Code()) 72 | assert.Equal(t, "Validation error", err.(*Error).Type.String()) 73 | }) 74 | 75 | t.Run("EBadRequest", func(t *testing.T) { 76 | err := EBadRequest("not EBadRequest", nil) 77 | assert.Equal(t, "not EBadRequest", err.Error()) 78 | assert.Equal(t, "", err.(*Error).Cause()) 79 | assert.Equal(t, http.StatusBadRequest, err.(*Error).Type.Code()) 80 | assert.Equal(t, "BadRequest error", err.(*Error).Type.String()) 81 | }) 82 | 83 | t.Run("EAlreadyExists", func(t *testing.T) { 84 | err := EAlreadyExists("not EAlreadyExists", errors.New("test")) 85 | assert.Equal(t, "not EAlreadyExists", err.Error()) 86 | assert.Equal(t, "test", err.(*Error).Cause()) 87 | assert.Equal(t, http.StatusBadRequest, err.(*Error).Type.Code()) 88 | assert.Equal(t, "Item already exists", err.(*Error).Type.String()) 89 | }) 90 | 91 | t.Run("EInternal", func(t *testing.T) { 92 | err := EInternal("not EInternal", errors.New("test")) 93 | assert.Equal(t, "not EInternal", err.Error()) 94 | assert.Equal(t, "test", err.(*Error).Cause()) 95 | assert.Equal(t, http.StatusInternalServerError, err.(*Error).Type.Code()) 96 | assert.Equal(t, "Internal Error", err.(*Error).Type.String()) 97 | }) 98 | 99 | t.Run("ENotFound", func(t *testing.T) { 100 | err := ENotFound("not validation", errors.New("test")) 101 | assert.Equal(t, "not validation", err.Error()) 102 | assert.Equal(t, "test", err.(*Error).Cause()) 103 | assert.Equal(t, http.StatusNotFound, err.(*Error).Type.Code()) 104 | assert.Equal(t, "Item not found", err.(*Error).Type.String()) 105 | }) 106 | 107 | t.Run("EUnauthorized", func(t *testing.T) { 108 | err := EUnauthorized("not validation", errors.New("test")) 109 | assert.Equal(t, "not validation", err.Error()) 110 | assert.Equal(t, "test", err.(*Error).Cause()) 111 | assert.Equal(t, http.StatusUnauthorized, err.(*Error).Type.Code()) 112 | assert.Equal(t, "Unauthorized error", err.(*Error).Type.String()) 113 | }) 114 | 115 | t.Run("Unknown Error", func(t *testing.T) { 116 | unknown := Type("Unknown") 117 | assert.Equal(t, http.StatusInternalServerError, unknown.Code()) 118 | assert.Equal(t, "Unknown error", unknown.String()) 119 | }) 120 | } 121 | 122 | func TestType_String(t *testing.T) { 123 | tests := []struct { 124 | name string 125 | t Type 126 | want string 127 | }{ 128 | { 129 | name: "Internal", 130 | t: Internal, 131 | want: "Internal Error", 132 | }, 133 | { 134 | name: "NotFound", 135 | t: NotFound, 136 | want: "Item not found", 137 | }, 138 | { 139 | name: "AlreadyExists", 140 | t: AlreadyExists, 141 | want: "Item already exists", 142 | }, 143 | { 144 | name: "Validation", 145 | t: Validation, 146 | want: "Validation error", 147 | }, 148 | { 149 | name: "BadRequest", 150 | t: BadRequest, 151 | want: "BadRequest error", 152 | }, 153 | { 154 | name: "Unauthorized", 155 | t: Unauthorized, 156 | want: "Unauthorized error", 157 | }, 158 | } 159 | for _, tt := range tests { 160 | t.Run(tt.name, func(t *testing.T) { 161 | if got := tt.t.String(); got != tt.want { 162 | t.Errorf("Type.String() = %v, want %v", got, tt.want) 163 | } 164 | }) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /pkg/fixtures/shorten.yml: -------------------------------------------------------------------------------- 1 | - 2 | slug: abcdef 3 | user_id : 1 4 | url: http://example.com 5 | hits: 1000 6 | 7 | -------------------------------------------------------------------------------- /pkg/fixtures/user.yml: -------------------------------------------------------------------------------- 1 | - 2 | id: 1 3 | email: test@gmail.com 4 | full_name: test 5 | avatar: http://example.com 6 | avatar_email: test@gmail.com 7 | 8 | - 9 | id: 2 10 | email: test1234@gmail.com 11 | full_name: test1234 12 | avatar: http://example.com 13 | avatar_email: test1234@gmail.com 14 | -------------------------------------------------------------------------------- /pkg/helper/jwt.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-ggz/ggz/pkg/config" 7 | "github.com/go-ggz/ggz/pkg/model" 8 | 9 | "github.com/dgrijalva/jwt-go" 10 | ) 11 | 12 | // GetUserDataFromToken from jwt parse token 13 | func GetUserDataFromToken(ctx context.Context) jwt.MapClaims { 14 | if _, ok := ctx.Value("user").(*jwt.Token); !ok { 15 | return jwt.MapClaims{} 16 | } 17 | 18 | return ctx.Value("user").(*jwt.Token).Claims.(jwt.MapClaims) 19 | } 20 | 21 | // GetUserDataFromModel from user model 22 | func GetUserDataFromModel(ctx context.Context) *model.User { 23 | if _, ok := ctx.Value(config.ContextKeyUser).(*model.User); !ok { 24 | return nil 25 | } 26 | 27 | return ctx.Value(config.ContextKeyUser).(*model.User) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/helper/qrcode.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/go-ggz/ggz/pkg/config" 8 | "github.com/go-ggz/ggz/pkg/module/storage" 9 | 10 | "github.com/skip2/go-qrcode" 11 | ) 12 | 13 | // QRCodeGenerator create QRCode 14 | func QRCodeGenerator(slug string) error { 15 | objectName := fmt.Sprintf("%s.png", slug) 16 | host := strings.TrimRight(config.Server.ShortenHost, "/") 17 | png, err := qrcode.Encode(host+"/"+slug, qrcode.Medium, 256) 18 | if err != nil { 19 | return nil 20 | } 21 | 22 | return storage.S3.UploadFile( 23 | config.Minio.Bucket, 24 | objectName, 25 | png, 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/helper/validator.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "net/url" 5 | "regexp" 6 | "strings" 7 | "unicode/utf8" 8 | ) 9 | 10 | const maxURLRuneCount = 2083 11 | const minURLRuneCount = 3 12 | 13 | const ( 14 | // IP string 15 | IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))` 16 | // URLSchema string 17 | URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)` 18 | // URLUsername string 19 | URLUsername string = `(\S+(:\S*)?@)` 20 | // URLPath string 21 | URLPath string = `((\/|\?|#)[^\s]*)` 22 | // URLPort string 23 | URLPort string = `(:(\d{1,5}))` 24 | // URLIP string 25 | URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))` 26 | // URLSubdomain string 27 | URLSubdomain string = `((www\.)|([a-zA-Z0-9]+([-_\.]?[a-zA-Z0-9])*[a-zA-Z0-9]\.[a-zA-Z0-9]+))` 28 | // URL string 29 | URL string = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$` 30 | ) 31 | 32 | var ( 33 | rxURL = regexp.MustCompile(URL) 34 | ) 35 | 36 | // IsURL check if the string is an URL. 37 | func IsURL(str string) bool { 38 | if str == "" || utf8.RuneCountInString(str) >= maxURLRuneCount || len(str) <= minURLRuneCount || strings.HasPrefix(str, ".") { 39 | return false 40 | } 41 | strTemp := str 42 | if strings.Contains(str, ":") && !strings.Contains(str, "://") { 43 | // support no indicated urlscheme but with colon for port number 44 | // http:// is appended so url.Parse will succeed, strTemp used so it does not impact rxURL.MatchString 45 | strTemp = "http://" + str 46 | } 47 | u, err := url.Parse(strTemp) 48 | if err != nil { 49 | return false 50 | } 51 | if strings.HasPrefix(u.Host, ".") { 52 | return false 53 | } 54 | if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) { 55 | return false 56 | } 57 | return rxURL.MatchString(str) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/middleware/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/go-ggz/ggz/pkg/config" 5 | "github.com/go-ggz/ggz/pkg/middleware/auth/auth0" 6 | "github.com/go-ggz/ggz/pkg/middleware/auth/firebase" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | // Check initializes the auth0 middleware. 13 | func Check() gin.HandlerFunc { 14 | switch config.Auth.Driver { 15 | case "auth0": 16 | return auth0.Check() 17 | case "firebase": 18 | return firebase.Check() 19 | default: 20 | log.Fatal().Msgf("Can't find the auth driver: %s", config.Auth.Driver) 21 | } 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/middleware/auth/auth0/auth0.go: -------------------------------------------------------------------------------- 1 | package auth0 2 | 3 | import ( 4 | "context" 5 | "crypto/rsa" 6 | "errors" 7 | "net/http" 8 | 9 | "github.com/go-ggz/ggz/assets" 10 | "github.com/go-ggz/ggz/pkg/config" 11 | "github.com/go-ggz/ggz/pkg/helper" 12 | "github.com/go-ggz/ggz/pkg/model" 13 | 14 | jwtmiddleware "github.com/auth0/go-jwt-middleware" 15 | "github.com/dgrijalva/jwt-go" 16 | "github.com/gin-gonic/gin" 17 | "github.com/rs/zerolog/log" 18 | ) 19 | 20 | // ParseRSAPublicKeyFromPEM Parse PEM encoded PKCS1 or PKCS8 public key 21 | func ParseRSAPublicKeyFromPEM() (*rsa.PublicKey, error) { 22 | var reader []byte 23 | var err error 24 | 25 | if config.Auth0.Key != "" { 26 | reader = []byte(config.Auth0.Key) 27 | } else { 28 | reader, err = assets.ReadSource(config.Auth0.PemPath) 29 | if err != nil { 30 | log.Warn().Err(err).Msgf("Failed to read builtin %s template.", reader) 31 | return nil, errors.New("Failed to read builtin auth0 pem file") 32 | } 33 | } 34 | 35 | return jwt.ParseRSAPublicKeyFromPEM(reader) 36 | } 37 | 38 | func errorHandler(w http.ResponseWriter, r *http.Request, err string) { 39 | } 40 | 41 | // Check initializes the auth0 middleware. 42 | func Check() gin.HandlerFunc { 43 | var user *model.User 44 | return func(c *gin.Context) { 45 | jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{ 46 | ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { 47 | return ParseRSAPublicKeyFromPEM() 48 | }, 49 | SigningMethod: jwt.SigningMethodRS256, 50 | ErrorHandler: errorHandler, 51 | Debug: config.Auth0.Debug, 52 | }) 53 | 54 | err := jwtMiddleware.CheckJWT(c.Writer, c.Request) 55 | 56 | // If there was an error, do not continue. 57 | if err != nil { 58 | c.Next() 59 | } else { 60 | userClaim := helper.GetUserDataFromToken(c.Request.Context()) 61 | if _, ok := userClaim["email"]; !ok { 62 | c.AbortWithStatusJSON( 63 | http.StatusOK, 64 | gin.H{ 65 | "data": nil, 66 | "errors": []map[string]interface{}{ 67 | { 68 | "message": "email not found.", 69 | }, 70 | }, 71 | }, 72 | ) 73 | return 74 | } 75 | 76 | // check user exist 77 | user, err = model.GetUserByEmail(userClaim["email"].(string)) 78 | 79 | if err != nil { 80 | if !model.IsErrUserNotExist(err) { 81 | log.Error().Err(err).Msg("database error.") 82 | c.AbortWithStatusJSON( 83 | http.StatusBadRequest, 84 | gin.H{ 85 | "data": nil, 86 | "errors": []map[string]interface{}{ 87 | { 88 | "message": "database query error", 89 | }, 90 | }, 91 | }, 92 | ) 93 | return 94 | } 95 | 96 | // create new user 97 | user = &model.User{ 98 | Email: userClaim["email"].(string), 99 | FullName: userClaim["name"].(string), 100 | IsActive: userClaim["email_verified"].(bool), 101 | } 102 | err := model.CreateUser(user) 103 | 104 | if err != nil { 105 | log.Error().Err(err).Msg("database error.") 106 | c.AbortWithStatusJSON( 107 | http.StatusOK, 108 | gin.H{ 109 | "data": nil, 110 | "errors": []map[string]interface{}{ 111 | { 112 | "message": "can't create new user", 113 | }, 114 | }, 115 | }, 116 | ) 117 | return 118 | } 119 | } 120 | 121 | ctx := context.WithValue(c.Request.Context(), config.ContextKeyUser, user) 122 | c.Request = c.Request.WithContext(ctx) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /pkg/middleware/auth/firebase/firebase.go: -------------------------------------------------------------------------------- 1 | package firebase 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/go-ggz/ggz/assets" 10 | "github.com/go-ggz/ggz/pkg/config" 11 | "github.com/go-ggz/ggz/pkg/model" 12 | 13 | "firebase.google.com/go" 14 | "github.com/gin-gonic/gin" 15 | "github.com/rs/zerolog/log" 16 | "google.golang.org/api/option" 17 | ) 18 | 19 | var ( 20 | // ErrEmptyAuthHeader can be thrown if authing with a HTTP header, the Auth header needs to be set 21 | ErrEmptyAuthHeader = errors.New("auth header is empty") 22 | 23 | // ErrInvalidAuthHeader indicates auth header is invalid, could for example have the wrong Realm name 24 | ErrInvalidAuthHeader = errors.New("auth header is invalid") 25 | ) 26 | 27 | func getFromHeader(c *gin.Context, key string) (string, error) { 28 | authHeader := c.Request.Header.Get(key) 29 | 30 | if authHeader == "" { 31 | return "", ErrEmptyAuthHeader 32 | } 33 | 34 | parts := strings.SplitN(authHeader, " ", 2) 35 | if !(len(parts) == 2 && parts[0] == "Bearer") { 36 | return "", ErrInvalidAuthHeader 37 | } 38 | 39 | return parts[1], nil 40 | } 41 | 42 | // Check initializes the firebase middleware. 43 | func Check() gin.HandlerFunc { 44 | credentials, err := assets.ReadSource("/firebase/serviceAccountKey.json") 45 | if err != nil { 46 | log.Fatal().Err(err).Msg("can't load credentials") 47 | } 48 | ctx := context.Background() 49 | opt := option.WithCredentialsJSON(credentials) 50 | app, err := firebase.NewApp(ctx, nil, opt) 51 | if err != nil { 52 | log.Fatal().Err(err).Msg("can't initial firebase app") 53 | } 54 | 55 | client, err := app.Auth(ctx) 56 | if err != nil { 57 | log.Fatal().Err(err).Msg("can't initial firebase client") 58 | } 59 | 60 | return func(c *gin.Context) { 61 | token, err := getFromHeader(c, "Authorization") 62 | 63 | if err != nil { 64 | c.Next() 65 | } else { 66 | userData, err := client.VerifyIDToken(ctx, token) 67 | if err != nil { 68 | log.Error().Err(err).Msg("verify firebase token error.") 69 | c.AbortWithStatusJSON( 70 | http.StatusOK, 71 | gin.H{ 72 | "data": nil, 73 | "errors": []map[string]interface{}{ 74 | { 75 | "message": "token expire or parse error", 76 | }, 77 | }, 78 | }, 79 | ) 80 | return 81 | } 82 | 83 | // check user exist 84 | user, err := model.GetUserByEmail(userData.Claims["email"].(string)) 85 | 86 | if err != nil { 87 | if !model.IsErrUserNotExist(err) { 88 | log.Error().Err(err).Msg("database error.") 89 | c.AbortWithStatusJSON( 90 | http.StatusBadRequest, 91 | gin.H{ 92 | "data": nil, 93 | "errors": []map[string]interface{}{ 94 | { 95 | "message": "database query error", 96 | }, 97 | }, 98 | }, 99 | ) 100 | return 101 | } 102 | 103 | // create new user 104 | user = &model.User{ 105 | Email: userData.Claims["email"].(string), 106 | IsActive: userData.Claims["email_verified"].(bool), 107 | } 108 | 109 | if v, ok := userData.Claims["name"]; ok { 110 | user.FullName = v.(string) 111 | } 112 | 113 | err := model.CreateUser(user) 114 | 115 | if err != nil { 116 | log.Error().Err(err).Msg("database error.") 117 | c.AbortWithStatusJSON( 118 | http.StatusOK, 119 | gin.H{ 120 | "data": nil, 121 | "errors": []map[string]interface{}{ 122 | { 123 | "message": "can't create new user", 124 | }, 125 | }, 126 | }, 127 | ) 128 | return 129 | } 130 | } 131 | 132 | ctx := context.WithValue(c.Request.Context(), config.ContextKeyUser, user) 133 | c.Request = c.Request.WithContext(ctx) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /pkg/middleware/header/header.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // Options is a middleware function that appends headers 10 | // for options requests and aborts then exits the middleware 11 | // chain and ends the request. 12 | func Options(c *gin.Context) { 13 | if c.Request.Method != "OPTIONS" { 14 | c.Next() 15 | } else { 16 | c.Header("Access-Control-Allow-Origin", "*") 17 | c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") 18 | c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept") 19 | c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") 20 | c.Header("Content-Type", "application/json") 21 | c.AbortWithStatus(http.StatusNoContent) 22 | } 23 | } 24 | 25 | // Secure is a middleware function that appends security 26 | // and resource access headers. 27 | func Secure(c *gin.Context) { 28 | c.Header("Access-Control-Allow-Origin", "*") 29 | c.Header("X-Frame-Options", "DENY") 30 | c.Header("X-Content-Type-Options", "nosniff") 31 | c.Header("X-XSS-Protection", "1; mode=block") 32 | if c.Request.TLS != nil { 33 | c.Header("Strict-Transport-Security", "max-age=31536000") 34 | } 35 | 36 | // Also consider adding Content-Security-Policy headers 37 | // c.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com") 38 | } 39 | -------------------------------------------------------------------------------- /pkg/model/errors.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // ErrURLExist represents a "ErrURLExist" kind of error. 9 | type ErrURLExist struct { 10 | Slug string 11 | URL string 12 | } 13 | 14 | // IsErrURLExist checks if an error is a ErrURLExist. 15 | func IsErrURLExist(err error) bool { 16 | return errors.As(err, &ErrURLExist{}) 17 | } 18 | 19 | func (err ErrURLExist) Error() string { 20 | return fmt.Sprintf("URL exist, slug: %s, url: %s", err.Slug, err.URL) 21 | } 22 | 23 | // ErrUserNotExist represents a "UserNotExist" kind of error. 24 | type ErrUserNotExist struct { 25 | UID int64 26 | Name string 27 | KeyID int64 28 | } 29 | 30 | // IsErrUserNotExist checks if an error is a ErrUserNotExist. 31 | func IsErrUserNotExist(err error) bool { 32 | return errors.As(err, &ErrUserNotExist{}) 33 | } 34 | 35 | func (err ErrUserNotExist) Error() string { 36 | return fmt.Sprintf("user does not exist [uid: %d, name: %s, keyid: %d]", err.UID, err.Name, err.KeyID) 37 | } 38 | 39 | // ErrShortenNotExist represents a "ShortenNotExist" kind of error. 40 | type ErrShortenNotExist struct { 41 | Slug string 42 | } 43 | 44 | // IsErrShortenNotExist checks if an error is a ErrUserNotExist. 45 | func IsErrShortenNotExist(err error) bool { 46 | return errors.As(err, &ErrShortenNotExist{}) 47 | } 48 | 49 | func (err ErrShortenNotExist) Error() string { 50 | return fmt.Sprintf("shorten slug does not exist [slug: %s]", err.Slug) 51 | } 52 | 53 | // ErrUserAlreadyExist represents a "user already exists" error. 54 | type ErrUserAlreadyExist struct { 55 | Name string 56 | } 57 | 58 | // IsErrUserAlreadyExist checks if an error is a ErrUserAlreadyExists. 59 | func IsErrUserAlreadyExist(err error) bool { 60 | return errors.As(err, &ErrUserAlreadyExist{}) 61 | } 62 | 63 | func (err ErrUserAlreadyExist) Error() string { 64 | return fmt.Sprintf("user already exists [name: %s]", err.Name) 65 | } 66 | 67 | // ErrEmailAlreadyUsed represents a "EmailAlreadyUsed" kind of error. 68 | type ErrEmailAlreadyUsed struct { 69 | Email string 70 | } 71 | 72 | // IsErrEmailAlreadyUsed checks if an error is a ErrEmailAlreadyUsed. 73 | func IsErrEmailAlreadyUsed(err error) bool { 74 | return errors.As(err, &ErrEmailAlreadyUsed{}) 75 | } 76 | 77 | func (err ErrEmailAlreadyUsed) Error() string { 78 | return fmt.Sprintf("e-mail has been used [email: %s]", err.Email) 79 | } 80 | 81 | // ErrAccessTokenNotExist represents a "AccessTokenNotExist" kind of error. 82 | type ErrAccessTokenNotExist struct { 83 | SHA string 84 | } 85 | 86 | // IsErrAccessTokenNotExist checks if an error is a ErrAccessTokenNotExist. 87 | func IsErrAccessTokenNotExist(err error) bool { 88 | return errors.As(err, &ErrAccessTokenNotExist{}) 89 | } 90 | 91 | func (err ErrAccessTokenNotExist) Error() string { 92 | return fmt.Sprintf("access token does not exist [sha: %s]", err.SHA) 93 | } 94 | 95 | // ErrAccessTokenEmpty represents a "AccessTokenEmpty" kind of error. 96 | type ErrAccessTokenEmpty struct { 97 | } 98 | 99 | // IsErrAccessTokenEmpty checks if an error is a ErrAccessTokenEmpty. 100 | func IsErrAccessTokenEmpty(err error) bool { 101 | return errors.As(err, &ErrAccessTokenEmpty{}) 102 | } 103 | 104 | func (err ErrAccessTokenEmpty) Error() string { 105 | return fmt.Sprintf("access token is empty") 106 | } 107 | -------------------------------------------------------------------------------- /pkg/model/main_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMain(m *testing.M) { 8 | MainTest(m, "../..") 9 | } 10 | -------------------------------------------------------------------------------- /pkg/model/migrate.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "xorm.io/xorm/migrate" 5 | ) 6 | 7 | // Migrations for db migrate 8 | var migrations = []*migrate.Migration{} 9 | -------------------------------------------------------------------------------- /pkg/model/models.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | "os" 9 | "path" 10 | "strings" 11 | "time" 12 | 13 | "github.com/go-ggz/ggz/pkg/config" 14 | 15 | // Needed for the MySQL driver 16 | _ "github.com/go-sql-driver/mysql" 17 | 18 | // Needed for the Postgresql driver 19 | _ "github.com/lib/pq" 20 | 21 | "xorm.io/core" 22 | "xorm.io/xorm" 23 | "xorm.io/xorm/migrate" 24 | ) 25 | 26 | var ( 27 | x *xorm.Engine 28 | tables []interface{} 29 | // EnableSQLite3 for enable sqlite 3 30 | EnableSQLite3 bool 31 | ) 32 | 33 | // Engine represents a xorm engine or session. 34 | type Engine interface { 35 | Table(tableNameOrBean interface{}) *xorm.Session 36 | Count(...interface{}) (int64, error) 37 | Decr(column string, arg ...interface{}) *xorm.Session 38 | Delete(interface{}) (int64, error) 39 | Exec(...interface{}) (sql.Result, error) 40 | Find(interface{}, ...interface{}) error 41 | Get(interface{}) (bool, error) 42 | ID(interface{}) *xorm.Session 43 | In(string, ...interface{}) *xorm.Session 44 | Incr(column string, arg ...interface{}) *xorm.Session 45 | Insert(...interface{}) (int64, error) 46 | InsertOne(interface{}) (int64, error) 47 | Iterate(interface{}, xorm.IterFunc) error 48 | Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *xorm.Session 49 | SQL(interface{}, ...interface{}) *xorm.Session 50 | Where(interface{}, ...interface{}) *xorm.Session 51 | } 52 | 53 | func init() { 54 | tables = append(tables, 55 | new(User), 56 | new(Shorten), 57 | ) 58 | 59 | gonicNames := []string{"SSL", "UID"} 60 | for _, name := range gonicNames { 61 | core.LintGonicMapper[name] = true 62 | } 63 | } 64 | 65 | func getEngine() (*xorm.Engine, error) { 66 | connStr := "" 67 | var Param = "?" 68 | if strings.Contains(config.Database.Name, Param) { 69 | Param = "&" 70 | } 71 | switch config.Database.Driver { 72 | case "mysql": 73 | if config.Database.Host[0] == '/' { // looks like a unix socket 74 | connStr = fmt.Sprintf("%s:%s@unix(%s)/%s%scharset=utf8&parseTime=true", 75 | config.Database.Username, config.Database.Password, config.Database.Host, config.Database.Name, Param) 76 | } else { 77 | connStr = fmt.Sprintf("%s:%s@tcp(%s)/%s%scharset=utf8&parseTime=true", 78 | config.Database.Username, config.Database.Password, config.Database.Host, config.Database.Name, Param) 79 | } 80 | case "postgres": 81 | host, port := parsePostgreSQLHostPort(config.Database.Host) 82 | if host[0] == '/' { // looks like a unix socket 83 | connStr = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&host=%s", 84 | url.QueryEscape(config.Database.Username), url.QueryEscape(config.Database.Password), port, config.Database.Name, Param, config.Database.SSLMode, host) 85 | } else { 86 | connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s", 87 | url.QueryEscape(config.Database.Username), url.QueryEscape(config.Database.Password), host, port, config.Database.Name, Param, config.Database.SSLMode) 88 | } 89 | case "sqlite3": 90 | if !EnableSQLite3 { 91 | return nil, errors.New("this binary version does not build support for SQLite3") 92 | } 93 | if err := os.MkdirAll(path.Dir(config.Database.Path), os.ModePerm); err != nil { 94 | return nil, fmt.Errorf("Failed to create directories: %v", err) 95 | } 96 | connStr = fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d", config.Database.Path, config.Database.TimeOut) 97 | default: 98 | return nil, fmt.Errorf("Unknown database type: %s", config.Database.Driver) 99 | } 100 | 101 | return xorm.NewEngine(config.Database.Driver, connStr) 102 | } 103 | 104 | // parsePostgreSQLHostPort parses given input in various forms defined in 105 | // https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING 106 | // and returns proper host and port number. 107 | func parsePostgreSQLHostPort(info string) (string, string) { 108 | host, port := "127.0.0.1", "5432" 109 | if strings.Contains(info, ":") && !strings.HasSuffix(info, "]") { 110 | idx := strings.LastIndex(info, ":") 111 | host = info[:idx] 112 | port = info[idx+1:] 113 | } else if len(info) > 0 { 114 | host = info 115 | } 116 | return host, port 117 | } 118 | 119 | // SetEngine sets the xorm.Engine 120 | func SetEngine() (err error) { 121 | x, err = getEngine() 122 | if err != nil { 123 | return fmt.Errorf("Failed to connect to database: %v", err) 124 | } 125 | 126 | x.SetMapper(core.GonicMapper{}) 127 | // WARNING: for serv command, MUST remove the output to os.stdout, 128 | // so use log file to instead print to stdout. 129 | // x.SetLogger(log.XORMLogger) 130 | x.ShowSQL(config.Server.Debug) 131 | 132 | // see https://github.com/go-gitea/gitea/pull/7071 133 | if config.Database.Driver == "mysql" { 134 | x.SetMaxIdleConns(0) 135 | x.SetConnMaxLifetime(3 * time.Second) 136 | } 137 | return nil 138 | } 139 | 140 | // NewEngine initializes a new xorm.Engine 141 | func NewEngine() (err error) { 142 | if err = SetEngine(); err != nil { 143 | return err 144 | } 145 | 146 | if err = x.Ping(); err != nil { 147 | return err 148 | } 149 | 150 | if err = x.StoreEngine("InnoDB").Sync2(tables...); err != nil { 151 | return fmt.Errorf("sync database struct error: %v", err) 152 | } 153 | 154 | m := migrate.New(x, migrate.DefaultOptions, migrations) 155 | if err = m.Migrate(); err != nil { 156 | return fmt.Errorf("migrate: %v", err) 157 | } 158 | 159 | return nil 160 | } 161 | 162 | // Statistic contains the database statistics 163 | type Statistic struct { 164 | Counter struct { 165 | User, Shorten int64 166 | } 167 | } 168 | 169 | // GetStatistic returns the database statistics 170 | func GetStatistic() (stats Statistic) { 171 | stats.Counter.User, _ = x.Count(new(User)) 172 | stats.Counter.Shorten, _ = x.Count(new(Shorten)) 173 | return 174 | } 175 | -------------------------------------------------------------------------------- /pkg/model/models_sqlite.go: -------------------------------------------------------------------------------- 1 | // +build sqlite 2 | 3 | package model 4 | 5 | import ( 6 | _ "github.com/mattn/go-sqlite3" 7 | ) 8 | 9 | func init() { 10 | EnableSQLite3 = true 11 | } 12 | -------------------------------------------------------------------------------- /pkg/model/models_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestParsePostgreSQLHostPort(t *testing.T) { 10 | tests := []struct { 11 | Name string 12 | HostPort string 13 | Host string 14 | Port string 15 | }{ 16 | { 17 | Name: "ip and port", 18 | HostPort: "127.0.0.1:1234", 19 | Host: "127.0.0.1", 20 | Port: "1234", 21 | }, 22 | { 23 | Name: "ip", 24 | HostPort: "127.0.0.1", 25 | Host: "127.0.0.1", 26 | Port: "5432", 27 | }, 28 | { 29 | Name: "ipv6 and port", 30 | HostPort: "[::1]:1234", 31 | Host: "[::1]", 32 | Port: "1234", 33 | }, 34 | { 35 | Name: "ipv6", 36 | HostPort: "[::1]", 37 | Host: "[::1]", 38 | Port: "5432", 39 | }, 40 | { 41 | Name: "socket and port", 42 | HostPort: "/tmp/pg.sock:1234", 43 | Host: "/tmp/pg.sock", 44 | Port: "1234", 45 | }, 46 | { 47 | Name: "socket", 48 | HostPort: "/tmp/pg.sock", 49 | Host: "/tmp/pg.sock", 50 | Port: "5432", 51 | }, 52 | } 53 | for _, tt := range tests { 54 | tt := tt 55 | t.Run(tt.Name, func(t *testing.T) { 56 | t.Parallel() 57 | host, port := parsePostgreSQLHostPort(tt.HostPort) 58 | assert.Equal(t, tt.Host, host) 59 | assert.Equal(t, tt.Port, port) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/model/shorten.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/go-ggz/ggz/pkg/module/meta" 8 | 9 | "github.com/appleboy/com/random" 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | // Shorten shortener URL 14 | type Shorten struct { 15 | Slug string `xorm:"pk VARCHAR(14)" json:"slug"` 16 | UserID int64 `xorm:"INDEX" json:"-"` 17 | URL string `xorm:"NOT NULL VARCHAR(620)" json:"url"` 18 | Date time.Time `json:"date"` 19 | Hits int64 `xorm:"NOT NULL DEFAULT 0" json:"hits"` 20 | Title string `xorm:"VARCHAR(512)" json:"title"` 21 | Description string `xorm:"TEXT" json:"description"` 22 | Type string `json:"type"` 23 | Image string `json:"image"` 24 | CreatedAt time.Time `xorm:"created" json:"created_at,omitempty"` 25 | UpdatedAt time.Time `xorm:"updated" json:"updated_at,omitempty"` 26 | 27 | // reference 28 | User *User `xorm:"-" json:"user"` 29 | } 30 | 31 | func getShortenBySlug(e Engine, slug string) (*Shorten, error) { 32 | s := new(Shorten) 33 | has, err := e.Where("slug = ?", slug).Get(s) 34 | if err != nil { 35 | return nil, err 36 | } else if !has { 37 | return nil, ErrShortenNotExist{slug} 38 | } 39 | return s, nil 40 | } 41 | 42 | // GetShortenBySlug returns the shorten object by given slug if exists. 43 | func GetShortenBySlug(slug string) (*Shorten, error) { 44 | return getShortenBySlug(x, slug) 45 | } 46 | 47 | // GetShortenFromURL check url exist 48 | func GetShortenFromURL(url string) (*Shorten, error) { 49 | shorten := new(Shorten) 50 | has, err := x. 51 | Where("url = ?", url). 52 | Get(shorten) 53 | 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | // get user data 59 | if shorten.UserID != 0 { 60 | u, _ := GetUserByID(shorten.UserID) 61 | if u != nil { 62 | shorten.User = u 63 | } 64 | } 65 | 66 | if has { 67 | return shorten, ErrURLExist{shorten.Slug, url} 68 | } 69 | 70 | return nil, nil 71 | } 72 | 73 | // CreateShorten create url item 74 | func CreateShorten(url string, size int, user *User) (_ *Shorten, err error) { 75 | row := &Shorten{ 76 | Date: time.Now(), 77 | URL: url, 78 | } 79 | exists := true 80 | slug := "" 81 | 82 | if user != nil { 83 | row.UserID = user.ID 84 | row.User = user 85 | } 86 | 87 | for exists { 88 | slug = random.String(size) 89 | _, err = getShortenBySlug(x, slug) 90 | if err != nil { 91 | if IsErrShortenNotExist(err) { 92 | exists = false 93 | continue 94 | } 95 | return nil, err 96 | } 97 | } 98 | 99 | row.Slug = slug 100 | 101 | if _, err := x.Insert(row); err != nil { 102 | return nil, err 103 | } 104 | 105 | go func() { 106 | if err := row.UpdateMetaData(); err != nil { 107 | log.Warn().Err(err).Msg("fail to update url metadata") 108 | } 109 | }() 110 | 111 | return row, nil 112 | } 113 | 114 | // UpdateHits udpate hit count 115 | func (s *Shorten) UpdateHits(slug string) error { 116 | if _, err := x.Exec("UPDATE `shorten` SET hits = hits + 1 WHERE slug = ?", slug); err != nil { 117 | return err 118 | } 119 | 120 | return nil 121 | } 122 | 123 | // UpdateMetaData form raw body 124 | func (s *Shorten) UpdateMetaData() error { 125 | data, err := meta.FetchData(s.URL) 126 | 127 | if err != nil { 128 | return err 129 | } 130 | 131 | s.Title = data.Title 132 | s.Description = data.Description 133 | s.Type = data.Type 134 | s.Image = data.Image 135 | 136 | if _, err := x.ID(s.Slug).Update(s); err != nil { 137 | return fmt.Errorf("update shorten [%s]: %v", s.Slug, err) 138 | } 139 | 140 | return nil 141 | } 142 | 143 | func (s *Shorten) getUser(e Engine) (err error) { 144 | if s.User != nil { 145 | return nil 146 | } 147 | 148 | s.User, err = getUserByID(e, s.UserID) 149 | return err 150 | } 151 | 152 | // GetUser returns the shorten owner 153 | func (s *Shorten) GetUser() error { 154 | return s.getUser(x) 155 | } 156 | 157 | // GetShortenURLs returns a list of urls of given user. 158 | func GetShortenURLs(userID int64, page, pageSize int, orderBy string) ([]*Shorten, error) { 159 | sess := x.NewSession() 160 | 161 | if len(orderBy) == 0 { 162 | orderBy = "date DESC" 163 | } 164 | 165 | if userID != 0 { 166 | sess = sess. 167 | Where("user_id = ?", userID) 168 | } 169 | 170 | sess = sess.OrderBy(orderBy) 171 | 172 | if page <= 0 { 173 | page = 1 174 | } 175 | sess.Limit(pageSize, (page-1)*pageSize) 176 | 177 | urls := make([]*Shorten, 0, pageSize) 178 | return urls, sess.Find(&urls) 179 | } 180 | -------------------------------------------------------------------------------- /pkg/model/test_fixtures.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "gopkg.in/testfixtures.v2" 5 | ) 6 | 7 | var fixtures *testfixtures.Context 8 | 9 | // InitFixtures initialize test fixtures for a test database 10 | func InitFixtures(helper testfixtures.Helper, dir string) (err error) { 11 | testfixtures.SkipDatabaseNameCheck(true) 12 | fixtures, err = testfixtures.NewFolder(x.DB().DB, helper, dir) 13 | return err 14 | } 15 | 16 | // LoadFixtures load fixtures for a test database 17 | func LoadFixtures() error { 18 | return fixtures.Load() 19 | } 20 | -------------------------------------------------------------------------------- /pkg/model/token.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-ggz/ggz/pkg/module/base" 7 | 8 | "github.com/satori/go.uuid" 9 | ) 10 | 11 | // AccessToken represents a personal access token. 12 | type AccessToken struct { 13 | ID int64 `xorm:"pk autoincr"` 14 | UserID int64 `xorm:"INDEX"` 15 | Name string 16 | Sha1 string `xorm:"UNIQUE VARCHAR(40)"` 17 | 18 | CreatedAt time.Time `xorm:"created" json:"created_at,omitempty"` 19 | UpdatedAt time.Time `xorm:"updated" json:"updated_at,omitempty"` 20 | HasRecentActivity bool `xorm:"-"` 21 | HasUsed bool `xorm:"-"` 22 | } 23 | 24 | // AfterLoad is invoked from XORM after setting the values of all fields of this object. 25 | func (t *AccessToken) AfterLoad() { 26 | t.HasUsed = t.UpdatedAt.Unix() > t.CreatedAt.Unix() 27 | } 28 | 29 | // NewAccessToken creates new access token. 30 | func NewAccessToken(t *AccessToken) error { 31 | uuid := uuid.NewV4() 32 | t.Sha1 = base.EncodeSha1(uuid.String()) 33 | _, err := x.Insert(t) 34 | return err 35 | } 36 | 37 | // GetAccessTokenBySHA returns access token by given sha1. 38 | func GetAccessTokenBySHA(sha string) (*AccessToken, error) { 39 | if sha == "" { 40 | return nil, ErrAccessTokenEmpty{} 41 | } 42 | t := &AccessToken{Sha1: sha} 43 | has, err := x.Get(t) 44 | if err != nil { 45 | return nil, err 46 | } else if !has { 47 | return nil, ErrAccessTokenNotExist{sha} 48 | } 49 | return t, nil 50 | } 51 | 52 | // UpdateAccessToken updates information of access token. 53 | func UpdateAccessToken(t *AccessToken) error { 54 | _, err := x.ID(t.ID).AllCols().Update(t) 55 | return err 56 | } 57 | 58 | // DeleteAccessTokenByID deletes access token by given ID. 59 | func DeleteAccessTokenByID(id, userID int64) error { 60 | cnt, err := x.ID(id).Delete(&AccessToken{ 61 | UserID: userID, 62 | }) 63 | if err != nil { 64 | return err 65 | } else if cnt != 1 { 66 | return ErrAccessTokenNotExist{} 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/model/unit_tests.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/go-ggz/ggz/pkg/config" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "gopkg.in/testfixtures.v2" 13 | "xorm.io/core" 14 | "xorm.io/xorm" 15 | ) 16 | 17 | func fatalTestError(fmtStr string, args ...interface{}) { 18 | fmt.Fprintf(os.Stderr, fmtStr, args...) 19 | os.Exit(1) 20 | } 21 | 22 | // MainTest a reusable TestMain(..) function for unit tests that need to use a 23 | // test database. Creates the test database, and sets necessary settings. 24 | func MainTest(m *testing.M, pathToRoot string) { 25 | var err error 26 | fixturesDir := filepath.Join(pathToRoot, "pkg", "fixtures") 27 | if err = createTestEngine(fixturesDir); err != nil { 28 | fatalTestError("Error creating test engine: %v\n", err) 29 | } 30 | os.Exit(m.Run()) 31 | } 32 | 33 | func createTestEngine(fixturesDir string) error { 34 | var err error 35 | x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared") 36 | if err != nil { 37 | return err 38 | } 39 | x.SetMapper(core.GonicMapper{}) 40 | if err = x.StoreEngine("InnoDB").Sync2(tables...); err != nil { 41 | return err 42 | } 43 | x.ShowSQL(config.Server.Debug) 44 | 45 | return InitFixtures(&testfixtures.SQLite{}, fixturesDir) 46 | } 47 | 48 | // PrepareTestDatabase load test fixtures into test database 49 | func PrepareTestDatabase() error { 50 | return LoadFixtures() 51 | } 52 | 53 | // PrepareTestEnv prepares the environment for unit tests. Can only be called 54 | // by tests that use the above MainTest(..) function. 55 | func PrepareTestEnv(t testing.TB) { 56 | assert.NoError(t, PrepareTestDatabase()) 57 | } 58 | 59 | type testCond struct { 60 | query interface{} 61 | args []interface{} 62 | } 63 | 64 | // Cond create a condition with arguments for a test 65 | func Cond(query interface{}, args ...interface{}) interface{} { 66 | return &testCond{query: query, args: args} 67 | } 68 | 69 | func whereConditions(sess *xorm.Session, conditions []interface{}) { 70 | for _, condition := range conditions { 71 | switch cond := condition.(type) { 72 | case *testCond: 73 | sess.Where(cond.query, cond.args...) 74 | default: 75 | sess.Where(cond) 76 | } 77 | } 78 | } 79 | 80 | func loadBeanIfExists(bean interface{}, conditions ...interface{}) (bool, error) { 81 | sess := x.NewSession() 82 | defer sess.Close() 83 | whereConditions(sess, conditions) 84 | return sess.Get(bean) 85 | } 86 | 87 | // BeanExists for testing, check if a bean exists 88 | func BeanExists(t testing.TB, bean interface{}, conditions ...interface{}) bool { 89 | exists, err := loadBeanIfExists(bean, conditions...) 90 | assert.NoError(t, err) 91 | return exists 92 | } 93 | 94 | // AssertExistsAndLoadBean assert that a bean exists and load it from the test 95 | // database 96 | func AssertExistsAndLoadBean(t testing.TB, bean interface{}, conditions ...interface{}) interface{} { 97 | exists, err := loadBeanIfExists(bean, conditions...) 98 | assert.NoError(t, err) 99 | assert.True(t, exists, 100 | "Expected to find %+v (of type %T, with conditions %+v), but did not", 101 | bean, bean, conditions) 102 | return bean 103 | } 104 | 105 | // GetCount get the count of a bean 106 | func GetCount(t testing.TB, bean interface{}, conditions ...interface{}) int { 107 | sess := x.NewSession() 108 | defer sess.Close() 109 | whereConditions(sess, conditions) 110 | count, err := sess.Count(bean) 111 | assert.NoError(t, err) 112 | return int(count) 113 | } 114 | 115 | // AssertNotExistsBean assert that a bean does not exist in the test database 116 | func AssertNotExistsBean(t testing.TB, bean interface{}, conditions ...interface{}) { 117 | exists, err := loadBeanIfExists(bean, conditions...) 118 | assert.NoError(t, err) 119 | assert.False(t, exists) 120 | } 121 | 122 | // AssertExistsIf asserts that a bean exists or does not exist, depending on 123 | // what is expected. 124 | func AssertExistsIf(t *testing.T, expected bool, bean interface{}, conditions ...interface{}) { 125 | exists, err := loadBeanIfExists(bean, conditions...) 126 | assert.NoError(t, err) 127 | assert.Equal(t, expected, exists) 128 | } 129 | 130 | // AssertSuccessfulInsert assert that beans is successfully inserted 131 | func AssertSuccessfulInsert(t testing.TB, beans ...interface{}) { 132 | _, err := x.Insert(beans...) 133 | assert.NoError(t, err) 134 | } 135 | 136 | // AssertCount assert the count of a bean 137 | func AssertCount(t testing.TB, bean interface{}, expected interface{}) { 138 | assert.EqualValues(t, expected, GetCount(t, bean)) 139 | } 140 | 141 | // AssertInt64InRange assert value is in range [low, high] 142 | func AssertInt64InRange(t testing.TB, low, high, value int64) { 143 | assert.True(t, value >= low && value <= high, 144 | "Expected value in range [%d, %d], found %d", low, high, value) 145 | } 146 | -------------------------------------------------------------------------------- /pkg/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/go-ggz/ggz/pkg/module/base" 8 | ) 9 | 10 | // User represents the object of individual and member of organization. 11 | type User struct { 12 | ID int64 `xorm:"pk autoincr" json:"id,omitempty"` 13 | FullName string `json:"fullname,omitempty"` 14 | // Email is the primary email address (to be used for communication) 15 | Email string `xorm:"UNIQUE NOT NULL" json:"email,omitempty"` 16 | Location string 17 | Website string 18 | IsActive bool `xorm:"INDEX"` // Activate primary email 19 | Avatar string `xorm:"VARCHAR(2048) NOT NULL" json:"avatar,omitempty"` 20 | AvatarEmail string `xorm:"NOT NULL" json:"avatar_email,omitempty"` 21 | CreatedAt time.Time `xorm:"created" json:"created_at,omitempty"` 22 | UpdatedAt time.Time `xorm:"updated" json:"updated_at,omitempty"` 23 | LastLogin time.Time `json:"lastlogin,omitempty"` 24 | } 25 | 26 | // BeforeInsert will be invoked by XORM before inserting a record 27 | func (u *User) BeforeInsert() { 28 | u.LastLogin = time.Now() 29 | } 30 | 31 | // BeforeUpdate is invoked from XORM before updating this object. 32 | func (u *User) BeforeUpdate() { 33 | // Organization does not need email 34 | u.Email = strings.ToLower(u.Email) 35 | if len(u.AvatarEmail) == 0 { 36 | u.AvatarEmail = u.Email 37 | } 38 | if len(u.AvatarEmail) > 0 { 39 | u.Avatar = base.HashEmail(u.AvatarEmail) 40 | } 41 | } 42 | 43 | func getUserByID(e Engine, id int64) (*User, error) { 44 | u := new(User) 45 | has, err := e.ID(id).Get(u) 46 | if err != nil { 47 | return nil, err 48 | } else if !has { 49 | return nil, ErrUserNotExist{id, "", 0} 50 | } 51 | return u, nil 52 | } 53 | 54 | // GetUserByID returns the user object by given ID if exists. 55 | func GetUserByID(id int64) (*User, error) { 56 | return getUserByID(x, id) 57 | } 58 | 59 | func isUserExist(e Engine, uid int64, email string) (bool, error) { 60 | if len(email) == 0 { 61 | return false, nil 62 | } 63 | return e. 64 | Where("id!=?", uid). 65 | Get(&User{Email: strings.ToLower(email)}) 66 | } 67 | 68 | // IsUserExist checks if given user email exist, 69 | // the user email should be noncased unique. 70 | // If uid is presented, then check will rule out that one, 71 | // it is used when update a user email in settings page. 72 | func IsUserExist(uid int64, email string) (bool, error) { 73 | return isUserExist(x, uid, email) 74 | } 75 | 76 | // GetUserByEmail returns the user object by given e-mail if exists. 77 | func GetUserByEmail(email string) (*User, error) { 78 | if len(email) == 0 { 79 | return nil, ErrUserNotExist{0, email, 0} 80 | } 81 | 82 | email = strings.ToLower(email) 83 | // First try to find the user by primary email 84 | user := &User{Email: email} 85 | has, err := x.Get(user) 86 | if err != nil { 87 | return nil, err 88 | } 89 | if has { 90 | return user, nil 91 | } 92 | 93 | return nil, ErrUserNotExist{0, email, 0} 94 | } 95 | 96 | // CreateUser creates record of a new user. 97 | func CreateUser(u *User) (err error) { 98 | sess := x.NewSession() 99 | defer sess.Close() 100 | if err = sess.Begin(); err != nil { 101 | return err 102 | } 103 | 104 | u.Email = strings.ToLower(u.Email) 105 | isExist, err := sess. 106 | Where("email=?", u.Email). 107 | Get(new(User)) 108 | if err != nil { 109 | return err 110 | } else if isExist { 111 | return ErrEmailAlreadyUsed{u.Email} 112 | } 113 | 114 | u.AvatarEmail = u.Email 115 | u.Avatar = base.HashEmail(u.AvatarEmail) 116 | 117 | if _, err = sess.Insert(u); err != nil { 118 | return err 119 | } 120 | 121 | return sess.Commit() 122 | } 123 | 124 | func updateUserCols(e Engine, u *User, cols ...string) error { 125 | _, err := e.ID(u.ID).Cols(cols...).Update(u) 126 | return err 127 | } 128 | 129 | func updateUser(e Engine, u *User) error { 130 | _, err := e.ID(u.ID).AllCols().Update(u) 131 | return err 132 | } 133 | 134 | // UpdateUser updates user's information. 135 | func UpdateUser(u *User) error { 136 | return updateUser(x, u) 137 | } 138 | 139 | // UpdateUserCols update user according special columns 140 | func UpdateUserCols(u *User, cols ...string) error { 141 | return updateUserCols(x, u, cols...) 142 | } 143 | -------------------------------------------------------------------------------- /pkg/model/user_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsUserExist(t *testing.T) { 10 | assert.NoError(t, PrepareTestDatabase()) 11 | exists, err := IsUserExist(0, "test@gmail.com") 12 | assert.NoError(t, err) 13 | assert.True(t, exists) 14 | 15 | exists, err = IsUserExist(0, "test123456@gmail.com") 16 | assert.NoError(t, err) 17 | assert.False(t, exists) 18 | 19 | exists, err = IsUserExist(1, "test1234@gmail.com") 20 | assert.NoError(t, err) 21 | assert.True(t, exists) 22 | 23 | exists, err = IsUserExist(1, "test123456@gmail.com") 24 | assert.NoError(t, err) 25 | assert.False(t, exists) 26 | } 27 | 28 | func TestGetUserByEmail(t *testing.T) { 29 | assert.NoError(t, PrepareTestDatabase()) 30 | 31 | t.Run("missing email", func(t *testing.T) { 32 | t.Parallel() 33 | user, err := GetUserByEmail("") 34 | assert.Error(t, err) 35 | assert.Nil(t, user) 36 | assert.True(t, IsErrUserNotExist(err)) 37 | }) 38 | 39 | t.Run("test exist email", func(t *testing.T) { 40 | t.Parallel() 41 | user, err := GetUserByEmail("test@gmail.com") 42 | assert.NoError(t, err) 43 | assert.NotNil(t, user) 44 | assert.Equal(t, int64(1), user.ID) 45 | }) 46 | 47 | t.Run("email not found", func(t *testing.T) { 48 | t.Parallel() 49 | user, err := GetUserByEmail("test123456@gmail.com") 50 | assert.Error(t, err) 51 | assert.Nil(t, user) 52 | assert.True(t, IsErrUserNotExist(err)) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/module/base/base.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "strings" 8 | ) 9 | 10 | // EncodeMD5 encodes string to md5 hex value. 11 | func EncodeMD5(str string) string { 12 | m := md5.New() 13 | m.Write([]byte(str)) 14 | return hex.EncodeToString(m.Sum(nil)) 15 | } 16 | 17 | // EncodeSha1 string to sha1 hex value. 18 | func EncodeSha1(str string) string { 19 | h := sha1.New() 20 | h.Write([]byte(str)) 21 | return hex.EncodeToString(h.Sum(nil)) 22 | } 23 | 24 | // HashEmail hashes email address to MD5 string. 25 | // https://en.gravatar.com/site/implement/hash/ 26 | func HashEmail(email string) string { 27 | return EncodeMD5(strings.ToLower(strings.TrimSpace(email))) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/module/base/base_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestEncodeMD5(t *testing.T) { 10 | assert.Equal(t, 11 | "3858f62230ac3c915f300c664312c63f", 12 | EncodeMD5("foobar"), 13 | ) 14 | } 15 | 16 | func TestEncodeSha1(t *testing.T) { 17 | assert.Equal(t, 18 | "8843d7f92416211de9ebb963ff4ce28125932878", 19 | EncodeSha1("foobar"), 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/module/loader/cache.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/go-ggz/ggz/pkg/model" 9 | "github.com/go-ggz/ggz/pkg/module/loader/lru" 10 | "github.com/go-ggz/ggz/pkg/module/loader/memory" 11 | 12 | "gopkg.in/nicksrandall/dataloader.v5" 13 | ) 14 | 15 | var ( 16 | // Cache for dataloader 17 | Cache dataloader.Cache 18 | // UserCache for user cache from ID 19 | UserCache *dataloader.Loader 20 | ) 21 | 22 | const sep = ":" 23 | 24 | func initLoader() { 25 | UserCache = dataloader.NewBatchedLoader(userBatch, dataloader.WithCache(Cache)) 26 | } 27 | 28 | func getCacheKey(module string, id interface{}) string { 29 | var str string 30 | switch v := id.(type) { 31 | case int64: 32 | str = strconv.FormatInt(v, 10) 33 | case string: 34 | str = v 35 | } 36 | return module + sep + str 37 | } 38 | 39 | func getCacheID(key string) (interface{}, error) { 40 | strs := strings.Split(key, sep) 41 | 42 | return strs[1], nil 43 | } 44 | 45 | // NewEngine for initialize cache engine 46 | func NewEngine(driver, prefix string, expire int) error { 47 | switch driver { 48 | case "lru": 49 | Cache = lru.NewEngine(prefix) 50 | case "memory": 51 | Cache = memory.NewEngine(prefix, expire) 52 | default: 53 | Cache = dataloader.NewCache() 54 | } 55 | 56 | // load cache 57 | initLoader() 58 | 59 | return nil 60 | } 61 | 62 | func userBatch(ctx context.Context, keys dataloader.Keys) []*dataloader.Result { 63 | var results []*dataloader.Result 64 | id, _ := getCacheID(keys[0].String()) 65 | n, _ := strconv.ParseInt(id.(string), 10, 64) 66 | 67 | user, err := model.GetUserByID(n) 68 | 69 | results = append(results, &dataloader.Result{ 70 | Data: user, 71 | Error: err, 72 | }) 73 | 74 | return results 75 | } 76 | 77 | // GetUserFromLoader get user cache 78 | func GetUserFromLoader(ctx context.Context, id interface{}) (*model.User, error) { 79 | key := getCacheKey("user", id) 80 | userCache, err := UserCache.Load(ctx, dataloader.StringKey(key))() 81 | 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | return userCache.(*model.User), nil 87 | } 88 | -------------------------------------------------------------------------------- /pkg/module/loader/lru/cache.go: -------------------------------------------------------------------------------- 1 | package lru 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/golang-lru" 7 | "gopkg.in/nicksrandall/dataloader.v5" 8 | ) 9 | 10 | // Cache implements the dataloader.Cache interface 11 | type Cache struct { 12 | *lru.ARCCache 13 | Prefix string 14 | } 15 | 16 | // Get gets an item from the cache 17 | func (c *Cache) Get(_ context.Context, key dataloader.Key) (dataloader.Thunk, bool) { 18 | v, ok := c.ARCCache.Get(c.Prefix + "::" + key.String()) 19 | if ok { 20 | return v.(dataloader.Thunk), ok 21 | } 22 | return nil, ok 23 | } 24 | 25 | // Set sets an item in the cache 26 | func (c *Cache) Set(_ context.Context, key dataloader.Key, value dataloader.Thunk) { 27 | c.ARCCache.Add(c.Prefix+"::"+key.String(), value) 28 | } 29 | 30 | // Delete deletes an item in the cache 31 | func (c *Cache) Delete(_ context.Context, key dataloader.Key) bool { 32 | if c.ARCCache.Contains(c.Prefix + "::" + key.String()) { 33 | c.ARCCache.Remove(c.Prefix + "::" + key.String()) 34 | return true 35 | } 36 | return false 37 | } 38 | 39 | // Clear cleasrs the cache 40 | func (c *Cache) Clear() { 41 | c.ARCCache.Purge() 42 | } 43 | 44 | // NewEngine for lru engine 45 | func NewEngine(prefix string) *Cache { 46 | c, _ := lru.NewARC(100) 47 | 48 | return &Cache{ 49 | ARCCache: c, 50 | Prefix: prefix, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/module/loader/memory/cache.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/patrickmn/go-cache" 8 | "gopkg.in/nicksrandall/dataloader.v5" 9 | ) 10 | 11 | // Cache implements the dataloader.Cache interface 12 | type Cache struct { 13 | c *cache.Cache 14 | Prefix string 15 | } 16 | 17 | // Get gets a value from the cache 18 | func (c *Cache) Get(_ context.Context, key dataloader.Key) (dataloader.Thunk, bool) { 19 | v, ok := c.c.Get(c.Prefix + "::" + key.String()) 20 | if ok { 21 | return v.(dataloader.Thunk), ok 22 | } 23 | return nil, ok 24 | } 25 | 26 | // Set sets a value in the cache 27 | func (c *Cache) Set(_ context.Context, key dataloader.Key, value dataloader.Thunk) { 28 | c.c.Set(c.Prefix+"::"+key.String(), value, 0) 29 | } 30 | 31 | // Delete deletes and item in the cache 32 | func (c *Cache) Delete(_ context.Context, key dataloader.Key) bool { 33 | if _, found := c.c.Get(c.Prefix + "::" + key.String()); found { 34 | c.c.Delete(c.Prefix + "::" + key.String()) 35 | return true 36 | } 37 | return false 38 | } 39 | 40 | // Clear clears the cache 41 | func (c *Cache) Clear() { 42 | c.c.Flush() 43 | } 44 | 45 | // NewEngine for memory engine 46 | func NewEngine(prefix string, s int) *Cache { 47 | expire := time.Duration(s) 48 | c := cache.New(expire*time.Minute, expire*time.Minute) 49 | 50 | return &Cache{ 51 | c: c, 52 | Prefix: prefix, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/module/mailer/mailer.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "github.com/rs/zerolog/log" 5 | ) 6 | 7 | // Mail for smtp or ses interface 8 | type Mail interface { 9 | From(string, string) Mail 10 | To(...string) Mail 11 | Cc(...string) Mail 12 | Subject(string) Mail 13 | Body(string) Mail 14 | Send() (interface{}, error) 15 | } 16 | 17 | // Config for mailer 18 | type Config struct { 19 | Host string 20 | Port string 21 | Username string 22 | Password string 23 | Driver string 24 | } 25 | 26 | // Client for mail interface 27 | var Client Mail 28 | 29 | // NewEngine return storage interface 30 | func NewEngine(c Config) (mail Mail, err error) { 31 | switch c.Driver { 32 | case "smtp": 33 | Client, err = SMTPEngine( 34 | c.Host, 35 | c.Port, 36 | c.Username, 37 | c.Password, 38 | ) 39 | if err != nil { 40 | return nil, err 41 | } 42 | case "ses": 43 | Client, err = SESEngine() 44 | if err != nil { 45 | return nil, err 46 | } 47 | default: 48 | log.Error().Msg("Unknown email driver") 49 | } 50 | 51 | return mail, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/module/mailer/ses.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/awserr" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/ses" 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | const ( 14 | // CharSet The character encoding for the email. 15 | CharSet = "UTF-8" 16 | ) 17 | 18 | // SES for aws ses 19 | type SES struct { 20 | sess *session.Session 21 | source *string 22 | to []*string 23 | cc []*string 24 | subject *string 25 | body *string 26 | } 27 | 28 | // From for sender information 29 | func (c SES) From(name, address string) Mail { 30 | c.source = aws.String(fmt.Sprintf("%s <%s>", name, address)) 31 | 32 | return c 33 | } 34 | 35 | // To for mailto list 36 | func (c SES) To(address ...string) Mail { 37 | for _, v := range address { 38 | c.to = append(c.to, aws.String(v)) 39 | } 40 | 41 | return c 42 | } 43 | 44 | // Cc for cc list 45 | func (c SES) Cc(address ...string) Mail { 46 | for _, v := range address { 47 | c.cc = append(c.cc, aws.String(v)) 48 | } 49 | 50 | return c 51 | } 52 | 53 | // Subject for email title 54 | func (c SES) Subject(subject string) Mail { 55 | c.subject = aws.String(subject) 56 | 57 | return c 58 | } 59 | 60 | // Body for email body 61 | func (c SES) Body(body string) Mail { 62 | c.body = aws.String(body) 63 | 64 | return c 65 | } 66 | 67 | // Send email 68 | func (c SES) Send() (interface{}, error) { 69 | // Create an SES session. 70 | svc := ses.New(c.sess) 71 | 72 | // Assemble the email. 73 | input := &ses.SendEmailInput{ 74 | Destination: &ses.Destination{ 75 | CcAddresses: c.cc, 76 | ToAddresses: c.to, 77 | }, 78 | Message: &ses.Message{ 79 | Body: &ses.Body{ 80 | Html: &ses.Content{ 81 | Charset: aws.String(CharSet), 82 | Data: c.body, 83 | }, 84 | Text: &ses.Content{ 85 | Charset: aws.String(CharSet), 86 | Data: c.body, 87 | }, 88 | }, 89 | Subject: &ses.Content{ 90 | Charset: aws.String(CharSet), 91 | Data: c.subject, 92 | }, 93 | }, 94 | Source: c.source, 95 | // Uncomment to use a configuration set 96 | //ConfigurationSetName: aws.String(ConfigurationSet), 97 | } 98 | 99 | // Attempt to send the email. 100 | resp, err := svc.SendEmail(input) 101 | 102 | // Display error messages if they occur. 103 | if err != nil { 104 | if aerr, ok := err.(awserr.Error); ok { 105 | switch aerr.Code() { 106 | case ses.ErrCodeMessageRejected: 107 | log.Error().Err(aerr).Msg(ses.ErrCodeMessageRejected) 108 | case ses.ErrCodeMailFromDomainNotVerifiedException: 109 | log.Error().Err(aerr).Msg(ses.ErrCodeMailFromDomainNotVerifiedException) 110 | case ses.ErrCodeConfigurationSetDoesNotExistException: 111 | log.Error().Err(aerr).Msg(ses.ErrCodeConfigurationSetDoesNotExistException) 112 | default: 113 | log.Error().Err(aerr).Msg("AWS SES Error") 114 | } 115 | } else { 116 | // Print the error, cast err to awserr.Error to get the Code and 117 | // Message from an error. 118 | log.Error().Err(aerr).Msg("Unknown Error") 119 | } 120 | 121 | return nil, err 122 | } 123 | 124 | return resp, nil 125 | } 126 | 127 | // SESEngine initial ses 128 | func SESEngine() (*SES, error) { 129 | // Create a new session in the us-west-2 region. 130 | // Replace us-west-2 with the AWS Region you're using for Amazon SES. 131 | sess, err := session.NewSession(&aws.Config{ 132 | Region: aws.String("us-west-2")}, 133 | ) 134 | 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | return &SES{ 140 | sess: sess, 141 | }, nil 142 | } 143 | -------------------------------------------------------------------------------- /pkg/module/mailer/smtp.go: -------------------------------------------------------------------------------- 1 | package mailer 2 | 3 | import ( 4 | "net/mail" 5 | "net/smtp" 6 | 7 | "github.com/scorredoira/email" 8 | ) 9 | 10 | type from struct { 11 | Name string 12 | Address string 13 | } 14 | 15 | // SMTP setting 16 | type SMTP struct { 17 | host string 18 | port string 19 | username string 20 | password string 21 | from from 22 | to []string 23 | cc []string 24 | subject string 25 | body string 26 | } 27 | 28 | // From for sender information 29 | func (c SMTP) From(name, address string) Mail { 30 | c.from = from{ 31 | Name: name, 32 | Address: address, 33 | } 34 | 35 | return c 36 | } 37 | 38 | // To for mailto list 39 | func (c SMTP) To(address ...string) Mail { 40 | c.to = address 41 | 42 | return c 43 | } 44 | 45 | // Cc for cc list 46 | func (c SMTP) Cc(address ...string) Mail { 47 | c.cc = address 48 | 49 | return c 50 | } 51 | 52 | // Subject for email title 53 | func (c SMTP) Subject(subject string) Mail { 54 | c.subject = subject 55 | 56 | return c 57 | } 58 | 59 | // Body for email body 60 | func (c SMTP) Body(body string) Mail { 61 | c.body = body 62 | 63 | return c 64 | } 65 | 66 | // Send email 67 | func (c SMTP) Send() (interface{}, error) { 68 | m := email.NewHTMLMessage(c.subject, c.body) 69 | m.From = mail.Address{ 70 | Name: c.from.Name, 71 | Address: c.from.Address, 72 | } 73 | m.To = c.to 74 | m.Cc = c.cc 75 | 76 | // send it 77 | auth := smtp.PlainAuth("", c.username, c.password, c.host) 78 | 79 | return nil, email.Send(c.host+":"+c.port, auth, m) 80 | } 81 | 82 | // SMTPEngine initial smtp object 83 | func SMTPEngine(host, port, username, password string) (*SMTP, error) { 84 | return &SMTP{ 85 | host: host, 86 | port: port, 87 | username: username, 88 | password: password, 89 | }, nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/module/meta/meta.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "time" 7 | 8 | m "github.com/keighl/metabolize" 9 | ) 10 | 11 | // URLMetaData struct 12 | type URLMetaData struct { 13 | Title string `meta:"og:title,title" json:"title"` 14 | Description string `meta:"og:description,description" json:"description"` 15 | Type string `meta:"og:type" json:"type"` 16 | URL url.URL `meta:"og:url" json:"url"` 17 | Image string `meta:"og:image" json:"image"` 18 | Time time.Time `meta:"article:published_time,parsely-pub-date" json:"time"` 19 | VideoWidth int64 `meta:"og:video:width" json:"video_width"` 20 | VideoHeight int64 `meta:"og:video:height" json:"video_height"` 21 | } 22 | 23 | // FetchData for fetch metadata from header of body 24 | func FetchData(url string) (*URLMetaData, error) { 25 | var res *http.Response 26 | var err error 27 | meta := new(URLMetaData) 28 | 29 | if res, err = http.Get(url); err != nil { 30 | return nil, err 31 | } 32 | 33 | if err := m.Metabolize(res.Body, meta); err != nil { 34 | return nil, err 35 | } 36 | 37 | return meta, nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/module/metrics/collector.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/go-ggz/ggz/pkg/model" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | const namespace = "ggz_" 10 | 11 | // Collector implements the prometheus.Collector interface and 12 | // exposes gitea metrics for prometheus 13 | type Collector struct { 14 | Shortens *prometheus.Desc 15 | Users *prometheus.Desc 16 | } 17 | 18 | // NewCollector returns a new Collector with all prometheus.Desc initialized 19 | func NewCollector() Collector { 20 | return Collector{ 21 | Users: prometheus.NewDesc( 22 | namespace+"users", 23 | "Number of Users", 24 | nil, nil, 25 | ), 26 | Shortens: prometheus.NewDesc( 27 | namespace+"shortens", 28 | "Number of Shortens", 29 | nil, nil, 30 | ), 31 | } 32 | 33 | } 34 | 35 | // Describe returns all possible prometheus.Desc 36 | func (c Collector) Describe(ch chan<- *prometheus.Desc) { 37 | ch <- c.Users 38 | ch <- c.Shortens 39 | } 40 | 41 | // Collect returns the metrics with values 42 | func (c Collector) Collect(ch chan<- prometheus.Metric) { 43 | stats := model.GetStatistic() 44 | 45 | ch <- prometheus.MustNewConstMetric( 46 | c.Users, 47 | prometheus.GaugeValue, 48 | float64(stats.Counter.User), 49 | ) 50 | ch <- prometheus.MustNewConstMetric( 51 | c.Shortens, 52 | prometheus.GaugeValue, 53 | float64(stats.Counter.Shorten), 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/module/socket/socket.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | // "context" 5 | // "errors" 6 | // "fmt" 7 | // "net/http" 8 | 9 | // "github.com/go-ggz/ggz/pkg/config" 10 | // "github.com/go-ggz/ggz/pkg/helper" 11 | // "github.com/go-ggz/ggz/pkg/middleware/auth/auth0" 12 | 13 | // "github.com/dgrijalva/jwt-go" 14 | "github.com/gin-gonic/gin" 15 | socketio "github.com/googollee/go-socket.io" 16 | // "github.com/rs/zerolog/log" 17 | ) 18 | 19 | // Server for socket server 20 | var Server *socketio.Server 21 | var err error 22 | var key = "user" 23 | 24 | // Test for testing websocket 25 | type Test struct { 26 | A int `json:"abc"` 27 | B string `json:"def"` 28 | } 29 | 30 | // NewEngine for socket server 31 | func NewEngine() error { 32 | // Server, err = socketio.NewServer(nil) 33 | // if err != nil { 34 | // log.Error().Err(err).Msg("can't create socker server.") 35 | // return err 36 | // } 37 | 38 | // Server.SetAllowRequest(func(r *http.Request) error { 39 | // token := r.URL.Query().Get("token") 40 | 41 | // if token == "" { 42 | // return errors.New("Required authorization token not found") 43 | // } 44 | 45 | // parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { 46 | // return auth0.ParseRSAPublicKeyFromPEM() 47 | // }) 48 | 49 | // if err != nil { 50 | // return fmt.Errorf("Error parsing token: %v", err) 51 | // } 52 | 53 | // if jwt.SigningMethodHS256.Alg() != parsedToken.Header["alg"] { 54 | // message := fmt.Sprintf("Expected %s signing method but token specified %s", 55 | // jwt.SigningMethodHS256.Alg(), 56 | // parsedToken.Header["alg"]) 57 | // return fmt.Errorf("Error validating token algorithm: %s", message) 58 | // } 59 | 60 | // if !parsedToken.Valid { 61 | // return errors.New("Token is invalid") 62 | // } 63 | 64 | // // If we get here, everything worked and we can set the 65 | // // user property in context. 66 | // newRequest := r.WithContext(context.WithValue(r.Context(), config.ContextKeyUser, parsedToken)) 67 | // // Update the current request with the new context information. 68 | // *r = *newRequest 69 | 70 | // return nil 71 | // }) 72 | 73 | // Server.On("connection", func(so socketio.Socket) { 74 | // user := helper.GetUserDataFromToken(so.Request().Context()) 75 | // room := user["email"].(string) 76 | // so.Join(room) 77 | 78 | // so.On("chat message", func(msg string) { 79 | // so.BroadcastTo(room, "chat message", Test{ 80 | // A: 1, 81 | // B: "100", 82 | // }) 83 | // }) 84 | 85 | // so.On("chat message with ack", func(msg string) string { 86 | // return msg 87 | // }) 88 | 89 | // so.On("disconnection", func() { 90 | // }) 91 | // }) 92 | 93 | // Server.On("error", func(so socketio.Socket, err error) { 94 | // log.Error().Err(err).Msg("socker server error.") 95 | // }) 96 | 97 | return nil 98 | } 99 | 100 | // Handler initializes the prometheus middleware. 101 | func Handler() gin.HandlerFunc { 102 | return func(c *gin.Context) { 103 | origin := c.GetHeader("Origin") 104 | c.Header("Access-Control-Allow-Credentials", "true") 105 | c.Header("Access-Control-Allow-Origin", origin) 106 | Server.ServeHTTP(c.Writer, c.Request) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/module/storage/disk/disk.go: -------------------------------------------------------------------------------- 1 | package disk 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/url" 6 | "os" 7 | "path" 8 | 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | // Disk client 13 | type Disk struct { 14 | Host string 15 | Path string 16 | } 17 | 18 | // NewEngine struct 19 | func NewEngine(host, path string) *Disk { 20 | return &Disk{ 21 | Host: host, 22 | Path: path, 23 | } 24 | } 25 | 26 | // UploadFile to s3 server 27 | func (d *Disk) UploadFile(bucketName, fileName string, content []byte) error { 28 | return ioutil.WriteFile(d.FilePath(bucketName, fileName), content, os.FileMode(0644)) 29 | } 30 | 31 | // CreateBucket create bucket 32 | func (d *Disk) CreateBucket(bucketName, region string) error { 33 | storage := path.Join(d.Path, bucketName) 34 | if err := os.MkdirAll(storage, os.ModePerm); err != nil { 35 | return nil 36 | } 37 | log.Info().Msgf("Successfully created disk path: %s", storage) 38 | 39 | return nil 40 | } 41 | 42 | // FilePath for store path + file name 43 | func (d *Disk) FilePath(bucketName, fileName string) string { 44 | return path.Join( 45 | d.Path, 46 | bucketName, 47 | fileName, 48 | ) 49 | } 50 | 51 | // DeleteFile delete file 52 | func (d *Disk) DeleteFile(bucketName, fileName string) error { 53 | return os.Remove(d.FilePath(bucketName, fileName)) 54 | } 55 | 56 | // GetFile for storage host + bucket + filename 57 | func (d *Disk) GetFile(bucketName, fileName string) string { 58 | if d.Host != "" { 59 | if u, err := url.Parse(d.Host); err == nil { 60 | u.Path = path.Join(u.Path, d.Path, bucketName, fileName) 61 | return u.String() 62 | } 63 | } 64 | return path.Join(d.Path, bucketName, fileName) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/module/storage/disk/disk_test.go: -------------------------------------------------------------------------------- 1 | package disk 2 | 3 | import "testing" 4 | 5 | func TestDisk_GetFile(t *testing.T) { 6 | type fields struct { 7 | Host string 8 | Path string 9 | } 10 | type args struct { 11 | bucketName string 12 | fileName string 13 | } 14 | tests := []struct { 15 | name string 16 | fields fields 17 | args args 18 | want string 19 | }{ 20 | { 21 | name: "without host", 22 | fields: fields{ 23 | Path: "./data/", 24 | }, 25 | args: args{ 26 | bucketName: "test", 27 | fileName: "a.png", 28 | }, 29 | want: "data/test/a.png", 30 | }, 31 | { 32 | name: "without host and absolute path", 33 | fields: fields{ 34 | Path: "/data/", 35 | }, 36 | args: args{ 37 | bucketName: "test", 38 | fileName: "a.png", 39 | }, 40 | want: "/data/test/a.png", 41 | }, 42 | { 43 | name: "with host", 44 | fields: fields{ 45 | Host: "http://localhost:8080/", 46 | Path: "./data/", 47 | }, 48 | args: args{ 49 | bucketName: "test", 50 | fileName: "a.png", 51 | }, 52 | want: "http://localhost:8080/data/test/a.png", 53 | }, 54 | { 55 | name: "with host and absolute path", 56 | fields: fields{ 57 | Host: "http://localhost:8080/", 58 | Path: "/data/", 59 | }, 60 | args: args{ 61 | bucketName: "test", 62 | fileName: "a.png", 63 | }, 64 | want: "http://localhost:8080/data/test/a.png", 65 | }, 66 | { 67 | name: "wrong host format", 68 | fields: fields{ 69 | Host: "localhost", 70 | Path: "/data/", 71 | }, 72 | args: args{ 73 | bucketName: "test", 74 | fileName: "a.png", 75 | }, 76 | want: "localhost/data/test/a.png", 77 | }, 78 | } 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | d := &Disk{ 82 | Host: tt.fields.Host, 83 | Path: tt.fields.Path, 84 | } 85 | if got := d.GetFile(tt.args.bucketName, tt.args.fileName); got != tt.want { 86 | t.Errorf("Disk.GetFile() = %v, want %v", got, tt.want) 87 | } 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pkg/module/storage/minio/minio.go: -------------------------------------------------------------------------------- 1 | package minio 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/minio/minio-go" 11 | "github.com/minio/minio-go/pkg/credentials" 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | // Minio client 16 | type Minio struct { 17 | host string 18 | client *minio.Client 19 | } 20 | 21 | // NewEngine struct 22 | func NewEngine(endpoint, accessID, secretKey string, ssl bool) (*Minio, error) { 23 | var client *minio.Client 24 | var err error 25 | if endpoint == "" { 26 | return nil, errors.New("endpoint can't be empty") 27 | } 28 | 29 | // Fetching from IAM roles assigned to an EC2 instance. 30 | if accessID == "" && secretKey == "" { 31 | iam := credentials.NewIAM("") 32 | client, err = minio.NewWithCredentials(endpoint, iam, ssl, "") 33 | } else { 34 | // Initialize minio client object. 35 | client, err = minio.New(endpoint, accessID, secretKey, ssl) 36 | } 37 | 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | host := "" 43 | if ssl { 44 | host = "https://" + endpoint 45 | } else { 46 | host = "http://" + endpoint 47 | } 48 | 49 | return &Minio{ 50 | host: host, 51 | client: client, 52 | }, nil 53 | } 54 | 55 | // UploadFile to s3 server 56 | func (m *Minio) UploadFile(bucketName, objectName string, content []byte) error { 57 | // Upload the zip file with FPutObject 58 | _, err := m.client.PutObject( 59 | bucketName, 60 | objectName, 61 | bytes.NewReader(content), 62 | int64(len(content)), 63 | minio.PutObjectOptions{ContentType: http.DetectContentType(content)}, 64 | ) 65 | 66 | return err 67 | } 68 | 69 | // CreateBucket create bucket 70 | func (m *Minio) CreateBucket(bucketName, region string) error { 71 | exists, err := m.client.BucketExists(bucketName) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | if exists { 77 | log.Info().Msgf("We already own %s bucket", bucketName) 78 | return nil 79 | } 80 | 81 | if err := m.client.MakeBucket(bucketName, region); err != nil { 82 | return err 83 | } 84 | log.Info().Msgf("Successfully created s3 bucket: %s", bucketName) 85 | 86 | return nil 87 | } 88 | 89 | // FilePath for store path + file name 90 | func (m *Minio) FilePath(_, fileName string) string { 91 | return fmt.Sprintf("%s/%s", os.TempDir(), fileName) 92 | } 93 | 94 | // DeleteFile delete file 95 | func (m *Minio) DeleteFile(bucketName, fileName string) error { 96 | return m.client.RemoveObject(bucketName, fileName) 97 | } 98 | 99 | // GetFile for storage host + bucket + filename 100 | func (m *Minio) GetFile(bucketName, fileName string) string { 101 | return m.host + "/" + bucketName + "/" + fileName 102 | } 103 | -------------------------------------------------------------------------------- /pkg/module/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/go-ggz/ggz/pkg/config" 5 | "github.com/go-ggz/ggz/pkg/module/storage/disk" 6 | "github.com/go-ggz/ggz/pkg/module/storage/minio" 7 | ) 8 | 9 | // Storage for s3 and disk 10 | type Storage interface { 11 | // CreateBucket for create new folder 12 | CreateBucket(string, string) error 13 | // UploadFile for upload single file 14 | UploadFile(string, string, []byte) error 15 | // DeleteFile for delete single file 16 | DeleteFile(string, string) error 17 | // FilePath for store path + file name 18 | FilePath(string, string) string 19 | // GetFile for storage host + bucket + filename 20 | GetFile(string, string) string 21 | } 22 | 23 | // S3 for storage interface 24 | var S3 Storage 25 | 26 | // NewEngine return storage interface 27 | func NewEngine() (Storage, error) { 28 | switch config.Storage.Driver { 29 | case "s3": 30 | return minio.NewEngine( 31 | config.Minio.EndPoint, 32 | config.Minio.AccessID, 33 | config.Minio.SecretKey, 34 | config.Minio.SSL, 35 | ) 36 | case "disk": 37 | return disk.NewEngine( 38 | config.Server.Host, 39 | config.Storage.Path, 40 | ), nil 41 | } 42 | 43 | return nil, nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/router/graphql.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/go-ggz/ggz/pkg/config" 5 | "github.com/go-ggz/ggz/pkg/schema" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/graphql-go/handler" 9 | ) 10 | 11 | // GraphQL initializes the graphql handler. 12 | func GraphQL() gin.HandlerFunc { 13 | // Creates a GraphQL-go HTTP handler with the defined schema 14 | h := handler.New(&handler.Config{ 15 | Schema: &schema.Schema, 16 | Pretty: config.Server.GraphiQL, 17 | GraphiQL: config.Server.GraphiQL, 18 | }) 19 | 20 | return func(c *gin.Context) { 21 | h.ServeHTTP(c.Writer, c.Request) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/router/metrics.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/prometheus/client_golang/prometheus/promhttp" 10 | ) 11 | 12 | var ( 13 | // errInvalidToken is returned when the api request token is invalid. 14 | errInvalidToken = errors.New("Invalid or missing token") 15 | ) 16 | 17 | // Metrics initializes the prometheus handler. 18 | func Metrics(token string) gin.HandlerFunc { 19 | h := promhttp.Handler() 20 | 21 | return func(c *gin.Context) { 22 | if token == "" { 23 | h.ServeHTTP(c.Writer, c.Request) 24 | return 25 | } 26 | 27 | header := c.Request.Header.Get("Authorization") 28 | 29 | if header == "" { 30 | c.String(http.StatusUnauthorized, errInvalidToken.Error()) 31 | return 32 | } 33 | 34 | bearer := fmt.Sprintf("Bearer %s", token) 35 | 36 | if header != bearer { 37 | c.String(http.StatusUnauthorized, errInvalidToken.Error()) 38 | return 39 | } 40 | 41 | h.ServeHTTP(c.Writer, c.Request) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/router/routes/main_test.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-ggz/ggz/pkg/model" 7 | ) 8 | 9 | func TestMain(m *testing.M) { 10 | model.MainTest(m, "../../..") 11 | } 12 | -------------------------------------------------------------------------------- /pkg/router/routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | "regexp" 7 | 8 | "github.com/go-ggz/ggz/api" 9 | "github.com/go-ggz/ggz/assets" 10 | "github.com/go-ggz/ggz/pkg/config" 11 | "github.com/go-ggz/ggz/pkg/middleware/auth" 12 | "github.com/go-ggz/ggz/pkg/middleware/header" 13 | "github.com/go-ggz/ggz/pkg/model" 14 | "github.com/go-ggz/ggz/pkg/module/loader" 15 | "github.com/go-ggz/ggz/pkg/module/metrics" 16 | "github.com/go-ggz/ggz/pkg/module/storage" 17 | "github.com/go-ggz/ggz/pkg/router" 18 | 19 | "github.com/gin-contrib/gzip" 20 | "github.com/gin-contrib/logger" 21 | "github.com/gin-contrib/pprof" 22 | "github.com/gin-gonic/gin" 23 | "github.com/prometheus/client_golang/prometheus" 24 | "github.com/rs/zerolog/log" 25 | ) 26 | 27 | var ( 28 | rxURL = regexp.MustCompile(`^/(socket.io|graphql).*`) 29 | ) 30 | 31 | // GlobalInit is for global configuration reload-able. 32 | func GlobalInit() { 33 | if err := model.NewEngine(); err != nil { 34 | log.Fatal().Err(err).Msg("Failed to initialize ORM engine.") 35 | } 36 | 37 | // initial socket module 38 | // if err := socket.NewEngine(); err != nil { 39 | // log.Fatal().Err(err).Msg("Failed to initialize Socket IO engine") 40 | // } 41 | 42 | if config.QRCode.Enable { 43 | var err error 44 | storage.S3, err = storage.NewEngine() 45 | if err != nil { 46 | log.Fatal().Err(err).Msg("Failed to create s3 interface") 47 | } 48 | 49 | if err := storage.S3.CreateBucket(config.Minio.Bucket, config.Minio.Region); err != nil { 50 | log.Fatal().Err(err).Msg("Failed to create s3 bucket") 51 | } 52 | } 53 | 54 | // initial dataloader cache 55 | if err := loader.NewEngine(config.Cache.Driver, config.Cache.Prefix, config.Cache.Expire); err != nil { 56 | log.Fatal().Err(err).Msg("Failed to initial dataloader.") 57 | } 58 | } 59 | 60 | // Load initializes the routing of the application. 61 | func Load(middleware ...gin.HandlerFunc) http.Handler { 62 | if config.Server.Debug { 63 | gin.SetMode(gin.DebugMode) 64 | } else { 65 | gin.SetMode(gin.ReleaseMode) 66 | } 67 | 68 | c := metrics.NewCollector() 69 | prometheus.MustRegister(c) 70 | 71 | e := gin.New() 72 | 73 | e.Use(gin.Recovery()) 74 | e.Use(logger.SetLogger(logger.Config{ 75 | UTC: true, 76 | SkipPathRegexp: rxURL, 77 | })) 78 | // e.Use(gzip.Gzip(gzip.DefaultCompression)) 79 | e.Use(header.Options) 80 | e.Use(header.Secure) 81 | e.Use(middleware...) 82 | 83 | if config.Server.Pprof { 84 | pprof.Register( 85 | e, 86 | path.Join(config.Server.Root, "debug", "pprof"), 87 | ) 88 | } 89 | 90 | // redirect to vue page 91 | e.NoRoute(gzip.Gzip(gzip.DefaultCompression), api.Index) 92 | 93 | // default route / 94 | root := e.Group(config.Server.Root) 95 | { 96 | if config.Storage.Driver == "disk" { 97 | root.StaticFS( 98 | "/storage", 99 | gin.Dir( 100 | config.Storage.Path, 101 | false, 102 | ), 103 | ) 104 | } 105 | 106 | root.StaticFS( 107 | "/public", 108 | assets.Load(), 109 | ) 110 | 111 | root.GET("", gzip.Gzip(gzip.DefaultCompression), api.Index) 112 | root.GET("/favicon.ico", api.Favicon) 113 | root.GET("/healthz", api.Heartbeat) 114 | root.GET("/assets/*name", gzip.Gzip(gzip.DefaultCompression), assets.ViewHandler()) 115 | if config.Metrics.Enabled { 116 | root.GET("/metrics", router.Metrics(config.Metrics.Token)) 117 | } 118 | 119 | v := e.Group("/v1") 120 | v.Use(auth.Check()) 121 | { 122 | v.POST("/url/meta", api.URLMeta) 123 | v.POST("/s", api.CreateShortenURL) 124 | } 125 | 126 | g := e.Group("/graphql") 127 | g.Use(auth.Check()) 128 | { 129 | g.POST("", router.GraphQL()) 130 | if config.Server.GraphiQL { 131 | g.GET("", router.GraphQL()) 132 | } 133 | } 134 | 135 | // socket connection 136 | // root.GET("/socket.io/", socket.Handler()) 137 | // root.POST("/socket.io/", socket.Handler()) 138 | // root.Handle("WS", "/socket.io", socket.Handler()) 139 | // root.Handle("WSS", "/socket.io", socket.Handler()) 140 | } 141 | 142 | return e 143 | } 144 | 145 | // LoadRedirct initializes the routing of the shorten URL application. 146 | func LoadRedirct(middleware ...gin.HandlerFunc) http.Handler { 147 | if config.Server.Debug { 148 | gin.SetMode(gin.DebugMode) 149 | } else { 150 | gin.SetMode(gin.ReleaseMode) 151 | } 152 | 153 | e := gin.New() 154 | 155 | e.Use(gin.Recovery()) 156 | e.Use(logger.SetLogger(logger.Config{ 157 | UTC: true, 158 | SkipPathRegexp: rxURL, 159 | })) 160 | e.Use(header.Options) 161 | e.Use(header.Secure) 162 | e.Use(middleware...) 163 | 164 | if config.Server.Pprof { 165 | pprof.Register( 166 | e, 167 | path.Join(config.Server.Root, "debug", "pprof"), 168 | ) 169 | } 170 | 171 | // 404 not found 172 | e.NoRoute(api.NotFound) 173 | 174 | // default route / 175 | root := e.Group(config.Server.Root) 176 | { 177 | root.GET("", api.Index) 178 | root.GET("/:slug", api.RedirectURL) 179 | } 180 | 181 | return e 182 | } 183 | -------------------------------------------------------------------------------- /pkg/router/routes/routes_test.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/go-ggz/ggz/pkg/model" 8 | 9 | "github.com/appleboy/gofight/v2" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestHealthzOnRedirectService(t *testing.T) { 14 | assert.NoError(t, model.PrepareTestDatabase()) 15 | 16 | r := gofight.New() 17 | 18 | t.Run("return 200", func(t *testing.T) { 19 | r.GET("/healthz"). 20 | Run(LoadRedirct(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 21 | assert.Equal(t, http.StatusOK, r.Code) 22 | assert.Equal(t, "ok", r.Body.String()) 23 | }) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/schema/errors.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | const ( 4 | errorYouAreNotLogin = "you are not login" 5 | errorUserNotFound = "user not found" 6 | errorShortenURLNotFound = "url not found" 7 | errorInvaildFormatURL = "invaild format url" 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/schema/main_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-ggz/ggz/pkg/config" 7 | "github.com/go-ggz/ggz/pkg/model" 8 | 9 | "github.com/kelseyhightower/envconfig" 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | func TestMain(m *testing.M) { 14 | if err := envconfig.Process("GGZ", config.Server); err != nil { 15 | log.Fatal().Err(err).Msg("can't load server config") 16 | } 17 | 18 | model.MainTest(m, "../..") 19 | } 20 | -------------------------------------------------------------------------------- /pkg/schema/schema.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/graphql-go/graphql" 5 | ) 6 | 7 | var rootQuery = graphql.NewObject( 8 | graphql.ObjectConfig{ 9 | Name: "RootQuery", 10 | Description: "Root Query", 11 | Fields: graphql.Fields{ 12 | "queryURLMetadata": &queryURLMeta, 13 | "queryShortenURL": &queryShortenURL, 14 | "queryAllShortenURL": &queryAllShortenURL, 15 | "queryMe": &queryMe, 16 | }, 17 | }) 18 | 19 | var rootMutation = graphql.NewObject( 20 | graphql.ObjectConfig{ 21 | Name: "RootMutation", 22 | Description: "Root Mutation", 23 | Fields: graphql.Fields{ 24 | "createShortenURL": &createShortenURL, 25 | }, 26 | }) 27 | 28 | // Schema is the GraphQL schema served by the server. 29 | var Schema, _ = graphql.NewSchema( 30 | graphql.SchemaConfig{ 31 | Query: rootQuery, 32 | Mutation: rootMutation, 33 | }) 34 | -------------------------------------------------------------------------------- /pkg/schema/unit_tests.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/go-ggz/ggz/pkg/config" 9 | "github.com/go-ggz/ggz/pkg/model" 10 | 11 | "github.com/graphql-go/graphql" 12 | "github.com/graphql-go/graphql/testutil" 13 | ) 14 | 15 | // T for graphql testing schema 16 | type T struct { 17 | Query string 18 | Schema graphql.Schema 19 | Expected *graphql.Result 20 | } 21 | 22 | func testGraphql(test T, p graphql.Params, t *testing.T) { 23 | result := graphql.Do(p) 24 | if len(result.Errors) > 0 { 25 | t.Fatalf("wrong result, unexpected errors: %v", result.Errors) 26 | } 27 | if !reflect.DeepEqual(result, test.Expected) { 28 | t.Fatalf("wrong result, query: %v, graphql result diff: %v", test.Query, testutil.Diff(test.Expected, result)) 29 | } 30 | } 31 | 32 | func testGraphqlErr(test T, p graphql.Params, t *testing.T) { 33 | result := graphql.Do(p) 34 | if len(result.Errors) != len(test.Expected.Errors) { 35 | t.Fatalf("Unexpected errors, Diff: %v", testutil.Diff(test.Expected.Errors, result.Errors)) 36 | } 37 | 38 | if len(test.Expected.Errors) > 0 && 39 | result.Errors[0].Message != test.Expected.Errors[0].Message { 40 | t.Fatalf("Unexpected error message, Diff: %v", testutil.Diff(test.Expected.Errors, result.Errors)) 41 | } 42 | } 43 | 44 | func newContextWithUser(ctx context.Context, u *model.User) context.Context { 45 | return context.WithValue(ctx, config.ContextKeyUser, u) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/schema/url.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/go-ggz/ggz/pkg/config" 5 | "github.com/go-ggz/ggz/pkg/errors" 6 | "github.com/go-ggz/ggz/pkg/helper" 7 | "github.com/go-ggz/ggz/pkg/model" 8 | "github.com/go-ggz/ggz/pkg/module/loader" 9 | "github.com/go-ggz/ggz/pkg/module/meta" 10 | 11 | "github.com/graphql-go/graphql" 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | var shortenType = graphql.NewObject(graphql.ObjectConfig{ 16 | Name: "ShortenType", 17 | Description: "Shorten URL Type", 18 | Fields: graphql.Fields{ 19 | "slug": &graphql.Field{ 20 | Type: graphql.String, 21 | }, 22 | "user": &graphql.Field{ 23 | Type: userType, 24 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 25 | o, ok := p.Source.(*model.Shorten) 26 | 27 | if !ok { 28 | return nil, errors.ENotFound(errorShortenURLNotFound, nil) 29 | } 30 | 31 | if o.User != nil { 32 | return o.User, nil 33 | } 34 | 35 | return loader.GetUserFromLoader(p.Context, o.UserID) 36 | }, 37 | }, 38 | "url": &graphql.Field{ 39 | Type: graphql.String, 40 | }, 41 | "date": &graphql.Field{ 42 | Type: graphql.DateTime, 43 | }, 44 | "hits": &graphql.Field{ 45 | Type: graphql.Int, 46 | }, 47 | "title": &graphql.Field{ 48 | Type: graphql.String, 49 | }, 50 | "description": &graphql.Field{ 51 | Type: graphql.String, 52 | }, 53 | "type": &graphql.Field{ 54 | Type: graphql.String, 55 | }, 56 | "image": &graphql.Field{ 57 | Type: graphql.String, 58 | }, 59 | }, 60 | }) 61 | 62 | var urlType = graphql.NewObject(graphql.ObjectConfig{ 63 | Name: "URL", 64 | Description: "URL Type", 65 | Fields: graphql.Fields{ 66 | "Scheme": &graphql.Field{ 67 | Type: graphql.String, 68 | }, 69 | "Opaque": &graphql.Field{ 70 | Type: graphql.String, 71 | }, 72 | "User": &graphql.Field{ 73 | Type: graphql.String, 74 | }, 75 | "Host": &graphql.Field{ 76 | Type: graphql.String, 77 | }, 78 | "Path": &graphql.Field{ 79 | Type: graphql.String, 80 | }, 81 | "RawPath": &graphql.Field{ 82 | Type: graphql.String, 83 | }, 84 | "ForceQuery": &graphql.Field{ 85 | Type: graphql.Boolean, 86 | }, 87 | "RawQuery": &graphql.Field{ 88 | Type: graphql.String, 89 | }, 90 | "Fragment": &graphql.Field{ 91 | Type: graphql.String, 92 | }, 93 | }, 94 | }) 95 | 96 | var urlMetaType = graphql.NewObject(graphql.ObjectConfig{ 97 | Name: "URLMeta", 98 | Description: "URL Meta Type", 99 | Fields: graphql.Fields{ 100 | "title": &graphql.Field{ 101 | Type: graphql.String, 102 | }, 103 | "description": &graphql.Field{ 104 | Type: graphql.String, 105 | }, 106 | "type": &graphql.Field{ 107 | Type: graphql.String, 108 | }, 109 | "image": &graphql.Field{ 110 | Type: graphql.String, 111 | }, 112 | "time": &graphql.Field{ 113 | Type: graphql.DateTime, 114 | }, 115 | "video_width": &graphql.Field{ 116 | Type: graphql.Int, 117 | }, 118 | "video_height": &graphql.Field{ 119 | Type: graphql.Int, 120 | }, 121 | "url": &graphql.Field{ 122 | Type: urlType, 123 | }, 124 | }, 125 | }) 126 | 127 | var queryURLMeta = graphql.Field{ 128 | Name: "QueryURLMeta", 129 | Description: "Query URL Metadata", 130 | Type: urlMetaType, 131 | Args: graphql.FieldConfigArgument{ 132 | "url": &graphql.ArgumentConfig{ 133 | Type: graphql.NewNonNull(graphql.String), 134 | }, 135 | }, 136 | Resolve: func(p graphql.ResolveParams) (result interface{}, err error) { 137 | url := p.Args["url"].(string) 138 | 139 | if !helper.IsURL(url) { 140 | return nil, errors.EBadRequest(errorInvaildFormatURL, nil) 141 | } 142 | 143 | return meta.FetchData(url) 144 | }, 145 | } 146 | 147 | var createShortenURL = graphql.Field{ 148 | Name: "CreateShortenURL", 149 | Description: "Create Shorten URL", 150 | Type: shortenType, 151 | Args: graphql.FieldConfigArgument{ 152 | "url": &graphql.ArgumentConfig{ 153 | Type: graphql.NewNonNull(graphql.String), 154 | }, 155 | }, 156 | Resolve: func(p graphql.ResolveParams) (result interface{}, err error) { 157 | url, _ := p.Args["url"].(string) 158 | user := helper.GetUserDataFromModel(p.Context) 159 | 160 | row, err := model.GetShortenFromURL(url) 161 | 162 | if model.IsErrURLExist(err) { 163 | return row, nil 164 | } 165 | 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | row, err = model.CreateShorten(url, config.Server.ShortenSize, user) 171 | 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | // upload QRCode image. 177 | go func(slug string) { 178 | if err := helper.QRCodeGenerator(slug); err != nil { 179 | log.Error().Err(err).Msg("QRCode Generator fail") 180 | } 181 | }(row.Slug) 182 | 183 | return row, nil 184 | }, 185 | } 186 | 187 | var queryShortenURL = graphql.Field{ 188 | Name: "QueryShortenURL", 189 | Description: "Query Shorten URL", 190 | Type: shortenType, 191 | Args: graphql.FieldConfigArgument{ 192 | "slug": &graphql.ArgumentConfig{ 193 | Type: graphql.NewNonNull(graphql.String), 194 | }, 195 | }, 196 | Resolve: func(p graphql.ResolveParams) (result interface{}, err error) { 197 | slug, _ := p.Args["slug"].(string) 198 | 199 | return model.GetShortenBySlug(slug) 200 | }, 201 | } 202 | 203 | var queryAllShortenURL = graphql.Field{ 204 | Name: "QueryAllShortenURL", 205 | Description: "Query All Shorten URL", 206 | Type: graphql.NewList(shortenType), 207 | Args: graphql.FieldConfigArgument{ 208 | "userID": &graphql.ArgumentConfig{ 209 | Type: graphql.Int, 210 | }, 211 | "page": &graphql.ArgumentConfig{ 212 | Type: graphql.Int, 213 | DefaultValue: 1, 214 | }, 215 | "pageSize": &graphql.ArgumentConfig{ 216 | Type: graphql.Int, 217 | DefaultValue: 10, 218 | }, 219 | }, 220 | Resolve: func(p graphql.ResolveParams) (result interface{}, err error) { 221 | id, _ := p.Args["userID"].(int) 222 | page, _ := p.Args["page"].(int) 223 | pageSize, _ := p.Args["pageSize"].(int) 224 | userID := int64(id) 225 | 226 | return model.GetShortenURLs(userID, page, pageSize, "") 227 | }, 228 | } 229 | -------------------------------------------------------------------------------- /pkg/schema/url_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/go-ggz/ggz/pkg/model" 8 | 9 | "github.com/graphql-go/graphql" 10 | "github.com/graphql-go/graphql/gqlerrors" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestQueryURLMeta(t *testing.T) { 15 | t.Run("invaild url", func(t *testing.T) { 16 | test := T{ 17 | Query: ` 18 | query queryURLMetadata ( 19 | $url: String! 20 | ) { 21 | queryURLMetadata(url: $url) { 22 | title 23 | } 24 | } 25 | `, 26 | Schema: Schema, 27 | Expected: &graphql.Result{ 28 | Data: map[string]interface{}{ 29 | "queryURLMetadata": map[string]interface{}{ 30 | "title": "http://example.com", 31 | }, 32 | }, 33 | Errors: []gqlerrors.FormattedError{ 34 | { 35 | Message: `Get example.com: unsupported protocol scheme ""`, 36 | }, 37 | }, 38 | }, 39 | } 40 | params := graphql.Params{ 41 | Schema: test.Schema, 42 | RequestString: test.Query, 43 | Context: newContextWithUser(context.TODO(), nil), 44 | VariableValues: map[string]interface{}{ 45 | "url": "example.com", 46 | }, 47 | } 48 | testGraphqlErr(test, params, t) 49 | }) 50 | 51 | t.Run("vaild url", func(t *testing.T) { 52 | test := T{ 53 | Query: ` 54 | query queryURLMetadata ( 55 | $url: String! 56 | ) { 57 | queryURLMetadata(url: $url) { 58 | title 59 | } 60 | } 61 | `, 62 | Schema: Schema, 63 | Expected: &graphql.Result{ 64 | Data: map[string]interface{}{ 65 | "queryURLMetadata": map[string]interface{}{ 66 | "title": "小惡魔 - 電腦技術 - 工作筆記 - AppleBOY", 67 | }, 68 | }, 69 | }, 70 | } 71 | params := graphql.Params{ 72 | Schema: test.Schema, 73 | RequestString: test.Query, 74 | Context: newContextWithUser(context.TODO(), nil), 75 | VariableValues: map[string]interface{}{ 76 | "url": "https://blog.wu-boy.com", 77 | }, 78 | } 79 | testGraphql(test, params, t) 80 | }) 81 | } 82 | 83 | func TestQueryShortenURL(t *testing.T) { 84 | assert.NoError(t, model.PrepareTestDatabase()) 85 | user := model.AssertExistsAndLoadBean(t, &model.User{ID: 1}).(*model.User) 86 | ctx := newContextWithUser(context.TODO(), user) 87 | 88 | t.Run("shorten url exist", func(t *testing.T) { 89 | test := T{ 90 | Query: ` 91 | query queryShortenURL ( 92 | $slug: String! 93 | ) { 94 | queryShortenURL(slug: $slug) { 95 | url 96 | } 97 | } 98 | `, 99 | Schema: Schema, 100 | Expected: &graphql.Result{ 101 | Data: map[string]interface{}{ 102 | "queryShortenURL": map[string]interface{}{ 103 | "url": "http://example.com", 104 | }, 105 | }, 106 | }, 107 | } 108 | params := graphql.Params{ 109 | Schema: test.Schema, 110 | RequestString: test.Query, 111 | Context: ctx, 112 | VariableValues: map[string]interface{}{ 113 | "slug": "abcdef", 114 | }, 115 | } 116 | testGraphql(test, params, t) 117 | }) 118 | 119 | t.Run("shorten url not exist", func(t *testing.T) { 120 | test := T{ 121 | Query: ` 122 | query queryShortenURL ( 123 | $slug: String! 124 | ) { 125 | queryShortenURL(slug: $slug) { 126 | url 127 | } 128 | } 129 | `, 130 | Schema: Schema, 131 | Expected: &graphql.Result{ 132 | Data: map[string]interface{}{ 133 | "queryShortenURL": nil, 134 | }, 135 | Errors: []gqlerrors.FormattedError{ 136 | { 137 | Message: `shorten slug does not exist [slug: 1234567890]`, 138 | }, 139 | }, 140 | }, 141 | } 142 | params := graphql.Params{ 143 | Schema: test.Schema, 144 | RequestString: test.Query, 145 | Context: ctx, 146 | VariableValues: map[string]interface{}{ 147 | "slug": "1234567890", 148 | }, 149 | } 150 | testGraphqlErr(test, params, t) 151 | }) 152 | } 153 | -------------------------------------------------------------------------------- /pkg/schema/user.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/go-ggz/ggz/pkg/errors" 5 | "github.com/go-ggz/ggz/pkg/helper" 6 | "github.com/go-ggz/ggz/pkg/model" 7 | "github.com/go-ggz/ggz/pkg/module/loader" 8 | 9 | "github.com/graphql-go/graphql" 10 | ) 11 | 12 | var userType = graphql.NewObject(graphql.ObjectConfig{ 13 | Name: "UserType", 14 | Description: "User Type", 15 | Fields: graphql.Fields{ 16 | "id": &graphql.Field{ 17 | Type: graphql.ID, 18 | }, 19 | "email": &graphql.Field{ 20 | Type: graphql.String, 21 | }, 22 | "fullname": &graphql.Field{ 23 | Type: graphql.String, 24 | }, 25 | "location": &graphql.Field{ 26 | Type: graphql.Int, 27 | }, 28 | "website": &graphql.Field{ 29 | Type: graphql.String, 30 | }, 31 | "is_active": &graphql.Field{ 32 | Type: graphql.Boolean, 33 | }, 34 | "created_at": &graphql.Field{ 35 | Type: graphql.DateTime, 36 | }, 37 | "updated_at": &graphql.Field{ 38 | Type: graphql.DateTime, 39 | }, 40 | }, 41 | }) 42 | 43 | var queryMe = graphql.Field{ 44 | Name: "QueryMe", 45 | Description: "Query Cureent User", 46 | Type: userType, 47 | Resolve: func(p graphql.ResolveParams) (result interface{}, err error) { 48 | user := helper.GetUserDataFromModel(p.Context) 49 | if user == nil { 50 | return nil, errors.EUnauthorized(errorYouAreNotLogin, nil) 51 | } 52 | 53 | return loader.GetUserFromLoader(p.Context, user.ID) 54 | }, 55 | } 56 | 57 | func init() { 58 | userType.AddFieldConfig("urls", &graphql.Field{ 59 | Type: graphql.NewList(shortenType), 60 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 61 | o, ok := p.Source.(*model.User) 62 | 63 | if !ok { 64 | return nil, errors.ENotFound(errorUserNotFound, nil) 65 | } 66 | 67 | return model.GetShortenURLs(o.ID, 0, 10, "") 68 | }, 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | // Version indicates development branch. Releases will be empty string. 10 | Version string 11 | // BuildDate is the ISO 8601 day drone was built. 12 | BuildDate string 13 | ) 14 | 15 | // PrintCLIVersion print server info 16 | func PrintCLIVersion() string { 17 | return fmt.Sprintf( 18 | "version %s, built on %s, %s", 19 | Version, 20 | BuildDate, 21 | runtime.Version(), 22 | ) 23 | } 24 | --------------------------------------------------------------------------------