├── .github └── workflows │ ├── general.yml │ ├── merge.yml │ └── release.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── adapter ├── app_config.go └── app_config_test.go ├── altair.go ├── cfg ├── app.go ├── app_bearer.go ├── app_bearer_test.go ├── app_test.go ├── cfg.go ├── database.go ├── database_bearer.go ├── database_bearer_test.go ├── database_test.go ├── example_test.go ├── helper.go ├── plugin.go ├── plugin_bearer.go ├── plugin_bearer_test.go └── plugin_test.go ├── core ├── cfg.go ├── controller.go ├── dispatcher.go ├── metric.go ├── migrator.go ├── plugin.go ├── provider.go └── routing.go ├── docker-compose.yaml ├── entity ├── app_config.go ├── app_config_test.go ├── db_config_mysql.go ├── db_config_mysql_test.go ├── entity.go ├── plugin_config.go └── router_path_test.go ├── env.sample ├── go.mod ├── go.sum ├── mock ├── mock_cfg.go └── mock_gin.go ├── module ├── apierror │ ├── provider.go │ └── usecase │ │ ├── apierror.go │ │ └── apierror_test.go ├── app │ ├── provider.go │ └── usecase │ │ ├── app.go │ │ └── app_test.go ├── controller │ ├── provider.go │ └── usecase │ │ ├── command.go │ │ ├── command_test.go │ │ ├── controller.go │ │ ├── controller_test.go │ │ ├── downstream.go │ │ ├── downstream_test.go │ │ ├── http.go │ │ ├── http_test.go │ │ ├── metric.go │ │ └── metric_test.go ├── healthcheck │ ├── controller │ │ └── http │ │ │ ├── health.go │ │ │ └── health_test.go │ └── loader.go ├── interface.go ├── mock │ └── interface.go ├── projectgenerator │ ├── controller │ │ └── command │ │ │ ├── new.go │ │ │ └── new_test.go │ ├── loader.go │ └── template │ │ ├── app.yml │ │ ├── database.yml │ │ ├── env.sample │ │ ├── plugin │ │ ├── metric.yml │ │ └── oauth.yml │ │ └── routes │ │ └── service-a.yml └── router │ ├── provider.go │ └── usecase │ ├── compiler.go │ ├── compiler_test.go │ ├── example_test.go │ ├── generator.go │ ├── generator_benchmark_test.go │ └── generator_test.go ├── plugin ├── loader.go ├── metric │ ├── entity │ │ └── metric_plugin.go │ ├── loader.go │ ├── module │ │ ├── dummy │ │ │ ├── controller │ │ │ │ └── metric │ │ │ │ │ ├── dummy.go │ │ │ │ │ └── dummy_test.go │ │ │ └── loader.go │ │ └── prometheus │ │ │ ├── controller │ │ │ ├── http │ │ │ │ ├── prometheus_controller.go │ │ │ │ └── prometheus_controller_test.go │ │ │ └── metric │ │ │ │ ├── prometheus.go │ │ │ │ └── prometheus_test.go │ │ │ └── loader.go │ └── version_1_0.go └── oauth │ ├── entity │ ├── insertable.go │ ├── json.go │ ├── model.go │ ├── oauth_plugin.go │ └── oauth_plugin_test.go │ ├── loader.go │ ├── module │ ├── application │ │ ├── controller │ │ │ ├── command │ │ │ │ ├── command.go │ │ │ │ ├── create_application.go │ │ │ │ ├── create_application_test.go │ │ │ │ └── mock │ │ │ │ │ └── mock.go │ │ │ ├── downstream │ │ │ │ ├── application_validation.go │ │ │ │ ├── application_validation_test.go │ │ │ │ ├── downstream.go │ │ │ │ └── mock │ │ │ │ │ └── mock.go │ │ │ └── http │ │ │ │ ├── create.go │ │ │ │ ├── create_test.go │ │ │ │ ├── http.go │ │ │ │ ├── list.go │ │ │ │ ├── list_test.go │ │ │ │ ├── mock │ │ │ │ └── mock.go │ │ │ │ ├── one.go │ │ │ │ ├── one_test.go │ │ │ │ ├── update.go │ │ │ │ └── update_test.go │ │ ├── loader.go │ │ └── usecase │ │ │ ├── application_manager.go │ │ │ ├── create.go │ │ │ ├── create_test.go │ │ │ ├── list.go │ │ │ ├── list_test.go │ │ │ ├── mock │ │ │ └── mock.go │ │ │ ├── one.go │ │ │ ├── one_test.go │ │ │ ├── update.go │ │ │ ├── update_test.go │ │ │ ├── validate_application.go │ │ │ └── validate_application_test.go │ ├── authorization │ │ ├── controller │ │ │ ├── downstream │ │ │ │ ├── downstream.go │ │ │ │ ├── mock │ │ │ │ │ └── mock.go │ │ │ │ ├── oauth_downstream.go │ │ │ │ └── oauth_downstream_test.go │ │ │ └── http │ │ │ │ ├── grant.go │ │ │ │ ├── grant_test.go │ │ │ │ ├── http.go │ │ │ │ ├── mock │ │ │ │ └── mock.go │ │ │ │ ├── revoke.go │ │ │ │ ├── revoke_test.go │ │ │ │ ├── token.go │ │ │ │ └── token_test.go │ │ ├── loader.go │ │ └── usecase │ │ │ ├── authorization.go │ │ │ ├── authorization_test.go │ │ │ ├── client_credential.go │ │ │ ├── client_credential_test.go │ │ │ ├── exception_mapping.go │ │ │ ├── find_and_validate_application.go │ │ │ ├── find_and_validate_application_test.go │ │ │ ├── grant.go │ │ │ ├── grant_authorization_code.go │ │ │ ├── grant_authorization_code_test.go │ │ │ ├── grant_refresh_token.go │ │ │ ├── grant_refresh_token_test.go │ │ │ ├── grant_test.go │ │ │ ├── grant_token.go │ │ │ ├── grant_token_from_authorization_code.go │ │ │ ├── grant_token_from_authorization_code_test.go │ │ │ ├── grant_token_from_refresh_token.go │ │ │ ├── grant_token_from_refresh_token_test.go │ │ │ ├── grant_token_test.go │ │ │ ├── implicit_grant.go │ │ │ ├── implicit_grant_test.go │ │ │ ├── mock │ │ │ └── mock.go │ │ │ ├── revoke_token.go │ │ │ ├── revoke_token_test.go │ │ │ ├── validate_authorization_grant.go │ │ │ ├── validate_authorization_grant_test.go │ │ │ ├── validate_token_authorization_code.go │ │ │ ├── validate_token_authorization_code_test.go │ │ │ ├── validate_token_grant.go │ │ │ └── validate_token_grant_test.go │ ├── formatter │ │ ├── provider.go │ │ └── usecase │ │ │ ├── access_grant.go │ │ │ ├── access_grant_from_authorization_request_insertable.go │ │ │ ├── access_token.go │ │ │ ├── access_token_client_credential_insertable.go │ │ │ ├── access_token_from_authorization_request_insertable.go │ │ │ ├── access_token_from_oauth_access_grant_insertable.go │ │ │ ├── access_token_from_oauth_refresh_token_insertable.go │ │ │ ├── application.go │ │ │ ├── application_list.go │ │ │ ├── formatter.go │ │ │ ├── formatter_test.go │ │ │ ├── oauth_application_insertable.go │ │ │ ├── refresh_token.go │ │ │ └── refresh_token_insertable.go │ └── migration │ │ ├── controller │ │ └── command │ │ │ ├── migrate_down.go │ │ │ ├── migrate_rollback.go │ │ │ └── migrate_up.go │ │ ├── loader.go │ │ └── mysql │ │ ├── 1_create_table_oauth_applications.down.sql │ │ ├── 1_create_table_oauth_applications.up.sql │ │ ├── 2_create_table_oauth_access_tokens.down.sql │ │ ├── 2_create_table_oauth_access_tokens.up.sql │ │ ├── 3_create_table_oauth_access_grants.down.sql │ │ ├── 3_create_table_oauth_access_grants.up.sql │ │ ├── 4_create_table_oauth_refresh_token.down.sql │ │ └── 4_create_table_oauth_refresh_token.up.sql │ ├── repository │ └── mysql │ │ ├── oauth_access_grant.go │ │ ├── oauth_access_grant_test.go │ │ ├── oauth_access_token.go │ │ ├── oauth_access_token_test.go │ │ ├── oauth_application.go │ │ ├── oauth_application_test.go │ │ ├── oauth_refresh_token.go │ │ └── oauth_refresh_token_test.go │ └── version_1.0.go ├── testhelper └── testhelper.go └── util ├── util.go └── util_test.go /.github/workflows/general.yml: -------------------------------------------------------------------------------- 1 | name: General workflows 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | name: Test Coverage 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up Go 1.x 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: ^1.19 18 | 19 | - uses: actions/checkout@v3 20 | 21 | - name: Install goveralls 22 | run: go install github.com/mattn/goveralls@latest 23 | 24 | - name: Lint 25 | uses: golangci/golangci-lint-action@v3 26 | with: 27 | version: v1.50.1 28 | 29 | - name: Unit Test 30 | run: make test 31 | 32 | - name: Upload coverage 33 | env: 34 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | run: goveralls -coverprofile=cover.out -service=github 36 | -------------------------------------------------------------------------------- /.github/workflows/merge.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Latest 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | push_to_registry: 8 | name: Push Docker image to Docker Hub Registry 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - name: Check out the repo 12 | uses: actions/checkout@v3 13 | 14 | - name: Set up Go 1.x 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: ^1.19 18 | 19 | - name: Logout first 20 | run: docker logout 21 | 22 | - name: Login to docker hub registry 23 | run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login docker.io -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 24 | 25 | - name: Build binary 26 | run: make build_linux 27 | 28 | - name: Build docker image 29 | run: make build_docker_latest 30 | 31 | - name: Tag docker latest 32 | run: make tag_docker_latest 33 | 34 | - name: Push docker image 35 | run: make push_docker_latest 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | push_to_registry: 7 | name: Push Docker image to Docker Hub Registry 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - name: Check out the repo 11 | uses: actions/checkout@v2 12 | 13 | - name: Login to docker hub registry 14 | run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login docker.io -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 15 | 16 | - name: Build binary 17 | run: make build_linux 18 | 19 | - name: Build docker image 20 | run: make build_docker 21 | 22 | - name: Push docker image 23 | run: make push_docker 24 | -------------------------------------------------------------------------------- /.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 | .env 15 | 16 | levelup 17 | deploy/_output 18 | vendor 19 | 20 | build_output/ 21 | 22 | .vscode 23 | routes/ 24 | config/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.17 2 | 3 | WORKDIR /opt/altair/ 4 | 5 | COPY ./build_output/linux/altair /usr/local/bin/ 6 | COPY ./env.sample /opt/altair/.env 7 | 8 | RUN apk --update upgrade 9 | RUN apk --no-cache add curl tzdata 10 | RUN altair new . 11 | 12 | EXPOSE 1304 13 | ENTRYPOINT ["altair", "run"] 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export VERSION ?= $(shell git show -q --format=%h) 2 | export IMAGE ?= kodefluence/altair 3 | 4 | test: 5 | go test -race -cover -coverprofile=cover.out $$(go list ./... | grep -Ev "altair$$|core|mock|interfaces|testhelper") 6 | 7 | mock_metric: 8 | mockgen -source core/metric.go -destination mock/mock_metric.go -package mock 9 | 10 | mock_plugin: 11 | mockgen -source core/plugin.go -destination mock/mock_plugin.go -package mock 12 | 13 | mock_loader: 14 | mockgen -source core/cfg.go -destination mock/mock_cfg.go -package mock 15 | 16 | mock_routing: 17 | mockgen -source core/routing.go -destination mock/mock_routing.go -package mock 18 | 19 | mock_all: mock_service mock_formatter mock_model mock_validator mock_plugin mock_routing 20 | 21 | build_linux: 22 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s" -o ./build_output/linux/altair 23 | upx -9 -k ./build_output/linux/altair 24 | 25 | build_darwin: 26 | GOOS=darwin GOARCH=386 CGO_ENABLED=0 go build -ldflags="-s" -o ./build_output/darwin/altair 27 | upx -9 -k ./build_output/darwin/altair 28 | 29 | build_windows: 30 | GOOS=windows GOARCH=386 CGO_ENABLED=0 go build -ldflags="-s" -o ./build_output/windows/altair 31 | upx -9 -k ./build_output/windows/altair 32 | 33 | build: build_linux build_darwin build_windows 34 | 35 | build_docker: build_docker_latest 36 | docker build -t $(IMAGE):$(VERSION) -f ./Dockerfile . 37 | 38 | build_docker_latest: 39 | docker build -t $(IMAGE):latest -f ./Dockerfile . 40 | 41 | push_docker: push_docker_latest 42 | docker push $(IMAGE):$(VERSION) 43 | 44 | tag_docker_latest: 45 | docker tag $(IMAGE):latest $(IMAGE):latest 46 | 47 | push_docker_latest: 48 | docker push $(IMAGE):latest 49 | 50 | docker-compose-up: 51 | docker-compose --env-file .env up -d 52 | 53 | docker-compose-start: 54 | docker-compose --env-file .env start 55 | 56 | docker-compose-stop: 57 | docker-compose stop 58 | 59 | docker-compose-down: 60 | docker-compose down -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Coverage Status](https://coveralls.io/repos/github/kodefluence/altair/badge.svg?branch=master) ![Go Report Card](https://goreportcard.com/badge/github.com/kodefluence/altair) ![GitHub Issues](https://img.shields.io/github/issues/kodefluence/altair) ![GitHub Forks](https://img.shields.io/github/forks/kodefluence/altair) ![GitHub Stars](https://img.shields.io/github/stars/kodefluence/altair) ![GitHub License](https://img.shields.io/github/license/kodefluence/altair) 2 | 3 | ## Concept 4 | 5 | Altair is an open source API gateway written in Go - designed to be distributed, lightweight, simple, fast, reliable, cross platform, programming language agnostic and robust - by default, for maximum efficiency, reliability and adaptability! 6 | 7 | ![Concept of Altair](https://user-images.githubusercontent.com/20650401/209427000-87ce8199-0a14-4e10-94ae-32f27ceca8b0.png) 8 | 9 | ## Documentation 10 | 11 | ### Get Started 12 | 13 | See [altair.codefluence.org](http://altair.codefluence.org/) 14 | 15 | ### Plugin API Documentation 16 | 17 | [Plugin API Documentation in Postman](https://documenter.getpostman.com/view/3666028/SzmcZJ79?version=latest#b870ae5a-b305-4016-8155-4899af1f26b1) 18 | 19 | ## Contribution 20 | 21 | ### How To Contribute 22 | 23 | Read [CONTRIBUTING.md](https://github.com/kodefluence/altair/blob/master/CONTRIBUTING.md) 24 | 25 | ### Community 26 | 27 | Join our discord channel [here](https://discord.gg/Wps4YrQ3SA). 28 | 29 | ### Dependency 30 | 31 | You can also contribute to Altair's dependency. 32 | 33 | 1. [Aurelia - Randomly generated hash function](https://github.com/kodefluence/aurelia) 34 | 2. [Monorepo - All of your packages in one place](https://github.com/kodefluence/monorepo) 35 | -------------------------------------------------------------------------------- /adapter/app_config.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "github.com/kodefluence/altair/core" 5 | "github.com/kodefluence/altair/entity" 6 | ) 7 | 8 | type ( 9 | appConfig struct{ c entity.AppConfig } 10 | ) 11 | 12 | func AppConfig(c entity.AppConfig) core.AppConfig { 13 | return &appConfig{c: c} 14 | } 15 | 16 | func (a *appConfig) Port() int { return a.c.Port() } 17 | func (a *appConfig) BasicAuthUsername() string { return a.c.BasicAuthUsername() } 18 | func (a *appConfig) BasicAuthPassword() string { return a.c.BasicAuthPassword() } 19 | func (a *appConfig) ProxyHost() string { return a.c.ProxyHost() } 20 | func (a *appConfig) PluginExists(pluginName string) bool { return a.c.PluginExists(pluginName) } 21 | func (a *appConfig) Plugins() []string { return a.c.Plugins() } 22 | func (a *appConfig) Dump() string { return a.c.Dump() } 23 | -------------------------------------------------------------------------------- /adapter/app_config_test.go: -------------------------------------------------------------------------------- 1 | package adapter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kodefluence/altair/adapter" 7 | "github.com/kodefluence/altair/entity" 8 | "github.com/stretchr/testify/assert" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | func TestAppConfigAdapter(t *testing.T) { 13 | appOption := entity.AppConfigOption{ 14 | Port: 1304, 15 | ProxyHost: "www.local.host", 16 | Plugins: []string{"oauth"}, 17 | } 18 | 19 | appOption.Authorization.Username = "altair" 20 | appOption.Authorization.Password = "secret" 21 | 22 | t.Run("Plugins", func(t *testing.T) { 23 | appConfig := adapter.AppConfig(entity.NewAppConfig(appOption)) 24 | 25 | assert.Equal(t, appOption.Plugins, appConfig.Plugins()) 26 | }) 27 | 28 | t.Run("Port", func(t *testing.T) { 29 | appConfig := adapter.AppConfig(entity.NewAppConfig(appOption)) 30 | 31 | assert.Equal(t, appOption.Port, appConfig.Port()) 32 | }) 33 | 34 | t.Run("BasicAuthUsername", func(t *testing.T) { 35 | appConfig := adapter.AppConfig(entity.NewAppConfig(appOption)) 36 | 37 | assert.Equal(t, appOption.Authorization.Username, appConfig.BasicAuthUsername()) 38 | }) 39 | 40 | t.Run("BasicAuthPassword", func(t *testing.T) { 41 | appConfig := adapter.AppConfig(entity.NewAppConfig(appOption)) 42 | 43 | assert.Equal(t, appOption.Authorization.Password, appConfig.BasicAuthPassword()) 44 | }) 45 | 46 | t.Run("ProxyHost", func(t *testing.T) { 47 | appConfig := adapter.AppConfig(entity.NewAppConfig(appOption)) 48 | 49 | assert.Equal(t, appOption.ProxyHost, appConfig.ProxyHost()) 50 | }) 51 | 52 | t.Run("PluginExists", func(t *testing.T) { 53 | t.Run("Not exists", func(t *testing.T) { 54 | appOption := entity.AppConfigOption{ 55 | Port: 1304, 56 | ProxyHost: "www.local.host", 57 | Plugins: []string{}, 58 | } 59 | 60 | appOption.Authorization.Username = "altair" 61 | appOption.Authorization.Password = "secret" 62 | 63 | t.Run("Return false", func(t *testing.T) { 64 | appConfig := adapter.AppConfig(entity.NewAppConfig(appOption)) 65 | 66 | assert.False(t, appConfig.PluginExists("oauth")) 67 | }) 68 | }) 69 | 70 | t.Run("Exists", func(t *testing.T) { 71 | t.Run("Return true", func(t *testing.T) { 72 | appConfig := adapter.AppConfig(entity.NewAppConfig(appOption)) 73 | 74 | assert.True(t, appConfig.PluginExists("oauth")) 75 | }) 76 | }) 77 | }) 78 | 79 | t.Run("Dump", func(t *testing.T) { 80 | appConfig := adapter.AppConfig(entity.NewAppConfig(appOption)) 81 | 82 | content, _ := yaml.Marshal(appOption) 83 | 84 | assert.Equal(t, string(content), appConfig.Dump()) 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /cfg/app.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strconv" 9 | 10 | "github.com/kodefluence/altair/adapter" 11 | "github.com/kodefluence/altair/core" 12 | "github.com/kodefluence/altair/entity" 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | type app struct{} 17 | 18 | type baseAppConfig struct { 19 | Version string `yaml:"version"` 20 | Plugins []string `yaml:"plugins"` 21 | Port string `yaml:"port"` 22 | ProxyHost string `yaml:"proxy_host"` 23 | Authorization struct { 24 | Username string `yaml:"username"` 25 | Password string `yaml:"password"` 26 | } `yaml:"authorization"` 27 | } 28 | 29 | func App() core.AppLoader { 30 | return &app{} 31 | } 32 | 33 | func (a *app) Compile(configPath string) (core.AppConfig, error) { 34 | f, err := os.Open(configPath) 35 | if err != nil { 36 | return nil, err 37 | } 38 | defer f.Close() 39 | 40 | contents, err := io.ReadAll(f) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | compiledContents, err := compileTemplate(contents) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | var config baseAppConfig 51 | 52 | if err := yaml.Unmarshal(compiledContents, &config); err != nil { 53 | return nil, err 54 | } 55 | 56 | switch v := config.Version; v { 57 | case "1.0": 58 | var appConfigOption entity.AppConfigOption 59 | 60 | if config.Authorization.Username == "" { 61 | return nil, errors.New("config authorization `username` cannot be empty") 62 | } 63 | 64 | if config.Authorization.Password == "" { 65 | return nil, errors.New("config authorization `password` cannot be empty") 66 | } 67 | 68 | if config.Port == "" { 69 | appConfigOption.Port = 1304 70 | } else { 71 | port, err := strconv.Atoi(config.Port) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | appConfigOption.Port = port 77 | } 78 | 79 | if config.ProxyHost == "" { 80 | appConfigOption.ProxyHost = "www.local.host" 81 | } else { 82 | appConfigOption.ProxyHost = config.ProxyHost 83 | } 84 | 85 | appConfigOption.Plugins = config.Plugins 86 | appConfigOption.Authorization.Username = config.Authorization.Username 87 | appConfigOption.Authorization.Password = config.Authorization.Password 88 | 89 | return adapter.AppConfig(entity.NewAppConfig(appConfigOption)), nil 90 | default: 91 | return nil, fmt.Errorf("undefined template version: %s for app.yaml", v) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /cfg/app_bearer.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/kodefluence/altair/core" 7 | ) 8 | 9 | type appBearer struct { 10 | config core.AppConfig 11 | downStreamPlugins []core.DownStreamPlugin 12 | appEngine core.APIEngine 13 | metricProvider core.Metric 14 | } 15 | 16 | // TODO: 17 | // Differentiate engine with baseAPIEngine and pluginAPIEngine 18 | // Also create injector for both baseAPIEngine and pluginAPIEngine 19 | func AppBearer(appEngine core.APIEngine, config core.AppConfig) core.AppBearer { 20 | return &appBearer{ 21 | appEngine: appEngine, 22 | config: config, 23 | downStreamPlugins: []core.DownStreamPlugin{}, 24 | } 25 | } 26 | 27 | func (a *appBearer) Config() core.AppConfig { 28 | return a.config 29 | } 30 | 31 | func (a *appBearer) DownStreamPlugins() []core.DownStreamPlugin { 32 | return a.downStreamPlugins 33 | } 34 | 35 | func (a *appBearer) InjectDownStreamPlugin(InjectedDownStreamPlugin core.DownStreamPlugin) { 36 | a.downStreamPlugins = append(a.downStreamPlugins, InjectedDownStreamPlugin) 37 | } 38 | 39 | func (a *appBearer) SetMetricProvider(metricProvider core.Metric) { 40 | if a.metricProvider == nil { 41 | a.metricProvider = metricProvider 42 | } 43 | } 44 | 45 | func (a *appBearer) MetricProvider() (core.Metric, error) { 46 | if a.metricProvider == nil { 47 | return nil, errors.New("Metric provider is empty") 48 | } 49 | 50 | return a.metricProvider, nil 51 | } 52 | -------------------------------------------------------------------------------- /cfg/cfg.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | -------------------------------------------------------------------------------- /cfg/database_bearer.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "github.com/kodefluence/altair/core" 5 | "github.com/kodefluence/monorepo/db" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // ErrDatabasesIsNotExists thrown when database is not exists or not initialized yet in database bearer 10 | var ErrDatabasesIsNotExists = errors.New("Database is not exists") 11 | 12 | type databaseBearer struct { 13 | databases map[string]db.DB 14 | configs map[string]core.DatabaseConfig 15 | } 16 | 17 | // DatabaseBearer handling on retrieval database instance 18 | func DatabaseBearer(databases map[string]db.DB, configs map[string]core.DatabaseConfig) core.DatabaseBearer { 19 | return &databaseBearer{databases: databases, configs: configs} 20 | } 21 | 22 | func (d *databaseBearer) Database(dbName string) (db.DB, core.DatabaseConfig, error) { 23 | db, ok := d.databases[dbName] 24 | if !ok { 25 | return nil, nil, ErrDatabasesIsNotExists 26 | } 27 | 28 | return db, d.configs[dbName], nil 29 | } 30 | -------------------------------------------------------------------------------- /cfg/database_bearer_test.go: -------------------------------------------------------------------------------- 1 | package cfg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | "github.com/kodefluence/altair/cfg" 8 | "github.com/kodefluence/altair/core" 9 | "github.com/kodefluence/altair/entity" 10 | "github.com/kodefluence/monorepo/db" 11 | mockdb "github.com/kodefluence/monorepo/db/mock" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestDatabaseBearer(t *testing.T) { 16 | mockCtrl := gomock.NewController(t) 17 | defer mockCtrl.Finish() 18 | 19 | dbConfig := entity.MYSQLDatabaseConfig{} 20 | 21 | sqldb := mockdb.NewMockDB(mockCtrl) 22 | databases := map[string]db.DB{ 23 | "main_database": sqldb, 24 | } 25 | 26 | configs := map[string]core.DatabaseConfig{ 27 | "main_database": dbConfig, 28 | } 29 | 30 | dbBearer := cfg.DatabaseBearer(databases, configs) 31 | 32 | t.Run("Database", func(t *testing.T) { 33 | t.Run("Given database name", func(t *testing.T) { 34 | t.Run("Database is found", func(t *testing.T) { 35 | t.Run("Return database instance", func(t *testing.T) { 36 | dbName := "main_database" 37 | 38 | loadedSQLDB, loadedDBConfig, err := dbBearer.Database(dbName) 39 | 40 | assert.Nil(t, err) 41 | assert.Equal(t, sqldb, loadedSQLDB) 42 | assert.Equal(t, dbConfig, loadedDBConfig) 43 | }) 44 | }) 45 | 46 | t.Run("Database is not found", func(t *testing.T) { 47 | t.Run("Return error", func(t *testing.T) { 48 | dbName := "this_is_not_exists_databases" 49 | 50 | loadedSQLDB, loadedDBConfig, err := dbBearer.Database(dbName) 51 | 52 | assert.Equal(t, cfg.ErrDatabasesIsNotExists, err) 53 | assert.Nil(t, loadedSQLDB) 54 | assert.Nil(t, loadedDBConfig) 55 | }) 56 | }) 57 | }) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /cfg/helper.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "text/template" 7 | 8 | "github.com/google/uuid" 9 | ) 10 | 11 | func compileTemplate(b []byte) ([]byte, error) { 12 | tpl, err := template.New(uuid.New().String()).Funcs(template.FuncMap{ 13 | "env": envFallback, 14 | }).Parse(string(b)) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | buf := bytes.NewBufferString("") 20 | err = tpl.Execute(buf, nil) 21 | return buf.Bytes(), err 22 | } 23 | 24 | func envFallback(envName string) string { 25 | env := os.Getenv(envName) 26 | if env == "" { 27 | return `""` 28 | } 29 | 30 | return env 31 | } 32 | -------------------------------------------------------------------------------- /cfg/plugin.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/kodefluence/altair/core" 10 | "github.com/kodefluence/altair/entity" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | type plugin struct{} 15 | 16 | func Plugin() core.PluginLoader { 17 | return &plugin{} 18 | } 19 | 20 | func (p *plugin) Compile(pluginPath string) (core.PluginBearer, error) { 21 | var pluginList = map[string]entity.Plugin{} 22 | 23 | listOfBytes, err := p.walkAllFiles(pluginPath) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | for _, b := range listOfBytes { 29 | var plugin entity.Plugin 30 | 31 | compiledBytes, err := compileTemplate(b) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | err = yaml.Unmarshal(compiledBytes, &plugin) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | if _, ok := pluginList[plugin.Plugin]; ok { 42 | return nil, fmt.Errorf("Plugin `%s` already defined", plugin.Plugin) 43 | } 44 | 45 | plugin.Raw = compiledBytes 46 | pluginList[plugin.Plugin] = plugin 47 | } 48 | 49 | return PluginBearer(pluginList), nil 50 | } 51 | 52 | func (p *plugin) walkAllFiles(pluginPath string) ([][]byte, error) { 53 | var files []string 54 | var routeFiles [][]byte 55 | 56 | err := filepath.Walk(pluginPath, func(path string, info os.FileInfo, err error) error { 57 | if err != nil { 58 | return err 59 | } 60 | 61 | if info.IsDir() || (filepath.Ext(path) != ".yaml" && filepath.Ext(path) != ".yml") { 62 | return nil 63 | } 64 | files = append(files, path) 65 | return nil 66 | }) 67 | if err != nil { 68 | return routeFiles, err 69 | } 70 | 71 | for _, path := range files { 72 | f, _ := os.Open(path) 73 | content, _ := io.ReadAll(f) 74 | routeFiles = append(routeFiles, content) 75 | } 76 | 77 | return routeFiles, nil 78 | } 79 | -------------------------------------------------------------------------------- /cfg/plugin_bearer.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/kodefluence/altair/core" 7 | "github.com/kodefluence/altair/entity" 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | var errPluginNotFound = errors.New("Plugin is not exists") 12 | 13 | type pluginBearer struct { 14 | plugins map[string]entity.Plugin 15 | } 16 | 17 | func PluginBearer(plugins map[string]entity.Plugin) core.PluginBearer { 18 | return &pluginBearer{plugins: plugins} 19 | } 20 | 21 | func (p *pluginBearer) Length() int { 22 | return len(p.plugins) 23 | } 24 | 25 | func (p *pluginBearer) ConfigExists(pluginName string) bool { 26 | _, err := p.PluginVersion(pluginName) 27 | return err == nil 28 | } 29 | 30 | func (p *pluginBearer) PluginVersion(pluginName string) (string, error) { 31 | plugin, ok := p.plugins[pluginName] 32 | if !ok { 33 | return "", errPluginNotFound 34 | } 35 | return plugin.Version, nil 36 | } 37 | 38 | func (p *pluginBearer) CompilePlugin(pluginName string, injectedStruct interface{}) error { 39 | if !p.ConfigExists(pluginName) { 40 | return errPluginNotFound 41 | } 42 | 43 | return yaml.Unmarshal(p.plugins[pluginName].Raw, injectedStruct) 44 | } 45 | 46 | func (p *pluginBearer) ForEach(callbackFunc func(pluginName string) error) { 47 | for _, plugin := range p.plugins { 48 | if err := callbackFunc(plugin.Plugin); err != nil { 49 | break 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cfg/plugin_test.go: -------------------------------------------------------------------------------- 1 | package cfg_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kodefluence/altair/cfg" 7 | "github.com/kodefluence/altair/testhelper" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestPlugin(t *testing.T) { 12 | 13 | t.Run("Compile", func(t *testing.T) { 14 | t.Run("Given plugin path", func(t *testing.T) { 15 | t.Run("Normal scenario", func(t *testing.T) { 16 | t.Run("Return map string of entity.Plugin", func(t *testing.T) { 17 | pluginPath := "./normal_scenario_plugin_path/" 18 | 19 | testhelper.GenerateTempTestFiles(pluginPath, PluginConfigNormal1, "oauth.yaml", 0666) 20 | testhelper.GenerateTempTestFiles(pluginPath, PluginConfigNormal2, "cache.yaml", 0666) 21 | 22 | pluginBearer, err := cfg.Plugin().Compile(pluginPath) 23 | 24 | assert.Nil(t, err) 25 | assert.Equal(t, 2, pluginBearer.Length()) 26 | assert.True(t, pluginBearer.ConfigExists("oauth")) 27 | assert.True(t, pluginBearer.ConfigExists("cache")) 28 | 29 | testhelper.RemoveTempTestFiles(pluginPath) 30 | }) 31 | }) 32 | 33 | t.Run("Plugin already defined", func(t *testing.T) { 34 | t.Run("Return error", func(t *testing.T) { 35 | pluginPath := "./plugin_already_defined/" 36 | 37 | testhelper.GenerateTempTestFiles(pluginPath, PluginConfigNormal1, "oauth.yaml", 0666) 38 | testhelper.GenerateTempTestFiles(pluginPath, PluginConfigNormal1, "oauth_2.yaml", 0666) 39 | 40 | pluginBearer, err := cfg.Plugin().Compile(pluginPath) 41 | 42 | assert.NotNil(t, err) 43 | assert.Nil(t, pluginBearer) 44 | 45 | testhelper.RemoveTempTestFiles(pluginPath) 46 | }) 47 | }) 48 | 49 | t.Run("Yaml unmarshal error", func(t *testing.T) { 50 | t.Run("Return error", func(t *testing.T) { 51 | pluginPath := "./plugin_config_yaml_unmarshal_error/" 52 | 53 | testhelper.GenerateTempTestFiles(pluginPath, PluginConfigYamlUnmarshalError, "oauth.yaml", 0666) 54 | 55 | pluginBearer, err := cfg.Plugin().Compile(pluginPath) 56 | 57 | assert.NotNil(t, err) 58 | assert.Nil(t, pluginBearer) 59 | 60 | testhelper.RemoveTempTestFiles(pluginPath) 61 | }) 62 | }) 63 | 64 | t.Run("Template parsing error error", func(t *testing.T) { 65 | t.Run("Return error", func(t *testing.T) { 66 | pluginPath := "./plugin_config_template_parsing_error/" 67 | 68 | testhelper.GenerateTempTestFiles(pluginPath, PluginConfigTemplateParsingError, "oauth.yaml", 0666) 69 | 70 | pluginBearer, err := cfg.Plugin().Compile(pluginPath) 71 | 72 | assert.NotNil(t, err) 73 | assert.Nil(t, pluginBearer) 74 | 75 | testhelper.RemoveTempTestFiles(pluginPath) 76 | }) 77 | }) 78 | 79 | t.Run("Dir is not exists", func(t *testing.T) { 80 | t.Run("Return error", func(t *testing.T) { 81 | pluginPath := "./plugin_config_dir_not_exists/" 82 | 83 | pluginBearer, err := cfg.Plugin().Compile(pluginPath) 84 | 85 | assert.NotNil(t, err) 86 | assert.Nil(t, pluginBearer) 87 | }) 88 | }) 89 | }) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /core/cfg.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kodefluence/monorepo/db" 7 | ) 8 | 9 | type AppLoader interface { 10 | Compile(configPath string) (AppConfig, error) 11 | } 12 | 13 | type DatabaseLoader interface { 14 | Compile(configPath string) (map[string]DatabaseConfig, error) 15 | } 16 | 17 | type PluginLoader interface { 18 | Compile(pluginPath string) (PluginBearer, error) 19 | } 20 | 21 | type PluginBearer interface { 22 | ConfigExists(pluginName string) bool 23 | PluginVersion(pluginName string) (string, error) 24 | CompilePlugin(pluginName string, injectedStruct interface{}) error 25 | ForEach(callbackFunc func(pluginName string) error) 26 | Length() int 27 | } 28 | 29 | type DatabaseConfig interface { 30 | Driver() string 31 | DBHost() string 32 | DBPort() (int, error) 33 | DBUsername() string 34 | DBPassword() string 35 | DBDatabase() string 36 | DBConnectionMaxLifetime() (time.Duration, error) 37 | DBMaxIddleConn() (int, error) 38 | DBMaxOpenConn() (int, error) 39 | Dump() string 40 | } 41 | 42 | type DatabaseBearer interface { 43 | Database(dbName string) (db.DB, DatabaseConfig, error) 44 | } 45 | 46 | type AppConfig interface { 47 | Port() int 48 | BasicAuthUsername() string 49 | BasicAuthPassword() string 50 | ProxyHost() string 51 | PluginExists(pluginName string) bool 52 | Plugins() []string 53 | Dump() string 54 | } 55 | 56 | type MetricConfig interface { 57 | Interface() string 58 | } 59 | 60 | type AppBearer interface { 61 | Config() AppConfig 62 | DownStreamPlugins() []DownStreamPlugin 63 | InjectDownStreamPlugin(InjectedDownStreamPlugin DownStreamPlugin) 64 | 65 | SetMetricProvider(metricProvider Metric) 66 | MetricProvider() (Metric, error) 67 | } 68 | -------------------------------------------------------------------------------- /core/controller.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type Controller interface { 6 | Control(c *gin.Context) 7 | 8 | // Relative path 9 | // /oauth/applications 10 | Path() string 11 | 12 | // GET PUT POST 13 | Method() string 14 | } 15 | 16 | type APIEngine interface { 17 | Handle(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes 18 | } 19 | -------------------------------------------------------------------------------- /core/dispatcher.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type RouteDispatcher interface { 4 | Compiler() RouteCompiler 5 | Generator() RouteGenerator 6 | } 7 | -------------------------------------------------------------------------------- /core/metric.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type Metric interface { 4 | InjectCounter(metricName string, labels ...string) 5 | InjectHistogram(metricName string, labels ...string) 6 | Inc(metricName string, labels map[string]string) error 7 | Observe(metricName string, value float64, labels map[string]string) error 8 | } 9 | -------------------------------------------------------------------------------- /core/migrator.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type Migrator interface { 4 | Up() error 5 | Down() error 6 | Steps(steps int) error 7 | Close() (error, error) 8 | } 9 | -------------------------------------------------------------------------------- /core/plugin.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/kodefluence/altair/entity" 8 | ) 9 | 10 | type DownStreamPlugin interface { 11 | Name() string 12 | Intervene(c *gin.Context, proxyReq *http.Request, r entity.RouterPath) error 13 | } 14 | -------------------------------------------------------------------------------- /core/provider.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "database/sql" 4 | 5 | type PluginProviderDispatcher interface{} 6 | 7 | type MigrationProviderDispatcher interface { 8 | GoMigrate(db *sql.DB, dbConfig DatabaseConfig) MigrationProvider 9 | } 10 | 11 | type MigrationProvider interface { 12 | Migrator() (Migrator, error) 13 | } 14 | -------------------------------------------------------------------------------- /core/routing.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/kodefluence/altair/entity" 6 | ) 7 | 8 | type RouteCompiler interface { 9 | Compile(routesPath string) ([]entity.RouteObject, error) 10 | } 11 | 12 | type RouteGenerator interface { 13 | Generate(engine *gin.Engine, metric Metric, routeObjects []entity.RouteObject, downStreamPlugin []DownStreamPlugin) error 14 | } 15 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | container_name: altair_development 5 | image: library/mysql:5.7 6 | command: --default-authentication-plugin=mysql_native_password 7 | restart: always 8 | environment: 9 | MYSQL_ROOT_PASSWORD: root 10 | MYSQL_DATABASE: ${DATABASE_NAME} 11 | MYSQL_USER: ${DATABASE_USERNAME} 12 | MYSQL_PASSWORD: ${DATABASE_PASSWORD} 13 | ports: 14 | - "127.0.0.1:3306:3306" -------------------------------------------------------------------------------- /entity/app_config.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "gopkg.in/yaml.v2" 5 | ) 6 | 7 | type AppConfigOption struct { 8 | Port int 9 | ProxyHost string 10 | Plugins []string 11 | Authorization struct { 12 | Username string 13 | Password string 14 | } 15 | } 16 | 17 | type AppConfig struct { 18 | plugins []string 19 | pluginMap map[string]bool 20 | port int 21 | proxyHost string 22 | basicAuthUsername string 23 | basicAuthPassword string 24 | } 25 | 26 | func NewAppConfig(option AppConfigOption) AppConfig { 27 | pluginMap := map[string]bool{} 28 | 29 | for _, p := range option.Plugins { 30 | pluginMap[p] = true 31 | } 32 | 33 | return AppConfig{ 34 | plugins: option.Plugins, 35 | pluginMap: pluginMap, 36 | port: option.Port, 37 | proxyHost: option.ProxyHost, 38 | basicAuthPassword: option.Authorization.Password, 39 | basicAuthUsername: option.Authorization.Username, 40 | } 41 | } 42 | 43 | func (a AppConfig) PluginExists(pluginName string) bool { 44 | _, ok := a.pluginMap[pluginName] 45 | return ok 46 | } 47 | 48 | func (a AppConfig) Plugins() []string { 49 | return a.plugins 50 | } 51 | 52 | func (a AppConfig) Port() int { 53 | return a.port 54 | } 55 | 56 | func (a AppConfig) BasicAuthUsername() string { 57 | return a.basicAuthUsername 58 | } 59 | 60 | func (a AppConfig) BasicAuthPassword() string { 61 | return a.basicAuthPassword 62 | } 63 | 64 | func (a AppConfig) ProxyHost() string { 65 | return a.proxyHost 66 | } 67 | 68 | func (a AppConfig) Dump() string { 69 | appConfigOption := AppConfigOption{ 70 | Port: a.port, 71 | Plugins: a.plugins, 72 | ProxyHost: a.proxyHost, 73 | } 74 | 75 | appConfigOption.Authorization.Username = a.basicAuthUsername 76 | appConfigOption.Authorization.Password = a.basicAuthPassword 77 | 78 | encodedContent, _ := yaml.Marshal(appConfigOption) 79 | return string(encodedContent) 80 | } 81 | -------------------------------------------------------------------------------- /entity/app_config_test.go: -------------------------------------------------------------------------------- 1 | package entity_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kodefluence/altair/entity" 7 | "github.com/stretchr/testify/assert" 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | func TestAppConfig(t *testing.T) { 12 | appOption := entity.AppConfigOption{ 13 | Port: 1304, 14 | ProxyHost: "www.local.host", 15 | Plugins: []string{"oauth"}, 16 | } 17 | 18 | appOption.Authorization.Username = "altair" 19 | appOption.Authorization.Password = "secret" 20 | 21 | t.Run("Plugins", func(t *testing.T) { 22 | appConfig := entity.NewAppConfig(appOption) 23 | 24 | assert.Equal(t, appOption.Plugins, appConfig.Plugins()) 25 | }) 26 | 27 | t.Run("Port", func(t *testing.T) { 28 | appConfig := entity.NewAppConfig(appOption) 29 | 30 | assert.Equal(t, appOption.Port, appConfig.Port()) 31 | }) 32 | 33 | t.Run("BasicAuthUsername", func(t *testing.T) { 34 | appConfig := entity.NewAppConfig(appOption) 35 | 36 | assert.Equal(t, appOption.Authorization.Username, appConfig.BasicAuthUsername()) 37 | }) 38 | 39 | t.Run("BasicAuthPassword", func(t *testing.T) { 40 | appConfig := entity.NewAppConfig(appOption) 41 | 42 | assert.Equal(t, appOption.Authorization.Password, appConfig.BasicAuthPassword()) 43 | }) 44 | 45 | t.Run("ProxyHost", func(t *testing.T) { 46 | appConfig := entity.NewAppConfig(appOption) 47 | 48 | assert.Equal(t, appOption.ProxyHost, appConfig.ProxyHost()) 49 | }) 50 | 51 | t.Run("PluginExists", func(t *testing.T) { 52 | t.Run("Not exists", func(t *testing.T) { 53 | appOption := entity.AppConfigOption{ 54 | Port: 1304, 55 | ProxyHost: "www.local.host", 56 | Plugins: []string{}, 57 | } 58 | 59 | appOption.Authorization.Username = "altair" 60 | appOption.Authorization.Password = "secret" 61 | 62 | t.Run("Return false", func(t *testing.T) { 63 | appConfig := entity.NewAppConfig(appOption) 64 | 65 | assert.False(t, appConfig.PluginExists("oauth")) 66 | }) 67 | }) 68 | 69 | t.Run("Exists", func(t *testing.T) { 70 | t.Run("Return true", func(t *testing.T) { 71 | appConfig := entity.NewAppConfig(appOption) 72 | 73 | assert.True(t, appConfig.PluginExists("oauth")) 74 | }) 75 | }) 76 | }) 77 | 78 | t.Run("Dump", func(t *testing.T) { 79 | appConfig := entity.NewAppConfig(appOption) 80 | 81 | content, _ := yaml.Marshal(appOption) 82 | 83 | assert.Equal(t, string(content), appConfig.Dump()) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /entity/db_config_mysql.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | type MYSQLDatabaseConfig struct { 11 | Database string `yaml:"database"` 12 | Username string `yaml:"username"` 13 | Password string `yaml:"password"` 14 | Host string `yaml:"host"` 15 | Port string `yaml:"port"` 16 | ConnectionMaxLifetime string `yaml:"connection_max_lifetime"` 17 | MaxIddleConnection string `yaml:"max_iddle_connection"` 18 | MaxOpenConnection string `yaml:"max_open_connection"` 19 | } 20 | 21 | func (m MYSQLDatabaseConfig) Driver() string { 22 | return "mysql" 23 | } 24 | 25 | func (m MYSQLDatabaseConfig) DBHost() string { 26 | return m.Host 27 | } 28 | 29 | func (m MYSQLDatabaseConfig) DBPort() (int, error) { 30 | return strconv.Atoi(m.Port) 31 | } 32 | 33 | func (m MYSQLDatabaseConfig) DBUsername() string { 34 | return m.Username 35 | } 36 | 37 | func (m MYSQLDatabaseConfig) DBPassword() string { 38 | return m.Password 39 | } 40 | 41 | func (m MYSQLDatabaseConfig) DBDatabase() string { 42 | return m.Database 43 | } 44 | 45 | func (m MYSQLDatabaseConfig) DBConnectionMaxLifetime() (time.Duration, error) { 46 | return time.ParseDuration(m.ConnectionMaxLifetime) 47 | } 48 | 49 | func (m MYSQLDatabaseConfig) DBMaxIddleConn() (int, error) { 50 | return strconv.Atoi(m.MaxIddleConnection) 51 | } 52 | 53 | func (m MYSQLDatabaseConfig) DBMaxOpenConn() (int, error) { 54 | return strconv.Atoi(m.MaxOpenConnection) 55 | } 56 | 57 | func (m MYSQLDatabaseConfig) Dump() string { 58 | encodedContent, _ := yaml.Marshal(m) 59 | return string(encodedContent) 60 | } 61 | -------------------------------------------------------------------------------- /entity/db_config_mysql_test.go: -------------------------------------------------------------------------------- 1 | package entity_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/kodefluence/altair/entity" 8 | "github.com/stretchr/testify/assert" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | func TestMYSQLDatabaseConfig(t *testing.T) { 13 | MYSQLConfig := entity.MYSQLDatabaseConfig{ 14 | Database: "altair_development", 15 | Username: "some_username", 16 | Password: "some_password", 17 | Host: "localhost", 18 | Port: "3306", 19 | ConnectionMaxLifetime: "120s", 20 | MaxIddleConnection: "100", 21 | MaxOpenConnection: "100", 22 | } 23 | 24 | expectedDatabase := "altair_development" 25 | expectedUsername := "some_username" 26 | expectedPassword := "some_password" 27 | expectedHost := "localhost" 28 | expectedPort := 3306 29 | expectedConnMaxLifetime := time.Second * 120 30 | expectedMaxIddleConn := 100 31 | expectedMaxOpenConn := 100 32 | 33 | assert.Equal(t, "mysql", MYSQLConfig.Driver()) 34 | assert.Equal(t, expectedDatabase, MYSQLConfig.DBDatabase()) 35 | assert.Equal(t, expectedUsername, MYSQLConfig.DBUsername()) 36 | assert.Equal(t, expectedPassword, MYSQLConfig.DBPassword()) 37 | assert.Equal(t, expectedHost, MYSQLConfig.DBHost()) 38 | 39 | actualPort, err := MYSQLConfig.DBPort() 40 | assert.Nil(t, err) 41 | assert.Equal(t, expectedPort, actualPort) 42 | 43 | actualConnMaxLifetime, err := MYSQLConfig.DBConnectionMaxLifetime() 44 | assert.Nil(t, err) 45 | assert.Equal(t, expectedConnMaxLifetime, actualConnMaxLifetime) 46 | 47 | actualMaxIddleConn, err := MYSQLConfig.DBMaxIddleConn() 48 | assert.Nil(t, err) 49 | assert.Equal(t, expectedMaxIddleConn, actualMaxIddleConn) 50 | 51 | actualMaxOpenConn, err := MYSQLConfig.DBMaxOpenConn() 52 | assert.Nil(t, err) 53 | assert.Equal(t, expectedMaxOpenConn, actualMaxOpenConn) 54 | 55 | content, _ := yaml.Marshal(MYSQLConfig) 56 | assert.Equal(t, string(content), MYSQLConfig.Dump()) 57 | } 58 | -------------------------------------------------------------------------------- /entity/entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type RouteObject struct { 4 | Name string `yaml:"name"` 5 | Auth string `yaml:"auth"` 6 | Prefix string `yaml:"prefix"` 7 | Host string `yaml:"host"` 8 | Path map[string]RouterPath `yaml:"path"` 9 | } 10 | 11 | type RouterPath struct { 12 | Auth string `yaml:"auth"` 13 | Scope string `yaml:"scope"` 14 | } 15 | 16 | func (r RouterPath) GetAuth() string { 17 | return r.Auth 18 | } 19 | 20 | func (r RouterPath) GetScope() string { 21 | return r.Scope 22 | } 23 | -------------------------------------------------------------------------------- /entity/plugin_config.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Plugin struct { 4 | Plugin string `yaml:"plugin"` 5 | Version string `yaml:"version"` 6 | Raw []byte `yaml:"-"` 7 | } 8 | -------------------------------------------------------------------------------- /entity/router_path_test.go: -------------------------------------------------------------------------------- 1 | package entity_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kodefluence/altair/entity" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestRouterPath(t *testing.T) { 11 | routerPath := entity.RouterPath{ 12 | Auth: "oauth", 13 | Scope: "public", 14 | } 15 | 16 | assert.Equal(t, routerPath.Auth, routerPath.GetAuth()) 17 | assert.Equal(t, routerPath.Scope, routerPath.GetScope()) 18 | } 19 | -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | # App config 2 | APP_ENV=local 3 | ACCESS_TOKEN_TIMEOUT=24h 4 | ACCESS_GRANT_TIMEOUT=2h 5 | 6 | # Database configuration 7 | DATABASE_HOST=localhost 8 | DATABASE_PORT=3306 9 | DATABASE_NAME=altair_development 10 | DATABASE_USERNAME=root 11 | DATABASE_PASSWORD=rootpw 12 | 13 | # Basic auth configuration 14 | BASIC_AUTH_USERNAME=altair 15 | BASIC_AUTH_PASSWORD=eaglethatflyinthebluesky 16 | 17 | PROXY_HOST=www.local.host 18 | 19 | EXAMPLE_USERS_SERVICE_HOST=www.local.host:3000 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kodefluence/altair 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.8.1 7 | github.com/go-sql-driver/mysql v1.6.0 8 | github.com/golang-migrate/migrate/v4 v4.15.2 9 | github.com/golang/mock v1.6.0 10 | github.com/google/uuid v1.3.0 11 | github.com/kodefluence/aurelia v0.0.0-20220717092613-4dd6082f36c5 12 | github.com/kodefluence/monorepo v1.3.1 13 | github.com/pkg/errors v0.9.1 14 | github.com/prometheus/client_golang v1.12.2 15 | github.com/rs/zerolog v1.27.0 16 | github.com/spf13/cobra v1.5.0 17 | github.com/spf13/pflag v1.0.5 18 | github.com/stretchr/testify v1.8.1 19 | github.com/subosito/gotenv v1.4.0 20 | gopkg.in/yaml.v2 v2.4.0 21 | gotest.tools v2.2.0+incompatible 22 | ) 23 | 24 | require ( 25 | github.com/Microsoft/go-winio v0.6.0 // indirect 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 28 | github.com/containerd/containerd v1.6.18 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/gin-contrib/sse v0.1.0 // indirect 31 | github.com/go-playground/locales v0.14.0 // indirect 32 | github.com/go-playground/universal-translator v0.18.0 // indirect 33 | github.com/go-playground/validator/v10 v10.11.0 // indirect 34 | github.com/goccy/go-json v0.9.10 // indirect 35 | github.com/golang/protobuf v1.5.2 // indirect 36 | github.com/google/go-cmp v0.5.6 // indirect 37 | github.com/hashicorp/errwrap v1.1.0 // indirect 38 | github.com/hashicorp/go-multierror v1.1.1 // indirect 39 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/leodido/go-urn v1.2.1 // indirect 42 | github.com/mattn/go-colorable v0.1.12 // indirect 43 | github.com/mattn/go-isatty v0.0.14 // indirect 44 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/opencontainers/image-spec v1.1.0-rc2 // indirect 48 | github.com/pelletier/go-toml/v2 v2.0.2 // indirect 49 | github.com/pmezard/go-difflib v1.0.0 // indirect 50 | github.com/prometheus/client_model v0.2.0 // indirect 51 | github.com/prometheus/common v0.37.0 // indirect 52 | github.com/prometheus/procfs v0.7.3 // indirect 53 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect 54 | github.com/sirupsen/logrus v1.9.0 // indirect 55 | github.com/ugorji/go/codec v1.2.7 // indirect 56 | go.uber.org/atomic v1.9.0 // indirect 57 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect 58 | golang.org/x/net v0.7.0 // indirect 59 | golang.org/x/sys v0.5.0 // indirect 60 | golang.org/x/text v0.7.0 // indirect 61 | golang.org/x/tools v0.4.0 // indirect 62 | google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 // indirect 63 | google.golang.org/grpc v1.51.0 // indirect 64 | google.golang.org/protobuf v1.28.1 // indirect 65 | gopkg.in/yaml.v3 v3.0.1 // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /module/apierror/provider.go: -------------------------------------------------------------------------------- 1 | package apierror 2 | 3 | import "github.com/kodefluence/altair/module/apierror/usecase" 4 | 5 | func Provide() *usecase.ApiError { 6 | return usecase.NewApiError() 7 | } 8 | -------------------------------------------------------------------------------- /module/apierror/usecase/apierror.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kodefluence/monorepo/exception" 8 | "github.com/kodefluence/monorepo/jsonapi" 9 | "github.com/kodefluence/monorepo/kontext" 10 | ) 11 | 12 | type ApiError struct{} 13 | 14 | func NewApiError() *ApiError { 15 | return &ApiError{} 16 | } 17 | 18 | func (*ApiError) InternalServerError(ktx kontext.Context) jsonapi.Option { 19 | err := fmt.Errorf("Something is not right, help us fix this problem. Contribute to https://github.com/kodefluence/altair. Tracing code: '%v'", ktx.GetWithoutCheck("request_id")) 20 | return jsonapi.WithException( 21 | "ERR0500", 22 | http.StatusInternalServerError, 23 | exception.Throw( 24 | err, 25 | exception.WithTitle("Internal server error"), 26 | exception.WithDetail(err.Error()), 27 | exception.WithType(exception.Unexpected), 28 | ), 29 | ) 30 | } 31 | 32 | func (*ApiError) BadRequestError(in string) jsonapi.Option { 33 | err := fmt.Errorf("You've send malformed request in your `%s`", in) 34 | return jsonapi.WithException( 35 | "ERR0400", 36 | http.StatusBadRequest, 37 | exception.Throw( 38 | err, 39 | exception.WithTitle("Bad request error"), 40 | exception.WithDetail(err.Error()), 41 | exception.WithType(exception.BadInput), 42 | ), 43 | ) 44 | } 45 | 46 | func (*ApiError) NotFoundError(ktx kontext.Context, entityType string) jsonapi.Option { 47 | err := fmt.Errorf("Resource of `%s` is not found. Tracing code: `%v`", entityType, ktx.GetWithoutCheck("request_id")) 48 | return jsonapi.WithException( 49 | "ERR0404", 50 | http.StatusNotFound, 51 | exception.Throw( 52 | err, 53 | exception.WithTitle("Not found error"), 54 | exception.WithDetail(err.Error()), 55 | exception.WithType(exception.NotFound), 56 | ), 57 | ) 58 | } 59 | 60 | func (*ApiError) UnauthorizedError() jsonapi.Option { 61 | err := fmt.Errorf("You are unauthorized") 62 | return jsonapi.WithException( 63 | "ERR0401", 64 | http.StatusUnauthorized, 65 | exception.Throw( 66 | err, 67 | exception.WithTitle("Unauthorized error"), 68 | exception.WithDetail(err.Error()), 69 | exception.WithType(exception.Unauthorized), 70 | ), 71 | ) 72 | } 73 | 74 | func (*ApiError) ForbiddenError(ktx kontext.Context, entityType, reason string) jsonapi.Option { 75 | err := fmt.Errorf("Resource of `%s` is forbidden to be accessed, because of: %s. Tracing code: `%v`", entityType, reason, ktx.GetWithoutCheck("request_id")) 76 | return jsonapi.WithException( 77 | "ERR0403", 78 | http.StatusForbidden, 79 | exception.Throw( 80 | err, 81 | exception.WithTitle("Forbidden error"), 82 | exception.WithDetail(err.Error()), 83 | exception.WithType(exception.Forbidden), 84 | ), 85 | ) 86 | } 87 | 88 | func (*ApiError) ValidationError(msg string) jsonapi.Option { 89 | err := fmt.Errorf(fmt.Sprintf("Validation error because of: %s", msg)) 90 | return jsonapi.WithException( 91 | "ERR1442", 92 | http.StatusUnprocessableEntity, 93 | exception.Throw( 94 | err, 95 | exception.WithTitle("Validation error"), 96 | exception.WithDetail(err.Error()), 97 | exception.WithType(exception.BadInput), 98 | ), 99 | ) 100 | } 101 | -------------------------------------------------------------------------------- /module/apierror/usecase/apierror_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/google/uuid" 9 | "github.com/kodefluence/altair/module/apierror/usecase" 10 | "github.com/kodefluence/monorepo/jsonapi" 11 | "github.com/kodefluence/monorepo/kontext" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestApiError(t *testing.T) { 16 | 17 | t.Run("Internal server error", func(t *testing.T) { 18 | ktx := kontext.Fabricate() 19 | uuid := uuid.New() 20 | ktx.Set("request_id", uuid) 21 | 22 | response := jsonapi.BuildResponse(usecase.NewApiError().InternalServerError(ktx)) 23 | 24 | assert.Equal(t, http.StatusInternalServerError, response.HTTPStatus()) 25 | assert.Equal( 26 | t, 27 | fmt.Sprintf("JSONAPI Error:\n[Internal server error] Detail: Something is not right, help us fix this problem. Contribute to https://github.com/kodefluence/altair. Tracing code: '%v', Code: ERR0500\n", uuid), 28 | response.Errors.Error(), 29 | ) 30 | }) 31 | 32 | t.Run("Unauthorized", func(t *testing.T) { 33 | response := jsonapi.BuildResponse(usecase.NewApiError().UnauthorizedError()) 34 | 35 | assert.Equal(t, http.StatusUnauthorized, response.HTTPStatus()) 36 | assert.Equal( 37 | t, 38 | "JSONAPI Error:\n[Unauthorized error] Detail: You are unauthorized, Code: ERR0401\n", 39 | response.Errors.Error(), 40 | ) 41 | }) 42 | 43 | t.Run("Bad request error", func(t *testing.T) { 44 | response := jsonapi.BuildResponse(usecase.NewApiError().BadRequestError("json")) 45 | 46 | assert.Equal(t, http.StatusBadRequest, response.HTTPStatus()) 47 | assert.Equal( 48 | t, 49 | "JSONAPI Error:\n[Bad request error] Detail: You've send malformed request in your `json`, Code: ERR0400\n", 50 | response.Errors.Error(), 51 | ) 52 | }) 53 | 54 | t.Run("Not found error", func(t *testing.T) { 55 | ktx := kontext.Fabricate() 56 | uuid := uuid.New() 57 | ktx.Set("request_id", uuid) 58 | entityType := "oauth_applications" 59 | 60 | response := jsonapi.BuildResponse(usecase.NewApiError().NotFoundError(ktx, entityType)) 61 | 62 | assert.Equal(t, http.StatusNotFound, response.HTTPStatus()) 63 | assert.Equal( 64 | t, 65 | fmt.Sprintf("JSONAPI Error:\n[Not found error] Detail: Resource of `oauth_applications` is not found. Tracing code: `%v`, Code: ERR0404\n", ktx.GetWithoutCheck("request_id")), 66 | response.Errors.Error(), 67 | ) 68 | }) 69 | 70 | t.Run("Forbidden error", func(t *testing.T) { 71 | ktx := kontext.Fabricate() 72 | uuid := uuid.New() 73 | ktx.Set("request_id", uuid) 74 | entityType := "oauth_applications" 75 | reason := "not have access" 76 | 77 | response := jsonapi.BuildResponse(usecase.NewApiError().ForbiddenError(ktx, entityType, reason)) 78 | 79 | assert.Equal(t, http.StatusForbidden, response.HTTPStatus()) 80 | assert.Equal( 81 | t, 82 | fmt.Sprintf("JSONAPI Error:\n[Forbidden error] Detail: Resource of `oauth_applications` is forbidden to be accessed, because of: not have access. Tracing code: `%v`, Code: ERR0403\n", ktx.GetWithoutCheck("request_id")), 83 | response.Errors.Error(), 84 | ) 85 | }) 86 | 87 | t.Run("Validation error", func(t *testing.T) { 88 | response := jsonapi.BuildResponse(usecase.NewApiError().ValidationError("some validation messages goes here")) 89 | 90 | assert.Equal(t, http.StatusUnprocessableEntity, response.HTTPStatus()) 91 | assert.Equal( 92 | t, 93 | "JSONAPI Error:\n[Validation error] Detail: Validation error because of: some validation messages goes here, Code: ERR1442\n", 94 | response.Errors.Error(), 95 | ) 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /module/app/provider.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/kodefluence/altair/module" 5 | "github.com/kodefluence/altair/module/app/usecase" 6 | ) 7 | 8 | func Provide(controller module.Controller) module.App { 9 | return usecase.NewApp(controller) 10 | } 11 | -------------------------------------------------------------------------------- /module/app/usecase/app.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import "github.com/kodefluence/altair/module" 4 | 5 | type App struct { 6 | controller module.Controller 7 | } 8 | 9 | func NewApp(controller module.Controller) *App { 10 | return &App{ 11 | controller: controller, 12 | } 13 | } 14 | 15 | func (a *App) Config() module.Config { return nil } 16 | func (a *App) Controller() module.Controller { return a.controller } 17 | -------------------------------------------------------------------------------- /module/app/usecase/app_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kodefluence/altair/module" 7 | "github.com/kodefluence/altair/module/app/usecase" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type fakeController struct{} 12 | 13 | func (*fakeController) InjectMetric(http ...module.MetricController) {} 14 | func (*fakeController) InjectHTTP(http ...module.HttpController) {} 15 | func (*fakeController) InjectCommand(command ...module.CommandController) {} 16 | func (*fakeController) InjectDownstream(downstream ...module.DownstreamController) {} 17 | func (*fakeController) ListDownstream() []module.DownstreamController { return nil } 18 | func (*fakeController) ListMetric() []module.MetricController { return nil } 19 | 20 | func TestApp(t *testing.T) { 21 | fakeCtrl := &fakeController{} 22 | app := usecase.NewApp(fakeCtrl) 23 | assert.Equal(t, fakeCtrl, app.Controller()) 24 | } 25 | -------------------------------------------------------------------------------- /module/controller/provider.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/kodefluence/altair/module" 5 | "github.com/kodefluence/altair/module/controller/usecase" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func Provide(httpInjector usecase.HttpInjector, apiError module.ApiError, rootCommand *cobra.Command) module.Controller { 10 | return usecase.NewController(httpInjector, apiError, rootCommand) 11 | } 12 | -------------------------------------------------------------------------------- /module/controller/usecase/command.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/module" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | // TODO: Return error when command already registered 9 | func (c *Controller) InjectCommand(commands ...module.CommandController) { 10 | for _, command := range commands { 11 | cmd := &cobra.Command{ 12 | Use: command.Use(), 13 | Short: command.Short(), 14 | Example: command.Example(), 15 | Run: command.Run, 16 | } 17 | command.ModifyFlags(cmd.Flags()) 18 | c.rootCommand.AddCommand(cmd) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /module/controller/usecase/command_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/pflag" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type CommandSuiteTest struct { 12 | *ControllerSuiteTest 13 | } 14 | 15 | func TestCommand(t *testing.T) { 16 | suite.Run(t, &CommandSuiteTest{ 17 | &ControllerSuiteTest{}, 18 | }) 19 | } 20 | 21 | type fakeCommand struct{} 22 | 23 | func (*fakeCommand) Use() string { return "fake" } 24 | func (*fakeCommand) Short() string { return "fake it" } 25 | func (*fakeCommand) Example() string { return "fake it" } 26 | func (*fakeCommand) Run(cmd *cobra.Command, args []string) {} 27 | func (*fakeCommand) ModifyFlags(flags *pflag.FlagSet) {} 28 | 29 | func (suite *HttpSuiteTest) TestInjectCommand() { 30 | suite.controller.InjectCommand(&fakeCommand{}, &fakeCommand{}, &fakeCommand{}, &fakeCommand{}) 31 | } 32 | -------------------------------------------------------------------------------- /module/controller/usecase/controller.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/kodefluence/altair/module" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type HttpInjector func(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes 10 | 11 | type Controller struct { 12 | httpController []module.HttpController 13 | commandController []module.CommandController 14 | downstreamController []module.DownstreamController 15 | metricController []module.MetricController 16 | 17 | httpInjector HttpInjector 18 | apiError module.ApiError 19 | 20 | rootCommand *cobra.Command 21 | commandList map[string]*cobra.Command 22 | } 23 | 24 | func NewController(httpInjector HttpInjector, apiError module.ApiError, rootCommand *cobra.Command) *Controller { 25 | return &Controller{ 26 | httpController: []module.HttpController{}, 27 | commandController: []module.CommandController{}, 28 | downstreamController: []module.DownstreamController{}, 29 | metricController: []module.MetricController{}, 30 | 31 | httpInjector: httpInjector, 32 | apiError: apiError, 33 | rootCommand: rootCommand, 34 | commandList: map[string]*cobra.Command{}, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /module/controller/usecase/controller_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/golang/mock/gomock" 6 | "github.com/kodefluence/altair/module" 7 | "github.com/kodefluence/altair/module/apierror" 8 | "github.com/kodefluence/altair/module/controller/usecase" 9 | "github.com/spf13/cobra" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type ControllerSuiteTest struct { 14 | mockCtrl *gomock.Controller 15 | controller *usecase.Controller 16 | httpInjector usecase.HttpInjector 17 | apierror module.ApiError 18 | apiengine *gin.Engine 19 | 20 | suite.Suite 21 | } 22 | 23 | func (suite *ControllerSuiteTest) SetupTest() { 24 | 25 | suite.mockCtrl = gomock.NewController(suite.T()) 26 | suite.apierror = apierror.Provide() 27 | 28 | gin.SetMode(gin.ReleaseMode) 29 | suite.apiengine = gin.New() 30 | suite.httpInjector = suite.apiengine.Handle 31 | 32 | suite.controller = usecase.NewController(suite.httpInjector, suite.apierror, &cobra.Command{}) 33 | } 34 | 35 | func (suite *ControllerSuiteTest) TearDownTest() { 36 | suite.mockCtrl.Finish() 37 | } 38 | 39 | func (suite *ControllerSuiteTest) Subtest(testcase string, subtest func()) { 40 | suite.SetupTest() 41 | suite.Run(testcase, subtest) 42 | suite.TearDownTest() 43 | } 44 | -------------------------------------------------------------------------------- /module/controller/usecase/downstream.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import "github.com/kodefluence/altair/module" 4 | 5 | func (c *Controller) ListDownstream() []module.DownstreamController { 6 | return c.downstreamController 7 | } 8 | 9 | func (c *Controller) InjectDownstream(downstream ...module.DownstreamController) { 10 | c.downstreamController = append(c.downstreamController, downstream...) 11 | } 12 | -------------------------------------------------------------------------------- /module/controller/usecase/downstream_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/kodefluence/altair/module" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type DownstreamSuiteTest struct { 13 | *ControllerSuiteTest 14 | } 15 | 16 | type fakeDownstream struct{} 17 | 18 | func (*fakeDownstream) Name() string { 19 | return "fake-downstream" 20 | } 21 | 22 | func (*fakeDownstream) Intervene(c *gin.Context, proxyReq *http.Request, r module.RouterPath) error { 23 | return nil 24 | } 25 | 26 | func TestDownstream(t *testing.T) { 27 | suite.Run(t, &DownstreamSuiteTest{ 28 | &ControllerSuiteTest{}, 29 | }) 30 | } 31 | 32 | func (suite *HttpSuiteTest) TestListDownstream() { 33 | suite.controller.InjectDownstream(&fakeDownstream{}, &fakeDownstream{}, &fakeDownstream{}, &fakeDownstream{}) 34 | suite.Assert().Equal(4, len(suite.controller.ListDownstream())) 35 | } 36 | 37 | func (suite *HttpSuiteTest) TestInjectDownstream() { 38 | suite.controller.InjectDownstream(&fakeDownstream{}, &fakeDownstream{}, &fakeDownstream{}, &fakeDownstream{}) 39 | } 40 | -------------------------------------------------------------------------------- /module/controller/usecase/metric.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import "github.com/kodefluence/altair/module" 4 | 5 | func (c *Controller) InjectMetric(metric ...module.MetricController) { 6 | c.metricController = append(c.metricController, metric...) 7 | } 8 | 9 | func (c *Controller) ListMetric() []module.MetricController { 10 | return c.metricController 11 | } 12 | -------------------------------------------------------------------------------- /module/controller/usecase/metric_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type MetricSuiteTest struct { 10 | *ControllerSuiteTest 11 | } 12 | 13 | type fakeMetric struct{} 14 | 15 | func (*fakeMetric) InjectCounter(metricName string, labels ...string) {} 16 | func (*fakeMetric) InjectHistogram(metricName string, labels ...string) {} 17 | func (*fakeMetric) Inc(metricName string, labels map[string]string) error { return nil } 18 | func (*fakeMetric) Observe(metricName string, value float64, labels map[string]string) error { 19 | return nil 20 | } 21 | 22 | func TestMetric(t *testing.T) { 23 | suite.Run(t, &MetricSuiteTest{ 24 | &ControllerSuiteTest{}, 25 | }) 26 | } 27 | 28 | func (suite *HttpSuiteTest) TestListMetric() { 29 | suite.controller.InjectMetric(&fakeMetric{}, &fakeMetric{}, &fakeMetric{}, &fakeMetric{}) 30 | suite.Assert().Equal(4, len(suite.controller.ListMetric())) 31 | } 32 | 33 | func (suite *HttpSuiteTest) TestInjectMetric() { 34 | suite.controller.InjectMetric(&fakeMetric{}, &fakeMetric{}, &fakeMetric{}, &fakeMetric{}) 35 | } 36 | -------------------------------------------------------------------------------- /module/healthcheck/controller/http/health.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/kodefluence/altair/module" 8 | "github.com/kodefluence/monorepo/kontext" 9 | ) 10 | 11 | type HealthController struct{} 12 | 13 | func NewHealthController() module.HttpController { 14 | return &HealthController{} 15 | } 16 | 17 | func (*HealthController) Path() string { 18 | return "/health" 19 | } 20 | 21 | func (*HealthController) Method() string { 22 | return "GET" 23 | } 24 | 25 | func (*HealthController) Control(ktx kontext.Context, c *gin.Context) { 26 | c.JSON(http.StatusOK, gin.H{ 27 | "message": "OK", 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /module/healthcheck/controller/http/health_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/kodefluence/altair/module/apierror" 9 | "github.com/kodefluence/altair/module/controller" 10 | healthcheckHttp "github.com/kodefluence/altair/module/healthcheck/controller/http" 11 | "github.com/kodefluence/altair/testhelper" 12 | "github.com/spf13/cobra" 13 | "gotest.tools/assert" 14 | ) 15 | 16 | func TestHealth(t *testing.T) { 17 | t.Run("Health", func(t *testing.T) { 18 | t.Run("Return OK response", func(t *testing.T) { 19 | gin.SetMode(gin.ReleaseMode) 20 | engine := gin.New() 21 | 22 | controller.Provide(engine.Handle, apierror.Provide(), &cobra.Command{}).InjectHTTP(healthcheckHttp.NewHealthController()) 23 | w := testhelper.PerformRequest(engine, "GET", "/health", nil) 24 | assert.Equal(t, http.StatusOK, w.Code) 25 | }) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /module/healthcheck/loader.go: -------------------------------------------------------------------------------- 1 | package healthcheck 2 | 3 | import ( 4 | "github.com/kodefluence/altair/module" 5 | "github.com/kodefluence/altair/module/healthcheck/controller/http" 6 | ) 7 | 8 | func Load(app module.App) { 9 | app.Controller().InjectHTTP(http.NewHealthController()) 10 | } 11 | -------------------------------------------------------------------------------- /module/interface.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/kodefluence/monorepo/jsonapi" 8 | "github.com/kodefluence/monorepo/kontext" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/pflag" 11 | ) 12 | 13 | // go:generate mockgen -source interface.go -destination mock/interface.go -package mock 14 | 15 | type App interface { 16 | // TODO: Enable config via module package instead of cfg 17 | // Config() Config 18 | Controller() Controller 19 | // TODO: Enable plugin via module package instead of cfg 20 | // Plugin() Plugin 21 | } 22 | type Config interface { 23 | Port() int 24 | BasicAuthUsername() string 25 | BasicAuthPassword() string 26 | ProxyHost() string 27 | Dump() string 28 | } 29 | 30 | type Plugin interface { 31 | List() []string 32 | Exist(plugin string) bool 33 | Plugin(plugin string) 34 | Dump() string 35 | } 36 | 37 | type Controller interface { 38 | InjectMetric(http ...MetricController) 39 | InjectHTTP(http ...HttpController) 40 | InjectCommand(command ...CommandController) 41 | InjectDownstream(downstream ...DownstreamController) 42 | 43 | ListDownstream() []DownstreamController 44 | ListMetric() []MetricController 45 | } 46 | 47 | type MetricController interface { 48 | InjectCounter(metricName string, labels ...string) 49 | InjectHistogram(metricName string, labels ...string) 50 | Inc(metricName string, labels map[string]string) error 51 | Observe(metricName string, value float64, labels map[string]string) error 52 | } 53 | 54 | type HttpController interface { 55 | Control(ktx kontext.Context, c *gin.Context) 56 | 57 | // Relative path 58 | // /oauth/applications 59 | Path() string 60 | 61 | // GET PUT POST 62 | Method() string 63 | } 64 | 65 | type CommandController interface { 66 | Use() string 67 | Short() string 68 | Example() string 69 | Run(cmd *cobra.Command, args []string) 70 | ModifyFlags(flags *pflag.FlagSet) 71 | } 72 | 73 | type DownstreamController interface { 74 | Name() string 75 | Intervene(c *gin.Context, proxyReq *http.Request, r RouterPath) error 76 | } 77 | 78 | type ApiError interface { 79 | InternalServerError(ktx kontext.Context) jsonapi.Option 80 | BadRequestError(in string) jsonapi.Option 81 | NotFoundError(ktx kontext.Context, entityType string) jsonapi.Option 82 | UnauthorizedError() jsonapi.Option 83 | ForbiddenError(ktx kontext.Context, entityType, reason string) jsonapi.Option 84 | ValidationError(msg string) jsonapi.Option 85 | } 86 | 87 | type RouterPath interface { 88 | GetAuth() string 89 | GetScope() string 90 | } 91 | 92 | // type RouterCompiler interface { 93 | // Compile(routesPath string) ([]entity.RouteObject, error) 94 | // } 95 | 96 | // type RouterForwarder interface { 97 | // Generate(engine *gin.Engine, metric MetricController, routeObjects []entity.RouteObject, downStreamPlugin []DownstreamController) error 98 | // } 99 | -------------------------------------------------------------------------------- /module/projectgenerator/controller/command/new.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "embed" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/pflag" 11 | ) 12 | 13 | type New struct { 14 | fs embed.FS 15 | } 16 | 17 | func NewNew(fs embed.FS) *New { 18 | return &New{fs: fs} 19 | } 20 | 21 | func (n *New) Use() string { 22 | return "new" 23 | } 24 | 25 | func (n *New) Short() string { 26 | return "Initiate altair API gateway project" 27 | } 28 | 29 | func (n *New) Example() string { 30 | return "altair new [project-directory]" 31 | } 32 | 33 | func (n *New) Run(cmd *cobra.Command, args []string) { 34 | if len(args) < 1 { 35 | fmt.Println("Invalid number of arguments, expected 1. Example `altair new [project-directory]`.") 36 | return 37 | } 38 | 39 | path := args[0] 40 | 41 | appYml, err := n.fs.ReadFile("template/app.yml") 42 | if err != nil { 43 | fmt.Println(err) 44 | return 45 | } 46 | 47 | databaseYml, err := n.fs.ReadFile("template/database.yml") 48 | if err != nil { 49 | fmt.Println(err) 50 | return 51 | } 52 | 53 | serviceYml, err := n.fs.ReadFile("template/routes/service-a.yml") 54 | if err != nil { 55 | fmt.Println(err) 56 | return 57 | } 58 | 59 | metricYml, err := n.fs.ReadFile("template/plugin/metric.yml") 60 | if err != nil { 61 | fmt.Println(err) 62 | return 63 | } 64 | 65 | oauthYml, err := n.fs.ReadFile("template/plugin/oauth.yml") 66 | if err != nil { 67 | fmt.Println(err) 68 | return 69 | } 70 | 71 | dotEnv, err := n.fs.ReadFile("template/env.sample") 72 | if err != nil { 73 | fmt.Println(err) 74 | return 75 | } 76 | 77 | for _, dir := range []string{path, fmt.Sprintf("%s/routes", path), fmt.Sprintf("%s/config", path), fmt.Sprintf("%s/config/plugin", path)} { 78 | if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) { 79 | fmt.Println("Directory does not exist, creating directory...") 80 | err = os.Mkdir(dir, 0755) 81 | if err != nil { 82 | fmt.Println(err) 83 | return 84 | } 85 | } 86 | } 87 | 88 | err = os.WriteFile(fmt.Sprintf("%s/config/app.yml", path), appYml, 0644) 89 | if err != nil { 90 | fmt.Println(err) 91 | return 92 | } 93 | 94 | err = os.WriteFile(fmt.Sprintf("%s/config/database.yml", path), databaseYml, 0644) 95 | if err != nil { 96 | fmt.Println(err) 97 | return 98 | } 99 | 100 | err = os.WriteFile(fmt.Sprintf("%s/routes/service-a.yml", path), serviceYml, 0644) 101 | if err != nil { 102 | fmt.Println(err) 103 | return 104 | } 105 | 106 | err = os.WriteFile(fmt.Sprintf("%s/config/plugin/metric.yml", path), metricYml, 0644) 107 | if err != nil { 108 | fmt.Println(err) 109 | return 110 | } 111 | 112 | err = os.WriteFile(fmt.Sprintf("%s/config/plugin/oauth.yml", path), oauthYml, 0644) 113 | if err != nil { 114 | fmt.Println(err) 115 | return 116 | } 117 | 118 | err = os.WriteFile(fmt.Sprintf("%s/.env", path), dotEnv, 0644) 119 | if err != nil { 120 | fmt.Println(err) 121 | return 122 | } 123 | 124 | } 125 | 126 | func (n *New) ModifyFlags(flags *pflag.FlagSet) { 127 | 128 | } 129 | -------------------------------------------------------------------------------- /module/projectgenerator/controller/command/new_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/kodefluence/altair/module/app" 8 | "github.com/kodefluence/altair/module/controller" 9 | "github.com/kodefluence/altair/module/projectgenerator" 10 | "github.com/spf13/cobra" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestCommandNew(t *testing.T) { 15 | cmd := &cobra.Command{ 16 | Use: "test", 17 | } 18 | appController := controller.Provide(nil, nil, cmd) 19 | appModule := app.Provide(appController) 20 | projectgenerator.Load(appModule) 21 | 22 | t.Run("Given embed file system, when command is executed then it would create a new folder contain all of altair config", func(t *testing.T) { 23 | os.Args = []string{"test", "new", "kuma"} 24 | err := cmd.Execute() 25 | assert.Nil(t, err) 26 | // testhelper.RemoveTempTestFiles("kuma") 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /module/projectgenerator/loader.go: -------------------------------------------------------------------------------- 1 | package projectgenerator 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/kodefluence/altair/module" 7 | "github.com/kodefluence/altair/module/projectgenerator/controller/command" 8 | ) 9 | 10 | //go:embed template 11 | var fs embed.FS 12 | 13 | func Load(app module.App) { 14 | app.Controller().InjectCommand(command.NewNew(fs)) 15 | } 16 | -------------------------------------------------------------------------------- /module/projectgenerator/template/app.yml: -------------------------------------------------------------------------------- 1 | # This is the sample of app config 2 | # version: string - Template version of app.yaml. 3 | # port: string | integer - Exposed port of application, example: 1304. Default: 1304. 4 | # proxy_host: string - Proxy host header when forwarding the request. Default: www.local.host 5 | # authorization: 6 | # username: string - Basic auth username for managing the application plugins, required. 7 | # password: string - Basic auth password for managing the application plugins, required. 8 | # plugins: - List of active plugins. Available plugins: oauth 9 | 10 | version: "1.0" 11 | port: 1304 12 | proxy_host: {{ env "PROXY_HOST" }} 13 | authorization: 14 | username: altair 15 | password: {{ env "BASIC_AUTH_PASSWORD" }} 16 | plugins: 17 | - oauth 18 | - metric 19 | -------------------------------------------------------------------------------- /module/projectgenerator/template/database.yml: -------------------------------------------------------------------------------- 1 | # This database file represent any instance of database that can be connected and used in any plugin of Altair 2 | # This config is environment agnostic, so it can be used in any environment based on it's environment variables. 3 | # This is the sample of database config: 4 | # : string - The database instance name 5 | # 6 | # driver: string - Database driver used, required. Available: mysql 7 | # database: string - Database name, required. 8 | # username: string - Username of the databases, required. 9 | # password: string - Database passwords, can be left empty. 10 | # host: string - Database host location, required. 11 | # port: string || integer - Database port, if left empty then will be use default value: 3306. 12 | # migration_source: string - Every database instance need a migration path. 13 | # connection_max_lifetime: string - Set max connection lifetime duration. 14 | # max_iddle_connection: string | integer - Set max iddle connection of the databases, if left empty then will be set unlimited. 15 | # max_open_connection: string | integer - Set max open connection of the databases, if left empty then will be set unlimited. 16 | 17 | main_database: 18 | driver: mysql 19 | database: {{ env "DATABASE_NAME" }} 20 | username: {{ env "DATABASE_USERNAME" }} 21 | password: {{ env "DATABASE_PASSWORD" }} 22 | host: {{ env "DATABASE_HOST" }} 23 | port: {{ env "DATABASE_PORT" }} 24 | connection_max_lifetime: 120s 25 | max_iddle_connection: 100 26 | max_open_connection: 100 27 | -------------------------------------------------------------------------------- /module/projectgenerator/template/env.sample: -------------------------------------------------------------------------------- 1 | # App config 2 | APP_ENV=local 3 | ACCESS_TOKEN_TIMEOUT=24h 4 | ACCESS_GRANT_TIMEOUT=2h 5 | 6 | # Database configuration 7 | DATABASE_HOST=localhost 8 | DATABASE_PORT=3306 9 | DATABASE_NAME=altair_development 10 | DATABASE_USERNAME=root 11 | DATABASE_PASSWORD=rootpw 12 | 13 | # Basic auth configuration 14 | BASIC_AUTH_USERNAME=altair 15 | BASIC_AUTH_PASSWORD=eaglethatflyinthebluesky 16 | 17 | PROXY_HOST=www.local.host 18 | 19 | EXAMPLE_USERS_SERVICE_HOST=www.local.host:3000 20 | -------------------------------------------------------------------------------- /module/projectgenerator/template/plugin/metric.yml: -------------------------------------------------------------------------------- 1 | # This is the sample of metric plugin config 2 | # plugin: string - Plugins name 3 | # version: string - Template version of metric plugin config 4 | # config: - List of configuration 5 | # 6 | # provider: string - Metric provider, currently only available for prometheus. 7 | 8 | plugin: metric 9 | version: "1.0" 10 | config: 11 | provider: prometheus 12 | -------------------------------------------------------------------------------- /module/projectgenerator/template/plugin/oauth.yml: -------------------------------------------------------------------------------- 1 | # This is the sample of oauth plugin config 2 | # plugin: string - Plugins name 3 | # version: string - Template version of oauth plugin config 4 | # config: - List of configuration 5 | # 6 | # database: string - Selected database object defined in database.yml 7 | # access_token_timeout: string - Expired duration of the access token 8 | # authorization_code_timeout: string - Expired duration of authorization code 9 | # refresh_token: - Refresh token configuration 10 | # - timeout: string - Expired duration of refresh token 11 | # - active: bool - Toggle to activate refresh token 12 | # refresh_token: - Implicit grant configurationn 13 | # - active: bool - Toggle to activate implicit grant. If this is activated, then access token would not return refresh_token in it's response 14 | 15 | plugin: oauth 16 | version: "1.0" 17 | config: 18 | database: main_database 19 | access_token_timeout: 24h 20 | authorization_code_timeout: 24h 21 | refresh_token: 22 | timeout: 24h 23 | active: true 24 | implicit_grant: 25 | active: false 26 | -------------------------------------------------------------------------------- /module/projectgenerator/template/routes/service-a.yml: -------------------------------------------------------------------------------- 1 | # This is the sample of route config for port forwarding 2 | # name: - The name of the service to be addressed 3 | # auth: - Authentication method. Available: oauth, none. Default: none 4 | # prefix: [format: ^\/.+] - The prefix of the services routes. Example: /users, /products & /stores 5 | # host: - The host of the services to be addressed. Example: localhost:3000 6 | # path: - The list of path in users services 7 | # 8 | # scope: - Plugin dependency: oauth. It will filter the current acces token with defined scope of a routes 9 | # auth: - Plugin dependency: oauth. Each route can have different auth method. If its not set then it will be following the parent configuration.s 10 | # 11 | # /me: {} - Then altair will be forwarding the request into example.com/users/me with any method 12 | 13 | name: users 14 | auth: oauth 15 | prefix: /users 16 | host: {{ env "EXAMPLE_USERS_SERVICE_HOST" }} 17 | path: 18 | /me: 19 | scope: "users" 20 | /profiles/:id: 21 | scope: "users" 22 | auth: "none" 23 | 24 | -------------------------------------------------------------------------------- /module/router/provider.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/kodefluence/altair/module" 5 | "github.com/kodefluence/altair/module/router/usecase" 6 | ) 7 | 8 | func Provide(downStreamPlugin []module.DownstreamController, metric []module.MetricController) (*usecase.Compiler, *usecase.Generator) { 9 | return usecase.NewCompiler(), usecase.NewGenerator(downStreamPlugin, metric) 10 | } 11 | -------------------------------------------------------------------------------- /module/router/usecase/compiler.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/google/uuid" 10 | "github.com/kodefluence/altair/entity" 11 | "github.com/kodefluence/altair/util" 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | type Compiler struct{} 16 | 17 | func NewCompiler() *Compiler { 18 | return &Compiler{} 19 | } 20 | 21 | func (c *Compiler) Compile(routesPath string) ([]entity.RouteObject, error) { 22 | var routeObjects []entity.RouteObject 23 | 24 | listOfBytes, err := c.walkAllFiles(routesPath) 25 | if err != nil { 26 | return routeObjects, err 27 | } 28 | 29 | for _, b := range listOfBytes { 30 | 31 | compiledBytes, err := c.compileTemplate(b) 32 | if err != nil { 33 | return routeObjects, err 34 | } 35 | 36 | var routeObject entity.RouteObject 37 | if err := yaml.Unmarshal(compiledBytes, &routeObject); err != nil { 38 | return routeObjects, err 39 | } 40 | 41 | if routeObject.Auth == "" { 42 | routeObject.Auth = "none" 43 | } 44 | 45 | routeObjects = append(routeObjects, routeObject) 46 | } 47 | 48 | return routeObjects, nil 49 | } 50 | 51 | func (c *Compiler) compileTemplate(b []byte) ([]byte, error) { 52 | tpl, err := template.New(uuid.New().String()).Funcs(template.FuncMap{ 53 | "env": os.Getenv, 54 | }).Parse(string(b)) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | buf := bytes.NewBufferString("") 60 | err = tpl.Execute(buf, nil) 61 | return buf.Bytes(), err 62 | } 63 | 64 | func (c *Compiler) walkAllFiles(routesPath string) ([][]byte, error) { 65 | var files []string 66 | var routeFiles [][]byte 67 | 68 | err := filepath.Walk(routesPath, func(path string, info os.FileInfo, err error) error { 69 | if err != nil { 70 | return err 71 | } 72 | 73 | if info.IsDir() || (filepath.Ext(path) != ".yaml" && filepath.Ext(path) != ".yml") { 74 | return nil 75 | } 76 | files = append(files, path) 77 | return nil 78 | }) 79 | if err != nil { 80 | return routeFiles, err 81 | } 82 | 83 | for _, path := range files { 84 | f, _ := util.ReadFileContent(path) 85 | routeFiles = append(routeFiles, f) 86 | } 87 | 88 | return routeFiles, nil 89 | } 90 | -------------------------------------------------------------------------------- /module/router/usecase/example_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | var ExampleRoutesGracefully = ` 4 | name: users 5 | auth: oauth 6 | prefix: /users 7 | host: {{ env "EXAMPLE_USERS_SERVICE_HOST" }} 8 | path: 9 | /me: {} 10 | /:id: {} 11 | ` 12 | 13 | var ExampleRoutesWithNoAuth = ` 14 | name: users 15 | prefix: /users 16 | host: {{ env "EXAMPLE_USERS_SERVICE_HOST" }} 17 | path: 18 | /me: {} 19 | /:id: {} 20 | ` 21 | 22 | var ExampleRoutesYamlError = ` 23 | name: 1 24 | auth: 2 25 | prefix: /users 26 | host: {{ env "EXAMPLE_USERS_SERVICE_HOST" }} 27 | this one make error 28 | path: 29 | /me: {} 30 | /:id: {} 31 | ` 32 | 33 | var ExampleTemplateParsingError = ` 34 | {{name: 1 35 | auth: 2 36 | prefix: /users 37 | host: {.envasadasd} 38 | path: 39 | /me: {} 40 | /:id: {}}} 41 | ` 42 | var ExampleTemplateExecutionError = ` 43 | name: users 44 | auth: execution_error 45 | prefix: /users 46 | host: {{ .env .x }} 47 | path: 48 | /me: {} 49 | /:id: {} 50 | ` 51 | -------------------------------------------------------------------------------- /module/router/usecase/generator_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/kodefluence/altair/entity" 10 | "github.com/kodefluence/altair/module" 11 | "github.com/kodefluence/altair/module/router/usecase" 12 | "github.com/kodefluence/altair/plugin/metric/module/dummy/controller/metric" 13 | "github.com/kodefluence/altair/testhelper" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func BenchmarkRoute(b *testing.B) { 18 | targetEngine := gin.Default() 19 | 20 | gatewayEngine := gin.New() 21 | 22 | var routeObjects []entity.RouteObject 23 | routeObjects = append( 24 | routeObjects, 25 | entity.RouteObject{ 26 | Auth: "none", 27 | Host: "localhost:5002", 28 | Name: "users", 29 | Prefix: "/users", 30 | Path: map[string]entity.RouterPath{ 31 | "/me": {Auth: "none"}, 32 | "/details/:id": {Auth: "none"}, 33 | }, 34 | }, 35 | ) 36 | 37 | for _, r := range routeObjects { 38 | buildTargetEngine(targetEngine, "GET", r) 39 | } 40 | 41 | var downStreamController []module.DownstreamController 42 | err := usecase.NewGenerator(downStreamController, []module.MetricController{metric.NewDummy()}).Generate(gatewayEngine, routeObjects) 43 | assert.Nil(b, err) 44 | 45 | srvTarget := &http.Server{ 46 | Addr: ":5002", 47 | Handler: targetEngine, 48 | } 49 | 50 | go func() { 51 | _ = srvTarget.ListenAndServe() 52 | }() 53 | 54 | // Given sleep time so the server can boot first 55 | time.Sleep(time.Millisecond * 50) 56 | 57 | for n := 0; n < b.N; n++ { 58 | assert.NotPanics(b, func() { 59 | rec := testhelper.PerformRequest(gatewayEngine, "GET", "/users/me", nil) 60 | assert.Equal(b, http.StatusOK, rec.Result().StatusCode) 61 | }) 62 | } 63 | 64 | _ = srvTarget.Close() 65 | } 66 | -------------------------------------------------------------------------------- /plugin/loader.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/kodefluence/altair/core" 5 | "github.com/kodefluence/altair/module" 6 | "github.com/kodefluence/altair/plugin/metric" 7 | "github.com/kodefluence/altair/plugin/oauth" 8 | ) 9 | 10 | // Load plugin for altair 11 | // TODO: Unit test, open for contributions. 12 | func Load(appBearer core.AppBearer, pluginBearer core.PluginBearer, dbBearer core.DatabaseBearer, apiError module.ApiError, appModule module.App) error { 13 | if err := metric.Load(appBearer, pluginBearer, appModule); err != nil { 14 | return err 15 | } 16 | 17 | if err := oauth.Load(appBearer, dbBearer, pluginBearer, apiError, appModule); err != nil { 18 | return err 19 | } 20 | 21 | return nil 22 | } 23 | 24 | // Load plugin for altair command 25 | // TODO: Unit test, open for contributions. 26 | func LoadCommand(appBearer core.AppBearer, pluginBearer core.PluginBearer, dbBearer core.DatabaseBearer, apiError module.ApiError, appModule module.App) error { 27 | if err := oauth.LoadCommand(appBearer, dbBearer, pluginBearer, apiError, appModule); err != nil { 28 | return err 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /plugin/metric/entity/metric_plugin.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type MetricPlugin struct { 4 | Config struct { 5 | Provider string `yaml:"provider"` 6 | } `yaml:"config"` 7 | } 8 | -------------------------------------------------------------------------------- /plugin/metric/loader.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kodefluence/altair/core" 7 | "github.com/kodefluence/altair/module" 8 | "github.com/kodefluence/altair/plugin/metric/module/dummy" 9 | ) 10 | 11 | func Load(appBearer core.AppBearer, pluginBearer core.PluginBearer, appModule module.App) error { 12 | if !appBearer.Config().PluginExists("metric") { 13 | dummy.Load(appModule) 14 | return nil 15 | } 16 | 17 | version, err := pluginBearer.PluginVersion("metric") 18 | if err != nil { 19 | return err 20 | } 21 | 22 | switch version { 23 | case "1.0": 24 | return version_1_0(appModule, pluginBearer) 25 | default: 26 | return fmt.Errorf("undefined template version: %s for metric plugin", version) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /plugin/metric/module/dummy/controller/metric/dummy.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | // DummyMetric 4 | type DummyMetric struct{} 5 | 6 | // New DummyMetric for metric placeholder 7 | func NewDummy() *DummyMetric { 8 | return &DummyMetric{} 9 | } 10 | 11 | func (p *DummyMetric) InjectCounter(metricName string, labels ...string) {} 12 | func (p *DummyMetric) InjectHistogram(metricName string, labels ...string) {} 13 | func (p *DummyMetric) Inc(metricName string, labels map[string]string) error { return nil } 14 | func (p *DummyMetric) Observe(metricName string, value float64, labels map[string]string) error { 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /plugin/metric/module/dummy/controller/metric/dummy_test.go: -------------------------------------------------------------------------------- 1 | package metric_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kodefluence/altair/plugin/metric/module/dummy/controller/metric" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPrometheus(t *testing.T) { 11 | dummyMetric := metric.NewDummy() 12 | 13 | t.Run("InjectCounter", func(t *testing.T) { 14 | dummyMetric.InjectCounter("testing_metrics", "blablabla") 15 | }) 16 | 17 | t.Run("InjectHistogram", func(t *testing.T) { 18 | dummyMetric.InjectHistogram("testing_metrics", "blablabla") 19 | }) 20 | 21 | t.Run("Inc", func(t *testing.T) { 22 | err := dummyMetric.Inc("testing_metrics", make(map[string]string)) 23 | assert.Nil(t, err) 24 | }) 25 | 26 | t.Run("Observer", func(t *testing.T) { 27 | err := dummyMetric.Observe("testing_metric", 0, make(map[string]string)) 28 | assert.Nil(t, err) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /plugin/metric/module/dummy/loader.go: -------------------------------------------------------------------------------- 1 | package dummy 2 | 3 | import ( 4 | "github.com/kodefluence/altair/module" 5 | "github.com/kodefluence/altair/plugin/metric/module/dummy/controller/metric" 6 | ) 7 | 8 | func Load(appModule module.App) { 9 | appModule.Controller().InjectMetric(metric.NewDummy()) 10 | } 11 | -------------------------------------------------------------------------------- /plugin/metric/module/prometheus/controller/http/prometheus_controller.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/kodefluence/monorepo/kontext" 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | ) 8 | 9 | type PrometheusController struct{} 10 | 11 | func NewPrometheusController() *PrometheusController { 12 | return &PrometheusController{} 13 | } 14 | 15 | func (*PrometheusController) Path() string { 16 | return "/metrics" 17 | } 18 | 19 | func (*PrometheusController) Method() string { 20 | return "GET" 21 | } 22 | 23 | func (*PrometheusController) Control(ktx kontext.Context, c *gin.Context) { 24 | promhttp.Handler().ServeHTTP(c.Writer, c.Request) 25 | } 26 | -------------------------------------------------------------------------------- /plugin/metric/module/prometheus/controller/http/prometheus_controller_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/kodefluence/altair/module/apierror" 9 | "github.com/kodefluence/altair/module/controller" 10 | prometheusHttp "github.com/kodefluence/altair/plugin/metric/module/prometheus/controller/http" 11 | "github.com/kodefluence/altair/testhelper" 12 | "github.com/spf13/cobra" 13 | "gotest.tools/assert" 14 | ) 15 | 16 | func TestPrometheusController(t *testing.T) { 17 | 18 | t.Run("Method", func(t *testing.T) { 19 | assert.Equal(t, "GET", prometheusHttp.NewPrometheusController().Method()) 20 | }) 21 | 22 | t.Run("Path", func(t *testing.T) { 23 | assert.Equal(t, "/metrics", prometheusHttp.NewPrometheusController().Path()) 24 | }) 25 | 26 | t.Run("Control", func(t *testing.T) { 27 | t.Run("Return metrics content", func(t *testing.T) { 28 | apiEngine := gin.Default() 29 | 30 | ctrl := prometheusHttp.NewPrometheusController() 31 | controller.Provide(apiEngine.Handle, apierror.Provide(), &cobra.Command{}).InjectHTTP(ctrl) 32 | w := testhelper.PerformRequest(apiEngine, ctrl.Method(), ctrl.Path(), nil) 33 | 34 | assert.Equal(t, http.StatusOK, w.Code) 35 | }) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /plugin/metric/module/prometheus/controller/metric/prometheus.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | ) 9 | 10 | type PrometheusMetric struct { 11 | counterMetrics map[string]*prometheus.CounterVec 12 | counterMetricLock *sync.Mutex 13 | 14 | histogramMetrics map[string]*prometheus.HistogramVec 15 | histogramMetricLock *sync.Mutex 16 | } 17 | 18 | func NewPrometheus() *PrometheusMetric { 19 | return &PrometheusMetric{ 20 | counterMetrics: map[string]*prometheus.CounterVec{}, 21 | counterMetricLock: &sync.Mutex{}, 22 | 23 | histogramMetrics: map[string]*prometheus.HistogramVec{}, 24 | histogramMetricLock: &sync.Mutex{}, 25 | } 26 | } 27 | 28 | func (p *PrometheusMetric) InjectCounter(metricName string, labels ...string) { 29 | if _, ok := p.counterMetrics[metricName]; ok { 30 | return 31 | } 32 | 33 | p.counterMetricLock.Lock() 34 | counterMetric := prometheus.NewCounterVec(prometheus.CounterOpts{ 35 | Name: metricName, 36 | }, labels) 37 | _ = prometheus.Register(counterMetric) 38 | p.counterMetrics[metricName] = counterMetric 39 | p.counterMetricLock.Unlock() 40 | } 41 | 42 | func (p *PrometheusMetric) InjectHistogram(metricName string, labels ...string) { 43 | if _, ok := p.histogramMetrics[metricName]; ok { 44 | return 45 | } 46 | 47 | p.histogramMetricLock.Lock() 48 | histogramMetric := prometheus.NewHistogramVec(prometheus.HistogramOpts{ 49 | Name: metricName, 50 | }, labels) 51 | _ = prometheus.Register(histogramMetric) 52 | p.histogramMetrics[metricName] = histogramMetric 53 | p.histogramMetricLock.Unlock() 54 | } 55 | 56 | func (p *PrometheusMetric) Inc(metricName string, labels map[string]string) error { 57 | counterMetric, ok := p.counterMetrics[metricName] 58 | if !ok { 59 | return fmt.Errorf("Metric `%s` is not exists", metricName) 60 | } 61 | 62 | counter, err := counterMetric.GetMetricWith(labels) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | counter.Inc() 68 | 69 | return nil 70 | } 71 | 72 | func (p *PrometheusMetric) Observe(metricName string, value float64, labels map[string]string) error { 73 | histogramMetric, ok := p.histogramMetrics[metricName] 74 | if !ok { 75 | return fmt.Errorf("Metric `%s` is not exists", metricName) 76 | } 77 | 78 | observer, err := histogramMetric.GetMetricWith(labels) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | observer.Observe(value) 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /plugin/metric/module/prometheus/controller/metric/prometheus_test.go: -------------------------------------------------------------------------------- 1 | package metric_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kodefluence/altair/plugin/metric/module/prometheus/controller/metric" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPrometheus(t *testing.T) { 11 | promMetric := metric.NewPrometheus() 12 | 13 | t.Run("InjectCounter", func(t *testing.T) { 14 | t.Run("Metric is not exists", func(t *testing.T) { 15 | promMetric.InjectCounter("some_metric") 16 | }) 17 | 18 | t.Run("Metric already exists", func(t *testing.T) { 19 | promMetric.InjectCounter("some_metric") 20 | }) 21 | }) 22 | 23 | t.Run("InjectHistogram", func(t *testing.T) { 24 | t.Run("Metric is not exists", func(t *testing.T) { 25 | promMetric.InjectHistogram("some_metric_histogram") 26 | }) 27 | 28 | t.Run("Metric already exists", func(t *testing.T) { 29 | promMetric.InjectHistogram("some_metric_histogram") 30 | }) 31 | }) 32 | 33 | t.Run("Inc", func(t *testing.T) { 34 | t.Run("Run gracefully", func(t *testing.T) { 35 | t.Run("Return nil", func(t *testing.T) { 36 | assert.Nil(t, promMetric.Inc("some_metric", nil)) 37 | }) 38 | }) 39 | 40 | t.Run("Metric is not exists", func(t *testing.T) { 41 | t.Run("Return error", func(t *testing.T) { 42 | assert.NotNil(t, promMetric.Inc("some_metric_that_not_exists", nil)) 43 | }) 44 | }) 45 | 46 | t.Run("Get metric with labels", func(t *testing.T) { 47 | t.Run("Return error", func(t *testing.T) { 48 | promMetric.InjectCounter("some_metric_with_labels", "label_a", "label_b") 49 | assert.NotNil(t, promMetric.Inc("some_metric_with_labels", nil)) 50 | }) 51 | }) 52 | }) 53 | 54 | t.Run("Observer", func(t *testing.T) { 55 | t.Run("Run gracefully", func(t *testing.T) { 56 | t.Run("Return nil", func(t *testing.T) { 57 | assert.Nil(t, promMetric.Observe("some_metric_histogram", 0, nil)) 58 | }) 59 | }) 60 | 61 | t.Run("Metric is not exists", func(t *testing.T) { 62 | t.Run("Return error", func(t *testing.T) { 63 | assert.NotNil(t, promMetric.Observe("some_metric_histogram_that_not_exists", 0, nil)) 64 | }) 65 | }) 66 | 67 | t.Run("Get metric with labels", func(t *testing.T) { 68 | t.Run("Return error", func(t *testing.T) { 69 | promMetric.InjectHistogram("some_metric_histogram_with_labels", "label_a", "label_b") 70 | assert.NotNil(t, promMetric.Observe("some_metric_histogram_with_labels", 0, nil)) 71 | }) 72 | }) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /plugin/metric/module/prometheus/loader.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "github.com/kodefluence/altair/module" 5 | "github.com/kodefluence/altair/plugin/metric/module/prometheus/controller/http" 6 | "github.com/kodefluence/altair/plugin/metric/module/prometheus/controller/metric" 7 | ) 8 | 9 | func Load(appModule module.App) { 10 | appModule.Controller().InjectMetric(metric.NewPrometheus()) 11 | appModule.Controller().InjectHTTP(http.NewPrometheusController()) 12 | } 13 | -------------------------------------------------------------------------------- /plugin/metric/version_1_0.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kodefluence/altair/core" 7 | "github.com/kodefluence/altair/module" 8 | "github.com/kodefluence/altair/plugin/metric/entity" 9 | "github.com/kodefluence/altair/plugin/metric/module/prometheus" 10 | ) 11 | 12 | func version_1_0(appModule module.App, pluginBearer core.PluginBearer) error { 13 | var metricPlugin entity.MetricPlugin 14 | if err := pluginBearer.CompilePlugin("metric", &metricPlugin); err != nil { 15 | return err 16 | } 17 | 18 | switch metricPlugin.Config.Provider { 19 | case "prometheus": 20 | prometheus.Load(appModule) 21 | default: 22 | return fmt.Errorf("Metric plugin `%s` is currently not supported", metricPlugin.Config.Provider) 23 | } 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /plugin/oauth/entity/insertable.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "time" 4 | 5 | // OauthAccessTokenInsertable use for creating new access token data 6 | type OauthAccessTokenInsertable struct { 7 | OauthApplicationID int 8 | ResourceOwnerID int 9 | Token string 10 | Scopes interface{} 11 | ExpiresIn time.Time 12 | } 13 | 14 | // OauthAccessGrantInsertable use for creating new access grant data 15 | type OauthAccessGrantInsertable struct { 16 | OauthApplicationID int 17 | ResourceOwnerID int 18 | Scopes interface{} 19 | Code string 20 | RedirectURI interface{} 21 | ExpiresIn time.Time 22 | } 23 | 24 | // OauthRefreshTokenInsertable use for creating new refresh token data 25 | type OauthRefreshTokenInsertable struct { 26 | ExpiresIn time.Time 27 | Token string 28 | OauthAccessTokenID int 29 | } 30 | 31 | // OauthApplicationInsertable use for creating new application data 32 | type OauthApplicationInsertable struct { 33 | OwnerID interface{} 34 | OwnerType string 35 | Description interface{} 36 | Scopes interface{} 37 | ClientUID string 38 | ClientSecret string 39 | } 40 | 41 | // OauthApplicationUpdateable use for updating application data 42 | type OauthApplicationUpdateable struct { 43 | Description interface{} 44 | Scopes interface{} 45 | } 46 | -------------------------------------------------------------------------------- /plugin/oauth/entity/json.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "time" 4 | 5 | // 6 | // 7 | // JSON Struct 8 | // 9 | // 10 | 11 | // OauthAccessGrantJSON is a json response from OauthAccessGrant 12 | type OauthAccessGrantJSON struct { 13 | ID *int `json:"id"` 14 | OauthApplicationID *int `json:"oauth_application_id"` 15 | ResourceOwnerID *int `json:"resource_owner_id"` 16 | Code *string `json:"code"` 17 | RedirectURI *string `json:"redirect_uri"` 18 | Scopes *string `json:"scopes"` 19 | ExpiresIn *int `json:"expires_in"` 20 | CreatedAt *time.Time `json:"created_at"` 21 | RevokedAT *time.Time `json:"revoked_at"` 22 | } 23 | 24 | // OauthAccessTokenJSON is a json response from OauthAccessToken 25 | type OauthAccessTokenJSON struct { 26 | ID *int `json:"id"` 27 | OauthApplicationID *int `json:"oauth_application_id"` 28 | ResourceOwnerID *int `json:"resource_owner_id"` 29 | Token *string `json:"token"` 30 | Scopes *string `json:"scopes"` 31 | ExpiresIn *int `json:"expires_in"` 32 | RedirectURI *string `json:"redirect_uri,omitempty"` 33 | CreatedAt *time.Time `json:"created_at"` 34 | RevokedAT *time.Time `json:"revoked_at"` 35 | RefreshToken *OauthRefreshTokenJSON `json:"refresh_token,omitempty"` 36 | } 37 | 38 | type OauthRefreshTokenJSON struct { 39 | Token *string `json:"token"` 40 | ExpiresIn *int `json:"expires_in"` 41 | CreatedAt *time.Time `json:"created_at"` 42 | RevokedAT *time.Time `json:"revoked_at"` 43 | } 44 | 45 | type OauthApplicationJSON struct { 46 | ID *int `json:"id"` 47 | OwnerID *int `json:"owner_id"` 48 | OwnerType *string `json:"owner_type"` 49 | Description *string `json:"description"` 50 | Scopes *string `json:"scopes"` 51 | ClientUID *string `json:"client_uid"` 52 | ClientSecret *string `json:"client_secret"` 53 | RevokedAt *time.Time `json:"revoked_at"` 54 | CreatedAt *time.Time `json:"created_at"` 55 | UpdatedAt *time.Time `json:"updated_at"` 56 | } 57 | 58 | type OauthApplicationUpdateJSON struct { 59 | Description *string `json:"description"` 60 | Scopes *string `json:"scopes"` 61 | } 62 | 63 | type AuthorizationRequestJSON struct { 64 | ResponseType *string `json:"response_type"` 65 | 66 | ResourceOwnerID *int `json:"resource_owner_id"` 67 | 68 | ClientUID *string `json:"client_uid"` 69 | ClientSecret *string `json:"client_secret"` 70 | 71 | RedirectURI *string `json:"redirect_uri"` 72 | Scopes *string `json:"scopes"` 73 | } 74 | 75 | type RevokeAccessTokenRequestJSON struct { 76 | Token *string `json:"token"` 77 | } 78 | 79 | type AccessTokenRequestJSON struct { 80 | GrantType *string `json:"grant_type"` 81 | 82 | ClientUID *string `json:"client_uid"` 83 | ClientSecret *string `json:"client_secret"` 84 | 85 | RefreshToken *string `json:"refresh_token"` 86 | 87 | Code *string `json:"code"` 88 | RedirectURI *string `json:"redirect_uri"` 89 | 90 | Scope *string `json:"scope"` 91 | } 92 | -------------------------------------------------------------------------------- /plugin/oauth/entity/model.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | ) 7 | 8 | // 9 | // 10 | // Model Struct 11 | // 12 | // 13 | 14 | // OauthApplication is a struct returned from interfaces.OauthApplicationModel 15 | type OauthApplication struct { 16 | ID int 17 | OwnerID sql.NullInt64 18 | OwnerType string 19 | Description sql.NullString 20 | Scopes sql.NullString 21 | ClientUID string 22 | ClientSecret string 23 | RevokedAt sql.NullTime 24 | CreatedAt time.Time 25 | UpdatedAt time.Time 26 | } 27 | 28 | // OauthAccessGrant is a struct returned from interfaces.OauthAccessGrantModel 29 | type OauthAccessGrant struct { 30 | ID int 31 | OauthApplicationID int 32 | ResourceOwnerID int 33 | Code string 34 | RedirectURI sql.NullString 35 | Scopes sql.NullString 36 | ExpiresIn time.Time 37 | CreatedAt time.Time 38 | RevokedAT sql.NullTime 39 | } 40 | 41 | // OauthAccessToken is a struct returned from interfaces.OauthAccessTokenModel 42 | type OauthAccessToken struct { 43 | ID int 44 | OauthApplicationID int 45 | ResourceOwnerID int 46 | Token string 47 | Scopes sql.NullString 48 | ExpiresIn time.Time 49 | CreatedAt time.Time 50 | RevokedAT sql.NullTime 51 | } 52 | 53 | // OauthRefreshToken is a struct returned from interfaces.OauthRefreshTokenModel 54 | type OauthRefreshToken struct { 55 | ID int 56 | OauthAccessTokenID int 57 | Token string 58 | ExpiresIn time.Time 59 | CreatedAt time.Time 60 | RevokedAT sql.NullTime 61 | } 62 | -------------------------------------------------------------------------------- /plugin/oauth/entity/oauth_plugin.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "time" 4 | 5 | // OauthPlugin holds all config variables 6 | type OauthPlugin struct { 7 | Config PluginConfig `yaml:"config"` 8 | } 9 | 10 | // PluginConfig holds all config variables for oauth plugin 11 | type PluginConfig struct { 12 | Database string `yaml:"database"` 13 | 14 | AccessTokenTimeoutRaw string `yaml:"access_token_timeout"` 15 | AuthorizationCodeTimeoutRaw string `yaml:"authorization_code_timeout"` 16 | 17 | RefreshToken struct { 18 | Timeout string `yaml:"timeout"` 19 | Active bool `yaml:"active"` 20 | } `yaml:"refresh_token"` 21 | 22 | ImplicitGrant struct { 23 | Active bool `yaml:"active"` 24 | } `yaml:"implicit_grant"` 25 | } 26 | 27 | type RefreshTokenConfig struct { 28 | Timeout time.Duration 29 | Active bool 30 | } 31 | 32 | func (o OauthPlugin) DatabaseInstance() string { 33 | return o.Config.Database 34 | } 35 | 36 | func (o OauthPlugin) AccessTokenTimeout() (time.Duration, error) { 37 | return time.ParseDuration(o.Config.AccessTokenTimeoutRaw) 38 | } 39 | 40 | func (o OauthPlugin) AuthorizationCodeTimeout() (time.Duration, error) { 41 | return time.ParseDuration(o.Config.AuthorizationCodeTimeoutRaw) 42 | } 43 | 44 | func (o OauthPlugin) RefreshTokenTimeout() (time.Duration, error) { 45 | return time.ParseDuration(o.Config.RefreshToken.Timeout) 46 | } 47 | -------------------------------------------------------------------------------- /plugin/oauth/entity/oauth_plugin_test.go: -------------------------------------------------------------------------------- 1 | package entity_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/kodefluence/altair/plugin/oauth/entity" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestOauthPlugin(t *testing.T) { 12 | 13 | oauthPlugin := entity.OauthPlugin{} 14 | oauthPlugin.Config.Database = "main_database" 15 | oauthPlugin.Config.AccessTokenTimeoutRaw = "24h" 16 | oauthPlugin.Config.AuthorizationCodeTimeoutRaw = "24h" 17 | oauthPlugin.Config.RefreshToken.Timeout = "24h" 18 | 19 | t.Run("DatabaseInstance", func(t *testing.T) { 20 | t.Run("Return database instance", func(t *testing.T) { 21 | assert.Equal(t, "main_database", oauthPlugin.DatabaseInstance()) 22 | }) 23 | }) 24 | 25 | t.Run("AccessTokenTimeout", func(t *testing.T) { 26 | t.Run("Right format", func(t *testing.T) { 27 | t.Run("Return duration", func(t *testing.T) { 28 | duration, err := oauthPlugin.AccessTokenTimeout() 29 | assert.Nil(t, err) 30 | assert.Equal(t, time.Hour*24, duration) 31 | }) 32 | }) 33 | 34 | t.Run("Wrong format", func(t *testing.T) { 35 | t.Run("Return error", func(t *testing.T) { 36 | oauthPlugin := entity.OauthPlugin{} 37 | oauthPlugin.Config.Database = "main_database" 38 | oauthPlugin.Config.AccessTokenTimeoutRaw = "abc" 39 | oauthPlugin.Config.AuthorizationCodeTimeoutRaw = "24h" 40 | 41 | _, err := oauthPlugin.AccessTokenTimeout() 42 | assert.NotNil(t, err) 43 | }) 44 | }) 45 | }) 46 | 47 | t.Run("AuthorizationCodeTimeout", func(t *testing.T) { 48 | t.Run("Right format", func(t *testing.T) { 49 | t.Run("Return duration", func(t *testing.T) { 50 | duration, err := oauthPlugin.AuthorizationCodeTimeout() 51 | assert.Nil(t, err) 52 | assert.Equal(t, time.Hour*24, duration) 53 | }) 54 | }) 55 | 56 | t.Run("Wrong format", func(t *testing.T) { 57 | t.Run("Return error", func(t *testing.T) { 58 | oauthPlugin := entity.OauthPlugin{} 59 | oauthPlugin.Config.Database = "main_database" 60 | oauthPlugin.Config.AccessTokenTimeoutRaw = "24h" 61 | oauthPlugin.Config.AuthorizationCodeTimeoutRaw = "abc" 62 | 63 | _, err := oauthPlugin.AuthorizationCodeTimeout() 64 | assert.NotNil(t, err) 65 | }) 66 | }) 67 | }) 68 | 69 | t.Run("RefreshTokenTimeout", func(t *testing.T) { 70 | t.Run("Right format", func(t *testing.T) { 71 | t.Run("Return duration", func(t *testing.T) { 72 | duration, err := oauthPlugin.RefreshTokenTimeout() 73 | assert.Nil(t, err) 74 | assert.Equal(t, time.Hour*24, duration) 75 | }) 76 | }) 77 | 78 | t.Run("Wrong format", func(t *testing.T) { 79 | t.Run("Return error", func(t *testing.T) { 80 | oauthPlugin := entity.OauthPlugin{} 81 | oauthPlugin.Config.RefreshToken.Timeout = "abc" 82 | 83 | _, err := oauthPlugin.RefreshTokenTimeout() 84 | assert.NotNil(t, err) 85 | }) 86 | }) 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /plugin/oauth/loader.go: -------------------------------------------------------------------------------- 1 | package oauth 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kodefluence/altair/core" 7 | "github.com/kodefluence/altair/module" 8 | ) 9 | 10 | // Provide create new oauth plugin provider 11 | func Load(appBearer core.AppBearer, dbBearer core.DatabaseBearer, pluginBearer core.PluginBearer, apiError module.ApiError, appModule module.App) error { 12 | if !appBearer.Config().PluginExists("oauth") { 13 | return nil 14 | } 15 | 16 | version, err := pluginBearer.PluginVersion("oauth") 17 | if err != nil { 18 | return err 19 | } 20 | 21 | switch version { 22 | case "1.0": 23 | return version_1_0(dbBearer, pluginBearer, apiError, appModule) 24 | default: 25 | return fmt.Errorf("undefined template version: %s for metric plugin", version) 26 | } 27 | } 28 | 29 | // Provide create new oauth plugin provider 30 | func LoadCommand(appBearer core.AppBearer, dbBearer core.DatabaseBearer, pluginBearer core.PluginBearer, apiError module.ApiError, appModule module.App) error { 31 | if !appBearer.Config().PluginExists("oauth") { 32 | return nil 33 | } 34 | 35 | version, err := pluginBearer.PluginVersion("oauth") 36 | if err != nil { 37 | return err 38 | } 39 | 40 | switch version { 41 | case "1.0": 42 | return version_1_0_command(dbBearer, pluginBearer, apiError, appModule) 43 | default: 44 | return fmt.Errorf("undefined template version: %s for metric plugin", version) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/controller/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/jsonapi" 6 | "github.com/kodefluence/monorepo/kontext" 7 | ) 8 | 9 | //go:generate mockgen -destination ./mock/mock.go -package mock -source ./command.go 10 | type ApplicationManager interface { 11 | Create(ktx kontext.Context, e entity.OauthApplicationJSON) (entity.OauthApplicationJSON, jsonapi.Errors) 12 | } 13 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/controller/command/create_application.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/kodefluence/altair/plugin/oauth/entity" 8 | "github.com/kodefluence/altair/util" 9 | "github.com/kodefluence/monorepo/kontext" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/pflag" 12 | ) 13 | 14 | // CreateOauthApplication struct of CreateOauthApplication command 15 | type CreateOauthApplication struct { 16 | applicationManager ApplicationManager 17 | 18 | flagOwnerID int 19 | flagOwnerType string 20 | flagScope string 21 | flagDesc string 22 | } 23 | 24 | // NewCreateOauthApplication return struct of CreateOauthApplication 25 | func NewCreateOauthApplication(applicationManager ApplicationManager) *CreateOauthApplication { 26 | return &CreateOauthApplication{ 27 | applicationManager: applicationManager, 28 | } 29 | } 30 | 31 | // Use return name of command 32 | func (m *CreateOauthApplication) Use() string { 33 | return "oauth/application:create" 34 | } 35 | 36 | // Short return short description of command 37 | func (m *CreateOauthApplication) Short() string { 38 | return "Create oauth application" 39 | } 40 | 41 | // Example return example of command 42 | func (m *CreateOauthApplication) Example() string { 43 | return "altair plugin oauth/application:create --owner-id 1 --scope read write --owner-type confidential" 44 | } 45 | 46 | // Run run command 47 | func (m *CreateOauthApplication) Run(cmd *cobra.Command, args []string) { 48 | var ownerID *int 49 | if m.flagOwnerID != 0 { 50 | ownerID = util.ValueToPointer(m.flagOwnerID) 51 | } 52 | 53 | var description *string 54 | if m.flagDesc != "" { 55 | description = util.ValueToPointer(m.flagDesc) 56 | } 57 | 58 | var scope *string 59 | if m.flagScope != "" { 60 | scope = util.ValueToPointer(m.flagScope) 61 | } 62 | 63 | oauthApplicationJSON := entity.OauthApplicationJSON{ 64 | OwnerID: ownerID, 65 | OwnerType: util.ValueToPointer(m.flagOwnerType), 66 | Description: description, 67 | Scopes: scope, 68 | } 69 | 70 | oauthApplicationJSON, err := m.applicationManager.Create(kontext.Fabricate(), oauthApplicationJSON) 71 | if err != nil { 72 | fmt.Println(err.Error()) 73 | return 74 | } 75 | 76 | content, _ := json.Marshal(oauthApplicationJSON) 77 | fmt.Println("Success creating oauth application:", string(content)) 78 | } 79 | 80 | // ModifyFlags modify flags of command 81 | func (m *CreateOauthApplication) ModifyFlags(flags *pflag.FlagSet) { 82 | flags.IntVar(&m.flagOwnerID, "owner-id", 0, "Owner ID, can be nil") 83 | flags.StringVar(&m.flagOwnerType, "owner-type", "", "Owner Type. Enum: confidential, public") 84 | flags.StringVar(&m.flagScope, "scope", "", "Scope of the application, separated by space") 85 | flags.StringVar(&m.flagDesc, "desc", "", "Description of the application") 86 | } 87 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/controller/command/create_application_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | "github.com/kodefluence/altair/module/controller" 8 | "github.com/kodefluence/altair/plugin/oauth/entity" 9 | "github.com/kodefluence/altair/plugin/oauth/module/application/controller/command" 10 | "github.com/kodefluence/altair/plugin/oauth/module/application/controller/command/mock" 11 | "github.com/kodefluence/altair/testhelper" 12 | "github.com/kodefluence/altair/util" 13 | "github.com/kodefluence/monorepo/jsonapi" 14 | "github.com/kodefluence/monorepo/kontext" 15 | "github.com/spf13/cobra" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | func TestCreateApplication(t *testing.T) { 20 | mockCtrl := gomock.NewController(t) 21 | defer mockCtrl.Finish() 22 | 23 | t.Run("Given owner_id and scope, when command is executed then it should create the applications", func(t *testing.T) { 24 | cmd := &cobra.Command{ 25 | Use: "test", 26 | } 27 | 28 | oauthApplicationJSON := entity.OauthApplicationJSON{ 29 | OwnerID: util.ValueToPointer(1), 30 | OwnerType: util.ValueToPointer("confidential"), 31 | Description: util.ValueToPointer("test"), 32 | Scopes: util.ValueToPointer("read write"), 33 | } 34 | 35 | applicationManager := mock.NewMockApplicationManager(mockCtrl) 36 | applicationManager.EXPECT().Create(gomock.Any(), gomock.Any()).DoAndReturn(func(ktx kontext.Context, e entity.OauthApplicationJSON) (entity.OauthApplicationJSON, jsonapi.Errors) { 37 | assert.Equal(t, "test", util.PointerToValue(e.Description)) 38 | assert.Equal(t, 1, util.PointerToValue(e.OwnerID)) 39 | assert.Equal(t, "read write", util.PointerToValue(e.Scopes)) 40 | assert.Equal(t, "confidential", util.PointerToValue(e.OwnerType)) 41 | return oauthApplicationJSON, nil 42 | }) 43 | 44 | command := command.NewCreateOauthApplication(applicationManager) 45 | 46 | appController := controller.Provide(nil, nil, cmd) 47 | appController.InjectCommand(command) 48 | 49 | // Given 50 | cmd.SetArgs([]string{"oauth/application:create", "--owner-id", "1", "--scope", "read write", "--owner-type", "confidential", "--desc", "test"}) 51 | 52 | // When 53 | err := cmd.Execute() 54 | 55 | // Then 56 | assert.Nil(t, err) 57 | }) 58 | 59 | t.Run("Given owner_id and scope, when there is error in command execution then it should print the error", func(t *testing.T) { 60 | cmd := &cobra.Command{ 61 | Use: "test", 62 | } 63 | 64 | applicationManager := mock.NewMockApplicationManager(mockCtrl) 65 | applicationManager.EXPECT().Create(gomock.Any(), gomock.Any()).DoAndReturn(func(ktx kontext.Context, e entity.OauthApplicationJSON) (entity.OauthApplicationJSON, jsonapi.Errors) { 66 | assert.Equal(t, "test", util.PointerToValue(e.Description)) 67 | assert.Equal(t, 1, util.PointerToValue(e.OwnerID)) 68 | assert.Equal(t, "read write", util.PointerToValue(e.Scopes)) 69 | assert.Equal(t, "confidential", util.PointerToValue(e.OwnerType)) 70 | return entity.OauthApplicationJSON{}, testhelper.ErrInternalServer() 71 | }) 72 | 73 | command := command.NewCreateOauthApplication(applicationManager) 74 | 75 | appController := controller.Provide(nil, nil, cmd) 76 | appController.InjectCommand(command) 77 | 78 | // Given 79 | cmd.SetArgs([]string{"oauth/application:create", "--owner-id", "1", "--scope", "read write", "--owner-type", "confidential", "--desc", "test"}) 80 | 81 | // When 82 | err := cmd.Execute() 83 | 84 | // Then 85 | assert.Nil(t, err) 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/controller/command/mock/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./command.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | entity "github.com/kodefluence/altair/plugin/oauth/entity" 12 | jsonapi "github.com/kodefluence/monorepo/jsonapi" 13 | kontext "github.com/kodefluence/monorepo/kontext" 14 | ) 15 | 16 | // MockApplicationManager is a mock of ApplicationManager interface. 17 | type MockApplicationManager struct { 18 | ctrl *gomock.Controller 19 | recorder *MockApplicationManagerMockRecorder 20 | } 21 | 22 | // MockApplicationManagerMockRecorder is the mock recorder for MockApplicationManager. 23 | type MockApplicationManagerMockRecorder struct { 24 | mock *MockApplicationManager 25 | } 26 | 27 | // NewMockApplicationManager creates a new mock instance. 28 | func NewMockApplicationManager(ctrl *gomock.Controller) *MockApplicationManager { 29 | mock := &MockApplicationManager{ctrl: ctrl} 30 | mock.recorder = &MockApplicationManagerMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockApplicationManager) EXPECT() *MockApplicationManagerMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Create mocks base method. 40 | func (m *MockApplicationManager) Create(ktx kontext.Context, e entity.OauthApplicationJSON) (entity.OauthApplicationJSON, jsonapi.Errors) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "Create", ktx, e) 43 | ret0, _ := ret[0].(entity.OauthApplicationJSON) 44 | ret1, _ := ret[1].(jsonapi.Errors) 45 | return ret0, ret1 46 | } 47 | 48 | // Create indicates an expected call of Create. 49 | func (mr *MockApplicationManagerMockRecorder) Create(ktx, e interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockApplicationManager)(nil).Create), ktx, e) 52 | } 53 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/controller/downstream/application_validation.go: -------------------------------------------------------------------------------- 1 | package downstream 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/kodefluence/altair/module" 10 | "github.com/kodefluence/altair/plugin/oauth/entity" 11 | "github.com/kodefluence/altair/util" 12 | "github.com/kodefluence/monorepo/db" 13 | "github.com/kodefluence/monorepo/exception" 14 | "github.com/kodefluence/monorepo/kontext" 15 | ) 16 | 17 | type ApplicationValidation struct { 18 | oauthApplicationRepo OauthApplicationRepository 19 | sqldb db.DB 20 | } 21 | 22 | // NewApplicationValidation create new downstream plugin to check the validity of application uid and application secret given by the client 23 | func NewApplicationValidation(oauthApplicationRepo OauthApplicationRepository, sqldb db.DB) *ApplicationValidation { 24 | return &ApplicationValidation{oauthApplicationRepo: oauthApplicationRepo, sqldb: sqldb} 25 | } 26 | 27 | // Name of downstream plugin 28 | func (o *ApplicationValidation) Name() string { 29 | return "application-validation-plugin" 30 | } 31 | 32 | // Intervene current request to check application_uid and application_secret 33 | func (o *ApplicationValidation) Intervene(c *gin.Context, proxyReq *http.Request, r module.RouterPath) error { 34 | if r.GetAuth() != "oauth_application" { 35 | return nil 36 | } 37 | 38 | applicationJSON := entity.OauthApplicationJSON{} 39 | 40 | if proxyReq.Body == nil { 41 | if clientUID := c.GetHeader("CLIENT_UID"); clientUID != "" { 42 | applicationJSON.ClientUID = util.ValueToPointer(clientUID) 43 | } 44 | 45 | if clientSecret := c.GetHeader("CLIENT_SECRET"); clientSecret != "" { 46 | applicationJSON.ClientSecret = util.ValueToPointer(clientSecret) 47 | } 48 | 49 | } else { 50 | body, err := proxyReq.GetBody() 51 | if err != nil { 52 | c.AbortWithStatus(http.StatusInternalServerError) 53 | return exception.Throw(errors.New("internal server error")) 54 | } 55 | 56 | err = json.NewDecoder(body).Decode(&applicationJSON) 57 | if err != nil { 58 | c.AbortWithStatus(http.StatusBadRequest) 59 | return exception.Throw(errors.New("invalid request"), exception.WithTitle("bad request"), exception.WithDetail("invalid json body"), exception.WithType(exception.BadInput)) 60 | } 61 | } 62 | 63 | if applicationJSON.ClientUID == nil || applicationJSON.ClientSecret == nil { 64 | 65 | c.AbortWithStatus(http.StatusUnprocessableEntity) 66 | 67 | return exception.Throw(errors.New("invalid request"), exception.WithTitle("bad request"), exception.WithDetail("`client_uid` and `client_secret` can't be null"), exception.WithType(exception.BadInput)) 68 | } 69 | 70 | _, exc := o.oauthApplicationRepo.OneByUIDandSecret(kontext.Fabricate(kontext.WithDefaultContext(c)), *applicationJSON.ClientUID, *applicationJSON.ClientSecret, o.sqldb) 71 | if exc != nil { 72 | if exc.Type() == exception.NotFound { 73 | c.AbortWithStatus(http.StatusUnauthorized) 74 | return exception.Throw(exc, exception.WithType(exception.Unauthorized)) 75 | } 76 | 77 | c.AbortWithStatus(http.StatusServiceUnavailable) 78 | return exc 79 | } 80 | 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/controller/downstream/downstream.go: -------------------------------------------------------------------------------- 1 | package downstream 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/db" 6 | "github.com/kodefluence/monorepo/exception" 7 | "github.com/kodefluence/monorepo/kontext" 8 | ) 9 | 10 | //go:generate mockgen -destination ./mock/mock.go -package mock -source ./downstream.go 11 | 12 | type OauthApplicationRepository interface { 13 | OneByUIDandSecret(ktx kontext.Context, clientUID, clientSecret string, tx db.TX) (entity.OauthApplication, exception.Exception) 14 | } 15 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/controller/http/create.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/kodefluence/altair/module" 9 | "github.com/kodefluence/altair/plugin/oauth/entity" 10 | "github.com/kodefluence/monorepo/jsonapi" 11 | "github.com/kodefluence/monorepo/kontext" 12 | "github.com/rs/zerolog" 13 | "github.com/rs/zerolog/log" 14 | ) 15 | 16 | // CreateController control flow of oauth application creation 17 | type CreateController struct { 18 | applicationManager ApplicationManager 19 | apiError module.ApiError 20 | } 21 | 22 | // NewCreate return struct of CreateController 23 | func NewCreate(applicationManager ApplicationManager, apiError module.ApiError) *CreateController { 24 | return &CreateController{ 25 | applicationManager: applicationManager, 26 | apiError: apiError, 27 | } 28 | } 29 | 30 | // Method POST 31 | func (cr *CreateController) Method() string { 32 | return "POST" 33 | } 34 | 35 | // Path /oauth/applications 36 | func (cr *CreateController) Path() string { 37 | return "/oauth/applications" 38 | } 39 | 40 | // Control creation of oauth application 41 | func (cr *CreateController) Control(ktx kontext.Context, c *gin.Context) { 42 | var oauthApplicationJSON entity.OauthApplicationJSON 43 | 44 | rawData, err := c.GetRawData() 45 | if err != nil { 46 | log.Error(). 47 | Err(err). 48 | Stack(). 49 | Interface("request_id", c.Value("request_id")). 50 | Array("tags", zerolog.Arr().Str("controller").Str("application").Str("create").Str("get_raw_data")). 51 | Msg("Cannot get raw data") 52 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(cr.apiError.BadRequestError("request body"))) 53 | return 54 | } 55 | 56 | err = json.Unmarshal(rawData, &oauthApplicationJSON) 57 | if err != nil { 58 | log.Error(). 59 | Err(err). 60 | Stack(). 61 | Interface("request_id", c.Value("request_id")). 62 | Array("tags", zerolog.Arr().Str("controller").Str("application").Str("update").Str("unmarshal")). 63 | Msg("Cannot unmarshal json") 64 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(cr.apiError.BadRequestError("invalid json format"))) 65 | return 66 | } 67 | 68 | result, jsonapiErr := cr.applicationManager.Create(ktx, oauthApplicationJSON) 69 | if jsonapiErr != nil { 70 | c.JSON(jsonapiErr.HTTPStatus(), jsonapi.BuildResponse(jsonapi.WithErrors(jsonapiErr))) 71 | return 72 | } 73 | 74 | c.JSON(http.StatusCreated, jsonapi.BuildResponse(jsonapi.WithData(result))) 75 | } 76 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/controller/http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/jsonapi" 6 | "github.com/kodefluence/monorepo/kontext" 7 | ) 8 | 9 | //go:generate mockgen -destination ./mock/mock.go -package mock -source ./http.go 10 | 11 | type ApplicationManager interface { 12 | Create(ktx kontext.Context, e entity.OauthApplicationJSON) (entity.OauthApplicationJSON, jsonapi.Errors) 13 | List(ktx kontext.Context, offset, limit int) ([]entity.OauthApplicationJSON, int, jsonapi.Errors) 14 | One(ktx kontext.Context, ID int) (entity.OauthApplicationJSON, jsonapi.Errors) 15 | Update(ktx kontext.Context, ID int, e entity.OauthApplicationUpdateJSON) (entity.OauthApplicationJSON, jsonapi.Errors) 16 | } 17 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/controller/http/list.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/kodefluence/altair/module" 9 | "github.com/kodefluence/monorepo/jsonapi" 10 | "github.com/kodefluence/monorepo/kontext" 11 | ) 12 | 13 | // ListController show list of oauth applications 14 | type ListController struct { 15 | applicationManager ApplicationManager 16 | apiError module.ApiError 17 | } 18 | 19 | // NewList return struct of ListController 20 | func NewList(applicationManager ApplicationManager, apiError module.ApiError) *ListController { 21 | return &ListController{ 22 | applicationManager: applicationManager, 23 | apiError: apiError, 24 | } 25 | } 26 | 27 | // Method GET 28 | func (l *ListController) Method() string { 29 | return "GET" 30 | } 31 | 32 | // Path /oauth/applications 33 | func (l *ListController) Path() string { 34 | return "/oauth/applications" 35 | } 36 | 37 | // Control list of oauth applications 38 | func (l *ListController) Control(ktx kontext.Context, c *gin.Context) { 39 | var offset, limit int 40 | var err error 41 | 42 | offset, err = strconv.Atoi(c.DefaultQuery("offset", "0")) 43 | if err != nil { 44 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(l.apiError.BadRequestError("query parameters: offset"))) 45 | return 46 | } 47 | 48 | limit, err = strconv.Atoi(c.DefaultQuery("limit", "10")) 49 | if err != nil { 50 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(l.apiError.BadRequestError("query parameters: limit"))) 51 | return 52 | 53 | } 54 | 55 | oauthApplicationJSON, total, jsonapiErr := l.applicationManager.List(ktx, offset, limit) 56 | if jsonapiErr != nil { 57 | c.JSON(jsonapiErr.HTTPStatus(), jsonapi.BuildResponse(jsonapi.WithErrors(jsonapiErr))) 58 | return 59 | } 60 | 61 | c.JSON(http.StatusOK, jsonapi.BuildResponse( 62 | jsonapi.WithData(oauthApplicationJSON), 63 | jsonapi.WithMeta("offset", offset), 64 | jsonapi.WithMeta("limit", limit), 65 | jsonapi.WithMeta("total", total), 66 | )) 67 | } 68 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/controller/http/one.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/kodefluence/altair/module" 9 | "github.com/kodefluence/monorepo/jsonapi" 10 | "github.com/kodefluence/monorepo/kontext" 11 | "github.com/rs/zerolog" 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | // OneController control flow of showing oauth applications detail 16 | type OneController struct { 17 | applicationManager ApplicationManager 18 | apiError module.ApiError 19 | } 20 | 21 | // NewOne return struct of OneController 22 | func NewOne(applicationManager ApplicationManager, apiError module.ApiError) *OneController { 23 | return &OneController{ 24 | applicationManager: applicationManager, 25 | apiError: apiError, 26 | } 27 | } 28 | 29 | // Method GET 30 | func (o *OneController) Method() string { 31 | return "GET" 32 | } 33 | 34 | // Path /oauth/applications/:id 35 | func (o *OneController) Path() string { 36 | return "/oauth/applications/:id" 37 | } 38 | 39 | // Control find oauth application 40 | func (o *OneController) Control(ktx kontext.Context, c *gin.Context) { 41 | id, err := strconv.Atoi(c.Param("id")) 42 | if err != nil { 43 | log.Error(). 44 | Err(err). 45 | Stack(). 46 | Interface("request_id", c.Value("request_id")). 47 | Array("tags", zerolog.Arr().Str("controller").Str("application").Str("one").Str("strconv")). 48 | Msg("Cannot convert ascii to integer") 49 | 50 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(o.apiError.BadRequestError("url parameters: id is not integer"))) 51 | return 52 | } 53 | 54 | oauthApplicationJSON, jsonAPIErr := o.applicationManager.One(ktx, id) 55 | if jsonAPIErr != nil { 56 | c.JSON(jsonAPIErr.HTTPStatus(), jsonapi.BuildResponse(jsonapi.WithErrors(jsonAPIErr))) 57 | return 58 | } 59 | 60 | c.JSON(http.StatusOK, jsonapi.BuildResponse(jsonapi.WithData(oauthApplicationJSON))) 61 | } 62 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/controller/http/update.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/kodefluence/altair/module" 10 | "github.com/kodefluence/altair/plugin/oauth/entity" 11 | "github.com/kodefluence/monorepo/jsonapi" 12 | "github.com/kodefluence/monorepo/kontext" 13 | "github.com/rs/zerolog" 14 | "github.com/rs/zerolog/log" 15 | ) 16 | 17 | // UpdateController control flow of update oauth application 18 | type UpdateController struct { 19 | applicationManager ApplicationManager 20 | apiError module.ApiError 21 | } 22 | 23 | // NewUpdate create struct of UpdateController 24 | func NewUpdate(applicationManager ApplicationManager, apiError module.ApiError) *UpdateController { 25 | return &UpdateController{ 26 | applicationManager: applicationManager, 27 | apiError: apiError, 28 | } 29 | } 30 | 31 | // Method PUT 32 | func (uc *UpdateController) Method() string { 33 | return "PUT" 34 | } 35 | 36 | // Path /oauth/applications/:id 37 | func (uc *UpdateController) Path() string { 38 | return "/oauth/applications/:id" 39 | } 40 | 41 | // Control update oauth applications 42 | func (uc *UpdateController) Control(ktx kontext.Context, c *gin.Context) { 43 | var oauthApplicationUpdateJSON entity.OauthApplicationUpdateJSON 44 | 45 | rawData, err := c.GetRawData() 46 | if err != nil { 47 | log.Error(). 48 | Err(err). 49 | Stack(). 50 | Interface("request_id", c.Value("request_id")). 51 | Array("tags", zerolog.Arr().Str("controller").Str("application").Str("update").Str("get_raw_data")). 52 | Msg("Cannot get raw data") 53 | 54 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(uc.apiError.BadRequestError("request body"))) 55 | return 56 | } 57 | 58 | err = json.Unmarshal(rawData, &oauthApplicationUpdateJSON) 59 | if err != nil { 60 | log.Error(). 61 | Err(err). 62 | Stack(). 63 | Interface("request_id", c.Value("request_id")). 64 | Array("tags", zerolog.Arr().Str("controller").Str("application").Str("update").Str("unmarshal")). 65 | Msg("Cannot unmarshal json") 66 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(uc.apiError.BadRequestError("invalid json format"))) 67 | return 68 | } 69 | 70 | id, err := strconv.Atoi(c.Param("id")) 71 | if err != nil { 72 | log.Error(). 73 | Err(err). 74 | Stack(). 75 | Interface("request_id", c.Value("request_id")). 76 | Array("tags", zerolog.Arr().Str("controller").Str("application").Str("update").Str("strconv")). 77 | Msg("Cannot convert ascii to integer") 78 | 79 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(uc.apiError.BadRequestError("url parameters: id is not integer"))) 80 | return 81 | } 82 | 83 | result, jsonapiErr := uc.applicationManager.Update(ktx, id, oauthApplicationUpdateJSON) 84 | if jsonapiErr != nil { 85 | c.JSON(jsonapiErr.HTTPStatus(), jsonapi.BuildResponse(jsonapi.WithErrors(jsonapiErr))) 86 | return 87 | } 88 | 89 | c.JSON(http.StatusOK, jsonapi.BuildResponse(jsonapi.WithData(result))) 90 | } 91 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/loader.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/kodefluence/altair/module" 5 | "github.com/kodefluence/altair/plugin/oauth/module/application/controller/command" 6 | "github.com/kodefluence/altair/plugin/oauth/module/application/controller/downstream" 7 | "github.com/kodefluence/altair/plugin/oauth/module/application/controller/http" 8 | "github.com/kodefluence/altair/plugin/oauth/module/application/usecase" 9 | "github.com/kodefluence/monorepo/db" 10 | ) 11 | 12 | func Load( 13 | appModule module.App, 14 | sqldb db.DB, 15 | oauthApplicationRepo usecase.OauthApplicationRepository, 16 | formatter usecase.Formatter, 17 | apiError module.ApiError, 18 | ) { 19 | applicationManager := usecase.NewApplicationManager(sqldb, oauthApplicationRepo, apiError, formatter) 20 | appModule.Controller().InjectHTTP( 21 | http.NewCreate(applicationManager, apiError), 22 | http.NewOne(applicationManager, apiError), 23 | http.NewList(applicationManager, apiError), 24 | http.NewUpdate(applicationManager, apiError), 25 | ) 26 | appModule.Controller().InjectDownstream(downstream.NewApplicationValidation(oauthApplicationRepo, sqldb)) 27 | } 28 | 29 | func LoadCommand( 30 | appModule module.App, 31 | sqldb db.DB, 32 | oauthApplicationRepo usecase.OauthApplicationRepository, 33 | formatter usecase.Formatter, 34 | apiError module.ApiError, 35 | ) { 36 | applicationManager := usecase.NewApplicationManager(sqldb, oauthApplicationRepo, apiError, formatter) 37 | appModule.Controller().InjectCommand(command.NewCreateOauthApplication(applicationManager)) 38 | } 39 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/usecase/application_manager.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/module" 5 | "github.com/kodefluence/altair/plugin/oauth/entity" 6 | "github.com/kodefluence/monorepo/db" 7 | "github.com/kodefluence/monorepo/exception" 8 | "github.com/kodefluence/monorepo/kontext" 9 | ) 10 | 11 | //go:generate mockgen -destination ./mock/mock.go -package mock -source ./application_manager.go 12 | 13 | type OauthApplicationRepository interface { 14 | Paginate(ktx kontext.Context, offset, limit int, tx db.TX) ([]entity.OauthApplication, exception.Exception) 15 | Count(ktx kontext.Context, tx db.TX) (int, exception.Exception) 16 | One(ktx kontext.Context, ID int, tx db.TX) (entity.OauthApplication, exception.Exception) 17 | OneByUIDandSecret(ktx kontext.Context, clientUID, clientSecret string, tx db.TX) (entity.OauthApplication, exception.Exception) 18 | Create(ktx kontext.Context, data entity.OauthApplicationInsertable, tx db.TX) (int, exception.Exception) 19 | Update(ktx kontext.Context, ID int, data entity.OauthApplicationUpdateable, tx db.TX) exception.Exception 20 | } 21 | 22 | type Formatter interface { 23 | ApplicationList(applications []entity.OauthApplication) []entity.OauthApplicationJSON 24 | Application(application entity.OauthApplication) entity.OauthApplicationJSON 25 | OauthApplicationInsertable(r entity.OauthApplicationJSON) entity.OauthApplicationInsertable 26 | } 27 | 28 | // ApplicationManager manage all oauth_applications CRUD 29 | type ApplicationManager struct { 30 | sqldb db.DB 31 | oauthApplicationRepo OauthApplicationRepository 32 | apiError module.ApiError 33 | formatter Formatter 34 | } 35 | 36 | // NewApplicationManager manage all oauth application data business logic 37 | func NewApplicationManager(sqldb db.DB, oauthApplicationRepo OauthApplicationRepository, apiError module.ApiError, formatter Formatter) *ApplicationManager { 38 | return &ApplicationManager{ 39 | sqldb: sqldb, 40 | oauthApplicationRepo: oauthApplicationRepo, 41 | apiError: apiError, 42 | formatter: formatter, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/usecase/create.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/jsonapi" 6 | "github.com/kodefluence/monorepo/kontext" 7 | "github.com/rs/zerolog" 8 | "github.com/rs/zerolog/log" 9 | ) 10 | 11 | // Create oauth application 12 | func (am *ApplicationManager) Create(ktx kontext.Context, e entity.OauthApplicationJSON) (entity.OauthApplicationJSON, jsonapi.Errors) { 13 | if err := am.ValidateApplication(e); err != nil { 14 | log.Error(). 15 | Err(err). 16 | Stack(). 17 | Interface("data", e). 18 | Array("tags", zerolog.Arr().Str("service").Str("application_manager").Str("create").Str("validate_application")). 19 | Msg("Got validation error from oauth application validator") 20 | return entity.OauthApplicationJSON{}, err 21 | } 22 | 23 | id, err := am.oauthApplicationRepo.Create(ktx, am.formatter.OauthApplicationInsertable(e), am.sqldb) 24 | if err != nil { 25 | log.Error(). 26 | Err(err). 27 | Stack(). 28 | Interface("data", e). 29 | Array("tags", zerolog.Arr().Str("service").Str("application_manager").Str("create").Str("model_create")). 30 | Msg("Error when creating oauth application data") 31 | 32 | return entity.OauthApplicationJSON{}, 33 | jsonapi.BuildResponse(am.apiError.InternalServerError(ktx)).Errors 34 | } 35 | 36 | return am.One(ktx, id) 37 | } 38 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/usecase/list.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/jsonapi" 6 | "github.com/kodefluence/monorepo/kontext" 7 | "github.com/rs/zerolog" 8 | "github.com/rs/zerolog/log" 9 | ) 10 | 11 | // List of oauth applications 12 | func (am *ApplicationManager) List(ktx kontext.Context, offset, limit int) ([]entity.OauthApplicationJSON, int, jsonapi.Errors) { 13 | oauthApplications, err := am.oauthApplicationRepo.Paginate(ktx, offset, limit, am.sqldb) 14 | if err != nil { 15 | log.Error(). 16 | Err(err). 17 | Stack(). 18 | Interface("request_id", ktx.GetWithoutCheck("request_id")). 19 | Int("offset", offset). 20 | Int("limit", limit). 21 | Array("tags", zerolog.Arr().Str("service").Str("application_manager").Str("list").Str("paginate")). 22 | Msg("Error paginating oauth applications") 23 | 24 | return []entity.OauthApplicationJSON(nil), 0, jsonapi.BuildResponse( 25 | am.apiError.InternalServerError(ktx), 26 | ).Errors 27 | } 28 | 29 | total, err := am.oauthApplicationRepo.Count(ktx, am.sqldb) 30 | if err != nil { 31 | log.Error(). 32 | Err(err). 33 | Stack(). 34 | Interface("request_id", ktx.GetWithoutCheck("request_id")). 35 | Int("offset", offset). 36 | Int("limit", limit). 37 | Array("tags", zerolog.Arr().Str("service").Str("application_manager").Str("list").Str("count")). 38 | Msg("Error count total of oauth applications") 39 | 40 | return []entity.OauthApplicationJSON(nil), 0, jsonapi.BuildResponse( 41 | am.apiError.InternalServerError(ktx), 42 | ).Errors 43 | } 44 | 45 | return am.formatter.ApplicationList(oauthApplications), total, nil 46 | } 47 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/usecase/one.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/exception" 6 | "github.com/kodefluence/monorepo/jsonapi" 7 | "github.com/kodefluence/monorepo/kontext" 8 | "github.com/rs/zerolog" 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | // One retrieve oauth application 13 | func (am *ApplicationManager) One(ktx kontext.Context, ID int) (entity.OauthApplicationJSON, jsonapi.Errors) { 14 | oauthApplication, err := am.oauthApplicationRepo.One(ktx, ID, am.sqldb) 15 | if err != nil { 16 | if err.Type() == exception.NotFound { 17 | 18 | return entity.OauthApplicationJSON{}, 19 | jsonapi.BuildResponse(am.apiError.NotFoundError(ktx, "oauth_application")).Errors 20 | } 21 | 22 | log.Error(). 23 | Err(err). 24 | Stack(). 25 | Int("id", ID). 26 | Array("tags", zerolog.Arr().Str("service").Str("application_manager").Str("one").Str("model_one")). 27 | Msg("Error when fetching single oauth application") 28 | 29 | return entity.OauthApplicationJSON{}, 30 | jsonapi.BuildResponse(am.apiError.InternalServerError(ktx)).Errors 31 | } 32 | 33 | formattedResult := am.formatter.Application(oauthApplication) 34 | return formattedResult, nil 35 | } 36 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/usecase/one_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | "github.com/kodefluence/altair/module/apierror" 10 | "github.com/kodefluence/altair/plugin/oauth/entity" 11 | "github.com/kodefluence/altair/plugin/oauth/module/application/usecase" 12 | "github.com/kodefluence/altair/plugin/oauth/module/application/usecase/mock" 13 | mockdb "github.com/kodefluence/monorepo/db/mock" 14 | "github.com/kodefluence/monorepo/exception" 15 | "github.com/kodefluence/monorepo/kontext" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | func TestOne(t *testing.T) { 20 | mockCtrl := gomock.NewController(t) 21 | defer mockCtrl.Finish() 22 | 23 | sqldb := mockdb.NewMockDB(mockCtrl) 24 | t.Run("One", func(t *testing.T) { 25 | t.Run("Given context and oauth application id", func(t *testing.T) { 26 | t.Run("Return oauth application data", func(t *testing.T) { 27 | oauthApplication := entity.OauthApplication{ 28 | ID: 1, 29 | } 30 | 31 | ktx := kontext.Fabricate() 32 | formatterUsecase := newFormatter() 33 | apierrorUsecase := apierror.Provide() 34 | oauthApplicationRepository := mock.NewMockOauthApplicationRepository(mockCtrl) 35 | 36 | oauthApplicationRepository.EXPECT().One(ktx, oauthApplication.ID, sqldb).Return(oauthApplication, nil) 37 | 38 | applicationManager := usecase.NewApplicationManager(sqldb, oauthApplicationRepository, apierrorUsecase, formatterUsecase) 39 | oauthApplicationJSON, err := applicationManager.One(ktx, oauthApplication.ID) 40 | assert.Nil(t, err) 41 | assert.Equal(t, formatterUsecase.Application(oauthApplication), oauthApplicationJSON) 42 | }) 43 | 44 | t.Run("Oauth application is not found", func(t *testing.T) { 45 | t.Run("Return 404", func(t *testing.T) { 46 | ktx := kontext.Fabricate() 47 | formatterUsecase := newFormatter() 48 | apierrorUsecase := apierror.Provide() 49 | oauthApplicationRepository := mock.NewMockOauthApplicationRepository(mockCtrl) 50 | 51 | oauthApplicationRepository.EXPECT().One(ktx, 1, sqldb).Return(entity.OauthApplication{}, exception.Throw(errors.New("not found"), exception.WithType(exception.NotFound))) 52 | 53 | applicationManager := usecase.NewApplicationManager(sqldb, oauthApplicationRepository, apierrorUsecase, formatterUsecase) 54 | _, err := applicationManager.One(ktx, 1) 55 | assert.NotNil(t, err) 56 | assert.Equal(t, http.StatusNotFound, err.HTTPStatus()) 57 | }) 58 | }) 59 | 60 | t.Run("Unexpected error", func(t *testing.T) { 61 | t.Run("Return internal server error", func(t *testing.T) { 62 | ktx := kontext.Fabricate() 63 | formatterUsecase := newFormatter() 64 | apierrorUsecase := apierror.Provide() 65 | oauthApplicationRepository := mock.NewMockOauthApplicationRepository(mockCtrl) 66 | 67 | oauthApplicationRepository.EXPECT().One(ktx, 1, sqldb).Return(entity.OauthApplication{}, exception.Throw(errors.New("unexpected error"), exception.WithType(exception.Unexpected))) 68 | 69 | applicationManager := usecase.NewApplicationManager(sqldb, oauthApplicationRepository, apierrorUsecase, formatterUsecase) 70 | _, err := applicationManager.One(ktx, 1) 71 | assert.NotNil(t, err) 72 | assert.Equal(t, http.StatusInternalServerError, err.HTTPStatus()) 73 | }) 74 | }) 75 | }) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/usecase/update.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/jsonapi" 6 | "github.com/kodefluence/monorepo/kontext" 7 | "github.com/rs/zerolog" 8 | "github.com/rs/zerolog/log" 9 | ) 10 | 11 | // Update oauth application 12 | func (am *ApplicationManager) Update(ktx kontext.Context, ID int, e entity.OauthApplicationUpdateJSON) (entity.OauthApplicationJSON, jsonapi.Errors) { 13 | err := am.oauthApplicationRepo.Update(ktx, ID, entity.OauthApplicationUpdateable{ 14 | Description: e.Description, 15 | Scopes: e.Scopes, 16 | }, am.sqldb) 17 | if err != nil { 18 | log.Error(). 19 | Err(err). 20 | Stack(). 21 | Interface("data", e). 22 | Array("tags", zerolog.Arr().Str("service").Str("application_manager").Str("update").Str("model_update")). 23 | Msg("Error when updating oauth application data") 24 | 25 | return entity.OauthApplicationJSON{}, 26 | jsonapi.BuildResponse(am.apiError.InternalServerError(ktx)).Errors 27 | } 28 | 29 | return am.One(ktx, ID) 30 | } 31 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/usecase/update_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | "github.com/kodefluence/altair/module/apierror" 10 | "github.com/kodefluence/altair/plugin/oauth/entity" 11 | "github.com/kodefluence/altair/plugin/oauth/module/application/usecase" 12 | "github.com/kodefluence/altair/plugin/oauth/module/application/usecase/mock" 13 | "github.com/kodefluence/altair/util" 14 | mockdb "github.com/kodefluence/monorepo/db/mock" 15 | "github.com/kodefluence/monorepo/exception" 16 | "github.com/kodefluence/monorepo/kontext" 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | func TestUpdate(t *testing.T) { 21 | mockCtrl := gomock.NewController(t) 22 | defer mockCtrl.Finish() 23 | 24 | sqldb := mockdb.NewMockDB(mockCtrl) 25 | 26 | t.Run("Update", func(t *testing.T) { 27 | t.Run("Given context, id and oauth application update json", func(t *testing.T) { 28 | t.Run("When update process is success and find one process is success", func(t *testing.T) { 29 | t.Run("Then it will return oauth application json", func(t *testing.T) { 30 | oauthApplication := entity.OauthApplication{ 31 | ID: 1, 32 | } 33 | 34 | ktx := kontext.Fabricate() 35 | formatterUsecase := newFormatter() 36 | apierrorUsecase := apierror.Provide() 37 | oauthApplicationRepository := mock.NewMockOauthApplicationRepository(mockCtrl) 38 | 39 | data := entity.OauthApplicationUpdateJSON{ 40 | Description: util.ValueToPointer("New description"), 41 | Scopes: util.ValueToPointer("users public"), 42 | } 43 | 44 | gomock.InOrder( 45 | oauthApplicationRepository.EXPECT().Update(gomock.Any(), oauthApplication.ID, entity.OauthApplicationUpdateable{ 46 | Description: data.Description, 47 | Scopes: data.Scopes, 48 | }, gomock.Any()).Return(nil), 49 | oauthApplicationRepository.EXPECT().One(ktx, 1, sqldb).Return(oauthApplication, nil), 50 | ) 51 | 52 | applicationManager := usecase.NewApplicationManager(sqldb, oauthApplicationRepository, apierrorUsecase, formatterUsecase) 53 | oauthApplicationJSON, err := applicationManager.Update(ktx, oauthApplication.ID, data) 54 | assert.Nil(t, err) 55 | assert.Equal(t, formatterUsecase.Application(oauthApplication), oauthApplicationJSON) 56 | }) 57 | }) 58 | 59 | t.Run("When update process failed", func(t *testing.T) { 60 | t.Run("Then it will return error", func(t *testing.T) { 61 | oauthApplication := entity.OauthApplication{ 62 | ID: 1, 63 | } 64 | 65 | ktx := kontext.Fabricate() 66 | formatterUsecase := newFormatter() 67 | apierrorUsecase := apierror.Provide() 68 | oauthApplicationRepository := mock.NewMockOauthApplicationRepository(mockCtrl) 69 | 70 | data := entity.OauthApplicationUpdateJSON{ 71 | Description: util.ValueToPointer("New description"), 72 | Scopes: util.ValueToPointer("users public"), 73 | } 74 | 75 | gomock.InOrder( 76 | oauthApplicationRepository.EXPECT().Update(gomock.Any(), oauthApplication.ID, entity.OauthApplicationUpdateable{ 77 | Description: data.Description, 78 | Scopes: data.Scopes, 79 | }, gomock.Any()).Return(exception.Throw(errors.New("unexpected"))), 80 | ) 81 | 82 | applicationManager := usecase.NewApplicationManager(sqldb, oauthApplicationRepository, apierrorUsecase, formatterUsecase) 83 | _, err := applicationManager.Update(ktx, oauthApplication.ID, data) 84 | assert.NotNil(t, err) 85 | assert.Equal(t, http.StatusInternalServerError, err.HTTPStatus()) 86 | }) 87 | }) 88 | }) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /plugin/oauth/module/application/usecase/validate_application.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/jsonapi" 6 | ) 7 | 8 | func (am *ApplicationManager) ValidateApplication(data entity.OauthApplicationJSON) jsonapi.Errors { 9 | var errorOptions []jsonapi.Option 10 | 11 | if data.OwnerType == nil { 12 | errorOptions = append(errorOptions, am.apiError.ValidationError("object `owner_type` is nil or not exists")) 13 | } else { 14 | if *data.OwnerType != "confidential" && *data.OwnerType != "public" { 15 | errorOptions = append(errorOptions, am.apiError.ValidationError("object `owner_type` must be either of `confidential` or `public`")) 16 | } 17 | } 18 | 19 | if len(errorOptions) > 0 { 20 | return jsonapi.BuildResponse(errorOptions...).Errors 21 | } 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/controller/downstream/downstream.go: -------------------------------------------------------------------------------- 1 | package downstream 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/db" 6 | "github.com/kodefluence/monorepo/exception" 7 | "github.com/kodefluence/monorepo/kontext" 8 | ) 9 | 10 | //go:generate mockgen -destination ./mock/mock.go -package mock -source ./downstream.go 11 | type OauthAccessTokenRepository interface { 12 | OneByToken(ktx kontext.Context, token string, tx db.TX) (entity.OauthAccessToken, exception.Exception) 13 | } 14 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/controller/downstream/mock/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./downstream.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | entity "github.com/kodefluence/altair/plugin/oauth/entity" 12 | db "github.com/kodefluence/monorepo/db" 13 | exception "github.com/kodefluence/monorepo/exception" 14 | kontext "github.com/kodefluence/monorepo/kontext" 15 | ) 16 | 17 | // MockOauthAccessTokenRepository is a mock of OauthAccessTokenRepository interface. 18 | type MockOauthAccessTokenRepository struct { 19 | ctrl *gomock.Controller 20 | recorder *MockOauthAccessTokenRepositoryMockRecorder 21 | } 22 | 23 | // MockOauthAccessTokenRepositoryMockRecorder is the mock recorder for MockOauthAccessTokenRepository. 24 | type MockOauthAccessTokenRepositoryMockRecorder struct { 25 | mock *MockOauthAccessTokenRepository 26 | } 27 | 28 | // NewMockOauthAccessTokenRepository creates a new mock instance. 29 | func NewMockOauthAccessTokenRepository(ctrl *gomock.Controller) *MockOauthAccessTokenRepository { 30 | mock := &MockOauthAccessTokenRepository{ctrl: ctrl} 31 | mock.recorder = &MockOauthAccessTokenRepositoryMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockOauthAccessTokenRepository) EXPECT() *MockOauthAccessTokenRepositoryMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // OneByToken mocks base method. 41 | func (m *MockOauthAccessTokenRepository) OneByToken(ktx kontext.Context, token string, tx db.TX) (entity.OauthAccessToken, exception.Exception) { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "OneByToken", ktx, token, tx) 44 | ret0, _ := ret[0].(entity.OauthAccessToken) 45 | ret1, _ := ret[1].(exception.Exception) 46 | return ret0, ret1 47 | } 48 | 49 | // OneByToken indicates an expected call of OneByToken. 50 | func (mr *MockOauthAccessTokenRepositoryMockRecorder) OneByToken(ktx, token, tx interface{}) *gomock.Call { 51 | mr.mock.ctrl.T.Helper() 52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OneByToken", reflect.TypeOf((*MockOauthAccessTokenRepository)(nil).OneByToken), ktx, token, tx) 53 | } 54 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/controller/http/grant.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/kodefluence/altair/module" 9 | "github.com/kodefluence/altair/plugin/oauth/entity" 10 | "github.com/kodefluence/monorepo/jsonapi" 11 | "github.com/kodefluence/monorepo/kontext" 12 | "github.com/rs/zerolog" 13 | "github.com/rs/zerolog/log" 14 | ) 15 | 16 | // GrantController control flow of grant access token / authorization code 17 | type GrantController struct { 18 | authorizationUsecase Authorization 19 | apiError module.ApiError 20 | } 21 | 22 | // NewGrant return struct ob GrantController 23 | func NewGrant(authorizationUsecase Authorization, apiError module.ApiError) *GrantController { 24 | return &GrantController{ 25 | authorizationUsecase: authorizationUsecase, 26 | apiError: apiError, 27 | } 28 | } 29 | 30 | // Method Post 31 | func (o *GrantController) Method() string { 32 | return "POST" 33 | } 34 | 35 | // Path /oauth/authorizations 36 | func (o *GrantController) Path() string { 37 | return "/oauth/authorizations" 38 | } 39 | 40 | // Control granting access token / authorization code 41 | func (o *GrantController) Control(ktx kontext.Context, c *gin.Context) { 42 | var req entity.AuthorizationRequestJSON 43 | 44 | rawData, err := c.GetRawData() 45 | if err != nil { 46 | log.Error(). 47 | Err(err). 48 | Stack(). 49 | Interface("request_id", c.Value("request_id")). 50 | Array("tags", zerolog.Arr().Str("controller").Str("authorization").Str("grant").Str("get_raw_data")). 51 | Msg("Cannot get raw data") 52 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(o.apiError.BadRequestError("request body"))) 53 | return 54 | } 55 | 56 | err = json.Unmarshal(rawData, &req) 57 | if err != nil { 58 | log.Error(). 59 | Err(err). 60 | Stack(). 61 | Interface("request_id", c.Value("request_id")). 62 | Array("tags", zerolog.Arr().Str("controller").Str("authorization").Str("grant").Str("unmarshal")). 63 | Msg("Cannot unmarshal json") 64 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(o.apiError.BadRequestError("request body"))) 65 | return 66 | } 67 | 68 | data, jsonapierr := o.authorizationUsecase.GrantAuthorizationCode(ktx, req) 69 | if jsonapierr != nil { 70 | c.JSON(jsonapierr.HTTPStatus(), jsonapi.BuildResponse(jsonapi.WithErrors(jsonapierr))) 71 | return 72 | } 73 | 74 | c.JSON(http.StatusOK, jsonapi.BuildResponse( 75 | jsonapi.WithData(data), 76 | )) 77 | } 78 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/controller/http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/jsonapi" 6 | "github.com/kodefluence/monorepo/kontext" 7 | ) 8 | 9 | //go:generate mockgen -destination ./mock/mock.go -package mock -source ./http.go 10 | type Authorization interface { 11 | GrantAuthorizationCode(ktx kontext.Context, authorizationReq entity.AuthorizationRequestJSON) (interface{}, jsonapi.Errors) 12 | GrantToken(ktx kontext.Context, accessTokenReq entity.AccessTokenRequestJSON) (entity.OauthAccessTokenJSON, jsonapi.Errors) 13 | RevokeToken(ktx kontext.Context, revokeAccessTokenReq entity.RevokeAccessTokenRequestJSON) jsonapi.Errors 14 | } 15 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/controller/http/mock/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./http.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | entity "github.com/kodefluence/altair/plugin/oauth/entity" 12 | jsonapi "github.com/kodefluence/monorepo/jsonapi" 13 | kontext "github.com/kodefluence/monorepo/kontext" 14 | ) 15 | 16 | // MockAuthorization is a mock of Authorization interface. 17 | type MockAuthorization struct { 18 | ctrl *gomock.Controller 19 | recorder *MockAuthorizationMockRecorder 20 | } 21 | 22 | // MockAuthorizationMockRecorder is the mock recorder for MockAuthorization. 23 | type MockAuthorizationMockRecorder struct { 24 | mock *MockAuthorization 25 | } 26 | 27 | // NewMockAuthorization creates a new mock instance. 28 | func NewMockAuthorization(ctrl *gomock.Controller) *MockAuthorization { 29 | mock := &MockAuthorization{ctrl: ctrl} 30 | mock.recorder = &MockAuthorizationMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockAuthorization) EXPECT() *MockAuthorizationMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // GrantAuthorizationCode mocks base method. 40 | func (m *MockAuthorization) GrantAuthorizationCode(ktx kontext.Context, authorizationReq entity.AuthorizationRequestJSON) (interface{}, jsonapi.Errors) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "GrantAuthorizationCode", ktx, authorizationReq) 43 | ret0, _ := ret[0].(interface{}) 44 | ret1, _ := ret[1].(jsonapi.Errors) 45 | return ret0, ret1 46 | } 47 | 48 | // GrantAuthorizationCode indicates an expected call of GrantAuthorizationCode. 49 | func (mr *MockAuthorizationMockRecorder) GrantAuthorizationCode(ktx, authorizationReq interface{}) *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantAuthorizationCode", reflect.TypeOf((*MockAuthorization)(nil).GrantAuthorizationCode), ktx, authorizationReq) 52 | } 53 | 54 | // GrantToken mocks base method. 55 | func (m *MockAuthorization) GrantToken(ktx kontext.Context, accessTokenReq entity.AccessTokenRequestJSON) (entity.OauthAccessTokenJSON, jsonapi.Errors) { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "GrantToken", ktx, accessTokenReq) 58 | ret0, _ := ret[0].(entity.OauthAccessTokenJSON) 59 | ret1, _ := ret[1].(jsonapi.Errors) 60 | return ret0, ret1 61 | } 62 | 63 | // GrantToken indicates an expected call of GrantToken. 64 | func (mr *MockAuthorizationMockRecorder) GrantToken(ktx, accessTokenReq interface{}) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantToken", reflect.TypeOf((*MockAuthorization)(nil).GrantToken), ktx, accessTokenReq) 67 | } 68 | 69 | // RevokeToken mocks base method. 70 | func (m *MockAuthorization) RevokeToken(ktx kontext.Context, revokeAccessTokenReq entity.RevokeAccessTokenRequestJSON) jsonapi.Errors { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "RevokeToken", ktx, revokeAccessTokenReq) 73 | ret0, _ := ret[0].(jsonapi.Errors) 74 | return ret0 75 | } 76 | 77 | // RevokeToken indicates an expected call of RevokeToken. 78 | func (mr *MockAuthorizationMockRecorder) RevokeToken(ktx, revokeAccessTokenReq interface{}) *gomock.Call { 79 | mr.mock.ctrl.T.Helper() 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeToken", reflect.TypeOf((*MockAuthorization)(nil).RevokeToken), ktx, revokeAccessTokenReq) 81 | } 82 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/controller/http/revoke.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/kodefluence/altair/module" 9 | "github.com/kodefluence/altair/plugin/oauth/entity" 10 | "github.com/kodefluence/monorepo/jsonapi" 11 | "github.com/kodefluence/monorepo/kontext" 12 | "github.com/rs/zerolog" 13 | "github.com/rs/zerolog/log" 14 | ) 15 | 16 | // RevokeController control flow of revoking access token 17 | type RevokeController struct { 18 | authorizationUsecase Authorization 19 | apiError module.ApiError 20 | } 21 | 22 | // NewRevoke create new revoke controller 23 | func NewRevoke(authorizationUsecase Authorization, apiError module.ApiError) *RevokeController { 24 | return &RevokeController{ 25 | authorizationUsecase: authorizationUsecase, 26 | apiError: apiError, 27 | } 28 | } 29 | 30 | // Method POST 31 | func (o *RevokeController) Method() string { 32 | return "POST" 33 | } 34 | 35 | // Path /oauth/authorizations/revoke 36 | func (o *RevokeController) Path() string { 37 | return "/oauth/authorizations/revoke" 38 | } 39 | 40 | // Control revoking access token 41 | func (o *RevokeController) Control(ktx kontext.Context, c *gin.Context) { 42 | var req entity.RevokeAccessTokenRequestJSON 43 | 44 | rawData, err := c.GetRawData() 45 | if err != nil { 46 | log.Error(). 47 | Err(err). 48 | Stack(). 49 | Interface("request_id", c.Value("request_id")). 50 | Array("tags", zerolog.Arr().Str("controller").Str("authorization").Str("revoke").Str("get_raw_data")). 51 | Msg("Cannot get raw data") 52 | 53 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(o.apiError.BadRequestError("request body"))) 54 | return 55 | } 56 | 57 | err = json.Unmarshal(rawData, &req) 58 | if err != nil { 59 | log.Error(). 60 | Err(err). 61 | Stack(). 62 | Interface("request_id", c.Value("request_id")). 63 | Array("tags", zerolog.Arr().Str("controller").Str("authorization").Str("revoke").Str("unmarshal")). 64 | Msg("Cannot unmarshal json") 65 | 66 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(o.apiError.BadRequestError("request body"))) 67 | return 68 | } 69 | 70 | jsonapierr := o.authorizationUsecase.RevokeToken(ktx, req) 71 | if jsonapierr != nil { 72 | c.JSON(jsonapierr.HTTPStatus(), jsonapi.BuildResponse(jsonapi.WithErrors(jsonapierr))) 73 | return 74 | } 75 | 76 | c.JSON(http.StatusOK, jsonapi.BuildResponse()) 77 | } 78 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/controller/http/token.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/kodefluence/altair/module" 9 | "github.com/kodefluence/altair/plugin/oauth/entity" 10 | "github.com/kodefluence/monorepo/jsonapi" 11 | "github.com/kodefluence/monorepo/kontext" 12 | "github.com/rs/zerolog" 13 | "github.com/rs/zerolog/log" 14 | ) 15 | 16 | // TokenController control flow of creating access token 17 | type TokenController struct { 18 | authorizationUsecase Authorization 19 | apiError module.ApiError 20 | } 21 | 22 | // NewToken create new token controller 23 | func NewToken(authorizationUsecase Authorization, apiError module.ApiError) *TokenController { 24 | return &TokenController{ 25 | authorizationUsecase: authorizationUsecase, 26 | apiError: apiError, 27 | } 28 | } 29 | 30 | // Method POST 31 | func (o *TokenController) Method() string { 32 | return "POST" 33 | } 34 | 35 | // Path /oauth/authorizations/token 36 | func (o *TokenController) Path() string { 37 | return "/oauth/authorizations/token" 38 | } 39 | 40 | // Control creating access token based on access token request 41 | func (o *TokenController) Control(ktx kontext.Context, c *gin.Context) { 42 | var req entity.AccessTokenRequestJSON 43 | 44 | rawData, err := c.GetRawData() 45 | if err != nil { 46 | log.Error(). 47 | Err(err). 48 | Stack(). 49 | Interface("request_id", c.Value("request_id")). 50 | Array("tags", zerolog.Arr().Str("controller").Str("authorization").Str("token").Str("get_raw_data")). 51 | Msg("Cannot get raw data") 52 | 53 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(o.apiError.BadRequestError("request body"))) 54 | return 55 | } 56 | 57 | err = json.Unmarshal(rawData, &req) 58 | if err != nil { 59 | log.Error(). 60 | Err(err). 61 | Stack(). 62 | Interface("request_id", c.Value("request_id")). 63 | Array("tags", zerolog.Arr().Str("controller").Str("authorization").Str("token").Str("unmarshal")). 64 | Msg("Cannot unmarshal json") 65 | 66 | c.JSON(http.StatusBadRequest, jsonapi.BuildResponse(o.apiError.BadRequestError("request body"))) 67 | return 68 | } 69 | 70 | data, jsonapierr := o.authorizationUsecase.GrantToken(ktx, req) 71 | if jsonapierr != nil { 72 | c.JSON(jsonapierr.HTTPStatus(), jsonapi.BuildResponse(jsonapi.WithErrors(jsonapierr))) 73 | return 74 | } 75 | 76 | c.JSON(http.StatusOK, jsonapi.BuildResponse( 77 | jsonapi.WithData(data), 78 | )) 79 | } 80 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/loader.go: -------------------------------------------------------------------------------- 1 | package authorization 2 | 3 | import ( 4 | "github.com/kodefluence/altair/module" 5 | "github.com/kodefluence/altair/plugin/oauth/entity" 6 | "github.com/kodefluence/altair/plugin/oauth/module/authorization/controller/downstream" 7 | "github.com/kodefluence/altair/plugin/oauth/module/authorization/controller/http" 8 | "github.com/kodefluence/altair/plugin/oauth/module/authorization/usecase" 9 | "github.com/kodefluence/monorepo/db" 10 | ) 11 | 12 | func Load( 13 | appModule module.App, 14 | oauthApplicationRepo usecase.OauthApplicationRepository, 15 | oauthAccessTokenRepo usecase.OauthAccessTokenRepository, 16 | oauthAccessGrantRepo usecase.OauthAccessGrantRepository, 17 | oauthRefreshTokenRepo usecase.OauthRefreshTokenRepository, 18 | formatter usecase.Formatter, 19 | config entity.OauthPlugin, 20 | sqldb db.DB, 21 | apiError module.ApiError, 22 | ) { 23 | authorizationUsecase := usecase.NewAuthorization(oauthApplicationRepo, oauthAccessTokenRepo, oauthAccessGrantRepo, oauthRefreshTokenRepo, formatter, config, sqldb, apiError) 24 | 25 | appModule.Controller().InjectHTTP( 26 | http.NewGrant(authorizationUsecase, apiError), 27 | http.NewToken(authorizationUsecase, apiError), 28 | http.NewRevoke(authorizationUsecase, apiError), 29 | ) 30 | 31 | appModule.Controller().InjectDownstream(downstream.NewOauth(oauthAccessTokenRepo, sqldb)) 32 | } 33 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/authorization_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/golang/mock/gomock" 7 | "github.com/kodefluence/altair/module" 8 | "github.com/kodefluence/altair/module/apierror" 9 | "github.com/kodefluence/altair/plugin/oauth/entity" 10 | "github.com/kodefluence/altair/plugin/oauth/module/authorization/usecase" 11 | "github.com/kodefluence/altair/plugin/oauth/module/authorization/usecase/mock" 12 | "github.com/kodefluence/altair/plugin/oauth/module/formatter" 13 | mockdb "github.com/kodefluence/monorepo/db/mock" 14 | "github.com/kodefluence/monorepo/kontext" 15 | "github.com/stretchr/testify/suite" 16 | ) 17 | 18 | type AuthorizationBaseSuiteTest struct { 19 | mockCtrl *gomock.Controller 20 | 21 | ktx kontext.Context 22 | 23 | oauthApplicationRepo *mock.MockOauthApplicationRepository 24 | oauthAccessTokenRepo *mock.MockOauthAccessTokenRepository 25 | oauthAccessGrantRepo *mock.MockOauthAccessGrantRepository 26 | oauthRefreshTokenRepo *mock.MockOauthRefreshTokenRepository 27 | formatter usecase.Formatter 28 | config entity.OauthPlugin 29 | apiError module.ApiError 30 | authorization *usecase.Authorization 31 | sqldb *mockdb.MockDB 32 | 33 | suite.Suite 34 | } 35 | 36 | func (suite *AuthorizationBaseSuiteTest) SetupTest() { 37 | suite.mockCtrl = gomock.NewController(suite.T()) 38 | 39 | suite.ktx = kontext.Fabricate() 40 | 41 | suite.config = entity.OauthPlugin{ 42 | Config: entity.PluginConfig{ 43 | Database: "main_database", 44 | AccessTokenTimeoutRaw: "24h", 45 | AuthorizationCodeTimeoutRaw: "24h", 46 | RefreshToken: struct { 47 | Timeout string "yaml:\"timeout\"" 48 | Active bool "yaml:\"active\"" 49 | }{ 50 | Timeout: "24h", 51 | Active: true, 52 | }, 53 | ImplicitGrant: struct { 54 | Active bool "yaml:\"active\"" 55 | }{ 56 | Active: true, 57 | }, 58 | }, 59 | } 60 | 61 | suite.oauthApplicationRepo = mock.NewMockOauthApplicationRepository(suite.mockCtrl) 62 | suite.oauthAccessTokenRepo = mock.NewMockOauthAccessTokenRepository(suite.mockCtrl) 63 | suite.oauthAccessGrantRepo = mock.NewMockOauthAccessGrantRepository(suite.mockCtrl) 64 | suite.oauthRefreshTokenRepo = mock.NewMockOauthRefreshTokenRepository(suite.mockCtrl) 65 | suite.formatter = formatter.Provide(24*time.Hour, 24*time.Hour, 24*time.Hour) 66 | suite.sqldb = mockdb.NewMockDB(suite.mockCtrl) 67 | suite.apiError = apierror.Provide() 68 | suite.authorization = usecase.NewAuthorization(suite.oauthApplicationRepo, suite.oauthAccessTokenRepo, suite.oauthAccessGrantRepo, suite.oauthRefreshTokenRepo, suite.formatter, suite.config, suite.sqldb, suite.apiError) 69 | } 70 | 71 | func (suite *AuthorizationBaseSuiteTest) TearDownTest() { 72 | suite.mockCtrl.Finish() 73 | } 74 | 75 | func (suite *AuthorizationBaseSuiteTest) Subtest(testcase string, subtest func()) { 76 | suite.SetupTest() 77 | suite.Run(testcase, subtest) 78 | suite.TearDownTest() 79 | } 80 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/client_credential.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/db" 6 | "github.com/kodefluence/monorepo/exception" 7 | "github.com/kodefluence/monorepo/jsonapi" 8 | "github.com/kodefluence/monorepo/kontext" 9 | "github.com/rs/zerolog" 10 | ) 11 | 12 | func (a *Authorization) ClientCredential(ktx kontext.Context, accessTokenReq entity.AccessTokenRequestJSON, oauthApplication entity.OauthApplication) (entity.OauthAccessToken, *entity.OauthRefreshToken, jsonapi.Errors) { 13 | var finalOauthAccessToken entity.OauthAccessToken 14 | var finalRefreshToken *entity.OauthRefreshToken 15 | 16 | exc := a.sqldb.Transaction(ktx, "authorization-grant-client-credential", func(tx db.TX) exception.Exception { 17 | id, err := a.oauthAccessTokenRepo.Create(ktx, a.formatter.AccessTokenClientCredentialInsertable(oauthApplication, accessTokenReq.Scope), tx) 18 | if err != nil { 19 | return exception.Throw(err, exception.WithDetail("error creating new oauth access token"), exception.WithType(exception.Unexpected), exception.WithTitle("access token creation error")) 20 | } 21 | 22 | oauthAccessToken, err := a.oauthAccessTokenRepo.One(ktx, id, tx) 23 | if err != nil { 24 | return exception.Throw(err, exception.WithDetail("error selecting newly created access token"), exception.WithType(exception.Unexpected), exception.WithTitle("access token creation error")) 25 | } 26 | 27 | if a.config.Config.RefreshToken.Active { 28 | if refreshToken, err := a.GrantRefreshToken(ktx, oauthAccessToken, oauthApplication, tx); err != nil { 29 | return exception.Throw(err, exception.WithType(exception.Unexpected), exception.WithTitle("Internal Server Error"), exception.WithDetail("error creating refresh token data")) 30 | } else { 31 | finalRefreshToken = &refreshToken 32 | } 33 | } 34 | 35 | finalOauthAccessToken = oauthAccessToken 36 | 37 | return nil 38 | }) 39 | 40 | if exc != nil { 41 | return entity.OauthAccessToken{}, nil, a.exceptionMapping(ktx, exc, zerolog.Arr().Str("service").Str("authorization").Str("refresh_token")) 42 | } 43 | return finalOauthAccessToken, finalRefreshToken, nil 44 | } 45 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/exception_mapping.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/kodefluence/monorepo/exception" 7 | "github.com/kodefluence/monorepo/jsonapi" 8 | "github.com/kodefluence/monorepo/kontext" 9 | "github.com/rs/zerolog" 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | func (a *Authorization) exceptionMapping(ktx kontext.Context, exc exception.Exception, tags *zerolog.Array) jsonapi.Errors { 14 | log.Error(). 15 | Err(exc). 16 | Stack(). 17 | Interface("request_id", ktx.GetWithoutCheck("request_id")). 18 | Array("tags", tags). 19 | Msg(exc.Detail()) 20 | 21 | switch exc.Type() { 22 | case exception.NotFound: 23 | return jsonapi.BuildResponse( 24 | jsonapi.WithException( 25 | "ERR0404", 26 | http.StatusNotFound, 27 | exc, 28 | ), 29 | ).Errors 30 | case exception.Forbidden: 31 | return jsonapi.BuildResponse( 32 | jsonapi.WithException( 33 | "ERR0403", 34 | http.StatusForbidden, 35 | exc, 36 | ), 37 | ).Errors 38 | default: 39 | return jsonapi.BuildResponse(a.apiError.InternalServerError(ktx)).Errors 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/find_and_validate_application.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/exception" 6 | "github.com/kodefluence/monorepo/jsonapi" 7 | "github.com/kodefluence/monorepo/kontext" 8 | "github.com/rs/zerolog" 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | func (a *Authorization) FindAndValidateApplication(ktx kontext.Context, clientUID, clientSecret *string) (entity.OauthApplication, jsonapi.Errors) { 13 | if clientUID == nil { 14 | return entity.OauthApplication{}, jsonapi.BuildResponse( 15 | a.apiError.ValidationError("client_uid cannot be empty"), 16 | ).Errors 17 | } 18 | 19 | if clientSecret == nil { 20 | return entity.OauthApplication{}, jsonapi.BuildResponse( 21 | a.apiError.ValidationError("client_secret cannot be empty"), 22 | ).Errors 23 | } 24 | 25 | oauthApplication, err := a.oauthApplicationRepo.OneByUIDandSecret(ktx, *clientUID, *clientSecret, a.sqldb) 26 | if err != nil { 27 | log.Error(). 28 | Err(err). 29 | Stack(). 30 | Interface("request_id", ktx.GetWithoutCheck("request_id")). 31 | Str("client_uid", *clientUID). 32 | Array("tags", zerolog.Arr().Str("service").Str("authorization").Str("find_secret")). 33 | Msg("application cannot be found because there was an error") 34 | 35 | if err.Type() == exception.NotFound { 36 | return entity.OauthApplication{}, 37 | jsonapi.BuildResponse(a.apiError.NotFoundError(ktx, "client_uid & client_secret")).Errors 38 | } 39 | 40 | return entity.OauthApplication{}, 41 | jsonapi.BuildResponse(a.apiError.InternalServerError(ktx)).Errors 42 | } 43 | 44 | return oauthApplication, nil 45 | } 46 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/grant.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/db" 6 | "github.com/kodefluence/monorepo/exception" 7 | "github.com/kodefluence/monorepo/jsonapi" 8 | "github.com/kodefluence/monorepo/kontext" 9 | "github.com/rs/zerolog" 10 | ) 11 | 12 | // Grant authorization an access code 13 | func (a *Authorization) Grant(ktx kontext.Context, authorizationReq entity.AuthorizationRequestJSON) (entity.OauthAccessGrantJSON, jsonapi.Errors) { 14 | var oauthAccessGrantJSON entity.OauthAccessGrantJSON 15 | 16 | oauthApplication, jsonapiErr := a.FindAndValidateApplication(ktx, authorizationReq.ClientUID, authorizationReq.ClientSecret) 17 | if jsonapiErr != nil { 18 | return oauthAccessGrantJSON, jsonapiErr 19 | } 20 | 21 | if err := a.ValidateAuthorizationGrant(ktx, authorizationReq, oauthApplication); err != nil { 22 | return oauthAccessGrantJSON, err 23 | } 24 | 25 | exc := a.sqldb.Transaction(ktx, "authorization-grant-authorization-code", func(tx db.TX) exception.Exception { 26 | id, err := a.oauthAccessGrantRepo.Create(ktx, a.formatter.AccessGrantFromAuthorizationRequestInsertable(authorizationReq, oauthApplication), tx) 27 | if err != nil { 28 | return exception.Throw(err, exception.WithDetail("error creating authorization code")) 29 | } 30 | 31 | oauthAccessGrant, err := a.oauthAccessGrantRepo.One(ktx, id, tx) 32 | if err != nil { 33 | return exception.Throw(err, exception.WithDetail("error selecting newly created authorization code")) 34 | } 35 | 36 | oauthAccessGrantJSON = a.formatter.AccessGrant(oauthAccessGrant) 37 | return nil 38 | }) 39 | if exc != nil { 40 | return oauthAccessGrantJSON, a.exceptionMapping(ktx, exc, zerolog.Arr().Str("service").Str("authorization").Str("grant")) 41 | } 42 | 43 | return oauthAccessGrantJSON, nil 44 | } 45 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/grant_authorization_code.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/jsonapi" 6 | "github.com/kodefluence/monorepo/kontext" 7 | ) 8 | 9 | func (a *Authorization) GrantAuthorizationCode(ktx kontext.Context, authorizationReq entity.AuthorizationRequestJSON) (interface{}, jsonapi.Errors) { 10 | if authorizationReq.ResponseType == nil { 11 | return nil, jsonapi.BuildResponse( 12 | a.apiError.ValidationError("response_type cannot be empty"), 13 | ).Errors 14 | } 15 | 16 | switch *authorizationReq.ResponseType { 17 | case "token": 18 | if !a.config.Config.ImplicitGrant.Active { 19 | break 20 | } 21 | 22 | return a.ImplicitGrant(ktx, authorizationReq) 23 | case "code": 24 | return a.Grant(ktx, authorizationReq) 25 | } 26 | 27 | return nil, jsonapi.BuildResponse( 28 | a.apiError.ValidationError("response_type is invalid. Should be either `token` or `code`"), 29 | ).Errors 30 | } 31 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/grant_refresh_token.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/db" 6 | "github.com/kodefluence/monorepo/jsonapi" 7 | "github.com/kodefluence/monorepo/kontext" 8 | ) 9 | 10 | func (a *Authorization) GrantRefreshToken(ktx kontext.Context, oauthAccessToken entity.OauthAccessToken, oauthApplication entity.OauthApplication, tx db.TX) (entity.OauthRefreshToken, jsonapi.Errors) { 11 | refreshTokenID, err := a.oauthRefreshTokenRepo.Create(ktx, a.formatter.RefreshTokenInsertable(oauthApplication, oauthAccessToken), tx) 12 | if err != nil { 13 | return entity.OauthRefreshToken{}, jsonapi.BuildResponse(a.apiError.InternalServerError(ktx)).Errors 14 | } 15 | 16 | oauthRefreshToken, err := a.oauthRefreshTokenRepo.One(ktx, refreshTokenID, tx) 17 | if err != nil { 18 | return entity.OauthRefreshToken{}, jsonapi.BuildResponse(a.apiError.InternalServerError(ktx)).Errors 19 | } 20 | 21 | return oauthRefreshToken, nil 22 | } 23 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/grant_token.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/jsonapi" 6 | "github.com/kodefluence/monorepo/kontext" 7 | ) 8 | 9 | func (a *Authorization) GrantToken(ktx kontext.Context, accessTokenReq entity.AccessTokenRequestJSON) (entity.OauthAccessTokenJSON, jsonapi.Errors) { 10 | if jsonapiErr := a.ValidateTokenGrant(accessTokenReq); jsonapiErr != nil { 11 | return entity.OauthAccessTokenJSON{}, jsonapiErr 12 | } 13 | 14 | var oauthApplication entity.OauthApplication 15 | var jsonapierr jsonapi.Errors 16 | 17 | if *accessTokenReq.GrantType != "refresh_token" { 18 | oauthApplication, jsonapierr = a.FindAndValidateApplication(ktx, accessTokenReq.ClientUID, accessTokenReq.ClientSecret) 19 | if jsonapierr != nil { 20 | return entity.OauthAccessTokenJSON{}, jsonapierr 21 | } 22 | } 23 | 24 | switch *accessTokenReq.GrantType { 25 | case "authorization_code": 26 | oauthAccessToken, oauthRefreshToken, redirectURI, jsonapierr := a.GrantTokenFromAuthorizationCode(ktx, accessTokenReq, oauthApplication) 27 | if jsonapierr != nil { 28 | return entity.OauthAccessTokenJSON{}, jsonapierr 29 | } 30 | 31 | if oauthRefreshToken == nil { 32 | return a.formatter.AccessToken(oauthAccessToken, redirectURI, nil), nil 33 | } 34 | 35 | refreshTokenJSON := a.formatter.RefreshToken(*oauthRefreshToken) 36 | return a.formatter.AccessToken(oauthAccessToken, redirectURI, &refreshTokenJSON), nil 37 | case "refresh_token": 38 | if a.config.Config.RefreshToken.Active { 39 | oauthAccessToken, oauthRefreshToken, jsonapierr := a.GrantTokenFromRefreshToken(ktx, accessTokenReq) 40 | if jsonapierr != nil { 41 | return entity.OauthAccessTokenJSON{}, jsonapierr 42 | } 43 | 44 | refreshTokenJSON := a.formatter.RefreshToken(oauthRefreshToken) 45 | return a.formatter.AccessToken(oauthAccessToken, "", &refreshTokenJSON), nil 46 | } 47 | case "client_credentials": 48 | oauthAccessToken, oauthRefreshToken, jsonapierr := a.ClientCredential(ktx, accessTokenReq, oauthApplication) 49 | if jsonapierr != nil { 50 | return entity.OauthAccessTokenJSON{}, jsonapierr 51 | } 52 | 53 | if oauthRefreshToken == nil { 54 | return a.formatter.AccessToken(oauthAccessToken, "", nil), nil 55 | } 56 | 57 | refreshTokenJSON := a.formatter.RefreshToken(*oauthRefreshToken) 58 | return a.formatter.AccessToken(oauthAccessToken, "", &refreshTokenJSON), nil 59 | } 60 | 61 | // This code is actually unreachable since there are already validation put in place in ValidateTokenGrant lol 62 | // But I'll keep here just in case 63 | return entity.OauthAccessTokenJSON{}, jsonapi.BuildResponse( 64 | a.apiError.ValidationError(`grant_type can't be empty`), 65 | ).Errors 66 | } 67 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/grant_token_from_authorization_code.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/db" 6 | "github.com/kodefluence/monorepo/exception" 7 | "github.com/kodefluence/monorepo/jsonapi" 8 | "github.com/kodefluence/monorepo/kontext" 9 | "github.com/rs/zerolog" 10 | ) 11 | 12 | func (a *Authorization) GrantTokenFromAuthorizationCode(ktx kontext.Context, accessTokenReq entity.AccessTokenRequestJSON, oauthApplication entity.OauthApplication) (entity.OauthAccessToken, *entity.OauthRefreshToken, string, jsonapi.Errors) { 13 | var finalOauthAccessToken entity.OauthAccessToken 14 | var finalRedirectURI string 15 | var finalRefreshToken *entity.OauthRefreshToken 16 | 17 | exc := a.sqldb.Transaction(ktx, "authorization-grant-token-from-refresh-token", func(tx db.TX) exception.Exception { 18 | oauthAccessGrant, err := a.oauthAccessGrantRepo.OneByCode(ktx, *accessTokenReq.Code, tx) 19 | if err != nil { 20 | if err.Type() == exception.NotFound { 21 | errorObject := jsonapi.BuildResponse(a.apiError.NotFoundError(ktx, "authorization_code")).Errors[0] 22 | return exception.Throw(err, exception.WithType(exception.NotFound), exception.WithDetail(errorObject.Detail), exception.WithTitle(errorObject.Title)) 23 | } 24 | 25 | return exception.Throw(err, exception.WithType(exception.Unexpected), exception.WithTitle("Internal Server Error"), exception.WithDetail("authorization code cannot be found because there was an error")) 26 | } 27 | 28 | if exc := a.ValidateTokenAuthorizationCode(ktx, accessTokenReq, oauthAccessGrant); exc != nil { 29 | return exc 30 | } 31 | 32 | id, err := a.oauthAccessTokenRepo.Create(ktx, a.formatter.AccessTokenFromOauthAccessGrantInsertable(oauthAccessGrant, oauthApplication), tx) 33 | if err != nil { 34 | return exception.Throw(err, exception.WithType(exception.Unexpected), exception.WithTitle("Internal Server Error"), exception.WithDetail("error creating access token data")) 35 | } 36 | 37 | oauthAccessToken, err := a.oauthAccessTokenRepo.One(ktx, id, tx) 38 | if err != nil { 39 | return exception.Throw(err, exception.WithType(exception.Unexpected), exception.WithTitle("Internal Server Error"), exception.WithDetail("error selecting newly created access token")) 40 | } 41 | 42 | err = a.oauthAccessGrantRepo.Revoke(ktx, *accessTokenReq.Code, tx) 43 | if err != nil { 44 | return exception.Throw(err, exception.WithType(exception.Unexpected), exception.WithTitle("Internal Server Error"), exception.WithDetail("error revoking oauth access grant")) 45 | } 46 | 47 | if a.config.Config.RefreshToken.Active { 48 | if refreshToken, err := a.GrantRefreshToken(ktx, oauthAccessToken, oauthApplication, tx); err != nil { 49 | return exception.Throw(err, exception.WithType(exception.Unexpected), exception.WithTitle("Internal Server Error"), exception.WithDetail("error creating refresh token data")) 50 | } else { 51 | finalRefreshToken = &refreshToken 52 | } 53 | } 54 | 55 | finalOauthAccessToken = oauthAccessToken 56 | finalRedirectURI = oauthAccessGrant.RedirectURI.String 57 | 58 | return nil 59 | }) 60 | if exc != nil { 61 | return entity.OauthAccessToken{}, nil, "", a.exceptionMapping(ktx, exc, zerolog.Arr().Str("service").Str("authorization").Str("refresh_token")) 62 | } 63 | 64 | return finalOauthAccessToken, finalRefreshToken, finalRedirectURI, nil 65 | } 66 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/implicit_grant.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/db" 6 | "github.com/kodefluence/monorepo/exception" 7 | "github.com/kodefluence/monorepo/jsonapi" 8 | "github.com/kodefluence/monorepo/kontext" 9 | "github.com/rs/zerolog" 10 | ) 11 | 12 | // ImplicitGrant implementation refer to this RFC 6749 Section 4.2 https://www.rfc-editor.org/rfc/rfc6749#section-4.2 13 | // In altair we implement only confidential oauth application that can request implicit grant 14 | func (a *Authorization) ImplicitGrant(ktx kontext.Context, authorizationReq entity.AuthorizationRequestJSON) (entity.OauthAccessTokenJSON, jsonapi.Errors) { 15 | var finalOauthTokenJSON entity.OauthAccessTokenJSON 16 | 17 | oauthApplication, jsonError := a.FindAndValidateApplication(ktx, authorizationReq.ClientUID, authorizationReq.ClientSecret) 18 | if jsonError != nil { 19 | return entity.OauthAccessTokenJSON{}, jsonError 20 | } 21 | 22 | if err := a.ValidateAuthorizationGrant(ktx, authorizationReq, oauthApplication); err != nil { 23 | return entity.OauthAccessTokenJSON{}, err 24 | } 25 | 26 | exc := a.sqldb.Transaction(ktx, "authorization-implicit-grant", func(tx db.TX) exception.Exception { 27 | id, err := a.oauthAccessTokenRepo.Create(ktx, a.formatter.AccessTokenFromAuthorizationRequestInsertable(authorizationReq, oauthApplication), tx) 28 | if err != nil { 29 | return exception.Throw(err, exception.WithDetail("error creating new oauth access token"), exception.WithType(exception.Unexpected), exception.WithTitle("access token creation error")) 30 | } 31 | 32 | oauthAccessToken, err := a.oauthAccessTokenRepo.One(ktx, id, tx) 33 | if err != nil { 34 | return exception.Throw(err, exception.WithDetail("error selecting newly created access token"), exception.WithType(exception.Unexpected), exception.WithTitle("access token creation error")) 35 | } 36 | 37 | finalOauthTokenJSON = a.formatter.AccessToken(oauthAccessToken, *authorizationReq.RedirectURI, nil) 38 | return nil 39 | }) 40 | if exc != nil { 41 | return entity.OauthAccessTokenJSON{}, a.exceptionMapping(ktx, exc, zerolog.Arr().Str("service").Str("authorization").Str("grant_token")) 42 | } 43 | 44 | return finalOauthTokenJSON, nil 45 | } 46 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/revoke_token.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/jsonapi" 6 | "github.com/kodefluence/monorepo/kontext" 7 | "github.com/rs/zerolog" 8 | ) 9 | 10 | // RevokeToken revoke given access token request 11 | func (a *Authorization) RevokeToken(ktx kontext.Context, revokeAccessTokenReq entity.RevokeAccessTokenRequestJSON) jsonapi.Errors { 12 | 13 | if revokeAccessTokenReq.Token == nil { 14 | return jsonapi.BuildResponse( 15 | a.apiError.ValidationError("token cannot be empty"), 16 | ).Errors 17 | } 18 | 19 | exc := a.oauthAccessTokenRepo.Revoke(ktx, *revokeAccessTokenReq.Token, a.sqldb) 20 | if exc != nil { 21 | return a.exceptionMapping(ktx, exc, zerolog.Arr().Str("service").Str("authorization").Str("revoke_token")) 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/revoke_token_test.go: -------------------------------------------------------------------------------- 1 | package usecase_test 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/kodefluence/altair/plugin/oauth/entity" 9 | "github.com/kodefluence/altair/util" 10 | "github.com/kodefluence/monorepo/exception" 11 | "github.com/stretchr/testify/suite" 12 | ) 13 | 14 | type RevokeTokenSuiteTest struct { 15 | *AuthorizationBaseSuiteTest 16 | 17 | revokeRequest entity.RevokeAccessTokenRequestJSON 18 | } 19 | 20 | func TestRevokeToken(t *testing.T) { 21 | suite.Run(t, &RevokeTokenSuiteTest{ 22 | AuthorizationBaseSuiteTest: &AuthorizationBaseSuiteTest{}, 23 | }) 24 | } 25 | 26 | func (suite *RevokeTokenSuiteTest) SetupTest() { 27 | suite.revokeRequest = entity.RevokeAccessTokenRequestJSON{ 28 | Token: util.ValueToPointer("some-token"), 29 | } 30 | } 31 | 32 | func (suite *RevokeTokenSuiteTest) Subtest(testcase string, subtest func()) { 33 | suite.SetupTest() 34 | suite.AuthorizationBaseSuiteTest.Subtest(testcase, subtest) 35 | suite.TearDownTest() 36 | } 37 | 38 | func (suite *RevokeTokenSuiteTest) TestRevokeToken() { 39 | suite.Run("Positive cases", func() { 40 | suite.Subtest("When all parameters is valid, then it would return nil", func() { 41 | suite.oauthAccessTokenRepo.EXPECT().Revoke(suite.ktx, *suite.revokeRequest.Token, suite.sqldb).Return(nil) 42 | err := suite.authorization.RevokeToken(suite.ktx, suite.revokeRequest) 43 | suite.Assert().Nil(err) 44 | }) 45 | }) 46 | 47 | suite.Run("Negative cases", func() { 48 | suite.Subtest("When token parameter is nil, then it would return error", func() { 49 | suite.revokeRequest.Token = nil 50 | err := suite.authorization.RevokeToken(suite.ktx, suite.revokeRequest) 51 | suite.Assert().Equal("JSONAPI Error:\n[Validation error] Detail: Validation error because of: token cannot be empty, Code: ERR1442\n", err.Error()) 52 | suite.Assert().Equal(http.StatusUnprocessableEntity, err.HTTPStatus()) 53 | }) 54 | 55 | suite.Subtest("When all parameters is valid but revoke return not found, then it would return error", func() { 56 | suite.oauthAccessTokenRepo.EXPECT().Revoke(suite.ktx, *suite.revokeRequest.Token, suite.sqldb).Return(exception.Throw(errors.New("not found"), exception.WithType(exception.NotFound), exception.WithDetail("oauth access token is not found"), exception.WithTitle("Not Found"))) 57 | err := suite.authorization.RevokeToken(suite.ktx, suite.revokeRequest) 58 | suite.Assert().Equal("JSONAPI Error:\n[Not Found] Detail: oauth access token is not found, Code: ERR0404\n", err.Error()) 59 | suite.Assert().Equal(http.StatusNotFound, err.HTTPStatus()) 60 | }) 61 | 62 | suite.Subtest("When all parameters is valid but revoke return unexpected error, then it would return error", func() { 63 | suite.oauthAccessTokenRepo.EXPECT().Revoke(suite.ktx, *suite.revokeRequest.Token, suite.sqldb).Return(exception.Throw(errors.New("unexpected"), exception.WithType(exception.Unexpected))) 64 | err := suite.authorization.RevokeToken(suite.ktx, suite.revokeRequest) 65 | suite.Assert().Equal("JSONAPI Error:\n[Internal server error] Detail: Something is not right, help us fix this problem. Contribute to https://github.com/kodefluence/altair. Tracing code: '', Code: ERR0500\n", err.Error()) 66 | suite.Assert().Equal(http.StatusInternalServerError, err.HTTPStatus()) 67 | }) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/validate_authorization_grant.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kodefluence/altair/plugin/oauth/entity" 8 | "github.com/kodefluence/altair/util" 9 | "github.com/kodefluence/monorepo/jsonapi" 10 | "github.com/kodefluence/monorepo/kontext" 11 | ) 12 | 13 | func (a *Authorization) ValidateAuthorizationGrant(ktx kontext.Context, r entity.AuthorizationRequestJSON, application entity.OauthApplication) jsonapi.Errors { 14 | var errorOptions []jsonapi.Option 15 | 16 | if r.ResponseType == nil { 17 | errorOptions = append(errorOptions, a.apiError.ValidationError("response_type can't be empty")) 18 | } 19 | 20 | if r.ResourceOwnerID == nil { 21 | errorOptions = append(errorOptions, a.apiError.ValidationError("resource_owner_id can't be empty")) 22 | } 23 | 24 | if r.RedirectURI == nil { 25 | errorOptions = append(errorOptions, a.apiError.ValidationError("redirect_uri can't be empty")) 26 | } 27 | 28 | if len(errorOptions) > 0 { 29 | return jsonapi.BuildResponse(errorOptions...).Errors 30 | } 31 | 32 | if r.Scopes == nil { 33 | r.Scopes = util.ValueToPointer("") 34 | } 35 | 36 | requestScopes := strings.Fields(*r.Scopes) 37 | applicationScopes := strings.Fields(application.Scopes.String) 38 | 39 | var invalidScope []string 40 | 41 | for _, rs := range requestScopes { 42 | 43 | scopeNotExists := true 44 | 45 | for _, as := range applicationScopes { 46 | if rs == as { 47 | scopeNotExists = false 48 | break 49 | } 50 | } 51 | 52 | if scopeNotExists { 53 | invalidScope = append(invalidScope, rs) 54 | } 55 | } 56 | 57 | if len(invalidScope) > 0 { 58 | return jsonapi.BuildResponse( 59 | a.apiError.ForbiddenError(ktx, "application", fmt.Sprintf("your requested scopes `(%v)` is not exists in application", invalidScope)), 60 | ).Errors 61 | } 62 | 63 | if *r.ResponseType == "token" && application.OwnerType != "confidential" { 64 | return jsonapi.BuildResponse( 65 | a.apiError.ForbiddenError(ktx, "access_token", "your response type is not allowed in this application"), 66 | ).Errors 67 | } 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/validate_token_authorization_code.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/kodefluence/altair/plugin/oauth/entity" 8 | "github.com/kodefluence/monorepo/exception" 9 | "github.com/kodefluence/monorepo/kontext" 10 | ) 11 | 12 | func (a *Authorization) ValidateTokenAuthorizationCode(ktx kontext.Context, r entity.AccessTokenRequestJSON, data entity.OauthAccessGrant) exception.Exception { 13 | if data.RevokedAT.Valid { 14 | return exception.Throw(errors.New("forbidden"), exception.WithType(exception.Forbidden), exception.WithDetail("authorization code already used"), exception.WithTitle("Forbidden resource access")) 15 | } 16 | 17 | if time.Now().After(data.ExpiresIn) { 18 | return exception.Throw(errors.New("forbidden"), exception.WithType(exception.Forbidden), exception.WithDetail("authorization code already expired"), exception.WithTitle("Forbidden resource access")) 19 | } 20 | 21 | if data.RedirectURI.String != *r.RedirectURI { 22 | return exception.Throw(errors.New("forbidden"), exception.WithType(exception.Forbidden), exception.WithDetail("redirect uri is different from one that generated before"), exception.WithTitle("Forbidden resource access")) 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /plugin/oauth/module/authorization/usecase/validate_token_grant.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/monorepo/jsonapi" 6 | ) 7 | 8 | func (a *Authorization) ValidateTokenGrant(r entity.AccessTokenRequestJSON) jsonapi.Errors { 9 | var errorOptions []jsonapi.Option 10 | 11 | if r.GrantType == nil { 12 | return jsonapi.BuildResponse(a.apiError.ValidationError(`grant_type can't be empty`)).Errors 13 | } 14 | 15 | switch *r.GrantType { 16 | case "authorization_code": 17 | if r.Code == nil { 18 | errorOptions = append(errorOptions, a.apiError.ValidationError(`code is not valid value`)) 19 | } 20 | 21 | if r.RedirectURI == nil { 22 | errorOptions = append(errorOptions, a.apiError.ValidationError(`redirect_uri is not valid value`)) 23 | } 24 | case "refresh_token": 25 | if r.RefreshToken == nil { 26 | errorOptions = append(errorOptions, a.apiError.ValidationError(`refresh_token is not valid value`)) 27 | } 28 | case "client_credentials": 29 | // No validations, since client_uid and client_secret validation already validated before 30 | default: 31 | errorOptions = append(errorOptions, a.apiError.ValidationError(`grant_type is not valid value`)) 32 | } 33 | 34 | if len(errorOptions) > 0 { 35 | return jsonapi.BuildResponse(errorOptions...).Errors 36 | } 37 | 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/provider.go: -------------------------------------------------------------------------------- 1 | package formatter 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kodefluence/altair/plugin/oauth/module/formatter/usecase" 7 | ) 8 | 9 | func Provide(tokenExpiresIn time.Duration, codeExpiresIn time.Duration, refreshTokenExpiresIn time.Duration) *usecase.Formatter { 10 | return usecase.NewFormatter(tokenExpiresIn, codeExpiresIn, refreshTokenExpiresIn) 11 | } 12 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/access_grant.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kodefluence/altair/plugin/oauth/entity" 7 | "github.com/kodefluence/altair/util" 8 | ) 9 | 10 | func (*Formatter) AccessGrant(e entity.OauthAccessGrant) entity.OauthAccessGrantJSON { 11 | var data entity.OauthAccessGrantJSON 12 | 13 | data.ID = &e.ID 14 | data.OauthApplicationID = &e.OauthApplicationID 15 | data.ResourceOwnerID = &e.ResourceOwnerID 16 | data.Code = &e.Code 17 | data.RedirectURI = &e.RedirectURI.String 18 | data.Scopes = &e.Scopes.String 19 | 20 | if time.Now().Before(e.ExpiresIn) { 21 | data.ExpiresIn = util.ValueToPointer(int(time.Until(e.ExpiresIn).Seconds())) 22 | } else { 23 | data.ExpiresIn = util.ValueToPointer(0) 24 | } 25 | 26 | data.CreatedAt = &e.CreatedAt 27 | 28 | if e.RevokedAT.Valid { 29 | data.RevokedAT = &e.RevokedAT.Time 30 | } 31 | 32 | return data 33 | } 34 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/access_grant_from_authorization_request_insertable.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kodefluence/altair/plugin/oauth/entity" 7 | "github.com/kodefluence/altair/util" 8 | ) 9 | 10 | func (f *Formatter) AccessGrantFromAuthorizationRequestInsertable(r entity.AuthorizationRequestJSON, application entity.OauthApplication) entity.OauthAccessGrantInsertable { 11 | var accessGrantInsertable entity.OauthAccessGrantInsertable 12 | 13 | accessGrantInsertable.OauthApplicationID = application.ID 14 | accessGrantInsertable.ResourceOwnerID = *r.ResourceOwnerID 15 | accessGrantInsertable.Scopes = util.PointerToValue(r.Scopes) 16 | accessGrantInsertable.Code = util.SHA1() 17 | accessGrantInsertable.RedirectURI = util.PointerToValue(r.RedirectURI) 18 | accessGrantInsertable.ExpiresIn = time.Now().Add(f.codeExpiresIn) 19 | 20 | return accessGrantInsertable 21 | } 22 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/access_token.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kodefluence/altair/plugin/oauth/entity" 7 | "github.com/kodefluence/altair/util" 8 | ) 9 | 10 | func (*Formatter) AccessToken(e entity.OauthAccessToken, redirectURI string, refreshTokenJSON *entity.OauthRefreshTokenJSON) entity.OauthAccessTokenJSON { 11 | var data entity.OauthAccessTokenJSON 12 | 13 | data.ID = &e.ID 14 | data.OauthApplicationID = &e.OauthApplicationID 15 | data.ResourceOwnerID = &e.ResourceOwnerID 16 | data.Token = &e.Token 17 | data.Scopes = &e.Scopes.String 18 | data.RedirectURI = &redirectURI 19 | data.CreatedAt = &e.CreatedAt 20 | 21 | if time.Now().Before(e.ExpiresIn) { 22 | data.ExpiresIn = util.ValueToPointer(int(time.Until(e.ExpiresIn).Seconds())) 23 | } else { 24 | data.ExpiresIn = util.ValueToPointer(0) 25 | } 26 | 27 | if e.RevokedAT.Valid { 28 | data.RevokedAT = &e.RevokedAT.Time 29 | } 30 | 31 | if refreshTokenJSON != nil { 32 | data.RefreshToken = refreshTokenJSON 33 | } 34 | 35 | return data 36 | } 37 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/access_token_client_credential_insertable.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/kodefluence/altair/plugin/oauth/entity" 8 | "github.com/kodefluence/aurelia" 9 | ) 10 | 11 | func (f *Formatter) AccessTokenClientCredentialInsertable(application entity.OauthApplication, scope *string) entity.OauthAccessTokenInsertable { 12 | var accessTokenInsertable entity.OauthAccessTokenInsertable 13 | 14 | accessTokenInsertable.OauthApplicationID = application.ID 15 | accessTokenInsertable.ResourceOwnerID = 0 16 | accessTokenInsertable.Token = aurelia.Hash(application.ClientUID, application.ClientSecret+strconv.Itoa(0)) 17 | 18 | if scope != nil { 19 | accessTokenInsertable.Scopes = *scope 20 | } 21 | accessTokenInsertable.ExpiresIn = time.Now().Add(f.tokenExpiresIn) 22 | 23 | return accessTokenInsertable 24 | } 25 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/access_token_from_authorization_request_insertable.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/kodefluence/altair/plugin/oauth/entity" 8 | "github.com/kodefluence/altair/util" 9 | "github.com/kodefluence/aurelia" 10 | ) 11 | 12 | func (f *Formatter) AccessTokenFromAuthorizationRequestInsertable(r entity.AuthorizationRequestJSON, application entity.OauthApplication) entity.OauthAccessTokenInsertable { 13 | var accessTokenInsertable entity.OauthAccessTokenInsertable 14 | 15 | accessTokenInsertable.OauthApplicationID = application.ID 16 | accessTokenInsertable.ResourceOwnerID = *r.ResourceOwnerID 17 | accessTokenInsertable.Token = aurelia.Hash(application.ClientUID, application.ClientSecret+strconv.Itoa(*r.ResourceOwnerID)) 18 | accessTokenInsertable.Scopes = util.PointerToValue(r.Scopes) 19 | accessTokenInsertable.ExpiresIn = time.Now().Add(f.tokenExpiresIn) 20 | 21 | return accessTokenInsertable 22 | } 23 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/access_token_from_oauth_access_grant_insertable.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/kodefluence/altair/plugin/oauth/entity" 8 | "github.com/kodefluence/aurelia" 9 | ) 10 | 11 | func (f *Formatter) AccessTokenFromOauthAccessGrantInsertable(oauthAccessGrant entity.OauthAccessGrant, application entity.OauthApplication) entity.OauthAccessTokenInsertable { 12 | var accessTokenInsertable entity.OauthAccessTokenInsertable 13 | 14 | accessTokenInsertable.OauthApplicationID = application.ID 15 | accessTokenInsertable.ResourceOwnerID = oauthAccessGrant.ResourceOwnerID 16 | accessTokenInsertable.Token = aurelia.Hash(application.ClientUID, application.ClientSecret+strconv.Itoa(oauthAccessGrant.ResourceOwnerID)) 17 | accessTokenInsertable.Scopes = oauthAccessGrant.Scopes.String 18 | accessTokenInsertable.ExpiresIn = time.Now().Add(f.tokenExpiresIn) 19 | 20 | return accessTokenInsertable 21 | } 22 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/access_token_from_oauth_refresh_token_insertable.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/kodefluence/altair/plugin/oauth/entity" 8 | "github.com/kodefluence/aurelia" 9 | ) 10 | 11 | func (f *Formatter) AccessTokenFromOauthRefreshTokenInsertable(application entity.OauthApplication, accessToken entity.OauthAccessToken) entity.OauthAccessTokenInsertable { 12 | var accessTokenInsertable entity.OauthAccessTokenInsertable 13 | 14 | accessTokenInsertable.OauthApplicationID = application.ID 15 | accessTokenInsertable.ResourceOwnerID = accessToken.ResourceOwnerID 16 | accessTokenInsertable.Token = aurelia.Hash(application.ClientUID, application.ClientSecret+strconv.Itoa(accessToken.ResourceOwnerID)) 17 | accessTokenInsertable.Scopes = accessToken.Scopes.String 18 | accessTokenInsertable.ExpiresIn = time.Now().Add(f.tokenExpiresIn) 19 | 20 | return accessTokenInsertable 21 | } 22 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/application.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | "github.com/kodefluence/altair/util" 6 | ) 7 | 8 | func (f *Formatter) Application(application entity.OauthApplication) entity.OauthApplicationJSON { 9 | oauthApplicationJSON := entity.OauthApplicationJSON{ 10 | ID: &application.ID, 11 | OwnerType: &application.OwnerType, 12 | ClientUID: &application.ClientUID, 13 | ClientSecret: &application.ClientSecret, 14 | CreatedAt: &application.CreatedAt, 15 | UpdatedAt: &application.UpdatedAt, 16 | } 17 | 18 | if application.OwnerID.Valid { 19 | oauthApplicationJSON.OwnerID = util.ValueToPointer(int(application.OwnerID.Int64)) 20 | } 21 | 22 | if application.RevokedAt.Valid { 23 | oauthApplicationJSON.RevokedAt = &application.RevokedAt.Time 24 | } 25 | 26 | if application.Description.Valid { 27 | oauthApplicationJSON.Description = &application.Description.String 28 | } 29 | 30 | if application.Scopes.Valid { 31 | oauthApplicationJSON.Scopes = &application.Scopes.String 32 | } 33 | 34 | return oauthApplicationJSON 35 | } 36 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/application_list.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/kodefluence/altair/plugin/oauth/entity" 5 | ) 6 | 7 | func (f *Formatter) ApplicationList(applications []entity.OauthApplication) []entity.OauthApplicationJSON { 8 | oauthApplicationJSON := make([]entity.OauthApplicationJSON, len(applications)) 9 | 10 | for k, v := range applications { 11 | oauthApplicationJSON[k] = f.Application(v) 12 | } 13 | 14 | return oauthApplicationJSON 15 | } 16 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/formatter.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import "time" 4 | 5 | type Formatter struct { 6 | tokenExpiresIn time.Duration 7 | codeExpiresIn time.Duration 8 | refreshTokenExpiresIn time.Duration 9 | } 10 | 11 | func NewFormatter(tokenExpiresIn time.Duration, codeExpiresIn time.Duration, refreshTokenExpiresIn time.Duration) *Formatter { 12 | return &Formatter{ 13 | tokenExpiresIn: tokenExpiresIn, 14 | codeExpiresIn: codeExpiresIn, 15 | refreshTokenExpiresIn: refreshTokenExpiresIn, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/oauth_application_insertable.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/kodefluence/altair/plugin/oauth/entity" 6 | "github.com/kodefluence/altair/util" 7 | "github.com/kodefluence/aurelia" 8 | ) 9 | 10 | func (*Formatter) OauthApplicationInsertable(r entity.OauthApplicationJSON) entity.OauthApplicationInsertable { 11 | var oauthApplicationInsertable entity.OauthApplicationInsertable 12 | 13 | oauthApplicationInsertable.OwnerID = util.PointerToValue(r.OwnerID) 14 | oauthApplicationInsertable.OwnerType = *r.OwnerType 15 | oauthApplicationInsertable.Description = util.PointerToValue(r.Description) 16 | oauthApplicationInsertable.Scopes = util.PointerToValue(r.Scopes) 17 | oauthApplicationInsertable.ClientUID = util.SHA1() 18 | oauthApplicationInsertable.ClientSecret = aurelia.Hash(oauthApplicationInsertable.ClientUID, uuid.New().String()) 19 | 20 | return oauthApplicationInsertable 21 | } 22 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/refresh_token.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kodefluence/altair/plugin/oauth/entity" 7 | "github.com/kodefluence/altair/util" 8 | ) 9 | 10 | func (*Formatter) RefreshToken(e entity.OauthRefreshToken) entity.OauthRefreshTokenJSON { 11 | var data entity.OauthRefreshTokenJSON 12 | 13 | data.CreatedAt = &e.CreatedAt 14 | data.Token = &e.Token 15 | 16 | if time.Now().Before(e.ExpiresIn) { 17 | data.ExpiresIn = util.ValueToPointer(int(time.Until(e.ExpiresIn).Seconds())) 18 | } else { 19 | data.ExpiresIn = util.ValueToPointer(0) 20 | } 21 | 22 | if e.RevokedAT.Valid { 23 | data.RevokedAT = &e.RevokedAT.Time 24 | } 25 | 26 | return data 27 | } 28 | -------------------------------------------------------------------------------- /plugin/oauth/module/formatter/usecase/refresh_token_insertable.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/kodefluence/altair/plugin/oauth/entity" 8 | "github.com/kodefluence/aurelia" 9 | ) 10 | 11 | func (f *Formatter) RefreshTokenInsertable(application entity.OauthApplication, accessToken entity.OauthAccessToken) entity.OauthRefreshTokenInsertable { 12 | var refreshTokenInsertable entity.OauthRefreshTokenInsertable 13 | 14 | refreshTokenInsertable.Token = aurelia.Hash(application.ClientUID, application.ClientSecret+strconv.Itoa(accessToken.ResourceOwnerID)) 15 | refreshTokenInsertable.OauthAccessTokenID = accessToken.ID 16 | refreshTokenInsertable.ExpiresIn = time.Now().Add(f.refreshTokenExpiresIn) 17 | 18 | return refreshTokenInsertable 19 | } 20 | -------------------------------------------------------------------------------- /plugin/oauth/module/migration/controller/command/migrate_down.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | 7 | "github.com/golang-migrate/migrate/v4" 8 | "github.com/golang-migrate/migrate/v4/database/mysql" 9 | "github.com/golang-migrate/migrate/v4/source/iofs" 10 | "github.com/kodefluence/altair/core" 11 | "github.com/kodefluence/monorepo/db" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/pflag" 14 | ) 15 | 16 | type MigrateDown struct { 17 | sqldb db.DB 18 | sqldbconfig core.DatabaseConfig 19 | fs embed.FS 20 | } 21 | 22 | func NewMigrateDown(sqldb db.DB, sqldbconfig core.DatabaseConfig, fs embed.FS) *MigrateDown { 23 | return &MigrateDown{sqldb: sqldb, sqldbconfig: sqldbconfig, fs: fs} 24 | } 25 | 26 | func (m *MigrateDown) Use() string { 27 | return "oauth/migrate:down" 28 | } 29 | 30 | func (m *MigrateDown) Short() string { 31 | return "Migrate oauth databases down" 32 | } 33 | 34 | func (m *MigrateDown) Example() string { 35 | return "altair plugin oauth/migrate:down" 36 | } 37 | 38 | func (m *MigrateDown) Run(cmd *cobra.Command, args []string) { 39 | dbDriver, err := mysql.WithInstance(m.sqldb.Eject(), &mysql.Config{ 40 | MigrationsTable: "oauth_plugin_db_versions", 41 | DatabaseName: m.sqldbconfig.DBDatabase(), 42 | }) 43 | if err != nil { 44 | fmt.Println("error", err) 45 | return 46 | } 47 | 48 | sourceDriver, err := iofs.New(m.fs, "mysql") 49 | if err != nil { 50 | fmt.Println("error", err) 51 | return 52 | } 53 | 54 | migrator, err := migrate.NewWithInstance("iofs", sourceDriver, "mysql", dbDriver) 55 | if err != nil { 56 | fmt.Println("error", err) 57 | return 58 | } 59 | 60 | if err := migrator.Down(); err != nil { 61 | fmt.Println("error", err) 62 | return 63 | } 64 | } 65 | 66 | func (m *MigrateDown) ModifyFlags(flags *pflag.FlagSet) {} 67 | -------------------------------------------------------------------------------- /plugin/oauth/module/migration/controller/command/migrate_rollback.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | 7 | "github.com/golang-migrate/migrate/v4" 8 | "github.com/golang-migrate/migrate/v4/database/mysql" 9 | "github.com/golang-migrate/migrate/v4/source/iofs" 10 | "github.com/kodefluence/altair/core" 11 | "github.com/kodefluence/monorepo/db" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/pflag" 14 | ) 15 | 16 | type MigrateRollback struct { 17 | sqldb db.DB 18 | sqldbconfig core.DatabaseConfig 19 | fs embed.FS 20 | } 21 | 22 | func NewMigrateRollback(sqldb db.DB, sqldbconfig core.DatabaseConfig, fs embed.FS) *MigrateRollback { 23 | return &MigrateRollback{sqldb: sqldb, sqldbconfig: sqldbconfig, fs: fs} 24 | } 25 | 26 | func (m *MigrateRollback) Use() string { 27 | return "oauth/migrate:rollback" 28 | } 29 | 30 | func (m *MigrateRollback) Short() string { 31 | return "Do a migration rollback from current versions into previous versions." 32 | } 33 | 34 | func (m *MigrateRollback) Example() string { 35 | return "altair plugin oauth/migrate:rollback" 36 | } 37 | 38 | func (m *MigrateRollback) Run(cmd *cobra.Command, args []string) { 39 | dbDriver, err := mysql.WithInstance(m.sqldb.Eject(), &mysql.Config{ 40 | MigrationsTable: "oauth_plugin_db_versions", 41 | DatabaseName: m.sqldbconfig.DBDatabase(), 42 | }) 43 | if err != nil { 44 | fmt.Println("error", err) 45 | return 46 | } 47 | 48 | sourceDriver, err := iofs.New(m.fs, "mysql") 49 | if err != nil { 50 | fmt.Println("error", err) 51 | return 52 | } 53 | 54 | migrator, err := migrate.NewWithInstance("iofs", sourceDriver, "mysql", dbDriver) 55 | if err != nil { 56 | fmt.Println("error", err) 57 | return 58 | } 59 | 60 | if err := migrator.Steps(-1); err != nil { 61 | fmt.Println("error", err) 62 | return 63 | } 64 | } 65 | 66 | func (m *MigrateRollback) ModifyFlags(flags *pflag.FlagSet) {} 67 | -------------------------------------------------------------------------------- /plugin/oauth/module/migration/controller/command/migrate_up.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | 7 | "github.com/golang-migrate/migrate/v4" 8 | "github.com/golang-migrate/migrate/v4/database/mysql" 9 | _ "github.com/golang-migrate/migrate/v4/source/file" 10 | "github.com/golang-migrate/migrate/v4/source/iofs" 11 | "github.com/kodefluence/altair/core" 12 | "github.com/kodefluence/monorepo/db" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/pflag" 15 | ) 16 | 17 | type MigrateUp struct { 18 | sqldb db.DB 19 | sqldbconfig core.DatabaseConfig 20 | fs embed.FS 21 | } 22 | 23 | func NewMigrateUp(sqldb db.DB, sqldbconfig core.DatabaseConfig, fs embed.FS) *MigrateUp { 24 | return &MigrateUp{sqldb: sqldb, sqldbconfig: sqldbconfig, fs: fs} 25 | } 26 | 27 | func (m *MigrateUp) Use() string { 28 | return "oauth/migrate:up" 29 | } 30 | 31 | func (m *MigrateUp) Short() string { 32 | return "Migrate oauth databases" 33 | } 34 | 35 | func (m *MigrateUp) Example() string { 36 | return "altair plugin oauth/migrate:up" 37 | } 38 | 39 | func (m *MigrateUp) Run(cmd *cobra.Command, args []string) { 40 | dbDriver, err := mysql.WithInstance(m.sqldb.Eject(), &mysql.Config{ 41 | MigrationsTable: "oauth_plugin_db_versions", 42 | DatabaseName: m.sqldbconfig.DBDatabase(), 43 | }) 44 | if err != nil { 45 | fmt.Println("error", err) 46 | return 47 | } 48 | 49 | sourceDriver, err := iofs.New(m.fs, "mysql") 50 | if err != nil { 51 | fmt.Println("error", err) 52 | return 53 | } 54 | 55 | migrator, err := migrate.NewWithInstance("iofs", sourceDriver, "mysql", dbDriver) 56 | if err != nil { 57 | fmt.Println("error", err) 58 | return 59 | } 60 | 61 | if err := migrator.Up(); err != nil { 62 | fmt.Println("error", err) 63 | return 64 | } 65 | } 66 | 67 | func (m *MigrateUp) ModifyFlags(flags *pflag.FlagSet) {} 68 | -------------------------------------------------------------------------------- /plugin/oauth/module/migration/loader.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/kodefluence/altair/core" 7 | "github.com/kodefluence/altair/module" 8 | "github.com/kodefluence/altair/plugin/oauth/module/migration/controller/command" 9 | "github.com/kodefluence/monorepo/db" 10 | ) 11 | 12 | //go:embed mysql/*.sql 13 | var fs embed.FS 14 | 15 | func LoadCommand(sqldb db.DB, sqldbconfig core.DatabaseConfig, appModule module.App) { 16 | appModule.Controller().InjectCommand( 17 | command.NewMigrateUp(sqldb, sqldbconfig, fs), 18 | command.NewMigrateDown(sqldb, sqldbconfig, fs), 19 | command.NewMigrateRollback(sqldb, sqldbconfig, fs), 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /plugin/oauth/module/migration/mysql/1_create_table_oauth_applications.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `oauth_applications`; -------------------------------------------------------------------------------- /plugin/oauth/module/migration/mysql/1_create_table_oauth_applications.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `oauth_applications` ( 2 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 3 | 4 | `owner_id` int(11) unsigned DEFAULT NUll, 5 | `owner_type` varchar(12) NOT NULL, 6 | 7 | `description` text, 8 | `scopes` text, 9 | 10 | `client_uid` varchar(255) NOT NULL, 11 | `client_secret` varchar(255) NOT NULL, 12 | 13 | `revoked_at` DATETIME DEFAULT NULL, 14 | `created_at` DATETIME NOT NULL, 15 | `updated_at` DATETIME NOT NULL, 16 | 17 | PRIMARY KEY (`id`), 18 | UNIQUE KEY `client_uid` (`client_uid`), 19 | UNIQUE KEY `client_secret` (`client_secret`), 20 | KEY `uid_secret` (`client_uid`, `client_secret`), 21 | KEY `uid_secret_revoked_at` (`client_uid`, `client_secret`, `revoked_at`), 22 | KEY `owner_id` (`owner_id`), 23 | KEY `owner_id_owner_type` (`owner_id`, `owner_type`) 24 | ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; -------------------------------------------------------------------------------- /plugin/oauth/module/migration/mysql/2_create_table_oauth_access_tokens.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `oauth_access_tokens`; -------------------------------------------------------------------------------- /plugin/oauth/module/migration/mysql/2_create_table_oauth_access_tokens.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `oauth_access_tokens` ( 2 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 3 | 4 | `oauth_application_id` int(11) unsigned NOT NULL, 5 | `resource_owner_id` int(11) unsigned NOT NULL, 6 | 7 | `token` varchar(255) NOT NULL, 8 | `scopes` text, 9 | 10 | `expires_in` DATETIME NOT NULL, 11 | `created_at` DATETIME NOT NULL, 12 | `revoked_at` DATETIME DEFAULT NULL, 13 | 14 | PRIMARY KEY (`id`), 15 | UNIQUE KEY `token` (`token`), 16 | KEY `id_oauth_application_id` (`id`, `oauth_application_id`), 17 | KEY `id_oauth_application_id_resource_owner_id` (`id`, `oauth_application_id`, `resource_owner_id`), 18 | KEY `oauth_application_id_resource_owner_id` (`oauth_application_id`, `resource_owner_id`) 19 | 20 | ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; 21 | -------------------------------------------------------------------------------- /plugin/oauth/module/migration/mysql/3_create_table_oauth_access_grants.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `oauth_access_grants`; -------------------------------------------------------------------------------- /plugin/oauth/module/migration/mysql/3_create_table_oauth_access_grants.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `oauth_access_grants` ( 2 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 3 | 4 | `oauth_application_id` int(11) unsigned NOT NULL, 5 | `resource_owner_id` int(11) unsigned NOT NULL, 6 | `scopes` text, 7 | 8 | `code` varchar(255) NOT NULL, 9 | `redirect_uri` text, 10 | 11 | `expires_in` DATETIME NOT NULL, 12 | `created_at` DATETIME NOT NULL, 13 | `revoked_at` DATETIME DEFAULT NULL, 14 | 15 | PRIMARY KEY (`id`), 16 | UNIQUE KEY `code` (`code`), 17 | KEY `id_oauth_application_id` (`id`, `oauth_application_id`), 18 | KEY `id_oauth_application_id_resource_owner_id` (`id`, `oauth_application_id`, `resource_owner_id`), 19 | KEY `oauth_application_id_resource_owner_id` (`oauth_application_id`, `resource_owner_id`) 20 | 21 | ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; -------------------------------------------------------------------------------- /plugin/oauth/module/migration/mysql/4_create_table_oauth_refresh_token.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `oauth_refresh_tokens`; 2 | -------------------------------------------------------------------------------- /plugin/oauth/module/migration/mysql/4_create_table_oauth_refresh_token.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `oauth_refresh_tokens` ( 2 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 3 | 4 | `oauth_access_token_id` int(11) unsigned NOT NULL, 5 | 6 | `token` varchar(255) NOT NULL, 7 | 8 | `expires_in` DATETIME NOT NULL, 9 | `created_at` DATETIME NOT NULL, 10 | `revoked_at` DATETIME DEFAULT NULL, 11 | 12 | PRIMARY KEY (`id`), 13 | UNIQUE KEY `token` (`token`), 14 | KEY `oauth_access_token_id` (`id`, `oauth_access_token_id`) 15 | 16 | ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; 17 | -------------------------------------------------------------------------------- /testhelper/testhelper.go: -------------------------------------------------------------------------------- 1 | package testhelper 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | 11 | "github.com/kodefluence/altair/module/apierror" 12 | "github.com/kodefluence/monorepo/jsonapi" 13 | "github.com/kodefluence/monorepo/kontext" 14 | ) 15 | 16 | type MockErrorIoReader struct { 17 | } 18 | 19 | func (m MockErrorIoReader) Read(x []byte) (int, error) { 20 | return 0, errors.New("read error") 21 | } 22 | 23 | func PerformRequest(r http.Handler, method, path string, body io.Reader, reqModifiers ...func(req *http.Request)) *httptest.ResponseRecorder { 24 | req, _ := http.NewRequest(method, path, body) 25 | for _, f := range reqModifiers { 26 | f(req) 27 | } 28 | w := httptest.NewRecorder() 29 | r.ServeHTTP(w, req) 30 | return w 31 | } 32 | 33 | func GenerateTempTestFiles(path, content, fileName string, mode os.FileMode) { 34 | err := os.Mkdir(path, os.ModePerm) 35 | if err != nil { 36 | if pathError, ok := err.(*os.PathError); ok && pathError.Err.Error() != "file exists" { 37 | panic(err) 38 | } 39 | } 40 | 41 | f, err := os.OpenFile(fmt.Sprintf("%s%s", path, fileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | _, err = f.WriteString(content) 47 | if err != nil { 48 | panic(err) 49 | } 50 | } 51 | 52 | func RemoveTempTestFiles(path string) { 53 | err := os.RemoveAll(path) 54 | if err != nil { 55 | panic(err) 56 | } 57 | } 58 | 59 | func ErrInternalServer() jsonapi.Errors { 60 | return jsonapi.BuildResponse(apierror.Provide().InternalServerError(kontext.Fabricate())).Errors 61 | } 62 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "io" 7 | "os" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type Value interface { 14 | int | string | time.Time 15 | } 16 | 17 | func ValueToPointer[V Value](v V) *V { 18 | return &v 19 | } 20 | 21 | func PointerToValue[V Value](v *V) V { 22 | if v == nil { 23 | return *new(V) 24 | } 25 | return *v 26 | } 27 | 28 | func ReadFileContent(path string) ([]byte, error) { 29 | f, err := os.Open(path) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | contents, err := io.ReadAll(f) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return contents, nil 40 | } 41 | 42 | func SHA1() string { 43 | hasher := sha1.New() 44 | hasher.Write([]byte(uuid.New().String())) 45 | return fmt.Sprintf("%x", hasher.Sum(nil)) 46 | } 47 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/kodefluence/altair/testhelper" 8 | "github.com/kodefluence/altair/util" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestUtil_ReadFileContent(t *testing.T) { 13 | 14 | t.Run("Given valid file name, then it does return file content as a string", func(t *testing.T) { 15 | testhelper.GenerateTempTestFiles("./test_file/", "test", "valid_file.txt", 0644) 16 | content, err := util.ReadFileContent("./test_file/valid_file.txt") 17 | assert.Nil(t, err) 18 | assert.Equal(t, []byte("test"), content) 19 | testhelper.RemoveTempTestFiles("./test_file/valid_file.txt") 20 | }) 21 | 22 | t.Run("Given invalid file name, then it does return error", func(t *testing.T) { 23 | _, err := util.ReadFileContent("./test_file/invalid_file.txt") 24 | assert.NotNil(t, err) 25 | }) 26 | } 27 | 28 | func TestUtil(t *testing.T) { 29 | intValue := 1 30 | assert.Equal(t, &intValue, util.ValueToPointer(intValue)) 31 | assert.Equal(t, intValue, util.PointerToValue(&intValue)) 32 | 33 | stringValue := "1" 34 | assert.Equal(t, &stringValue, util.ValueToPointer(stringValue)) 35 | assert.Equal(t, stringValue, util.PointerToValue(&stringValue)) 36 | 37 | timeValue := time.Now() 38 | assert.Equal(t, &timeValue, util.ValueToPointer(timeValue)) 39 | assert.Equal(t, timeValue, util.PointerToValue(&timeValue)) 40 | 41 | var intNil *int 42 | assert.Equal(t, 0, util.PointerToValue(intNil)) 43 | } 44 | --------------------------------------------------------------------------------