You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-06-15 00:15:15 +02:00
authenticator storage engines
Created a storage interface used by authenticator to support multiple types of storage types for private keys. Added a new file storage engine which is now the default for web-api. Migrated aws secrets manager to be optional.
This commit is contained in:
@ -7,10 +7,10 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/schema"
|
|
||||||
"github.com/lib/pq"
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/schema"
|
||||||
"github.com/kelseyhightower/envconfig"
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
"github.com/lib/pq"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
|
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
|
||||||
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
|
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
|
||||||
|
@ -5,6 +5,8 @@ LABEL maintainer="lee@geeksinthewoods.com"
|
|||||||
RUN apk --update --no-cache add \
|
RUN apk --update --no-cache add \
|
||||||
git
|
git
|
||||||
|
|
||||||
|
RUN go get -u github.com/swaggo/swag/cmd/swag
|
||||||
|
|
||||||
# go to base project
|
# go to base project
|
||||||
WORKDIR $GOPATH/src/gitlab.com/geeks-accelerator/oss/saas-starter-kit/example-project
|
WORKDIR $GOPATH/src/gitlab.com/geeks-accelerator/oss/saas-starter-kit/example-project
|
||||||
|
|
||||||
@ -26,6 +28,9 @@ COPY cmd/web-api/templates /templates
|
|||||||
|
|
||||||
WORKDIR ./cmd/web-api
|
WORKDIR ./cmd/web-api
|
||||||
|
|
||||||
|
# Update the API documentation.
|
||||||
|
RUN swag init
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /gosrv .
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /gosrv .
|
||||||
|
|
||||||
FROM alpine:3.9
|
FROM alpine:3.9
|
||||||
|
26
example-project/cmd/web-api/README.md
Normal file
26
example-project/cmd/web-api/README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# SaaS Web API
|
||||||
|
|
||||||
|
Copyright 2019, Geeks Accelerator
|
||||||
|
accelerator@geeksinthewoods.com.com
|
||||||
|
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Service exposes a JSON api.
|
||||||
|
|
||||||
|
|
||||||
|
## Local Installation
|
||||||
|
|
||||||
|
### Build
|
||||||
|
```bash
|
||||||
|
go build .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
To build using the docker file, need to be in the project root directory. `Dockerfile` references go.mod in root directory.
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f cmd/web-api/Dockerfile -t saas-web-api .
|
||||||
|
```
|
80
example-project/cmd/web-api/docs/docs.go
Normal file
80
example-project/cmd/web-api/docs/docs.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||||
|
// This file was generated by swaggo/swag at
|
||||||
|
// 2019-06-24 15:42:25.999684 -0800 AKDT m=+0.030714022
|
||||||
|
|
||||||
|
package docs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/alecthomas/template"
|
||||||
|
"github.com/swaggo/swag"
|
||||||
|
)
|
||||||
|
|
||||||
|
var doc = `{
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "This is a sample server celler server.",
|
||||||
|
"title": "SaaS Example API",
|
||||||
|
"termsOfService": "http://geeksinthewoods.com/terms",
|
||||||
|
"contact": {
|
||||||
|
"name": "API Support",
|
||||||
|
"url": "https://gitlab.com/geeks-accelerator/oss/saas-starter-kit",
|
||||||
|
"email": "support@geeksinthewoods.com"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "Apache 2.0",
|
||||||
|
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
|
},
|
||||||
|
"version": "{{.Version}}"
|
||||||
|
},
|
||||||
|
"host": "{{.Host}}",
|
||||||
|
"basePath": "{{.BasePath}}",
|
||||||
|
"paths": {},
|
||||||
|
"securityDefinitions": {
|
||||||
|
"ApiKeyAuth": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header"
|
||||||
|
},
|
||||||
|
"BasicAuth": {
|
||||||
|
"type": "basic"
|
||||||
|
},
|
||||||
|
"OAuth2Password": {
|
||||||
|
"type": "oauth2",
|
||||||
|
"flow": "password",
|
||||||
|
"tokenUrl": "https://example.com/v1/oauth/token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
type swaggerInfo struct {
|
||||||
|
Version string
|
||||||
|
Host string
|
||||||
|
BasePath string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||||
|
var SwaggerInfo swaggerInfo
|
||||||
|
|
||||||
|
type s struct{}
|
||||||
|
|
||||||
|
func (s *s) ReadDoc() string {
|
||||||
|
t, err := template.New("swagger_info").Parse(doc)
|
||||||
|
if err != nil {
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
var tpl bytes.Buffer
|
||||||
|
if err := t.Execute(&tpl, SwaggerInfo); err != nil {
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
return tpl.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
swag.Register(swag.Name, &s{})
|
||||||
|
}
|
36
example-project/cmd/web-api/docs/swagger.json
Normal file
36
example-project/cmd/web-api/docs/swagger.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "This is a sample server celler server.",
|
||||||
|
"title": "SaaS Example API",
|
||||||
|
"termsOfService": "http://geeksinthewoods.com/terms",
|
||||||
|
"contact": {
|
||||||
|
"name": "API Support",
|
||||||
|
"url": "https://gitlab.com/geeks-accelerator/oss/saas-starter-kit",
|
||||||
|
"email": "support@geeksinthewoods.com"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "Apache 2.0",
|
||||||
|
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
|
},
|
||||||
|
"version": "{{.Version}}"
|
||||||
|
},
|
||||||
|
"host": "{{.Host}}",
|
||||||
|
"basePath": "{{.BasePath}}",
|
||||||
|
"paths": {},
|
||||||
|
"securityDefinitions": {
|
||||||
|
"ApiKeyAuth": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header"
|
||||||
|
},
|
||||||
|
"BasicAuth": {
|
||||||
|
"type": "basic"
|
||||||
|
},
|
||||||
|
"OAuth2Password": {
|
||||||
|
"type": "oauth2",
|
||||||
|
"flow": "password",
|
||||||
|
"tokenUrl": "https://example.com/v1/oauth/token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
example-project/cmd/web-api/docs/swagger.yaml
Normal file
27
example-project/cmd/web-api/docs/swagger.yaml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
basePath: '{{.BasePath}}'
|
||||||
|
host: '{{.Host}}'
|
||||||
|
info:
|
||||||
|
contact:
|
||||||
|
email: support@geeksinthewoods.com
|
||||||
|
name: API Support
|
||||||
|
url: https://gitlab.com/geeks-accelerator/oss/saas-starter-kit
|
||||||
|
description: This is a sample server celler server.
|
||||||
|
license:
|
||||||
|
name: Apache 2.0
|
||||||
|
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
termsOfService: http://geeksinthewoods.com/terms
|
||||||
|
title: SaaS Example API
|
||||||
|
version: '{{.Version}}'
|
||||||
|
paths: {}
|
||||||
|
securityDefinitions:
|
||||||
|
ApiKeyAuth:
|
||||||
|
in: header
|
||||||
|
name: Authorization
|
||||||
|
type: apiKey
|
||||||
|
BasicAuth:
|
||||||
|
type: basic
|
||||||
|
OAuth2Password:
|
||||||
|
flow: password
|
||||||
|
tokenUrl: https://example.com/v1/oauth/token
|
||||||
|
type: oauth2
|
||||||
|
swagger: "2.0"
|
191
example-project/cmd/web-api/handlers/account.go
Normal file
191
example-project/cmd/web-api/handlers/account.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Account represents the Account API method handler set.
|
||||||
|
type Account struct {
|
||||||
|
MasterDB *sqlx.DB
|
||||||
|
|
||||||
|
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns all the existing accounts in the system.
|
||||||
|
func (a *Account) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req account.AccountFindRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
return errors.Wrap(err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := account.Find(ctx, claims, a.MasterDB, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, res, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read returns the specified account from the system.
|
||||||
|
func (a *Account) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var includeArchived bool
|
||||||
|
if qv := r.URL.Query().Get("include-archived"); qv != "" {
|
||||||
|
var err error
|
||||||
|
includeArchived, err = strconv.ParseBool(qv)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Invalid value for include-archived : %s", qv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := account.Read(ctx, claims, a.MasterDB, params["id"], includeArchived)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case account.ErrInvalidID:
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
case account.ErrNotFound:
|
||||||
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
case account.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "ID: %s", params["id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, res, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create inserts a new account into the system.
|
||||||
|
func (a *Account) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req account.AccountCreateRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
return errors.Wrap(err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := account.Create(ctx, claims, a.MasterDB, req, v.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case account.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "User: %+v", &req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, res, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the specified account in the system.
|
||||||
|
func (a *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req account.AccountUpdateRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
return errors.Wrap(err, "")
|
||||||
|
}
|
||||||
|
req.ID = params["id"]
|
||||||
|
|
||||||
|
err := account.Update(ctx, claims, a.MasterDB, req, v.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case account.ErrInvalidID:
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
case account.ErrNotFound:
|
||||||
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
case account.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "Id: %s Account: %+v", params["id"], &req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive soft-deletes the specified account from the system.
|
||||||
|
func (a *Account) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := account.Archive(ctx, claims, a.MasterDB, params["id"], v.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case account.ErrInvalidID:
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
case account.ErrNotFound:
|
||||||
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
case account.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "Id: %s", params["id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the specified account from the system.
|
||||||
|
func (a *Account) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := account.Delete(ctx, claims, a.MasterDB, params["id"])
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case account.ErrInvalidID:
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
case account.ErrNotFound:
|
||||||
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
case account.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "Id: %s", params["id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
@ -6,20 +6,31 @@ import (
|
|||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check provides support for orchestration health checks.
|
// Check provides support for orchestration health checks.
|
||||||
type Check struct {
|
type Check struct {
|
||||||
MasterDB *sqlx.DB
|
MasterDB *sqlx.DB
|
||||||
|
Redis *redis.Client
|
||||||
|
|
||||||
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
|
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health validates the service is healthy and ready to accept requests.
|
// Health validates the service is healthy and ready to accept requests.
|
||||||
func (c *Check) Health(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (c *Check) Health(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
|
// check postgres
|
||||||
_, err := c.MasterDB.Exec("SELECT 1")
|
_, err := c.MasterDB.Exec("SELECT 1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "Postgres failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check redis
|
||||||
|
err = c.Redis.Ping().Err()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Redis failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
status := struct {
|
status := struct {
|
||||||
|
@ -3,7 +3,9 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
@ -18,30 +20,56 @@ type Project struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List returns all the existing projects in the system.
|
// List returns all the existing projects in the system.
|
||||||
func (p *Project) List(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
projects, err := project.List(ctx, p.MasterDB)
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req project.ProjectFindRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
return errors.Wrap(err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := project.Find(ctx, claims, p.MasterDB, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, projects, http.StatusOK)
|
return web.RespondJson(ctx, w, res, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve returns the specified project from the system.
|
// Read returns the specified project from the system.
|
||||||
func (p *Project) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (p *Project) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
prod, err := project.Retrieve(ctx, p.MasterDB, params["id"])
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var includeArchived bool
|
||||||
|
if qv := r.URL.Query().Get("include-archived"); qv != "" {
|
||||||
|
var err error
|
||||||
|
includeArchived, err = strconv.ParseBool(qv)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Invalid value for include-archived : %s", qv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := project.Read(ctx, claims, p.MasterDB, params["id"], includeArchived)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case project.ErrInvalidID:
|
case project.ErrInvalidID:
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
case project.ErrNotFound:
|
case project.ErrNotFound:
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
case project.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "ID: %s", params["id"])
|
return errors.Wrapf(err, "ID: %s", params["id"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, prod, http.StatusOK)
|
return web.RespondJson(ctx, w, res, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create inserts a new project into the system.
|
// Create inserts a new project into the system.
|
||||||
@ -51,17 +79,27 @@ func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
return web.NewShutdownError("web value missing from context")
|
return web.NewShutdownError("web value missing from context")
|
||||||
}
|
}
|
||||||
|
|
||||||
var np project.NewProject
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
if err := web.Decode(r, &np); err != nil {
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req project.ProjectCreateRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
return errors.Wrap(err, "")
|
return errors.Wrap(err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
nUsr, err := project.Create(ctx, p.MasterDB, &np, v.Now)
|
res, err := project.Create(ctx, claims, p.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "Project: %+v", &np)
|
switch err {
|
||||||
|
case project.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "Project: %+v", &req)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, nUsr, http.StatusCreated)
|
return web.RespondJson(ctx, w, res, http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates the specified project in the system.
|
// Update updates the specified project in the system.
|
||||||
@ -71,20 +109,57 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
return web.NewShutdownError("web value missing from context")
|
return web.NewShutdownError("web value missing from context")
|
||||||
}
|
}
|
||||||
|
|
||||||
var up project.UpdateProject
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
if err := web.Decode(r, &up); err != nil {
|
if !ok {
|
||||||
return errors.Wrap(err, "")
|
return errors.New("claims missing from context")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := project.Update(ctx, p.MasterDB, params["id"], up, v.Now)
|
var req project.ProjectUpdateRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
return errors.Wrap(err, "")
|
||||||
|
}
|
||||||
|
req.ID = params["id"]
|
||||||
|
|
||||||
|
err := project.Update(ctx, claims, p.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case project.ErrInvalidID:
|
case project.ErrInvalidID:
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
case project.ErrNotFound:
|
case project.ErrNotFound:
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
case project.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "ID: %s Update: %+v", params["id"], up)
|
return errors.Wrapf(err, "ID: %s Update: %+v", params["id"], req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive soft-deletes the specified project from the system.
|
||||||
|
func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := project.Archive(ctx, claims, p.MasterDB, params["id"], v.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case project.ErrInvalidID:
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
case project.ErrNotFound:
|
||||||
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
case project.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "Id: %s", params["id"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,13 +168,20 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
// Delete removes the specified project from the system.
|
// Delete removes the specified project from the system.
|
||||||
func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
err := project.Delete(ctx, p.MasterDB, params["id"])
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := project.Delete(ctx, claims, p.MasterDB, params["id"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case project.ErrInvalidID:
|
case project.ErrInvalidID:
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
case project.ErrNotFound:
|
case project.ErrNotFound:
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
case project.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "Id: %s", params["id"])
|
return errors.Wrapf(err, "Id: %s", params["id"])
|
||||||
}
|
}
|
||||||
|
@ -6,13 +6,15 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid"
|
||||||
|
saasSwagger "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid/saas-swagger"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
// API returns a handler for a set of routes.
|
// API returns a handler for a set of routes.
|
||||||
func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, authenticator *auth.Authenticator) http.Handler {
|
func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator) http.Handler {
|
||||||
|
|
||||||
// Construct the web.App which holds all routes as well as common Middleware.
|
// Construct the web.App which holds all routes as well as common Middleware.
|
||||||
app := web.NewApp(shutdown, log, mid.Trace(), mid.Logger(log), mid.Errors(log), mid.Metrics(), mid.Panics())
|
app := web.NewApp(shutdown, log, mid.Trace(), mid.Logger(log), mid.Errors(log), mid.Metrics(), mid.Panics())
|
||||||
@ -28,24 +30,43 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, authentica
|
|||||||
MasterDB: masterDB,
|
MasterDB: masterDB,
|
||||||
TokenGenerator: authenticator,
|
TokenGenerator: authenticator,
|
||||||
}
|
}
|
||||||
app.Handle("GET", "/v1/users", u.List, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("GET", "/v1/users", u.Find, mid.Authenticate(authenticator))
|
||||||
app.Handle("POST", "/v1/users", u.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("POST", "/v1/users", u.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("GET", "/v1/users/:id", u.Retrieve, mid.Authenticate(authenticator))
|
app.Handle("GET", "/v1/users/:id", u.Read, mid.Authenticate(authenticator))
|
||||||
app.Handle("PUT", "/v1/users/:id", u.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("PATCH", "/v1/users/:id", u.Update, mid.Authenticate(authenticator))
|
||||||
|
app.Handle("PATCH", "/v1/users/:id/password", u.UpdatePassword, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("PATCH", "/v1/users/:id/archive", u.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("PATCH", "/v1/users/switch-account/:accountId", u.SwitchAccount, mid.Authenticate(authenticator))
|
||||||
|
|
||||||
// This route is not authenticated
|
// This route is not authenticated
|
||||||
app.Handle("GET", "/v1/users/token", u.Token)
|
app.Handle("GET", "/v1/oauth/token", u.Token)
|
||||||
|
|
||||||
// Register project and sale endpoints.
|
// Register account endpoints.
|
||||||
|
a := Account{
|
||||||
|
MasterDB: masterDB,
|
||||||
|
}
|
||||||
|
app.Handle("GET", "/v1/accounts", a.Find, mid.Authenticate(authenticator))
|
||||||
|
app.Handle("POST", "/v1/accounts", a.Create, mid.Authenticate(authenticator))
|
||||||
|
app.Handle("GET", "/v1/accounts/:id", a.Read, mid.Authenticate(authenticator))
|
||||||
|
app.Handle("PATCH", "/v1/accounts/:id", a.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("PATCH", "/v1/accounts/:id/archive", a.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("DELETE", "/v1/accounts/:id", a.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
|
||||||
|
// Register project.
|
||||||
p := Project{
|
p := Project{
|
||||||
MasterDB: masterDB,
|
MasterDB: masterDB,
|
||||||
}
|
}
|
||||||
app.Handle("GET", "/v1/projects", p.List, mid.Authenticate(authenticator))
|
app.Handle("GET", "/v1/projects", p.Find, mid.Authenticate(authenticator))
|
||||||
app.Handle("POST", "/v1/projects", p.Create, mid.Authenticate(authenticator))
|
app.Handle("POST", "/v1/projects", p.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("GET", "/v1/projects/:id", p.Retrieve, mid.Authenticate(authenticator))
|
app.Handle("GET", "/v1/projects/:id", p.Read, mid.Authenticate(authenticator))
|
||||||
app.Handle("PUT", "/v1/projects/:id", p.Update, mid.Authenticate(authenticator))
|
app.Handle("PATCH", "/v1/projects/:id", p.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator))
|
app.Handle("PATCH", "/v1/projects/:id/archive", p.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
|
||||||
|
// Register swagger documentation.
|
||||||
|
app.Handle("GET", "/swagger/", saasSwagger.WrapHandler, mid.Authenticate(authenticator))
|
||||||
|
app.Handle("GET", "/swagger/*", saasSwagger.WrapHandler, mid.Authenticate(authenticator))
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
@ -12,6 +13,9 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// sessionTtl defines the auth token expiration.
|
||||||
|
var sessionTtl = time.Hour * 24
|
||||||
|
|
||||||
// User represents the User API method handler set.
|
// User represents the User API method handler set.
|
||||||
type User struct {
|
type User struct {
|
||||||
MasterDB *sqlx.DB
|
MasterDB *sqlx.DB
|
||||||
@ -21,7 +25,7 @@ type User struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List returns all the existing users in the system.
|
// List returns all the existing users in the system.
|
||||||
func (u *User) List(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("claims missing from context")
|
return errors.New("claims missing from context")
|
||||||
@ -32,22 +36,160 @@ func (u *User) List(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
|||||||
return errors.Wrap(err, "")
|
return errors.Wrap(err, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
usrs, err := user.Find(ctx, claims, u.MasterDB, req)
|
res, err := user.Find(ctx, claims, u.MasterDB, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, usrs, http.StatusOK)
|
return web.RespondJson(ctx, w, res, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve returns the specified user from the system.
|
// Read returns the specified user from the system.
|
||||||
func (u *User) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("claims missing from context")
|
return errors.New("claims missing from context")
|
||||||
}
|
}
|
||||||
|
|
||||||
usr, err := user.FindById(ctx, claims, u.MasterDB, params["id"], false)
|
var includeArchived bool
|
||||||
|
if qv := r.URL.Query().Get("include-archived"); qv != "" {
|
||||||
|
var err error
|
||||||
|
includeArchived, err = strconv.ParseBool(qv)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Invalid value for include-archived : %s", qv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := user.Read(ctx, claims, u.MasterDB, params["id"], includeArchived)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case user.ErrInvalidID:
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
case user.ErrNotFound:
|
||||||
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
case user.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "ID: %s", params["id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, res, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create inserts a new user into the system.
|
||||||
|
func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req user.UserCreateRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
return errors.Wrap(err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := user.Create(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case user.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "User: %+v", &req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, res, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the specified user in the system.
|
||||||
|
func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req user.UserUpdateRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
return errors.Wrap(err, "")
|
||||||
|
}
|
||||||
|
req.ID = params["id"]
|
||||||
|
|
||||||
|
err := user.Update(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case user.ErrInvalidID:
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
case user.ErrNotFound:
|
||||||
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
case user.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "Id: %s User: %+v", params["id"], &req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the password for a specified user in the system.
|
||||||
|
func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req user.UserUpdatePasswordRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
return errors.Wrap(err, "")
|
||||||
|
}
|
||||||
|
req.ID = params["id"]
|
||||||
|
|
||||||
|
err := user.UpdatePassword(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case user.ErrInvalidID:
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
case user.ErrNotFound:
|
||||||
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
case user.ErrForbidden:
|
||||||
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "Id: %s User: %+v", params["id"], &req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive soft-deletes the specified user from the system.
|
||||||
|
func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := user.Archive(ctx, claims, u.MasterDB, params["id"], v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case user.ErrInvalidID:
|
case user.ErrInvalidID:
|
||||||
@ -61,66 +203,6 @@ func (u *User) Retrieve(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, usr, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create inserts a new user into the system.
|
|
||||||
func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("claims missing from context")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
|
||||||
if !ok {
|
|
||||||
return web.NewShutdownError("web value missing from context")
|
|
||||||
}
|
|
||||||
|
|
||||||
var newU user.CreateUserRequest
|
|
||||||
if err := web.Decode(r, &newU); err != nil {
|
|
||||||
return errors.Wrap(err, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
usr, err := user.Create(ctx, claims, u.MasterDB, newU, v.Now)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "User: %+v", &usr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, usr, http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update updates the specified user in the system.
|
|
||||||
func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("claims missing from context")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
|
||||||
if !ok {
|
|
||||||
return web.NewShutdownError("web value missing from context")
|
|
||||||
}
|
|
||||||
|
|
||||||
var upd user.UpdateUserRequest
|
|
||||||
if err := web.Decode(r, &upd); err != nil {
|
|
||||||
return errors.Wrap(err, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := user.Update(ctx, claims, u.MasterDB, upd, v.Now)
|
|
||||||
if err != nil {
|
|
||||||
switch err {
|
|
||||||
case user.ErrInvalidID:
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case user.ErrNotFound:
|
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
|
||||||
case user.ErrForbidden:
|
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
|
||||||
default:
|
|
||||||
return errors.Wrapf(err, "Id: %s User: %+v", params["id"], &upd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +230,31 @@ func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SwitchAccount updates the claims.
|
||||||
|
func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn, err := user.SwitchAccount(ctx, u.MasterDB, u.TokenGenerator, claims, params["accountId"], sessionTtl, v.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case user.ErrAuthenticationFailure:
|
||||||
|
return web.NewRequestError(err, http.StatusUnauthorized)
|
||||||
|
default:
|
||||||
|
return errors.Wrap(err, "switch account")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, tkn, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
// Token handles a request to authenticate a user. It expects a request using
|
// Token handles a request to authenticate a user. It expects a request using
|
||||||
// Basic Auth with a user's email and password. It responds with a JWT.
|
// Basic Auth with a user's email and password. It responds with a JWT.
|
||||||
func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
@ -162,8 +269,7 @@ func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request
|
|||||||
return web.NewRequestError(err, http.StatusUnauthorized)
|
return web.NewRequestError(err, http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Constant for token lifespan?
|
tkn, err := user.Authenticate(ctx, u.MasterDB, u.TokenGenerator, email, pass, sessionTtl, v.Now)
|
||||||
tkn, err := user.Authenticate(ctx, u.MasterDB, u.TokenGenerator, email, pass, time.Hour * 24, v.Now)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case user.ErrAuthenticationFailure:
|
case user.ErrAuthenticationFailure:
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"expvar"
|
"expvar"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
@ -16,6 +15,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/docs"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
|
||||||
@ -29,6 +29,7 @@ import (
|
|||||||
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
|
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
|
||||||
redistrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
redistrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||||
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
|
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
|
||||||
|
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// build is the git version of this program. It is set using build flags in the makefile.
|
// build is the git version of this program. It is set using build flags in the makefile.
|
||||||
@ -39,6 +40,26 @@ var build = "develop"
|
|||||||
// ie: export WEB_API_ENV=dev
|
// ie: export WEB_API_ENV=dev
|
||||||
var service = "WEB_API"
|
var service = "WEB_API"
|
||||||
|
|
||||||
|
// @title SaaS Example API
|
||||||
|
// @description This is a sample server celler server.
|
||||||
|
// @termsOfService http://geeksinthewoods.com/terms
|
||||||
|
|
||||||
|
// @contact.name API Support
|
||||||
|
// @contact.email support@geeksinthewoods.com
|
||||||
|
// @contact.url https://gitlab.com/geeks-accelerator/oss/saas-starter-kit
|
||||||
|
|
||||||
|
// @license.name Apache 2.0
|
||||||
|
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
// @securityDefinitions.basic BasicAuth
|
||||||
|
|
||||||
|
// @securityDefinitions.apikey ApiKeyAuth
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
|
||||||
|
// @securitydefinitions.oauth2.password OAuth2Password
|
||||||
|
// @tokenUrl https://example.com/v1/oauth/token
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@ -98,6 +119,7 @@ func main() {
|
|||||||
UseRole bool `envconfig:"AWS_USE_ROLE"`
|
UseRole bool `envconfig:"AWS_USE_ROLE"`
|
||||||
}
|
}
|
||||||
Auth struct {
|
Auth struct {
|
||||||
|
UseAwsSecretManager bool `default:false envconfig:"USE_AWS_SECRET_MANAGER"`
|
||||||
AwsSecretID string `default:"auth-secret-key" envconfig:"AWS_SECRET_ID"`
|
AwsSecretID string `default:"auth-secret-key" envconfig:"AWS_SECRET_ID"`
|
||||||
KeyExpiration time.Duration `default:"3600s" envconfig:"KEY_EXPIRATION"`
|
KeyExpiration time.Duration `default:"3600s" envconfig:"KEY_EXPIRATION"`
|
||||||
}
|
}
|
||||||
@ -252,8 +274,13 @@ func main() {
|
|||||||
defer masterDb.Close()
|
defer masterDb.Close()
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Load auth keys from AWS and init new Authenticator
|
// Init new Authenticator
|
||||||
authenticator, err := auth.NewAuthenticator(awsSession, cfg.Auth.AwsSecretID, time.Now().UTC(), cfg.Auth.KeyExpiration)
|
var authenticator *auth.Authenticator
|
||||||
|
if cfg.Auth.UseAwsSecretManager {
|
||||||
|
authenticator, err = auth.NewAuthenticatorAws(awsSession, cfg.Auth.AwsSecretID, time.Now().UTC(), cfg.Auth.KeyExpiration)
|
||||||
|
} else {
|
||||||
|
authenticator, err = auth.NewAuthenticatorFile("", time.Now().UTC(), cfg.Auth.KeyExpiration)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("main : Constructing authenticator : %v", err)
|
log.Fatalf("main : Constructing authenticator : %v", err)
|
||||||
}
|
}
|
||||||
@ -282,6 +309,19 @@ func main() {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Start API Service
|
// Start API Service
|
||||||
|
|
||||||
|
// Programmatically set swagger info.
|
||||||
|
{
|
||||||
|
docs.SwaggerInfo.Version = build
|
||||||
|
|
||||||
|
u, err := url.Parse(cfg.App.BaseUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("main : Parse app base url %s : %v", cfg.App.BaseUrl, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
docs.SwaggerInfo.Host = u.Host
|
||||||
|
docs.SwaggerInfo.BasePath = "/v1"
|
||||||
|
}
|
||||||
|
|
||||||
// Make a channel to listen for an interrupt or terminate signal from the OS.
|
// Make a channel to listen for an interrupt or terminate signal from the OS.
|
||||||
// Use a buffered channel because the signal package requires it.
|
// Use a buffered channel because the signal package requires it.
|
||||||
shutdown := make(chan os.Signal, 1)
|
shutdown := make(chan os.Signal, 1)
|
||||||
@ -289,7 +329,7 @@ func main() {
|
|||||||
|
|
||||||
api := http.Server{
|
api := http.Server{
|
||||||
Addr: cfg.HTTP.Host,
|
Addr: cfg.HTTP.Host,
|
||||||
Handler: handlers.API(shutdown, log, masterDb, authenticator),
|
Handler: handlers.API(shutdown, log, masterDb, redisClient, authenticator),
|
||||||
ReadTimeout: cfg.HTTP.ReadTimeout,
|
ReadTimeout: cfg.HTTP.ReadTimeout,
|
||||||
WriteTimeout: cfg.HTTP.WriteTimeout,
|
WriteTimeout: cfg.HTTP.WriteTimeout,
|
||||||
MaxHeaderBytes: 1 << 20,
|
MaxHeaderBytes: 1 << 20,
|
||||||
|
@ -30,6 +30,8 @@ func TestProjects(t *testing.T) {
|
|||||||
t.Run("crudProjects", crudProject)
|
t.Run("crudProjects", crudProject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: need to test Archive
|
||||||
|
|
||||||
// getProjects200Empty validates an empty projects list can be retrieved with the endpoint.
|
// getProjects200Empty validates an empty projects list can be retrieved with the endpoint.
|
||||||
func getProjects200Empty(t *testing.T) {
|
func getProjects200Empty(t *testing.T) {
|
||||||
r := httptest.NewRequest("GET", "/v1/projects", nil)
|
r := httptest.NewRequest("GET", "/v1/projects", nil)
|
||||||
|
27
example-project/cmd/web-app/README.md
Normal file
27
example-project/cmd/web-app/README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# SaaS Web App
|
||||||
|
|
||||||
|
Copyright 2019, Geeks Accelerator
|
||||||
|
accelerator@geeksinthewoods.com.com
|
||||||
|
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Provides an http service.
|
||||||
|
|
||||||
|
|
||||||
|
## Local Installation
|
||||||
|
|
||||||
|
### Build
|
||||||
|
```bash
|
||||||
|
go build .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
To build using the docker file, need to be in the project root directory. `Dockerfile` references go.mod in root directory.
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f cmd/web-app/Dockerfile -t saas-web-app .
|
||||||
|
```
|
||||||
|
|
@ -2,15 +2,18 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check provides support for orchestration health checks.
|
// Check provides support for orchestration health checks.
|
||||||
type Check struct {
|
type Check struct {
|
||||||
MasterDB *sqlx.DB
|
MasterDB *sqlx.DB
|
||||||
|
Redis *redis.Client
|
||||||
Renderer web.Renderer
|
Renderer web.Renderer
|
||||||
|
|
||||||
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
|
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
|
||||||
@ -22,7 +25,13 @@ func (c *Check) Health(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
// check postgres
|
// check postgres
|
||||||
_, err := c.MasterDB.Exec("SELECT 1")
|
_, err := c.MasterDB.Exec("SELECT 1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "Postgres failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check redis
|
||||||
|
err = c.Redis.Ping().Err()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Redis failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
|
@ -112,6 +112,7 @@ func main() {
|
|||||||
UseRole bool `envconfig:"AWS_USE_ROLE"`
|
UseRole bool `envconfig:"AWS_USE_ROLE"`
|
||||||
}
|
}
|
||||||
Auth struct {
|
Auth struct {
|
||||||
|
UseAwsSecretManager bool `default:false envconfig:"USE_AWS_SECRET_MANAGER"`
|
||||||
AwsSecretID string `default:"auth-secret-key" envconfig:"AWS_SECRET_ID"`
|
AwsSecretID string `default:"auth-secret-key" envconfig:"AWS_SECRET_ID"`
|
||||||
KeyExpiration time.Duration `default:"3600s" envconfig:"KEY_EXPIRATION"`
|
KeyExpiration time.Duration `default:"3600s" envconfig:"KEY_EXPIRATION"`
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,27 @@
|
|||||||
module geeks-accelerator/oss/saas-starter-kit/example-project
|
module geeks-accelerator/oss/saas-starter-kit/example-project
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
|
||||||
github.com/aws/aws-sdk-go v1.19.33
|
github.com/aws/aws-sdk-go v1.19.33
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
github.com/dimfeld/httptreemux v5.0.1+incompatible
|
github.com/dimfeld/httptreemux v5.0.1+incompatible
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/fatih/camelcase v1.0.0
|
github.com/fatih/camelcase v1.0.0
|
||||||
github.com/fatih/structtag v1.0.0
|
github.com/fatih/structtag v1.0.0
|
||||||
github.com/geeks-accelerator/sqlxmigrate v0.0.0-20190527223850-4a863a2d30db
|
github.com/geeks-accelerator/sqlxmigrate v0.0.0-20190527223850-4a863a2d30db
|
||||||
|
github.com/go-openapi/spec v0.19.2 // indirect
|
||||||
github.com/go-playground/locales v0.12.1
|
github.com/go-playground/locales v0.12.1
|
||||||
github.com/go-playground/universal-translator v0.16.0
|
github.com/go-playground/universal-translator v0.16.0
|
||||||
github.com/go-redis/redis v6.15.2+incompatible
|
github.com/go-redis/redis v6.15.2+incompatible
|
||||||
github.com/golang/protobuf v1.3.1 // indirect
|
|
||||||
github.com/google/go-cmp v0.2.0
|
github.com/google/go-cmp v0.2.0
|
||||||
|
github.com/gorilla/schema v1.1.0
|
||||||
github.com/huandu/go-sqlbuilder v1.4.0
|
github.com/huandu/go-sqlbuilder v1.4.0
|
||||||
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365
|
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365
|
||||||
github.com/jmoiron/sqlx v1.2.0
|
github.com/jmoiron/sqlx v1.2.0
|
||||||
github.com/kelseyhightower/envconfig v1.3.0
|
github.com/kelseyhightower/envconfig v1.3.0
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
|
||||||
github.com/leodido/go-urn v1.1.0 // indirect
|
github.com/leodido/go-urn v1.1.0 // indirect
|
||||||
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01
|
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481 // indirect
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
github.com/onsi/ginkgo v1.8.0 // indirect
|
github.com/onsi/ginkgo v1.8.0 // indirect
|
||||||
github.com/onsi/gomega v1.5.0
|
github.com/onsi/gomega v1.5.0
|
||||||
@ -30,16 +31,17 @@ require (
|
|||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/sergi/go-diff v1.0.0
|
github.com/sergi/go-diff v1.0.0
|
||||||
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0
|
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0
|
||||||
|
github.com/stretchr/testify v1.3.0
|
||||||
|
github.com/swaggo/files v0.0.0-20190110041405-30649e0721f8
|
||||||
|
github.com/swaggo/swag v1.5.1
|
||||||
github.com/tinylib/msgp v1.1.0 // indirect
|
github.com/tinylib/msgp v1.1.0 // indirect
|
||||||
github.com/urfave/cli v1.20.0
|
github.com/urfave/cli v1.20.0
|
||||||
github.com/uudashr/go-module v0.0.0-20180827225833-c0ca9c3a4966 // indirect
|
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4
|
||||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20190526052359-791d8a0f4d09 // indirect
|
golang.org/x/tools v0.0.0-20190624190245-7f2218787638 // indirect
|
||||||
golang.org/x/text v0.3.2 // indirect
|
|
||||||
google.golang.org/appengine v1.6.0 // indirect
|
google.golang.org/appengine v1.6.0 // indirect
|
||||||
gopkg.in/DataDog/dd-trace-go.v1 v1.14.0
|
gopkg.in/DataDog/dd-trace-go.v1 v1.14.0
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||||
gopkg.in/go-playground/validator.v9 v9.29.0
|
gopkg.in/go-playground/validator.v9 v9.29.0
|
||||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce
|
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/aws/aws-sdk-go v1.19.33 h1:qz9ZQtxCUuwBKdc5QiY6hKuISYGeRQyLVA2RryDEDaQ=
|
github.com/aws/aws-sdk-go v1.19.33 h1:qz9ZQtxCUuwBKdc5QiY6hKuISYGeRQyLVA2RryDEDaQ=
|
||||||
github.com/aws/aws-sdk-go v1.19.33/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
github.com/aws/aws-sdk-go v1.19.33/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
@ -18,6 +25,21 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
|
|||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/geeks-accelerator/sqlxmigrate v0.0.0-20190527223850-4a863a2d30db h1:mjErP7mTFHQ3cw/ibAkW3CvQ8gM4k19EkfzRzRINDAE=
|
github.com/geeks-accelerator/sqlxmigrate v0.0.0-20190527223850-4a863a2d30db h1:mjErP7mTFHQ3cw/ibAkW3CvQ8gM4k19EkfzRzRINDAE=
|
||||||
github.com/geeks-accelerator/sqlxmigrate v0.0.0-20190527223850-4a863a2d30db/go.mod h1:dzpCjo4q7chhMVuHDzs/odROkieZ5Wjp70rNDuX83jU=
|
github.com/geeks-accelerator/sqlxmigrate v0.0.0-20190527223850-4a863a2d30db/go.mod h1:dzpCjo4q7chhMVuHDzs/odROkieZ5Wjp70rNDuX83jU=
|
||||||
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.2 h1:A9+F4Dc/MCNB5jibxf6rRvOvR/iFgQdyNx9eIhnGqq0=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
||||||
|
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
|
||||||
|
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||||
|
github.com/go-openapi/spec v0.19.2 h1:SStNd1jRcYtfKCN7R0laGNs80WYYvn5CbBjM2sOmCrE=
|
||||||
|
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
|
||||||
|
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||||
|
github.com/go-openapi/swag v0.19.2 h1:jvO6bCMBEilGwMfHhrd61zIID4oIFdwb76V17SM88dE=
|
||||||
|
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
||||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||||
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
||||||
@ -27,11 +49,12 @@ github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w
|
|||||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
|
||||||
|
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/huandu/go-sqlbuilder v1.4.0 h1:2LIlTDOz63lOETLOIiKBPEu4PUbikmS5LUc3EekwYqM=
|
github.com/huandu/go-sqlbuilder v1.4.0 h1:2LIlTDOz63lOETLOIiKBPEu4PUbikmS5LUc3EekwYqM=
|
||||||
@ -49,6 +72,7 @@ github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa
|
|||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
|
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
|
||||||
@ -57,6 +81,10 @@ github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
|||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01 h1:EPw7R3OAyxHBCyl0oqh3lUZqS5lu3KSxzzGasE0opXQ=
|
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01 h1:EPw7R3OAyxHBCyl0oqh3lUZqS5lu3KSxzzGasE0opXQ=
|
||||||
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481 h1:IaSjLMT6WvkoZZjspGxy3rdaTEmWLoRm49WbtVUi9sA=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
@ -82,34 +110,49 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
|
|||||||
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 h1:X9XMOYjxEfAYSy3xK1DzO5dMkkWhs9E9UCcS1IERx2k=
|
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 h1:X9XMOYjxEfAYSy3xK1DzO5dMkkWhs9E9UCcS1IERx2k=
|
||||||
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns=
|
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/swaggo/files v0.0.0-20190110041405-30649e0721f8 h1:ENF9W2s6+pqe/CmdQTQFPuzSdCB91LQ3WWzdMWucs7c=
|
||||||
|
github.com/swaggo/files v0.0.0-20190110041405-30649e0721f8/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
|
||||||
|
github.com/swaggo/swag v1.5.1 h1:2Agm8I4K5qb00620mHq0VJ05/KT4FtmALPIcQR9lEZM=
|
||||||
|
github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
|
||||||
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
|
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
|
||||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||||
github.com/uudashr/go-module v0.0.0-20180827225833-c0ca9c3a4966 h1:7dS/ZO0dIwrtj/FGTt9I6urVpx7LEHzucegv4ORYK3M=
|
|
||||||
github.com/uudashr/go-module v0.0.0-20180827225833-c0ca9c3a4966/go.mod h1:P6Nk1sQWL6jcdBIxnLVlqCsOl0arao7gg7sPoM6gx4A=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
|
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
|
||||||
|
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190526052359-791d8a0f4d09 h1:IlD35wZE03o2qJy2o37WIskL33b7PT6cHdGnE8bieZs=
|
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190526052359-791d8a0f4d09/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
|
||||||
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190624190245-7f2218787638 h1:uIfBkD8gLczr4XDgYpt/qJYds2YJwZRNw4zs7wSnNhk=
|
||||||
|
golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw=
|
google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw=
|
||||||
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
@ -130,5 +173,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
|
|||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
@ -257,7 +257,7 @@ func uniqueName(ctx context.Context, dbConn *sqlx.DB, name, accountId string) (b
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create inserts a new account into the database.
|
// Create inserts a new account into the database.
|
||||||
func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req CreateAccountRequest, now time.Time) (*Account, error) {
|
func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req AccountCreateRequest, now time.Time) (*Account, error) {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.account.Create")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.account.Create")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
@ -364,7 +364,7 @@ func Read(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, i
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update replaces an account in the database.
|
// Update replaces an account in the database.
|
||||||
func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UpdateAccountRequest, now time.Time) error {
|
func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req AccountUpdateRequest, now time.Time) error {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.account.Update")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.account.Update")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
|
@ -29,8 +29,8 @@ type Account struct {
|
|||||||
ArchivedAt pq.NullTime `json:"archived_at"`
|
ArchivedAt pq.NullTime `json:"archived_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAccountRequest contains information needed to create a new Account.
|
// AccountCreateRequest contains information needed to create a new Account.
|
||||||
type CreateAccountRequest struct {
|
type AccountCreateRequest struct {
|
||||||
Name string `json:"name" validate:"required,unique"`
|
Name string `json:"name" validate:"required,unique"`
|
||||||
Address1 string `json:"address1" validate:"required"`
|
Address1 string `json:"address1" validate:"required"`
|
||||||
Address2 string `json:"address2" validate:"omitempty"`
|
Address2 string `json:"address2" validate:"omitempty"`
|
||||||
@ -44,13 +44,13 @@ type CreateAccountRequest struct {
|
|||||||
BillingUserID *string `json:"billing_user_id" validate:"omitempty,uuid"`
|
BillingUserID *string `json:"billing_user_id" validate:"omitempty,uuid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAccountRequest defines what information may be provided to modify an existing
|
// AccountUpdateRequest defines what information may be provided to modify an existing
|
||||||
// Account. All fields are optional so clients can send just the fields they want
|
// Account. All fields are optional so clients can send just the fields they want
|
||||||
// changed. It uses pointer fields so we can differentiate between a field that
|
// changed. It uses pointer fields so we can differentiate between a field that
|
||||||
// was not provided and a field that was provided as explicitly blank. Normally
|
// was not provided and a field that was provided as explicitly blank. Normally
|
||||||
// we do not want to use pointers to basic types but we make exceptions around
|
// we do not want to use pointers to basic types but we make exceptions around
|
||||||
// marshalling/unmarshalling.
|
// marshalling/unmarshalling.
|
||||||
type UpdateAccountRequest struct {
|
type AccountUpdateRequest struct {
|
||||||
ID string `validate:"required,uuid"`
|
ID string `validate:"required,uuid"`
|
||||||
Name *string `json:"name" validate:"omitempty,unique"`
|
Name *string `json:"name" validate:"omitempty,unique"`
|
||||||
Address1 *string `json:"address1" validate:"omitempty"`
|
Address1 *string `json:"address1" validate:"omitempty"`
|
||||||
@ -68,12 +68,12 @@ type UpdateAccountRequest struct {
|
|||||||
// AccountFindRequest defines the possible options to search for accounts. By default
|
// AccountFindRequest defines the possible options to search for accounts. By default
|
||||||
// archived accounts will be excluded from response.
|
// archived accounts will be excluded from response.
|
||||||
type AccountFindRequest struct {
|
type AccountFindRequest struct {
|
||||||
Where *string
|
Where *string `schema:"where"`
|
||||||
Args []interface{}
|
Args []interface{} `schema:"args"`
|
||||||
Order []string
|
Order []string `schema:"order"`
|
||||||
Limit *uint
|
Limit *uint `schema:"limit"`
|
||||||
Offset *uint
|
Offset *uint `schema:"offset"`
|
||||||
IncludedArchived bool
|
IncludedArchived bool `schema:"included-archived"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountStatus represents the status of an account.
|
// AccountStatus represents the status of an account.
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||||
|
// This file was generated by swaggo/swag at
|
||||||
|
// 2019-06-24 13:07:10.15232 -0800 AKDT m=+0.076902855
|
||||||
|
|
||||||
|
package docs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/alecthomas/template"
|
||||||
|
"github.com/swaggo/swag"
|
||||||
|
)
|
||||||
|
|
||||||
|
var doc = `{
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "This is a sample server celler server.",
|
||||||
|
"title": "SaaS Example API",
|
||||||
|
"termsOfService": "http://geeksinthewoods.com/terms",
|
||||||
|
"contact": {
|
||||||
|
"name": "API Support",
|
||||||
|
"url": "https://gitlab.com/geeks-accelerator/oss/saas-starter-kit",
|
||||||
|
"email": "support@geeksinthewoods.com"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "Apache 2.0",
|
||||||
|
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
|
},
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
"host": "example-api.saas.geeksinthewoods.com",
|
||||||
|
"basePath": "/v1",
|
||||||
|
"paths": {}
|
||||||
|
}`
|
||||||
|
|
||||||
|
type swaggerInfo struct {
|
||||||
|
Version string
|
||||||
|
Host string
|
||||||
|
BasePath string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||||
|
var SwaggerInfo swaggerInfo
|
||||||
|
|
||||||
|
type s struct{}
|
||||||
|
|
||||||
|
func (s *s) ReadDoc() string {
|
||||||
|
t, err := template.New("swagger_info").Parse(doc)
|
||||||
|
if err != nil {
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
var tpl bytes.Buffer
|
||||||
|
if err := t.Execute(&tpl, SwaggerInfo); err != nil {
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
return tpl.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
swag.Register(swag.Name, &s{})
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "This is a sample server celler server.",
|
||||||
|
"title": "SaaS Example API",
|
||||||
|
"termsOfService": "http://geeksinthewoods.com/terms",
|
||||||
|
"contact": {
|
||||||
|
"name": "API Support",
|
||||||
|
"url": "https://gitlab.com/geeks-accelerator/oss/saas-starter-kit",
|
||||||
|
"email": "support@geeksinthewoods.com"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "Apache 2.0",
|
||||||
|
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
|
},
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
"host": "example-api.saas.geeksinthewoods.com",
|
||||||
|
"basePath": "/v1",
|
||||||
|
"paths": {}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
basePath: /v1
|
||||||
|
host: example-api.saas.geeksinthewoods.com
|
||||||
|
info:
|
||||||
|
contact:
|
||||||
|
email: support@geeksinthewoods.com
|
||||||
|
name: API Support
|
||||||
|
url: https://gitlab.com/geeks-accelerator/oss/saas-starter-kit
|
||||||
|
description: This is a sample server celler server.
|
||||||
|
license:
|
||||||
|
name: Apache 2.0
|
||||||
|
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
termsOfService: http://geeksinthewoods.com/terms
|
||||||
|
title: SaaS Example API
|
||||||
|
version: "1.0"
|
||||||
|
paths: {}
|
||||||
|
swagger: "2.0"
|
150
example-project/internal/mid/saas-swagger/example/main.go
Normal file
150
example-project/internal/mid/saas-swagger/example/main.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid"
|
||||||
|
saasSwagger "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid/saas-swagger"
|
||||||
|
_ "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid/saas-swagger/example/docs" // docs is generated by Swag CLI, you have to import it.
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
// build is the git version of this program. It is set using build flags in the makefile.
|
||||||
|
var build = "develop"
|
||||||
|
|
||||||
|
// service is the name of the program used for logging, tracing and the
|
||||||
|
// the prefix used for loading env variables
|
||||||
|
// ie: export WEB_API_ENV=dev
|
||||||
|
var service = "EXAMPLE_API"
|
||||||
|
|
||||||
|
// @title SaaS Example API
|
||||||
|
// @version 1.0
|
||||||
|
// @description This is a sample server celler server.
|
||||||
|
// @termsOfService http://geeksinthewoods.com/terms
|
||||||
|
|
||||||
|
// @contact.name API Support
|
||||||
|
// @contact.email support@geeksinthewoods.com
|
||||||
|
// @contact.url https://gitlab.com/geeks-accelerator/oss/saas-starter-kit
|
||||||
|
|
||||||
|
// @license.name Apache 2.0
|
||||||
|
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
// @host example-api.saas.geeksinthewoods.com
|
||||||
|
// @BasePath /v1
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Logging
|
||||||
|
|
||||||
|
log := log.New(os.Stdout, service+" : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Configuration
|
||||||
|
var cfg struct {
|
||||||
|
Env string `default:"dev" envconfig:"ENV"`
|
||||||
|
HTTP struct {
|
||||||
|
Host string `default:"0.0.0.0:1323" envconfig:"HOST"`
|
||||||
|
ReadTimeout time.Duration `default:"10s" envconfig:"READ_TIMEOUT"`
|
||||||
|
WriteTimeout time.Duration `default:"10s" envconfig:"WRITE_TIMEOUT"`
|
||||||
|
}
|
||||||
|
App struct {
|
||||||
|
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For additional details refer to https://github.com/kelseyhightower/envconfig
|
||||||
|
if err := envconfig.Process(service, &cfg); err != nil {
|
||||||
|
log.Fatalf("main : Parsing Config : %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := flag.Process(&cfg); err != nil {
|
||||||
|
if err != flag.ErrHelp {
|
||||||
|
log.Fatalf("main : Parsing Command Line : %v", err)
|
||||||
|
}
|
||||||
|
return // We displayed help.
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Start API Service
|
||||||
|
|
||||||
|
// Make a channel to listen for an interrupt or terminate signal from the OS.
|
||||||
|
// Use a buffered channel because the signal package requires it.
|
||||||
|
shutdown := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
api := http.Server{
|
||||||
|
Addr: cfg.HTTP.Host,
|
||||||
|
Handler: API(shutdown, log),
|
||||||
|
ReadTimeout: cfg.HTTP.ReadTimeout,
|
||||||
|
WriteTimeout: cfg.HTTP.WriteTimeout,
|
||||||
|
MaxHeaderBytes: 1 << 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a channel to listen for errors coming from the listener. Use a
|
||||||
|
// buffered channel so the goroutine can exit if we don't collect this error.
|
||||||
|
serverErrors := make(chan error, 1)
|
||||||
|
|
||||||
|
// Start the service listening for requests.
|
||||||
|
go func() {
|
||||||
|
log.Printf("main : API Listening %s", cfg.HTTP.Host)
|
||||||
|
serverErrors <- api.ListenAndServe()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Shutdown
|
||||||
|
|
||||||
|
// Blocking main and waiting for shutdown.
|
||||||
|
select {
|
||||||
|
case err := <-serverErrors:
|
||||||
|
log.Fatalf("main : Error starting server: %v", err)
|
||||||
|
|
||||||
|
case sig := <-shutdown:
|
||||||
|
log.Printf("main : %v : Start shutdown..", sig)
|
||||||
|
|
||||||
|
// Create context for Shutdown call.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.App.ShutdownTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Asking listener to shutdown and load shed.
|
||||||
|
err := api.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("main : Graceful shutdown did not complete in %v : %v", cfg.App.ShutdownTimeout, err)
|
||||||
|
err = api.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the status of this shutdown.
|
||||||
|
switch {
|
||||||
|
case sig == syscall.SIGSTOP:
|
||||||
|
log.Fatal("main : Integrity issue caused shutdown")
|
||||||
|
case err != nil:
|
||||||
|
log.Fatalf("main : Could not stop server gracefully : %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API returns a handler for a set of routes.
|
||||||
|
func API(shutdown chan os.Signal, log *log.Logger) http.Handler {
|
||||||
|
|
||||||
|
// Construct the web.App which holds all routes as well as common Middleware.
|
||||||
|
app := web.NewApp(shutdown, log, mid.Trace(), mid.Logger(log), mid.Errors(log), mid.Metrics(), mid.Panics())
|
||||||
|
|
||||||
|
app.Handle("GET", "/swagger/", saasSwagger.WrapHandler)
|
||||||
|
app.Handle("GET", "/swagger/*", saasSwagger.WrapHandler)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Or can use SaasWrapHandler func with configurations.
|
||||||
|
url := saasSwagger.URL("http://localhost:1323/swagger/doc.json") //The url pointing to API definition
|
||||||
|
e.GET("/swagger/*", saasSwagger.SaasWrapHandler(url))
|
||||||
|
*/
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
211
example-project/internal/mid/saas-swagger/swagger.go
Normal file
211
example-project/internal/mid/saas-swagger/swagger.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
package saasSwagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/swaggo/files"
|
||||||
|
"github.com/swaggo/swag"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNotFound used when a file doesn't exist.
|
||||||
|
ErrNotFound = errors.New("File not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config stores echoSwagger configuration variables.
|
||||||
|
type Config struct {
|
||||||
|
//The url pointing to API definition (normally swagger.json or swagger.yaml). Default is `doc.json`.
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL presents the url pointing to API definition (normally swagger.json or swagger.yaml).
|
||||||
|
func URL(url string) func(c *Config) {
|
||||||
|
return func(c *Config) {
|
||||||
|
c.URL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapHandler wraps swaggerFiles.Handler and returns web.Handler
|
||||||
|
var WrapHandler = SaasWrapHandler()
|
||||||
|
|
||||||
|
// SaasWrapHandler wraps `http.Handler` into `web.Handler`.
|
||||||
|
func SaasWrapHandler(confs ...func(c *Config)) web.Handler {
|
||||||
|
|
||||||
|
handler := swaggerFiles.Handler
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
URL: "doc.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range confs {
|
||||||
|
c(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a template with name
|
||||||
|
t := template.New("swagger_index.html")
|
||||||
|
index, _ := t.Parse(indexTempl)
|
||||||
|
|
||||||
|
type pro struct {
|
||||||
|
Host string
|
||||||
|
}
|
||||||
|
|
||||||
|
var re = regexp.MustCompile(`(.*)(index\.html|doc\.json|favicon-16x16\.png|favicon-32x32\.png|/oauth2-redirect\.html|swagger-ui\.css|swagger-ui\.css\.map|swagger-ui\.js|swagger-ui\.js\.map|swagger-ui-bundle\.js|swagger-ui-bundle\.js\.map|swagger-ui-standalone-preset\.js|swagger-ui-standalone-preset\.js\.map)[\?|.]*`)
|
||||||
|
|
||||||
|
// Create the handler that will be attached in the middleware chain.
|
||||||
|
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
|
var (
|
||||||
|
path string
|
||||||
|
prefix string
|
||||||
|
)
|
||||||
|
|
||||||
|
matches := re.FindStringSubmatch(r.RequestURI)
|
||||||
|
|
||||||
|
if len(matches) == 3 {
|
||||||
|
path = matches[2]
|
||||||
|
prefix = matches[1]
|
||||||
|
} else if len(matches) > 0 {
|
||||||
|
err := errors.WithMessagef(ErrNotFound, "page %s not found", r.RequestURI)
|
||||||
|
return web.NewRequestError(err, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to index page.
|
||||||
|
if path == "" {
|
||||||
|
path = "index.html"
|
||||||
|
prefix = r.RequestURI
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the http prefix.
|
||||||
|
handler.Prefix = prefix
|
||||||
|
|
||||||
|
switch path {
|
||||||
|
case "index.html":
|
||||||
|
index.Execute(w, config)
|
||||||
|
case "doc.json":
|
||||||
|
doc, err := swag.ReadDoc()
|
||||||
|
if err != nil {
|
||||||
|
return web.NewRequestError(err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return web.RespondJson(ctx, w, doc, http.StatusOK)
|
||||||
|
default:
|
||||||
|
if strings.HasSuffix(path, ".html") {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
} else if strings.HasSuffix(path, ".css") {
|
||||||
|
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
||||||
|
} else if strings.HasSuffix(path, ".js") {
|
||||||
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
|
} else if strings.HasSuffix(path, ".json") {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexTempl = `<!-- HTML for static distribution bundle build -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Swagger UI</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
|
||||||
|
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
|
||||||
|
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
|
||||||
|
<style>
|
||||||
|
html
|
||||||
|
{
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: -moz-scrollbars-vertical;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
*,
|
||||||
|
*:before,
|
||||||
|
*:after
|
||||||
|
{
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin:0;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
|
||||||
|
<defs>
|
||||||
|
<symbol viewBox="0 0 20 20" id="unlocked">
|
||||||
|
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol viewBox="0 0 20 20" id="locked">
|
||||||
|
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol viewBox="0 0 20 20" id="close">
|
||||||
|
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol viewBox="0 0 20 20" id="large-arrow">
|
||||||
|
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol viewBox="0 0 20 20" id="large-arrow-down">
|
||||||
|
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
|
||||||
|
<symbol viewBox="0 0 24 24" id="jump-to">
|
||||||
|
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol viewBox="0 0 24 24" id="expand">
|
||||||
|
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
|
||||||
|
<script src="./swagger-ui-bundle.js"> </script>
|
||||||
|
<script src="./swagger-ui-standalone-preset.js"> </script>
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
// Build a system
|
||||||
|
const ui = SwaggerUIBundle({
|
||||||
|
url: "{{.URL}}",
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
validatorUrl: null,
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
SwaggerUIStandalonePreset
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
SwaggerUIBundle.plugins.DownloadUrl
|
||||||
|
],
|
||||||
|
layout: "StandaloneLayout"
|
||||||
|
})
|
||||||
|
|
||||||
|
window.ui = ui
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
`
|
43
example-project/internal/mid/saas-swagger/swagger_test.go
Normal file
43
example-project/internal/mid/saas-swagger/swagger_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package saasSwagger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid/saas-swagger/example/docs"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWrapHandler(t *testing.T) {
|
||||||
|
|
||||||
|
log := log.New(os.Stdout, "", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
|
||||||
|
log.SetOutput(ioutil.Discard)
|
||||||
|
|
||||||
|
app := web.NewApp(nil, log)
|
||||||
|
app.Handle("GET", "/swagger/*", WrapHandler)
|
||||||
|
|
||||||
|
w1 := performRequest("GET", "/swagger/index.html", app)
|
||||||
|
assert.Equal(t, 200, w1.Code)
|
||||||
|
|
||||||
|
w2 := performRequest("GET", "/swagger/doc.json", app)
|
||||||
|
assert.Equal(t, 200, w2.Code)
|
||||||
|
|
||||||
|
w3 := performRequest("GET", "/swagger/favicon-16x16.png", app)
|
||||||
|
assert.Equal(t, 200, w3.Code)
|
||||||
|
|
||||||
|
w4 := performRequest("GET", "/swagger/notfound", app)
|
||||||
|
assert.Equal(t, 404, w4.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func performRequest(method, target string, app *web.App) *httptest.ResponseRecorder {
|
||||||
|
r := httptest.NewRequest(method, target, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
app.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
@ -1,18 +1,10 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
|
||||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
|
||||||
"github.com/aws/aws-sdk-go/service/secretsmanager"
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
"github.com/dgrijalva/jwt-go"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -30,7 +22,7 @@ type KeyFunc func(keyID string) (*rsa.PublicKey, error)
|
|||||||
|
|
||||||
// NewKeyFunc is a multiple implementation of KeyFunc that
|
// NewKeyFunc is a multiple implementation of KeyFunc that
|
||||||
// supports a map of keys.
|
// supports a map of keys.
|
||||||
func NewKeyFunc(keys map[string]*rsa.PrivateKey) KeyFunc {
|
func NewKeyFunc(keys map[string]*PrivateKey) KeyFunc {
|
||||||
return func(kid string) (*rsa.PublicKey, error) {
|
return func(kid string) (*rsa.PublicKey, error) {
|
||||||
key, ok := keys[kid]
|
key, ok := keys[kid]
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -43,200 +35,33 @@ func NewKeyFunc(keys map[string]*rsa.PrivateKey) KeyFunc {
|
|||||||
// Authenticator is used to authenticate clients. It can generate a token for a
|
// Authenticator is used to authenticate clients. It can generate a token for a
|
||||||
// set of user claims and recreate the claims by parsing the token.
|
// set of user claims and recreate the claims by parsing the token.
|
||||||
type Authenticator struct {
|
type Authenticator struct {
|
||||||
privateKey *rsa.PrivateKey
|
privateKey *PrivateKey
|
||||||
keyID string
|
keyID string
|
||||||
algorithm string
|
algorithm string
|
||||||
kf KeyFunc
|
kf KeyFunc
|
||||||
parser *jwt.Parser
|
parser *jwt.Parser
|
||||||
|
Storage Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateKey is used to associate a private key with a keyID and algorithm.
|
||||||
|
type PrivateKey struct {
|
||||||
|
*rsa.PrivateKey
|
||||||
|
keyID string
|
||||||
|
algorithm string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthenticator creates an *Authenticator for use.
|
// NewAuthenticator creates an *Authenticator for use.
|
||||||
// key expiration is optional to filter out old keys
|
// key expiration is optional to filter out old keys
|
||||||
// It will error if:
|
// It will error if:
|
||||||
// - The aws session is nil.
|
|
||||||
// - The aws secret id is blank.
|
|
||||||
// - The specified algorithm is unsupported.
|
// - The specified algorithm is unsupported.
|
||||||
func NewAuthenticator(awsSession *session.Session, awsSecretID string, now time.Time, keyExpiration time.Duration) (*Authenticator, error) {
|
// - No current private key exists.
|
||||||
if awsSession == nil {
|
func NewAuthenticator(storage Storage, now time.Time) (*Authenticator, error) {
|
||||||
return nil, errors.New("aws session cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if awsSecretID == "" {
|
|
||||||
return nil, errors.New("aws secret id cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if now.IsZero() {
|
|
||||||
now = time.Now().UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time threshold to stop loading keys, any key with a created date
|
|
||||||
// before this value will not be loaded.
|
|
||||||
var disabledCreatedDate time.Time
|
|
||||||
|
|
||||||
// Time threshold to create a new key. If a current key exists and the
|
|
||||||
// created date of the key is before this value, a new key will be created.
|
|
||||||
var activeCreatedDate time.Time
|
|
||||||
|
|
||||||
// If an expiration duration is included, convert to past time from now.
|
|
||||||
if keyExpiration.Seconds() != 0 {
|
|
||||||
// Ensure the expiration is a time in the past for comparison below.
|
|
||||||
if keyExpiration.Seconds() > 0 {
|
|
||||||
keyExpiration = keyExpiration * -1
|
|
||||||
}
|
|
||||||
// Stop loading keys when the created date exceeds two times the key expiration
|
|
||||||
disabledCreatedDate = now.UTC().Add(keyExpiration * 2)
|
|
||||||
|
|
||||||
// Time used to determine when a new key should be created.
|
|
||||||
activeCreatedDate = now.UTC().Add(keyExpiration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init new AWS Secret Manager using provided AWS session.
|
|
||||||
secretManager := secretsmanager.New(awsSession)
|
|
||||||
|
|
||||||
// A List of version ids for the stored secret. All keys will be stored under
|
|
||||||
// the same name in AWS secret manager. We still want to load old keys for a
|
|
||||||
// short period of time to ensure any requests in flight have the opportunity
|
|
||||||
// to be completed.
|
|
||||||
var versionIds []string
|
|
||||||
|
|
||||||
// Exec call to AWS secret manager to return a list of version ids for the
|
|
||||||
// provided secret ID.
|
|
||||||
listParams := &secretsmanager.ListSecretVersionIdsInput{
|
|
||||||
SecretId: aws.String(awsSecretID),
|
|
||||||
}
|
|
||||||
err := secretManager.ListSecretVersionIdsPages(listParams,
|
|
||||||
func(page *secretsmanager.ListSecretVersionIdsOutput, lastPage bool) bool {
|
|
||||||
for _, v := range page.Versions {
|
|
||||||
// When disabled CreatedDate is not empty, compare the created date
|
|
||||||
// for each key version to the disabled cut off time.
|
|
||||||
if !disabledCreatedDate.IsZero() && v.CreatedDate != nil && !v.CreatedDate.IsZero() {
|
|
||||||
// Skip any version ids that are less than the expiration time.
|
|
||||||
if v.CreatedDate.UTC().Unix() < disabledCreatedDate.UTC().Unix() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.VersionId != nil {
|
|
||||||
versionIds = append(versionIds, *v.VersionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return !lastPage
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Flag whether the secret exists and update needs to be used
|
|
||||||
// instead of create.
|
|
||||||
var awsSecretIDNotFound bool
|
|
||||||
if err != nil {
|
|
||||||
if aerr, ok := err.(awserr.Error); ok {
|
|
||||||
switch aerr.Code() {
|
|
||||||
case secretsmanager.ErrCodeResourceNotFoundException:
|
|
||||||
awsSecretIDNotFound = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !awsSecretIDNotFound {
|
|
||||||
return nil, errors.Wrapf(err, "aws list secret version ids for secret ID %s failed", awsSecretID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map of keys stored by version id. version id is kid.
|
|
||||||
keyContents := make(map[string][]byte)
|
|
||||||
|
|
||||||
// The current key id if there is an active one.
|
|
||||||
var curKeyId string
|
|
||||||
|
|
||||||
// If the list of version ids is not empty, load the keys from secret manager.
|
|
||||||
if len(versionIds) > 0 {
|
|
||||||
// The max created data to determine the most recent key.
|
|
||||||
var lastCreatedDate time.Time
|
|
||||||
|
|
||||||
for _, id := range versionIds {
|
|
||||||
res, err := secretManager.GetSecretValue(&secretsmanager.GetSecretValueInput{
|
|
||||||
SecretId: aws.String(awsSecretID),
|
|
||||||
VersionId: aws.String(id),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "aws secret id %s, version id %s value failed", awsSecretID, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(res.SecretBinary) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
keyContents[*res.VersionId] = res.SecretBinary
|
|
||||||
|
|
||||||
if lastCreatedDate.IsZero() || res.CreatedDate.UTC().Unix() > lastCreatedDate.UTC().Unix() {
|
|
||||||
curKeyId = *res.VersionId
|
|
||||||
lastCreatedDate = res.CreatedDate.UTC()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
if !activeCreatedDate.IsZero() && lastCreatedDate.UTC().Unix() < activeCreatedDate.UTC().Unix() {
|
|
||||||
curKeyId = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no keys stored in secret manager, create a new one or
|
|
||||||
// if the current key needs to be rotated, generate a new key and update the secret.
|
|
||||||
// @TODO: When a new key is generated and there are multiple instances of the service running
|
|
||||||
// its possible based on the key expiration set that requests fail because keys are only
|
|
||||||
// refreshed on instance launch. Could store keys in a kv store and update that value
|
|
||||||
// when new keys are generated
|
|
||||||
if len(keyContents) == 0 || curKeyId == "" {
|
|
||||||
privateKey, err := Keygen()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to generate new private key")
|
|
||||||
}
|
|
||||||
|
|
||||||
if awsSecretIDNotFound {
|
|
||||||
res, err := secretManager.CreateSecret(&secretsmanager.CreateSecretInput{
|
|
||||||
Name: aws.String(awsSecretID),
|
|
||||||
SecretBinary: privateKey,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to create new secret with private key")
|
|
||||||
}
|
|
||||||
curKeyId = *res.VersionId
|
|
||||||
} else {
|
|
||||||
res, err := secretManager.UpdateSecret(&secretsmanager.UpdateSecretInput{
|
|
||||||
SecretId: aws.String(awsSecretID),
|
|
||||||
SecretBinary: privateKey,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to create new secret with private key")
|
|
||||||
}
|
|
||||||
curKeyId = *res.VersionId
|
|
||||||
}
|
|
||||||
|
|
||||||
keyContents[curKeyId] = privateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map of keys by kid (version id).
|
|
||||||
keys := make(map[string]*rsa.PrivateKey)
|
|
||||||
|
|
||||||
// The current active key to be used.
|
|
||||||
var curPrivateKey *rsa.PrivateKey
|
|
||||||
|
|
||||||
// Loop through all the key bytes and load the private key.
|
|
||||||
for kid, keyContent := range keyContents {
|
|
||||||
key, err := jwt.ParseRSAPrivateKeyFromPEM(keyContent)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "parsing auth private key")
|
|
||||||
}
|
|
||||||
keys[kid] = key
|
|
||||||
if kid == curKeyId {
|
|
||||||
curPrivateKey = key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lookup function to be used by the middleware to validate the kid and
|
// Lookup function to be used by the middleware to validate the kid and
|
||||||
// Return the associated public key.
|
// Return the associated public key.
|
||||||
publicKeyLookup := NewKeyFunc(keys)
|
publicKeyLookup := NewKeyFunc(storage.Keys())
|
||||||
|
|
||||||
// Algorithm to be used to for the private key.
|
// Validate the globally defined encryption algorithm is valid.
|
||||||
algorithm := "RS256"
|
|
||||||
if jwt.GetSigningMethod(algorithm) == nil {
|
if jwt.GetSigningMethod(algorithm) == nil {
|
||||||
return nil, errors.Errorf("unknown algorithm %v", algorithm)
|
return nil, errors.Errorf("unknown algorithm %v", algorithm)
|
||||||
}
|
}
|
||||||
@ -248,9 +73,15 @@ func NewAuthenticator(awsSession *session.Session, awsSecretID string, now time.
|
|||||||
ValidMethods: []string{algorithm},
|
ValidMethods: []string{algorithm},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the current key from the storage engine.
|
||||||
|
curKey := storage.Current()
|
||||||
|
if curKey == nil {
|
||||||
|
return nil, errors.New("Missing private key")
|
||||||
|
}
|
||||||
|
|
||||||
a := Authenticator{
|
a := Authenticator{
|
||||||
privateKey: curPrivateKey,
|
privateKey: curKey,
|
||||||
keyID: curKeyId,
|
keyID: curKey.keyID,
|
||||||
algorithm: algorithm,
|
algorithm: algorithm,
|
||||||
kf: publicKeyLookup,
|
kf: publicKeyLookup,
|
||||||
parser: &parser,
|
parser: &parser,
|
||||||
@ -306,23 +137,3 @@ func (a *Authenticator) ParseClaims(tknStr string) (Claims, error) {
|
|||||||
|
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keygen creates an x509 private key for signing auth tokens.
|
|
||||||
func Keygen() ([]byte, error) {
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return []byte{}, errors.Wrap(err, "generating keys")
|
|
||||||
}
|
|
||||||
|
|
||||||
block := pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
if err := pem.Encode(buf, &block); err != nil {
|
|
||||||
return []byte{}, errors.Wrap(err, "encoding to private file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
33
example-project/internal/platform/auth/key_gen.go
Normal file
33
example-project/internal/platform/auth/key_gen.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Algorithm to be used to for the private key.
|
||||||
|
const algorithm = "RS256"
|
||||||
|
|
||||||
|
// keyGen creates an x509 private key for signing auth tokens.
|
||||||
|
func keyGen() ([]byte, error) {
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, errors.Wrap(err, "generating keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
block := pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := pem.Encode(buf, &block); err != nil {
|
||||||
|
return []byte{}, errors.Wrap(err, "encoding to private file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
212
example-project/internal/platform/auth/storage.go
Normal file
212
example-project/internal/platform/auth/storage.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/pborman/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Storage provides the ability to persist keys to custom locations.
|
||||||
|
type Storage interface {
|
||||||
|
// Keys returns a map of private keys by kID.
|
||||||
|
Keys() map[string]*PrivateKey
|
||||||
|
// Current returns the most recently generated private key.
|
||||||
|
Current() *PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageFile is a storage engine that stores private keys on the local file system.
|
||||||
|
type StorageFile struct {
|
||||||
|
// Local directory for storing private keys.
|
||||||
|
localDir string
|
||||||
|
// Duration for keys to be valid.
|
||||||
|
keyExpiration time.Duration
|
||||||
|
// Map of keys by kid (version id).
|
||||||
|
keys map[string]*PrivateKey
|
||||||
|
// The current active key to be used.
|
||||||
|
curPrivateKey *PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys returns a map of private keys by kID.
|
||||||
|
func (s *StorageFile) Keys() map[string]*PrivateKey {
|
||||||
|
if s == nil || s.keys == nil {
|
||||||
|
return map[string]*PrivateKey{}
|
||||||
|
}
|
||||||
|
return s.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current returns the most recently generated private key.
|
||||||
|
func (s *StorageFile) Current() *PrivateKey {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.curPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthenticatorFile is a help function that inits a new Authenticator
|
||||||
|
// using the file storage.
|
||||||
|
func NewAuthenticatorFile(localDir string, now time.Time, keyExpiration time.Duration) (*Authenticator, error) {
|
||||||
|
storage, err := NewStorageFile(localDir, now, keyExpiration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewAuthenticator(storage, time.Now().UTC())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStorageFile implements the interface Storage to support persisting private keys
|
||||||
|
// to the local file system.
|
||||||
|
// It will error if:
|
||||||
|
func NewStorageFile(localDir string, now time.Time, keyExpiration time.Duration) (*StorageFile, error) {
|
||||||
|
if localDir == "" {
|
||||||
|
localDir = filepath.Join(os.TempDir(), "auth-private-keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(localDir); os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(localDir, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to create storage directory %s", localDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storage := &StorageFile{
|
||||||
|
localDir: localDir,
|
||||||
|
keyExpiration: keyExpiration,
|
||||||
|
keys: make(map[string]*PrivateKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
if now.IsZero() {
|
||||||
|
now = time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time threshold to stop loading keys, any key with a created date
|
||||||
|
// before this value will not be loaded.
|
||||||
|
var disabledCreatedDate time.Time
|
||||||
|
|
||||||
|
// Time threshold to create a new key. If a current key exists and the
|
||||||
|
// created date of the key is before this value, a new key will be created.
|
||||||
|
var activeCreatedDate time.Time
|
||||||
|
|
||||||
|
// If an expiration duration is included, convert to past time from now.
|
||||||
|
if keyExpiration.Seconds() != 0 {
|
||||||
|
// Ensure the expiration is a time in the past for comparison below.
|
||||||
|
if keyExpiration.Seconds() > 0 {
|
||||||
|
keyExpiration = keyExpiration * -1
|
||||||
|
}
|
||||||
|
// Stop loading keys when the created date exceeds two times the key expiration
|
||||||
|
disabledCreatedDate = now.UTC().Add(keyExpiration * 2)
|
||||||
|
|
||||||
|
// Time used to determine when a new key should be created.
|
||||||
|
activeCreatedDate = now.UTC().Add(keyExpiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values used to format filename.
|
||||||
|
filePrefix := "auth_"
|
||||||
|
fileExt := ".privatekey"
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(localDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to list files in directory %s", localDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map of keys stored by version id. version id is kid.
|
||||||
|
keyContents := make(map[string][]byte)
|
||||||
|
|
||||||
|
// The current key id if there is an active one.
|
||||||
|
var curKeyId string
|
||||||
|
|
||||||
|
// The max created data to determine the most recent key.
|
||||||
|
var lastCreatedDate time.Time
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
if !strings.HasPrefix(f.Name(), filePrefix) || !strings.HasSuffix(f.Name(), fileExt) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the created timestamp and kID from the filename.
|
||||||
|
fname := strings.TrimSuffix(f.Name(), fileExt)
|
||||||
|
pts := strings.Split(fname, "_")
|
||||||
|
if len(pts) != 3 {
|
||||||
|
return nil, errors.Errorf("unable to parse filename %s", f.Name())
|
||||||
|
}
|
||||||
|
createdAt := pts[1]
|
||||||
|
kID := pts[2]
|
||||||
|
|
||||||
|
// Covert string timestamp to int.
|
||||||
|
createdAtSecs, err := strconv.Atoi(createdAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed parse timestamp from %s", f.Name())
|
||||||
|
}
|
||||||
|
ts := time.Unix(int64(createdAtSecs), 0)
|
||||||
|
|
||||||
|
// If the created time of the key is less than the disabled threshold, skip.
|
||||||
|
if !disabledCreatedDate.IsZero() && ts.UTC().Unix() < disabledCreatedDate.UTC().Unix() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(localDir, f.Name())
|
||||||
|
dat, err := ioutil.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed read file %s", f.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
keyContents[kID] = dat
|
||||||
|
|
||||||
|
if lastCreatedDate.IsZero() || ts.UTC().Unix() > lastCreatedDate.UTC().Unix() {
|
||||||
|
curKeyId = kID
|
||||||
|
lastCreatedDate = ts.UTC()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
if !activeCreatedDate.IsZero() && lastCreatedDate.UTC().Unix() < activeCreatedDate.UTC().Unix() {
|
||||||
|
curKeyId = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no keys or the current key needs to be rotated, generate a new key.
|
||||||
|
if len(keyContents) == 0 || curKeyId == "" {
|
||||||
|
privateKey, err := keyGen()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to generate new private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
kID := uuid.NewRandom().String()
|
||||||
|
|
||||||
|
fname := fmt.Sprintf("%s_%d_%s%s", filePrefix, now.UTC().Unix(), kID, fileExt)
|
||||||
|
|
||||||
|
filePath := filepath.Join(localDir, fname)
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(filePath, privateKey, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed write file %s", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyContents[curKeyId] = privateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through all the key bytes and load the private key.
|
||||||
|
for kid, key := range keyContents {
|
||||||
|
pk, err := jwt.ParseRSAPrivateKeyFromPEM(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "parsing auth private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.keys[kid] = &PrivateKey{
|
||||||
|
PrivateKey: pk,
|
||||||
|
keyID: kid,
|
||||||
|
algorithm: algorithm,
|
||||||
|
}
|
||||||
|
|
||||||
|
if kid == curKeyId {
|
||||||
|
storage.curPrivateKey = storage.keys[kid]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return storage, nil
|
||||||
|
}
|
236
example-project/internal/platform/auth/storage_aws.go
Normal file
236
example-project/internal/platform/auth/storage_aws.go
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/secretsmanager"
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StorageAws is a storage engine that uses AWS Secrets Manager to persist private keys.
|
||||||
|
type StorageAws struct {
|
||||||
|
keyExpiration time.Duration
|
||||||
|
// Map of keys by kid (version id).
|
||||||
|
keys map[string]*PrivateKey
|
||||||
|
// The current active key to be used.
|
||||||
|
curPrivateKey *PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys returns a map of private keys by kID.
|
||||||
|
func (s *StorageAws) Keys() map[string]*PrivateKey {
|
||||||
|
if s == nil || s.keys == nil {
|
||||||
|
return map[string]*PrivateKey{}
|
||||||
|
}
|
||||||
|
return s.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current returns the most recently generated private key.
|
||||||
|
func (s *StorageAws) Current() *PrivateKey {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.curPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthenticatorAws is a help function that inits a new Authenticator
|
||||||
|
// using the AWS storage.
|
||||||
|
func NewAuthenticatorAws(awsSession *session.Session, awsSecretID string, now time.Time, keyExpiration time.Duration) (*Authenticator, error) {
|
||||||
|
storage, err := NewStorageAws(awsSession, awsSecretID, now, keyExpiration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewAuthenticator(storage, time.Now().UTC())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStorageAws implements the interface Storage to support persisting private keys
|
||||||
|
// to AWS Secrets Manager.
|
||||||
|
// It will error if:
|
||||||
|
// - The aws session is nil.
|
||||||
|
// - The aws secret id is blank.
|
||||||
|
func NewStorageAws(awsSession *session.Session, awsSecretID string, now time.Time, keyExpiration time.Duration) (*StorageAws, error) {
|
||||||
|
if awsSession == nil {
|
||||||
|
return nil, errors.New("aws session cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if awsSecretID == "" {
|
||||||
|
return nil, errors.New("aws secret id cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
storage := &StorageAws{
|
||||||
|
keyExpiration: keyExpiration,
|
||||||
|
keys: make(map[string]*PrivateKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
if now.IsZero() {
|
||||||
|
now = time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time threshold to stop loading keys, any key with a created date
|
||||||
|
// before this value will not be loaded.
|
||||||
|
var disabledCreatedDate time.Time
|
||||||
|
|
||||||
|
// Time threshold to create a new key. If a current key exists and the
|
||||||
|
// created date of the key is before this value, a new key will be created.
|
||||||
|
var activeCreatedDate time.Time
|
||||||
|
|
||||||
|
// If an expiration duration is included, convert to past time from now.
|
||||||
|
if keyExpiration.Seconds() != 0 {
|
||||||
|
// Ensure the expiration is a time in the past for comparison below.
|
||||||
|
if keyExpiration.Seconds() > 0 {
|
||||||
|
keyExpiration = keyExpiration * -1
|
||||||
|
}
|
||||||
|
// Stop loading keys when the created date exceeds two times the key expiration
|
||||||
|
disabledCreatedDate = now.UTC().Add(keyExpiration * 2)
|
||||||
|
|
||||||
|
// Time used to determine when a new key should be created.
|
||||||
|
activeCreatedDate = now.UTC().Add(keyExpiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init new AWS Secret Manager using provided AWS session.
|
||||||
|
secretManager := secretsmanager.New(awsSession)
|
||||||
|
|
||||||
|
// A List of version ids for the stored secret. All keys will be stored under
|
||||||
|
// the same name in AWS secret manager. We still want to load old keys for a
|
||||||
|
// short period of time to ensure any requests in flight have the opportunity
|
||||||
|
// to be completed.
|
||||||
|
var versionIds []string
|
||||||
|
|
||||||
|
// Exec call to AWS secret manager to return a list of version ids for the
|
||||||
|
// provided secret ID.
|
||||||
|
listParams := &secretsmanager.ListSecretVersionIdsInput{
|
||||||
|
SecretId: aws.String(awsSecretID),
|
||||||
|
}
|
||||||
|
err := secretManager.ListSecretVersionIdsPages(listParams,
|
||||||
|
func(page *secretsmanager.ListSecretVersionIdsOutput, lastPage bool) bool {
|
||||||
|
for _, v := range page.Versions {
|
||||||
|
// When disabled CreatedDate is not empty, compare the created date
|
||||||
|
// for each key version to the disabled cut off time.
|
||||||
|
if !disabledCreatedDate.IsZero() && v.CreatedDate != nil && !v.CreatedDate.IsZero() {
|
||||||
|
// Skip any version ids that are less than the expiration time.
|
||||||
|
if v.CreatedDate.UTC().Unix() < disabledCreatedDate.UTC().Unix() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.VersionId != nil {
|
||||||
|
versionIds = append(versionIds, *v.VersionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !lastPage
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Flag whether the secret exists and update needs to be used
|
||||||
|
// instead of create.
|
||||||
|
var awsSecretIDNotFound bool
|
||||||
|
if err != nil {
|
||||||
|
if aerr, ok := err.(awserr.Error); ok {
|
||||||
|
switch aerr.Code() {
|
||||||
|
case secretsmanager.ErrCodeResourceNotFoundException:
|
||||||
|
awsSecretIDNotFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !awsSecretIDNotFound {
|
||||||
|
return nil, errors.Wrapf(err, "aws list secret version ids for secret ID %s failed", awsSecretID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map of keys stored by version id. version id is kid.
|
||||||
|
keyContents := make(map[string][]byte)
|
||||||
|
|
||||||
|
// The current key id if there is an active one.
|
||||||
|
var curKeyId string
|
||||||
|
|
||||||
|
// If the list of version ids is not empty, load the keys from secret manager.
|
||||||
|
if len(versionIds) > 0 {
|
||||||
|
// The max created data to determine the most recent key.
|
||||||
|
var lastCreatedDate time.Time
|
||||||
|
|
||||||
|
for _, id := range versionIds {
|
||||||
|
res, err := secretManager.GetSecretValue(&secretsmanager.GetSecretValueInput{
|
||||||
|
SecretId: aws.String(awsSecretID),
|
||||||
|
VersionId: aws.String(id),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "aws secret id %s, version id %s value failed", awsSecretID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.SecretBinary) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
keyContents[*res.VersionId] = res.SecretBinary
|
||||||
|
|
||||||
|
if lastCreatedDate.IsZero() || res.CreatedDate.UTC().Unix() > lastCreatedDate.UTC().Unix() {
|
||||||
|
curKeyId = *res.VersionId
|
||||||
|
lastCreatedDate = res.CreatedDate.UTC()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
if !activeCreatedDate.IsZero() && lastCreatedDate.UTC().Unix() < activeCreatedDate.UTC().Unix() {
|
||||||
|
curKeyId = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no keys stored in secret manager, create a new one or
|
||||||
|
// if the current key needs to be rotated, generate a new key and update the secret.
|
||||||
|
// @TODO: When a new key is generated and there are multiple instances of the service running
|
||||||
|
// its possible based on the key expiration set that requests fail because keys are only
|
||||||
|
// refreshed on instance launch. Could store keys in a kv store and update that value
|
||||||
|
// when new keys are generated
|
||||||
|
if len(keyContents) == 0 || curKeyId == "" {
|
||||||
|
privateKey, err := keyGen()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to generate new private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if awsSecretIDNotFound {
|
||||||
|
res, err := secretManager.CreateSecret(&secretsmanager.CreateSecretInput{
|
||||||
|
Name: aws.String(awsSecretID),
|
||||||
|
SecretBinary: privateKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create new secret with private key")
|
||||||
|
}
|
||||||
|
curKeyId = *res.VersionId
|
||||||
|
} else {
|
||||||
|
res, err := secretManager.UpdateSecret(&secretsmanager.UpdateSecretInput{
|
||||||
|
SecretId: aws.String(awsSecretID),
|
||||||
|
SecretBinary: privateKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create new secret with private key")
|
||||||
|
}
|
||||||
|
curKeyId = *res.VersionId
|
||||||
|
}
|
||||||
|
|
||||||
|
keyContents[curKeyId] = privateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through all the key bytes and load the private key.
|
||||||
|
for kid, key := range keyContents {
|
||||||
|
pk, err := jwt.ParseRSAPrivateKeyFromPEM(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "parsing auth private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.keys[kid] = &PrivateKey{
|
||||||
|
PrivateKey: pk,
|
||||||
|
keyID: kid,
|
||||||
|
algorithm: algorithm,
|
||||||
|
}
|
||||||
|
|
||||||
|
if kid == curKeyId {
|
||||||
|
storage.curPrivateKey = storage.keys[kid]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return storage, nil
|
||||||
|
}
|
@ -7,9 +7,10 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
en "github.com/go-playground/locales/en"
|
"github.com/go-playground/locales/en"
|
||||||
ut "github.com/go-playground/universal-translator"
|
ut "github.com/go-playground/universal-translator"
|
||||||
validator "gopkg.in/go-playground/validator.v9"
|
"github.com/gorilla/schema"
|
||||||
|
"gopkg.in/go-playground/validator.v9"
|
||||||
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
|
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,11 +48,19 @@ func init() {
|
|||||||
//
|
//
|
||||||
// If the provided value is a struct then it is checked for validation tags.
|
// If the provided value is a struct then it is checked for validation tags.
|
||||||
func Decode(r *http.Request, val interface{}) error {
|
func Decode(r *http.Request, val interface{}) error {
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete {
|
||||||
decoder := json.NewDecoder(r.Body)
|
decoder := json.NewDecoder(r.Body)
|
||||||
decoder.DisallowUnknownFields()
|
decoder.DisallowUnknownFields()
|
||||||
if err := decoder.Decode(val); err != nil {
|
if err := decoder.Decode(val); err != nil {
|
||||||
return NewRequestError(err, http.StatusBadRequest)
|
return NewRequestError(err, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
decoder := schema.NewDecoder()
|
||||||
|
if err := decoder.Decode(val, r.URL.Query()); err != nil {
|
||||||
|
return NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := validate.Struct(val); err != nil {
|
if err := validate.Struct(val); err != nil {
|
||||||
|
|
||||||
|
@ -72,11 +72,16 @@ func RespondJson(ctx context.Context, w http.ResponseWriter, data interface{}, s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check to see if the json has already been encoded.
|
||||||
|
jsonData, ok := data.([]byte)
|
||||||
|
if !ok {
|
||||||
// Convert the response value to JSON.
|
// Convert the response value to JSON.
|
||||||
jsonData, err := json.Marshal(data)
|
var err error
|
||||||
|
jsonData, err = json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set the content type and headers once we know marshaling has succeeded.
|
// Set the content type and headers once we know marshaling has succeeded.
|
||||||
w.Header().Set("Content-Type", MIMEApplicationJSONCharsetUTF8)
|
w.Header().Set("Content-Type", MIMEApplicationJSONCharsetUTF8)
|
||||||
@ -111,6 +116,7 @@ func RespondErrorStatus(ctx context.Context, w http.ResponseWriter, er error, st
|
|||||||
// Respond writes the data to the client with the specified HTTP status code and
|
// Respond writes the data to the client with the specified HTTP status code and
|
||||||
// content type.
|
// content type.
|
||||||
func Respond(ctx context.Context, w http.ResponseWriter, data []byte, statusCode int, contentType string) error {
|
func Respond(ctx context.Context, w http.ResponseWriter, data []byte, statusCode int, contentType string) error {
|
||||||
|
|
||||||
// Set the status code for the request logger middleware.
|
// Set the status code for the request logger middleware.
|
||||||
// If the context is missing this value, request the service
|
// If the context is missing this value, request the service
|
||||||
// to be shutdown gracefully.
|
// to be shutdown gracefully.
|
||||||
|
@ -53,9 +53,13 @@ func NewApp(shutdown chan os.Signal, log *log.Logger, mw ...Middleware) *App {
|
|||||||
|
|
||||||
// SignalShutdown is used to gracefully shutdown the app when an integrity
|
// SignalShutdown is used to gracefully shutdown the app when an integrity
|
||||||
// issue is identified.
|
// issue is identified.
|
||||||
func (a *App) SignalShutdown() {
|
func (a *App) SignalShutdown() bool {
|
||||||
|
if a.shutdown == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
a.log.Println("error returned from handler indicated integrity issue, shutting down service")
|
a.log.Println("error returned from handler indicated integrity issue, shutting down service")
|
||||||
a.shutdown <- syscall.SIGSTOP
|
a.shutdown <- syscall.SIGSTOP
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle is our mechanism for mounting Handlers for a given HTTP verb and path
|
// Handle is our mechanism for mounting Handlers for a given HTTP verb and path
|
||||||
@ -78,9 +82,24 @@ func (a *App) Handle(verb, path string, handler Handler, mw ...Middleware) {
|
|||||||
ctx := context.WithValue(r.Context(), KeyValues, &v)
|
ctx := context.WithValue(r.Context(), KeyValues, &v)
|
||||||
|
|
||||||
// Call the wrapped handler functions.
|
// Call the wrapped handler functions.
|
||||||
if err := handler(ctx, w, r, params); err != nil {
|
err := handler(ctx, w, r, params)
|
||||||
|
if err != nil {
|
||||||
|
// If we have specifically handled the error, then no need
|
||||||
|
// to initiate a shutdown.
|
||||||
|
if webErr, ok := err.(*Error); ok {
|
||||||
|
// Render an error response.
|
||||||
|
if rerr := RespondErrorStatus(ctx, w, webErr.Err, webErr.Status); rerr == nil {
|
||||||
|
// If there was not error rending the error, then no need to continue.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a.log.Printf("*****> critical shutdown error: %v", err)
|
a.log.Printf("*****> critical shutdown error: %v", err)
|
||||||
a.SignalShutdown()
|
if ok := a.SignalShutdown(); !ok {
|
||||||
|
// When shutdown chan is nil, in the case of unit testing
|
||||||
|
// we need to force display of the error.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,12 +39,12 @@ type ProjectUpdateRequest struct {
|
|||||||
// ProjectFindRequest defines the possible options to search for projects. By default
|
// ProjectFindRequest defines the possible options to search for projects. By default
|
||||||
// archived project will be excluded from response.
|
// archived project will be excluded from response.
|
||||||
type ProjectFindRequest struct {
|
type ProjectFindRequest struct {
|
||||||
Where *string
|
Where *string `schema:"where"`
|
||||||
Args []interface{}
|
Args []interface{} `schema:"args"`
|
||||||
Order []string
|
Order []string `schema:"order"`
|
||||||
Limit *uint
|
Limit *uint `schema:"limit"`
|
||||||
Offset *uint
|
Offset *uint `schema:"offset"`
|
||||||
IncludedArchived bool
|
IncludedArchived bool `schema:"included-archived"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectStatus represents the status of project.
|
// ProjectStatus represents the status of project.
|
||||||
|
@ -25,8 +25,8 @@ type User struct {
|
|||||||
ArchivedAt pq.NullTime `json:"archived_at"`
|
ArchivedAt pq.NullTime `json:"archived_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUserRequest contains information needed to create a new User.
|
// UserCreateRequest contains information needed to create a new User.
|
||||||
type CreateUserRequest struct {
|
type UserCreateRequest struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Email string `json:"email" validate:"required,email,unique"`
|
Email string `json:"email" validate:"required,email,unique"`
|
||||||
Password string `json:"password" validate:"required"`
|
Password string `json:"password" validate:"required"`
|
||||||
@ -34,21 +34,21 @@ type CreateUserRequest struct {
|
|||||||
Timezone *string `json:"timezone" validate:"omitempty"`
|
Timezone *string `json:"timezone" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUserRequest defines what information may be provided to modify an existing
|
// UserUpdateRequest defines what information may be provided to modify an existing
|
||||||
// User. All fields are optional so clients can send just the fields they want
|
// User. All fields are optional so clients can send just the fields they want
|
||||||
// changed. It uses pointer fields so we can differentiate between a field that
|
// changed. It uses pointer fields so we can differentiate between a field that
|
||||||
// was not provided and a field that was provided as explicitly blank. Normally
|
// was not provided and a field that was provided as explicitly blank. Normally
|
||||||
// we do not want to use pointers to basic types but we make exceptions around
|
// we do not want to use pointers to basic types but we make exceptions around
|
||||||
// marshalling/unmarshalling.
|
// marshalling/unmarshalling.
|
||||||
type UpdateUserRequest struct {
|
type UserUpdateRequest struct {
|
||||||
ID string `validate:"required,uuid"`
|
ID string `validate:"required,uuid"`
|
||||||
Name *string `json:"name" validate:"omitempty"`
|
Name *string `json:"name" validate:"omitempty"`
|
||||||
Email *string `json:"email" validate:"omitempty,email,unique"`
|
Email *string `json:"email" validate:"omitempty,email,unique"`
|
||||||
Timezone *string `json:"timezone" validate:"omitempty"`
|
Timezone *string `json:"timezone" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePassword defines what information is required to update a user password.
|
// UserUpdatePasswordRequest defines what information is required to update a user password.
|
||||||
type UpdatePasswordRequest struct {
|
type UserUpdatePasswordRequest struct {
|
||||||
ID string `validate:"required,uuid"`
|
ID string `validate:"required,uuid"`
|
||||||
Password string `json:"password" validate:"required"`
|
Password string `json:"password" validate:"required"`
|
||||||
PasswordConfirm string `json:"password_confirm" validate:"omitempty,eqfield=Password"`
|
PasswordConfirm string `json:"password_confirm" validate:"omitempty,eqfield=Password"`
|
||||||
@ -57,12 +57,12 @@ type UpdatePasswordRequest struct {
|
|||||||
// UserFindRequest defines the possible options to search for users. By default
|
// UserFindRequest defines the possible options to search for users. By default
|
||||||
// archived users will be excluded from response.
|
// archived users will be excluded from response.
|
||||||
type UserFindRequest struct {
|
type UserFindRequest struct {
|
||||||
Where *string
|
Where *string `schema:"where"`
|
||||||
Args []interface{}
|
Args []interface{} `schema:"args"`
|
||||||
Order []string
|
Order []string `schema:"order"`
|
||||||
Limit *uint
|
Limit *uint `schema:"limit"`
|
||||||
Offset *uint
|
Offset *uint `schema:"offset"`
|
||||||
IncludedArchived bool
|
IncludedArchived bool `schema:"included-archived"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token is the payload we deliver to users when they authenticate.
|
// Token is the payload we deliver to users when they authenticate.
|
||||||
|
@ -257,7 +257,7 @@ func uniqueEmail(ctx context.Context, dbConn *sqlx.DB, email, userId string) (bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create inserts a new user into the database.
|
// Create inserts a new user into the database.
|
||||||
func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req CreateUserRequest, now time.Time) (*User, error) {
|
func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserCreateRequest, now time.Time) (*User, error) {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Create")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Create")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
@ -368,7 +368,7 @@ func Read(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, i
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update replaces a user in the database.
|
// Update replaces a user in the database.
|
||||||
func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UpdateUserRequest, now time.Time) error {
|
func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserUpdateRequest, now time.Time) error {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Update")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Update")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
@ -452,9 +452,9 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Update
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update replaces a user in the database.
|
// Update changes the password for a user in the database.
|
||||||
func UpdatePassword(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UpdatePasswordRequest, now time.Time) error {
|
func UpdatePassword(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserUpdatePasswordRequest, now time.Time) error {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Update")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.UpdatePassword")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
|
@ -65,12 +65,12 @@ type DeleteUserAccountRequest struct {
|
|||||||
// UserAccountFindRequest defines the possible options to search for users accounts.
|
// UserAccountFindRequest defines the possible options to search for users accounts.
|
||||||
// By default archived user accounts will be excluded from response.
|
// By default archived user accounts will be excluded from response.
|
||||||
type UserAccountFindRequest struct {
|
type UserAccountFindRequest struct {
|
||||||
Where *string
|
Where *string `schema:"where"`
|
||||||
Args []interface{}
|
Args []interface{} `schema:"args"`
|
||||||
Order []string
|
Order []string `schema:"order"`
|
||||||
Limit *uint
|
Limit *uint `schema:"limit"`
|
||||||
Offset *uint
|
Offset *uint `schema:"offset"`
|
||||||
IncludedArchived bool
|
IncludedArchived bool `schema:"included-archived"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserAccountStatus represents the status of a user for an account.
|
// UserAccountStatus represents the status of a user for an account.
|
||||||
|
@ -88,11 +88,9 @@ func ParseLines(lines []string, depth int) (objs *GoObjects, err error) {
|
|||||||
|
|
||||||
ld := lineDepth(l)
|
ld := lineDepth(l)
|
||||||
|
|
||||||
|
|
||||||
//fmt.Println("l", l)
|
//fmt.Println("l", l)
|
||||||
//fmt.Println("> Depth", ld, "???", depth)
|
//fmt.Println("> Depth", ld, "???", depth)
|
||||||
|
|
||||||
|
|
||||||
if ld == depth {
|
if ld == depth {
|
||||||
if strings.HasPrefix(ls, "/*") {
|
if strings.HasPrefix(ls, "/*") {
|
||||||
multiLine = true
|
multiLine = true
|
||||||
@ -113,7 +111,6 @@ func ParseLines(lines []string, depth int) (objs *GoObjects, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//fmt.Println("> multiLine", multiLine)
|
//fmt.Println("> multiLine", multiLine)
|
||||||
//fmt.Println("> multiComment", multiComment)
|
//fmt.Println("> multiComment", multiComment)
|
||||||
//fmt.Println("> muiliVar", muiliVar)
|
//fmt.Println("> muiliVar", muiliVar)
|
||||||
|
@ -100,7 +100,6 @@ func TestParseLines1(t *testing.T) {
|
|||||||
g.Expect(objs.Lines()).Should(gomega.Equal(lines))
|
g.Expect(objs.Lines()).Should(gomega.Equal(lines))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLines2(t *testing.T) {
|
func TestParseLines2(t *testing.T) {
|
||||||
|
@ -133,7 +133,6 @@ func main() {
|
|||||||
cli.StringFlag{Name: "templateDir, templates", Value: "./templates/dbtable2crud"},
|
cli.StringFlag{Name: "templateDir, templates", Value: "./templates/dbtable2crud"},
|
||||||
cli.StringFlag{Name: "projectPath"},
|
cli.StringFlag{Name: "projectPath"},
|
||||||
cli.BoolFlag{Name: "saveChanges, save"},
|
cli.BoolFlag{Name: "saveChanges, save"},
|
||||||
|
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
dbTable := strings.TrimSpace(c.String("dbtable"))
|
dbTable := strings.TrimSpace(c.String("dbtable"))
|
||||||
|
@ -21,12 +21,13 @@ type {{ FormatCamel $.Model.Name }}UpdateRequest struct {
|
|||||||
// {{ FormatCamel $.Model.Name }}FindRequest defines the possible options to search for {{ FormatCamelPluralTitleLower $.Model.Name }}. By default
|
// {{ FormatCamel $.Model.Name }}FindRequest defines the possible options to search for {{ FormatCamelPluralTitleLower $.Model.Name }}. By default
|
||||||
// archived {{ FormatCamelLowerTitle $.Model.Name }} will be excluded from response.
|
// archived {{ FormatCamelLowerTitle $.Model.Name }} will be excluded from response.
|
||||||
type {{ FormatCamel $.Model.Name }}FindRequest struct {
|
type {{ FormatCamel $.Model.Name }}FindRequest struct {
|
||||||
Where *string
|
Where *string `schema:"where"`
|
||||||
Args []interface{}
|
Args []interface{} `schema:"args"`
|
||||||
Order []string
|
Order []string `schema:"order"`
|
||||||
Limit *uint
|
Limit *uint `schema:"limit"`
|
||||||
Offset *uint
|
Offset *uint `schema:"offset"`
|
||||||
{{ $hasArchived := (StringListHasValue $.Model.ColumnNames "archived_at") }}{{ if $hasArchived }}IncludedArchived bool{{ end }}
|
IncludedArchived bool
|
||||||
|
{{ $hasArchived := (StringListHasValue $.Model.ColumnNames "archived_at") }}{{ if $hasArchived }}IncludedArchived bool `schema:"included-archived"`{{ end }}
|
||||||
}
|
}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ define "Enums"}}
|
{{ define "Enums"}}
|
||||||
|
Reference in New Issue
Block a user