├── .dockerignore ├── .editorconfig ├── .github ├── FUNDING.yml ├── manual-workflow-trigger.ffs └── workflows │ ├── docker-canary.yml │ ├── docker-master.yml │ ├── docker-tags.yml │ └── main.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── Gopkg.toml ├── LICENSE ├── Makefile ├── README.md ├── ci ├── docker-deploy.sh └── travis.sh ├── cmd ├── server │ └── main.go └── testing │ └── main.go ├── config ├── example.config.yml └── insomnia │ └── insomnia-export.json ├── docker-compose.yml ├── docs ├── cookie-usage.md ├── imprint.txt └── restapi-docs.md ├── go.mod ├── go.sum ├── internal ├── assets │ └── avatarhandler.go ├── auth │ └── middleware.go ├── caching │ ├── internal.go │ ├── middleware.go │ └── redis.go ├── config │ └── config.go ├── database │ ├── middleware.go │ └── mongodb.go ├── logger │ └── logger.go ├── mailserver │ └── mailserver.go ├── objects │ ├── apitoken.go │ ├── page.go │ ├── refreshtoken.go │ ├── session.go │ ├── share.go │ └── user.go ├── ratelimit │ └── ratelimit.go ├── shared │ └── shared.go ├── static │ ├── idnodes.go │ ├── ldflags.go │ └── static.go ├── storage │ ├── file.go │ ├── middleware.go │ └── minio.go └── webserver │ ├── auth.go │ ├── handlers.go │ ├── helpers.go │ ├── structs.go │ └── websrever.go ├── pkg ├── comparison │ ├── alphabetical.go │ └── istrue.go ├── ddragon │ ├── ddragon.go │ ├── formatters.go │ ├── static.go │ └── structs.go ├── etag │ └── etag.go ├── lifecycletimer │ └── lifecycletimer.go ├── random │ └── random.go ├── recapatcha │ └── recapatcha.go └── workerpool │ └── workerpool.go └── scripts └── get-champ-avis.bash /.dockerignore: -------------------------------------------------------------------------------- 1 | assets/ 2 | ci/ 3 | config/ 4 | docs/ 5 | scripts/ 6 | vendor/ 7 | .prettierrc.yml 8 | .travis.yml 9 | Gopkg.toml 10 | Makefile 11 | *.md 12 | node_modules -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = crlf 4 | indent_size = 2 5 | indent_style = space 6 | insert_final_newline = false 7 | trim_trailing_whitespace = true 8 | 9 | [*.md] 10 | trim_trailing_whitespace = false 11 | 12 | [*.go] 13 | indent_size = 4 14 | indent_style = tab 15 | 16 | [Makefile*] 17 | indent_size = 4 18 | indent_style = tab -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: 13 | - https://paypal.me/zekro 14 | -------------------------------------------------------------------------------- /.github/manual-workflow-trigger.ffs: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /.github/workflows/docker-canary.yml: -------------------------------------------------------------------------------- 1 | name: Docker CD Canary 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | branches: 8 | - dev 9 | 10 | jobs: 11 | 12 | build_docker: 13 | name: Build Docker Image 14 | runs-on: ubuntu-latest 15 | steps: 16 | 17 | - name: Check out code 18 | uses: actions/checkout@v1 19 | 20 | - name: Build Docker Image 21 | run: | 22 | docker build . \ 23 | -t myrunes/backend:canary \ 24 | --build-arg RELEASE=TRUE 25 | 26 | - name: Publish Image 27 | run: | 28 | docker login -u zekro -p ${{ secrets.DOCKER_PASSWORD }} 29 | docker push myrunes/backend:canary -------------------------------------------------------------------------------- /.github/workflows/docker-master.yml: -------------------------------------------------------------------------------- 1 | name: Docker CD Master 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | branches: 8 | - master 9 | 10 | jobs: 11 | 12 | build_docker: 13 | name: Build Docker Image 14 | runs-on: ubuntu-latest 15 | steps: 16 | 17 | - name: Check out code 18 | uses: actions/checkout@v1 19 | 20 | - name: Build Docker Image 21 | run: | 22 | docker build . \ 23 | -t myrunes/backend:latest \ 24 | --build-arg RELEASE=TRUE 25 | 26 | - name: Publish Image 27 | run: | 28 | docker login -u zekro -p ${{ secrets.DOCKER_PASSWORD }} 29 | docker push myrunes/backend:latest -------------------------------------------------------------------------------- /.github/workflows/docker-tags.yml: -------------------------------------------------------------------------------- 1 | name: Docker CD Tag 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | tags: 8 | - '*' 9 | 10 | jobs: 11 | 12 | build_docker: 13 | name: Build Docker Image 14 | runs-on: ubuntu-latest 15 | steps: 16 | 17 | - name: Check out code 18 | uses: actions/checkout@v1 19 | 20 | - name: Build Docker Image 21 | run: | 22 | docker build . \ 23 | -t myrunes/backend:$(git describe --tags) \ 24 | --build-arg RELEASE=FALSE 25 | 26 | - name: Publish Image 27 | run: | 28 | docker login -u zekro -p ${{ secrets.DOCKER_PASSWORD }} 29 | docker push myrunes/backend:$(git describe --tags) -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | 8 | jobs: 9 | 10 | build_backend: 11 | name: Build Back End 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v1 17 | with: 18 | go-version: ^1.14 19 | 20 | - name: Check out code 21 | uses: actions/checkout@v1 22 | 23 | - name: Get dependencies 24 | run: | 25 | go get -v -t -d ./... 26 | 27 | - name: Build Backend 28 | run: | 29 | go build -v ./cmd/server/*.go 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # EXTRA STUFF 18 | private.* 19 | *.lock 20 | vendor/* 21 | 22 | node_modules 23 | dist 24 | bin/* 25 | 26 | .DS_Store 27 | 28 | # local env files 29 | .env.local 30 | .env.*.local 31 | 32 | # Log files 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | 37 | # Editor directories and files 38 | .idea 39 | .vscode 40 | *.suo 41 | *.ntvs* 42 | *.njsproj 43 | *.sln 44 | *.sw? 45 | 46 | __debug_bin 47 | 48 | fsdata/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: go 4 | 5 | go: 6 | - tip 7 | 8 | jobs: 9 | include: 10 | 11 | - stage: build 12 | name: Build 13 | script: time bash ./ci/travis.sh 14 | 15 | # - stage: docker-deploy 16 | # name: Docker Deploy Branch 17 | # if: branch = master OR branch = dev 18 | # script: bash ./ci/docker-deploy.sh 19 | 20 | # - stage: docker-deploy-tag 21 | # name: Docker Deploy Tag 22 | # if: tag IS present 23 | # script: | 24 | # docker build . -t zekro/myrunes:${TRAVIS_TAG} 25 | # docker login -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD} 26 | # docker push zekro/myrunes:${TRAVIS_TAG} -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine as build 2 | ARG RELEASE=TRUE 3 | WORKDIR /var/myrunes 4 | ADD . . 5 | 6 | RUN apk add git 7 | RUN go mod download 8 | RUN go build \ 9 | -v -o /app/myrunes -ldflags "\ 10 | -X github.com/myrunes/backend/internal/static.Release=${RELEASE} \ 11 | -X github.com/myrunes/backend/internal/static.AppVersion=$(git describe --tags --abbrev=0)+$(git describe --tags | sed -n 's/^[0-9]\+\.[0-9]\+\.[0-9]\+-\([0-9]\+\)-.*$/\1/p')" \ 12 | ./cmd/server/*.go 13 | 14 | # ---------------------------------------------------------- 15 | 16 | FROM alpine:latest AS final 17 | LABEL maintainer="zekro " 18 | WORKDIR /app 19 | 20 | RUN apk add ca-certificates 21 | COPY --from=build /app . 22 | 23 | EXPOSE 8080 24 | RUN mkdir -p /etc/myrunes 25 | CMD ["/app/myrunes", "-c", "/etc/myrunes/config.yml"] 26 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [prune] 29 | go-tests = true 30 | unused-packages = true 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ringo Hoffmann (zekro Development) 4 | Copyright (c) 2019 MYRUNES 5 | 6 | Logo and Grapgic Deisgn: 7 | Copyright (c) 2019 luxtracon 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ### NAMES AND LOCS ############################ 2 | APPNAME = lol-runes 3 | PACKAGE = github.com/myrunes/backend 4 | LDPAKAGE = internal/static 5 | CONFIG = $(CURDIR)/config/private.config.yml 6 | BINPATH = $(CURDIR)/bin 7 | ############################################### 8 | 9 | ### EXECUTABLES ############################### 10 | GO = go 11 | GOLINT = golint 12 | GREP = grep 13 | ############################################### 14 | 15 | # --------------------------------------------- 16 | 17 | BIN = $(BINPATH)/$(APPNAME) 18 | 19 | TAG = $(shell git describe --tags) 20 | COMMIT = $(shell git rev-parse HEAD) 21 | 22 | 23 | ifneq ($(GOOS),) 24 | BIN := $(BIN)_$(GOOS) 25 | endif 26 | 27 | ifneq ($(GOARCH),) 28 | BIN := $(BIN)_$(GOARCH) 29 | endif 30 | 31 | ifneq ($(TAG),) 32 | BIN := $(BIN)_$(TAG) 33 | endif 34 | 35 | ifeq ($(OS),Windows_NT) 36 | ifeq ($(GOOS),) 37 | BIN := $(BIN).exe 38 | endif 39 | endif 40 | 41 | ifeq ($(GOOS),windows) 42 | BIN := $(BIN).exe 43 | endif 44 | 45 | 46 | PHONY = _make 47 | _make: deps build fe cleanup 48 | 49 | PHONY += build 50 | build: $(BIN) 51 | 52 | PHONY += deps 53 | deps: 54 | $(DEP) ensure -v 55 | 56 | $(BIN): 57 | $(GO) build \ 58 | -v -o $@ -ldflags "\ 59 | -X $(PACKAGE)/$(LDPAKAGE).AppVersion=$(TAG) \ 60 | -X $(PACKAGE)/$(LDPAKAGE).AppCommit=$(COMMIT) \ 61 | -X $(PACKAGE)/$(LDPAKAGE).Release=TRUE" \ 62 | $(CURDIR)/cmd/server/*.go 63 | 64 | PHONY += test 65 | test: 66 | $(GO) test -v -cover ./... 67 | 68 | PHONY += lint 69 | lint: 70 | $(GOLINT) ./... | $(GREP) -v vendor || true 71 | 72 | PHONY += run 73 | run: 74 | $(GO) run -v \ 75 | $(CURDIR)/cmd/server/*.go -c $(CONFIG) -skipFetch 76 | 77 | PHONY += cleanup 78 | cleanup: 79 | 80 | PHONY += help 81 | help: 82 | @echo "Available targets:" 83 | @echo " # - creates binary in ./bin" 84 | @echo " cleanup - tidy up temporary stuff created by build or scripts" 85 | @echo " deps - ensure dependencies are installed" 86 | @echo " fe - build font end files" 87 | @echo " lint - run linters (golint)" 88 | @echo " run - debug run app (go run) with test config" 89 | @echo " test - run tests (go test)" 90 | @echo "" 91 | @echo "Cross Compiling:" 92 | @echo " (env GOOS=linux GOARCH=arm make)" 93 | @echo "" 94 | @echo "Use different configs for run:" 95 | @echo " make CONF=./myCustomConfig.yml run" 96 | @echo "" 97 | 98 | 99 | .PHONY: $(PHONY) 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | Save your League of Legends rune pages without wasting money.

5 |   6 |   7 |   8 | 9 |

