diff --git a/.env.example b/.env.example index decff0d..593a860 100644 --- a/.env.example +++ b/.env.example @@ -2,9 +2,14 @@ ENV=development # DB configs DATABASE_URL=postgres://world:hello@postgres/echo_rest_api?sslmode=disable -REDIS_URL=redis://redis:6379 +# REDIS_URL=redis://redis:6379 + +AUTH_PROVIDER=firebase +FIREBASE_CREDENTIALS='{firebase json admin key}' +FIREBASE_AUTH_CREDENTIALS='{filebase json auth key}' + +# KRATOS_API_ENDPOINT=http://kratos:4433/ +# KRATOS_WEBHOOK_API_KEY=very-very-very-secure-api-key -KRATOS_API_ENDPOINT=http://kratos:4433/ -KRATOS_WEBHOOK_API_KEY=very-very-very-secure-api-key AUTO_MIGRATE=true PORT=8088 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3cdcd6b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch API", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/api", + "cwd": "${workspaceFolder}/cmd/api", + "envFile": "${workspaceFolder}/.env", + }, + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "envFile": "${workspaceFolder}/.env" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7a73a41..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/3rd-parties/kratos/config/hooks/body.jsonnet b/3rd-parties/kratos/config/hooks/body.jsonnet deleted file mode 100644 index 03633f9..0000000 --- a/3rd-parties/kratos/config/hooks/body.jsonnet +++ /dev/null @@ -1,6 +0,0 @@ -function(ctx) { - identity_id: if ctx["identity"] != null then ctx.identity.id, - traits: ctx.identity.traits, - state: ctx.identity.state, - verifiable_addresses: ctx.identity.verifiable_addresses, -} \ No newline at end of file diff --git a/3rd-parties/kratos/identity.schema.json b/3rd-parties/kratos/identity.schema.json deleted file mode 100644 index 7b46858..0000000 --- a/3rd-parties/kratos/identity.schema.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "$id": "https://schemas.ory.sh/presets/kratos/identity.basic.schema.json", - "title": "Person", - "type": "object", - "properties": { - "traits": { - "type": "object", - "properties": { - "email": { - "type": "string", - "format": "email", - "title": "E-Mail", - "ory.sh/kratos": { - "credentials": { - "password": { - "identifier": true - }, - "totp": { - "account_name": true - } - }, - "recovery": { - "via": "email" - }, - "verification": { - "via": "email" - } - }, - "maxLength": 320 - }, - "name": { - "type": "object", - "properties": { - "first": { - "type": "string", - "title": "First name", - "maxLength": 256 - }, - "last": { - "type": "string", - "title": "Last name", - "maxLength": 256 - } - } - }, - "newsletter": { - "type": "boolean", - "title": "Newsletter subscription" - } - }, - "required": [ - "email" - ], - "additionalProperties": false - } - } - } \ No newline at end of file diff --git a/3rd-parties/kratos/kratos.yml b/3rd-parties/kratos/kratos.yml deleted file mode 100644 index edbb9f1..0000000 --- a/3rd-parties/kratos/kratos.yml +++ /dev/null @@ -1,122 +0,0 @@ -version: v0.9.0-alpha.3 - -dsn: memory - -serve: - public: - base_url: http://echo-rest.local:4433/ - cors: - enabled: false - allowed_origins: - - http://echo-rest.local:4455 - - http://echo-rest.local:4433 - allowed_methods: - - GET - - OPTIONS - - PUT - - PATCH - - DELETE - admin: - base_url: http://echo-rest.local:4434/ - -session: - cookie: - domain: echo-rest.local - -selfservice: - default_browser_return_url: http://echo-rest.local:4455/ - allowed_return_urls: - - http://echo-rest.local:4455 - - http://echo-rest.local:4433 - - methods: - password: - enabled: true - - flows: - error: - ui_url: http://echo-rest.local:4455/error - - settings: - ui_url: http://echo-rest.local:4455/settings - privileged_session_max_age: 15m - after: - hooks: - - hook: web_hook - config: - url: http://rest_api:8088/hooks/kratos/after-settings - method: POST # GET, DELETE, etc - body: file:///etc/config/kratos/config/hooks/body.jsonnet - auth: - type: api_key - config: - name: X-Api-Key - value: very-very-very-secure-api-key - in: header - - recovery: - enabled: true - ui_url: http://echo-rest.local:4455/recovery - - verification: - enabled: true - ui_url: http://echo-rest.local:4455/verification - after: - default_browser_return_url: http://echo-rest.local:4455/ - - logout: - after: - default_browser_return_url: http://echo-rest.local:4455/login - - login: - ui_url: http://echo-rest.local:4455/login - lifespan: 10m - - registration: - lifespan: 10m - ui_url: http://echo-rest.local:4455/registration - after: - password: - hooks: - - hook: session - - hook: web_hook - config: - url: http://rest_api:8088/hooks/kratos/after-registration - method: POST # GET, DELETE, etc - body: file:///etc/config/kratos/config/hooks/body.jsonnet - auth: - type: api_key - config: - name: X-Api-Key - #SELFSERVICE_FLOWS_REGISTRATION_AFTER_HOOKS_0_CONFIG_AUTH_CONFIG_VALUE - value: very-very-very-secure-api-key - in: header - -log: - level: debug - format: text - leak_sensitive_values: true - -secrets: - cookie: - - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE - cipher: - - 32-LONG-SECRET-NOT-SECURE-AT-ALL - -ciphers: - algorithm: xchacha20-poly1305 - -hashers: - algorithm: bcrypt - bcrypt: - cost: 8 - -identity: - default_schema_id: default - schemas: - - id: default - url: file:///etc/config/kratos/identity.schema.json - -courier: - smtp: - connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true \ No newline at end of file diff --git a/3rd-parties/postgres-scripts/kratos.sql b/3rd-parties/postgres-scripts/kratos.sql deleted file mode 100755 index 4d9877b..0000000 --- a/3rd-parties/postgres-scripts/kratos.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE DATABASE kratos; -GRANT ALL PRIVILEGES ON DATABASE kratos TO world; \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c8e9560..dd4ebd2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Load golang image to build -FROM golang:1.18-alpine as builder +FROM golang:1.19-alpine as builder ARG APP_PATH RUN mkdir -p /app diff --git a/README.md b/README.md index e162c87..e6befc9 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,15 @@ Any feedback and pull requests are welcome and highly appreciated. Feel free to - [Echo REST API boilerplate](#echo-rest-api-boilerplate) - * [Overview](#overview) - * [Features](#features) - * [Running the project](#running-the-project) - * [Environment variables](#environment-variables) - * [Commands](#commands) - * [Folder structure](#folder-structure) - * [Open source refs](#open-source-refs) - * [Contributing](#contributing) - * [TODOs](#todos) + - [HOW TO USE THIS TEMPLATE](#how-to-use-this-template) + - [Features](#features) + - [Running the project](#running-the-project) + - [Environment variables](#environment-variables) + - [Commands](#commands) + - [Folder structure](#folder-structure) + - [Open source refs](#open-source-refs) + - [Contributing](#contributing) + - [TODOs](#todos) @@ -37,13 +37,14 @@ Any feedback and pull requests are welcome and highly appreciated. Feel free to > **NOTE**: **WAIT** until first CI run on github actions before cloning your new project. + ## Features -- [x] User Auth functionality (Signup, Login, Forgot Password, Reset Password, 2FA) using [Ory/Kratos](https://github.com/ory/kratos). +- [x] User Auth functionality (Signup, Login, Forgot Password, Reset Password, 2FA) using **Firebase Auth** - [x] REST API using [labstack/echo](https://github.com/labstack/echo). - [x] DB Migration using [golang-migrate/migrate](https://github.com/golang-migrate/migrate). - [x] Modular structure. @@ -59,10 +60,8 @@ Any feedback and pull requests are welcome and highly appreciated. Feel free to - Make sure you have docker installed. - Copy `.env.example` to `.env.docker` -- Add a new line `127.0.0.1 echo-rest.local` to `/etc/hosts` file. - Run `docker compose up -d`. -- Go to `echo-rest.local:8088` to verify if the API server works. -- Go to `echo-rest.local:4455` to verify if the Kratos works. +- Go to `localhost:8088` to verify if the API server works. ## Environment variables @@ -75,13 +74,15 @@ Setting your config as Environment Variables is recommended as by 12-Factor App. | Name | Type | Description | Example value | |------------------------|---------|------------------------------------------------------------------|-----------------------------------------------| | DATABASE_URL | string | Data source URL for main DB | postgres://world:hello@postgres/echo_rest_api | -| KRATOS_API_ENDPOINT | string | Public endpoint of Kratos | http://kratos:4433/ | -| KRATOS_WEBHOOK_API_KEY | string | Api key for Kratos integration | very-very-very-secure-api-key | | PORT | integer | Http port (accepts also port number only for heroku compability) | 8088 | | AUTO_MIGRATE | boolean | Enable run migration every time the application starts | true | | ENV | string | Environment name | development | | REDIS_URL | string | Optional | redis://redis:6379 | - +| AUTH_PROVIDER | string | Optional | firebase_auth | +| FIREBASE_CREDENTIALS | json | firebase json admin key | {firebase_admin_key} | +| FIREBASE_AUTH_CREDENTIALS | json | filebase json auth key | {firebase_auth_key} | +| KRATOS_API_ENDPOINT | string | [DEPRECATED] Public endpoint of Kratos | http://kratos:4433/ | +| KRATOS_WEBHOOK_API_KEY | string | [DEPRECATED] Api key for Kratos integration | very-very-very-secure-api-key | ## Commands @@ -92,7 +93,7 @@ Setting your config as Environment Variables is recommended as by 12-Factor App. | `make build-api` | Build application binary | | `make setup` | Run commands to setup development env | | `make run-db` | Run DB docker container on local | -| `go run ./tools/mod/ gen` | Generate module component codes. | +| `go run ./tools/mod/ gen` | Generate module component codes. e.g: `go run ./tools/mod/ gen -n Booking` | | `make migration-create [migration_name]` | Create migration files. migration_name should be snake case | | `make git-hooks` | Setup git hooks | | `make routes` | Generate routes file for authorization | @@ -132,7 +133,6 @@ Setting your config as Environment Variables is recommended as by 12-Factor App. ``` ## Open source refs -- https://www.ory.sh/docs/kratos/self-service - https://cuelang.org/docs/about/ - https://www.openpolicyagent.org/docs/latest/ - https://echo.labstack.com/guide/ diff --git a/cmd/api/di/di.go b/cmd/api/di/di.go index fdc8981..041f039 100644 --- a/cmd/api/di/di.go +++ b/cmd/api/di/di.go @@ -1,9 +1,12 @@ package di import ( + "io/fs" + "github.com/dzungtran/echo-rest-api/config" "github.com/dzungtran/echo-rest-api/infrastructure/datastore" "github.com/dzungtran/echo-rest-api/modules/core" + coreTemplates "github.com/dzungtran/echo-rest-api/modules/core/handlers/templates" "github.com/dzungtran/echo-rest-api/modules/projects" "github.com/dzungtran/echo-rest-api/pkg/logger" "github.com/dzungtran/echo-rest-api/pkg/middlewares" @@ -70,3 +73,7 @@ func RegisterModules(e *echo.Echo, container *dig.Container) error { return err } + +func GetCoreTemplates() fs.FS { + return coreTemplates.CoreTemplates +} diff --git a/cmd/api/main.go b/cmd/api/main.go index d28972b..f1947bb 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -2,6 +2,8 @@ package main import ( "context" + "html/template" + "io" "net/http" "os" "os/signal" @@ -18,6 +20,14 @@ import ( echoSwagger "github.com/swaggo/echo-swagger" ) +type TemplateRenderer struct { + templates *template.Template +} + +func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + return t.templates.ExecuteTemplate(w, name, data) +} + // @title Echo REST API // @version 1.0 // @description This documentation for Echo REST server. @@ -30,8 +40,17 @@ import ( // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @host docs.api.com // @BasePath / + +// @securityDefinitions.apikey XUserEmailAuth +// @in header +// @name X-User-Email +// @description This method just enabled for local development + +// @securityDefinitions.apikey XFirebaseBearer +// @in header +// @name Authorization +// @description Enter the token with the `Bearer ` prefix, e.g. `Bearer jwt_token_string`. func main() { // init app config conf, _ := config.InitAppConfig() @@ -44,12 +63,16 @@ func main() { // Echo instance e := echo.New() + t := &TemplateRenderer{ + templates: template.Must(template.ParseFS(di.GetCoreTemplates(), "*.go.tpl")), + } + e.Renderer = t + // Bind default middleware e.Use(middleware.LoggerWithConfig(config.GetEchoLogConfig(conf))) e.Use(middleware.Recover()) e.Use(middleware.RequestID()) e.HideBanner = true - // e.HidePort = true e.Validator = conf.Validator // Setup infra @@ -85,7 +108,7 @@ func main() { e.Logger.Fatal(err) } - e.GET("/swagger/*", echoSwagger.WrapHandler) + e.GET("/docs/*", echoSwagger.WrapHandler) // Start server go func() { diff --git a/config/app.go b/config/app.go index a9c02dc..2e51f42 100644 --- a/config/app.go +++ b/config/app.go @@ -1,28 +1,39 @@ package config import ( + "context" + "fmt" "os" + firebase "firebase.google.com/go/v4" "github.com/go-playground/validator/v10" + "github.com/joho/godotenv" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "google.golang.org/api/option" ) // AppConfig - Init app config type AppConfig struct { Environment string `json:"environment"` AppPort string `json:"app_port"` + BaseURL string `json:"base_url"` DatabaseURL string `json:"database_url"` RedisURL string `json:"redis_url"` - Validator echo.Validator `json:"-"` - CORSConfig middleware.CORSConfig `json:"-"` + Validator echo.Validator `json:"-"` + CORSConfig middleware.CORSConfig `json:"-"` + FirebaseApp *firebase.App `json:"-"` // 3rd-parties settings + AutoMigrate bool `json:"auto_migrate"` + LogLevel string `json:"log_level"` + + AuthProvider string `json:"auth_provider"` + FirebaseCreds string `json:"firebase_creds"` + FirebaseAuthCreds string `json:"firebase_auth_creds"` KratosWebhookApiKey string `json:"kratos_webhook_api_key"` KratosApiEndpoint string `json:"kratos_api_endpoint"` - AutoMigrate bool `json:"auto_migrate"` - LogLevel string `json:"log_level"` } type AppValidator struct { @@ -37,7 +48,19 @@ func (cv *AppValidator) Validate(i interface{}) error { } func InitAppConfig() (*AppConfig, error) { + var fbApp *firebase.App + var err error currentEnv := "development" + + // load env file if exists + _, err = os.Stat(".env") + if err == nil { + err = godotenv.Load(os.ExpandEnv(".env")) + if err != nil { + return nil, fmt.Errorf("error initializing app: %v", err) + } + } + appPort := os.Getenv("PORT") if appPort == "" { appPort = "8088" @@ -47,18 +70,32 @@ func InitAppConfig() (*AppConfig, error) { currentEnv = os.Getenv("ENV") } + fbOpt := option.WithCredentialsJSON([]byte(os.Getenv("FIREBASE_CREDENTIALS"))) + if os.Getenv("FIREBASE_CREDENTIALS") != "" { + fbApp, err = firebase.NewApp(context.Background(), nil, fbOpt) + if err != nil { + return nil, fmt.Errorf("error initializing app: %v", err) + } + } + return &AppConfig{ Environment: currentEnv, AppPort: appPort, + BaseURL: os.Getenv("BASE_URL"), DatabaseURL: os.Getenv("DATABASE_URL"), RedisURL: os.Getenv("REDIS_URL"), Validator: &AppValidator{validator: validator.New()}, CORSConfig: middleware.DefaultCORSConfig, // 3rd-parties settings - AutoMigrate: os.Getenv("AUTO_MIGRATE") == "true", + AutoMigrate: os.Getenv("AUTO_MIGRATE") == "true", + LogLevel: os.Getenv("LOG_LEVEL"), + + AuthProvider: os.Getenv("AUTH_PROVIDER"), KratosWebhookApiKey: os.Getenv("KRATOS_WEBHOOK_API_KEY"), KratosApiEndpoint: os.Getenv("KRATOS_API_ENDPOINT"), - LogLevel: os.Getenv("LOG_LEVEL"), + FirebaseApp: fbApp, + FirebaseCreds: os.Getenv("FIREBASE_CREDENTIALS"), + FirebaseAuthCreds: os.Getenv("FIREBASE_AUTH_CREDENTIALS"), }, nil } diff --git a/config/echo.go b/config/echo.go index be1985a..fd14cab 100644 --- a/config/echo.go +++ b/config/echo.go @@ -1,14 +1,14 @@ package config import ( - "fmt" + "time" "github.com/labstack/echo/v4/middleware" ) func GetEchoLogConfig(appConf *AppConfig) middleware.LoggerConfig { echoLogConf := middleware.DefaultLoggerConfig - echoLogConf.CustomTimeFormat = `2006-01-02T15:04:05.000Z0700` - echoLogConf.Format = fmt.Sprintln(`{"level":"info","source":"echo","id":"${id}","mt":"${method}","uri":"${uri}","st":${status},"e":"${error}","lc":"${latency_human}","ts":"${time_custom}"}`) + echoLogConf.CustomTimeFormat = time.RFC3339 + // echoLogConf.Format = fmt.Sprintln(`{"level":"info","source":"echo","id":"${id}","mt":"${method}","uri":"${uri}","st":${status},"e":"${error}","lc":"${latency_human}","ts":"${time_custom}"}`) return echoLogConf } diff --git a/docker-compose.yaml b/docker-compose.yaml index 5ff5c7e..08b28d3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -27,68 +27,9 @@ services: POSTGRES_DB: echo_rest_api POSTGRES_USER: world POSTGRES_PASSWORD: hello - volumes: - - type: bind - source: ./3rd-parties/postgres-scripts - target: /docker-entrypoint-initdb.d/ ports: - "5432:5432" - kratos: - image: oryd/kratos:v0.9.0-alpha.3 - networks: - - backend - depends_on: - - postgres - - kratos-migrate - environment: - - DSN=postgres://world:hello@postgres:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 - volumes: - - type: bind - source: ./3rd-parties/kratos - target: /etc/config/kratos - command: serve -c /etc/config/kratos/kratos.yml --sqa-opt-out --dev - ports: - - "4433:4433" - - kratos-migrate: - image: oryd/kratos:v0.9.0-alpha.3 - environment: - - DSN=postgres://world:hello@postgres:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 - networks: - - backend - depends_on: - - postgres - volumes: - - type: bind - source: ./3rd-parties/kratos - target: /etc/config/kratos - command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes - - kratos-selfservice-ui-node: - image: oryd/kratos-selfservice-ui-node:latest - links: - - "kratos:echo-rest.local" - environment: - - KRATOS_PUBLIC_URL=http://echo-rest.local:4433 - - PORT=4455 - - BASE_PATH=/ - networks: - - backend - depends_on: - - kratos - ports: - - "4455:4455" - restart: on-failure - - mailslurper: - image: oryd/mailslurper:latest-smtps - networks: - - backend - ports: - - "4437:4437" - - "4436:4436" - redis: image: redis:latest networks: diff --git a/docs/docs.go b/docs/docs.go index c7b41ff..aa74444 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,5 +1,5 @@ -// Package docs GENERATED BY SWAG; DO NOT EDIT -// This file was generated by swaggo/swag +// Code generated by swaggo/swag. DO NOT EDIT. + package docs import "github.com/swaggo/swag" @@ -25,9 +25,234 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/users/{userId}": { + "/admin/orgs": { + "get": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Get list org", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "orgs" + ], + "summary": "Get list org", + "parameters": [ + { + "type": "integer", + "description": "Number of records should be returned", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domains.Org" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Create a new org", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "orgs" + ], + "summary": "Create a new org", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domains.Org" + } + } + } + ] + } + } + } + } + }, + "/admin/orgs/{orgId}": { + "get": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Get org info by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "orgs" + ], + "summary": "Get org info", + "parameters": [ + { + "type": "integer", + "description": "Org ID", + "name": "orgId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domains.Org" + } + } + } + ] + } + } + } + } + }, + "/admin/users": { + "get": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Get list user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get list user info", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domains.User" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "Craete a new user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Craete a new user", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domains.User" + } + } + } + ] + } + } + } + } + }, + "/admin/users/{userId}": { "get": { - "description": "Get user info ID", + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Get user info by ID", "consumes": [ "application/json" ], @@ -67,10 +292,149 @@ const docTemplate = `{ } } } + }, + "put": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Update user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Update user info", + "parameters": [ + { + "description": "Request body update user", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateUserReq" + } + }, + { + "type": "integer", + "description": "User ID", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/wrapper.SuccessResponse" + } + } + } + }, + "delete": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Delete user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/wrapper.SuccessResponse" + } + } + } + } + }, + "/me": { + "get": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Get current user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get current user info", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domains.User" + } + } + } + ] + } + } + } } } }, "definitions": { + "domains.Org": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "logo": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "domains.User": { "description": "User account information", "type": "object", @@ -107,12 +471,16 @@ const docTemplate = `{ "example": "+84 0986415xxxx" }, "status": { - "type": "string", "enum": [ "active", "deactivated", "banned" ], + "allOf": [ + { + "$ref": "#/definitions/domains.UserStatus" + } + ], "example": "active" }, "updated_at": { @@ -120,6 +488,38 @@ const docTemplate = `{ } } }, + "domains.UserStatus": { + "type": "string", + "enum": [ + "active", + "deactivated", + "banned", + "invited" + ], + "x-enum-varnames": [ + "UserStatusActive", + "UserStatusDeactivated", + "UserStatusBanned", + "UserStatusInvited" + ] + }, + "dto.UpdateUserReq": { + "type": "object", + "properties": { + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "wrapper.SuccessResponse": { "type": "object", "properties": { @@ -131,13 +531,27 @@ const docTemplate = `{ } } } + }, + "securityDefinitions": { + "XFirebaseBearer": { + "description": "Enter the token with the ` + "`" + `Bearer ` + "`" + ` prefix, e.g. ` + "`" + `Bearer jwt_token_string` + "`" + `.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "XUserEmailAuth": { + "description": "This method just enabled for local development", + "type": "apiKey", + "name": "X-User-Email", + "in": "header" + } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.0", - Host: "docs.api.com", + Host: "", BasePath: "/", Schemes: []string{}, Title: "Echo REST API", diff --git a/docs/swagger.json b/docs/swagger.json index 4bb2bb8..2322c0c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -15,12 +15,236 @@ }, "version": "1.0" }, - "host": "docs.api.com", "basePath": "/", "paths": { - "/users/{userId}": { + "/admin/orgs": { "get": { - "description": "Get user info ID", + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Get list org", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "orgs" + ], + "summary": "Get list org", + "parameters": [ + { + "type": "integer", + "description": "Number of records should be returned", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domains.Org" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Create a new org", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "orgs" + ], + "summary": "Create a new org", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domains.Org" + } + } + } + ] + } + } + } + } + }, + "/admin/orgs/{orgId}": { + "get": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Get org info by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "orgs" + ], + "summary": "Get org info", + "parameters": [ + { + "type": "integer", + "description": "Org ID", + "name": "orgId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domains.Org" + } + } + } + ] + } + } + } + } + }, + "/admin/users": { + "get": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Get list user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get list user info", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domains.User" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "Craete a new user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Craete a new user", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domains.User" + } + } + } + ] + } + } + } + } + }, + "/admin/users/{userId}": { + "get": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Get user info by ID", "consumes": [ "application/json" ], @@ -60,10 +284,149 @@ } } } + }, + "put": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Update user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Update user info", + "parameters": [ + { + "description": "Request body update user", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateUserReq" + } + }, + { + "type": "integer", + "description": "User ID", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/wrapper.SuccessResponse" + } + } + } + }, + "delete": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Delete user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/wrapper.SuccessResponse" + } + } + } + } + }, + "/me": { + "get": { + "security": [ + { + "XFirebaseBearer": [] + } + ], + "description": "Get current user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get current user info", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/wrapper.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domains.User" + } + } + } + ] + } + } + } } } }, "definitions": { + "domains.Org": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "logo": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "domains.User": { "description": "User account information", "type": "object", @@ -100,12 +463,16 @@ "example": "+84 0986415xxxx" }, "status": { - "type": "string", "enum": [ "active", "deactivated", "banned" ], + "allOf": [ + { + "$ref": "#/definitions/domains.UserStatus" + } + ], "example": "active" }, "updated_at": { @@ -113,6 +480,38 @@ } } }, + "domains.UserStatus": { + "type": "string", + "enum": [ + "active", + "deactivated", + "banned", + "invited" + ], + "x-enum-varnames": [ + "UserStatusActive", + "UserStatusDeactivated", + "UserStatusBanned", + "UserStatusInvited" + ] + }, + "dto.UpdateUserReq": { + "type": "object", + "properties": { + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "wrapper.SuccessResponse": { "type": "object", "properties": { @@ -124,5 +523,19 @@ } } } + }, + "securityDefinitions": { + "XFirebaseBearer": { + "description": "Enter the token with the `Bearer ` prefix, e.g. `Bearer jwt_token_string`.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "XUserEmailAuth": { + "description": "This method just enabled for local development", + "type": "apiKey", + "name": "X-User-Email", + "in": "header" + } } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index fad6ae7..5af8622 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,5 +1,24 @@ basePath: / definitions: + domains.Org: + properties: + code: + type: string + created_at: + type: string + description: + type: string + domain: + type: string + id: + type: integer + logo: + type: string + name: + type: string + updated_at: + type: string + type: object domains.User: description: User account information properties: @@ -28,15 +47,39 @@ definitions: example: +84 0986415xxxx type: string status: + allOf: + - $ref: '#/definitions/domains.UserStatus' enum: - active - deactivated - banned example: active - type: string updated_at: type: string type: object + domains.UserStatus: + enum: + - active + - deactivated + - banned + - invited + type: string + x-enum-varnames: + - UserStatusActive + - UserStatusDeactivated + - UserStatusBanned + - UserStatusInvited + dto.UpdateUserReq: + properties: + first_name: + type: string + last_name: + type: string + phone: + type: string + status: + type: string + type: object wrapper.SuccessResponse: properties: data: {} @@ -45,7 +88,6 @@ definitions: example: true type: boolean type: object -host: docs.api.com info: contact: email: support@api.com @@ -59,11 +101,152 @@ info: title: Echo REST API version: "1.0" paths: - /users/{userId}: + /admin/orgs: + get: + consumes: + - application/json + description: Get list org + parameters: + - description: Number of records should be returned + in: query + name: limit + type: integer + - description: Page + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/wrapper.SuccessResponse' + - properties: + data: + items: + $ref: '#/definitions/domains.Org' + type: array + type: object + security: + - XFirebaseBearer: [] + summary: Get list org + tags: + - orgs + post: + consumes: + - application/json + description: Create a new org + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/wrapper.SuccessResponse' + - properties: + data: + $ref: '#/definitions/domains.Org' + type: object + security: + - XFirebaseBearer: [] + summary: Create a new org + tags: + - orgs + /admin/orgs/{orgId}: + get: + consumes: + - application/json + description: Get org info by ID + parameters: + - description: Org ID + in: path + name: orgId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/wrapper.SuccessResponse' + - properties: + data: + $ref: '#/definitions/domains.Org' + type: object + security: + - XFirebaseBearer: [] + summary: Get org info + tags: + - orgs + /admin/users: get: consumes: - application/json - description: Get user info ID + description: Get list user info + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/wrapper.SuccessResponse' + - properties: + data: + items: + $ref: '#/definitions/domains.User' + type: array + type: object + security: + - XFirebaseBearer: [] + summary: Get list user info + tags: + - users + post: + consumes: + - application/json + description: Craete a new user + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/wrapper.SuccessResponse' + - properties: + data: + $ref: '#/definitions/domains.User' + type: object + summary: Craete a new user + tags: + - users + /admin/users/{userId}: + delete: + consumes: + - application/json + description: Delete user + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/wrapper.SuccessResponse' + security: + - XFirebaseBearer: [] + summary: Delete user + tags: + - users + get: + consumes: + - application/json + description: Get user info by ID parameters: - description: User ID in: path @@ -82,7 +265,70 @@ paths: data: $ref: '#/definitions/domains.User' type: object + security: + - XFirebaseBearer: [] summary: Get user info tags: - users + put: + consumes: + - application/json + description: Update user info + parameters: + - description: Request body update user + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.UpdateUserReq' + - description: User ID + in: path + name: userId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/wrapper.SuccessResponse' + security: + - XFirebaseBearer: [] + summary: Update user info + tags: + - users + /me: + get: + consumes: + - application/json + description: Get current user info + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/wrapper.SuccessResponse' + - properties: + data: + $ref: '#/definitions/domains.User' + type: object + security: + - XFirebaseBearer: [] + summary: Get current user info + tags: + - users +securityDefinitions: + XFirebaseBearer: + description: Enter the token with the `Bearer ` prefix, e.g. `Bearer jwt_token_string`. + in: header + name: Authorization + type: apiKey + XUserEmailAuth: + description: This method just enabled for local development + in: header + name: X-User-Email + type: apiKey swagger: "2.0" diff --git a/go.mod b/go.mod index 4cfa6d4..0c914dc 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/dzungtran/echo-rest-api -go 1.18 +go 1.19 require ( cuelang.org/go v0.4.3 + firebase.google.com/go/v4 v4.10.0 github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/Masterminds/squirrel v1.5.3 github.com/gertd/go-pluralize v0.2.1 @@ -14,6 +15,7 @@ require ( github.com/jackc/pgx/v4 v4.17.1 github.com/jinzhu/copier v0.3.5 github.com/jmoiron/sqlx v1.3.5 + github.com/joho/godotenv v1.3.0 github.com/labstack/echo/v4 v4.10.2 github.com/lib/pq v1.10.6 github.com/lithammer/shortuuid/v4 v4.0.0 @@ -28,11 +30,19 @@ require ( go.uber.org/dig v1.15.0 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e + google.golang.org/api v0.102.0 gopkg.in/yaml.v2 v2.4.0 ) require ( + cloud.google.com/go v0.105.0 // indirect + cloud.google.com/go/compute v1.12.1 // indirect + cloud.google.com/go/compute/metadata v0.2.1 // indirect + cloud.google.com/go/firestore v1.6.1 // indirect + cloud.google.com/go/iam v0.7.0 // indirect + cloud.google.com/go/storage v1.27.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect + github.com/MicahParks/keyfunc v1.5.1 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect @@ -48,9 +58,13 @@ require ( github.com/go-playground/universal-translator v0.18.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang-jwt/jwt/v4 v4.4.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/flatbuffers v2.0.6+incompatible // indirect github.com/google/go-cmp v0.5.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect + github.com/googleapis/gax-go/v2 v2.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect @@ -84,6 +98,7 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect + go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect golang.org/x/crypto v0.6.0 // indirect @@ -93,7 +108,11 @@ require ( golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.1.12 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine/v2 v2.0.2 // indirect + google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect + google.golang.org/grpc v1.52.3 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b687166..2f72706 100644 --- a/go.sum +++ b/go.sum @@ -29,15 +29,26 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.6.1 h1:8rBq3zRjnHx8UtBvaOWqBB1xq9jH6/wltfQLlTMh2Fw= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -48,9 +59,13 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= cuelang.org/go v0.4.3 h1:W3oBBjDTm7+IZfCKZAmC8uDG0eYfJL4Pp/xbbCMKaVo= cuelang.org/go v0.4.3/go.mod h1:7805vR9H+VoBNdWFdI7jyDR3QLUPp4+naHfbcgp55HI= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4= +firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= @@ -85,6 +100,8 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/MicahParks/keyfunc v1.5.1 h1:RlyyYgKQI/adkIw1yXYtPvTAOb7hBhSX42aH23d8N0Q= +github.com/MicahParks/keyfunc v1.5.1/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -560,6 +577,8 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-migrate/migrate/v4 v4.15.2 h1:vU+M05vs6jWHKDdmE1Ecwj0BznygFc4QsdRe2E/L7kc= github.com/golang-migrate/migrate/v4 v4.15.2/go.mod h1:f2toGLkYqD3JH+Todi4aZ2ZdbeUNx4sIwiOK96rE9Lw= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= @@ -633,9 +652,11 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -658,10 +679,14 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.6.0 h1:SXk3ABtQYDT/OH8jAyvEOQ58mgawq5C4o/4/89qN2ZU= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= @@ -802,6 +827,7 @@ github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXL github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= @@ -1485,6 +1511,7 @@ golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1504,6 +1531,7 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= @@ -1634,6 +1662,7 @@ golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1644,6 +1673,7 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1764,6 +1794,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= @@ -1800,8 +1832,11 @@ google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6 google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/api v0.102.0 h1:JxJl2qQ85fRMPNvlZY/enexbxpCjLwGhZUtgfGeQ51I= +google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1812,6 +1847,8 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk= +google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1876,6 +1913,8 @@ google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= @@ -1884,6 +1923,7 @@ google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 h1:a2S6M0+660BgMNl++4JPlcAO/CjkqYItDEZwkoDQK7c= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1918,6 +1958,7 @@ google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.52.3 h1:pf7sOysg4LdgBqduXveGKrcEwbStiK2rtfghdzlUYDQ= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/modules/core/domains/user_org.go b/modules/core/domains/user_org.go index e4147a5..57098a7 100644 --- a/modules/core/domains/user_org.go +++ b/modules/core/domains/user_org.go @@ -18,8 +18,8 @@ type UserOrg struct { Id int64 `json:"id" db:"id"` UserId int64 `json:"user_id" db:"user_id"` OrgId int64 `json:"org_id" db:"org_id"` - Role UserOrgRole `json:"role" db:"role"` - Status UserStatus `json:"status" db:"status"` + Role UserOrgRole `json:"role" db:"role" example:"owner" enums:"owner,manager,editor,viewer,guest"` + Status UserStatus `json:"status" db:"status" example:"active" enums:"active"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } diff --git a/modules/core/handlers/auth.go b/modules/core/handlers/auth.go new file mode 100644 index 0000000..b9471dc --- /dev/null +++ b/modules/core/handlers/auth.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "net/http" + + "github.com/dzungtran/echo-rest-api/config" + "github.com/dzungtran/echo-rest-api/modules/core/usecases" + "github.com/dzungtran/echo-rest-api/pkg/middlewares" + "github.com/labstack/echo/v4" +) + +type AuthHandler struct { + UserUC usecases.UserUsecase + Configs *config.AppConfig +} + +func NewAuthHandler(g *echo.Group, middManager *middlewares.MiddlewareManager, userUsecase usecases.UserUsecase, appCfgs *config.AppConfig) { + handler := &AuthHandler{ + UserUC: userUsecase, + Configs: appCfgs, + } + + apiV1 := g.Group("auth") + apiV1.GET("/login", handler.LoginForm) + apiV1.GET("/success", handler.LoginSuccess) +} + +func (h *AuthHandler) LoginForm(c echo.Context) error { + data := map[string]interface{}{ + "config": h.Configs.FirebaseAuthCreds, + "success_url": h.Configs.BaseURL + "auth/success", + } + return c.Render(http.StatusOK, "login.go.tpl", data) +} + +func (h *AuthHandler) LoginSuccess(c echo.Context) error { + data := map[string]interface{}{ + "config": h.Configs.FirebaseAuthCreds, + } + return c.Render(http.StatusOK, "success.go.tpl", data) +} diff --git a/modules/core/handlers/kratos.go b/modules/core/handlers/kratos.go index 7aa6489..1cee510 100644 --- a/modules/core/handlers/kratos.go +++ b/modules/core/handlers/kratos.go @@ -1,7 +1,7 @@ package handlers import ( - "io/ioutil" + "io" "net/http" "github.com/dzungtran/echo-rest-api/modules/core/dto" @@ -33,7 +33,7 @@ func NewKratosHookHandler(g *echo.Group, middManager *middlewares.MiddlewareMana // Create will store the user by given request body func (h *KratosHookHandler) AfterRegistration(c echo.Context) wrapper.Response { ctx := c.Request().Context() - resp, _ := ioutil.ReadAll(c.Request().Body) + resp, _ := io.ReadAll(c.Request().Body) var err error req := dto.CreateUserReq{ Email: gjson.GetBytes(resp, "traits.email").String(), @@ -89,7 +89,7 @@ func (h *KratosHookHandler) AfterRegistration(c echo.Context) wrapper.Response { func (h *KratosHookHandler) AfterSettings(c echo.Context) wrapper.Response { ctx := c.Request().Context() - resp, _ := ioutil.ReadAll(c.Request().Body) + resp, _ := io.ReadAll(c.Request().Body) var err error userId := int64(0) diff --git a/modules/core/handlers/org.go b/modules/core/handlers/org.go index 0608c50..fffc3e7 100644 --- a/modules/core/handlers/org.go +++ b/modules/core/handlers/org.go @@ -43,7 +43,15 @@ func NewOrgHandler(g *echo.Group, middManager *middlewares.MiddlewareManager, or apiV1Resource.POST("/invites", wrapper.Wrap(handler.Invite)).Name = "invite:org" } -// Create will store the Org by given request body +// CreateNewOrg godoc +// @Summary Create a new org +// @Description Create a new org +// @Tags orgs +// @Accept json +// @Produce json +// @Success 200 {object} wrapper.SuccessResponse{data=domains.Org} +// @Security XFirebaseBearer +// @Router /admin/orgs [post] func (h *OrgHandler) Create(c echo.Context) wrapper.Response { ctx := c.Request().Context() var req dto.CreateOrgReq @@ -79,7 +87,16 @@ func (h *OrgHandler) Create(c echo.Context) wrapper.Response { return wrapper.Response{Status: http.StatusCreated, Data: newOrg} } -// GetByID will get Org by given id +// GetOrgInfo godoc +// @Summary Get org info +// @Description Get org info by ID +// @Tags orgs +// @Accept json +// @Produce json +// @Param orgId path int true "Org ID" +// @Success 200 {object} wrapper.SuccessResponse{data=domains.Org} +// @Security XFirebaseBearer +// @Router /admin/orgs/{orgId} [get] func (h *OrgHandler) GetByID(c echo.Context) wrapper.Response { org := contexts.GetOrgFromContext(c) return wrapper.Response{ @@ -87,7 +104,17 @@ func (h *OrgHandler) GetByID(c echo.Context) wrapper.Response { } } -// Fetch will fetch the Org +// GetListOrgsOfCurrentUser godoc +// @Summary Get list org +// @Description Get list org +// @Tags orgs +// @Accept json +// @Produce json +// @Param limit query int false "Number of records should be returned" +// @Param page query int false "Page" +// @Success 200 {object} wrapper.SuccessResponse{data=[]domains.Org} +// @Security XFirebaseBearer +// @Router /admin/orgs [get] func (h *OrgHandler) Fetch(c echo.Context) wrapper.Response { ctx := c.Request().Context() diff --git a/modules/core/handlers/templates/embed.go b/modules/core/handlers/templates/embed.go new file mode 100644 index 0000000..d0ed817 --- /dev/null +++ b/modules/core/handlers/templates/embed.go @@ -0,0 +1,6 @@ +package templates + +import "embed" + +//go:embed *.tpl +var CoreTemplates embed.FS diff --git a/modules/core/handlers/templates/login.go.tpl b/modules/core/handlers/templates/login.go.tpl new file mode 100644 index 0000000..8a6a390 --- /dev/null +++ b/modules/core/handlers/templates/login.go.tpl @@ -0,0 +1,67 @@ + + + + + + + DWorks - Simple Login + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/modules/core/handlers/templates/success.go.tpl b/modules/core/handlers/templates/success.go.tpl new file mode 100644 index 0000000..c64993c --- /dev/null +++ b/modules/core/handlers/templates/success.go.tpl @@ -0,0 +1,76 @@ + + + + + DWorks - Simple Login + + + + + + + + + + +

