1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-17 00:17:59 +02:00

Completed signup package and hooked up to web-api. Can use the swagger

ui to signup a new account.
This commit is contained in:
Lee Brown
2019-06-25 02:40:29 -08:00
parent 957bd9bf36
commit 2fbda74a73
21 changed files with 1110 additions and 167 deletions

View File

@ -1,16 +1,26 @@
FROM golang:alpine3.9 AS build_base FROM golang:1.12.6-alpine3.9 AS build_base
LABEL maintainer="lee@geeksinthewoods.com" LABEL maintainer="lee@geeksinthewoods.com"
RUN apk --update --no-cache add \ RUN apk --update --no-cache add \
git git build-base gcc
RUN go get -u github.com/swaggo/swag/cmd/swag # Hack to get swag init to work correctly.
RUN GO111MODULE=off go get gopkg.in/go-playground/validator.v9 && \
GO111MODULE=off go get github.com/go-playground/universal-translator && \
GO111MODULE=off go get github.com/leodido/go-urn && \
GO111MODULE=off go get github.com/lib/pq/oid && \
GO111MODULE=off go get github.com/lib/pq/scram && \
GO111MODULE=off go get github.com/tinylib/msgp/msgp && \
GO111MODULE=off go get gopkg.in/DataDog/dd-trace-go.v1/ddtrace
# go to base project # Install swag with go modules enabled.
RUN GO111MODULE=on go get -u github.com/swaggo/swag/cmd/swag
# Change dir to project base.
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
# enable go modules # Enable go modules.
ENV GO111MODULE="on" ENV GO111MODULE="on"
COPY go.mod . COPY go.mod .
COPY go.sum . COPY go.sum .
@ -18,10 +28,10 @@ RUN go mod download
FROM build_base AS builder FROM build_base AS builder
# copy shared packages # Copy shared packages.
COPY internal ./internal COPY internal ./internal
# copy cmd specific package # Copy cmd specific packages.
COPY cmd/web-api ./cmd/web-api COPY cmd/web-api ./cmd/web-api
COPY cmd/web-api/templates /templates COPY cmd/web-api/templates /templates
#COPY cmd/web-api/static /static #COPY cmd/web-api/static /static

View File

