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
Completed signup package and hooked up to web-api. Can use the swagger
ui to signup a new account.
This commit is contained in:
@ -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"
|
||||
|
||||
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
|
||||
|
||||
# enable go modules
|
||||
# Enable go modules.
|
||||
ENV GO111MODULE="on"
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
@ -18,10 +28,10 @@ RUN go mod download
|
||||
|
||||
FROM build_base AS builder
|
||||
|
||||
# copy shared packages
|
||||
# Copy shared packages.
|
||||
COPY internal ./internal
|
||||
|
||||
# copy cmd specific package
|
||||
# Copy cmd specific packages.
|
||||
COPY cmd/web-api ./cmd/web-api
|
||||
COPY cmd/web-api/templates /templates
|
||||
#COPY cmd/web-api/static /static
|
||||
|
@ -1,6 +1,6 @@
|
||||
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||
// 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
|
||||
|
||||
@ -16,10 +16,10 @@ var doc = `{
|
||||
"info": {
|
||||
"description": "This is a sample server celler server.",
|
||||
"title": "SaaS Example API",
|
||||
"termsOfService": "/terms",
|
||||
"termsOfService": "http://example.com/terms",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "/support",
|
||||
"url": "http://example.com/support",
|
||||
"email": "support@geeksinthewoods.com"
|
||||
},
|
||||
"license": {
|
||||
@ -40,6 +40,9 @@ var doc = `{
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"account"
|
||||
],
|
||||
"summary": "Read returns the specified account from the system.",
|
||||
"operationId": "get-string-by-int",
|
||||
"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": {
|
||||
@ -95,10 +215,12 @@ var doc = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address1": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "221 Tatitlek Ave"
|
||||
},
|
||||
"address2": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "Box #1832"
|
||||
},
|
||||
"archived_at": {
|
||||
"type": "string"
|
||||
@ -107,36 +229,166 @@ var doc = `{
|
||||
"type": "string"
|
||||
},
|
||||
"city": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "Valdez"
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "USA"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "c4653bf9-5978-48b7-89c5-95704aebb7e2"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "Company Name"
|
||||
},
|
||||
"region": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "AK"
|
||||
},
|
||||
"signup_user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "AccountStatus"
|
||||
"type": "string",
|
||||
"example": "active"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "America/Anchorage"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,10 @@
|
||||
"info": {
|
||||
"description": "This is a sample server celler server.",
|
||||
"title": "SaaS Example API",
|
||||
"termsOfService": "/terms",
|
||||
"termsOfService": "http://example.com/terms",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "/support",
|
||||
"url": "http://example.com/support",
|
||||
"email": "support@geeksinthewoods.com"
|
||||
},
|
||||
"license": {
|
||||
@ -27,6 +27,9 @@
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"account"
|
||||
],
|
||||
"summary": "Read returns the specified account from the system.",
|
||||
"operationId": "get-string-by-int",
|
||||
"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": {
|
||||
@ -82,10 +202,12 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address1": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "221 Tatitlek Ave"
|
||||
},
|
||||
"address2": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "Box #1832"
|
||||
},
|
||||
"archived_at": {
|
||||
"type": "string"
|
||||
@ -94,36 +216,166 @@
|
||||
"type": "string"
|
||||
},
|
||||
"city": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "Valdez"
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "USA"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "c4653bf9-5978-48b7-89c5-95704aebb7e2"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "Company Name"
|
||||
},
|
||||
"region": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "AK"
|
||||
},
|
||||
"signup_user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "AccountStatus"
|
||||
"type": "string",
|
||||
"example": "active"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "America/Anchorage"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -3,36 +3,134 @@ definitions:
|
||||
account.Account:
|
||||
properties:
|
||||
address1:
|
||||
example: 221 Tatitlek Ave
|
||||
type: string
|
||||
address2:
|
||||
example: 'Box #1832'
|
||||
type: string
|
||||
archived_at:
|
||||
type: string
|
||||
billing_user_id:
|
||||
type: string
|
||||
city:
|
||||
example: Valdez
|
||||
type: string
|
||||
country:
|
||||
example: USA
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
id:
|
||||
example: c4653bf9-5978-48b7-89c5-95704aebb7e2
|
||||
type: string
|
||||
name:
|
||||
example: Company Name
|
||||
type: string
|
||||
region:
|
||||
example: AK
|
||||
type: string
|
||||
signup_user_id:
|
||||
type: string
|
||||
status:
|
||||
type: AccountStatus
|
||||
example: active
|
||||
type: string
|
||||
timezone:
|
||||
example: America/Anchorage
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
zipcode:
|
||||
example: "99686"
|
||||
type: string
|
||||
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:
|
||||
properties:
|
||||
err:
|
||||
@ -49,12 +147,12 @@ info:
|
||||
contact:
|
||||
email: support@geeksinthewoods.com
|
||||
name: API Support
|
||||
url: /support
|
||||
url: http://example.com/support
|
||||
description: This is a sample server celler server.
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
termsOfService: /terms
|
||||
termsOfService: http://example.com/terms
|
||||
title: SaaS Example API
|
||||
version: '{{.Version}}'
|
||||
paths:
|
||||
@ -98,6 +196,88 @@ paths:
|
||||
$ref: '#/definitions/web.Error'
|
||||
type: object
|
||||
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:
|
||||
OAuth2Password:
|
||||
flow: password
|
||||
|
@ -42,6 +42,7 @@ func (a *Account) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
||||
// Read godoc
|
||||
// @Summary Read returns the specified account from the system.
|
||||
// @Description get string by ID
|
||||
// @Tags account
|
||||
// @ID get-string-by-int
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
@ -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("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.
|
||||
p := Project{
|
||||
MasterDB: masterDB,
|
||||
|
58
example-project/cmd/web-api/handlers/signup.go
Normal file
58
example-project/cmd/web-api/handlers/signup.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||
if !ok {
|
||||
|
@ -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"
|
||||
|
||||
RUN apk --update --no-cache add \
|
||||
git
|
||||
|
||||
# go to base project
|
||||
# Change dir to project base.
|
||||
WORKDIR $GOPATH/src/gitlab.com/geeks-accelerator/oss/saas-starter-kit/example-project
|
||||
|
||||
# enable go modules
|
||||
# Enable go modules.
|
||||
ENV GO111MODULE="on"
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
@ -16,10 +16,10 @@ RUN go mod download
|
||||
|
||||
FROM build_base AS builder
|
||||
|
||||
# copy shared packages
|
||||
# Copy shared packages.
|
||||
COPY internal ./internal
|
||||
|
||||
# copy cmd specific package
|
||||
# Copy cmd specific packages.
|
||||
COPY cmd/web-app ./cmd/web-app
|
||||
COPY cmd/web-app/templates /templates
|
||||
COPY cmd/web-app/static /static
|
||||
|
@ -317,10 +317,10 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accoun
|
||||
}
|
||||
|
||||
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 {
|
||||
a.BillingUserID = sql.NullString{String: *req.BillingUserID, Valid: true}
|
||||
a.BillingUserID = &sql.NullString{String: *req.BillingUserID, Valid: true}
|
||||
}
|
||||
|
||||
// Build the insert SQL statement.
|
||||
|
@ -12,36 +12,36 @@ import (
|
||||
|
||||
// Account represents someone with access to our system.
|
||||
type Account struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address1 string `json:"address1"`
|
||||
Address2 string `json:"address2"`
|
||||
City string `json:"city"`
|
||||
Region string `json:"region"`
|
||||
Country string `json:"country"`
|
||||
Zipcode string `json:"zipcode"`
|
||||
Status AccountStatus `json:"status"`
|
||||
Timezone string `json:"timezone"`
|
||||
SignupUserID sql.NullString `json:"signup_user_id"`
|
||||
BillingUserID sql.NullString `json:"billing_user_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ArchivedAt pq.NullTime `json:"archived_at"`
|
||||
ID string `json:"id" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||
Name string `json:"name" example:"Company Name"`
|
||||
Address1 string `json:"address1" example:"221 Tatitlek Ave"`
|
||||
Address2 string `json:"address2" example:"Box #1832"`
|
||||
City string `json:"city" example:"Valdez"`
|
||||
Region string `json:"region" example:"AK"`
|
||||
Country string `json:"country" example:"USA"`
|
||||
Zipcode string `json:"zipcode" example:"99686"`
|
||||
Status AccountStatus `json:"status" swaggertype:"string" example:"active"`
|
||||
Timezone string `json:"timezone" example:"America/Anchorage"`
|
||||
SignupUserID *sql.NullString `json:"signup_user_id,omitempty" swaggertype:"string"`
|
||||
BillingUserID *sql.NullString `json:"billing_user_id,omitempty" swaggertype:"string"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
|
||||
}
|
||||
|
||||
// AccountCreateRequest contains information needed to create a new Account.
|
||||
type AccountCreateRequest struct {
|
||||
Name string `json:"name" validate:"required,unique"`
|
||||
Address1 string `json:"address1" validate:"required"`
|
||||
Address2 string `json:"address2" validate:"omitempty"`
|
||||
City string `json:"city" validate:"required"`
|
||||
Region string `json:"region" validate:"required"`
|
||||
Country string `json:"country" validate:"required"`
|
||||
Zipcode string `json:"zipcode" validate:"required"`
|
||||
Status *AccountStatus `json:"status" validate:"omitempty,oneof=active pending disabled"`
|
||||
Timezone *string `json:"timezone" validate:"omitempty"`
|
||||
SignupUserID *string `json:"signup_user_id" validate:"omitempty,uuid"`
|
||||
BillingUserID *string `json:"billing_user_id" validate:"omitempty,uuid"`
|
||||
Name string `json:"name" validate:"required,unique" example:"Company Name"`
|
||||
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"`
|
||||
Status *AccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active pending disabled" swaggertype:"string" enums:"active,pending,disabled" example:"active"`
|
||||
Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
|
||||
SignupUserID *string `json:"signup_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
|
||||
BillingUserID *string `json:"billing_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
|
||||
}
|
||||
|
||||
// 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
|
||||
// marshalling/unmarshalling.
|
||||
type AccountUpdateRequest struct {
|
||||
ID string `validate:"required,uuid"`
|
||||
Name *string `json:"name" validate:"omitempty,unique"`
|
||||
Address1 *string `json:"address1" validate:"omitempty"`
|
||||
Address2 *string `json:"address2" validate:"omitempty"`
|
||||
City *string `json:"city" validate:"omitempty"`
|
||||
Region *string `json:"region" validate:"omitempty"`
|
||||
Country *string `json:"country" validate:"omitempty"`
|
||||
Zipcode *string `json:"zipcode" validate:"omitempty"`
|
||||
Status *AccountStatus `json:"status" validate:"omitempty,oneof=active pending disabled"`
|
||||
Timezone *string `json:"timezone" validate:"omitempty"`
|
||||
SignupUserID *string `json:"signup_user_id" validate:"omitempty,uuid"`
|
||||
BillingUserID *string `json:"billing_user_id" validate:"omitempty,uuid"`
|
||||
ID string `json:"id" validate:"required,uuid"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,unique"`
|
||||
Address1 *string `json:"address1,omitempty" validate:"omitempty"`
|
||||
Address2 *string `json:"address2,omitempty" validate:"omitempty"`
|
||||
City *string `json:"city,omitempty" validate:"omitempty"`
|
||||
Region *string `json:"region,omitempty" validate:"omitempty"`
|
||||
Country *string `json:"country,omitempty" validate:"omitempty"`
|
||||
Zipcode *string `json:"zipcode,omitempty" validate:"omitempty"`
|
||||
Status *AccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active pending disabled" swaggertype:"string" enums:"active,pending,disabled"`
|
||||
Timezone *string `json:"timezone,omitempty" validate:"omitempty"`
|
||||
SignupUserID *string `json:"signup_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
|
||||
BillingUserID *string `json:"billing_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
|
||||
}
|
||||
|
||||
// AccountFindRequest defines the possible options to search for accounts. By default
|
||||
// archived accounts will be excluded from response.
|
||||
type AccountFindRequest struct {
|
||||
Where *string `schema:"where"`
|
||||
Args []interface{} `schema:"args"`
|
||||
Order []string `schema:"order"`
|
||||
Limit *uint `schema:"limit"`
|
||||
Offset *uint `schema:"offset"`
|
||||
IncludedArchived bool `schema:"included-archived"`
|
||||
Where *string `json:"where"`
|
||||
Args []interface{} `json:"args" swaggertype:"array,string"`
|
||||
Order []string `json:"order"`
|
||||
Limit *uint `json:"limit"`
|
||||
Offset *uint `json:"offset"`
|
||||
IncludedArchived bool `json:"included-archived"`
|
||||
}
|
||||
|
||||
// AccountStatus represents the status of an account.
|
||||
|
121
example-project/internal/mid/saas-swagger/README.md
Normal file
121
example-project/internal/mid/saas-swagger/README.md
Normal 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}"`
|
||||
```
|
@ -2,6 +2,8 @@ package saasSwagger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/pborman/uuid"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@ -92,6 +94,24 @@ func SaasWrapHandler(confs ...func(c *Config)) web.Handler {
|
||||
if err != nil {
|
||||
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)
|
||||
default:
|
||||
if strings.HasSuffix(path, ".html") {
|
||||
|
@ -41,6 +41,11 @@ func init() {
|
||||
}
|
||||
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
|
||||
|
@ -10,20 +10,20 @@ import (
|
||||
|
||||
// Project represents a workflow.
|
||||
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"`
|
||||
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"`
|
||||
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.
|
||||
type ProjectCreateRequest struct {
|
||||
AccountID string `json:"account_id" validate:"required,uuid"`
|
||||
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
|
||||
@ -32,19 +32,19 @@ type ProjectCreateRequest struct {
|
||||
// was not provided and a field that was provided as explicitly blank.
|
||||
type ProjectUpdateRequest struct {
|
||||
ID string `json:"id" validate:"required,uuid"`
|
||||
Name *string `json:"name" validate:"omitempty"`
|
||||
Status *ProjectStatus `json:"status" validate:"omitempty,oneof=active disabled"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
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
|
||||
// archived project will be excluded from response.
|
||||
type ProjectFindRequest struct {
|
||||
Where *string `schema:"where"`
|
||||
Args []interface{} `schema:"args"`
|
||||
Order []string `schema:"order"`
|
||||
Limit *uint `schema:"limit"`
|
||||
Offset *uint `schema:"offset"`
|
||||
IncludedArchived bool `schema:"included-archived"`
|
||||
Where *string `json:"where"`
|
||||
Args []interface{} `json:"args" swaggertype:"array,string"`
|
||||
Order []string `json:"order"`
|
||||
Limit *uint `json:"limit"`
|
||||
Offset *uint `json:"offset"`
|
||||
IncludedArchived bool `json:"included-archived"`
|
||||
}
|
||||
|
||||
// ProjectStatus represents the status of project.
|
||||
@ -52,7 +52,6 @@ type ProjectStatus string
|
||||
|
||||
// ProjectStatus values define the status field of project.
|
||||
const (
|
||||
|
||||
// ProjectStatus_Active defines the status of active for project.
|
||||
ProjectStatus_Active ProjectStatus = "active"
|
||||
// ProjectStatus_Disabled defines the status of disabled for project.
|
||||
@ -61,7 +60,6 @@ const (
|
||||
|
||||
// ProjectStatus_Values provides list of valid ProjectStatus values.
|
||||
var ProjectStatus_Values = []ProjectStatus{
|
||||
|
||||
ProjectStatus_Active,
|
||||
ProjectStatus_Disabled,
|
||||
}
|
||||
|
@ -7,8 +7,22 @@ import (
|
||||
|
||||
// SignupRequest contains information needed perform signup.
|
||||
type SignupRequest struct {
|
||||
Account account.AccountCreateRequest `json:"account" validate:"required"`
|
||||
User user.UserCreateRequest `json:"user" validate:"required"`
|
||||
Account struct {
|
||||
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.
|
||||
|
@ -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")
|
||||
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()
|
||||
|
||||
// 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
|
||||
|
||||
// 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.
|
||||
resp.User, err = user.Create(ctx, claims, dbConn, req.User, now)
|
||||
resp.User, err = user.Create(ctx, claims, dbConn, userReq, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set the signup and billing user IDs for reference.
|
||||
req.Account.SignupUserID = &resp.User.ID
|
||||
req.Account.BillingUserID = &resp.User.ID
|
||||
accountStatus := account.AccountStatus_Active
|
||||
accountReq := account.AccountCreateRequest{
|
||||
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.
|
||||
resp.Account, err = account.Create(ctx, claims, dbConn, req.Account, now)
|
||||
resp.Account, err = account.Create(ctx, claims, dbConn, accountReq, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -10,28 +10,28 @@ import (
|
||||
|
||||
// User represents someone with access to our system.
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
ID string `json:"id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||
Name string `json:"name" validate:"required" example:"Gabi May"`
|
||||
Email string `json:"email" example:"gabi@geeksinthewoods.com"`
|
||||
|
||||
PasswordSalt string `json:"-"`
|
||||
PasswordHash []byte `json:"-"`
|
||||
PasswordReset sql.NullString `json:"-"`
|
||||
PasswordSalt string `json:"-"`
|
||||
PasswordHash []byte `json:"-"`
|
||||
PasswordReset *sql.NullString `json:"-"`
|
||||
|
||||
Timezone string `json:"timezone"`
|
||||
Timezone string `json:"timezone" example:"America/Anchorage"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ArchivedAt pq.NullTime `json:"archived_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
|
||||
}
|
||||
|
||||
// UserCreateRequest contains information needed to create a new User.
|
||||
type UserCreateRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Email string `json:"email" validate:"required,email,unique"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"`
|
||||
Timezone *string `json:"timezone" validate:"omitempty"`
|
||||
Name string `json:"name" validate:"required" example:"Gabi May"`
|
||||
Email string `json:"email" validate:"required,email,unique" example:"gabi@geeksinthewoods.com"`
|
||||
Password string `json:"password" validate:"required" example:"SecretString"`
|
||||
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password" example:"SecretString"`
|
||||
Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
|
||||
}
|
||||
|
||||
// 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
|
||||
// marshalling/unmarshalling.
|
||||
type UserUpdateRequest struct {
|
||||
ID string `validate:"required,uuid"`
|
||||
Name *string `json:"name" validate:"omitempty"`
|
||||
Email *string `json:"email" validate:"omitempty,email,unique"`
|
||||
Timezone *string `json:"timezone" validate:"omitempty"`
|
||||
ID string `json:"id" validate:"required,uuid"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
Email *string `json:"email,omitempty" validate:"omitempty,email,unique"`
|
||||
Timezone *string `json:"timezone,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
// UserUpdatePasswordRequest defines what information is required to update a user password.
|
||||
type UserUpdatePasswordRequest struct {
|
||||
ID string `validate:"required,uuid"`
|
||||
ID string `json:"id" validate:"required,uuid"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
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
|
||||
// archived users will be excluded from response.
|
||||
type UserFindRequest struct {
|
||||
Where *string `schema:"where"`
|
||||
Args []interface{} `schema:"args"`
|
||||
Order []string `schema:"order"`
|
||||
Limit *uint `schema:"limit"`
|
||||
Offset *uint `schema:"offset"`
|
||||
IncludedArchived bool `schema:"included-archived"`
|
||||
Where *string `json:"where"`
|
||||
Args []interface{} `json:"args" swaggertype:"array,string"`
|
||||
Order []string `json:"order"`
|
||||
Limit *uint `json:"limit"`
|
||||
Offset *uint `json:"offset"`
|
||||
IncludedArchived bool `json:"included-archived"`
|
||||
}
|
||||
|
||||
// Token is the payload we deliver to users when they authenticate.
|
||||
type Token struct {
|
||||
Token string `json:"token"`
|
||||
Token string `json:"token" validate:"required"`
|
||||
claims auth.Claims `json:"-"`
|
||||
}
|
||||
|
@ -17,14 +17,14 @@ import (
|
||||
// application. The status will allow users to be managed on by account with users
|
||||
// being global to the application.
|
||||
type UserAccount struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
AccountID string `json:"account_id"`
|
||||
Roles UserAccountRoles `json:"roles"`
|
||||
Status UserAccountStatus `json:"status"`
|
||||
ID string `json:"id" example:"72938896-a998-4258-a17b-6418dcdb80e3"`
|
||||
UserID string `json:"user_id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||
AccountID string `json:"account_id" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||
Roles UserAccountRoles `json:"roles" swaggertype:"array,string" enums:"admin,user" example:"admin"`
|
||||
Status UserAccountStatus `json:"status" swaggertype:"string" enums:"active,invited,disabled" example:"active"`
|
||||
CreatedAt time.Time `json:"created_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
|
||||
@ -32,45 +32,45 @@ type UserAccount struct {
|
||||
// on an account level. If a current entry exists in the database but is archived,
|
||||
// it will be un-archived.
|
||||
type CreateUserAccountRequest struct {
|
||||
UserID string `validate:"required,uuid"`
|
||||
AccountID string `validate:"required,uuid"`
|
||||
Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user"`
|
||||
Status *UserAccountStatus `json:"status" validate:"omitempty,oneof=active invited disabled"`
|
||||
UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||
Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"admin"`
|
||||
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
|
||||
// status for an existing user account.
|
||||
type UpdateUserAccountRequest struct {
|
||||
UserID string `validate:"required,uuid"`
|
||||
AccountID string `validate:"required,uuid"`
|
||||
Roles *UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user"`
|
||||
Status *UserAccountStatus `json:"status" validate:"omitempty,oneof=active invited disabled"`
|
||||
UserID string `json:"user_id" validate:"required,uuid"`
|
||||
AccountID string `json:"account_id" validate:"required,uuid"`
|
||||
Roles *UserAccountRoles `json:"roles,omitempty" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"user"`
|
||||
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.
|
||||
}
|
||||
|
||||
// ArchiveUserAccountRequest defines the information needed to remove an existing account
|
||||
// for a user. This will archive (soft-delete) the existing database entry.
|
||||
type ArchiveUserAccountRequest struct {
|
||||
UserID string `validate:"required,uuid"`
|
||||
AccountID string `validate:"required,uuid"`
|
||||
UserID string `json:"user_id" validate:"required,uuid"`
|
||||
AccountID string `json:"account_id" validate:"required,uuid"`
|
||||
}
|
||||
|
||||
// DeleteUserAccountRequest defines the information needed to delete an existing account
|
||||
// for a user. This will hard delete the existing database entry.
|
||||
type DeleteUserAccountRequest struct {
|
||||
UserID string `validate:"required,uuid"`
|
||||
AccountID string `validate:"required,uuid"`
|
||||
UserID string `json:"user_id" validate:"required,uuid"`
|
||||
AccountID string `json:"account_id" validate:"required,uuid"`
|
||||
}
|
||||
|
||||
// UserAccountFindRequest defines the possible options to search for users accounts.
|
||||
// By default archived user accounts will be excluded from response.
|
||||
type UserAccountFindRequest struct {
|
||||
Where *string `schema:"where"`
|
||||
Args []interface{} `schema:"args"`
|
||||
Order []string `schema:"order"`
|
||||
Limit *uint `schema:"limit"`
|
||||
Offset *uint `schema:"offset"`
|
||||
IncludedArchived bool `schema:"included-archived"`
|
||||
Where *string `json:"where"`
|
||||
Args []interface{} `json:"args" swaggertype:"array,string"`
|
||||
Order []string `json:"order"`
|
||||
Limit *uint `json:"limit"`
|
||||
Offset *uint `json:"offset"`
|
||||
IncludedArchived bool `json:"included-archived"`
|
||||
}
|
||||
|
||||
// UserAccountStatus represents the status of a user for an account.
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
||||
"github.com/lib/pq"
|
||||
"time"
|
||||
|
||||
"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.Roles = req.Roles
|
||||
ua.UpdatedAt = now
|
||||
ua.ArchivedAt = pq.NullTime{}
|
||||
ua.ArchivedAt = nil
|
||||
} else {
|
||||
ua = UserAccount{
|
||||
ID: uuid.NewRandom().String(),
|
||||
|
@ -352,7 +352,7 @@ func TestCreateExistingEntry(t *testing.T) {
|
||||
if err != nil || arcRes == nil {
|
||||
t.Log("\t\tGot :", err)
|
||||
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)
|
||||
}
|
||||
|
||||
@ -657,7 +657,7 @@ func TestCrud(t *testing.T) {
|
||||
Status: ua.Status,
|
||||
CreatedAt: ua.CreatedAt,
|
||||
UpdatedAt: now,
|
||||
ArchivedAt: pq.NullTime{Time: now, Valid: true},
|
||||
ArchivedAt: &pq.NullTime{Time: now, Valid: true},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(findRes, expected); diff != "" {
|
||||
|
Reference in New Issue
Block a user