Welcome to My Awesome App

+

+    
+

+  
+
\ No newline at end of file
diff --git a/modules/core/handlers/user.go b/modules/core/handlers/user.go
index 34e55a1..95cad83 100644
--- a/modules/core/handlers/user.go
+++ b/modules/core/handlers/user.go
@@ -10,6 +10,7 @@ import (
 	"github.com/dzungtran/echo-rest-api/modules/core/usecases"
 	"github.com/dzungtran/echo-rest-api/pkg/authz"
 	"github.com/dzungtran/echo-rest-api/pkg/constants"
+	"github.com/dzungtran/echo-rest-api/pkg/contexts"
 	"github.com/dzungtran/echo-rest-api/pkg/middlewares"
 	"github.com/dzungtran/echo-rest-api/pkg/utils"
 	"github.com/dzungtran/echo-rest-api/pkg/wrapper"
@@ -30,13 +31,23 @@ func NewUserHandler(g *echo.Group, middManager *middlewares.MiddlewareManager, u
 	apiV1.GET("", wrapper.Wrap(handler.Fetch)).Name = "list:user"
 	apiV1.POST("", wrapper.Wrap(handler.Create)).Name = "create:user"
 
+	apiMeV1 := g.Group("me", middManager.Auth())
+	apiMeV1.GET("", wrapper.Wrap(handler.GetCurrentUserInfo)).Name = "read:me"
+
 	apiV1Resource := g.Group("admin/users/:userId", middManager.Auth())
 	apiV1Resource.GET("", wrapper.Wrap(handler.GetByID)).Name = "read:user"
 	apiV1Resource.PUT("", wrapper.Wrap(handler.Update), middManager.CheckPolicies()).Name = "update:user"
 	apiV1Resource.DELETE("", wrapper.Wrap(handler.Delete), middManager.CheckPolicies()).Name = "delete:user"
 }
 
-// Create will store the user by given request body
+// CreateANewUser godoc
+// @Summary      Craete a new user
+// @Description  Craete a new user
+// @Tags         users
+// @Accept       json
+// @Produce      json
+// @Success      200  {object}  wrapper.SuccessResponse{data=domains.User}
+// @Router       /admin/users [post]
 func (h *UserHandler) Create(c echo.Context) wrapper.Response {
 	ctx := c.Request().Context()
 	var req dto.CreateUserReq
@@ -62,13 +73,14 @@ func (h *UserHandler) Create(c echo.Context) wrapper.Response {
 
 // GetUserInfo godoc
 // @Summary      Get user info
-// @Description  Get user info ID
+// @Description  Get user info by ID
 // @Tags         users
 // @Accept       json
 // @Produce      json
 // @Param        userId   path      int  true  "User ID"
 // @Success      200  {object}  wrapper.SuccessResponse{data=domains.User}
-// @Router       /users/{userId} [get]
+// @Security     XFirebaseBearer
+// @Router       /admin/users/{userId} [get]
 func (h *UserHandler) GetByID(c echo.Context) wrapper.Response {
 	ctx := c.Request().Context()
 	id, err := strconv.Atoi(c.Param("userId"))
@@ -112,7 +124,31 @@ func (h *UserHandler) GetByID(c echo.Context) wrapper.Response {
 	}
 }
 
-// Fetch will fetch the user
+// GetCurrentUserInfo godoc
+// @Summary      Get current user info
+// @Description  Get current user info
+// @Tags         users
+// @Accept       json
+// @Produce      json
+// @Success      200  {object}  wrapper.SuccessResponse{data=domains.User}
+// @Security     XFirebaseBearer
+// @Router       /me [get]
+func (h *UserHandler) GetCurrentUserInfo(c echo.Context) wrapper.Response {
+	user, _ := contexts.GetUserFromContext(c)
+	return wrapper.Response{
+		Data: user,
+	}
+}
+
+// GetListUser godoc
+// @Summary      Get list user info
+// @Description  Get list user info
+// @Tags         users
+// @Accept       json
+// @Produce      json
+// @Success      200  {object}  wrapper.SuccessResponse{data=[]domains.User}
+// @Security     XFirebaseBearer
+// @Router       /admin/users [get]
 func (h *UserHandler) Fetch(c echo.Context) wrapper.Response {
 	ctx := c.Request().Context()
 
@@ -139,7 +175,17 @@ func (h *UserHandler) Fetch(c echo.Context) wrapper.Response {
 	}
 }
 
-// Update will get user by given request body
+// UpdateUserInfo godoc
+// @Summary      Update user info
+// @Description  Update user info
+// @Tags         users
+// @Accept       json
+// @Produce      json
+// @Param        body     body      dto.UpdateUserReq  true  "Request body update user"
+// @Param        userId   path      int  true  "User ID"
+// @Success      200  {object}  wrapper.SuccessResponse{}
+// @Security     XFirebaseBearer
+// @Router       /admin/users/{userId} [put]
 func (h *UserHandler) Update(c echo.Context) wrapper.Response {
 	ctx := c.Request().Context()
 	id, err := strconv.Atoi(c.Param("userId"))
@@ -188,7 +234,15 @@ func (h *UserHandler) Update(c echo.Context) wrapper.Response {
 	return wrapper.Response{}
 }
 
-// Delete will delete user by given param
+// DeleteUser godoc
+// @Summary      Delete user
+// @Description  Delete user
+// @Tags         users
+// @Accept       json
+// @Produce      json
+// @Success      200  {object}  wrapper.SuccessResponse{}
+// @Security     XFirebaseBearer
+// @Router       /admin/users/{userId} [delete]
 func (h *UserHandler) Delete(c echo.Context) wrapper.Response {
 	ctx := c.Request().Context()
 	id, err := strconv.Atoi(c.Param("userId"))
diff --git a/modules/core/mod.go b/modules/core/mod.go
index c17b291..90e4bac 100644
--- a/modules/core/mod.go
+++ b/modules/core/mod.go
@@ -1,6 +1,7 @@
 package core
 
 import (
+	"github.com/dzungtran/echo-rest-api/config"
 	"github.com/dzungtran/echo-rest-api/modules/core/handlers"
 	"github.com/dzungtran/echo-rest-api/modules/core/repositories"
 	"github.com/dzungtran/echo-rest-api/modules/core/usecases"
@@ -28,6 +29,7 @@ func (coreModule) RegisterUseCases(container *dig.Container) error {
 
 func (coreModule) RegisterHandlers(g *echo.Group, container *dig.Container) error {
 	return container.Invoke(func(
+		appConf *config.AppConfig,
 		middManager *middlewares.MiddlewareManager,
 		userUsecase usecases.UserUsecase,
 		orgUsecase usecases.OrgUsecase,
@@ -35,5 +37,6 @@ func (coreModule) RegisterHandlers(g *echo.Group, container *dig.Container) erro
 		handlers.NewOrgHandler(g, middManager, orgUsecase)
 		handlers.NewUserHandler(g, middManager, userUsecase)
 		handlers.NewKratosHookHandler(g, middManager, userUsecase)
+		handlers.NewAuthHandler(g, middManager, userUsecase, appConf)
 	})
 }
diff --git a/pkg/authz/data.json b/pkg/authz/data.json
index beef6a3..62a819c 100644
--- a/pkg/authz/data.json
+++ b/pkg/authz/data.json
@@ -4,35 +4,28 @@
   },
   "roles_chart": {
     "owner": {
-      "access": [
-        "delete:org",
-        "update:org"
-      ]
+      "access": ["delete:org", "update:org"]
     },
     "manager": {
-      "access": [
-        "invite:org"
-      ],
+      "access": ["invite:org"],
       "owner": "owner"
     },
     "editor": {
-      "access": [
-        "create:project",
-        "update:project",
-        "delete:project"
-      ],
+      "access": ["create:project", "update:project", "delete:project"],
       "owner": "manager"
     },
     "viewer": {
       "access": [
         "read:user",
         "update:user",
-        
+
         "read:org",
         "list:org",
 
         "list:project",
-        "read:project"
+        "read:project",
+
+        "read:me"
       ],
       "owner": "manager"
     },
@@ -41,4 +34,4 @@
       "owner": "manager"
     }
   }
-}
\ No newline at end of file
+}
diff --git a/pkg/authz/rego/no_need_role_check.rego b/pkg/authz/rego/no_need_role_check.rego
index bef272e..35cf0a6 100644
--- a/pkg/authz/rego/no_need_role_check.rego
+++ b/pkg/authz/rego/no_need_role_check.rego
@@ -34,6 +34,16 @@ no_need_role_check_user_endpoint[act] {
 	}
 }
 
+no_need_role_check_user_endpoint[act] {
+	# Get current user info
+	input.endpoint = "/me"
+	input.method = "GET"
+	act = {
+		"endpoint": input.endpoint,
+		"method": input.method,
+	}
+}
+
 no_need_role_check_org_endpoint[act] {
 	# Get list org
 	input.endpoint = "/admin/orgs"
diff --git a/pkg/authz/routes.json b/pkg/authz/routes.json
index 5eacad7..4104ff3 100644
--- a/pkg/authz/routes.json
+++ b/pkg/authz/routes.json
@@ -28,5 +28,8 @@
     "DELETE": "delete:user",
     "GET": "read:user",
     "PUT": "update:user"
+  },
+  "/me": {
+    "GET": "read:me"
   }
 }
\ No newline at end of file
diff --git a/pkg/middlewares/firebase_auth.go b/pkg/middlewares/firebase_auth.go
new file mode 100644
index 0000000..aa9deab
--- /dev/null
+++ b/pkg/middlewares/firebase_auth.go
@@ -0,0 +1,84 @@
+package middlewares
+
+import (
+	"errors"
+	"net/http"
+	"strings"
+
+	"github.com/dzungtran/echo-rest-api/modules/core/domains"
+	"github.com/dzungtran/echo-rest-api/modules/core/dto"
+	"github.com/dzungtran/echo-rest-api/pkg/constants"
+	"github.com/labstack/echo/v4"
+)
+
+func (m *MiddlewareManager) FireBaseAuth(next echo.HandlerFunc) echo.HandlerFunc {
+	return func(c echo.Context) error {
+		cli, err := m.appConf.FirebaseApp.Auth(c.Request().Context())
+		if err != nil {
+			return c.JSON(http.StatusInternalServerError, map[string]interface{}{
+				"error": err.Error(),
+			})
+		}
+
+		idToken := strings.Replace(c.Request().Header.Get("Authorization"), "Bearer ", "", 1)
+		tkn, err := cli.VerifyIDToken(c.Request().Context(), idToken)
+		if err != nil {
+			return c.JSON(http.StatusInternalServerError, map[string]interface{}{
+				"error": err.Error(),
+			})
+		}
+
+		tknUser := parseTokenClaimsToUser(tkn.Claims)
+		var u *domains.UserWithRoles
+
+		// Get user info
+		u, err = m.fetchUserFromAuth(c.Request().Context(), tknUser.Code, tknUser.Email)
+		if err != nil {
+			if errors.Is(err, constants.ErrNotFound) {
+				_, err = m.userUC.Register(c.Request().Context(), dto.CreateUserReq{
+					FirstName: tknUser.FirstName,
+					Email:     tknUser.Email,
+					Code:      tknUser.Code,
+				})
+
+				if err != nil {
+					return c.JSON(http.StatusInternalServerError, map[string]interface{}{
+						"error": err.Error(),
+					})
+				}
+
+				u, err = m.fetchUserFromAuth(c.Request().Context(), tknUser.Code, tknUser.Email)
+				if err != nil {
+					return c.JSON(http.StatusInternalServerError, map[string]interface{}{
+						"error": err.Error(),
+					})
+				}
+			} else {
+				return c.JSON(http.StatusInternalServerError, map[string]interface{}{
+					"error": err.Error(),
+				})
+			}
+		}
+
+		if u.Status != domains.UserStatusActive {
+			return c.JSON(http.StatusUnauthorized, map[string]interface{}{
+				"error": "user is not active",
+			})
+		}
+
+		c.Set(constants.ContextKeyUser, u)
+		return next(c)
+	}
+}
+
+func parseTokenClaimsToUser(tknClaims map[string]interface{}) *domains.User {
+	userExtId := tknClaims["user_id"].(string)
+	userEmail := tknClaims["email"].(string)
+	name := tknClaims["name"].(string)
+
+	return &domains.User{
+		Code:      userExtId,
+		Email:     userEmail,
+		FirstName: name,
+	}
+}
diff --git a/pkg/middlewares/kratos.go b/pkg/middlewares/kratos.go
index 463a057..f16baed 100644
--- a/pkg/middlewares/kratos.go
+++ b/pkg/middlewares/kratos.go
@@ -28,7 +28,7 @@ func (m *MiddlewareManager) KratosAuth(next echo.HandlerFunc) echo.HandlerFunc {
 		var err error
 
 		ctx := c.Request().Context()
-		ss, _, err = m.kratosClient.V0alpha2Api.ToSession(ctx).
+		ss, _, err = m.kratosClient.FrontendApi.ToSession(ctx).
 			Cookie(c.Request().Header.Get("Cookie")).
 			XSessionToken(c.Request().Header.Get("X-Session-Token")).
 			Execute()
diff --git a/pkg/middlewares/middleware.go b/pkg/middlewares/middleware.go
index 073a7e1..34a06d4 100644
--- a/pkg/middlewares/middleware.go
+++ b/pkg/middlewares/middleware.go
@@ -4,17 +4,16 @@ import (
 	"context"
 	"net/http"
 	"reflect"
-	"strings"
 
 	"github.com/dzungtran/echo-rest-api/config"
 	"github.com/dzungtran/echo-rest-api/modules/core/domains"
 	coreRepo "github.com/dzungtran/echo-rest-api/modules/core/repositories"
+	"github.com/dzungtran/echo-rest-api/modules/core/usecases"
 	projectRepo "github.com/dzungtran/echo-rest-api/modules/projects/repositories"
 	"github.com/dzungtran/echo-rest-api/pkg/authz"
 	"github.com/dzungtran/echo-rest-api/pkg/constants"
 	"github.com/dzungtran/echo-rest-api/pkg/contexts"
 	"github.com/dzungtran/echo-rest-api/pkg/kratos"
-	"github.com/dzungtran/echo-rest-api/pkg/logger"
 	"github.com/dzungtran/echo-rest-api/pkg/utils"
 	"github.com/labstack/echo/v4"
 	ory "github.com/ory/kratos-client-go"
@@ -23,62 +22,73 @@ import (
 // MiddlewareManager ...
 // This file contains common functions for auth
 type MiddlewareManager struct {
-	appConf      *config.AppConfig
-	userRepo     coreRepo.UserRepository
-	userOrgRepo  coreRepo.UserOrgRepository
-	projectRepo  projectRepo.ProjectRepository
-	orgRepo      coreRepo.OrgRepository
+	appConf *config.AppConfig
+
+	userRepo    coreRepo.UserRepository
+	userOrgRepo coreRepo.UserOrgRepository
+	orgRepo     coreRepo.OrgRepository
+	projectRepo projectRepo.ProjectRepository
+
+	userUC       usecases.UserUsecase
 	kratosClient *ory.APIClient
 }
 
 // NewMiddlewareManager will create new an MiddlewareManager object
 func NewMiddlewareManager(
 	appConf *config.AppConfig,
+
 	userRepo coreRepo.UserRepository,
 	userOrgRepo coreRepo.UserOrgRepository,
-	projectRepo projectRepo.ProjectRepository,
 	orgRepo coreRepo.OrgRepository,
+	projectRepo projectRepo.ProjectRepository,
+
+	userUC usecases.UserUsecase,
 ) *MiddlewareManager {
 	return &MiddlewareManager{
 		appConf:      appConf,
 		userRepo:     userRepo,
 		userOrgRepo:  userOrgRepo,
-		projectRepo:  projectRepo,
 		orgRepo:      orgRepo,
+		userUC:       userUC,
 		kratosClient: kratos.NewKratosSelfHostedClient(appConf.KratosApiEndpoint, appConf.Environment == "development"),
 	}
 }
 
 func (m MiddlewareManager) Auth() echo.MiddlewareFunc {
 	return func(next echo.HandlerFunc) echo.HandlerFunc {
-		if m.appConf.Environment != "local" {
-			// current just support Kratos Authn
+
+		switch m.appConf.AuthProvider {
+		case "kratos":
 			return m.KratosAuth(next)
+		default:
+			// Default auth method
+			return m.FireBaseAuth(next)
 		}
 
 		// Default auth here for local debug and development
 		// Bypass auth
-		return func(c echo.Context) error {
-			ctx := c.Request().Context()
-			email := c.Request().Header.Get(constants.HeaderXUserEmail)
-
-			u, err := m.fetchUserFromAuth(ctx, "", email)
-			if err != nil {
-				logger.Log().Errorw("error while fetch user for auth", "email", strings.ReplaceAll(email, "\n", ""), "error", err)
-				return c.JSON(http.StatusUnauthorized, map[string]interface{}{
-					"error": "cannot fetch user",
-				})
-			}
-
-			if u.Status != domains.UserStatusActive {
-				return c.JSON(http.StatusUnauthorized, map[string]interface{}{
-					"error": "user is not active",
-				})
-			}
 
-			c.Set(constants.ContextKeyUser, u)
-			return next(c)
-		}
+		// return func(c echo.Context) error {
+		// 	ctx := c.Request().Context()
+		// 	email := c.Request().Header.Get(constants.HeaderXUserEmail)
+
+		// 	u, err := m.fetchUserFromAuth(ctx, "", email)
+		// 	if err != nil {
+		// 		logger.Log().Errorw("error while fetch user for auth", "email", strings.ReplaceAll(email, "\n", ""), "error", err)
+		// 		return c.JSON(http.StatusUnauthorized, map[string]interface{}{
+		// 			"error": "cannot fetch user",
+		// 		})
+		// 	}
+
+		// 	if u.Status != domains.UserStatusActive {
+		// 		return c.JSON(http.StatusUnauthorized, map[string]interface{}{
+		// 			"error": "user is not active",
+		// 		})
+		// 	}
+
+		// 	c.Set(constants.ContextKeyUser, u)
+		// 	return next(c)
+		// }
 	}
 }
 
@@ -176,13 +186,28 @@ func (m MiddlewareManager) CheckPoliciesWithRequestPayload(payloadInst interface
 				})
 			}
 
-			if err := c.Bind(payloadInst); err != nil {
-				return c.JSON(http.StatusBadRequest, map[string]interface{}{
-					"error": err.Error(),
+			currPayload := c.Get(constants.ContextKeyPayload)
+			if currPayload == nil {
+				if err := c.Bind(payloadInst); err != nil {
+					return c.JSON(http.StatusBadRequest, map[string]interface{}{
+						"error": err.Error(),
+					})
+				}
+			} else {
+				payloadInst = currPayload
+			}
+
+			denyMsg, err := authz.CheckPoliciesContext(c, authz.WithInputExtraData("payload", payloadInst))
+			if err != nil {
+				msg := err.Error()
+				if len(denyMsg) > 0 {
+					msg = denyMsg[0]
+				}
+				return c.JSON(http.StatusForbidden, map[string]interface{}{
+					"error": msg,
 				})
 			}
 
-			c.Set(constants.ContextKeyPayload, payloadInst)
 			return next(c)
 		}
 	}
diff --git a/tools/mod/cmd/modgen/template/handlers/placeholder.gotpl b/tools/mod/cmd/modgen/template/handlers/placeholder.gotpl
index 82a78e2..a77f5e0 100644
--- a/tools/mod/cmd/modgen/template/handlers/placeholder.gotpl
+++ b/tools/mod/cmd/modgen/template/handlers/placeholder.gotpl
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"net/http"
 
+	"{{ .RootPackage }}/modules/{{ .PluralName | ToKebab }}/domains"
 	"{{ .RootPackage }}/modules/{{ .PluralName | ToKebab }}/dto"
 	"{{ .RootPackage }}/modules/{{ .PluralName | ToKebab }}/usecases"
 	"{{ .RootPackage }}/pkg/constants"
@@ -42,7 +43,7 @@ func (h *{{ .SingularName }}Handler) Create(c echo.Context) wrapper.Response {
 	var err error
 
 	payload := c.Get(constants.ContextKeyPayload)
-	req, ok := payload.(dto.Create{{ .SingularName }}Req)
+	req, ok := payload.(*dto.Create{{ .SingularName }}Req)
 	if !ok {
 		return wrapper.Response{
 			Status: http.StatusBadRequest,
@@ -82,7 +83,7 @@ func (h *{{ .SingularName }}Handler) GetByID(c echo.Context) wrapper.Response {
 func (h *{{ .SingularName }}Handler) Fetch(c echo.Context) wrapper.Response {
 	ctx := c.Request().Context()
 	payload := c.Get(constants.ContextKeyPayload)
-	req, ok := payload.(dto.Search{{ .PluralName }}Req)
+	req, ok := payload.(*dto.Search{{ .PluralName }}Req)
 	if !ok {
 		return wrapper.Response{
 			Status: http.StatusBadRequest,
diff --git a/tools/mod/cmd/modgen/template/usecases/placeholder.gotpl b/tools/mod/cmd/modgen/template/usecases/placeholder.gotpl
index 5cd7b5c..a92703c 100644
--- a/tools/mod/cmd/modgen/template/usecases/placeholder.gotpl
+++ b/tools/mod/cmd/modgen/template/usecases/placeholder.gotpl
@@ -12,10 +12,10 @@ import (
 
 // {{ .SingularName }}Usecase represent the {{ .SingularName | ToLowerCamel }}'s usecase contract
 type {{ .SingularName }}Usecase interface {
-	Create(ctx context.Context, request dto.Create{{ .SingularName }}Req) (*domains.{{ .SingularName }}, error)
+	Create(ctx context.Context, request *dto.Create{{ .SingularName }}Req) (*domains.{{ .SingularName }}, error)
 	GetByID(ctx context.Context, id int64) (*domains.{{ .SingularName }}, error)
-	Fetch(ctx context.Context, req dto.Search{{ .PluralName }}Req) ([]*domains.{{ .SingularName }}, int64, error)
-	Update(ctx context.Context, id int64, request dto.Update{{ .SingularName }}Req) error
+	Fetch(ctx context.Context, req *dto.Search{{ .PluralName }}Req) ([]*domains.{{ .SingularName }}, int64, error)
+	Update(ctx context.Context, id int64, request *dto.Update{{ .SingularName }}Req) error
 	Delete(ctx context.Context, id int64) error
 }
 
@@ -30,7 +30,7 @@ func New{{ .SingularName }}Usecase({{ .SingularName | ToLowerCamel }}Repo reposi
 	}
 }
 
-func (u *{{ .SingularName | ToLowerCamel }}Usecase) Create(ctx context.Context, req dto.Create{{ .SingularName }}Req) ({{ .SingularName | ToLowerCamel }} *domains.{{ .SingularName }}, err error) {
+func (u *{{ .SingularName | ToLowerCamel }}Usecase) Create(ctx context.Context, req *dto.Create{{ .SingularName }}Req) ({{ .SingularName | ToLowerCamel }} *domains.{{ .SingularName }}, err error) {
 	{{ .SingularName | ToLowerCamel }} = &domains.{{ .SingularName }}{}
 	copier.Copy({{ .SingularName | ToLowerCamel }}, req)
 
@@ -52,7 +52,7 @@ func (u *{{ .SingularName | ToLowerCamel }}Usecase) GetByID(ctx context.Context,
 	return
 }
 
-func (u *{{ .SingularName | ToLowerCamel }}Usecase) Fetch(ctx context.Context, req dto.Search{{ .PluralName }}Req) ({{ .PluralName | ToLowerCamel }} []*domains.{{ .SingularName }}, count int64, err error) {
+func (u *{{ .SingularName | ToLowerCamel }}Usecase) Fetch(ctx context.Context, req *dto.Search{{ .PluralName }}Req) ({{ .PluralName | ToLowerCamel }} []*domains.{{ .SingularName }}, count int64, err error) {
 	p := repositories.ParamsForFetch{{ .PluralName }}{
 		CommonParamsForFetch: contexts.CommonParamsForFetch{
 			Page:  uint64(req.Page),
@@ -68,7 +68,7 @@ func (u *{{ .SingularName | ToLowerCamel }}Usecase) Fetch(ctx context.Context, r
 	return
 }
 
-func (u *{{ .SingularName | ToLowerCamel }}Usecase) Update(ctx context.Context, id int64, req dto.Update{{ .SingularName }}Req) (err error) {
+func (u *{{ .SingularName | ToLowerCamel }}Usecase) Update(ctx context.Context, id int64, req *dto.Update{{ .SingularName }}Req) (err error) {
 	{{ .SingularName | ToLowerCamel }}, err := u.{{ .SingularName | ToLowerCamel }}Repo.GetByID(ctx, id)
 	if err != nil {
 		return err