@ -1,6 +1,6 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at // This file was generated by swaggo/swag at
// 2019-06-24 20:15:37.524606 -0800 AKDT m=+13.872100491 // 2019-06-25 02:19:21.144417 -0800 AKDT m=+51.040366621
package docs package docs
@ -16,10 +16,10 @@ var doc = `{
"info": { "info": {
"description": "This is a sample server celler server.", "description": "This is a sample server celler server.",
"title": "SaaS Example API", "title": "SaaS Example API",
"termsOfService": "/terms", "termsOfService": "http://example.com/terms",
"contact": { "contact": {
"name": "API Support", "name": "API Support",
"url": "/support", "url": "http://example.com/support",
"email": "support@geeksinthewoods.com" "email": "support@geeksinthewoods.com"
}, },
"license": { "license": {
@ -40,6 +40,9 @@ var doc = `{
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"account"
],
"summary": "Read returns the specified account from the system.", "summary": "Read returns the specified account from the system.",
"operationId": "get-string-by-int", "operationId": "get-string-by-int",
"parameters": [ "parameters": [
@ -88,6 +91,123 @@ var doc = `{
} }
} }
} }
},
"/signup": {
"post": {
"description": "Signup creates a new account and user in the system.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"signup"
],
"summary": "Signup handles new account creation.",
"parameters": [
{
"description": "Signup details",
"name": "data",
"in": "body",
"required": true,
"schema": {
"type": "object",
"$ref": "#/definitions/signup.SignupRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/signup.SignupResponse"
},
"headers": {
"Token": {
"type": "string",
"description": "qwerty"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
}
}
}
},
"/users/{id}": {
"get": {
"description": "get string by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Read returns the specified user from the system.",
"operationId": "get-string-by-int",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/user.User"
},
"headers": {
"Token": {
"type": "string",
"description": "qwerty"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@ -95,10 +215,12 @@ var doc = `{
"type": "object", "type": "object",
"properties": { "properties": {
"address1": { "address1": {
"type": "string" "type": "string",
"example": "221 Tatitlek Ave"
}, },
"address2": { "address2": {
"type": "string" "type": "string",
"example": "Box #1832"
}, },
"archived_at": { "archived_at": {
"type": "string" "type": "string"
@ -107,36 +229,166 @@ var doc = `{
"type": "string" "type": "string"
}, },
"city": { "city": {
"type": "string" "type": "string",
"example": "Valdez"
}, },
"country": { "country": {
"type": "string" "type": "string",
"example": "USA"
}, },
"created_at": { "created_at": {
"type": "string" "type": "string"
}, },
"id": { "id": {
"type": "string" "type": "string",
"example": "c4653bf9-5978-48b7-89c5-95704aebb7e2"
}, },
"name": { "name": {
"type": "string" "type": "string",
"example": "Company Name"
}, },
"region": { "region": {
"type": "string" "type": "string",
"example": "AK"
}, },
"signup_user_id": { "signup_user_id": {
"type": "string" "type": "string"
}, },
"status": { "status": {
"type": "AccountStatus" "type": "string",
"example": "active"
}, },
"timezone": { "timezone": {
"type": "string" "type": "string",
"example": "America/Anchorage"
}, },
"updated_at": { "updated_at": {
"type": "string" "type": "string"
}, },
"zipcode": { "zipcode": {
"type": "string",
"example": "99686"
}
}
},
"signup.SignupRequest": {
"type": "object",
"properties": {
"account": {
"type": "object",
"required": [
"name",
"address1",
"city",
"region",
"country",
"zipcode"
],
"properties": {
"address1": {
"type": "string",
"example": "221 Tatitlek Ave"
},
"address2": {
"type": "string",
"example": "Box #1832"
},
"city": {
"type": "string",
"example": "Valdez"
},
"country": {
"type": "string",
"example": "USA"
},
"name": {
"type": "string",
"example": "Company {RANDOM_UUID}"
},
"region": {
"type": "string",
"example": "AK"
},
"timezone": {
"type": "string",
"example": "America/Anchorage"
},
"zipcode": {
"type": "string",
"example": "99686"
}
}
},
"user": {
"type": "object",
"required": [
"name",
"email",
"password"
],
"properties": {
"email": {
"type": "string",
"example": "{RANDOM_EMAIL}"
},
"name": {
"type": "string",
"example": "Gabi May"
},
"password": {
"type": "string",
"example": "SecretString"
},
"password_confirm": {
"type": "string",
"example": "SecretString"
}
}
}
}
},
"signup.SignupResponse": {
"type": "object",
"properties": {
"account": {
"type": "object",
"$ref": "#/definitions/account.Account"
},
"user": {
"type": "object",
"$ref": "#/definitions/user.User"
}
}
},
"user.User": {
"type": "object",
"required": [
"name"
],
"properties": {
"archived_at": {
"type": "string"
},
"created_at": {
"type": "string"
},
"email": {
"type": "string",
"example": "gabi@geeksinthewoods.com"
},
"id": {
"type": "string",
"example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2"
},
"name": {
"type": "string",
"example": "Gabi May"
},
"timezone": {
"type": "string",
"example": "America/Anchorage"
},
"updated_at": {
"type": "string" "type": "string"
} }
} }

View File

@ -3,10 +3,10 @@
"info": { "info": {
"description": "This is a sample server celler server.", "description": "This is a sample server celler server.",
"title": "SaaS Example API", "title": "SaaS Example API",
"termsOfService": "/terms", "termsOfService": "http://example.com/terms",
"contact": { "contact": {
"name": "API Support", "name": "API Support",
"url": "/support", "url": "http://example.com/support",
"email": "support@geeksinthewoods.com" "email": "support@geeksinthewoods.com"
}, },
"license": { "license": {
@ -27,6 +27,9 @@
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [
"account"
],
"summary": "Read returns the specified account from the system.", "summary": "Read returns the specified account from the system.",
"operationId": "get-string-by-int", "operationId": "get-string-by-int",
"parameters": [ "parameters": [
@ -75,6 +78,123 @@
} }
} }
} }
},
"/signup": {
"post": {
"description": "Signup creates a new account and user in the system.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"signup"
],
"summary": "Signup handles new account creation.",
"parameters": [
{
"description": "Signup details",
"name": "data",
"in": "body",
"required": true,
"schema": {
"type": "object",
"$ref": "#/definitions/signup.SignupRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/signup.SignupResponse"
},
"headers": {
"Token": {
"type": "string",
"description": "qwerty"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
}
}
}
},
"/users/{id}": {
"get": {
"description": "get string by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Read returns the specified user from the system.",
"operationId": "get-string-by-int",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"$ref": "#/definitions/user.User"
},
"headers": {
"Token": {
"type": "string",
"description": "qwerty"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"type": "object",
"$ref": "#/definitions/web.Error"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@ -82,10 +202,12 @@
"type": "object", "type": "object",
"properties": { "properties": {
"address1": { "address1": {
"type": "string" "type": "string",
"example": "221 Tatitlek Ave"
}, },
"address2": { "address2": {
"type": "string" "type": "string",
"example": "Box #1832"
}, },
"archived_at": { "archived_at": {
"type": "string" "type": "string"
@ -94,36 +216,166 @@
"type": "string" "type": "string"
}, },
"city": { "city": {
"type": "string" "type": "string",
"example": "Valdez"
}, },
"country": { "country": {
"type": "string" "type": "string",
"example": "USA"
}, },
"created_at": { "created_at": {
"type": "string" "type": "string"
}, },
"id": { "id": {
"type": "string" "type": "string",
"example": "c4653bf9-5978-48b7-89c5-95704aebb7e2"
}, },
"name": { "name": {
"type": "string" "type": "string",
"example": "Company Name"
}, },
"region": { "region": {
"type": "string" "type": "string",
"example": "AK"
}, },
"signup_user_id": { "signup_user_id": {
"type": "string" "type": "string"
}, },
"status": { "status": {
"type": "AccountStatus" "type": "string",
"example": "active"
}, },
"timezone": { "timezone": {
"type": "string" "type": "string",
"example": "America/Anchorage"
}, },
"updated_at": { "updated_at": {
"type": "string" "type": "string"
}, },
"zipcode": { "zipcode": {
"type": "string",
"example": "99686"
}
}
},
"signup.SignupRequest": {
"type": "object",
"properties": {
"account": {
"type": "object",
"required": [
"name",
"address1",
"city",
"region",
"country",
"zipcode"
],
"properties": {
"address1": {
"type": "string",
"example": "221 Tatitlek Ave"
},
"address2": {
"type": "string",
"example": "Box #1832"
},
"city": {
"type": "string",
"example": "Valdez"
},
"country": {
"type": "string",
"example": "USA"
},
"name": {
"type": "string",
"example": "Company {RANDOM_UUID}"
},
"region": {
"type": "string",
"example": "AK"
},
"timezone": {
"type": "string",
"example": "America/Anchorage"
},
"zipcode": {
"type": "string",
"example": "99686"
}
}
},
"user": {
"type": "object",
"required": [
"name",
"email",
"password"
],
"properties": {
"email": {
"type": "string",
"example": "{RANDOM_EMAIL}"
},
"name": {
"type": "string",
"example": "Gabi May"
},
"password": {
"type": "string",
"example": "SecretString"
},
"password_confirm": {
"type": "string",
"example": "SecretString"
}
}
}
}
},
"signup.SignupResponse": {
"type": "object",
"properties": {
"account": {
"type": "object",
"$ref": "#/definitions/account.Account"
},
"user": {
"type": "object",
"$ref": "#/definitions/user.User"
}
}
},
"user.User": {
"type": "object",
"required": [
"name"
],
"properties": {
"archived_at": {
"type": "string"
},
"created_at": {
"type": "string"
},
"email": {
"type": "string",
"example": "gabi@geeksinthewoods.com"
},
"id": {
"type": "string",
"example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2"
},
"name": {
"type": "string",
"example": "Gabi May"
},
"timezone": {
"type": "string",
"example": "America/Anchorage"
},
"updated_at": {
"type": "string" "type": "string"
} }
} }

View File

@ -3,36 +3,134 @@ definitions:
account.Account: account.Account:
properties: properties:
address1: address1:
example: 221 Tatitlek Ave
type: string type: string
address2: address2:
example: 'Box #1832'
type: string type: string
archived_at: archived_at:
type: string type: string
billing_user_id: billing_user_id:
type: string type: string
city: city:
example: Valdez
type: string type: string
country: country:
example: USA
type: string type: string
created_at: created_at:
type: string type: string
id: id:
example: c4653bf9-5978-48b7-89c5-95704aebb7e2
type: string type: string
name: name:
example: Company Name
type: string type: string
region: region:
example: AK
type: string type: string
signup_user_id: signup_user_id:
type: string type: string
status: status:
type: AccountStatus example: active
type: string
timezone: timezone:
example: America/Anchorage
type: string type: string
updated_at: updated_at:
type: string type: string
zipcode: zipcode:
example: "99686"
type: string type: string
type: object type: object
signup.SignupRequest:
properties:
account:
properties:
address1:
example: 221 Tatitlek Ave
type: string
address2:
example: 'Box #1832'
type: string
city:
example: Valdez
type: string
country:
example: USA
type: string
name:
example: Company {RANDOM_UUID}
type: string
region:
example: AK
type: string
timezone:
example: America/Anchorage
type: string
zipcode:
example: "99686"
type: string
required:
- name
- address1
- city
- region
- country
- zipcode
type: object
user:
properties:
email:
example: '{RANDOM_EMAIL}'
type: string
name:
example: Gabi May
type: string
password:
example: SecretString
type: string
password_confirm:
example: SecretString
type: string
required:
- name
- email
- password
type: object
type: object
signup.SignupResponse:
properties:
account:
$ref: '#/definitions/account.Account'
type: object
user:
$ref: '#/definitions/user.User'
type: object
type: object
user.User:
properties:
archived_at:
type: string
created_at:
type: string
email:
example: gabi@geeksinthewoods.com
type: string
id:
example: d69bdef7-173f-4d29-b52c-3edc60baf6a2
type: string
name:
example: Gabi May
type: string
timezone:
example: America/Anchorage
type: string
updated_at:
type: string
required:
- name
type: object
web.Error: web.Error:
properties: properties:
err: err:
@ -49,12 +147,12 @@ info:
contact: contact:
email: support@geeksinthewoods.com email: support@geeksinthewoods.com
name: API Support name: API Support
url: /support url: http://example.com/support
description: This is a sample server celler server. description: This is a sample server celler server.
license: license:
name: Apache 2.0 name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html url: http://www.apache.org/licenses/LICENSE-2.0.html
termsOfService: /terms termsOfService: http://example.com/terms
title: SaaS Example API title: SaaS Example API
version: '{{.Version}}' version: '{{.Version}}'
paths: paths:
@ -98,6 +196,88 @@ paths:
$ref: '#/definitions/web.Error' $ref: '#/definitions/web.Error'
type: object type: object
summary: Read returns the specified account from the system. summary: Read returns the specified account from the system.
tags:
- account
/signup:
post:
consumes:
- application/json
description: Signup creates a new account and user in the system.
parameters:
- description: Signup details
in: body
name: data
required: true
schema:
$ref: '#/definitions/signup.SignupRequest'
type: object
produces:
- application/json
responses:
"200":
description: OK
headers:
Token:
description: qwerty
type: string
schema:
$ref: '#/definitions/signup.SignupResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/web.Error'
type: object
"403":
description: Forbidden
schema:
$ref: '#/definitions/web.Error'
type: object
summary: Signup handles new account creation.
tags:
- signup
/users/{id}:
get:
consumes:
- application/json
description: get string by ID
operationId: get-string-by-int
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
headers:
Token:
description: qwerty
type: string
schema:
$ref: '#/definitions/user.User'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/web.Error'
type: object
"403":
description: Forbidden
schema:
$ref: '#/definitions/web.Error'
type: object
"404":
description: Not Found
schema:
$ref: '#/definitions/web.Error'
type: object
summary: Read returns the specified user from the system.
tags:
- user
securityDefinitions: securityDefinitions:
OAuth2Password: OAuth2Password:
flow: password flow: password

View File

@ -42,6 +42,7 @@ func (a *Account) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
// Read godoc // Read godoc
// @Summary Read returns the specified account from the system. // @Summary Read returns the specified account from the system.
// @Description get string by ID // @Description get string by ID
// @Tags account
// @ID get-string-by-int // @ID get-string-by-int
// @Accept json // @Accept json
// @Produce json // @Produce json

View File

@ -53,6 +53,12 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
app.Handle("PATCH", "/v1/accounts/:id/archive", a.Archive, 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)) app.Handle("DELETE", "/v1/accounts/:id", a.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
// Register signup endpoints.
s := Signup{
MasterDB: masterDB,
}
app.Handle("POST", "/v1/signup", s.Signup)
// Register project. // Register project.
p := Project{ p := Project{
MasterDB: masterDB, MasterDB: masterDB,

View File

@ -0,0 +1,58 @@
package handlers
import (
"context"
"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"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"net/http"
)
// Signup represents the Signup API method handler set.
type Signup struct {
MasterDB *sqlx.DB
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
}
// Signup godoc
// @Summary Signup handles new account creation.
// @Description Signup creates a new account and user in the system.
// @Tags signup
// @Accept json
// @Produce json
// @Param data body signup.SignupRequest true "Signup details"
// @Success 200 {object} signup.SignupResponse
// @Header 200 {string} Token "qwerty"
// @Failure 400 {object} web.Error
// @Failure 403 {object} web.Error
// @Router /signup [post]
func (c *Signup) Signup(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 are optional as authentication is not required ATM for this method.
claims, _ := ctx.Value(auth.Key).(auth.Claims)
var req signup.SignupRequest
if err := web.Decode(r, &req); err != nil {
return errors.Wrap(err, "")
}
res, err := signup.Signup(ctx, claims, c.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)
}

View File

@ -44,7 +44,20 @@ func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request,
return web.RespondJson(ctx, w, res, http.StatusOK) return web.RespondJson(ctx, w, res, http.StatusOK)
} }
// Read returns the specified user from the system. // Read godoc
// @Summary Read returns the specified user from the system.
// @Description get string by ID
// @Tags user
// @ID get-string-by-int
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} user.User
// @Header 200 {string} Token "qwerty"
// @Failure 400 {object} web.Error
// @Failure 403 {object} web.Error
// @Failure 404 {object} web.Error
// @Router /users/{id} [get]
func (u *User) Read(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 {

View File

@ -1,14 +1,14 @@
FROM golang:alpine3.9 AS build_base FROM golang:1.12.6-alpine3.9 AS build_base
LABEL maintainer="lee@geeksinthewoods.com" LABEL maintainer="lee@geeksinthewoods.com"
RUN apk --update --no-cache add \ RUN apk --update --no-cache add \
git git
# go to base project # Change dir to project base.
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
# enable go modules # Enable go modules.
ENV GO111MODULE="on" ENV GO111MODULE="on"
COPY go.mod . COPY go.mod .
COPY go.sum . COPY go.sum .
@ -16,10 +16,10 @@ RUN go mod download
FROM build_base AS builder FROM build_base AS builder
# copy shared packages # Copy shared packages.
COPY internal ./internal COPY internal ./internal
# copy cmd specific package # Copy cmd specific packages.
COPY cmd/web-app ./cmd/web-app COPY cmd/web-app ./cmd/web-app
COPY cmd/web-app/templates /templates COPY cmd/web-app/templates /templates
COPY cmd/web-app/static /static COPY cmd/web-app/static /static

View File

@ -317,10 +317,10 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accoun
} }
if req.SignupUserID != nil { if req.SignupUserID != nil {
a.SignupUserID = sql.NullString{String: *req.SignupUserID, Valid: true} a.SignupUserID = &sql.NullString{String: *req.SignupUserID, Valid: true}
} }
if req.BillingUserID != nil { if req.BillingUserID != nil {
a.BillingUserID = sql.NullString{String: *req.BillingUserID, Valid: true} a.BillingUserID = &sql.NullString{String: *req.BillingUserID, Valid: true}
} }
// Build the insert SQL statement. // Build the insert SQL statement.

View File

@ -12,36 +12,36 @@ import (
// Account represents someone with access to our system. // Account represents someone with access to our system.
type Account struct { type Account struct {
ID string `json:"id"` ID string `json:"id" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
Name string `json:"name"` Name string `json:"name" example:"Company Name"`
Address1 string `json:"address1"` Address1 string `json:"address1" example:"221 Tatitlek Ave"`
Address2 string `json:"address2"` Address2 string `json:"address2" example:"Box #1832"`
City string `json:"city"` City string `json:"city" example:"Valdez"`
Region string `json:"region"` Region string `json:"region" example:"AK"`
Country string `json:"country"` Country string `json:"country" example:"USA"`
Zipcode string `json:"zipcode"` Zipcode string `json:"zipcode" example:"99686"`
Status AccountStatus `json:"status"` Status AccountStatus `json:"status" swaggertype:"string" example:"active"`
Timezone string `json:"timezone"` Timezone string `json:"timezone" example:"America/Anchorage"`
SignupUserID sql.NullString `json:"signup_user_id"` SignupUserID *sql.NullString `json:"signup_user_id,omitempty" swaggertype:"string"`
BillingUserID sql.NullString `json:"billing_user_id"` BillingUserID *sql.NullString `json:"billing_user_id,omitempty" swaggertype:"string"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
ArchivedAt pq.NullTime `json:"archived_at"` ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
} }
// AccountCreateRequest contains information needed to create a new Account. // AccountCreateRequest contains information needed to create a new Account.
type AccountCreateRequest struct { type AccountCreateRequest struct {
Name string `json:"name" validate:"required,unique"` Name string `json:"name" validate:"required,unique" example:"Company Name"`
Address1 string `json:"address1" validate:"required"` Address1 string `json:"address1" validate:"required" example:"221 Tatitlek Ave"`
Address2 string `json:"address2" validate:"omitempty"` Address2 string `json:"address2" validate:"omitempty" example:"Box #1832"`
City string `json:"city" validate:"required"` City string `json:"city" validate:"required" example:"Valdez"`
Region string `json:"region" validate:"required"` Region string `json:"region" validate:"required" example:"AK"`
Country string `json:"country" validate:"required"` Country string `json:"country" validate:"required" example:"USA"`
Zipcode string `json:"zipcode" validate:"required"` Zipcode string `json:"zipcode" validate:"required" example:"99686"`
Status *AccountStatus `json:"status" validate:"omitempty,oneof=active pending disabled"` Status *AccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active pending disabled" swaggertype:"string" enums:"active,pending,disabled" example:"active"`
Timezone *string `json:"timezone" validate:"omitempty"` Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
SignupUserID *string `json:"signup_user_id" validate:"omitempty,uuid"` SignupUserID *string `json:"signup_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
BillingUserID *string `json:"billing_user_id" validate:"omitempty,uuid"` BillingUserID *string `json:"billing_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
} }
// AccountUpdateRequest defines what information may be provided to modify an existing // AccountUpdateRequest defines what information may be provided to modify an existing
@ -51,29 +51,29 @@ type AccountCreateRequest struct {
// 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 AccountUpdateRequest struct { type AccountUpdateRequest struct {
ID string `validate:"required,uuid"` ID string `json:"id" validate:"required,uuid"`
Name *string `json:"name" validate:"omitempty,unique"` Name *string `json:"name,omitempty" validate:"omitempty,unique"`
Address1 *string `json:"address1" validate:"omitempty"` Address1 *string `json:"address1,omitempty" validate:"omitempty"`
Address2 *string `json:"address2" validate:"omitempty"` Address2 *string `json:"address2,omitempty" validate:"omitempty"`
City *string `json:"city" validate:"omitempty"` City *string `json:"city,omitempty" validate:"omitempty"`
Region *string `json:"region" validate:"omitempty"` Region *string `json:"region,omitempty" validate:"omitempty"`
Country *string `json:"country" validate:"omitempty"` Country *string `json:"country,omitempty" validate:"omitempty"`
Zipcode *string `json:"zipcode" validate:"omitempty"` Zipcode *string `json:"zipcode,omitempty" validate:"omitempty"`
Status *AccountStatus `json:"status" validate:"omitempty,oneof=active pending disabled"` Status *AccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active pending disabled" swaggertype:"string" enums:"active,pending,disabled"`
Timezone *string `json:"timezone" validate:"omitempty"` Timezone *string `json:"timezone,omitempty" validate:"omitempty"`
SignupUserID *string `json:"signup_user_id" validate:"omitempty,uuid"` SignupUserID *string `json:"signup_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
BillingUserID *string `json:"billing_user_id" validate:"omitempty,uuid"` BillingUserID *string `json:"billing_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
} }
// 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 `schema:"where"` Where *string `json:"where"`
Args []interface{} `schema:"args"` Args []interface{} `json:"args" swaggertype:"array,string"`
Order []string `schema:"order"` Order []string `json:"order"`
Limit *uint `schema:"limit"` Limit *uint `json:"limit"`
Offset *uint `schema:"offset"` Offset *uint `json:"offset"`
IncludedArchived bool `schema:"included-archived"` IncludedArchived bool `json:"included-archived"`
} }
// AccountStatus represents the status of an account. // AccountStatus represents the status of an account.

View File

@ -0,0 +1,121 @@
# SaaS swagger
Copyright 2019, Geeks Accelerator
accelerator@geeksinthewoods.com.com
## Description
saas middleware to automatically generate RESTful API documentation with Swagger 2.0.
## Usage
### Start using it
1. Add comments to your API source code, [See Declarative Comments Format](https://github.com/swaggo/swag#declarative-comments-format).
2. Download [Swag](https://github.com/swaggo/swag) for Go by using:
```sh
$ go get github.com/swaggo/swag/cmd/swag
```
3. Run the [Swag](https://github.com/swaggo/swag) in your Go project root folder which contains `main.go` file, [Swag](https://github.com/swaggo/swag) will parse comments and generate required files(`docs` folder and `docs/doc.go`).
```sh_ "github.com/swaggo/echo-swagger/v2/example/docs"
$ swag init
```
4. Import following in your code:
```go
import "geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid/saas-swagger" // saas-swagger middleware
```
**Canonical example:**
```go
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/web"
)
// @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, "", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
// Configuration
...
// =========================================================================
// 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)
// 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))
*/
...
}
```
5. Run it, and browser to http://localhost:1323/swagger/index.html, you can see Swagger 2.0 Api documents.
### Dynamic Placeholders
To help ease use of the Swagger UI, dynamic placeholders have been added to the middleware. They are replaced on each
request before the JSON is returned to the browser. These can be used in an `example` struct tag.
1. `{RANDOM_UUID}`
Generates a random UUID.
Example:
```
Name string `json:"name" validate:"required" example:"Company {RANDOM_UUID}"`
```
2. `{RANDOM_EMAIL}`
Generate a random email address. Format will be UUID@example.com
Example:
```
Email string `json:"email" validate:"required,email" example:"{RANDOM_EMAIL}"`
```

View File

@ -2,6 +2,8 @@ package saasSwagger
import ( import (
"context" "context"
"fmt"
"github.com/pborman/uuid"
"html/template" "html/template"
"net/http" "net/http"
"regexp" "regexp"
@ -92,6 +94,24 @@ func SaasWrapHandler(confs ...func(c *Config)) web.Handler {
if err != nil { if err != nil {
return web.NewRequestError(err, http.StatusInternalServerError) return web.NewRequestError(err, http.StatusInternalServerError)
} }
// Replace the dynamic placeholder {RANDOM_UUID}
for {
if !strings.Contains(doc, "{RANDOM_UUID}") {
break
}
doc = strings.Replace(doc, "{RANDOM_UUID}", uuid.NewRandom().String(), 1)
}
// Replace the dynamic placeholder {RANDOM_EMAIL}
for {
if !strings.Contains(doc, "{RANDOM_EMAIL}") {
break
}
randEmail := fmt.Sprintf("%s@example.com", uuid.NewRandom().String())
doc = strings.Replace(doc, "{RANDOM_EMAIL}", randEmail, 1)
}
return web.RespondJson(ctx, w, []byte(doc), http.StatusOK) return web.RespondJson(ctx, w, []byte(doc), http.StatusOK)
default: default:
if strings.HasSuffix(path, ".html") { if strings.HasSuffix(path, ".html") {

View File

@ -41,6 +41,11 @@ func init() {
} }
return name return name
}) })
f := func(fl validator.FieldLevel) bool {
return true
}
validate.RegisterValidation("unique", f)
} }
// Decode reads the body of an HTTP request looking for a JSON document. The // Decode reads the body of an HTTP request looking for a JSON document. The

View File

@ -10,20 +10,20 @@ import (
// Project represents a workflow. // Project represents a workflow.
type Project struct { type Project struct {
ID string `json:"id" validate:"required,uuid"` ID string `json:"id" validate:"required,uuid" example:"985f1746-1d9f-459f-a2d9-fc53ece5ae86"`
AccountID string `json:"account_id" validate:"required,uuid" truss:"api-create"` AccountID string `json:"account_id" validate:"required,uuid" truss:"api-create"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Status ProjectStatus `json:"status" validate:"omitempty,oneof=active disabled"` Status ProjectStatus `json:"status" validate:"omitempty,oneof=active disabled" enums:"active,disabled" swaggertype:"string"`
CreatedAt time.Time `json:"created_at" truss:"api-read"` CreatedAt time.Time `json:"created_at" truss:"api-read"`
UpdatedAt time.Time `json:"updated_at" truss:"api-read"` UpdatedAt time.Time `json:"updated_at" truss:"api-read"`
ArchivedAt pq.NullTime `json:"archived_at" truss:"api-hide"` ArchivedAt *pq.NullTime `json:"archived_at,omitempty" truss:"api-hide"`
} }
// ProjectCreateRequest contains information needed to create a new Project. // ProjectCreateRequest contains information needed to create a new Project.
type ProjectCreateRequest struct { type ProjectCreateRequest struct {
AccountID string `json:"account_id" validate:"required,uuid"` AccountID string `json:"account_id" validate:"required,uuid"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Status *ProjectStatus `json:"status" validate:"omitempty,oneof=active disabled"` Status *ProjectStatus `json:"status,omitempty" validate:"omitempty,oneof=active disabled" enums:"active,disabled" swaggertype:"string"`
} }
// ProjectUpdateRequest defines what information may be provided to modify an existing // ProjectUpdateRequest defines what information may be provided to modify an existing
@ -32,19 +32,19 @@ type ProjectCreateRequest struct {
// was not provided and a field that was provided as explicitly blank. // was not provided and a field that was provided as explicitly blank.
type ProjectUpdateRequest struct { type ProjectUpdateRequest struct {
ID string `json:"id" validate:"required,uuid"` ID string `json:"id" validate:"required,uuid"`
Name *string `json:"name" validate:"omitempty"` Name *string `json:"name,omitempty" validate:"omitempty"`
Status *ProjectStatus `json:"status" validate:"omitempty,oneof=active disabled"` Status *ProjectStatus `json:"status,omitempty" validate:"omitempty,oneof=active disabled" enums:"active,disabled" swaggertype:"string"`
} }
// 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 `schema:"where"` Where *string `json:"where"`
Args []interface{} `schema:"args"` Args []interface{} `json:"args" swaggertype:"array,string"`
Order []string `schema:"order"` Order []string `json:"order"`
Limit *uint `schema:"limit"` Limit *uint `json:"limit"`
Offset *uint `schema:"offset"` Offset *uint `json:"offset"`
IncludedArchived bool `schema:"included-archived"` IncludedArchived bool `json:"included-archived"`
} }
// ProjectStatus represents the status of project. // ProjectStatus represents the status of project.
@ -52,7 +52,6 @@ type ProjectStatus string
// ProjectStatus values define the status field of project. // ProjectStatus values define the status field of project.
const ( const (
// ProjectStatus_Active defines the status of active for project. // ProjectStatus_Active defines the status of active for project.
ProjectStatus_Active ProjectStatus = "active" ProjectStatus_Active ProjectStatus = "active"
// ProjectStatus_Disabled defines the status of disabled for project. // ProjectStatus_Disabled defines the status of disabled for project.
@ -61,7 +60,6 @@ const (
// ProjectStatus_Values provides list of valid ProjectStatus values. // ProjectStatus_Values provides list of valid ProjectStatus values.
var ProjectStatus_Values = []ProjectStatus{ var ProjectStatus_Values = []ProjectStatus{
ProjectStatus_Active, ProjectStatus_Active,
ProjectStatus_Disabled, ProjectStatus_Disabled,
} }

View File

@ -7,8 +7,22 @@ import (
// SignupRequest contains information needed perform signup. // SignupRequest contains information needed perform signup.
type SignupRequest struct { type SignupRequest struct {
Account account.AccountCreateRequest `json:"account" validate:"required"` Account struct {
User user.UserCreateRequest `json:"user" validate:"required"` Name string `json:"name" validate:"required,unique" example:"Company {RANDOM_UUID}"`
Address1 string `json:"address1" validate:"required" example:"221 Tatitlek Ave"`
Address2 string `json:"address2" validate:"omitempty" example:"Box #1832"`
City string `json:"city" validate:"required" example:"Valdez"`
Region string `json:"region" validate:"required" example:"AK"`
Country string `json:"country" validate:"required" example:"USA"`
Zipcode string `json:"zipcode" validate:"required" example:"99686"`
Timezone *string `json:"timezone" validate:"omitempty" example:"America/Anchorage"`
} `json:"account" validate:"required"` // Account details.
User struct {
Name string `json:"name" validate:"required" example:"Gabi May"`
Email string `json:"email" validate:"required,email,unique" example:"{RANDOM_EMAIL}"`
Password string `json:"password" validate:"required" example:"SecretString"`
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password" example:"SecretString"`
} `json:"user" validate:"required"` // User details.
} }
// SignupResponse contains information needed perform signup. // SignupResponse contains information needed perform signup.

View File

@ -19,12 +19,6 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
span, ctx := tracer.StartSpanFromContext(ctx, "internal.signup.Signup") span, ctx := tracer.StartSpanFromContext(ctx, "internal.signup.Signup")
defer span.Finish() defer span.Finish()
// Default account status to active for signup if now set.
if req.Account.Status == nil {
s := account.AccountStatus_Active
req.Account.Status = &s
}
v := validator.New() v := validator.New()
// Validate the user email address is unique in the database. // Validate the user email address is unique in the database.
@ -64,18 +58,38 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
var resp SignupResponse var resp SignupResponse
// UserCreateRequest contains information needed to create a new User.
userReq := user.UserCreateRequest{
Name: req.User.Name,
Email: req.User.Email,
Password: req.User.Password,
PasswordConfirm: req.User.PasswordConfirm,
Timezone: req.Account.Timezone,
}
// Execute user creation. // Execute user creation.
resp.User, err = user.Create(ctx, claims, dbConn, req.User, now) resp.User, err = user.Create(ctx, claims, dbConn, userReq, now)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Set the signup and billing user IDs for reference. accountStatus := account.AccountStatus_Active
req.Account.SignupUserID = &resp.User.ID accountReq := account.AccountCreateRequest{
req.Account.BillingUserID = &resp.User.ID Name: req.Account.Name,
Address1: req.Account.Address1,
Address2: req.Account.Address2,
City: req.Account.City,
Region: req.Account.Region,
Country: req.Account.Country,
Zipcode: req.Account.Zipcode,
Status: &accountStatus,
Timezone: req.Account.Timezone,
SignupUserID: &resp.User.ID,
BillingUserID: &resp.User.ID,
}
// Execute account creation. // Execute account creation.
resp.Account, err = account.Create(ctx, claims, dbConn, req.Account, now) resp.Account, err = account.Create(ctx, claims, dbConn, accountReq, now)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -10,28 +10,28 @@ import (
// User represents someone with access to our system. // User represents someone with access to our system.
type User struct { type User struct {
ID string `json:"id"` ID string `json:"id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
Name string `json:"name"` Name string `json:"name" validate:"required" example:"Gabi May"`
Email string `json:"email"` Email string `json:"email" example:"gabi@geeksinthewoods.com"`
PasswordSalt string `json:"-"` PasswordSalt string `json:"-"`
PasswordHash []byte `json:"-"` PasswordHash []byte `json:"-"`
PasswordReset sql.NullString `json:"-"` PasswordReset *sql.NullString `json:"-"`
Timezone string `json:"timezone"` Timezone string `json:"timezone" example:"America/Anchorage"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
ArchivedAt pq.NullTime `json:"archived_at"` ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
} }
// UserCreateRequest contains information needed to create a new User. // UserCreateRequest contains information needed to create a new User.
type UserCreateRequest struct { type UserCreateRequest struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required" example:"Gabi May"`
Email string `json:"email" validate:"required,email,unique"` Email string `json:"email" validate:"required,email,unique" example:"gabi@geeksinthewoods.com"`
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required" example:"SecretString"`
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"` PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password" example:"SecretString"`
Timezone *string `json:"timezone" validate:"omitempty"` Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
} }
// UserUpdateRequest defines what information may be provided to modify an existing // UserUpdateRequest defines what information may be provided to modify an existing
@ -41,15 +41,15 @@ type UserCreateRequest struct {
// 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 UserUpdateRequest struct { type UserUpdateRequest struct {
ID string `validate:"required,uuid"` ID string `json:"id" validate:"required,uuid"`
Name *string `json:"name" validate:"omitempty"` Name *string `json:"name,omitempty" validate:"omitempty"`
Email *string `json:"email" validate:"omitempty,email,unique"` Email *string `json:"email,omitempty" validate:"omitempty,email,unique"`
Timezone *string `json:"timezone" validate:"omitempty"` Timezone *string `json:"timezone,omitempty" validate:"omitempty"`
} }
// UserUpdatePasswordRequest defines what information is required to update a user password. // UserUpdatePasswordRequest defines what information is required to update a user password.
type UserUpdatePasswordRequest struct { type UserUpdatePasswordRequest struct {
ID string `validate:"required,uuid"` ID string `json:"id" 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,16 +57,16 @@ type UserUpdatePasswordRequest 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 `schema:"where"` Where *string `json:"where"`
Args []interface{} `schema:"args"` Args []interface{} `json:"args" swaggertype:"array,string"`
Order []string `schema:"order"` Order []string `json:"order"`
Limit *uint `schema:"limit"` Limit *uint `json:"limit"`
Offset *uint `schema:"offset"` Offset *uint `json:"offset"`
IncludedArchived bool `schema:"included-archived"` IncludedArchived bool `json:"included-archived"`
} }
// Token is the payload we deliver to users when they authenticate. // Token is the payload we deliver to users when they authenticate.
type Token struct { type Token struct {
Token string `json:"token"` Token string `json:"token" validate:"required"`
claims auth.Claims `json:"-"` claims auth.Claims `json:"-"`
} }

View File

@ -17,14 +17,14 @@ import (
// application. The status will allow users to be managed on by account with users // application. The status will allow users to be managed on by account with users
// being global to the application. // being global to the application.
type UserAccount struct { type UserAccount struct {
ID string `json:"id"` ID string `json:"id" example:"72938896-a998-4258-a17b-6418dcdb80e3"`
UserID string `json:"user_id"` UserID string `json:"user_id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
AccountID string `json:"account_id"` AccountID string `json:"account_id" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
Roles UserAccountRoles `json:"roles"` Roles UserAccountRoles `json:"roles" swaggertype:"array,string" enums:"admin,user" example:"admin"`
Status UserAccountStatus `json:"status"` Status UserAccountStatus `json:"status" swaggertype:"string" enums:"active,invited,disabled" example:"active"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
ArchivedAt pq.NullTime `json:"archived_at"` ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
} }
// CreateUserAccountRequest defines the information is needed to associate a user to an // CreateUserAccountRequest defines the information is needed to associate a user to an
@ -32,45 +32,45 @@ type UserAccount struct {
// on an account level. If a current entry exists in the database but is archived, // on an account level. If a current entry exists in the database but is archived,
// it will be un-archived. // it will be un-archived.
type CreateUserAccountRequest struct { type CreateUserAccountRequest struct {
UserID string `validate:"required,uuid"` UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
AccountID string `validate:"required,uuid"` AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user"` Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"admin"`
Status *UserAccountStatus `json:"status" validate:"omitempty,oneof=active invited disabled"` Status *UserAccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active invited disabled" enums:"active,invited,disabled" swaggertype:"string" example:"active"`
} }
// UpdateUserAccountRequest defines the information needed to update the roles or the // UpdateUserAccountRequest defines the information needed to update the roles or the
// status for an existing user account. // status for an existing user account.
type UpdateUserAccountRequest struct { type UpdateUserAccountRequest struct {
UserID string `validate:"required,uuid"` UserID string `json:"user_id" validate:"required,uuid"`
AccountID string `validate:"required,uuid"` AccountID string `json:"account_id" validate:"required,uuid"`
Roles *UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user"` Roles *UserAccountRoles `json:"roles,omitempty" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"user"`
Status *UserAccountStatus `json:"status" validate:"omitempty,oneof=active invited disabled"` Status *UserAccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active invited disabled" enums:"active,invited,disabled" swaggertype:"string" example:"disabled"`
unArchive bool `json:"-"` // Internal use only. unArchive bool `json:"-"` // Internal use only.
} }
// ArchiveUserAccountRequest defines the information needed to remove an existing account // ArchiveUserAccountRequest defines the information needed to remove an existing account
// for a user. This will archive (soft-delete) the existing database entry. // for a user. This will archive (soft-delete) the existing database entry.
type ArchiveUserAccountRequest struct { type ArchiveUserAccountRequest struct {
UserID string `validate:"required,uuid"` UserID string `json:"user_id" validate:"required,uuid"`
AccountID string `validate:"required,uuid"` AccountID string `json:"account_id" validate:"required,uuid"`
} }
// DeleteUserAccountRequest defines the information needed to delete an existing account // DeleteUserAccountRequest defines the information needed to delete an existing account
// for a user. This will hard delete the existing database entry. // for a user. This will hard delete the existing database entry.
type DeleteUserAccountRequest struct { type DeleteUserAccountRequest struct {
UserID string `validate:"required,uuid"` UserID string `json:"user_id" validate:"required,uuid"`
AccountID string `validate:"required,uuid"` AccountID string `json:"account_id" validate:"required,uuid"`
} }
// 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 `schema:"where"` Where *string `json:"where"`
Args []interface{} `schema:"args"` Args []interface{} `json:"args" swaggertype:"array,string"`
Order []string `schema:"order"` Order []string `json:"order"`
Limit *uint `schema:"limit"` Limit *uint `json:"limit"`
Offset *uint `schema:"offset"` Offset *uint `json:"offset"`
IncludedArchived bool `schema:"included-archived"` IncludedArchived bool `json:"included-archived"`
} }
// UserAccountStatus represents the status of a user for an account. // UserAccountStatus represents the status of a user for an account.

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"database/sql" "database/sql"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
"github.com/lib/pq"
"time" "time"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
@ -258,7 +257,7 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Create
ua = *existing[0] ua = *existing[0]
ua.Roles = req.Roles ua.Roles = req.Roles
ua.UpdatedAt = now ua.UpdatedAt = now
ua.ArchivedAt = pq.NullTime{} ua.ArchivedAt = nil
} else { } else {
ua = UserAccount{ ua = UserAccount{
ID: uuid.NewRandom().String(), ID: uuid.NewRandom().String(),

View File

@ -352,7 +352,7 @@ func TestCreateExistingEntry(t *testing.T) {
if err != nil || arcRes == nil { if err != nil || arcRes == nil {
t.Log("\t\tGot :", err) t.Log("\t\tGot :", err)
t.Fatalf("\t%s\tFind user account failed.", tests.Failed) t.Fatalf("\t%s\tFind user account failed.", tests.Failed)
} else if findRes.ArchivedAt.Valid && !findRes.ArchivedAt.Time.IsZero() { } else if findRes.ArchivedAt != nil && findRes.ArchivedAt.Valid && !findRes.ArchivedAt.Time.IsZero() {
t.Fatalf("\t%s\tExpected user account to have archived_at empty", tests.Failed) t.Fatalf("\t%s\tExpected user account to have archived_at empty", tests.Failed)
} }
@ -657,7 +657,7 @@ func TestCrud(t *testing.T) {
Status: ua.Status, Status: ua.Status,
CreatedAt: ua.CreatedAt, CreatedAt: ua.CreatedAt,
UpdatedAt: now, UpdatedAt: now,
ArchivedAt: pq.NullTime{Time: now, Valid: true}, ArchivedAt: &pq.NullTime{Time: now, Valid: true},
}, },
} }
if diff := cmp.Diff(findRes, expected); diff != "" { if diff := cmp.Diff(findRes, expected); diff != "" {