10 |   11 |   12 |   13 |
14 | 15 | --- 16 | 17 | # Introduction 18 | 19 | MYRUNES is a little web tool where you can simply create and store League of Legends rune pages without spending ingame (or even real) money for rune pages. Just visit [myrunes.com](https://myrunes.com), create an account and save your runes to be ready for the next pick and ban. 20 | Of course, if you don't trust us, you can download the source code and build the binaries and front end to be hosted on your own server environment. 21 | 22 | --- 23 | 24 | # To Do & Future Goals 25 | 26 | To get an overview about current goals and milestones for the next release version, take a look into the [**Projects**](https://github.com/myrunes/backend/projects) page. 27 | 28 | A list of open issues and ideas is availabe with the [**issue tracker**](https://github.com/myrunes/backend/issues) of this repository. 29 | 30 | --- 31 | 32 | # Self Hosting 33 | 34 | ## Docker 35 | 36 | You can self-host this application by using the supplied [**docker images**](https://cloud.docker.com/u/zekro/repository/docker/zekro/myrunes). 37 | 38 | Just use the following command to pull the latest stable image: 39 | ``` 40 | # docker pull zekro/myrunes:latest 41 | ``` 42 | 43 | On startup, you need to bind the exposed web server port `8080` and the volume `/etc/myrunes` to your host system: 44 | 45 | ``` 46 | # docker run \ 47 | -p 443:8080 \ 48 | -v /etc/myrunes:/etc/myrunes \ 49 | zekro/myrunes:latest 50 | ``` 51 | 52 | You can use following configuration with a MongoDB container using Docker Compose: 53 | 54 | ```yml 55 | version: '3' 56 | 57 | services: 58 | 59 | mongo: 60 | image: 'mongo:latest' 61 | expose: 62 | - '27017' 63 | volumes: 64 | - './mongodb/data/db:/data/db' 65 | - '/home/mgr/dump:/var/dump' 66 | command: '--auth' 67 | restart: always 68 | 69 | myrunes: 70 | image: "zekro/myrunes:latest" 71 | ports: 72 | - "443:8080" 73 | volumes: 74 | - "/etc/myrunes:/etc/myrunes" 75 | environment: 76 | # You dont need to define the configuration 77 | # with environment variables fi you prefer 78 | # using the config file instead. 79 | - 'DB_HOST=mongo' 80 | - 'DB_PORT=27017' 81 | - 'DB_USERNAME=myrunes' 82 | - 'DB_PASSWORD=somepw' 83 | - 'DB_AUTHDB=myrunes' 84 | - 'DB_DATADB=myrunes' 85 | - 'TLS_ENABLE=true' 86 | - 'TLS_KEY=/etc/cert/key.pem' 87 | - 'TLS_CERT=/etc/cert/cert.pem' 88 | ports: 89 | - '443:8080' 90 | restart: always 91 | ``` 92 | 93 | ## As daemon 94 | 95 | First of all, if you want to self host the MYRUNES system, your environment should pass certain requirements: 96 | 97 | - [**MongoDB**](https://www.mongodb.com/) 98 | The server application uses MongoDB as database and storage system. 99 | 100 | - **[PM2](https://pm2.io/)** or **[screen](https://linux.die.net/man/1/screen)** 101 | ...or something else to deamonize an application which is highly recommended for running the server component. 102 | 103 | Also, you need the following toolchains for building the backend and frontend components: 104 | 105 | - **[git](https://git-scm.com/)** 106 | - **[go compiler toolchain](https://golang.org/)** 107 | - **[dep package manager](https://github.com/golang/dep)** 108 | - **[nodejs](https://nodejs.org/en/)** and **[npm](https://www.npmjs.com/)** *(npm will be automatically installed with nodejs)* 109 | - **[Vue CLI](https://cli.vuejs.org/)** 110 | 111 | Also, it is highly recommended to install **[GNU make](https://www.gnu.org/software/make/)** to simplify the build process. If you are using windows, you can install **[make for Windows](http://gnuwin32.sourceforge.net/packages/make.htm)**. 112 | 113 | ## Compiling 114 | 115 | 1. Set up GOPATH, if not done yet. Read [here](https://golang.org/pkg/go/build/#hdr-Go_Path) how to do this. 116 | 117 | 2. Clone the repository into your GOPATH: 118 | ``` 119 | $ git clone https://github.com/myrunes/backend $GOPATH/src/github.com/myrunes/backend 120 | $ cd $GOPATH/src/github.com/myrunes/backend 121 | ``` 122 | 123 | 3. Build binaries and assets using the `Makefile`: 124 | ``` 125 | $ make 126 | ``` 127 | 128 | Now, the server binary and the web assets are located in the `./bin` directory. You can move them wherever you want, just always keep the `web` folder in the same location where the server binary is located to ensure that all web assets can be found by the web server. 129 | 130 | ## Startup 131 | 132 | Now, you just need to start the server binary passing the location of your preferred config location. A preset config file will be then automatically created. Now, enter your preferences and restart the server. 133 | 134 | ``` 135 | $ ./server -c /etc/myrunes/config.yml 136 | ``` 137 | 138 | --- 139 | 140 | © 2019-20 Ringo Hoffmann (zekro Development) 141 | Covered by the MIT Licence. -------------------------------------------------------------------------------- /ci/docker-deploy.sh: -------------------------------------------------------------------------------- 1 | BRANCH=${TRAVIS_BRANCH} 2 | RELEASE=TRUE 3 | 4 | if [ "${BRANCH}" == "master" ]; then 5 | BRANCH=latest 6 | fi 7 | 8 | if [ "${BRANCH}" == "dev" ]; then 9 | BRANCH=canary 10 | RELEASE=FALSE 11 | fi 12 | 13 | docker build . -t zekro/myrunes:${BRANCH} --build-arg RELEASE=${RELEASE} 14 | docker login -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD} 15 | docker push zekro/myrunes:${BRANCH} -------------------------------------------------------------------------------- /ci/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ensuring backend server dependencies 4 | go mod tidy 5 | 6 | # building backend 7 | go build \ 8 | -v -o ./bin/myrunes -ldflags "\ 9 | -X github.com/myrunes/backend/internal/static.Release=TRUE" \ 10 | ./cmd/server/*.go 11 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "os" 7 | "os/signal" 8 | "strings" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/myrunes/backend/pkg/ddragon" 13 | "github.com/myrunes/backend/pkg/lifecycletimer" 14 | 15 | "github.com/myrunes/backend/internal/assets" 16 | "github.com/myrunes/backend/internal/caching" 17 | "github.com/myrunes/backend/internal/config" 18 | "github.com/myrunes/backend/internal/database" 19 | "github.com/myrunes/backend/internal/logger" 20 | "github.com/myrunes/backend/internal/mailserver" 21 | "github.com/myrunes/backend/internal/storage" 22 | "github.com/myrunes/backend/internal/webserver" 23 | ) 24 | 25 | var ( 26 | flagConfig = flag.String("c", "config.yml", "config file location") 27 | flagSkipFetch = flag.Bool("skipFetch", false, "skip avatar asset fetching") 28 | ) 29 | 30 | func initStorage(c *config.Main) (st storage.Middleware, err error) { 31 | var cfg interface{} 32 | 33 | switch c.Storage.Typ { 34 | case "file", "fs": 35 | st = new(storage.File) 36 | cfg = *c.Storage.File 37 | case "minio", "s3": 38 | st = new(storage.Minio) 39 | cfg = *c.Storage.Minio 40 | default: 41 | return nil, errors.New("invalid storage type") 42 | } 43 | 44 | if cfg == nil { 45 | return nil, errors.New("invalid storage config") 46 | } 47 | 48 | err = st.Init(cfg) 49 | 50 | return 51 | } 52 | 53 | func fetchAssets(a *assets.AvatarHandler) error { 54 | if *flagSkipFetch { 55 | return nil 56 | } 57 | 58 | cChamps := make(chan string) 59 | cError := make(chan error) 60 | 61 | go a.FetchAll(cChamps, cError) 62 | 63 | go func() { 64 | for _, c := range ddragon.DDragonInstance.Champions { 65 | cChamps <- c.UID 66 | } 67 | close(cChamps) 68 | }() 69 | 70 | for err := range cError { 71 | if err != nil { 72 | return err 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func refetch(a *assets.AvatarHandler) { 80 | var err error 81 | 82 | logger.Info("DDRAGON :: refetch") 83 | if ddragon.DDragonInstance, err = ddragon.Fetch("latest"); err != nil { 84 | logger.Error("DDRAGON :: failed polling data from ddragon: %s", err.Error()) 85 | } 86 | 87 | logger.Info("ASSETHANDLER :: refetch") 88 | if err = fetchAssets(a); err != nil { 89 | logger.Fatal("ASSETHANDLER :: failed fetching assets: %s", err.Error()) 90 | } 91 | } 92 | 93 | func cleanupExpiredRefreshTokens(db database.Middleware) { 94 | n, err := db.CleanupExpiredTokens() 95 | if err != nil { 96 | logger.Error("DATABASE :: failed cleaning up expired refresh tokens: %s", err.Error()) 97 | } else { 98 | logger.Info("AUTH :: cleaned %d expired refresh tokens", n) 99 | } 100 | } 101 | 102 | func main() { 103 | flag.Parse() 104 | 105 | logger.Setup(`%{color}▶ %{level:.4s} %{id:03d}%{color:reset} %{message}`, 5) 106 | 107 | logger.Info("CONFIG :: initialization") 108 | cfg, err := config.Open(*flagConfig) 109 | if err != nil { 110 | logger.Fatal("CONFIG :: failed creating or opening config: %s", err.Error()) 111 | } 112 | if cfg == nil { 113 | logger.Info("CONFIG :: config file was created at '%s'. Set your config values and restart.", *flagConfig) 114 | return 115 | } 116 | 117 | if v := os.Getenv("DB_HOST"); v != "" { 118 | cfg.MongoDB.Host = v 119 | } 120 | if v := os.Getenv("DB_PORT"); v != "" { 121 | cfg.MongoDB.Port = v 122 | } 123 | if v := os.Getenv("DB_USERNAME"); v != "" { 124 | cfg.MongoDB.Username = v 125 | } 126 | if v := os.Getenv("DB_PASSWORD"); v != "" { 127 | cfg.MongoDB.Password = v 128 | } 129 | if v := os.Getenv("DB_AUTHDB"); v != "" { 130 | cfg.MongoDB.AuthDB = v 131 | } 132 | if v := os.Getenv("DB_DATADB"); v != "" { 133 | cfg.MongoDB.DataDB = v 134 | } 135 | if v := strings.ToLower(os.Getenv("TLS_ENABLE")); v == "true" || v == "t" || v == "1" { 136 | cfg.WebServer.TLS.Enabled = true 137 | } 138 | if v := os.Getenv("TLS_KEY"); v != "" { 139 | cfg.WebServer.TLS.Key = v 140 | } 141 | if v := os.Getenv("TLS_CERT"); v != "" { 142 | cfg.WebServer.TLS.Cert = v 143 | } 144 | 145 | logger.Info("DDRAGON :: initialization") 146 | if ddragon.DDragonInstance, err = ddragon.Fetch("latest"); err != nil { 147 | logger.Fatal("DDRAGON :: failed polling data from ddragon: %s", err.Error()) 148 | } 149 | logger.Info("DDRAGON :: initialized") 150 | 151 | db := new(database.MongoDB) 152 | logger.Info("DATABASE :: initialization") 153 | if err = db.Connect(cfg.MongoDB); err != nil { 154 | logger.Fatal("DATABASE :: failed establishing connection to database: %s", err.Error()) 155 | } 156 | defer func() { 157 | logger.Info("DATABASE :: teardown") 158 | db.Close() 159 | }() 160 | 161 | logger.Info("STORAGE :: initialization") 162 | st, err := initStorage(cfg) 163 | if err != nil { 164 | logger.Fatal("STORAGE :: failed initializing storage: %s", err.Error()) 165 | } 166 | 167 | logger.Info("ASSETHANDLER :: initialization") 168 | avatarAssetsHandler := assets.NewAvatarHandler(st) 169 | if err = fetchAssets(avatarAssetsHandler); err != nil { 170 | logger.Fatal("ASSETHANDLER :: failed fetching assets: %s", err.Error()) 171 | } 172 | 173 | var ms *mailserver.MailServer 174 | if cfg.MailServer != nil { 175 | logger.Info("MAILSERVER :: initialization") 176 | ms, err = mailserver.NewMailServer(cfg.MailServer, "noreply@myrunes.com", "myrunes") 177 | if err != nil { 178 | logger.Fatal("MAILSERVER :: failed connecting to mail account: %s", err.Error()) 179 | } 180 | logger.Info("MAILSERVER :: started") 181 | } else { 182 | logger.Warning("MAILSERVER :: mail server is disabled due to missing configuration") 183 | } 184 | 185 | var cache caching.CacheMiddleware 186 | if cfg.Redis != nil && cfg.Redis.Enabled { 187 | cache = caching.NewRedis(cfg.Redis) 188 | } else { 189 | cache = caching.NewInternal() 190 | } 191 | cache.SetDatabase(db) 192 | 193 | logger.Info("WEBSERVER :: initialization") 194 | ws, err := webserver.NewWebServer(db, cache, ms, avatarAssetsHandler, cfg.WebServer) 195 | if err != nil { 196 | logger.Fatal("WEBSERVER :: failed creating web server: %s", err.Error()) 197 | } 198 | go func() { 199 | if err := ws.ListenAndServeBlocking(); err != nil { 200 | logger.Fatal("WEBSERVER :: failed starting web server: %s", err.Error()) 201 | } 202 | }() 203 | logger.Info("WEBSERVER :: started") 204 | 205 | lct := lifecycletimer.New(24 * time.Hour). 206 | Handle(func() { refetch(avatarAssetsHandler) }). 207 | Handle(func() { cleanupExpiredRefreshTokens(db) }). 208 | Start() 209 | defer lct.Stop() 210 | logger.Info("LIFECYCLETIMER :: started") 211 | 212 | sc := make(chan os.Signal, 1) 213 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) 214 | <-sc 215 | } 216 | -------------------------------------------------------------------------------- /cmd/testing/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/myrunes/backend/pkg/ddragon" 7 | ) 8 | 9 | func main() { 10 | d, _ := ddragon.Fetch("latest") 11 | fmt.Printf(d.Runes[0].Slots[1].Runes[1].UID) 12 | } 13 | -------------------------------------------------------------------------------- /config/example.config.yml: -------------------------------------------------------------------------------- 1 | # MYRUNES example configuration. 2 | # All values given are default values 3 | # which are automatically set on creation. 4 | 5 | # MongoDB config 6 | mongodb: 7 | # Authorization database name 8 | auth_db: lol-runes 9 | # Data database name 10 | data_db: lol-runes 11 | # Host address of the database server 12 | host: localhost 13 | # Port of the database server 14 | # Default MongoDB port is 27017 15 | port: "27017" 16 | # Username to be authenticated against 17 | # authorization database 18 | username: lol-runes 19 | # Password to be used for authentication 20 | password: "" 21 | 22 | # Redis config 23 | redis: 24 | # Enable or disable redis caching 25 | enabled: false 26 | # Address and port of the redis server 27 | addr: localhost:6379 28 | # Database to be selected 29 | db: 0 30 | # Password of the redis server 31 | password: "" 32 | 33 | # Webserver config 34 | webserver: 35 | # Address of the web server 36 | # Defaultly hostname:443 37 | addr: localhost:443 38 | # Enable attachment of CORS headers. 39 | # to allow cross origin requests to 40 | # this API. 41 | enablecors: false 42 | # The JWT secret key to be used to 43 | # sign JWTs. If this is unset, a random 44 | # key will be generated on each startup. 45 | jwtkey: "" 46 | # The path prefix to the API 47 | # For example, if this is set to '/api', 48 | # then requests will be grouped as 49 | # localhost:443/api/... 50 | pathprefix: "" 51 | # The public URL this API is available 52 | # online. This is important for generating 53 | # confirmation URLs which are sent via 54 | # Email on registration and password reset. 55 | publicaddress: https://myrunes.com 56 | # TLS/SSL config 57 | tls: 58 | # Enabel or disable TLS 59 | enabled: true 60 | # TLS certificate PEM file 61 | certfile: "/etc/cert/cert.pem" 62 | # TLS key PEM file 63 | keyfile: "/etc/cert/key.pem" 64 | 65 | # Mail server config 66 | mailserver: 67 | # SMTP address of the mail server 68 | host: "smtp.example.com" 69 | # SMTP port 70 | # Default is 465 71 | port: 465 72 | # Login username 73 | username: "" 74 | # Login password 75 | password: "" -------------------------------------------------------------------------------- /config/insomnia/insomnia-export.json: -------------------------------------------------------------------------------- 1 | {"_type":"export","__export_format":4,"__export_date":"2019-07-26T07:16:58.875Z","__export_source":"insomnia.desktop.app:v6.5.4","resources":[{"_id":"req_281f545266eb4826b3339eaa71d60e7e","authentication":{},"body":{},"created":1564124946148,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1564124946149,"method":"GET","modified":1564124970165,"name":"/apitoken","parameters":[],"parentId":"fld_d6f6b852d3c2443dbe91aae442f6abcc","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/apitoken","_type":"request"},{"_id":"fld_d6f6b852d3c2443dbe91aae442f6abcc","created":1564121803015,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1564121803015,"modified":1564121803015,"name":"apitoken","parentId":"wrk_64b23dd3cee54c9d89eee0f0019b3538","_type":"request_group"},{"_id":"wrk_64b23dd3cee54c9d89eee0f0019b3538","created":1559545217822,"description":"","modified":1559545217822,"name":"lol-runes","parentId":null,"_type":"workspace"},{"_id":"req_084a95592a444d6a8ebdd5444b5031e8","authentication":{},"body":{},"created":1564124983277,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1563346788762,"method":"POST","modified":1564125026787,"name":"/apitoken","parameters":[],"parentId":"fld_d6f6b852d3c2443dbe91aae442f6abcc","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/apitoken","_type":"request"},{"_id":"req_237e5bb2c39e47f1bfc156c978a4de42","authentication":{},"body":{},"created":1564125029562,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1562957710068.5,"method":"DELETE","modified":1564125038208,"name":"/apitoken","parameters":[],"parentId":"fld_d6f6b852d3c2443dbe91aae442f6abcc","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/apitoken","_type":"request"},{"_id":"req_cb2d58a2cb2840789520557521fbce95","authentication":{},"body":{},"created":1559549329281,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1560244118698.75,"method":"GET","modified":1560244130712,"name":"/api/pages/:UID","parameters":[],"parentId":"fld_70b8581dd7614241bdbf77bfbe8abbc8","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/pages/1135467880709390336","_type":"request"},{"_id":"fld_70b8581dd7614241bdbf77bfbe8abbc8","created":1560244122121,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1560244122121,"modified":1560244122121,"name":"pages","parentId":"wrk_64b23dd3cee54c9d89eee0f0019b3538","_type":"request_group"},{"_id":"req_06b69cce6fb34228987ae7bd36164759","authentication":{},"body":{},"created":1559548746085,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1560244118636.25,"method":"GET","modified":1560244140895,"name":"/api/pages","parameters":[],"parentId":"fld_70b8581dd7614241bdbf77bfbe8abbc8","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/pages","_type":"request"},{"_id":"req_5a06979bc4ae4f5e9ddf18a901eed432","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"title\": \"testpage\",\n\t\"champions\": [\"kindred\"],\n\t\"primary\": {\n\t\t\"tree\": \"domination\",\n\t\t\"rows\": [\n\t\t\t\"electrocute\",\n\t\t\t\"cheap-shot\",\n\t\t\t\"zombie-ward\",\n\t\t\t\"ravenous-hunter\"\n\t\t]\n\t},\n\t\"secondary\": {\n\t\t\"tree\": \"inspiration\",\n\t\t\"rows\": [\n\t\t\t\"hextech-flashtraption\",\n\t\t\t\"futures-market\",\n\t\t\t\"cosmic-insight\"\n\t\t]\n\t},\n\t\"perks\": {\n\t\t\"rows\": [\n\t\t\t\"diamond\",\n\t\t\t\"shield\",\n\t\t\t\"heart\"\n\t\t]\n\t}\n}"},"created":1559545706833,"description":"","headers":[{"id":"pair_31a90383ec1b4d3c92a32e80f9163f19","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1560244118623.75,"method":"POST","modified":1560244138339,"name":"/api/pages","parameters":[],"parentId":"fld_70b8581dd7614241bdbf77bfbe8abbc8","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/pages","_type":"request"},{"_id":"req_1b7643813d284a58a98eb506ddce79b8","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"title\": \"testpage\",\n\t\"champions\": [\"kindred\", \"aatrox\"],\n\t\"primary\": {\n\t\t\"tree\": \"domination\",\n\t\t\"rows\": [\n\t\t\t\"electrocute\",\n\t\t\t\"cheap-shot\",\n\t\t\t\"zombie-ward\",\n\t\t\t\"ingenious-hunter\"\n\t\t]\n\t},\n\t\"secondary\": {\n\t\t\"tree\": \"inspiration\",\n\t\t\"rows\": [\n\t\t\t\"hextech-flashtraption\",\n\t\t\t\"futures-market\",\n\t\t\t\"cosmic-insight\"\n\t\t]\n\t},\n\t\"perks\": {\n\t\t\"rows\": [\n\t\t\t\"diamond\",\n\t\t\t\"shield\",\n\t\t\t\"heart\"\n\t\t]\n\t}\n}"},"created":1559550618066,"description":"","headers":[{"id":"pair_88b43b0295dd4a84acb43ae2b4d6e11c","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1560244118598.75,"method":"POST","modified":1560244135182,"name":"/api/pages/:UID","parameters":[],"parentId":"fld_70b8581dd7614241bdbf77bfbe8abbc8","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/pages/1135467880709390336","_type":"request"},{"_id":"req_927f30581b15454aa05e84e3b45348f4","authentication":{},"body":{},"created":1559551465289,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1560244118548.75,"method":"DELETE","modified":1560244146247,"name":"/api/pages/:UID","parameters":[],"parentId":"fld_70b8581dd7614241bdbf77bfbe8abbc8","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/pages/1135464485877481472","_type":"request"},{"_id":"req_5705da5dc02e49d39d5d08922a7364fb","authentication":{},"body":{},"created":1559551153146,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1559551176271,"method":"GET","modified":1560244114308,"name":"/api/resources/champions","parameters":[],"parentId":"fld_982cd250ac3840f9b1202c2f43fbfa42","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/resources/champions","_type":"request"},{"_id":"fld_982cd250ac3840f9b1202c2f43fbfa42","created":1560244108432,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1560244108432,"modified":1560244108432,"name":"resources","parentId":"wrk_64b23dd3cee54c9d89eee0f0019b3538","_type":"request_group"},{"_id":"req_36e7b13b319840f3a208c4cc7aafd41c","authentication":{},"body":{},"created":1559551176221,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1559551176221,"method":"GET","modified":1560244113019,"name":"/api/resources/runes","parameters":[],"parentId":"fld_982cd250ac3840f9b1202c2f43fbfa42","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/resources/runes","_type":"request"},{"_id":"req_cf18b746fd5442fdb0e6bf117f01c7ce","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"username\": \"ahsdjkhasjdk\",\n\t\"password\": \"asdasdasd\"\n}"},"created":1559718502979,"description":"","headers":[{"id":"pair_2cb4edc87b4747a0bfa8418828728e2b","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1560244072761.5,"method":"POST","modified":1560244089422,"name":"/api/users","parameters":[],"parentId":"fld_2287ce9bd3d94fa5b2219a5c8ad9b75d","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/users","_type":"request"},{"_id":"fld_2287ce9bd3d94fa5b2219a5c8ad9b75d","created":1560244084564,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1560244084564,"modified":1560244084564,"name":"users","parentId":"wrk_64b23dd3cee54c9d89eee0f0019b3538","_type":"request_group"},{"_id":"req_14bde6a010ae4fb49a935a5010f36b13","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"currpassword\": \"1234\",\n\t\"newpassword\": \"test\"\n}"},"created":1559545646384,"description":"","headers":[{"id":"pair_2cb4edc87b4747a0bfa8418828728e2b","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1560244072711.5,"method":"POST","modified":1560244091779,"name":"/api/users/me","parameters":[],"parentId":"fld_2287ce9bd3d94fa5b2219a5c8ad9b75d","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/users/me","_type":"request"},{"_id":"req_a963541f680b4cb9a7cdb7b3e1934f24","authentication":{},"body":{},"created":1559551922276,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1560244072661.5,"method":"GET","modified":1564125385758,"name":"/api/users/me","parameters":[],"parentId":"fld_2287ce9bd3d94fa5b2219a5c8ad9b75d","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/users/me","_type":"request"},{"_id":"req_6ca3ae01bf3647029130d5bf3fa7b52a","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"currpassword\": \"test\"\n}"},"created":1559719207122,"description":"","headers":[{"id":"pair_2cb4edc87b4747a0bfa8418828728e2b","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1560244072611.5,"method":"DELETE","modified":1560244096519,"name":"/api/users/me","parameters":[],"parentId":"fld_2287ce9bd3d94fa5b2219a5c8ad9b75d","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/users/me","_type":"request"},{"_id":"req_71398ec5c02b48259766902c0c74d35d","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"page\": 1136539895017013248\n}"},"created":1560242917279,"description":"","headers":[{"id":"pair_678ac2b797f34814a117823fd934c04b","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1560070553854.5,"method":"POST","modified":1560252758236,"name":"/api/shares","parameters":[],"parentId":"fld_6ddbe375dcd844ebbb36337f62fc057c","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/shares","_type":"request"},{"_id":"fld_6ddbe375dcd844ebbb36337f62fc057c","created":1560244060958,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1560244060959,"modified":1560244079245,"name":"shares","parentId":"wrk_64b23dd3cee54c9d89eee0f0019b3538","_type":"request_group"},{"_id":"req_02963fcf054c498f9db1dd4b84cac30e","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"maxaccesses\": 4\n}"},"created":1560255234783,"description":"","headers":[{"id":"pair_678ac2b797f34814a117823fd934c04b","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1560070553829.5,"method":"POST","modified":1562052216117,"name":"/api/shares/:ID","parameters":[],"parentId":"fld_6ddbe375dcd844ebbb36337f62fc057c","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/shares/z9qrkRQ=","_type":"request"},{"_id":"req_1237615a1b414e808ac2ad722feb16df","authentication":{},"body":{},"created":1560243246715,"description":"","headers":[{"disabled":true,"id":"pair_313b571fe0b44b20a3ca24db8cd26a6e","name":"User-Agent","value":"Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)"}],"isPrivate":false,"metaSortKey":-1560070553804.5,"method":"GET","modified":1562052472304,"name":"/api/shares/:IDENT","parameters":[],"parentId":"fld_6ddbe375dcd844ebbb36337f62fc057c","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/shares/z9qrkRQ=","_type":"request"},{"_id":"req_dfef87d7276c48ae92b77e46285ec373","authentication":{},"body":{},"created":1560244183760,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1559810865037.75,"method":"DELETE","modified":1560252787354,"name":"/api/shares/:IDENT","parameters":[],"parentId":"fld_6ddbe375dcd844ebbb36337f62fc057c","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/shares/1138408704213319680","_type":"request"},{"_id":"req_e55da9b38cd64e0a86040a209ca1fe36","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"username\": \"tester\",\n\t\"password\": \"123123123\",\n\t\"remember\": true\n}"},"created":1559545234627,"description":"","headers":[{"id":"pair_ed2c1b28125b43609f784c332707711b","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1559545234627,"method":"POST","modified":1564125080716,"name":"/api/login","parameters":[],"parentId":"wrk_64b23dd3cee54c9d89eee0f0019b3538","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/login","_type":"request"},{"_id":"req_2512899d7a64494da45d2b6feb2c71eb","authentication":{},"body":{},"created":1559568577163,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1558194733146,"method":"POST","modified":1559568598010,"name":"/api/logout","parameters":[],"parentId":"wrk_64b23dd3cee54c9d89eee0f0019b3538","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"http://localhost:8080/api/logout","_type":"request"},{"_id":"env_1de1dbd4ac57f9b53b767ec5dd703ebb4739b2b8","color":null,"created":1559545218535,"data":{},"dataPropertyOrder":null,"isPrivate":false,"metaSortKey":1559545218536,"modified":1559545218535,"name":"Base Environment","parentId":"wrk_64b23dd3cee54c9d89eee0f0019b3538","_type":"environment"},{"_id":"jar_1de1dbd4ac57f9b53b767ec5dd703ebb4739b2b8","cookies":[],"created":1559545218565,"modified":1564125261936,"name":"Default Jar","parentId":"wrk_64b23dd3cee54c9d89eee0f0019b3538","_type":"cookie_jar"}]} -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # This docker-compose config is only for development! 2 | 3 | version: '3.7' 4 | 5 | volumes: 6 | mongo: 7 | 8 | services: 9 | mongodb: 10 | image: 'mongo:latest' 11 | ports: 12 | - '27017:27017' 13 | volumes: 14 | - 'mongo:/data/db' 15 | command: --auth 16 | environment: 17 | MONGO_INITDB_ROOT_USERNAME: lol-runes 18 | MONGO_INITDB_ROOT_PASSWORD: lol-runes 19 | restart: always -------------------------------------------------------------------------------- /docs/cookie-usage.md: -------------------------------------------------------------------------------- 1 | # Cookie Usage 2 | 3 | ## What is a cookie? 4 | 5 | Here, a short definition from [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies): 6 | > An HTTP cookie (web cookie, browser cookie) is a small piece of data that a server sends to the user's web browser. The browser may store it and send it back with the next request to the same server. Typically, it's used to tell if two requests came from the same browser — keeping a user logged-in, for example. [...] 7 | 8 | ## How does MYRUNES use cookies? 9 | 10 | We are using cookies to store a session key in your browser which authorizes your requests and authenticates your account. 11 | *In more detail, the session key is a 128 bit base64 encoded string which will be matched against the database to check if there is any user linked to this session.* 12 | 13 | ## How are local settings saved? 14 | 15 | For saving local settings like the acception of the cookie information, we are using [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). This is not a very secure way of saving private information, but it is verry efficient for local user settings which are not nessecary to be protected like a session key or user ID's. 16 | 17 | ## Deleting cookies or local storage preferences? 18 | 19 | [This](https://support.grammarly.com/hc/en-us/articles/115000090272-How-do-I-clear-cache-and-cookies-) is a very useful guide from gramarly to delete cookies and local data. Just adapt these steps for `myrunes.com`. ;) -------------------------------------------------------------------------------- /docs/restapi-docs.md: -------------------------------------------------------------------------------- 1 | # REST API 2 | 3 | The MYRUNES backend provides a RESTful HTTP JSON API providing 100% of the functionalities of MYRUNES. 4 | 5 | ## Index 6 | 7 | - [**Authenticate**](#authenticate) 8 | - [**Body Content Type**](#body-content-type) 9 | - [**Request Parameters**](#request-parameters) 10 | - [**Error Responses**](#error-responses) 11 | - [**Rate Limiting**](#rate-limiting) 12 | - [**API Objects**](#api-objects) 13 | - [User Object](#user-object) 14 | - [Page Object](#page-object) 15 | - [Share Object](#share-object) 16 | - [Session Object](#session-object) 17 | - [API Token Object](#api-token-object) 18 | - [**Resources**](#resources) 19 | - [Champions](#champions) 20 | - [Runes and Perks](#runes-and-perks) 21 | - [**Information**](#information) 22 | - [Version](#version) 23 | - [ReCAPTCHA](#recaptcha) 24 | - [**Endpoints**](#endpoints) 25 | - [Users](#users) 26 | - [Get Self User](#get-self-user) 27 | - [Check User Name](#check-user-name) 28 | - [Create User](#create-user) 29 | - [Update Self User](#update-self-user) 30 | - [Delete Self User](#delete-self-user) 31 | - [Pages](#pages) 32 | - [Shares](#shares) 33 | - [Sessions](#sessions) 34 | - [API Token](#api-token) 35 | 36 | ## Authenticate 37 | 38 | There are two ways to authenticate against the API: 39 | 40 | - **API Tokens** 41 | In the `MY SETTINGS` page of MYRUNES, you can generate an access token which is a 64 character base64 string used to authenticate against the API. 42 | You must pas this token on **each request** as **`Basic`** type token in the **`Authorization`** header. Example: 43 | ``` 44 | Authorization: Basic 5lTGAsTFwCKG... 45 | ``` 46 | 47 | - **JWT Session Cookies** 48 | This method generates a JWT which must then be stored as cookie and delivered on each following request in the **`Cookie`** header: 49 | ``` 50 | Cookie: jwt_token=eyJhbGciOiJIUzI1NiIs... 51 | ``` 52 | To get a JWT, request the **[login](#login)** endpoint passing username and password in the JSON body of the request and the server will respond with a **`Set-Cookie`** header containing the JWT after the `jwt_token` key. Keep in mind, that you must maintain the expiration of the Cookie because the session will eventually become invalid after a certain time and must be refreshed. Session are defaultly valid for 2 hours. This value can be extended to a maximum expire duration of 30 days when `remember` is set to `true` in the login request payload. 53 | 54 | 55 | ## Body Content Type 56 | 57 | All request bodies which are sent to the API or returned by the API are using content type **`application/json`**. 58 | 59 | 60 | ## Request Parameters 61 | 62 | Optional request parameters are styled *`like this`* and non-optionals are styled `like this` in the documentation. There are different types how parameters must be passed. 63 | 64 | - Either as `Path` variable as part of the request resource like `/api/pages/`**`87128927213891273`** or 65 | - as `URL Query` like `/api/pages`**`?sort_by=date`** or 66 | - as `Body` parameter like 67 | ```json 68 | { 69 | "username": "zekro" 70 | } 71 | ``` 72 | 73 | ## Error Responses 74 | 75 | The API uses the standard HTTP/1.1 status codes like defined in [RFC 2616 - section 10](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html). 76 | 77 | Also, every error response contains a body with the status `code` and an error `message` as description of the error. 78 | 79 | ```json 80 | { 81 | "code": 429, 82 | "message": "You are being rate limited" 83 | } 84 | ``` 85 | 86 | ## Rate Limiting 87 | 88 | The API is rate limited by a per-connection and per-endpoint [token bucket](https://en.wikipedia.org/wiki/Token_bucket) limiter system. Also, there is a global limiter across all endpoints per-connection. 89 | 90 | For each endpoint, you will have a maximum ammount of tokens you can use for requests. Each request, one token will be consumed. Each time a specified ammount of time elapses, a new token will be added to your bucket. 91 | 92 | You can check your current rate limit status by examining the passed headers 93 | - `X-Ratelimit-Limit` 94 | which displays the total ammount of maximum token you can have, 95 | - `X-Ratelimit-Remaining` 96 | which presents the ammount of tokens you can stil use, 97 | - `X-Ratelimit-Reset` 98 | gives the UNIX time stamp (seconds) when you are able to request again after consumption of all tokens 99 | 100 | --- 101 | 102 | ## API Objects 103 | 104 | There are different types of API objects which will be returned by the API. 105 | 106 | ### User Object 107 | 108 | > A MYRUNES registered user account. 109 | 110 | | Key | Type | Description | 111 | |-----|------|--------------| 112 | | `uid` | string | Unique user ID in form of a [snowflake](https://developer.twitter.com/en/docs/basics/twitter-ids.html) like object | 113 | | `username` | string | The unique user name of the user | 114 | | `displayname` | string | The display name of the user (may not be unique) | 115 | | `lastlogin` | string | Time of last successful login | 116 | | `created` | string | Time of account creation | 117 | | `favorites` | List\ | List of favorited champion IDs | 118 | 119 | ```json 120 | { 121 | "uid": "1154685560976457728", 122 | "username": "zekro", 123 | "displayname": "zekro der Echte", 124 | "lastlogin": "2019-07-26T09:31:05.62Z", 125 | "created": "2019-07-26T09:31:04.993Z", 126 | "favorites": [ 127 | "kindred", 128 | "pyke", 129 | "lux" 130 | ] 131 | } 132 | ``` 133 | 134 | ### Page Object 135 | 136 | > A rune page. 137 | 138 | Rune pages consists of following sub-objects. 139 | A full list featuring all available trees, runes and perks you can get py requesting the [`resources`](#resources) paths. 140 | 141 | **Primary Tree Object** 142 | 143 | > The primary tree of a rune page. 144 | 145 | ```json 146 | { 147 | "tree": "domination", 148 | "rows": [ 149 | "electrocute", 150 | "cheap-shot", 151 | "zombie-ward", 152 | "ravenous-hunter" 153 | ] 154 | } 155 | ``` 156 | 157 | **Secondary Tree Object** 158 | 159 | > The secondary tree of a rune page. 160 | 161 | ```json 162 | { 163 | "tree": "precision", 164 | "rows": [ 165 | "legend-bloodline", 166 | "cut-down" 167 | ] 168 | } 169 | ``` 170 | 171 | **Perks Object** 172 | 173 | > The perks collection of a rune page. 174 | 175 | ```json 176 | { 177 | "rows": [ 178 | "diamond", 179 | "diamond", 180 | "heart" 181 | ] 182 | } 183 | ``` 184 | 185 | The actual page object is built like follwing: 186 | 187 | | Key | Type | Description | 188 | |-----|------|--------------| 189 | | `uid` | string | Unique page ID in form of a [snowflake](https://developer.twitter.com/en/docs/basics/twitter-ids.html) like object | 190 | | `owner` | string | The user UID of the owner/creator of the page | 191 | | `title` | string | The title of the page | 192 | | `created` | string | The date of creation of the page | 193 | | `edited` | string | The date of the last modification of the page | 194 | | `champions` | List\ | List of champion IDs the page is linked to | 195 | | `primary` | Primary Tree Object | | 196 | | `secondary` | Secondary Tree Object | | 197 | | `perks` | Perks Object | | 198 | 199 | ```json 200 | { 201 | "uid": "1136539895017013248", 202 | "owner": "1136250237250584576", 203 | "title": "asdasd", 204 | "created": "2019-06-06T07:46:41.517Z", 205 | "edited": "2019-06-11T13:02:53.124Z", 206 | "champions": [ 207 | "lux" 208 | ], 209 | "primary": { Primary Page Object }, 210 | "secondary": { Secondary Page Object }, 211 | "perks": { Perks Object } 212 | } 213 | ``` 214 | 215 | ### Share Object 216 | 217 | > A representation of data of a shared rune page. 218 | 219 | | Key | Type | Description | 220 | |-----|------|--------------| 221 | | `uid` | string | Unique share ID in form of a [snowflake](https://developer.twitter.com/en/docs/basics/twitter-ids.html) like object | 222 | | `ident` | string | The unique random identifier in format of a 8 character long base64 string used to request the shared page and represent in the share link | 223 | | `owner` | string | The unique ID of the owner of the shared page | 224 | | `page` | string | The unique ID of the shared page | 225 | | `created` | string | Date of the creation of the share | 226 | | `maxaccesses` | number | Maximum ammount of accesses available | 227 | | `accesses` | number | Ammount of accesses until now | 228 | | `expires` | string | The date of expiration. This will alway be a valid parsable value even though expiration is not set, this will be a time very far in the future | 229 | | `lastaccess` | string | Date of the last access | 230 | 231 | ```json 232 | { 233 | "uid": "1145956056344100864", 234 | "ident": "z9qrkRQ=", 235 | "owner": "1136250237250584576", 236 | "page": "1136961585131847680", 237 | "created": "2019-07-02T07:23:09.323Z", 238 | "maxaccesses": 4, 239 | "expires": "2119-06-08T07:23:09.323Z", 240 | "accesses": 1, 241 | "lastaccess": "2019-07-02T07:26:52.875Z" 242 | } 243 | ``` 244 | 245 | ### Session Object 246 | 247 | > **ATTENTION: Sessions are deprecated since main version 1.7.** 248 | 249 | > Information around a session key bound to a user to authenticate their requests. 250 | 251 | | Key | Type | Description | 252 | |-----|------|--------------| 253 | | `sessionid` | string | Unique session ID in form of a [snowflake](https://developer.twitter.com/en/docs/basics/twitter-ids.html) like object | 254 | | `key` | string | A pseudo-representation of the session key showing the first and last 3 characters of the key | 255 | | `uid` | string | Unique ID of the user bound to this session | 256 | | `expires` | string | Date when the session key turns invalid | 257 | | `lastaccess` | string | Date of the last authentication using this session key | 258 | | `lastaccessip` | string | The remote address of the last authenticated request using this session kes | 259 | 260 | ```json 261 | { 262 | "sessionid": "1154652075600297984", 263 | "key": "f07...l8=", 264 | "uid": "1136250237250584576", 265 | "expires": "2019-08-25T07:18:01.879Z", 266 | "lastaccess": "2019-07-26T09:30:52.085Z", 267 | "lastaccessip": "127.0.0.1" 268 | } 269 | ``` 270 | 271 | ### API Token Object 272 | 273 | > Information around an API token bound to a user to authenticate their requests. 274 | 275 | | Key | Type | Description | 276 | |-----|------|--------------| 277 | | `userid` | string | The unique ID of the user bound to this token | 278 | | `token` | string | The API token secret | 279 | | `created` | string | Date the API token was generated | 280 | 281 | --- 282 | 283 | ## Resources 284 | 285 | ### Champions 286 | 287 | You can get a list of all featured champion IDs *(names - lowercased)* by requesting following endpoint *(does not require authentication)*: 288 | 289 | ``` 290 | GET /api/resources/champions 291 | ``` 292 | 293 | The response will look like following: 294 | 295 | ```json 296 | { 297 | "n": 144, 298 | "data": [ 299 | "aatrox", 300 | "ahri", 301 | "akali", 302 | ... 303 | ] 304 | } 305 | ``` 306 | 307 | ### Runes and Perks 308 | 309 | You can get currently featured sets of runes and perks by requesting following endpoint: 310 | 311 | ``` 312 | GET /api/resources/runes 313 | ``` 314 | 315 | The response will contain nested multidimensional arrays of runes available for each row of the respective tree. 316 | 317 | ```json 318 | { 319 | "perks": [ 320 | [ "diamond", ... ], 321 | [ "diamond", ... ], 322 | [ "heart", ... ] 323 | ], 324 | "primary": { 325 | "domination": [ 326 | [ "electrocute", ... ], 327 | [ "cheap-shot", ... ], 328 | [ "zombie-ward", ... ], 329 | [ "ravenous-hunter", ... ] 330 | ], 331 | ... 332 | }, 333 | "secondary": { 334 | "domination": [ ... ], 335 | ... 336 | }, 337 | "trees": [ 338 | "precision", 339 | "domination", 340 | "sorcery", 341 | "resolve", 342 | "inspiration" 343 | ] 344 | } 345 | ``` 346 | 347 | --- 348 | 349 | ## Information 350 | 351 | ### Version 352 | 353 | > `GET /api/version` 354 | 355 | **Response** 356 | 357 | ``` 358 | HTTP/1.1 200 OK 359 | Cache-Control: max-age=2592000; must-revalidate; proxy-revalidate; public 360 | Content-Type: application/json 361 | Date: Sun, 20 Sep 2020 08:02:45 GMT 362 | Etag: W/"3d17185be51edc954586c4feff03cf31c5d278e6" 363 | Server: MYRUNES v.1.7.1+26 364 | X-Ratelimit-limit: 50 365 | X-Ratelimit-remaining: 49 366 | X-Ratelimit-reset: 0 367 | Content-Length: 60 368 | ``` 369 | ```json 370 | { 371 | "apiversion":"1.8.0", 372 | "release":"TRUE", 373 | "version":"1.7.1+26" 374 | } 375 | ``` 376 | 377 | ### Recaptcha 378 | 379 | > `GET /api/recaptchainfo` 380 | 381 | **Response** 382 | 383 | ``` 384 | HTTP/1.1 200 OK 385 | Cache-Control: max-age=2592000; must-revalidate; proxy-revalidate; public 386 | Content-Type: application/json 387 | Date: Sun, 20 Sep 2020 08:02:45 GMT 388 | Etag: W/"0f3279816a04027124c7d6eaca3b967e15c2d166" 389 | Server: MYRUNES v.1.7.1+26 390 | X-Ratelimit-limit: 50 391 | X-Ratelimit-remaining: 49 392 | X-Ratelimit-reset: 0 393 | Content-Length: 54 394 | ``` 395 | ```json 396 | { 397 | "sitekey":"6Le6IM4ZAAAAAL8iQ0akcye5Sw4I5JbBqRMyD0J8" 398 | } 399 | ``` 400 | 401 | --- 402 | 403 | ## Endpoints 404 | 405 | ### Authentication 406 | 407 | #### Login 408 | 409 | > `POST /api/login` 410 | 411 | **Parameters** 412 | 413 | | Name | Type | Via | Default | Description | 414 | |------|------|-----|---------|-------------| 415 | | `username` | string | Body | | The username of the account | 416 | | `password` | string | Body | | The password of the given user | 417 | | *`remember`* | boolean | Body | `false` | Sessions defaultly expire after 2 hours. Setting this to true, this duration will be expanded to 30 days. | 418 | 419 | **Response** 420 | 421 | ``` 422 | HTTP/1.1 200 OK 423 | Content-Length: 36 424 | Content-Type: application/json 425 | Date: Fri, 26 Jul 2019 11:04:02 GMT 426 | Server: MYRUNES v.DEBUG_BUILD 427 | X-Ratelimit-Limit: 50 428 | X-Ratelimit-Remaining: 48 429 | X-Ratelimit-Reset: 0 430 | ``` 431 | ```json 432 | { 433 | "code": 200, 434 | "message": "ok" 435 | } 436 | ``` 437 | 438 | #### Logout 439 | 440 | > `POST /api/logout` 441 | 442 | **Parameters** 443 | 444 | *No parameters necessary.* 445 | 446 | **Response** 447 | 448 | ``` 449 | HTTP/1.1 200 OK 450 | Content-Length: 36 451 | Content-Type: application/json 452 | Date: Fri, 26 Jul 2019 11:06:43 GMT 453 | Server: MYRUNES v.DEBUG_BUILD 454 | X-Ratelimit-Limit: 50 455 | X-Ratelimit-Remaining: 49 456 | X-Ratelimit-Reset: 0 457 | ``` 458 | ```json 459 | { 460 | "code": 200, 461 | "message": "ok" 462 | } 463 | ``` 464 | 465 | ### Users 466 | 467 | #### Get Self User 468 | 469 | > `GET /api/users/me` 470 | 471 | **Parameters** 472 | 473 | *No parameters necessary.* 474 | 475 | **Response** 476 | 477 | ``` 478 | HTTP/1.1 200 OK 479 | Content-Length: 199 480 | Content-Type: application/json 481 | Date: Fri, 26 Jul 2019 11:21:07 GMT 482 | Server: MYRUNES v.DEBUG_BUILD 483 | X-Ratelimit-Limit: 50 484 | X-Ratelimit-Remaining: 49 485 | X-Ratelimit-Reset: 0 486 | ``` 487 | ```json 488 | { User Object } 489 | ``` 490 | 491 | #### Check User Name 492 | 493 | > `GET /api/users/:USERNAME` 494 | 495 | *This endpoint is concipated for checking the availability of a username on registration, not to gather user information from another account which is not possible yet over the API.* 496 | *If the given username is unused, a 404 Not Found response will be returned which then should be interpreted as success or available.* 497 | 498 | **Parameters** 499 | 500 | | Name | Type | Via | Default | Description | 501 | |------|------|-----|---------|-------------| 502 | | `USERNAME` | string | Path | | The username to be checked | 503 | 504 | **Response** 505 | 506 | ``` 507 | HTTP/1.1 200 OK 508 | Content-Length: 36 509 | Content-Type: application/json 510 | Date: Fri, 26 Jul 2019 11:08:07 GMT 511 | Server: MYRUNES v.DEBUG_BUILD 512 | X-Ratelimit-Limit: 50 513 | X-Ratelimit-Remaining: 49 514 | X-Ratelimit-Reset: 0 515 | ``` 516 | ```json 517 | { 518 | "code": 200, 519 | "message": "ok" 520 | } 521 | ``` 522 | 523 | #### Create User 524 | 525 | > `POST /api/users` 526 | 527 | **Parameters** 528 | 529 | | Name | Type | Via | Default | Description | 530 | |------|------|-----|---------|-------------| 531 | | `username` | string | Body | | The username of the account | 532 | | `password` | string | Body | | The password of the given user | 533 | | `recaptcharesponse` | string | Body | | ReCAPTCHA verification response token. | 534 | | *`remember`* | boolean | Body | `false` | Sessions defaultly expire after 2 hours. Setting this to true, this duration will be expanded to 30 days. | 535 | 536 | **Response** 537 | 538 | ``` 539 | HTTP/1.1 201 Created 540 | Content-Length: 210 541 | Content-Type: application/json 542 | Date: Fri, 26 Jul 2019 11:28:40 GMT 543 | Server: MYRUNES v.DEBUG_BUILD 544 | X-Ratelimit-Limit: 1 545 | X-Ratelimit-Remaining: 0 546 | X-Ratelimit-Reset: 1564140534 547 | ``` 548 | ```json 549 | { User Object } 550 | ``` 551 | 552 | #### Update Self User 553 | 554 | > `POST /api/users/me` 555 | 556 | **Parameters** 557 | 558 | | Name | Type | Via | Default | Description | 559 | |------|------|-----|---------|-------------| 560 | | `currpassword` | string | Body | | The current password of the users account | 561 | | *`newpassword`* | string | Body | | A new password which will replace the current one | 562 | | *`displayname`* | string | Body | | A new display name | 563 | | *`username`* | string | Body | | A new user name | 564 | 565 | **Response** 566 | 567 | ``` 568 | HTTP/1.1 200 OK 569 | Content-Length: 36 570 | Content-Type: application/json 571 | Date: Fri, 26 Jul 2019 11:34:22 GMT 572 | Server: MYRUNES v.DEBUG_BUILD 573 | X-Ratelimit-Limit: 50 574 | X-Ratelimit-Remaining: 48 575 | X-Ratelimit-Reset: 0 576 | ``` 577 | ```json 578 | { 579 | "code": 200, 580 | "message": "ok" 581 | } 582 | ``` 583 | 584 | #### Delete Self User 585 | 586 | > `DELETE /api/users/me` 587 | 588 | **Parameters** 589 | 590 | | Name | Type | Via | Default | Description | 591 | |------|------|-----|---------|-------------| 592 | | `currpassword` | string | Body | | The current password of the users account | 593 | 594 | **Response** 595 | 596 | ``` 597 | HTTP/1.1 200 OK 598 | Content-Length: 36 599 | Content-Type: application/json 600 | Date: Fri, 26 Jul 2019 11:39:15 GMT 601 | Server: MYRUNES v.DEBUG_BUILD 602 | X-Ratelimit-Limit: 50 603 | X-Ratelimit-Remaining: 48 604 | X-Ratelimit-Reset: 0 605 | ``` 606 | ```json 607 | { 608 | "code": 200, 609 | "message": "ok" 610 | } 611 | ``` 612 | 613 | ### Pages 614 | 615 | #### Get Pages 616 | 617 | > `GET /api/pages` 618 | 619 | **Parameters** 620 | 621 | *No parameters necessary.* 622 | 623 | **Response** 624 | 625 | ``` 626 | HTTP/1.1 200 OK 627 | Content-Length: 9692 628 | Content-Type: application/json 629 | Date: Fri, 26 Jul 2019 11:57:23 GMT 630 | Server: MYRUNES v.DEBUG_BUILD 631 | X-Ratelimit-Limit: 50 632 | X-Ratelimit-Remaining: 49 633 | X-Ratelimit-Reset: 0 634 | ``` 635 | ```json 636 | { 637 | "n": 14, 638 | "data": [ 639 | { Page Object }, 640 | { Page Object }, 641 | ... 642 | ] 643 | } 644 | ``` 645 | 646 | #### Get Page 647 | 648 | > `GET /api/pages/:PAGEID` 649 | 650 | *You can only request pages that you own or that are shared publically. If you request a page ID of an existing page not owned by you and not shared, you will get a 404 Not Found response.* 651 | 652 | **Parameters** 653 | 654 | | Name | Type | Via | Default | Description | 655 | |------|------|-----|---------|-------------| 656 | | `PAGEID` | string | Path | | The unique ID of the rune page | 657 | 658 | **Response** 659 | 660 | ``` 661 | HTTP/1.1 200 OK 662 | Content-Length: 560 663 | Content-Type: application/json 664 | Date: Fri, 26 Jul 2019 11:58:52 GMT 665 | Server: MYRUNES v.DEBUG_BUILD 666 | X-Ratelimit-Limit: 50 667 | X-Ratelimit-Remaining: 47 668 | X-Ratelimit-Reset: 0 669 | ``` 670 | ```json 671 | { Page Object } 672 | ``` 673 | 674 | #### Create Page 675 | 676 | > `POST /api/pages` 677 | 678 | **Parameters** 679 | 680 | The request body is a **Page Object** containing the desired values. if values for `uid`, `owner`, `created` or `edited` are passed, they will be ignored by the server. 681 | 682 | **Response** 683 | 684 | ``` 685 | HTTP/1.1 201 Created 686 | Content-Length: 567 687 | Content-Type: application/json 688 | Date: Fri, 26 Jul 2019 12:04:56 GMT 689 | Server: MYRUNES v.DEBUG_BUILD 690 | X-Ratelimit-Limit: 5 691 | X-Ratelimit-Remaining: 4 692 | X-Ratelimit-Reset: 0 693 | ``` 694 | ```json 695 | { Page Object } 696 | ``` 697 | 698 | #### Edit Page 699 | 700 | > `POST /api/pages/:PAGEID` 701 | 702 | *You can only edit pages that you own. If you try to update a page ID of an existing page not owned by you, you will get a 404 Not Found response.* 703 | 704 | **Parameters** 705 | 706 | | Name | Type | Via | Default | Description | 707 | |------|------|-----|---------|-------------| 708 | | `PAGEID` | string | Path | | The unique ID of the rune page | 709 | 710 | The request body is a **Page Object** containing the desired values. if values for `uid`, `owner`, `created` or `edited` are passed, they will be ignored by the server. 711 | 712 | **Response** 713 | 714 | ``` 715 | HTTP/1.1 200 OK 716 | Content-Length: 557 717 | Content-Type: application/json 718 | Date: Fri, 26 Jul 2019 12:08:09 GMT 719 | Server: MYRUNES v.DEBUG_BUILD 720 | X-Ratelimit-Limit: 50 721 | X-Ratelimit-Remaining: 48 722 | X-Ratelimit-Reset: 0 723 | ``` 724 | ```json 725 | { Page Object } 726 | ``` 727 | 728 | #### Delete Page 729 | 730 | > `DELETE /api/pages/:PAGEID` 731 | 732 | *You can only delete pages that you own. If you try to delete a page ID of an existing page not owned by you, you will get a 404 Not Found response.* 733 | 734 | **Parameters** 735 | 736 | | Name | Type | Via | Default | Description | 737 | |------|------|-----|---------|-------------| 738 | | `PAGEID` | string | Path | | The unique ID of the rune page | 739 | 740 | **Response** 741 | 742 | ``` 743 | HTTP/1.1 200 OK 744 | Content-Length: 36 745 | Content-Type: application/json 746 | Date: Fri, 26 Jul 2019 12:10:36 GMT 747 | Server: MYRUNES v.DEBUG_BUILD 748 | X-Ratelimit-Limit: 50 749 | X-Ratelimit-Remaining: 48 750 | X-Ratelimit-Reset: 0 751 | ``` 752 | ```json 753 | { 754 | "code": 200, 755 | "message": "ok" 756 | } 757 | ``` 758 | 759 | ### Shares 760 | 761 | #### Get Share 762 | 763 | > `GET /api/shares/:IDENT` 764 | 765 | **Parameters** 766 | 767 | | Name | Type | Via | Default | Description | 768 | |------|------|-----|---------|-------------| 769 | | `IDENT` | string | Path | | Either the shares identifier string, the unique ID of the original page or of the share | 770 | 771 | **Response** 772 | 773 | ``` 774 | HTTP/1.1 200 OK 775 | Content-Length: 1391 776 | Content-Type: application/json 777 | Date: Fri, 26 Jul 2019 12:32:33 GMT 778 | Server: MYRUNES v.DEBUG_BUILD 779 | X-Ratelimit-Limit: 50 780 | X-Ratelimit-Remaining: 48 781 | X-Ratelimit-Reset: 0 782 | ``` 783 | ```json 784 | { 785 | "share": { Share Object }, 786 | "page": { Page Object }, 787 | "user": { User Object } 788 | } 789 | ``` 790 | 791 | #### Create Share 792 | 793 | > `POST /api/shares` 794 | 795 | **Parameters** 796 | 797 | | Name | Type | Via | Default | Description | 798 | |------|------|-----|---------|-------------| 799 | | `page` | string | Body | | The UID of the page to be shared | 800 | | *`expires`* | string | Body | `none` (never) | The date the share will expire | 801 | | *`maxaccesses`* | number | Body | `-1` (no max accesses) | The ammount of maximum left accesses which will be decreased on each access. `0` means the share is no more accessable anymore and `-1` defines no access limit. | 802 | 803 | **Response** 804 | 805 | ``` 806 | HTTP/1.1 201 Created 807 | Content-Length: 312 808 | Content-Type: application/json 809 | Date: Fri, 26 Jul 2019 12:39:07 GMT 810 | Server: MYRUNES v.DEBUG_BUILD 811 | X-Ratelimit-Limit: 50 812 | X-Ratelimit-Remaining: 48 813 | X-Ratelimit-Reset: 0 814 | ``` 815 | ```json 816 | { Share Object } 817 | ``` 818 | 819 | #### Update Share 820 | 821 | > `POST /api/shares/:SHAREID` 822 | 823 | **Parameters** 824 | 825 | | Name | Type | Via | Default | Description | 826 | |------|------|-----|---------|-------------| 827 | | `SHAREID` | string | Path | | The UID of the share | 828 | | *`expires`* | string | Body | `none` (never) | The date the share will expire | 829 | | *`maxaccesses`* | number | Body | `-1` (no max accesses) | The ammount of maximum left accesses which will be decreased on each access. `0` means the share is no more accessable anymore and `-1` defines no access limit. | 830 | 831 | **Response** 832 | 833 | ``` 834 | HTTP/1.1 200 OK 835 | Content-Length: 283 836 | Content-Type: application/json 837 | Date: Fri, 26 Jul 2019 12:44:52 GMT 838 | Server: MYRUNES v.DEBUG_BUILD 839 | X-Ratelimit-Limit: 50 840 | X-Ratelimit-Remaining: 48 841 | X-Ratelimit-Reset: 0 842 | ``` 843 | ```json 844 | { Share Object } 845 | ``` 846 | 847 | #### Delete Share 848 | 849 | > `DELETE /api/shares/:SHAREID` 850 | 851 | **Parameters** 852 | 853 | | Name | Type | Via | Default | Description | 854 | |------|------|-----|---------|-------------| 855 | | `SHAREID` | string | Path | | The UID of the share | 856 | 857 | **Response** 858 | 859 | ``` 860 | HTTP/1.1 200 OK 861 | Content-Length: 36 862 | Content-Type: application/json 863 | Date: Fri, 26 Jul 2019 12:50:56 GMT 864 | Server: MYRUNES v.DEBUG_BUILD 865 | X-Ratelimit-Limit: 50 866 | X-Ratelimit-Remaining: 48 867 | X-Ratelimit-Reset: 0 868 | ``` 869 | ```json 870 | { 871 | "code": 200, 872 | "message": "ok" 873 | } 874 | ``` 875 | 876 | ### Sessions 877 | 878 | > **ATTENTION: Sessions are deprecated since main version 1.7. These endpoints are disabled and will be removed in following versions.** 879 | 880 | #### Get Sessions 881 | 882 | > `GET /api/sessions` 883 | 884 | Because this endpoint is deprecated, an `410 Gone` error will be returned on request: 885 | ```json 886 | { 887 | "code": 410, 888 | "message": "dreprecated" 889 | } 890 | ``` 891 | 892 | #### Delete Session 893 | 894 | > `DELETE /api/sessions/:SESSIONID` 895 | 896 | Because this endpoint is deprecated, an `410 Gone` error will be returned on request: 897 | ```json 898 | { 899 | "code": 410, 900 | "message": "dreprecated" 901 | } 902 | ``` 903 | 904 | ### API Token 905 | 906 | #### Get API Token 907 | 908 | > `GET /api/apitoken` 909 | 910 | **Parameters** 911 | 912 | *No parameters necessary.* 913 | 914 | **Response** 915 | 916 | ``` 917 | HTTP/1.1 200 OK 918 | Content-Length: 181 919 | Content-Type: application/json 920 | Date: Fri, 26 Jul 2019 13:43:21 GMT 921 | Server: MYRUNES v.DEBUG_BUILD 922 | X-Ratelimit-Limit: 50 923 | X-Ratelimit-Remaining: 49 924 | X-Ratelimit-Reset: 0 925 | ``` 926 | ```json 927 | { API Token Object } 928 | ``` 929 | 930 | #### Generate API Token 931 | 932 | > `POST /api/apitoken` 933 | 934 | **Parameters** 935 | 936 | *No parameters necessary.* 937 | 938 | **Response** 939 | 940 | ``` 941 | HTTP/1.1 200 OK 942 | Content-Length: 190 943 | Content-Type: application/json 944 | Date: Fri, 26 Jul 2019 13:44:51 GMT 945 | Server: MYRUNES v.DEBUG_BUILD 946 | X-Ratelimit-Limit: 50 947 | X-Ratelimit-Remaining: 49 948 | X-Ratelimit-Reset: 0 949 | ``` 950 | ```json 951 | { API Token Object } 952 | ``` 953 | 954 | #### Delete API Token 955 | 956 | > `DELETE /api/apitoken` 957 | 958 | **Parameters** 959 | 960 | *No parameters necessary.* 961 | 962 | **Response** 963 | 964 | ``` 965 | HTTP/1.1 200 OK 966 | Content-Length: 36 967 | Content-Type: application/json 968 | Date: Fri, 26 Jul 2019 13:45:38 GMT 969 | Server: MYRUNES v.DEBUG_BUILD 970 | X-Ratelimit-Limit: 50 971 | X-Ratelimit-Remaining: 49 972 | X-Ratelimit-Reset: 0 973 | ``` 974 | ```json 975 | { 976 | "code": 200, 977 | "message": "ok" 978 | } 979 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/myrunes/backend 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/alexedwards/argon2id v0.0.0-20200802152012-2464efd3196b 7 | github.com/aws/aws-sdk-go v1.34.27 // indirect 8 | github.com/bwmarrin/snowflake v0.3.0 9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 10 | github.com/ghodss/yaml v1.0.0 11 | github.com/go-ini/ini v1.61.0 // indirect 12 | github.com/go-ozzo/ozzo-routing v2.1.4+incompatible // indirect 13 | github.com/go-redis/redis v6.15.9+incompatible 14 | github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect 15 | github.com/jmespath/go-jmespath v0.4.0 // indirect 16 | github.com/klauspost/compress v1.11.0 // indirect 17 | github.com/minio/minio-go v6.0.14+incompatible 18 | github.com/mitchellh/go-homedir v1.1.0 // indirect 19 | github.com/onsi/ginkgo v1.15.1 // indirect 20 | github.com/onsi/gomega v1.11.0 // indirect 21 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 22 | github.com/qiangxue/fasthttp-routing v0.0.0-20160225050629-6ccdc2a18d87 23 | github.com/smartystreets/goconvey v1.6.4 // indirect 24 | github.com/valyala/fasthttp v1.16.0 25 | github.com/zekroTJA/ratelimit v0.0.0-20190321090824-219ca33049a5 26 | github.com/zekroTJA/timedmap v1.3.1 27 | go.mongodb.org/mongo-driver v1.4.1 28 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a 29 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 30 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 31 | gopkg.in/ini.v1 v1.62.0 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/alexedwards/argon2id v0.0.0-20200802152012-2464efd3196b h1:rcCpjI1OMGtBY8nnBvExeM1pXNoaM35zqmXBGpgJR2o= 4 | github.com/alexedwards/argon2id v0.0.0-20200802152012-2464efd3196b/go.mod h1:GFtu6vaWaRJV5EvSFaVqgq/3Iq95xyYElBV/aupGzUo= 5 | github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= 6 | github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 7 | github.com/aws/aws-sdk-go v1.29.15 h1:0ms/213murpsujhsnxnNKNeVouW60aJqSd992Ks3mxs= 8 | github.com/aws/aws-sdk-go v1.29.15/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= 9 | github.com/aws/aws-sdk-go v1.34.27 h1:qBqccUrlz43Zermh0U1O502bHYZsgMlBm+LUVabzBPA= 10 | github.com/aws/aws-sdk-go v1.34.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= 11 | github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= 12 | github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= 13 | github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 18 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 19 | github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 20 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 21 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 22 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 23 | github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= 24 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 25 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 26 | github.com/go-ini/ini v1.61.0 h1:+IytwU4FcXqB+i5Vqiu/Ybf/Jdin9Pwzdxs5lmuT10o= 27 | github.com/go-ini/ini v1.61.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 28 | github.com/go-ozzo/ozzo-routing v2.1.4+incompatible h1:gQmNyAwMnBHr53Nma2gPTfVVc6i2BuAwCWPam2hIvKI= 29 | github.com/go-ozzo/ozzo-routing v2.1.4+incompatible/go.mod h1:hvoxy5M9SJaY0viZvcCsODidtUm5CzRbYKEWuQpr+2A= 30 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 31 | github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 32 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 33 | github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 34 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 35 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 36 | github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= 37 | github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= 38 | github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= 39 | github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 40 | github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 41 | github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= 42 | github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 43 | github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 44 | github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= 45 | github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= 46 | github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= 47 | github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= 48 | github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= 49 | github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= 50 | github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= 51 | github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= 52 | github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= 53 | github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 54 | github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 55 | github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 56 | github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 57 | github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= 58 | github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= 59 | github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= 60 | github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg= 61 | github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4= 62 | github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 63 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 64 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 65 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 66 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 67 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 68 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 69 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 70 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 71 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 72 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 73 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 74 | github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 75 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 76 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 77 | github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 78 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 79 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 80 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 81 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 82 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 83 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 84 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 85 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 86 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 87 | github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 88 | github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= 89 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 90 | github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= 91 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 92 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= 93 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 94 | github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= 95 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 96 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 97 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 98 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 99 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 100 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 101 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 102 | github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= 103 | github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= 104 | github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 105 | github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg= 106 | github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 107 | github.com/klauspost/compress v1.11.0 h1:wJbzvpYMVGG9iTI9VxpnNZfd4DzMPoCWze3GgSqz8yg= 108 | github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 109 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 110 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 111 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 112 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 113 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 114 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 115 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 116 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 117 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 118 | github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 119 | github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= 120 | github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= 121 | github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 122 | github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 123 | github.com/minio/minio-go v6.0.14+incompatible h1:fnV+GD28LeqdN6vT2XdGKW8Qe/IfjJDswNVuni6km9o= 124 | github.com/minio/minio-go v6.0.14+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8= 125 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 126 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 127 | github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 128 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 129 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 130 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 131 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 132 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 133 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 134 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 135 | github.com/onsi/ginkgo v1.15.1 h1:DsXNrKujDlkMS9Rsxmd+Fg7S6Kc5lhE+qX8tY6laOxc= 136 | github.com/onsi/ginkgo v1.15.1/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= 137 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 138 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 139 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 140 | github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug= 141 | github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= 142 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= 143 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 144 | github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 145 | github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= 146 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 147 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 148 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 149 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 150 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 151 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 152 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 153 | github.com/qiangxue/fasthttp-routing v0.0.0-20160225050629-6ccdc2a18d87 h1:u7uCM+HS2caoEKSPtSFQvvUDXQtqZdu3MYtF+QEw7vA= 154 | github.com/qiangxue/fasthttp-routing v0.0.0-20160225050629-6ccdc2a18d87/go.mod h1:zwr0xP4ZJxwCS/g2d+AUOUwfq/j2NC7a1rK3F0ZbVYM= 155 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 156 | github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 157 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 158 | github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 159 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 160 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 161 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 162 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 163 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 164 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 165 | github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 166 | github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= 167 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 168 | github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 169 | github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 170 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 171 | github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= 172 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 173 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 174 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 175 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 176 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 177 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 178 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 179 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 180 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 181 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 182 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 183 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 184 | github.com/valyala/fasthttp v1.16.0 h1:9zAqOYLl8Tuy3E5R6ckzGDJ1g8+pw15oQp2iL9Jl6gQ= 185 | github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= 186 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 187 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= 188 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= 189 | github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc h1:n+nNi93yXLkJvKwXNP9d55HC7lGK4H/SRcwB5IaUZLo= 190 | github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= 191 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 192 | github.com/zekroTJA/ratelimit v0.0.0-20190321090824-219ca33049a5 h1:EryoK8mdGm7qU0FZjxpt+7Bd9VGBmzsV880DQKq10W4= 193 | github.com/zekroTJA/ratelimit v0.0.0-20190321090824-219ca33049a5/go.mod h1:5aXVBC8pKM3Tva/5YihZ0yOLD+ULDq5P73lpRuBSLNg= 194 | github.com/zekroTJA/timedmap v1.3.1 h1:Tsm17mApGV+KaaoDyZiELWTv4ugtV++0uQbko1bz7QM= 195 | github.com/zekroTJA/timedmap v1.3.1/go.mod h1:ktlw5aYhoXQvOvWFL9SzltGXn1bQgJXxZzHJK4wQvsI= 196 | go.mongodb.org/mongo-driver v1.4.1 h1:38NSAyDPagwnFpUA/D5SFgbugUYR3NzYRNa4Qk9UxKs= 197 | go.mongodb.org/mongo-driver v1.4.1/go.mod h1:llVBH2pkj9HywK0Dtdt6lDikOjFLbceHVu/Rc0iMKLs= 198 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 199 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 200 | golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 201 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 202 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 203 | golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 204 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 205 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= 206 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 207 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 208 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 209 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 210 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 211 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 212 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 213 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 214 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 215 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= 216 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 217 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 218 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= 219 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 220 | golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 221 | golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 222 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 223 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 224 | golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 226 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 227 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 228 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= 229 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 230 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 231 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 232 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 233 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 235 | golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 236 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 239 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 240 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 241 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 242 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 243 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 244 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 245 | golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= 246 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 247 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 248 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 249 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 250 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 251 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 252 | golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 253 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 254 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 255 | golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 256 | golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 257 | golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 258 | golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 259 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 260 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 261 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 262 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 263 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 264 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 265 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 266 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 267 | google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 268 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 269 | google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 270 | google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 271 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 272 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 273 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 274 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 275 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 276 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 277 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 278 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= 279 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= 280 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 281 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 282 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 283 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 284 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 285 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 286 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 287 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= 288 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= 289 | gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= 290 | gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 291 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 292 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 293 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 294 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 295 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 296 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 297 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 298 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 299 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 300 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 301 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 302 | -------------------------------------------------------------------------------- /internal/assets/avatarhandler.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/myrunes/backend/internal/logger" 9 | "github.com/myrunes/backend/internal/storage" 10 | "github.com/myrunes/backend/pkg/workerpool" 11 | ) 12 | 13 | const ( 14 | avatarCDNURL = "https://www.mobafire.com/images/avatars/%s-classic.png" 15 | avatarBucketName = "myrunes-assets-championavatars" 16 | avatarMimeType = "image/png" 17 | ) 18 | 19 | type AvatarHandler struct { 20 | storage storage.Middleware 21 | } 22 | 23 | func NewAvatarHandler(st storage.Middleware) *AvatarHandler { 24 | return &AvatarHandler{st} 25 | } 26 | 27 | func (ah *AvatarHandler) Get(champ string) (io.ReadCloser, int64, error) { 28 | return ah.storage.GetObject(avatarBucketName, getObjectName(champ)) 29 | } 30 | 31 | func (ah *AvatarHandler) FetchAll(cChampNames chan string, cError chan error) { 32 | wp := workerpool.New(5) 33 | 34 | go func() { 35 | for res := range wp.Results() { 36 | if err, _ := res.(error); err != nil { 37 | cError <- err 38 | } 39 | } 40 | }() 41 | 42 | for champ := range cChampNames { 43 | wp.Push(ah.jobFetchSingle, champ) 44 | } 45 | wp.Close() 46 | 47 | wp.WaitBlocking() 48 | close(cError) 49 | } 50 | 51 | func (ah *AvatarHandler) jobFetchSingle(workerId int, params ...interface{}) interface{} { 52 | champ := params[0].(string) 53 | 54 | logger.Info("ASSETSHANDLER :: [%d] fetch champion avatar asset of '%s'...", workerId, champ) 55 | 56 | url := fmt.Sprintf(avatarCDNURL, champ) 57 | resp, err := http.Get(url) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | if resp.StatusCode >= 400 { 63 | return fmt.Errorf("resuest failed with code %d", resp.StatusCode) 64 | } 65 | 66 | return ah.put(champ, resp.Body, resp.ContentLength) 67 | } 68 | 69 | func (ah *AvatarHandler) put(champ string, reader io.Reader, size int64) error { 70 | return ah.storage.PutObject(avatarBucketName, getObjectName(champ), reader, size, avatarMimeType) 71 | } 72 | 73 | func getObjectName(champ string) string { 74 | return fmt.Sprintf("%s.png", champ) 75 | } 76 | -------------------------------------------------------------------------------- /internal/auth/middleware.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/bwmarrin/snowflake" 5 | routing "github.com/qiangxue/fasthttp-routing" 6 | ) 7 | 8 | // AuthMiddleware describes a module which provides 9 | // functionality to hash passwords, check password 10 | // hases, create HTTP sessions and authorize HTTP 11 | // requests. 12 | type AuthMiddleware interface { 13 | 14 | // CreateHash creates a secure password hash 15 | // for the given password string and returns the 16 | // hash as string containing the hashing algorithm, 17 | // the parameters used to create the hash and the 18 | // hash itself as base64 vlaue. 19 | CreateHash(pass string) (string, error) 20 | 21 | // CheckHash checks if the given hash matches a 22 | // given password. The result is returned as 23 | // boolean. If something fails during the hashing, 24 | // the reutrned result will be 'false'. 25 | CheckHash(hash, pass string) bool 26 | 27 | // CreateAndSetRefreshToken creates a new refresh 28 | // token and sets it to the given response context 29 | // as secure session cookie. 30 | CreateAndSetRefreshToken(ctx *routing.Context, uid snowflake.ID, remember bool) (string, error) 31 | 32 | // ObtainAccessToken takes a refreshToken from 33 | // the given request context and returns an 34 | // accessToken, which can be used to to further API 35 | // requests by setting it as Authorization request 36 | // header. 37 | ObtainAccessToken(ctx *routing.Context) (string, error) 38 | 39 | // Login collects login credentials from the 40 | // request payload. After successful authorization, 41 | // a session will be generated and set to the 42 | // response via CreateSession. 43 | // Otherwise, a 401 Untauthorized response will 44 | // be sent back. 45 | Login(ctx *routing.Context) bool 46 | 47 | // Logout removes the session identification 48 | // from the requested user so that following 49 | // requests can not be authorized anymore. 50 | Logout(ctx *routing.Context) error 51 | 52 | // CheckRequestAuth tries to authorize the 53 | // request. On siccess, the authorized user 54 | // object will be collected from the database 55 | // and set as "user" key to the request Context. 56 | // Otherwise, a 401 Unauthorized response will 57 | // be sent back. 58 | CheckRequestAuth(ctx *routing.Context) error 59 | } 60 | -------------------------------------------------------------------------------- /internal/caching/internal.go: -------------------------------------------------------------------------------- 1 | package caching 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bwmarrin/snowflake" 7 | "github.com/myrunes/backend/internal/database" 8 | "github.com/myrunes/backend/internal/objects" 9 | "github.com/zekroTJA/timedmap" 10 | ) 11 | 12 | const ( 13 | secUsers = iota 14 | secPages 15 | ) 16 | 17 | // Internal provides a caching module which uses 18 | // a timedmap.TimedMap instance to store and 19 | // manage cache values. 20 | type Internal struct { 21 | db database.Middleware 22 | 23 | m *timedmap.TimedMap 24 | users timedmap.Section 25 | pages timedmap.Section 26 | } 27 | 28 | // NewInternal creates a new instance of 29 | // Internal. 30 | func NewInternal() *Internal { 31 | tm := timedmap.New(15 * time.Minute) 32 | return &Internal{ 33 | m: tm, 34 | users: tm.Section(secUsers), 35 | pages: tm.Section(secPages), 36 | } 37 | } 38 | 39 | func (c *Internal) SetDatabase(db database.Middleware) { 40 | c.db = db 41 | } 42 | 43 | func (c *Internal) GetUserByID(id snowflake.ID) (*objects.User, error) { 44 | var err error 45 | user, ok := c.users.GetValue(id).(*objects.User) 46 | if !ok || user == nil { 47 | user, err = c.db.GetUser(id, "") 48 | if err != nil { 49 | return nil, err 50 | } 51 | c.SetUserByID(id, user) 52 | } 53 | 54 | return user, nil 55 | } 56 | 57 | func (c *Internal) SetUserByID(id snowflake.ID, user *objects.User) error { 58 | if user == nil { 59 | c.users.Remove(id) 60 | } else { 61 | c.users.Set(id, user, expireDef) 62 | } 63 | return nil 64 | } 65 | 66 | func (c *Internal) GetUserByToken(token string) (*objects.User, bool) { 67 | val, ok := c.users.GetValue(token).(*objects.User) 68 | return val, ok && val != nil 69 | } 70 | 71 | func (c *Internal) SetUserByToken(token string, user *objects.User) error { 72 | if user == nil { 73 | c.users.Remove(token) 74 | } else { 75 | c.users.Set(token, user, expireDef) 76 | } 77 | return nil 78 | } 79 | 80 | func (c *Internal) GetPageByID(id snowflake.ID) (*objects.Page, error) { 81 | var err error 82 | page, ok := c.pages.GetValue(id).(*objects.Page) 83 | if !ok || page == nil { 84 | page, err = c.db.GetPage(id) 85 | if err != nil { 86 | return nil, err 87 | } 88 | c.SetPageByID(id, page) 89 | } 90 | 91 | return page, nil 92 | } 93 | 94 | func (c *Internal) SetPageByID(id snowflake.ID, page *objects.Page) error { 95 | if page == nil { 96 | c.pages.Remove(id) 97 | } else { 98 | c.pages.Set(id, page, expireDef) 99 | } 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /internal/caching/middleware.go: -------------------------------------------------------------------------------- 1 | package caching 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bwmarrin/snowflake" 7 | "github.com/myrunes/backend/internal/database" 8 | "github.com/myrunes/backend/internal/objects" 9 | ) 10 | 11 | var ( 12 | // expireDef is the default expiration time 13 | // used for cache values 14 | expireDef = 1 * time.Hour 15 | ) 16 | 17 | // CacheMiddleware describes a caching module providing 18 | // functionality to store and fetch data to/from 19 | // a cache storage. 20 | // This module will be initialized with SetDatabase. 21 | // The set database must be used to fetch data from 22 | // when a value is not found in cache. This value 23 | // must then be saved in the cache storage. 24 | type CacheMiddleware interface { 25 | 26 | // SetDatabase sets the passed database module 27 | // as cache storage fallback. 28 | SetDatabase(db database.Middleware) 29 | 30 | // GetUserByID returns a User object by ID 31 | GetUserByID(id snowflake.ID) (*objects.User, error) 32 | // SetUserByID sets a User object to the passed ID 33 | SetUserByID(id snowflake.ID, user *objects.User) error 34 | // GetUserByToken returns a User object by token string 35 | GetUserByToken(token string) (*objects.User, bool) 36 | // SetUserByToken sets a User object to the passed 37 | // token string 38 | SetUserByToken(token string, user *objects.User) error 39 | 40 | // GetPageByID returns a Page object by ID 41 | GetPageByID(id snowflake.ID) (*objects.Page, error) 42 | // SetPageByID sets a Page object to the passed ID 43 | SetPageByID(id snowflake.ID, page *objects.Page) error 44 | } 45 | -------------------------------------------------------------------------------- /internal/caching/redis.go: -------------------------------------------------------------------------------- 1 | package caching 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/bwmarrin/snowflake" 9 | "github.com/go-redis/redis" 10 | "github.com/myrunes/backend/internal/database" 11 | "github.com/myrunes/backend/internal/objects" 12 | ) 13 | 14 | const ( 15 | keyUserByID = "USER:ID" 16 | keyUserByToken = "USER:TK" 17 | keyPageByID = "PAGE:ID" 18 | ) 19 | 20 | // RedisConfig contains configuration 21 | // values for the Redis Database 22 | // connection. 23 | type RedisConfig struct { 24 | Enabled bool `json:"enabled"` 25 | 26 | Addr string `json:"addr"` 27 | Password string `json:"password"` 28 | DB int `json:"db"` 29 | } 30 | 31 | // Redis provides a caching module which 32 | // uses Redis to store and manage cache 33 | // values. 34 | type Redis struct { 35 | db database.Middleware 36 | 37 | client *redis.Client 38 | } 39 | 40 | // NewRedis creates a new instance of 41 | // Redis with the given RedisConfig 42 | // instance cfg. 43 | func NewRedis(cfg *RedisConfig) *Redis { 44 | return &Redis{ 45 | client: redis.NewClient(&redis.Options{ 46 | Addr: cfg.Addr, 47 | Password: cfg.Password, 48 | DB: cfg.DB, 49 | }), 50 | } 51 | } 52 | 53 | func (c *Redis) SetDatabase(db database.Middleware) { 54 | c.db = db 55 | } 56 | 57 | func (c *Redis) GetUserByID(id snowflake.ID) (*objects.User, error) { 58 | key := fmt.Sprintf("%s:%d", keyUserByID, id) 59 | 60 | var user *objects.User 61 | err := c.get(key, user) 62 | if err != nil || user == nil { 63 | user, err = c.db.GetUser(id, "") 64 | if err != nil { 65 | return nil, err 66 | } 67 | c.SetUserByID(id, user) 68 | } 69 | 70 | return user, nil 71 | } 72 | 73 | func (c *Redis) SetUserByID(id snowflake.ID, user *objects.User) error { 74 | key := fmt.Sprintf("%s:%d", keyUserByID, id) 75 | 76 | if user == nil { 77 | return c.set(key, nil, expireDef) 78 | } 79 | return c.set(key, user, expireDef) 80 | } 81 | 82 | func (c *Redis) GetUserByToken(token string) (*objects.User, bool) { 83 | key := fmt.Sprintf("%s:%s", keyUserByToken, token) 84 | 85 | var user *objects.User 86 | err := c.get(key, user) 87 | 88 | return user, err == nil && user != nil 89 | } 90 | 91 | func (c *Redis) SetUserByToken(token string, user *objects.User) error { 92 | key := fmt.Sprintf("%s:%s", keyUserByToken, token) 93 | 94 | if user == nil { 95 | return c.set(key, nil, expireDef) 96 | } 97 | return c.set(key, user, expireDef) 98 | } 99 | 100 | func (c *Redis) GetPageByID(id snowflake.ID) (*objects.Page, error) { 101 | key := fmt.Sprintf("%s:%d", keyPageByID, id) 102 | 103 | var page *objects.Page 104 | err := c.get(key, page) 105 | if err != nil || page == nil { 106 | page, err = c.db.GetPage(id) 107 | if err != nil { 108 | return nil, err 109 | } 110 | c.SetPageByID(id, page) 111 | } 112 | 113 | return page, nil 114 | } 115 | 116 | func (c *Redis) SetPageByID(id snowflake.ID, page *objects.Page) error { 117 | key := fmt.Sprintf("%s:%d", keyPageByID, id) 118 | 119 | if page == nil { 120 | return c.set(key, nil, expireDef) 121 | } 122 | return c.set(key, page, expireDef) 123 | } 124 | 125 | // set sets a value in the database to the given key with the 126 | // defined expiration duration. 127 | // The value v must be a reference to a JSON serializable 128 | // object instance. 129 | func (c *Redis) set(key string, v interface{}, expiration time.Duration) error { 130 | if v == nil { 131 | return c.client.Del(key).Err() 132 | } 133 | 134 | d, err := json.Marshal(v) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | return c.client.Set(key, d, expiration).Err() 140 | } 141 | 142 | // get fetches a value from the database by key and writes 143 | // the result to v. 144 | // The value v must be a reference to a JSON serializable 145 | // object instance. 146 | func (c *Redis) get(key string, v interface{}) error { 147 | b, err := c.client.Get(key).Bytes() 148 | if err != nil { 149 | return err 150 | } 151 | 152 | return json.Unmarshal(b, v) 153 | } 154 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | 8 | "github.com/ghodss/yaml" 9 | "github.com/myrunes/backend/internal/caching" 10 | "github.com/myrunes/backend/internal/database" 11 | "github.com/myrunes/backend/internal/mailserver" 12 | "github.com/myrunes/backend/internal/storage" 13 | "github.com/myrunes/backend/internal/webserver" 14 | ) 15 | 16 | // Main wraps all sub config objects 17 | type Main struct { 18 | MongoDB *database.MongoConfig `json:"mongodb"` 19 | Redis *caching.RedisConfig `json:"redis"` 20 | WebServer *webserver.Config `json:"webserver"` 21 | MailServer *mailserver.Config `json:"mailserver"` 22 | 23 | Storage struct { 24 | Typ string `json:"type"` 25 | File *storage.FileConfig `json:"file"` 26 | Minio *storage.MinioConfig `json:"minio"` 27 | } `json:"storage"` 28 | } 29 | 30 | // Open checks for the passed config 31 | // loc. If the file exists, the file 32 | // will be opend and parsed to a Main 33 | // config object. 34 | // Otherwise a default config file will 35 | // be generated on the defiled loc. 36 | func Open(loc string) (*Main, error) { 37 | data, err := ioutil.ReadFile(loc) 38 | if os.IsNotExist(err) { 39 | err = createDefault(loc) 40 | return nil, err 41 | } 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | cfg := new(Main) 47 | err = yaml.Unmarshal(data, cfg) 48 | return cfg, err 49 | } 50 | 51 | // createDefault generates a default Mail 52 | // config object and writes it to the 53 | // defined loc. 54 | func createDefault(loc string) error { 55 | def := &Main{ 56 | MongoDB: &database.MongoConfig{ 57 | Host: "localhost", 58 | Port: "27017", 59 | Username: "lol-runes", 60 | AuthDB: "lol-runes", 61 | DataDB: "lol-runes", 62 | }, 63 | Redis: &caching.RedisConfig{ 64 | Enabled: false, 65 | Addr: "localhost:6379", 66 | DB: 0, 67 | }, 68 | WebServer: &webserver.Config{ 69 | Addr: ":443", 70 | PublicAddr: "https://myrunes.com", 71 | TLS: &webserver.TLSConfig{ 72 | Enabled: true, 73 | }, 74 | }, 75 | MailServer: &mailserver.Config{ 76 | Port: 465, 77 | }, 78 | } 79 | 80 | data, err := yaml.Marshal(def) 81 | 82 | basePath := path.Dir(loc) 83 | if _, err = os.Stat(basePath); os.IsNotExist(err) { 84 | err = os.MkdirAll(basePath, 0750) 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | err = ioutil.WriteFile(loc, data, 0750) 90 | return err 91 | } 92 | -------------------------------------------------------------------------------- /internal/database/middleware.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/bwmarrin/snowflake" 5 | "github.com/myrunes/backend/internal/objects" 6 | ) 7 | 8 | // Middleware describes the structure of a 9 | // database provider module. 10 | // 11 | // On fetching values from the database 12 | // which dont exist, an error returning 13 | // is not expected. Errors should only be 14 | // returned if something went wrong 15 | // accessing the database and will be 16 | // returned as 500 Internal Error from 17 | // the REST API. If an non-existing 18 | // object was fetched, only return nil 19 | // (or the default value for the type) 20 | // for both the value and the error. 21 | type Middleware interface { 22 | // Connect to the database using the 23 | // defined parameters. 24 | Connect(params interface{}) error 25 | // Close the connection to the database. 26 | Close() 27 | 28 | // CreateUser creates a new user object 29 | // in the database from the given user 30 | // object. 31 | CreateUser(user *objects.User) error 32 | // GetUser returns a user object by the 33 | // passed uid or username or e-mail, which 34 | // is passed as username parameter. 35 | // Therefore, the priority of matching is: 36 | // 1. UID, 2. username, 3. e-mail 37 | GetUser(uid snowflake.ID, username string) (*objects.User, error) 38 | // EditUser updates a user object in the 39 | // database to the object passed by its 40 | // UID. 41 | EditUser(user *objects.User) error 42 | // DeleteUser removes a user from the database 43 | // or marks it as removed so that the object 44 | // can not be fetched anymore. 45 | DeleteUser(uid snowflake.ID) error 46 | 47 | // CreatePage creates a page object in the 48 | // database from the passed page object. 49 | CreatePage(page *objects.Page) error 50 | // GetPages returns a collection of pages 51 | // owned by the given users uid. 52 | // If champion is not empty or "general", 53 | // only pages which champions collections 54 | // contain the given champion must be 55 | // returned. 56 | // If filter is not empty, only pages 57 | // which titles contain the filter string 58 | // or which champions collections contain 59 | // a champion which contains the filter 60 | // stirng must be returned. 61 | // If sortLess is not null, the result 62 | // collection must be lesss-sorted by 63 | // the given sortLess function. 64 | GetPages( 65 | uid snowflake.ID, 66 | champion, 67 | filter string, 68 | sortLess func(i, j *objects.Page) bool, 69 | ) ([]*objects.Page, error) 70 | // GetPage returns a page object by the 71 | // given pages uid. 72 | GetPage(uid snowflake.ID) (*objects.Page, error) 73 | // EditPage replaces the page object in 74 | // the database by the passed page 75 | // object by its UID. 76 | EditPage(page *objects.Page) error 77 | // DeletePage removes a page object from 78 | // the database or marks it as removed 79 | // so it's not accessable anymore. 80 | DeletePage(uid snowflake.ID) error 81 | // DeleteUserPages deletes all pages 82 | // of the users UID passed. 83 | DeleteUserPages(uid snowflake.ID) error 84 | 85 | // GetRefreshToken returns a refresh token object 86 | // from the database matching the given refresh 87 | // token string. 88 | GetRefreshToken(token string) (*objects.RefreshToken, error) 89 | // GetRefreshTokens returns a list of refresh tokens 90 | // belonging to the given userID. 91 | GetRefreshTokens(userID snowflake.ID) ([]*objects.RefreshToken, error) 92 | // SetRefreshToken sets a given refresh token 93 | // object to the database or updates one. 94 | SetRefreshToken(t *objects.RefreshToken) error 95 | // RemoveRefreshToken removes a refresh token from 96 | // database if existent by the given token. 97 | RemoveRefreshToken(id snowflake.ID) error 98 | // CleanupExpiredTokens removes all expired tokens 99 | // from the database. 100 | CleanupExpiredTokens() (int, error) 101 | 102 | // SetAPIToken sets the passed API token 103 | // to the user defined in the APIToken 104 | // object. 105 | SetAPIToken(token *objects.APIToken) error 106 | // GetAPIToken returns the APIToken object, 107 | // if available, of the passed users uid. 108 | GetAPIToken(uid snowflake.ID) (*objects.APIToken, error) 109 | // ResetAPIToken deletes the APIToken 110 | // object of the passed users uid so 111 | // that it is no more accessable. 112 | ResetAPIToken(uid snowflake.ID) error 113 | // VerifyAPIToken returns a User object 114 | // which the passed API token string 115 | // belongs to. 116 | VerifyAPIToken(tokenStr string) (*objects.User, error) 117 | 118 | // SetShare creates a nnew share entry 119 | // in the database from the passed SharePage 120 | // object. 121 | SetShare(share *objects.SharePage) error 122 | // GetShare returns the SharePage object by 123 | // the shares ident, uid or pageID of the 124 | // RunePage the share is assigned to. 125 | // (Priority in this order) 126 | GetShare(ident string, uid, pageID snowflake.ID) (*objects.SharePage, error) 127 | // DeleteShare removes a SharePage object 128 | // from the database or makes it inaccessable 129 | // by the shares ident, uid oder the pageID 130 | // of the RunePage the share is belonging to. 131 | // (Priority in this order) 132 | DeleteShare(ident string, uid, pageID snowflake.ID) error 133 | } 134 | -------------------------------------------------------------------------------- /internal/database/mongodb.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "sort" 8 | "time" 9 | 10 | "go.mongodb.org/mongo-driver/bson" 11 | 12 | "github.com/myrunes/backend/internal/objects" 13 | 14 | "github.com/bwmarrin/snowflake" 15 | "go.mongodb.org/mongo-driver/mongo" 16 | "go.mongodb.org/mongo-driver/mongo/options" 17 | "go.mongodb.org/mongo-driver/mongo/readpref" 18 | ) 19 | 20 | type MongoDB struct { 21 | client *mongo.Client 22 | db *mongo.Database 23 | collections *collections 24 | } 25 | 26 | type MongoConfig struct { 27 | Host string `json:"host"` 28 | Port string `json:"port"` 29 | Username string `json:"username"` 30 | Password string `json:"password"` 31 | AuthDB string `json:"auth_db"` 32 | DataDB string `json:"data_db"` 33 | } 34 | 35 | type collections struct { 36 | users, 37 | pages, 38 | apitokens, 39 | refreshtokens, 40 | shares *mongo.Collection 41 | } 42 | 43 | func (m *MongoDB) Connect(params interface{}) (err error) { 44 | cfg, ok := params.(*MongoConfig) 45 | if !ok { 46 | return errors.New("invalid config data type") 47 | } 48 | 49 | uri := fmt.Sprintf("mongodb://%s:%s@%s:%s/%s", 50 | cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.AuthDB) 51 | if m.client, err = mongo.NewClient(options.Client().ApplyURI(uri)); err != nil { 52 | return 53 | } 54 | 55 | ctxConnect, cancelConnect := ctxTimeout(5 * time.Second) 56 | defer cancelConnect() 57 | 58 | if err = m.client.Connect(ctxConnect); err != nil { 59 | return 60 | } 61 | 62 | ctxPing, cancelPing := ctxTimeout(5 * time.Second) 63 | defer cancelPing() 64 | 65 | if err = m.client.Ping(ctxPing, readpref.Primary()); err != nil { 66 | return err 67 | } 68 | 69 | m.db = m.client.Database(cfg.DataDB) 70 | 71 | m.collections = &collections{ 72 | users: m.db.Collection("users"), 73 | pages: m.db.Collection("pages"), 74 | shares: m.db.Collection("shares"), 75 | apitokens: m.db.Collection("apitokens"), 76 | refreshtokens: m.db.Collection("refreshtokens"), 77 | } 78 | 79 | return err 80 | } 81 | 82 | func (m *MongoDB) Close() { 83 | ctx, cancel := ctxTimeout(5 * time.Second) 84 | defer cancel() 85 | 86 | m.client.Disconnect(ctx) 87 | } 88 | 89 | func (m *MongoDB) CreateUser(user *objects.User) error { 90 | return m.insert(m.collections.users, user) 91 | } 92 | 93 | func (m *MongoDB) GetUser(uid snowflake.ID, username string) (*objects.User, error) { 94 | user := new(objects.User) 95 | 96 | ok, err := m.get(m.collections.users, bson.M{"$or": bson.A{ 97 | bson.M{"username": equalsAndNotEmpty(username)}, 98 | bson.M{"mailaddress": equalsAndNotEmpty(username)}, 99 | bson.M{"uid": uid}, 100 | }}, user) 101 | 102 | if !ok { 103 | user = nil 104 | } 105 | 106 | return user, err 107 | } 108 | 109 | func (m *MongoDB) EditUser(user *objects.User) error { 110 | return m.insertOrUpdate(m.collections.users, 111 | bson.M{"uid": user.UID}, user) 112 | } 113 | 114 | func (m *MongoDB) DeleteUser(uid snowflake.ID) error { 115 | ctxDelOne, cancelDelOne := ctxTimeout(5 * time.Second) 116 | defer cancelDelOne() 117 | 118 | _, err := m.collections.users.DeleteOne(ctxDelOne, bson.M{"uid": uid}) 119 | 120 | return err 121 | } 122 | 123 | func (m *MongoDB) CreatePage(page *objects.Page) error { 124 | return m.insert(m.collections.pages, page) 125 | } 126 | 127 | func (m *MongoDB) GetPages(uid snowflake.ID, champion, filter string, sortLess func(i, j *objects.Page) bool) ([]*objects.Page, error) { 128 | var query bson.M 129 | if champion != "" && champion != "general" { 130 | query = bson.M{"owner": uid, "champions": champion} 131 | } else { 132 | query = bson.M{"owner": uid} 133 | } 134 | 135 | if filter != "" { 136 | query["$or"] = bson.A{ 137 | bson.M{ 138 | "title": bson.M{ 139 | "$regex": fmt.Sprintf("(?i).*%s.*", filter), 140 | }, 141 | }, 142 | bson.M{ 143 | "champions": bson.M{ 144 | "$regex": fmt.Sprintf("(?i).*%s.*", filter), 145 | }, 146 | }, 147 | } 148 | } 149 | 150 | count, err := m.count(m.collections.pages, query) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | pages := make([]*objects.Page, count) 156 | 157 | if count == 0 { 158 | return pages, nil 159 | } 160 | 161 | ctxFind, cancelFind := ctxTimeout(5 * time.Second) 162 | defer cancelFind() 163 | 164 | res, err := m.collections.pages.Find(ctxFind, query) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | ctxNext, cancelNext := ctxTimeout(5 * time.Second) 170 | defer cancelNext() 171 | 172 | i := 0 173 | for res.Next(ctxNext) { 174 | page := new(objects.Page) 175 | err = res.Decode(page) 176 | if err != nil { 177 | return nil, err 178 | } 179 | pages[i] = page 180 | i++ 181 | } 182 | 183 | if sortLess != nil { 184 | sort.Slice(pages, func(i, j int) bool { 185 | return sortLess(pages[i], pages[j]) 186 | }) 187 | } 188 | 189 | return pages, nil 190 | } 191 | 192 | func (m *MongoDB) GetPage(uid snowflake.ID) (*objects.Page, error) { 193 | page := new(objects.Page) 194 | ok, err := m.get(m.collections.pages, bson.M{"uid": uid}, page) 195 | if err != nil || !ok { 196 | return nil, err 197 | } 198 | return page, nil 199 | } 200 | 201 | func (m *MongoDB) EditPage(page *objects.Page) error { 202 | return m.insertOrUpdate(m.collections.pages, bson.M{"uid": page.UID}, page) 203 | } 204 | 205 | func (m *MongoDB) DeletePage(uid snowflake.ID) error { 206 | ctx, cancel := ctxTimeout(5 * time.Second) 207 | defer cancel() 208 | 209 | _, err := m.collections.pages.DeleteOne(ctx, bson.M{"uid": uid}) 210 | return err 211 | } 212 | 213 | func (m *MongoDB) DeleteUserPages(uid snowflake.ID) error { 214 | ctxDelMany, cancelDelMany := ctxTimeout(5 * time.Second) 215 | defer cancelDelMany() 216 | 217 | _, err := m.collections.pages.DeleteMany(ctxDelMany, 218 | bson.M{"owner": uid}) 219 | 220 | return err 221 | } 222 | 223 | func (m *MongoDB) SetAPIToken(token *objects.APIToken) error { 224 | return m.insertOrUpdate(m.collections.apitokens, &bson.M{"userid": token.UserID}, token) 225 | } 226 | 227 | func (m *MongoDB) GetAPIToken(uID snowflake.ID) (*objects.APIToken, error) { 228 | token := new(objects.APIToken) 229 | ok, err := m.get(m.collections.apitokens, bson.M{"userid": uID}, token) 230 | if err != nil || !ok { 231 | return nil, err 232 | } 233 | return token, nil 234 | } 235 | 236 | func (m *MongoDB) ResetAPIToken(uID snowflake.ID) error { 237 | ctx, cancel := ctxTimeout(5 * time.Second) 238 | defer cancel() 239 | 240 | _, err := m.collections.apitokens.DeleteOne(ctx, bson.M{"userid": uID}) 241 | return err 242 | } 243 | 244 | func (m *MongoDB) VerifyAPIToken(tokenStr string) (*objects.User, error) { 245 | token := new(objects.APIToken) 246 | ok, err := m.get(m.collections.apitokens, bson.M{"token": tokenStr}, token) 247 | if err != nil || !ok { 248 | return nil, err 249 | } 250 | 251 | return m.GetUser(token.UserID, "") 252 | } 253 | 254 | func (m *MongoDB) SetShare(share *objects.SharePage) error { 255 | return m.insertOrUpdate(m.collections.shares, bson.M{ 256 | "$or": bson.A{ 257 | bson.M{"uid": share.UID}, 258 | bson.M{"pageid": share.PageID}, 259 | }, 260 | }, share) 261 | } 262 | 263 | func (m *MongoDB) GetShare(ident string, uid, pageID snowflake.ID) (*objects.SharePage, error) { 264 | share := new(objects.SharePage) 265 | 266 | ok, err := m.get(m.collections.shares, bson.M{ 267 | "$or": bson.A{ 268 | bson.M{"ident": equalsAndNotEmpty(ident)}, 269 | bson.M{"uid": uid}, 270 | bson.M{"pageid": pageID}, 271 | }, 272 | }, share) 273 | 274 | if err != nil { 275 | return nil, err 276 | } 277 | 278 | if !ok { 279 | return nil, nil 280 | } 281 | 282 | return share, nil 283 | } 284 | 285 | func (m *MongoDB) DeleteShare(ident string, uid, pageID snowflake.ID) error { 286 | ctx, cancel := ctxTimeout(5 * time.Second) 287 | defer cancel() 288 | 289 | _, err := m.collections.shares.DeleteOne(ctx, bson.M{ 290 | "$or": bson.A{ 291 | bson.M{"ident": equalsAndNotEmpty(ident)}, 292 | bson.M{"uid": uid}, 293 | bson.M{"pageid": pageID}, 294 | }, 295 | }) 296 | 297 | return err 298 | } 299 | 300 | func (m *MongoDB) GetRefreshToken(token string) (t *objects.RefreshToken, err error) { 301 | t = new(objects.RefreshToken) 302 | ok, err := m.get(m.collections.refreshtokens, bson.M{"token": token}, t) 303 | if !ok { 304 | t = nil 305 | } 306 | return 307 | } 308 | 309 | func (m *MongoDB) GetRefreshTokens(userID snowflake.ID) (res []*objects.RefreshToken, err error) { 310 | ctx, cancel := ctxTimeout(10 * time.Second) 311 | defer cancel() 312 | 313 | res = make([]*objects.RefreshToken, 0) 314 | cursor, err := m.collections.refreshtokens.Find(ctx, &bson.M{"userid": userID}) 315 | if err == mongo.ErrNoDocuments { 316 | err = nil 317 | } 318 | if err != nil { 319 | return 320 | } 321 | 322 | now := time.Now() 323 | for cursor.Next(ctx) { 324 | v := new(objects.RefreshToken) 325 | if err = cursor.Decode(v); err != nil { 326 | return 327 | } 328 | if now.Before(v.Deadline) { 329 | res = append(res, v) 330 | } 331 | } 332 | 333 | return 334 | } 335 | 336 | func (m *MongoDB) SetRefreshToken(t *objects.RefreshToken) error { 337 | return m.insertOrUpdate(m.collections.refreshtokens, bson.M{"id": t.ID}, t) 338 | } 339 | 340 | func (m *MongoDB) RemoveRefreshToken(id snowflake.ID) error { 341 | ctx, cancel := ctxTimeout(5 * time.Second) 342 | defer cancel() 343 | 344 | _, err := m.collections.refreshtokens.DeleteOne(ctx, bson.M{"id": id}) 345 | if err == mongo.ErrNoDocuments { 346 | err = nil 347 | } 348 | 349 | return err 350 | } 351 | 352 | func (m *MongoDB) CleanupExpiredTokens() (n int, err error) { 353 | ctx, cancel := ctxTimeout(10 * time.Second) 354 | defer cancel() 355 | 356 | now := time.Now() 357 | res, err := m.collections.refreshtokens.DeleteMany(ctx, bson.M{ 358 | "deadline": bson.M{ 359 | "$lte": now, 360 | }, 361 | }) 362 | if res != nil { 363 | n = int(res.DeletedCount) 364 | } 365 | 366 | return 367 | } 368 | 369 | // --- HELPERS ------------------------------------------------------------------ 370 | 371 | // insert adds the given vaalue v to the passed collection. 372 | func (m *MongoDB) insert(collection *mongo.Collection, v interface{}) error { 373 | ctx, cancel := ctxTimeout(5 * time.Second) 374 | defer cancel() 375 | 376 | _, err := collection.InsertOne(ctx, v) 377 | return err 378 | } 379 | 380 | // insertOrUpdate checks if the given value v is existent 381 | // in the passed collection by using the passed filter BSON 382 | // command. 383 | // If the value does not exist, the value winn be inserted. 384 | func (m *MongoDB) insertOrUpdate(collection *mongo.Collection, filter, v interface{}) error { 385 | ctx, cancel := ctxTimeout(5 * time.Second) 386 | defer cancel() 387 | 388 | res, err := collection.UpdateOne( 389 | ctx, 390 | filter, bson.M{ 391 | "$set": v, 392 | }) 393 | 394 | if err != nil { 395 | return err 396 | } 397 | 398 | if res.MatchedCount == 0 { 399 | return m.insert(collection, v) 400 | } 401 | 402 | return err 403 | } 404 | 405 | // get tries to find a value in the passed collection by 406 | // using the passed filter BSON command. 407 | // If successful, the value will be scanned into v and 408 | // the function returns true. 409 | // If the value could not be found, false will be returned. 410 | // An error is only returned if the database access failed, 411 | // not if the value was not found. 412 | func (m *MongoDB) get(collection *mongo.Collection, filter interface{}, v interface{}) (bool, error) { 413 | ctx, cancel := ctxTimeout(5 * time.Second) 414 | defer cancel() 415 | 416 | res := collection.FindOne(ctx, filter) 417 | 418 | if res == nil { 419 | return false, nil 420 | } 421 | 422 | err := res.Decode(v) 423 | if err == mongo.ErrNoDocuments { 424 | return false, nil 425 | } 426 | if err != nil { 427 | return false, err 428 | } 429 | 430 | return true, nil 431 | } 432 | 433 | // count returns the number of values in the passed 434 | // collection matching the passed filter BSON command. 435 | func (M *MongoDB) count(collection *mongo.Collection, filter interface{}) (int64, error) { 436 | ctx, cancel := ctxTimeout(5 * time.Second) 437 | defer cancel() 438 | return collection.CountDocuments(ctx, filter) 439 | } 440 | 441 | // ctxTimeout creates a timeout context with the 442 | // passed timeout duration and returns the context 443 | // object and a cancelation function. 444 | func ctxTimeout(d time.Duration) (context.Context, context.CancelFunc) { 445 | ctx, cancel := context.WithTimeout(context.Background(), d) 446 | return ctx, cancel 447 | } 448 | 449 | // equalsAndNotEmpty creates a BSON filter to 450 | // find an object where the given string key 451 | // equals v and is not empty (""). 452 | func equalsAndNotEmpty(v string) bson.M { 453 | return bson.M{ 454 | "$eq": v, 455 | "$ne": "", 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/op/go-logging" 5 | ) 6 | 7 | const mainLoggerName = "main" 8 | 9 | var log = logging.MustGetLogger(mainLoggerName) 10 | 11 | // Setup sets configuration for logger 12 | func Setup(format string, level int) { 13 | formatter := logging.MustStringFormatter(format) 14 | logging.SetFormatter(formatter) 15 | logging.SetLevel(logging.Level(level), mainLoggerName) 16 | } 17 | 18 | // SetLogLevel sets the log level for the current logger 19 | func SetLogLevel(logLevel int) { 20 | logging.SetLevel(logging.Level(logLevel), mainLoggerName) 21 | } 22 | 23 | // Debug prints a (formatted) debug message 24 | func Debug(format string, args ...interface{}) { 25 | log.Debugf(format, args...) 26 | } 27 | 28 | // Info prints a (formatted) info message 29 | func Info(format string, args ...interface{}) { 30 | log.Infof(format, args...) 31 | } 32 | 33 | // Warning prints a (formatted) warning message 34 | func Warning(format string, args ...interface{}) { 35 | log.Warningf(format, args...) 36 | } 37 | 38 | // Error prints a (formatted) error message 39 | func Error(format string, args ...interface{}) { 40 | log.Errorf(format, args...) 41 | } 42 | 43 | // Fatal prints a (formatted) fatal message 44 | // followed by an exit call with code 1. 45 | func Fatal(format string, args ...interface{}) { 46 | log.Fatalf(format, args...) 47 | } 48 | -------------------------------------------------------------------------------- /internal/mailserver/mailserver.go: -------------------------------------------------------------------------------- 1 | package mailserver 2 | 3 | import ( 4 | "gopkg.in/gomail.v2" 5 | ) 6 | 7 | // Config wraps the configuration values 8 | // for the mail server. 9 | type Config struct { 10 | Host string `json:"host"` 11 | Port int `json:"port"` 12 | Username string `json:"username"` 13 | Password string `json:"password"` 14 | } 15 | 16 | // MailServer provides a connection 17 | // to a mail-server to send e-mails. 18 | type MailServer struct { 19 | dialer *gomail.Dialer 20 | 21 | defFrom string 22 | defFromName string 23 | } 24 | 25 | // NewMailServer initializes a new mail server with 26 | // the given mail server configuration, default "from" 27 | // mail address (defFrom) and default "from" sender 28 | // name (defFromName). 29 | func NewMailServer(config *Config, defFrom, defFromName string) (*MailServer, error) { 30 | ms := new(MailServer) 31 | ms.dialer = gomail.NewPlainDialer(config.Host, config.Port, config.Username, config.Password) 32 | 33 | closer, err := ms.dialer.Dial() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | defer closer.Close() 39 | 40 | ms.defFrom = defFrom 41 | ms.defFromName = defFromName 42 | 43 | return ms, nil 44 | } 45 | 46 | // SendMailRaw dials the connection to the 47 | // mail server and sends the passed Message 48 | // object. 49 | func (ms *MailServer) SendMailRaw(msg *gomail.Message) error { 50 | return ms.dialer.DialAndSend(msg) 51 | } 52 | 53 | // SendMail wraps the given data from, fromName, to, 54 | // subject, body and bodyType to a Message object 55 | // which is then sent via SendMailRaw. 56 | // If from and fromName is empty, the defFrom and 57 | // defFromName will be used instead. 58 | func (ms *MailServer) SendMail(from, fromName, to, subject, body, bodyType string) error { 59 | if from == "" { 60 | from = ms.defFrom 61 | } 62 | 63 | if fromName == "" { 64 | fromName = ms.defFromName 65 | } 66 | 67 | msg := gomail.NewMessage() 68 | msg.SetAddressHeader("From", from, fromName) 69 | msg.SetAddressHeader("To", to, "") 70 | msg.SetHeader("Subject", subject) 71 | msg.SetBody(bodyType, body) 72 | return ms.SendMailRaw(msg) 73 | } 74 | 75 | // SendMailFromDef is shorthand for SendMail 76 | // with defFrom and defFromName as sender 77 | // specifications. 78 | func (ms *MailServer) SendMailFromDef(to, subject, body, bodyType string) error { 79 | return ms.SendMail("", "", to, subject, body, bodyType) 80 | } 81 | -------------------------------------------------------------------------------- /internal/objects/apitoken.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bwmarrin/snowflake" 7 | ) 8 | 9 | // APIToken wraps an API access token string 10 | // with the UserID belonging to the token and 11 | // a time when the token was created. 12 | type APIToken struct { 13 | UserID snowflake.ID `json:"userid"` 14 | Token string `json:"token"` 15 | Created time.Time `json:"created"` 16 | } 17 | -------------------------------------------------------------------------------- /internal/objects/page.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/myrunes/backend/pkg/ddragon" 8 | 9 | "github.com/bwmarrin/snowflake" 10 | "github.com/myrunes/backend/internal/static" 11 | ) 12 | 13 | // pageIDNode is the node to generate page snowflake IDs. 14 | var pageIDNode, _ = snowflake.NewNode(static.NodeIDPages) 15 | 16 | var ( 17 | ErrInvalidChamp = errors.New("invalid champion") 18 | 19 | errInvalidTree = errors.New("invalid tree") 20 | errInvalidPriRune = errors.New("invalid primary rune") 21 | errInvalidSecRune = errors.New("invalid secondary rune") 22 | errInvalidPerk = errors.New("invalid perk") 23 | errInvalidTitle = errors.New("invalid title") 24 | ) 25 | 26 | // PerksPool describes the matrix of 27 | // avalibale rune perks 28 | var PerksPool = [][]string{ 29 | {"diamond", "axe", "time"}, 30 | {"diamond", "shield", "circle"}, 31 | {"heart", "shield", "circle"}, 32 | } 33 | 34 | // Page describes a rune page object 35 | // and the selection of runes and 36 | // perks for this page. 37 | type Page struct { 38 | UID snowflake.ID `json:"uid"` 39 | Owner snowflake.ID `json:"owner"` 40 | Title string `json:"title"` 41 | Created time.Time `json:"created"` 42 | Edited time.Time `json:"edited"` 43 | Champions []string `json:"champions"` 44 | Primary *PrimaryTree `json:"primary"` 45 | Secondary *SecondaryTree `json:"secondary"` 46 | Perks *Perks `json:"perks"` 47 | } 48 | 49 | // PrimaryTree holds the tree type 50 | // and the selected runes in the 51 | // primary rune tree. 52 | type PrimaryTree struct { 53 | Tree string `json:"tree"` 54 | Rows [4]string `json:"rows"` 55 | } 56 | 57 | // PrimaryTree holds the tree type 58 | // and the selected runes in the 59 | // secondary rune tree. 60 | type SecondaryTree struct { 61 | Tree string `json:"tree"` 62 | Rows [2]string `json:"rows"` 63 | } 64 | 65 | // Perks holds the three selected 66 | // perks of the rune page. 67 | type Perks struct { 68 | Rows [3]string `json:"rows"` 69 | } 70 | 71 | // NewEmptyPage creates a new Page 72 | // object and initializes the 73 | // underlying tree and perk 74 | // structure. 75 | func NewEmptyPage() *Page { 76 | return &Page{ 77 | Champions: make([]string, 0), 78 | Primary: &PrimaryTree{ 79 | Rows: [4]string{}, 80 | }, 81 | Secondary: &SecondaryTree{ 82 | Rows: [2]string{}, 83 | }, 84 | Perks: &Perks{ 85 | Rows: [3]string{}, 86 | }, 87 | } 88 | } 89 | 90 | // Validate checks if the page is 91 | // built by specification. 92 | // If the page is invalid, the 93 | // returned error holds the validation 94 | // failure reason. 95 | func (p *Page) Validate() error { 96 | // Check for Title 97 | if p.Title == "" || len(p.Title) > 1024 { 98 | return errInvalidTitle 99 | } 100 | 101 | // Check if primary and secondary tree are the same, 102 | // which is not allowed 103 | if p.Secondary.Tree == p.Primary.Tree { 104 | return errInvalidTree 105 | } 106 | 107 | // Get Primary and Secondary Tree Objects From ddragon 108 | // instance by rune tree UIDs 109 | var primaryTree, secondaryTree *ddragon.RuneTree 110 | for _, tree := range ddragon.DDragonInstance.Runes { 111 | if tree.UID == p.Primary.Tree { 112 | primaryTree = tree 113 | } else if tree.UID == p.Secondary.Tree { 114 | secondaryTree = tree 115 | } 116 | } 117 | 118 | // If no passing trees could be matched, this 119 | // is an invalid tree request 120 | if primaryTree == nil || secondaryTree == nil { 121 | return errInvalidTree 122 | } 123 | 124 | // Check if more rune rows are passed as possible 125 | // rune slots are available 126 | if len(p.Primary.Rows) > len(primaryTree.Slots) { 127 | return errInvalidTree 128 | } 129 | 130 | // Check if primary selected runes exist by UID 131 | for i, row := range p.Primary.Rows { 132 | var exists bool 133 | for _, r := range primaryTree.Slots[i].Runes { 134 | if r.UID == row { 135 | exists = true 136 | } 137 | } 138 | if !exists { 139 | return errInvalidPriRune 140 | } 141 | } 142 | 143 | // Check if secondary selected runes exist by 144 | // UID and check if count is equal 2, else this 145 | // rune tree is invalid. 146 | sec := 0 147 | for _, row := range secondaryTree.Slots { 148 | for _, ru := range row.Runes { 149 | var exists bool 150 | for _, r := range p.Secondary.Rows { 151 | if r == ru.UID { 152 | exists = true 153 | } 154 | } 155 | if exists { 156 | sec++ 157 | break 158 | } 159 | } 160 | } 161 | if sec != 2 { 162 | return errInvalidSecRune 163 | } 164 | 165 | // Check perks 166 | for i, row := range p.Perks.Rows { 167 | var exists bool 168 | for _, p := range PerksPool[i] { 169 | if row == p { 170 | exists = true 171 | } 172 | } 173 | if !exists { 174 | return errInvalidPerk 175 | } 176 | } 177 | 178 | // Check if listed champions exists by their 179 | // champion UIDs 180 | champMap := map[string]interface{}{} 181 | for _, champ := range p.Champions { 182 | var exists bool 183 | for _, c := range ddragon.DDragonInstance.Champions { 184 | if champ == c.UID { 185 | exists = true 186 | } 187 | } 188 | if !exists { 189 | return ErrInvalidChamp 190 | } 191 | 192 | champMap[champ] = nil 193 | } 194 | 195 | champs := make([]string, len(champMap)) 196 | i := 0 197 | for k := range champMap { 198 | champs[i] = k 199 | i++ 200 | } 201 | 202 | p.Champions = champs 203 | 204 | return nil 205 | } 206 | 207 | // FinalizeCreate sets final values of 208 | // the page like the UID, the owner ID, 209 | // creation date and last edit date. 210 | func (p *Page) FinalizeCreate(owner snowflake.ID) { 211 | now := time.Now() 212 | p.UID = pageIDNode.Generate() 213 | p.Owner = owner 214 | p.Created = now 215 | p.Edited = now 216 | } 217 | 218 | // Update sets mutable data to the 219 | // current page from the passed newPage. 220 | // Non-Mutable data like UID, ownerID, 221 | // and creation date will not be updated. 222 | // Edited time will be set to the 223 | // current time. 224 | func (p *Page) Update(newPage *Page) { 225 | p.Edited = time.Now() 226 | p.Title = newPage.Title 227 | p.Champions = newPage.Champions 228 | p.Perks = newPage.Perks 229 | p.Primary = newPage.Primary 230 | p.Secondary = newPage.Secondary 231 | } 232 | -------------------------------------------------------------------------------- /internal/objects/refreshtoken.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bwmarrin/snowflake" 7 | "github.com/myrunes/backend/internal/static" 8 | ) 9 | 10 | var refreshTokenIDNode, _ = snowflake.NewNode(static.NodeIDRefreshTokens) 11 | 12 | type RefreshToken struct { 13 | ID snowflake.ID `json:"id"` 14 | Token string `json:"token,omitempty"` 15 | UserID snowflake.ID `json:"userid"` 16 | Deadline time.Time `json:"deadline"` 17 | LastAccess time.Time `json:"lastaccess"` 18 | LastAccessClient string `json:"lastaccessclient"` 19 | LastAccessIP string `json:"lastaccessip"` 20 | } 21 | 22 | type AccessToken struct { 23 | Token string `json:"accesstoken"` 24 | } 25 | 26 | func (rt *RefreshToken) SetID() *RefreshToken { 27 | rt.ID = refreshTokenIDNode.Generate() 28 | return rt 29 | } 30 | 31 | func (rt *RefreshToken) Sanitize() { 32 | rt.Token = "" 33 | } 34 | 35 | func (rt *RefreshToken) IsExpired() bool { 36 | return time.Now().After(rt.Deadline) 37 | } 38 | -------------------------------------------------------------------------------- /internal/objects/session.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bwmarrin/snowflake" 7 | "github.com/myrunes/backend/internal/static" 8 | ) 9 | 10 | // sessionIDNode is the node to generate session snowflake IDs. 11 | var sessionIDNode, _ = snowflake.NewNode(static.NodeIDRefreshTokens) 12 | 13 | // Session wraps a brwoser login session instance. 14 | type Session struct { 15 | SessionID snowflake.ID `json:"sessionid"` 16 | Key string `json:"key"` 17 | UID snowflake.ID `json:"uid"` 18 | Expires time.Time `json:"expires"` 19 | LastAccess time.Time `json:"lastaccess"` 20 | LastAccessIP string `json:"lastaccessip"` 21 | } 22 | 23 | // NewSession creates a session instance with the 24 | // given session key, expires time, last access IP 25 | // addr - belonging to the passed user ID (uID). 26 | func NewSession(key string, uID snowflake.ID, expires time.Time, addr string) *Session { 27 | return &Session{ 28 | Key: key, 29 | UID: uID, 30 | Expires: expires, 31 | LastAccessIP: addr, 32 | LastAccess: time.Now(), 33 | SessionID: sessionIDNode.Generate(), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/objects/share.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/myrunes/backend/pkg/random" 7 | 8 | "github.com/bwmarrin/snowflake" 9 | "github.com/myrunes/backend/internal/static" 10 | ) 11 | 12 | // shareIDNode is the node to generate share snowflake IDs. 13 | var shareIDNode, _ = snowflake.NewNode(static.NodeIDShares) 14 | 15 | // SharePage wraps a RunePage public share. 16 | type SharePage struct { 17 | UID snowflake.ID `json:"uid"` 18 | Ident string `json:"ident"` 19 | OwnerID snowflake.ID `json:"owner"` 20 | PageID snowflake.ID `json:"page"` 21 | Created time.Time `json:"created"` 22 | MaxAccesses int `json:"maxaccesses"` 23 | Expires time.Time `json:"expires"` 24 | Accesses int `json:"accesses"` 25 | LastAccess time.Time `json:"lastaccess"` 26 | AccessIPs []string `json:"accessips,omitempty"` 27 | } 28 | 29 | // NEwSharePage creates a new SharePage instance with 30 | // the passed ownerID, pageID, maxAccess count and 31 | // expiration time. 32 | // If maxAccesses is 0, maxAccesses is set to -1 which 33 | // indicates that the share is not access limited by 34 | // access count. 35 | // If expire time is the default TIme object Time{}, 36 | // the expiration will be set to 100 years, which 37 | // should be enough to count as unlimited access 38 | // by time. 39 | func NewSharePage(ownerID, pageID snowflake.ID, maxAccesses int, expires time.Time) (*SharePage, error) { 40 | now := time.Now() 41 | var err error 42 | 43 | if (expires == time.Time{}) { 44 | expires = now.Add(100 * 365 * 24 * time.Hour) 45 | } 46 | 47 | if maxAccesses == 0 { 48 | maxAccesses = -1 49 | } 50 | 51 | share := &SharePage{ 52 | Accesses: 0, 53 | Created: now, 54 | Expires: expires, 55 | LastAccess: now, 56 | MaxAccesses: maxAccesses, 57 | OwnerID: ownerID, 58 | PageID: pageID, 59 | UID: shareIDNode.Generate(), 60 | AccessIPs: make([]string, 0), 61 | } 62 | 63 | const identSubset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 64 | share.Ident, err = random.String(5, identSubset) 65 | 66 | return share, err 67 | } 68 | -------------------------------------------------------------------------------- /internal/objects/user.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | "time" 8 | 9 | "github.com/myrunes/backend/internal/auth" 10 | "github.com/myrunes/backend/internal/static" 11 | 12 | "github.com/bwmarrin/snowflake" 13 | ) 14 | 15 | // userIDNode is the node to generate user snowflake IDs. 16 | var userIDNode, _ = snowflake.NewNode(static.NodeIDUsers) 17 | 18 | // allowedUNameChars is a regular expression which matches 19 | // on user name strings which are valid. 20 | var allowedUNameChars = regexp.MustCompile(`[\w_\-]+`) 21 | 22 | var ( 23 | ErrInvalidUsername = errors.New("invalid username") 24 | ) 25 | 26 | // User wraps a general user object. 27 | type User struct { 28 | UID snowflake.ID `json:"uid"` 29 | Username string `json:"username"` 30 | MailAddress string `json:"mailaddress,omitempty"` 31 | DisplayName string `json:"displayname"` 32 | LastLogin time.Time `json:"lastlogin,omitempty"` 33 | Created time.Time `json:"created"` 34 | Favorites []string `json:"favorites,omitempty"` 35 | PageOrder map[string][]snowflake.ID `json:"pageorder,omitempty"` 36 | HasOldPassword bool `json:"hasoldpw,omitempty"` 37 | 38 | PassHash []byte `json:"-"` 39 | } 40 | 41 | // NewUser creates a new User object with the given 42 | // username and password which will be hashed using 43 | // the passed authModdleware and then saved to the 44 | // user object. 45 | func NewUser(username, password string, authMiddleware auth.AuthMiddleware) (*User, error) { 46 | now := time.Now() 47 | passHash, err := authMiddleware.CreateHash(password) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | user := &User{ 53 | Created: now, 54 | LastLogin: now, 55 | PassHash: []byte(passHash), 56 | UID: userIDNode.Generate(), 57 | Username: strings.ToLower(username), 58 | DisplayName: username, 59 | Favorites: []string{}, 60 | } 61 | 62 | return user, nil 63 | } 64 | 65 | // Update sets mutable user data to the 66 | // current user object from the given 67 | // newUser object properties. 68 | // If login is set to true, lastLogin 69 | // will be set to the current time. 70 | func (u *User) Update(newUser *User, login bool) { 71 | if login { 72 | u.LastLogin = time.Now() 73 | } 74 | 75 | if newUser == nil { 76 | return 77 | } 78 | 79 | if newUser.DisplayName != "" { 80 | u.DisplayName = newUser.DisplayName 81 | } 82 | 83 | if newUser.Favorites != nil { 84 | u.Favorites = newUser.Favorites 85 | } 86 | 87 | if newUser.Username != "" { 88 | u.Username = newUser.Username 89 | } 90 | 91 | if newUser.PassHash != nil && len(newUser.PassHash) > 0 { 92 | u.PassHash = newUser.PassHash 93 | } 94 | 95 | if newUser.PageOrder != nil { 96 | u.PageOrder = newUser.PageOrder 97 | } 98 | 99 | if newUser.MailAddress != "" { 100 | if newUser.MailAddress == "__RESET__" { 101 | u.MailAddress = "" 102 | } else { 103 | u.MailAddress = newUser.MailAddress 104 | } 105 | } 106 | } 107 | 108 | // Validate checks if the user object 109 | // is built by specification. 110 | // If the validation fails, the failure 111 | // will be returned as error object. 112 | func (u *User) Validate(acceptEmptyUsername bool) error { 113 | if (!acceptEmptyUsername && len(u.Username) < 3) || 114 | len(allowedUNameChars.FindAllString(u.Username, -1)) > 1 { 115 | 116 | return ErrInvalidUsername 117 | } 118 | 119 | return nil 120 | } 121 | 122 | // Sanitize creates a new User object from 123 | // the current User object which only contains 124 | // information which shall be publicly visible. 125 | func (u *User) Sanitize() *User { 126 | return &User{ 127 | UID: u.UID, 128 | Created: u.Created, 129 | DisplayName: u.DisplayName, 130 | Username: u.Username, 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /internal/ratelimit/ratelimit.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/myrunes/backend/internal/shared" 8 | routing "github.com/qiangxue/fasthttp-routing" 9 | "github.com/zekroTJA/ratelimit" 10 | "github.com/zekroTJA/timedmap" 11 | ) 12 | 13 | const ( 14 | cleanupInterval = 15 * time.Minute 15 | entryLifetime = 1 * time.Hour 16 | ) 17 | 18 | // A RateLimitManager maintains all 19 | // rate limiters for each connection. 20 | type RateLimitManager struct { 21 | limits *timedmap.TimedMap 22 | handler []*rateLimitHandler 23 | } 24 | 25 | type rateLimitHandler struct { 26 | id int 27 | handler routing.Handler 28 | } 29 | 30 | // New creates a new instance 31 | // of RateLimitManager. 32 | func New() *RateLimitManager { 33 | return &RateLimitManager{ 34 | limits: timedmap.New(cleanupInterval), 35 | handler: make([]*rateLimitHandler, 0), 36 | } 37 | } 38 | 39 | // GetHandler returns a new afsthttp-routing 40 | // handler which manages per-route and connection- 41 | // based rate limiting. 42 | // Rate limit information is added as 'X-RateLimit-Limit', 43 | // 'X-RateLimit-Remaining' and 'X-RateLimit-Reset' 44 | // headers. 45 | // This handler aborts the execution of following 46 | // handlers when rate limit is exceed and throws 47 | // a json error body in combination with a 429 48 | // status code. 49 | func (rlm *RateLimitManager) GetHandler(limit time.Duration, burst int) routing.Handler { 50 | rlh := &rateLimitHandler{ 51 | id: len(rlm.handler), 52 | } 53 | 54 | rlh.handler = func(ctx *routing.Context) error { 55 | limiterID := fmt.Sprintf("%d#%s", 56 | rlh.id, shared.GetIPAddr(ctx)) 57 | ok, res := rlm.GetLimiter(limiterID, limit, burst).Reserve() 58 | 59 | ctx.Response.Header.Set("X-RateLimit-Limit", fmt.Sprintf("%d", res.Burst)) 60 | ctx.Response.Header.Set("X-RateLimit-Remaining", fmt.Sprintf("%d", res.Remaining)) 61 | ctx.Response.Header.Set("X-RateLimit-Reset", fmt.Sprintf("%d", res.Reset.Unix())) 62 | 63 | if !ok { 64 | ctx.Abort() 65 | ctx.Response.Header.SetContentType("application/json") 66 | ctx.SetStatusCode(429) 67 | ctx.SetBodyString( 68 | "{\n \"code\": 429,\n \"message\": \"you are being rate limited\"\n}") 69 | } 70 | 71 | return nil 72 | } 73 | 74 | rlm.handler = append(rlm.handler, rlh) 75 | 76 | return rlh.handler 77 | } 78 | 79 | // GetLimiter tries to get an existent limiter 80 | // from the limiter map. If there is no limiter 81 | // existent for this address, a new limiter 82 | // will be created and added to the map. 83 | func (rlm *RateLimitManager) GetLimiter(addr string, limit time.Duration, burst int) *ratelimit.Limiter { 84 | var ok bool 85 | var limiter *ratelimit.Limiter 86 | 87 | if rlm.limits.Contains(addr) { 88 | limiter, ok = rlm.limits.GetValue(addr).(*ratelimit.Limiter) 89 | if !ok { 90 | limiter = rlm.createLimiter(addr, limit, burst) 91 | } 92 | } else { 93 | limiter = rlm.createLimiter(addr, limit, burst) 94 | } 95 | 96 | return limiter 97 | } 98 | 99 | // createLimiter creates a new limiter and 100 | // adds it to the limiters map by the passed 101 | // address. 102 | func (rlm *RateLimitManager) createLimiter(addr string, limit time.Duration, burst int) *ratelimit.Limiter { 103 | limiter := ratelimit.NewLimiter(limit, burst) 104 | rlm.limits.Set(addr, limiter, entryLifetime) 105 | return limiter 106 | } 107 | -------------------------------------------------------------------------------- /internal/shared/shared.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import routing "github.com/qiangxue/fasthttp-routing" 4 | 5 | var ( 6 | headerXForwardedFor = []byte("X-Forwarded-For") 7 | ) 8 | 9 | // GetIPAddr returns the IP address as string 10 | // from the given request context. When the 11 | // request contains a 'X-Forwarded-For' header, 12 | // the value of this will be returned as address. 13 | // Else, the conext remote address will be returned. 14 | func GetIPAddr(ctx *routing.Context) string { 15 | forwardedfor := ctx.Request.Header.PeekBytes(headerXForwardedFor) 16 | if forwardedfor != nil && len(forwardedfor) > 0 { 17 | return string(forwardedfor) 18 | } 19 | 20 | return ctx.RemoteIP().String() 21 | } 22 | -------------------------------------------------------------------------------- /internal/static/idnodes.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | // Snowflake node IDs 4 | const ( 5 | NodeIDUsers = iota*100 + 100 6 | NodeIDPages 7 | NodeIDRefreshTokens 8 | NodeIDShares 9 | ) 10 | -------------------------------------------------------------------------------- /internal/static/ldflags.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | // Flags which are set on compilation 4 | // via -ldflag tag. 5 | var ( 6 | Release = "FALSE" 7 | AppVersion = "DEBUG_BUILD" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/static/static.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | const ( 4 | // User-Agend header context of requests executed 5 | // by discordapp.com when pinging posted links 6 | // in chat. 7 | DiscordUserAgentPingHeaderVal = "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)" 8 | 9 | // Current API specification version. 10 | APIVersion = "1.8.0" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/storage/file.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "path" 8 | ) 9 | 10 | type FileConfig struct { 11 | Location string `json:"location"` 12 | } 13 | 14 | // File implements the Storage interface for a 15 | // local file storage provider. 16 | type File struct { 17 | location string 18 | } 19 | 20 | func (f *File) Init(param ...interface{}) (err error) { 21 | if len(param) == 0 { 22 | return errors.New("storage location must be given") 23 | } 24 | 25 | c, ok := param[0].(FileConfig) 26 | if !ok { 27 | return errors.New("invalid config type") 28 | } 29 | 30 | f.location = c.Location 31 | 32 | return nil 33 | } 34 | 35 | func (f *File) BucketExists(name string) (bool, error) { 36 | stat, err := os.Stat(path.Join(f.location, name)) 37 | if os.IsNotExist(err) { 38 | return false, nil 39 | } 40 | if err != nil { 41 | return false, err 42 | } 43 | if !stat.IsDir() { 44 | return false, errors.New("location is a file") 45 | } 46 | return true, nil 47 | } 48 | 49 | func (f *File) CreateBucket(name string, location ...string) error { 50 | return os.MkdirAll(path.Join(f.location, name), os.ModeDir) 51 | } 52 | 53 | func (f *File) CreateBucketIfNotExists(name string, location ...string) (err error) { 54 | ok, err := f.BucketExists(name) 55 | if err == nil && !ok { 56 | err = f.CreateBucket(name, location...) 57 | } 58 | 59 | return 60 | } 61 | 62 | func (f *File) PutObject(bucketName string, objectName string, reader io.Reader, objectSize int64, mimeType string) (err error) { 63 | if err = f.CreateBucketIfNotExists(bucketName); err != nil { 64 | return 65 | } 66 | 67 | fd := path.Join(f.location, bucketName, objectName) 68 | 69 | stat, err := os.Stat(fd) 70 | var fh *os.File 71 | 72 | if os.IsNotExist(err) { 73 | fh, err = os.Create(fd) 74 | } else if err != nil { 75 | return 76 | } else if stat.IsDir() { 77 | return errors.New("given file dir is a location") 78 | } else { 79 | fh, err = os.Create(fd) 80 | } 81 | 82 | if err != nil { 83 | return 84 | } 85 | 86 | defer fh.Close() 87 | 88 | _, err = io.CopyN(fh, reader, objectSize) 89 | return 90 | } 91 | 92 | func (f *File) GetObject(bucketName string, objectName string) (io.ReadCloser, int64, error) { 93 | fd := path.Join(f.location, bucketName, objectName) 94 | stat, err := os.Stat(fd) 95 | var fh *os.File 96 | 97 | if os.IsNotExist(err) { 98 | return nil, 0, errors.New("file does not exist") 99 | } else if err != nil { 100 | return nil, 0, err 101 | } else if stat.IsDir() { 102 | return nil, 0, errors.New("given file dir is a location") 103 | } else { 104 | fh, err = os.Open(fd) 105 | } 106 | 107 | return fh, stat.Size(), err 108 | } 109 | 110 | func (f *File) DeleteObject(bucketName, objectName string) error { 111 | fd := path.Join(f.location, bucketName, objectName) 112 | return os.Remove(fd) 113 | } 114 | -------------------------------------------------------------------------------- /internal/storage/middleware.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // Middleware interface provides functionalities to 8 | // access an object storage driver. 9 | type Middleware interface { 10 | Init(param ...interface{}) error 11 | 12 | BucketExists(name string) (bool, error) 13 | CreateBucket(name string, location ...string) error 14 | CreateBucketIfNotExists(name string, location ...string) error 15 | 16 | PutObject(bucketName, objectName string, reader io.Reader, objectSize int64, mimeType string) error 17 | GetObject(bucketName, objectName string) (io.ReadCloser, int64, error) 18 | DeleteObject(bucketName, objectName string) error 19 | } 20 | -------------------------------------------------------------------------------- /internal/storage/minio.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | 7 | "github.com/minio/minio-go" 8 | ) 9 | 10 | type MinioConfig struct { 11 | Endpoint string `json:"endpoint"` 12 | AccessKey string `json:"access_key"` 13 | AccessSecret string `json:"access_secret"` 14 | Location string `json:"location"` 15 | Secure bool `json:"secure"` 16 | } 17 | 18 | // Minio implements the Storage interface for 19 | // the MinIO SDK to connect to a MinIO instance, 20 | // Amazon S3 or Google Cloud. 21 | type Minio struct { 22 | client *minio.Client 23 | location string 24 | } 25 | 26 | func (m *Minio) Init(params ...interface{}) (err error) { 27 | if len(params) == 0 { 28 | return errors.New("minio config must be passed") 29 | } 30 | 31 | c, ok := params[0].(MinioConfig) 32 | if !ok { 33 | return errors.New("invalid config type") 34 | } 35 | 36 | m.client, err = minio.New(c.Endpoint, c.AccessKey, c.AccessSecret, c.Secure) 37 | m.location = c.Location 38 | 39 | return 40 | } 41 | 42 | func (m *Minio) BucketExists(name string) (bool, error) { 43 | return m.client.BucketExists(name) 44 | } 45 | 46 | func (m *Minio) CreateBucket(name string, location ...string) error { 47 | return m.client.MakeBucket(name, m.getLocation(location)) 48 | } 49 | 50 | func (m *Minio) CreateBucketIfNotExists(name string, location ...string) (err error) { 51 | ok, err := m.BucketExists(name) 52 | if err == nil && !ok { 53 | err = m.CreateBucket(name, location...) 54 | } 55 | 56 | return 57 | } 58 | 59 | func (m *Minio) PutObject(bucketName, objectName string, reader io.Reader, objectSize int64, mimeType string) (err error) { 60 | if err = m.CreateBucketIfNotExists(bucketName, m.location); err != nil { 61 | return 62 | } 63 | _, err = m.client.PutObject(bucketName, objectName, reader, objectSize, minio.PutObjectOptions{ 64 | ContentType: mimeType, 65 | }) 66 | return 67 | } 68 | 69 | func (m *Minio) GetObject(bucketName, objectName string) (io.ReadCloser, int64, error) { 70 | obj, err := m.client.GetObject(bucketName, objectName, minio.GetObjectOptions{}) 71 | if err != nil { 72 | return nil, 0, err 73 | } 74 | 75 | stat, err := obj.Stat() 76 | if err != nil { 77 | return nil, 0, err 78 | } 79 | 80 | return obj, stat.Size, err 81 | } 82 | 83 | func (m *Minio) DeleteObject(bucketName, objectName string) error { 84 | return m.client.RemoveObject(bucketName, objectName) 85 | } 86 | 87 | func (m *Minio) getLocation(loc []string) string { 88 | if len(loc) > 0 { 89 | return loc[0] 90 | } 91 | return m.location 92 | } 93 | -------------------------------------------------------------------------------- /internal/webserver/auth.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "runtime" 7 | "strings" 8 | "time" 9 | 10 | "github.com/alexedwards/argon2id" 11 | "github.com/bwmarrin/snowflake" 12 | "github.com/dgrijalva/jwt-go" 13 | "github.com/valyala/fasthttp" 14 | 15 | "github.com/myrunes/backend/internal/caching" 16 | "github.com/myrunes/backend/internal/database" 17 | "github.com/myrunes/backend/internal/objects" 18 | "github.com/myrunes/backend/internal/ratelimit" 19 | "github.com/myrunes/backend/internal/shared" 20 | "github.com/myrunes/backend/internal/static" 21 | "github.com/myrunes/backend/pkg/random" 22 | routing "github.com/qiangxue/fasthttp-routing" 23 | "golang.org/x/crypto/bcrypt" 24 | ) 25 | 26 | const ( 27 | // time until a new rate limiter ticket is 28 | // generated for login tries 29 | attemptLimit = 5 * time.Minute 30 | // ammount of tickets which can be stashed 31 | // for login attempts 32 | attemptBurst = 5 33 | // character length of generated API tokens 34 | apiTokenLength = 64 35 | // The byte length of the signing key of 36 | // accessTokens 37 | signingKeyLength = 128 38 | // The character length of refreshTokens 39 | refreshTokenLength = 64 40 | // defaul time until a default login 41 | // session expires 42 | sessionExpireDefault = 2 * time.Hour 43 | // default time until a "remembered" 44 | // login session expires 45 | sessionExpireRemember = 30 * 24 * time.Hour 46 | // time until an access token must be 47 | // reacquired 48 | accessTokenLifetime = 1 * time.Hour 49 | // cookie key name of the refreshToken 50 | refreshTokenCookieName = "refreshToken" 51 | ) 52 | 53 | var ( 54 | errBadRequest = errors.New("bad request") 55 | errUnauthorized = errors.New("unauthorized") 56 | errInvalidAccess = errors.New("invalid access key") 57 | errRateLimited = errors.New("rate limited") 58 | 59 | setCookieHeader = []byte("Set-Cookie") 60 | authorizationHeader = []byte("Authorization") 61 | 62 | jwtGenerationMethod = jwt.SigningMethodHS256 63 | 64 | argon2Params = getArgon2Params() 65 | ) 66 | 67 | // loginRequests describes the request 68 | // model of the login endpoint 69 | type loginRequest struct { 70 | reCaptchaResponse 71 | 72 | UserName string `json:"username"` 73 | Password string `json:"password"` 74 | Remember bool `json:"remember"` 75 | } 76 | 77 | // Authorization provides functionalities 78 | // for HTTP session authorization and 79 | // session lifecycle maintainance. 80 | type Authorization struct { 81 | signingKey []byte 82 | 83 | db database.Middleware 84 | cache caching.CacheMiddleware 85 | rlm *ratelimit.RateLimitManager 86 | } 87 | 88 | // NewAuthorization initializes a new 89 | // Authorization instance using the passed 90 | // jwtKey, which will be used to sign JWTs, 91 | // the database driver, cache driver and 92 | // rate limit manager. 93 | // If the passed jwtKey is nil or empty, 94 | // a random key will be generated on 95 | // initialization. 96 | func NewAuthorization(signingKey []byte, db database.Middleware, cache caching.CacheMiddleware, rlm *ratelimit.RateLimitManager) (auth *Authorization, err error) { 97 | auth = new(Authorization) 98 | auth.db = db 99 | auth.cache = cache 100 | auth.rlm = rlm 101 | 102 | if signingKey == nil || len(signingKey) == 0 { 103 | if auth.signingKey, err = random.ByteArray(signingKeyLength); err != nil { 104 | return 105 | } 106 | } else if len(signingKey) < 32 { 107 | err = errors.New("JWT key must have at least 128 bit") 108 | return 109 | } else { 110 | auth.signingKey = signingKey 111 | } 112 | 113 | return 114 | } 115 | 116 | // CreateHash creates a hash string from the passed 117 | // pass string containing information about the used 118 | // algorithm and parameters used to generate the hash 119 | // together with the actual hash data. 120 | // 121 | // This implementation uses Argon2id hash generation. 122 | func (auth *Authorization) CreateHash(pass string) (string, error) { 123 | return argon2id.CreateHash(pass, argon2Params) 124 | } 125 | 126 | // CheckHash tries to compare the passed hash string 127 | // with the passed pass string by using the method and 128 | // parameters specified in the hash string. 129 | // 130 | // This imlementation supports both the old hash 131 | // algorithm used in myrunes before batch 1.7.x 132 | // (bcrypt) and the current implementation argon2id. 133 | func (auth *Authorization) CheckHash(hash, pass string) bool { 134 | if strings.HasPrefix(hash, "$2a") { 135 | return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)) == nil 136 | } 137 | 138 | if strings.HasPrefix(hash, "$argon2id") { 139 | ok, err := argon2id.ComparePasswordAndHash(pass, hash) 140 | return ok && err == nil 141 | } 142 | 143 | return false 144 | } 145 | 146 | // Login provides a handler accepting login credentials 147 | // as JSON POST body. This is used to authenticate a user 148 | // and create a login session on successful authentication. 149 | func (auth *Authorization) Login(ctx *routing.Context) bool { 150 | login := new(loginRequest) 151 | if err := parseJSONBody(ctx, login); err != nil { 152 | return jsonError(ctx, errBadRequest, fasthttp.StatusBadRequest) != nil 153 | } 154 | 155 | limiter := auth.rlm.GetLimiter(fmt.Sprintf("loginAttempt#%s", shared.GetIPAddr(ctx)), attemptLimit, attemptBurst) 156 | 157 | if limiter.Tokens() <= 0 { 158 | return jsonError(ctx, errRateLimited, fasthttp.StatusTooManyRequests) != nil 159 | } 160 | 161 | user, err := auth.db.GetUser(snowflake.ID(-1), strings.ToLower(login.UserName)) 162 | if err != nil { 163 | return jsonError(ctx, err, fasthttp.StatusInternalServerError) != nil 164 | } 165 | if user == nil { 166 | limiter.Allow() 167 | return jsonError(ctx, errUnauthorized, fasthttp.StatusUnauthorized) != nil 168 | } 169 | 170 | // Querrying user in cache to set cache entry 171 | auth.cache.GetUserByID(user.UID) 172 | 173 | if !auth.CheckHash(string(user.PassHash), login.Password) { 174 | limiter.Allow() 175 | return jsonError(ctx, errUnauthorized, fasthttp.StatusUnauthorized) != nil 176 | } 177 | 178 | if token, err := auth.CreateAndSetRefreshToken(ctx, user.UID, login.Remember); err != nil { 179 | auth.cache.SetUserByToken(token, user) 180 | } 181 | 182 | return true 183 | } 184 | 185 | // CreateSession creates a login session for the specified 186 | // user. This generates a JWT which is signed with the internal 187 | // jwtKey and then stored as cookie on response. 188 | func (auth *Authorization) CreateAndSetRefreshToken(ctx *routing.Context, uid snowflake.ID, remember bool) (token string, err error) { 189 | expires := time.Now() 190 | if remember { 191 | expires = expires.Add(sessionExpireRemember) 192 | } else { 193 | expires = expires.Add(sessionExpireDefault) 194 | } 195 | 196 | if token, err = random.Base64(refreshTokenLength); err != nil { 197 | err = jsonError(ctx, err, fasthttp.StatusInternalServerError) 198 | return 199 | } 200 | 201 | err = auth.db.SetRefreshToken((&objects.RefreshToken{ 202 | Token: token, 203 | UserID: uid, 204 | Deadline: expires, 205 | }).SetID()) 206 | if err != nil { 207 | err = jsonError(ctx, err, fasthttp.StatusInternalServerError) 208 | return 209 | } 210 | 211 | user, err := auth.cache.GetUserByID(uid) 212 | if err != nil { 213 | return 214 | } 215 | 216 | user.Update(nil, true) 217 | if err = user.Validate(true); err != nil { 218 | err = jsonError(ctx, err, fasthttp.StatusBadRequest) 219 | return 220 | } 221 | if err = auth.db.EditUser(user); err != nil { 222 | err = jsonError(ctx, err, fasthttp.StatusInternalServerError) 223 | return 224 | } 225 | 226 | cookieSecurity := "" 227 | if static.Release == "TRUE" { 228 | cookieSecurity = "; Secure; SameSite=Strict" 229 | } 230 | 231 | cookie := fmt.Sprintf("%s=%s; Expires=%s; Path=/; HttpOnly%s", 232 | refreshTokenCookieName, token, expires.Format(time.RFC1123), cookieSecurity) 233 | ctx.Response.Header.AddBytesK(setCookieHeader, cookie) 234 | 235 | return 236 | } 237 | 238 | func (auth *Authorization) ObtainAccessToken(ctx *routing.Context) (string, error) { 239 | key := ctx.Request.Header.Cookie(refreshTokenCookieName) 240 | if key == nil || len(key) == 0 { 241 | return "", jsonError(ctx, errUnauthorized, fasthttp.StatusUnauthorized) 242 | } 243 | 244 | refreshToken := string(key) 245 | token, err := auth.db.GetRefreshToken(refreshToken) 246 | if err != nil { 247 | return "", jsonError(ctx, err, fasthttp.StatusInternalServerError) 248 | } 249 | if token == nil { 250 | return "", jsonError(ctx, errUnauthorized, fasthttp.StatusUnauthorized) 251 | } 252 | 253 | now := time.Now() 254 | if now.After(token.Deadline) { 255 | auth.db.RemoveRefreshToken(token.ID) 256 | return "", jsonError(ctx, errUnauthorized, fasthttp.StatusUnauthorized) 257 | } 258 | 259 | accessToken, err := jwt.NewWithClaims(jwtGenerationMethod, jwt.StandardClaims{ 260 | Subject: token.UserID.String(), 261 | ExpiresAt: now.Add(accessTokenLifetime).Unix(), 262 | IssuedAt: now.Unix(), 263 | }).SignedString(auth.signingKey) 264 | if err != nil { 265 | return "", jsonError(ctx, err, fasthttp.StatusInternalServerError) 266 | } 267 | 268 | token.LastAccess = time.Now() 269 | token.LastAccessClient = string(ctx.Request.Header.Peek("user-agent")) 270 | token.LastAccessIP = shared.GetIPAddr(ctx) 271 | if err = auth.db.SetRefreshToken(token); err != nil { 272 | return "", jsonError(ctx, err, fasthttp.StatusInternalServerError) 273 | } 274 | 275 | return accessToken, nil 276 | } 277 | 278 | // CheckRequestAuth provides a handler which 279 | // cancels the current handler stack if no valid 280 | // session authentication or API token could be 281 | // identified in the request. 282 | func (auth *Authorization) CheckRequestAuth(ctx *routing.Context) error { 283 | var user *objects.User 284 | var err error 285 | var authValue string 286 | 287 | authValueB := ctx.Request.Header.PeekBytes(authorizationHeader) 288 | if authValueB != nil && len(authValueB) > 0 { 289 | authValue = string(authValueB) 290 | } 291 | if strings.HasPrefix(strings.ToLower(authValue), "basic") { 292 | authValue = authValue[6:] 293 | var ok bool 294 | if user, ok = auth.cache.GetUserByToken(authValue); !ok { 295 | if user, err = auth.db.VerifyAPIToken(authValue); err == nil { 296 | auth.cache.SetUserByToken(authValue, user) 297 | } 298 | } 299 | } else if strings.HasPrefix(strings.ToLower(authValue), "accesstoken ") { 300 | authValue = authValue[12:] 301 | 302 | jwtToken, err := jwt.Parse(authValue, func(t *jwt.Token) (interface{}, error) { 303 | return auth.signingKey, nil 304 | }) 305 | if err != nil || !jwtToken.Valid { 306 | return jsonError(ctx, errInvalidAccess, fasthttp.StatusUnauthorized) 307 | } 308 | 309 | claimsMap, ok := jwtToken.Claims.(jwt.MapClaims) 310 | if !ok { 311 | return jsonError(ctx, errInvalidAccess, fasthttp.StatusUnauthorized) 312 | } 313 | 314 | claims := jwt.StandardClaims{} 315 | claims.Subject, _ = claimsMap["sub"].(string) 316 | 317 | userID, _ := snowflake.ParseString(claims.Subject) 318 | user, err = auth.cache.GetUserByID(userID) 319 | } 320 | 321 | if err != nil { 322 | return jsonError(ctx, err, fasthttp.StatusInternalServerError) 323 | } 324 | if user == nil { 325 | return jsonError(ctx, errInvalidAccess, fasthttp.StatusUnauthorized) 326 | } 327 | 328 | ctx.Set("user", user) 329 | ctx.Set("apitoken", authValue) 330 | 331 | return nil 332 | } 333 | 334 | // Logout provides a handler which removes the 335 | // session JWT cookie by setting an invalid, 336 | // expired session cookie. 337 | func (auth *Authorization) Logout(ctx *routing.Context) error { 338 | key := ctx.Request.Header.Cookie(refreshTokenCookieName) 339 | if key == nil || len(key) == 0 { 340 | return jsonError(ctx, errUnauthorized, fasthttp.StatusUnauthorized) 341 | } 342 | 343 | cookie := fmt.Sprintf("%s=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; HttpOnly", refreshTokenCookieName) 344 | ctx.Response.Header.AddBytesK(setCookieHeader, cookie) 345 | 346 | return jsonResponse(ctx, nil, fasthttp.StatusOK) 347 | } 348 | 349 | // getArgon2Params returns an instance of default 350 | // parameters which are used for generating 351 | // Argon2id password hashes. 352 | func getArgon2Params() *argon2id.Params { 353 | cpus := runtime.NumCPU() 354 | 355 | return &argon2id.Params{ 356 | Memory: 128 * 1024, 357 | Iterations: 4, 358 | Parallelism: uint8(cpus), 359 | SaltLength: 16, 360 | KeyLength: 32, 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /internal/webserver/helpers.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha1" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/myrunes/backend/internal/static" 12 | "github.com/myrunes/backend/pkg/recapatcha" 13 | 14 | routing "github.com/qiangxue/fasthttp-routing" 15 | "github.com/valyala/fasthttp" 16 | ) 17 | 18 | var emptyResponseBody = []byte("{}") 19 | 20 | var ( 21 | headerUserAgent = []byte("User-Agent") 22 | headerCacheControl = []byte("Cache-Control") 23 | headerETag = []byte("ETag") 24 | 25 | headerCacheControlValue = []byte("max-age=2592000; must-revalidate; proxy-revalidate; public") 26 | 27 | bcryptPrefix = []byte("$2a") 28 | ) 29 | 30 | var defStatusBoddies = map[int][]byte{ 31 | http.StatusOK: []byte("{\n \"code\": 200,\n \"message\": \"ok\"\n}"), 32 | http.StatusCreated: []byte("{\n \"code\": 201,\n \"message\": \"created\"\n}"), 33 | http.StatusNotFound: []byte("{\n \"code\": 404,\n \"message\": \"not found\"\n}"), 34 | http.StatusUnauthorized: []byte("{\n \"code\": 401,\n \"message\": \"unauthorized\"\n}"), 35 | } 36 | 37 | // jsonError writes the error message of err and the 38 | // passed status to response context and aborts the 39 | // execution of following registered handlers ONLY IF 40 | // err != nil. 41 | // This function always returns a nil error that the 42 | // default error handler can be bypassed. 43 | func jsonError(ctx *routing.Context, err error, status int) error { 44 | if err != nil { 45 | ctx.Response.Header.SetContentType("application/json") 46 | ctx.SetStatusCode(status) 47 | ctx.SetBodyString(fmt.Sprintf("{\n \"code\": %d,\n \"message\": \"%s\"\n}", 48 | status, err.Error())) 49 | ctx.Abort() 50 | } 51 | return nil 52 | } 53 | 54 | // jsonResponse tries to parse the passed interface v 55 | // to JSON and writes it to the response context body 56 | // as same as the passed status code. 57 | // If the parsing fails, this will result in a jsonError 58 | // output of the error with status 500. 59 | // This function always returns a nil error. 60 | func jsonResponse(ctx *routing.Context, v interface{}, status int) error { 61 | var err error 62 | data := emptyResponseBody 63 | 64 | if v == nil { 65 | if d, ok := defStatusBoddies[status]; ok { 66 | data = d 67 | } 68 | } else { 69 | if static.Release != "TRUE" { 70 | data, err = json.MarshalIndent(v, "", " ") 71 | } else { 72 | data, err = json.Marshal(v) 73 | } 74 | if err != nil { 75 | return jsonError(ctx, err, fasthttp.StatusInternalServerError) 76 | } 77 | } 78 | 79 | ctx.Response.Header.SetContentType("application/json") 80 | ctx.SetStatusCode(status) 81 | _, err = ctx.Write(data) 82 | 83 | return jsonError(ctx, err, fasthttp.StatusInternalServerError) 84 | } 85 | 86 | // jsonCachableResponse implements the same functionality 87 | // as jsonReponse and adds cache control headers so that 88 | // brwosers will hold the response data in cacne. 89 | // 90 | // This should only be used on responses which are 91 | // static. 92 | func jsonCachableResponse(ctx *routing.Context, v interface{}, status int) error { 93 | var err error 94 | data := emptyResponseBody 95 | 96 | if v == nil { 97 | if d, ok := defStatusBoddies[status]; ok { 98 | data = d 99 | } 100 | } else { 101 | if static.Release != "TRUE" { 102 | data, err = json.MarshalIndent(v, "", " ") 103 | } else { 104 | data, err = json.Marshal(v) 105 | } 106 | if err != nil { 107 | return jsonError(ctx, err, fasthttp.StatusInternalServerError) 108 | } 109 | } 110 | 111 | ctx.Response.Header.SetContentType("application/json") 112 | ctx.Response.Header.SetBytesKV(headerCacheControl, headerCacheControlValue) 113 | ctx.Response.Header.SetBytesK(headerETag, getETag(data, true)) 114 | ctx.SetStatusCode(status) 115 | _, err = ctx.Write(data) 116 | 117 | return jsonError(ctx, err, fasthttp.StatusInternalServerError) 118 | } 119 | 120 | // parseJSONBody tries to parse a requests JSON 121 | // body to the passed object pointer. If the 122 | // parsing fails, this will result in a jsonError 123 | // output with status 400. 124 | // This function always returns a nil error. 125 | func parseJSONBody(ctx *routing.Context, v interface{}) error { 126 | data := ctx.PostBody() 127 | err := json.Unmarshal(data, v) 128 | if err != nil { 129 | jsonError(ctx, err, fasthttp.StatusBadRequest) 130 | } 131 | return err 132 | } 133 | 134 | func (ws *WebServer) addHeaders(ctx *routing.Context) error { 135 | ctx.Response.Header.SetServer("MYRUNES v." + static.AppVersion) 136 | 137 | if ws.config.PublicAddr != "" && ws.config.EnableCors { 138 | ctx.Response.Header.Set("Access-Control-Allow-Origin", ws.config.PublicAddr) 139 | ctx.Response.Header.Set("Access-Control-Allow-Headers", "authorization, content-type, set-cookie, cookie, server") 140 | ctx.Response.Header.Set("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS") 141 | ctx.Response.Header.Set("Access-Control-Allow-Credentials", "true") 142 | } 143 | 144 | return nil 145 | } 146 | 147 | func (ws *WebServer) validateReCaptcha(ctx *routing.Context, rcr *reCaptchaResponse) (bool, error) { 148 | if rcr.ReCaptchaResponse == "" { 149 | return false, jsonError(ctx, errMissingReCaptchaResponse, fasthttp.StatusBadRequest) 150 | } 151 | 152 | rcRes, err := recapatcha.Validate(ws.config.ReCaptcha.SecretKey, rcr.ReCaptchaResponse) 153 | if err != nil { 154 | return false, jsonError(ctx, err, fasthttp.StatusInternalServerError) 155 | } 156 | if !rcRes.Success { 157 | return false, jsonError(ctx, 158 | fmt.Errorf("recaptcha challenge failed: %+v", rcRes.ErrorCodes), 159 | fasthttp.StatusBadRequest) 160 | } 161 | 162 | return true, nil 163 | } 164 | 165 | // checkPageName takes an actual pageName, a guess and 166 | // a float value for tollerance between 0 and 1. 167 | // Both, the pageName and guess will be lowercased and 168 | // spaces will be removed. Then, the guess will be matched 169 | // on the pageName. If the proportion of characters which 170 | // do not match the pageName is larger than the value of 171 | // tollerance, this function returns false. 172 | func checkPageName(pageName, guess string, tollerance float64) bool { 173 | if pageName == "" || guess == "" { 174 | return false 175 | } 176 | 177 | lenPageName := float64(len(strings.Replace(pageName, " ", "", -1))) 178 | lenGuesses := float64(len(strings.Replace(guess, " ", "", -1))) 179 | 180 | pageNameSplit := strings.Split(strings.ToLower(pageName), " ") 181 | guessSplit := strings.Split(strings.ToLower(guess), " ") 182 | 183 | var matchedChars int 184 | for _, wordName := range pageNameSplit { 185 | for _, guessName := range guessSplit { 186 | if wordName == guessName { 187 | matchedChars += len(wordName) 188 | } 189 | } 190 | } 191 | 192 | return float64(matchedChars)/lenPageName >= (1-tollerance) && 193 | float64(matchedChars)/lenGuesses >= (1-tollerance) 194 | } 195 | 196 | // getETag generates an ETag by the passed 197 | // body data. The generated ETag can either be 198 | // weak or strong, depending on the passed 199 | // value for weak. 200 | func getETag(body []byte, weak bool) string { 201 | hash := sha1.Sum(body) 202 | 203 | weakTag := "" 204 | if weak { 205 | weakTag = "W/" 206 | } 207 | 208 | tag := fmt.Sprintf("%s\"%x\"", weakTag, hash) 209 | 210 | return tag 211 | } 212 | 213 | // isOldPasswordHash returns true if the 214 | // passed hash starts with the identifier 215 | // for bcrypt ('$2a'). 216 | func isOldPasswordHash(hash []byte) bool { 217 | return bytes.HasPrefix(hash, bcryptPrefix) 218 | } 219 | -------------------------------------------------------------------------------- /internal/webserver/structs.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bwmarrin/snowflake" 7 | "github.com/myrunes/backend/internal/objects" 8 | ) 9 | 10 | // listResponse wraps a response of 11 | // an arra of elements containing the 12 | // array as Data and the length of the 13 | // array as N. 14 | type listResponse struct { 15 | N int `json:"n"` 16 | Data interface{} `json:"data"` 17 | } 18 | 19 | // userRequest describes a request body 20 | // for altering a user object. 21 | type userRequest struct { 22 | Username string `json:"username"` 23 | DisplayName string `json:"displayname"` 24 | NewPassword string `json:"newpassword"` 25 | CurrentPassword string `json:"currpassword"` 26 | } 27 | 28 | // alterFavoriteRequest describes the 29 | // request body for modifying the array 30 | // of favorites of a user. 31 | type alterFavoriteRequest struct { 32 | Favorites []string `json:"favorites"` 33 | } 34 | 35 | // createShareRequest describes the 36 | // request body for creating a page 37 | // share. 38 | type createShareRequest struct { 39 | MaxAccesses int `json:"maxaccesses"` 40 | Expires time.Time `json:"expires"` 41 | Page string `json:"page"` 42 | } 43 | 44 | // shareResponse wraps the response 45 | // data when requesting a page share 46 | // object containing the data for the 47 | // share and the liquified data for 48 | // the page which is shared and the 49 | // user which owns the page. 50 | type shareResponse struct { 51 | Share *objects.SharePage `json:"share"` 52 | Page *objects.Page `json:"page"` 53 | User *objects.User `json:"user"` 54 | } 55 | 56 | // pageOrderRequest describes the request 57 | // when modifying the users page order. 58 | type pageOrderRequest struct { 59 | PageOrder []snowflake.ID `json:"pageorder"` 60 | } 61 | 62 | // setMailRequest describes the reuqest 63 | // model for setting or resetting a 64 | // users e-mail specification. 65 | type setMailRequest struct { 66 | MailAddress string `json:"mailaddress"` 67 | Reset bool `json:"reset"` 68 | CurrentPassword string `json:"currpassword"` 69 | } 70 | 71 | // confirmMail desribes the request model 72 | // to confirm a mail settings change. 73 | type confirmMail struct { 74 | Token string `json:"token"` 75 | } 76 | 77 | // passwordReset describes the request model 78 | // on requesting a password reset mail. 79 | type passwordReset struct { 80 | MailAddress string `json:"mailaddress"` 81 | } 82 | 83 | // confirmPasswordReset describes the password 84 | // reset model containing the generated 85 | // verification, the new password string and 86 | // the page name guesses. 87 | type confirmPasswordReset struct { 88 | reCaptchaResponse 89 | 90 | Token string `json:"token"` 91 | NewPassword string `json:"new_password"` 92 | } 93 | 94 | // mailConfirmtationData wraps the mail address 95 | // and user ID which is saved in the mail 96 | // confirmation cache to check and identify 97 | // e-mail confirmations. 98 | type mailConfirmationData struct { 99 | UserID snowflake.ID 100 | MailAddress string 101 | } 102 | 103 | // reCaptchaResponse wraps a ReCAPTCHA response 104 | // token for ReCAPTCHA validation. 105 | type reCaptchaResponse struct { 106 | ReCaptchaResponse string `json:"recaptcharesponse"` 107 | } 108 | -------------------------------------------------------------------------------- /internal/webserver/websrever.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/zekroTJA/timedmap" 8 | 9 | "github.com/myrunes/backend/internal/assets" 10 | "github.com/myrunes/backend/internal/caching" 11 | "github.com/myrunes/backend/internal/database" 12 | "github.com/myrunes/backend/internal/mailserver" 13 | "github.com/myrunes/backend/internal/ratelimit" 14 | 15 | routing "github.com/qiangxue/fasthttp-routing" 16 | "github.com/valyala/fasthttp" 17 | ) 18 | 19 | // Error Objects 20 | var ( 21 | errNotFound = errors.New("not found") 22 | errInvalidArguments = errors.New("invalid arguments") 23 | errUNameInUse = errors.New("user name already in use") 24 | errNoAccess = errors.New("access denied") 25 | errMissingReCaptchaResponse = errors.New("missing recaptcha challenge response") 26 | errEmailAlreadyTaken = errors.New("e-mail address is already taken by another account") 27 | ) 28 | 29 | // Config wraps properties for the 30 | // HTTP REST API server. 31 | type Config struct { 32 | Addr string `json:"addr"` 33 | PathPrefix string `json:"pathprefix"` 34 | TLS *TLSConfig `json:"tls"` 35 | ReCaptcha *ReCaptchaConfig `json:"recaptcha"` 36 | PublicAddr string `json:"publicaddress"` 37 | EnableCors bool `json:"enablecors"` 38 | JWTKey string `json:"jwtkey"` 39 | } 40 | 41 | // TLSConfig wraps properties for 42 | // TLS encryption. 43 | type TLSConfig struct { 44 | Enabled bool `json:"enabled"` 45 | Cert string `json:"certfile"` 46 | Key string `json:"keyfile"` 47 | } 48 | 49 | // ReCaptchaConfig wraps key and secret 50 | // for ReCAPTCHA v2. 51 | type ReCaptchaConfig struct { 52 | SiteKey string `json:"sitekey"` 53 | SecretKey string `json:"secretkey"` 54 | } 55 | 56 | // WebServer provices a HTTP REST 57 | // API router. 58 | type WebServer struct { 59 | server *fasthttp.Server 60 | router *routing.Router 61 | 62 | db database.Middleware 63 | cache caching.CacheMiddleware 64 | ms *mailserver.MailServer 65 | auth *Authorization 66 | rlm *ratelimit.RateLimitManager 67 | 68 | avatarAssetsHandler *assets.AvatarHandler 69 | 70 | mailConfirmation *timedmap.TimedMap 71 | pwReset *timedmap.TimedMap 72 | 73 | config *Config 74 | } 75 | 76 | // NewWebServer initializes a WebServer instance using 77 | // the specified database driver, cache driver, mail 78 | // server instance and configuration instance. 79 | func NewWebServer(db database.Middleware, cache caching.CacheMiddleware, 80 | ms *mailserver.MailServer, avatarAssetsHandler *assets.AvatarHandler, 81 | config *Config) (ws *WebServer, err error) { 82 | 83 | ws = new(WebServer) 84 | 85 | ws.config = config 86 | ws.db = db 87 | ws.cache = cache 88 | ws.ms = ms 89 | ws.rlm = ratelimit.New() 90 | ws.router = routing.New() 91 | ws.server = &fasthttp.Server{ 92 | Handler: ws.router.HandleRequest, 93 | } 94 | 95 | ws.avatarAssetsHandler = avatarAssetsHandler 96 | 97 | if ws.auth, err = NewAuthorization([]byte(config.JWTKey), db, cache, ws.rlm); err != nil { 98 | return 99 | } 100 | 101 | ws.mailConfirmation = timedmap.New(1 * time.Hour) 102 | ws.pwReset = timedmap.New(1 * time.Minute) 103 | 104 | ws.registerHandlers() 105 | 106 | return 107 | } 108 | 109 | // registerHandlers creates all rate limiter buckets and 110 | // registers all routes and request handlers. 111 | func (ws *WebServer) registerHandlers() { 112 | rlGlobal := ws.rlm.GetHandler(500*time.Millisecond, 50) 113 | rlUsersCreate := ws.rlm.GetHandler(15*time.Second, 1) 114 | rlPageCreate := ws.rlm.GetHandler(5*time.Second, 5) 115 | rlPostMail := ws.rlm.GetHandler(60*time.Second, 3) 116 | rlPwReset := ws.rlm.GetHandler(60*time.Second, 3) 117 | 118 | ws.router.Use(ws.addHeaders, rlGlobal) 119 | 120 | api := ws.router.Group(ws.config.PathPrefix) 121 | api. 122 | Post("/login", ws.handlerLogin) 123 | api. 124 | Get("/accesstoken", ws.handlerGetAccessToken) 125 | api. 126 | Post("/logout", ws.auth.CheckRequestAuth, ws.auth.Logout) 127 | 128 | api.Get("/version", ws.handlerGetVersion) 129 | api.Get("/recaptchainfo", ws.handlerGetReCaptchaInfo) 130 | 131 | refreshTokens := api.Group("/refreshtokens") 132 | refreshTokens. 133 | Get("", ws.auth.CheckRequestAuth, ws.handlerGetRefreshTokens) 134 | refreshTokens. 135 | Delete("/", ws.auth.CheckRequestAuth, ws.handlerDeleteRefreshToken) 136 | 137 | assets := api.Group("/assets") 138 | assets. 139 | Get("/champions/avatars/", ws.handlerGetAssetsChampionAvatars) 140 | 141 | resources := api.Group("/resources") 142 | resources. 143 | Get("/champions", ws.handlerGetChamps) 144 | resources. 145 | Get("/runes", ws.handlerGetRunes) 146 | 147 | users := api.Group("/users") 148 | users. 149 | Post("", rlUsersCreate, ws.handlerCreateUser) 150 | users. 151 | Post("/me", ws.auth.CheckRequestAuth, ws.handlerPostMe). 152 | Get(ws.auth.CheckRequestAuth, ws.handlerGetMe). 153 | Delete(ws.auth.CheckRequestAuth, ws.handlerDeleteMe) 154 | users. 155 | Get("/", ws.handlerCheckUsername) 156 | users. 157 | Post("/me/pageorder", ws.auth.CheckRequestAuth, ws.handlerPostPageOrder) 158 | 159 | email := users.Group("/me/mail") 160 | email. 161 | Post("", ws.auth.CheckRequestAuth, rlPostMail, ws.handlerPostMail) 162 | email. 163 | Post("/confirm", ws.handlerPostConfirmMail) 164 | 165 | pwReset := users.Group("/me/passwordreset") 166 | pwReset. 167 | Post("", rlPwReset, ws.handlerPostPwReset) 168 | pwReset. 169 | Post("/confirm", rlPwReset, ws.handlerPostPwResetConfirm) 170 | 171 | pages := api.Group("/pages", ws.addHeaders, rlGlobal, ws.auth.CheckRequestAuth) 172 | pages. 173 | Post("", rlPageCreate, ws.handlerCreatePage). 174 | Get(ws.handlerGetPages) 175 | pages. 176 | Get(`/`, ws.handlerGetPage). 177 | Post(ws.handlerEditPage). 178 | Delete(ws.handlerDeletePage) 179 | 180 | favorites := api.Group("/favorites", ws.addHeaders, rlGlobal, ws.auth.CheckRequestAuth) 181 | favorites. 182 | Get("", ws.handlerGetFavorites). 183 | Post(ws.handlerPostFavorite) 184 | 185 | shares := api.Group("/shares", ws.addHeaders, rlGlobal) 186 | shares. 187 | Post("", ws.auth.CheckRequestAuth, ws.handlerCreateShare) 188 | shares. 189 | Get(`/`, ws.auth.CheckRequestAuth, ws.handlerGetShare) 190 | shares. 191 | Get("/", ws.handlerGetShare) 192 | shares. 193 | Post(`/`, ws.auth.CheckRequestAuth, ws.handlerPostShare). 194 | Delete(ws.auth.CheckRequestAuth, ws.handlerDeleteShare) 195 | 196 | apitoken := api.Group("/apitoken", ws.addHeaders, rlGlobal, ws.auth.CheckRequestAuth) 197 | apitoken. 198 | Get("", ws.handlerGetAPIToken). 199 | Post(ws.handlerPostAPIToken). 200 | Delete(ws.handlerDeleteAPIToken) 201 | 202 | } 203 | 204 | // ListenAndServeBLocing starts the web servers 205 | // listen and serving lifecycle which blocks 206 | // the current goroutine. 207 | func (ws *WebServer) ListenAndServeBlocking() error { 208 | tls := ws.config.TLS 209 | 210 | if tls.Enabled { 211 | if tls.Cert == "" || tls.Key == "" { 212 | return errors.New("cert file and key file must be specified") 213 | } 214 | return ws.server.ListenAndServeTLS(ws.config.Addr, tls.Cert, tls.Key) 215 | } 216 | 217 | return ws.server.ListenAndServe(ws.config.Addr) 218 | } 219 | -------------------------------------------------------------------------------- /pkg/comparison/alphabetical.go: -------------------------------------------------------------------------------- 1 | // Package comparison provides general functionalities 2 | // for comparing different types and forms of data. 3 | package comparison 4 | 5 | import "strings" 6 | 7 | // diffRunes returns the numerical 8 | // difference of the byte valeus 9 | // between b and a (b - a). 10 | func diffRunes(a, b byte) int { 11 | return int(b) - int(a) 12 | } 13 | 14 | // Alphabetically returns true, if string 15 | // a is, if sorted alphabetically, indexed 16 | // before string b. 17 | // 18 | // This can be used, for example, to sort a 19 | // slice of strings alphabetically: 20 | // 21 | // sort.Slice(s, func (a, b int) bool { 22 | // return comparison.Alphabetically(s[a], s[b]) 23 | // }) 24 | func Alphabetically(a, b string) bool { 25 | a = strings.ToLower(a) 26 | b = strings.ToLower(b) 27 | 28 | minLen := len(a) 29 | 30 | lenB := len(b) 31 | if lenB < minLen { 32 | minLen = lenB 33 | } 34 | 35 | for i := 0; i < minLen; i++ { 36 | diff := diffRunes(a[i], b[i]) 37 | if diff != 0 { 38 | return diff > 0 39 | } 40 | } 41 | 42 | return false 43 | } 44 | -------------------------------------------------------------------------------- /pkg/comparison/istrue.go: -------------------------------------------------------------------------------- 1 | // Package comparison provides general functionalities 2 | // for comparing different types and forms of data. 3 | package comparison 4 | 5 | import "strings" 6 | 7 | // IsTrue returns true, if the passed string 8 | // lowercased either equals "1" or "true". 9 | func IsTrue(s string) bool { 10 | return s == "1" || strings.ToLower(s) == "true" 11 | } 12 | -------------------------------------------------------------------------------- /pkg/ddragon/ddragon.go: -------------------------------------------------------------------------------- 1 | // Package ddragon provides bindings to the 2 | // Riots data dragon CDN. 3 | package ddragon 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | // endpoint definitions 12 | const ( 13 | epVersions = "https://ddragon.leagueoflegends.com/api/versions.json" 14 | epChampions = "https://ddragon.leagueoflegends.com/cdn/%s/data/en_US/champion.json" 15 | epRunes = "https://ddragon.leagueoflegends.com/cdn/%s/data/en_US/runesReforged.json" 16 | ) 17 | 18 | // Fetch collects version, champion and rune 19 | // information from the Datadragon API and 20 | // wraps them into a DDragon object returned. 21 | func Fetch(version string) (d *DDragon, err error) { 22 | d = new(DDragon) 23 | 24 | if d.Version, err = GetVersion(version); err != nil { 25 | return 26 | } 27 | 28 | if d.Champions, err = GetChampions(d.Version); err != nil { 29 | return 30 | } 31 | 32 | if d.Runes, err = GetRunes(d.Version); err != nil { 33 | return 34 | } 35 | 36 | return 37 | } 38 | 39 | // GetVersions returns an array of valid LoL patch 40 | // version strings. 41 | func GetVersions() (res []string, err error) { 42 | err = getJSON(epVersions, &res) 43 | return 44 | } 45 | 46 | // GetVersion validates the given version v 47 | // against the array of valid versions collected 48 | // from the API. If v is empty or equals "latest", 49 | // the most recent version string will be returned. 50 | // If the given version string is invalid, an 51 | // error will be returned. 52 | func GetVersion(v string) (string, error) { 53 | versions, err := GetVersions() 54 | if err != nil { 55 | return "", err 56 | } 57 | 58 | if v == "" || v == "latest" { 59 | return versions[0], nil 60 | } 61 | 62 | for _, ver := range versions { 63 | if ver == v { 64 | return v, nil 65 | } 66 | } 67 | 68 | return "", fmt.Errorf("invalid version") 69 | } 70 | 71 | // GetChampions returns an array of Champion objects 72 | // collected from the datadragon API. 73 | func GetChampions(v string) ([]*Champion, error) { 74 | res := new(championsWrapper) 75 | err := getJSON(fmt.Sprintf(epChampions, v), res) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | champs := res.Data 81 | fChamps := make([]*Champion, len(champs)) 82 | var i int 83 | for _, c := range champs { 84 | c.UID = championUIDFormatter(c.Name) 85 | fChamps[i] = c 86 | i++ 87 | } 88 | 89 | return fChamps, nil 90 | } 91 | 92 | // GetRunes returns an array of RuneTree objects 93 | // collected from the datadragon API. 94 | func GetRunes(v string) (res []*RuneTree, err error) { 95 | err = getJSON(fmt.Sprintf(epRunes, v), &res) 96 | 97 | for _, tree := range res { 98 | tree.UID = runeTreeUIDFormatter(tree.Name) 99 | for _, slot := range tree.Slots { 100 | for _, r := range slot.Runes { 101 | r.UID = runeUIDFormatter(r.Name) 102 | } 103 | } 104 | } 105 | 106 | return 107 | } 108 | 109 | // getJSON executes a GET request on the given URL 110 | // and tries to decode the JSON response body 111 | // into the given object reference v. 112 | func getJSON(url string, v interface{}) error { 113 | res, err := http.Get(url) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | if res.StatusCode >= 400 { 119 | return fmt.Errorf("status code was %d", res.StatusCode) 120 | } 121 | 122 | dec := json.NewDecoder(res.Body) 123 | return dec.Decode(v) 124 | } 125 | -------------------------------------------------------------------------------- /pkg/ddragon/formatters.go: -------------------------------------------------------------------------------- 1 | package ddragon 2 | 3 | import "strings" 4 | 5 | var uidExceptions = map[string]string{ 6 | "Nunu \u0026 Willump": "nunu", 7 | } 8 | 9 | func snailCase(name string) string { 10 | name = strings.ToLower(name) 11 | name = strings.Replace(name, " ", "-", -1) 12 | name = strings.Replace(name, "'", "", -1) 13 | name = strings.Replace(name, ".", "", -1) 14 | name = strings.Replace(name, ":", "", -1) 15 | 16 | return name 17 | } 18 | 19 | func championUIDFormatter(name string) string { 20 | if u, ok := uidExceptions[name]; ok { 21 | return u 22 | } 23 | 24 | return snailCase(name) 25 | } 26 | 27 | func runeTreeUIDFormatter(name string) string { 28 | return snailCase(name) 29 | } 30 | 31 | func runeUIDFormatter(name string) string { 32 | return snailCase(name) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/ddragon/static.go: -------------------------------------------------------------------------------- 1 | package ddragon 2 | 3 | var DDragonInstance *DDragon 4 | -------------------------------------------------------------------------------- /pkg/ddragon/structs.go: -------------------------------------------------------------------------------- 1 | package ddragon 2 | 3 | // DDragon wraps the current LoL patch version and 4 | // information about champions and runes collected 5 | // from Riot's Datadragon API. 6 | type DDragon struct { 7 | Version string `json:"version"` 8 | Champions []*Champion `json:"champions"` 9 | Runes []*RuneTree `json:"runes"` 10 | } 11 | 12 | // Champion describes a champion object. 13 | type Champion struct { 14 | UID string `json:"uid"` 15 | Name string `json:"name"` 16 | } 17 | 18 | // RuneTree describes a rune tree and 19 | // contains the rune slots for this tree. 20 | type RuneTree struct { 21 | UID string `json:"uid"` 22 | Name string `json:"name"` 23 | Slots []*RuneSlot `json:"slots"` 24 | } 25 | 26 | // RuneSlot wraps a row of runes. 27 | type RuneSlot struct { 28 | Runes []*Rune `json:"runes"` 29 | } 30 | 31 | // Rune describes the properties of 32 | // a rune in a rune tree row. 33 | type Rune struct { 34 | UID string `json:"uid"` 35 | Name string `json:"name"` 36 | ShortDesc string `json:"shortDesc"` 37 | LongDesc string `json:"longDesc"` 38 | } 39 | 40 | // championsWrapper describes the response 41 | // model of the champions ddragon API 42 | // endpoint response. 43 | type championsWrapper struct { 44 | Data map[string]*Champion `json:"data"` 45 | } 46 | -------------------------------------------------------------------------------- /pkg/etag/etag.go: -------------------------------------------------------------------------------- 1 | // Package etag implements generation functionalities 2 | // for the ETag specification of RFC7273 2.3. 3 | // https://tools.ietf.org/html/rfc7232#section-2.3.1 4 | package etag 5 | 6 | import ( 7 | "crypto/sha1" 8 | "fmt" 9 | ) 10 | 11 | // Generate an ETag by body byte array. 12 | // weak specifies if the generated ETag should 13 | // be tagged as "weak". 14 | func Generate(body []byte, weak bool) string { 15 | hash := sha1.Sum(body) 16 | 17 | weakTag := "" 18 | if weak { 19 | weakTag = "W/" 20 | } 21 | 22 | tag := fmt.Sprintf("%s\"%x\"", weakTag, hash) 23 | 24 | return tag 25 | } 26 | -------------------------------------------------------------------------------- /pkg/lifecycletimer/lifecycletimer.go: -------------------------------------------------------------------------------- 1 | // Package lifecycletimer provides functionalities 2 | // to register handlers which will be executed 3 | // asyncronously during program lifetime. 4 | package lifecycletimer 5 | 6 | import "time" 7 | 8 | // Handler is a function which will be executed 9 | // on each lifecycle elapse. 10 | type Handler func() 11 | 12 | // Timer manages the lifecycle ticker 13 | // and provides functions for registering 14 | // handler and starting/stopping the 15 | // lifecycle ticker. 16 | type Timer struct { 17 | ticker *time.Ticker 18 | 19 | cStop chan bool 20 | 21 | interval time.Duration 22 | handlers []Handler 23 | } 24 | 25 | // New initializes a new instance of 26 | // Timer with the passed duration as 27 | // time between each lifecycle elapse. 28 | // This does not automatically start the 29 | // lifecycle timer. 30 | func New(interval time.Duration) *Timer { 31 | return &Timer{ 32 | handlers: make([]Handler, 0), 33 | interval: interval, 34 | } 35 | } 36 | 37 | // Handle registers the passed handler so 38 | // that it will be executed on each lifecycle 39 | // timer elapse. 40 | func (t *Timer) Handle(h Handler) *Timer { 41 | t.handlers = append(t.handlers, h) 42 | return t 43 | } 44 | 45 | // Start starts the lifecycle timer. 46 | // The first handler execution will 47 | // occur after the first cycle elapsed. 48 | func (t *Timer) Start() *Timer { 49 | t.ticker = time.NewTicker(t.interval) 50 | 51 | go func() { 52 | for { 53 | select { 54 | 55 | case <-t.cStop: 56 | return 57 | 58 | case <-t.ticker.C: 59 | for _, h := range t.handlers { 60 | if h != nil { 61 | h() 62 | } 63 | } 64 | } 65 | } 66 | }() 67 | 68 | return t 69 | } 70 | 71 | // Stop stops the lifecycle timer. 72 | func (t *Timer) Stop() { 73 | go func() { 74 | t.cStop <- true 75 | }() 76 | } 77 | -------------------------------------------------------------------------------- /pkg/random/random.go: -------------------------------------------------------------------------------- 1 | // Package random generates cryptographically 2 | // random values. 3 | package random 4 | 5 | import ( 6 | "crypto/rand" 7 | "encoding/base64" 8 | "math/big" 9 | ) 10 | 11 | // ByteArray returns a cryptographically random byte 12 | // array of the given length. 13 | func ByteArray(lngth int) ([]byte, error) { 14 | arr := make([]byte, lngth) 15 | _, err := rand.Read(arr) 16 | return arr, err 17 | } 18 | 19 | // Base64 creates a cryptographically randomly 20 | // generated set of bytes with the length of lngth which 21 | // is returned as base64 encoded string. 22 | func Base64(lngth int) (string, error) { 23 | str, err := ByteArray(lngth) 24 | if err != nil { 25 | return "", err 26 | } 27 | return base64.StdEncoding.EncodeToString(str), nil 28 | } 29 | 30 | // String returns a cryptographically random string with 31 | // the given lngth from the set of characters passed. 32 | func String(lngth int, set string) (string, error) { 33 | res := make([]byte, lngth) 34 | setlen := big.NewInt(int64(len(set))) 35 | 36 | for i := 0; i < lngth; i++ { 37 | randn, err := rand.Int(rand.Reader, setlen) 38 | if err != nil { 39 | return "", err 40 | } 41 | res[i] = set[randn.Int64()] 42 | } 43 | 44 | return string(res), nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/recapatcha/recapatcha.go: -------------------------------------------------------------------------------- 1 | // package recapatcha provides bindings to google's 2 | // ReCAPTCHA validation service. 3 | package recapatcha 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | const ( 13 | endpoint = "https://www.google.com/recaptcha/api/siteverify" 14 | contentType = "application/json" 15 | ) 16 | 17 | // Response wraps the HTTP response from the 18 | // validation endpoint containing success state, 19 | // challenge timestamp, hostname and possible 20 | // error codes on validation failure. 21 | // 22 | // Read here for more information: 23 | // https://developers.google.com/recaptcha/docs/verify 24 | type Response struct { 25 | Success bool `json:"success"` 26 | ChallengeTS time.Time `json:"challenge_ts"` 27 | Hostname string `json:"hostname"` 28 | ErrorCodes []string `json:"error-codes"` 29 | } 30 | 31 | // Validate sends a HTTP POST request to google's ReCAPTCHA 32 | // validation endpoint using the specified secret, response 33 | // and remoteIP (optional) as parameters. 34 | // 35 | // The prased Response result object is returned. Errors are 36 | // only returned when the response parsing or the request 37 | // itself failed. Invalid validation will not return an error. 38 | func Validate(secret, response string, remoteIP ...string) (res *Response, err error) { 39 | remoteAddrParam := "" 40 | if len(remoteIP) > 0 { 41 | remoteAddrParam = "&remoteip=" + remoteIP[0] 42 | } 43 | 44 | url := fmt.Sprintf("%s?secret=%s&response=%s%s", endpoint, secret, response, remoteAddrParam) 45 | 46 | var httpRes *http.Response 47 | if httpRes, err = http.Post(url, contentType, nil); err != nil { 48 | return 49 | } 50 | 51 | if httpRes.StatusCode != 200 { 52 | err = fmt.Errorf("response code was %d", httpRes.StatusCode) 53 | return 54 | } 55 | 56 | res = new(Response) 57 | err = json.NewDecoder(httpRes.Body).Decode(res) 58 | 59 | return 60 | } 61 | -------------------------------------------------------------------------------- /pkg/workerpool/workerpool.go: -------------------------------------------------------------------------------- 1 | // Package workerpool provides a simple implementation 2 | // of goroutine "thread" pools. 3 | package workerpool 4 | 5 | import ( 6 | "sync" 7 | ) 8 | 9 | // Job defines a job function which will be executed 10 | // in a worker getting passed the worker ID and 11 | // parameters specified when pushing the job. 12 | type Job func(workerId int, params ...interface{}) interface{} 13 | 14 | // WorkerPool provides a simple "thread" pool 15 | // implementation based on goroutines. 16 | type WorkerPool struct { 17 | jobs chan jobWrapper 18 | results chan interface{} 19 | wg sync.WaitGroup 20 | } 21 | 22 | // jobWrapper wraps a Job and its specified 23 | // parameters. 24 | type jobWrapper struct { 25 | job Job 26 | params []interface{} 27 | } 28 | 29 | // New creates a new instance of WorkerPool and 30 | // spawns the defined number (size) of workers 31 | // available waiting for jobs. 32 | func New(size int) *WorkerPool { 33 | w := &WorkerPool{ 34 | jobs: make(chan jobWrapper), 35 | results: make(chan interface{}), 36 | } 37 | 38 | for i := 0; i < size; i++ { 39 | go w.spawnWorker(i) 40 | } 41 | 42 | return w 43 | } 44 | 45 | // Push enqueues a job with specified parameters which 46 | // will be passed on executing the job. 47 | func (w *WorkerPool) Push(job Job, params ...interface{}) { 48 | w.jobs <- jobWrapper{ 49 | job: job, 50 | params: params, 51 | } 52 | } 53 | 54 | // Close closes the Jobs channel so that the workers 55 | // stop after executing all enqueued jobs. 56 | // This is nessecary to be executed before WaitBlocking 57 | // is called. 58 | func (w *WorkerPool) Close() { 59 | close(w.jobs) 60 | } 61 | 62 | // Results returns the read-only results channel where 63 | // executed job results are pushed in. 64 | func (w *WorkerPool) Results() <-chan interface{} { 65 | return w.results 66 | } 67 | 68 | // WaitBlocking blocks until all jobs are finished. 69 | func (w *WorkerPool) WaitBlocking() { 70 | w.wg.Wait() 71 | close(w.results) 72 | } 73 | 74 | // spawnWorker spawns a new worker with the passed 75 | // worker id and starts listening for incomming jobs. 76 | func (w *WorkerPool) spawnWorker(id int) { 77 | for job := range w.jobs { 78 | if job.job != nil { 79 | w.wg.Add(1) 80 | w.results <- job.job(id, job.params...) 81 | w.wg.Done() 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /scripts/get-champ-avis.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | URL="https://www.mobafire.com/images/champion/square" 4 | 5 | xargs -a ./champs.txt -d '\n' -I {} \ 6 | wget $URL/{}.png --------------------------------------------------------------------------------