You've already forked golang-saas-starter-kit
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:
@ -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
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
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)
|
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 {
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
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 (
|
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") {
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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:"-"`
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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(),
|
||||||
|
@ -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 != "" {
|
||||||
|
Reference in New Issue
Block a user