You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-08-08 22:36:41 +02:00
completed coding user package and starting unittests
This commit is contained in:
@@ -3,13 +3,13 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/schema"
|
||||
"github.com/lib/pq"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
|
||||
"github.com/gitwak/sqlxmigrate"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
_ "github.com/lib/pq"
|
||||
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
|
||||
@@ -114,17 +114,8 @@ func main() {
|
||||
// =========================================================================
|
||||
// Start Migrations
|
||||
|
||||
// Load list of Schema migrations and init new sqlxmigrate client
|
||||
migrations := migrationList(masterDb, log)
|
||||
m := sqlxmigrate.New(masterDb, sqlxmigrate.DefaultOptions, migrations)
|
||||
m.SetLogger(log)
|
||||
|
||||
// Append any schema that need to be applied if this is a fresh migration
|
||||
// ie. the migrations database table does not exist.
|
||||
m.InitSchema(initSchema(masterDb, log))
|
||||
|
||||
// Execute the migrations
|
||||
if err = m.Migrate(); err != nil {
|
||||
if err = schema.Migrate(masterDb, log); err != nil {
|
||||
log.Fatalf("main : Migrate : %v", err)
|
||||
}
|
||||
log.Printf("main : Migrate : Completed")
|
@@ -2,21 +2,24 @@ module geeks-accelerator/oss/saas-starter-kit/example-project
|
||||
|
||||
require (
|
||||
github.com/GuiaBolso/darwin v0.0.0-20170210191649-86919dfcf808 // indirect
|
||||
github.com/Masterminds/squirrel v1.1.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.19.33
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/dimfeld/httptreemux v5.0.1+incompatible
|
||||
github.com/gitwak/gondolier v0.0.0-20190521205431-504d297a6c42 // indirect
|
||||
github.com/gitwak/sqlxmigrate v0.0.0-20190525131054-1f06ba9f0748
|
||||
github.com/gitwak/sqlxmigrate v0.0.0-20190527063335-e98d5d44fc0b
|
||||
github.com/go-playground/locales v0.12.1
|
||||
github.com/go-playground/universal-translator v0.16.0
|
||||
github.com/go-redis/redis v6.15.2+incompatible
|
||||
github.com/golang/protobuf v1.3.1 // indirect
|
||||
github.com/google/go-cmp v0.2.0
|
||||
github.com/hashicorp/golang-lru v0.5.1 // indirect
|
||||
github.com/huandu/go-sqlbuilder v1.4.0
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/kelseyhightower/envconfig v1.3.0
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.1.0 // indirect
|
||||
github.com/lib/pq v1.1.1
|
||||
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/opentracing/opentracing-go v1.1.0 // indirect
|
||||
github.com/openzipkin/zipkin-go v0.1.1 // indirect
|
||||
@@ -25,14 +28,19 @@ require (
|
||||
github.com/philippgille/gokv v0.5.0 // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
github.com/tinylib/msgp v1.1.0 // indirect
|
||||
go.opencensus.io v0.14.0
|
||||
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b
|
||||
golang.org/x/sys v0.0.0-20190516110030-61b9204099cb // indirect
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect
|
||||
golang.org/x/sys v0.0.0-20190526052359-791d8a0f4d09 // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
golang.org/x/tools v0.0.0-20190525145741-7be61e1b0e51 // indirect
|
||||
google.golang.org/appengine v1.6.0 // indirect
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.14.0
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||
gopkg.in/go-playground/validator.v9 v9.28.0
|
||||
gopkg.in/go-playground/validator.v9 v9.29.0
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce
|
||||
gopkg.in/yaml.v2 v2.2.1 // indirect
|
||||
)
|
||||
|
@@ -1,11 +1,14 @@
|
||||
github.com/GuiaBolso/darwin v0.0.0-20170210191649-86919dfcf808 h1:rxDa2t7Ep7E26WMVHjl+mdLr9Un7yRSzz1CwRW6fWNY=
|
||||
github.com/GuiaBolso/darwin v0.0.0-20170210191649-86919dfcf808/go.mod h1:3sqgkckuISJ5rs1EpOp6vCvwOUKe/z9vPmyuIlq8Q/A=
|
||||
github.com/Masterminds/squirrel v1.1.0 h1:baP1qLdoQCeTw3ifCdOq2dkYc6vGcmRdaociKLbEJXs=
|
||||
github.com/Masterminds/squirrel v1.1.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA=
|
||||
github.com/aws/aws-sdk-go v1.19.32 h1:/usjSR6qsKfOKzk4tDNvZq7LqmP5+J0Cq/Uwsr2XVG8=
|
||||
github.com/aws/aws-sdk-go v1.19.32/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.19.33 h1:qz9ZQtxCUuwBKdc5QiY6hKuISYGeRQyLVA2RryDEDaQ=
|
||||
github.com/aws/aws-sdk-go v1.19.33/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dimfeld/httptreemux v5.0.1+incompatible h1:Qj3gVcDNoOthBAqftuD596rm4wg/adLLz5xh5CmpiCA=
|
||||
@@ -18,6 +21,8 @@ github.com/gitwak/sqlxmigrate v0.0.0-20190525050002-e22c656832a9 h1:se8XE/N8ZWAC
|
||||
github.com/gitwak/sqlxmigrate v0.0.0-20190525050002-e22c656832a9/go.mod h1:e7vYkZWKUHC2Vl0/dIiQRKR3z2HMuswoLf2IiQmnMoQ=
|
||||
github.com/gitwak/sqlxmigrate v0.0.0-20190525131054-1f06ba9f0748 h1:ln68Q5KHq1hCO2yxOek7ejF0ijfhRkWJqI5D5jjWF3g=
|
||||
github.com/gitwak/sqlxmigrate v0.0.0-20190525131054-1f06ba9f0748/go.mod h1:e7vYkZWKUHC2Vl0/dIiQRKR3z2HMuswoLf2IiQmnMoQ=
|
||||
github.com/gitwak/sqlxmigrate v0.0.0-20190527063335-e98d5d44fc0b h1:e1tl9Xzj+Ews1RJiO+G+udgZ5r2IGT3iyyVLe7qcChI=
|
||||
github.com/gitwak/sqlxmigrate v0.0.0-20190527063335-e98d5d44fc0b/go.mod h1:e7vYkZWKUHC2Vl0/dIiQRKR3z2HMuswoLf2IiQmnMoQ=
|
||||
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
|
||||
@@ -27,10 +32,13 @@ github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/huandu/go-sqlbuilder v1.4.0 h1:2LIlTDOz63lOETLOIiKBPEu4PUbikmS5LUc3EekwYqM=
|
||||
github.com/huandu/go-sqlbuilder v1.4.0/go.mod h1:mYfGcZTUS6yJsahUQ3imkYSkGGT3A+owd54+79kkW+U=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||
@@ -43,12 +51,18 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01 h1:EPw7R3OAyxHBCyl0oqh3lUZqS5lu3KSxzzGasE0opXQ=
|
||||
github.com/lib/pq v1.1.2-0.20190507191818-2ff3cb3adc01/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
@@ -72,6 +86,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 h1:X9XMOYjxEfAYSy3xK1DzO5dMkkWhs9E9UCcS1IERx2k=
|
||||
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
|
||||
@@ -80,13 +96,27 @@ go.opencensus.io v0.14.0 h1:1eTLxqxSIAylcKoxnNkdhvvBNZDA8JwkKNXxgyma0IA=
|
||||
go.opencensus.io v0.14.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0=
|
||||
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ=
|
||||
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190516110030-61b9204099cb h1:k07iPOt0d6nEnwXF+kHB+iEg+WSuKe/SOQuFM2QoD+E=
|
||||
golang.org/x/sys v0.0.0-20190516110030-61b9204099cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190526052359-791d8a0f4d09/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190525145741-7be61e1b0e51/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.13.1 h1:oTzOClfuudNhW9Skkp2jxjqYO92uDKXqKLbiuPA13Rk=
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.13.1/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.14.0 h1:p/8j8WV6HC+6c99FMWIPrPPs+PiXU/ShrBxHbO8S8V0=
|
||||
@@ -98,6 +128,8 @@ gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXa
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v9 v9.28.0 h1:6pzvnzx1RWaaQiAmv6e1DvCFULRaz5cKoP5j1VcrLsc=
|
||||
gopkg.in/go-playground/validator.v9 v9.28.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.0 h1:5ofssLNYgAA/inWn6rTZ4juWpRJUwEnXc1LG2IeXwgQ=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
|
@@ -22,18 +22,21 @@ const Key ctxKey = 1
|
||||
|
||||
// Claims represents the authorization claims transmitted via a JWT.
|
||||
type Claims struct {
|
||||
Roles []string `json:"roles"`
|
||||
AccountIds []string `json:"accounts"`
|
||||
Roles []string `json:"roles"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
// NewClaims constructs a Claims value for the identified user. The Claims
|
||||
// expire within a specified duration of the provided time. Additional fields
|
||||
// of the Claims can be set after calling NewClaims is desired.
|
||||
func NewClaims(subject string, roles []string, now time.Time, expires time.Duration) Claims {
|
||||
func NewClaims(userId, accountId string, accountIds []string, roles []string, now time.Time, expires time.Duration) Claims {
|
||||
c := Claims{
|
||||
Roles: roles,
|
||||
AccountIds: accountIds,
|
||||
Roles: roles,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Subject: subject,
|
||||
Subject: userId,
|
||||
Audience: accountId,
|
||||
IssuedAt: now.Unix(),
|
||||
ExpiresAt: now.Add(expires).Unix(),
|
||||
},
|
||||
|
@@ -10,13 +10,19 @@ import (
|
||||
|
||||
// Container contains the information about the container.
|
||||
type Container struct {
|
||||
ID string
|
||||
Port string
|
||||
ID string
|
||||
Port string
|
||||
User string
|
||||
Pass string
|
||||
Database string
|
||||
}
|
||||
|
||||
// StartMongo runs a mongo container to execute commands.
|
||||
func StartMongo(log *log.Logger) (*Container, error) {
|
||||
cmd := exec.Command("docker", "run", "-P", "-d", "mongo:3-jessie")
|
||||
// StartPostgres runs a postgres container to execute commands.
|
||||
func StartPostgres(log *log.Logger) (*Container, error) {
|
||||
user := "postgres"
|
||||
pass := "postgres"
|
||||
|
||||
cmd := exec.Command("docker", "run", "--env", "POSTGRES_USER="+user, "--env", "POSTGRES_PASSWORD="+pass, "-P", "-d", "postgres:11-alpine")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
if err := cmd.Run(); err != nil {
|
||||
@@ -36,9 +42,9 @@ func StartMongo(log *log.Logger) (*Container, error) {
|
||||
var doc []struct {
|
||||
NetworkSettings struct {
|
||||
Ports struct {
|
||||
TCP27017 []struct {
|
||||
TCP5432 []struct {
|
||||
HostPort string `json:"HostPort"`
|
||||
} `json:"27017/tcp"`
|
||||
} `json:"5432/tcp"`
|
||||
} `json:"Ports"`
|
||||
} `json:"NetworkSettings"`
|
||||
}
|
||||
@@ -47,8 +53,11 @@ func StartMongo(log *log.Logger) (*Container, error) {
|
||||
}
|
||||
|
||||
c := Container{
|
||||
ID: id,
|
||||
Port: doc[0].NetworkSettings.Ports.TCP27017[0].HostPort,
|
||||
ID: id,
|
||||
Port: doc[0].NetworkSettings.Ports.TCP5432[0].HostPort,
|
||||
User: user,
|
||||
Pass: pass,
|
||||
Database: "postgres",
|
||||
}
|
||||
|
||||
log.Println("DB Port:", c.Port)
|
||||
@@ -56,8 +65,8 @@ func StartMongo(log *log.Logger) (*Container, error) {
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// StopMongo stops and removes the specified container.
|
||||
func StopMongo(log *log.Logger, c *Container) error {
|
||||
// StopPostgres stops and removes the specified container.
|
||||
func StopPostgres(log *log.Logger, c *Container) error {
|
||||
if err := exec.Command("docker", "stop", c.ID).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -3,6 +3,8 @@ package tests
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/schema"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
@@ -13,7 +15,6 @@ import (
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pborman/uuid"
|
||||
)
|
||||
|
||||
// Success and failure markers.
|
||||
@@ -40,12 +41,13 @@ func New() *Test {
|
||||
|
||||
// ============================================================
|
||||
// Init AWS Session
|
||||
|
||||
awsSession := session.Must(session.NewSession())
|
||||
|
||||
// ============================================================
|
||||
// Startup Mongo container
|
||||
// Startup Postgres container
|
||||
|
||||
container, err := docker.StartMongo(log)
|
||||
container, err := docker.StartPostgres(log)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
@@ -53,18 +55,43 @@ func New() *Test {
|
||||
// ============================================================
|
||||
// Configuration
|
||||
|
||||
dbDialTimeout := 25 * time.Second
|
||||
dbHost := fmt.Sprintf("mongodb://localhost:%s/gotraining", container.Port)
|
||||
dbHost := fmt.Sprintf("postgres://%s:%s@127.0.0.1:%s/%s?timezone=UTC&sslmode=disable", container.User, container.Pass, container.Port, container.Database)
|
||||
|
||||
fmt.Println(dbHost)
|
||||
|
||||
// ============================================================
|
||||
// Start Mongo
|
||||
// Start Postgres
|
||||
|
||||
log.Println("main : Started : Initialize Postgres")
|
||||
var masterDB *sqlx.DB
|
||||
for i := 0; i <= 20; i++ {
|
||||
masterDB, err = sqlx.Open("postgres", dbHost)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Make sure the database is ready for queries.
|
||||
_, err = masterDB.Exec("SELECT 1")
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("main : Started : Initialize Mongo")
|
||||
masterDB, err := db.New(dbHost, dbDialTimeout)
|
||||
if err != nil {
|
||||
log.Fatalf("startup : Register DB : %v", err)
|
||||
}
|
||||
|
||||
// Execute the migrations
|
||||
if err = schema.Migrate(masterDB, log); err != nil {
|
||||
log.Fatalf("main : Migrate : %v", err)
|
||||
}
|
||||
log.Printf("main : Migrate : Completed")
|
||||
|
||||
return &Test{log, masterDB, container, awsSession}
|
||||
}
|
||||
|
||||
@@ -72,7 +99,7 @@ func New() *Test {
|
||||
// done in a defer immediately after calling New.
|
||||
func (t *Test) TearDown() {
|
||||
t.MasterDB.Close()
|
||||
if err := docker.StopMongo(t.Log, t.container); err != nil {
|
||||
if err := docker.StopPostgres(t.Log, t.container); err != nil {
|
||||
t.Log.Println(err)
|
||||
}
|
||||
}
|
||||
@@ -87,7 +114,7 @@ func Recover(t *testing.T) {
|
||||
// Context returns an app level context for testing.
|
||||
func Context() context.Context {
|
||||
values := web.Values{
|
||||
TraceID: uuid.New(),
|
||||
TraceID: uint64(time.Now().UnixNano()),
|
||||
Now: time.Now(),
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package schema
|
||||
|
||||
import (
|
||||
"github.com/jmoiron/sqlx"
|
146
example-project/internal/schema/migrations.go
Normal file
146
example-project/internal/schema/migrations.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
|
||||
"github.com/gitwak/sqlxmigrate"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// migrationList returns a list of migrations to be executed. If the id of the
|
||||
// migration already exists in the migrations table it will be skipped.
|
||||
func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
|
||||
return []*sqlxmigrate.Migration{
|
||||
// create table users
|
||||
{
|
||||
ID: "20190522-01a",
|
||||
Migrate: func(tx *sql.Tx) error {
|
||||
q1 := `CREATE TYPE user_status_t as enum('active','disabled')`
|
||||
if _, err := tx.Exec(q1); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q1)
|
||||
}
|
||||
|
||||
q2 := `CREATE TABLE IF NOT EXISTS users (
|
||||
id char(36) NOT NULL,
|
||||
email varchar(200) NOT NULL,
|
||||
name varchar(200) NOT NULL DEFAULT '',
|
||||
password_hash varchar(256) NOT NULL,
|
||||
password_salt varchar(36) NOT NULL,
|
||||
password_reset varchar(36) DEFAULT NULL,
|
||||
status user_status_t NOT NULL DEFAULT 'active',
|
||||
timezone varchar(128) NOT NULL DEFAULT 'America/Anchorage',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
|
||||
archived_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT email UNIQUE (email)
|
||||
) ;`
|
||||
if _, err := tx.Exec(q2); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q2)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *sql.Tx) error {
|
||||
q1 := `DROP TYPE user_status_t`
|
||||
if _, err := tx.Exec(q1); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q1)
|
||||
}
|
||||
|
||||
q2 := `DROP TABLE IF EXISTS users`
|
||||
if _, err := tx.Exec(q2); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q2)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
// create new table accounts
|
||||
{
|
||||
ID: "20190522-01b",
|
||||
Migrate: func(tx *sql.Tx) error {
|
||||
q1 := `CREATE TYPE account_status_t as enum('active','pending','disabled')`
|
||||
if _, err := tx.Exec(q1); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q1)
|
||||
}
|
||||
|
||||
q2 := `CREATE TABLE IF NOT EXISTS accounts (
|
||||
id char(36) NOT NULL,
|
||||
name varchar(255) NOT NULL,
|
||||
address1 varchar(255) NOT NULL DEFAULT '',
|
||||
address2 varchar(255) NOT NULL DEFAULT '',
|
||||
city varchar(100) NOT NULL DEFAULT '',
|
||||
region varchar(255) NOT NULL DEFAULT '',
|
||||
country varchar(255) NOT NULL DEFAULT '',
|
||||
zipcode varchar(20) NOT NULL DEFAULT '',
|
||||
status account_status_t NOT NULL DEFAULT 'active',
|
||||
timezone varchar(128) NOT NULL DEFAULT 'America/Anchorage',
|
||||
signup_user_id char(36) DEFAULT NULL,
|
||||
billing_user_id char(36) DEFAULT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
|
||||
archived_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT name UNIQUE (name)
|
||||
)`
|
||||
if _, err := tx.Exec(q2); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q2)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *sql.Tx) error {
|
||||
q1 := `DROP TYPE account_status_t`
|
||||
if _, err := tx.Exec(q1); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q1)
|
||||
}
|
||||
|
||||
q2 := `DROP TABLE IF EXISTS accounts`
|
||||
if _, err := tx.Exec(q2); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q2)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
// create new table user_accounts
|
||||
{
|
||||
ID: "20190522-01c",
|
||||
Migrate: func(tx *sql.Tx) error {
|
||||
q1 := `CREATE TYPE user_account_role_t as enum('admin', 'user')`
|
||||
if _, err := tx.Exec(q1); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q1)
|
||||
}
|
||||
|
||||
q2 := `CREATE TABLE IF NOT EXISTS users_accounts (
|
||||
id char(36) NOT NULL,
|
||||
account_id char(36) NOT NULL,
|
||||
user_id char(36) NOT NULL,
|
||||
roles user_account_role_t[] NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
|
||||
archived_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT user_account UNIQUE (user_id,account_id)
|
||||
)`
|
||||
if _, err := tx.Exec(q2); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q2)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *sql.Tx) error {
|
||||
q1 := `DROP TYPE user_account_role_t`
|
||||
if _, err := tx.Exec(q1); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q1)
|
||||
}
|
||||
|
||||
q2 := `DROP TABLE IF EXISTS users_accounts`
|
||||
if _, err := tx.Exec(q2); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q2)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
22
example-project/internal/schema/schema.go
Normal file
22
example-project/internal/schema/schema.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gitwak/sqlxmigrate"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
func Migrate(masterDb *sqlx.DB, log *log.Logger) error {
|
||||
// Load list of Schema migrations and init new sqlxmigrate client
|
||||
migrations := migrationList(masterDb, log)
|
||||
m := sqlxmigrate.New(masterDb, sqlxmigrate.DefaultOptions, migrations)
|
||||
m.SetLogger(log)
|
||||
|
||||
// Append any schema that need to be applied if this is a fresh migration
|
||||
// ie. the migrations database table does not exist.
|
||||
m.InitSchema(initSchema(masterDb, log))
|
||||
|
||||
// Execute the migrations
|
||||
return m.Migrate()
|
||||
}
|
85
example-project/internal/user/auth.go
Normal file
85
example-project/internal/user/auth.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||
)
|
||||
|
||||
// TokenGenerator is the behavior we need in our Authenticate to generate
|
||||
// tokens for authenticated users.
|
||||
type TokenGenerator interface {
|
||||
GenerateToken(auth.Claims) (string, error)
|
||||
}
|
||||
|
||||
// Authenticate finds a user by their email and verifies their password. On
|
||||
// success it returns a Token that can be used to authenticate in the future.
|
||||
func Authenticate(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator, now time.Time, email, password string) (Token, error) {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Authenticate")
|
||||
defer span.Finish()
|
||||
|
||||
// Generate sql query to select user by email address
|
||||
query := sqlbuilder.NewSelectBuilder()
|
||||
query.Where(query.Equal("email", email))
|
||||
|
||||
// Run the find, use empty claims to bypass ACLs
|
||||
res, err := find(ctx, auth.Claims{}, dbConn, query, false)
|
||||
if err != nil {
|
||||
return Token{}, err
|
||||
} else if res == nil || len(res) == 0 {
|
||||
err = errors.WithStack(ErrAuthenticationFailure)
|
||||
return Token{}, err
|
||||
}
|
||||
u := res[0]
|
||||
|
||||
// Append the salt from the user record to the supplied password.
|
||||
saltedPassword := password + string(u.PasswordHash)
|
||||
|
||||
// Compare the provided password with the saved hash. Use the bcrypt
|
||||
// comparison function so it is cryptographically secure.
|
||||
if err := bcrypt.CompareHashAndPassword(u.PasswordHash, []byte(saltedPassword)); err != nil {
|
||||
err = errors.WithStack(ErrAuthenticationFailure)
|
||||
return Token{}, err
|
||||
}
|
||||
|
||||
// Get a list of all the account ids associated with the user.
|
||||
accounts, err := FindAccountsByUserID(ctx, auth.Claims{}, dbConn, u.ID, false)
|
||||
if err != nil {
|
||||
return Token{}, err
|
||||
}
|
||||
|
||||
// Claims needs an audience, select the first account associated with
|
||||
// the user.
|
||||
var (
|
||||
accountId string
|
||||
roles []string
|
||||
)
|
||||
if len(accounts) > 0 {
|
||||
accountId = accounts[0].ID
|
||||
roles = accounts[0].Roles
|
||||
}
|
||||
|
||||
// Generate a list of all the account IDs associated with the user so
|
||||
// the use has the ability to switch between accounts.
|
||||
accountIds := []string{}
|
||||
for _, a := range accounts {
|
||||
accountIds = append(accountIds, a.ID)
|
||||
}
|
||||
|
||||
// If we are this far the request is valid. Create some claims for the user.
|
||||
claims := auth.NewClaims(u.ID, accountId, accountIds, roles, now, time.Hour)
|
||||
|
||||
// Generate a token for the user with the defined claims.
|
||||
tkn, err := tknGen.GenerateToken(claims)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(err, "generating token")
|
||||
}
|
||||
|
||||
return Token{Token: tkn}, nil
|
||||
}
|
@@ -1,45 +1,168 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"time"
|
||||
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
"github.com/lib/pq"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
// User represents someone with access to our system.
|
||||
type User struct {
|
||||
ID bson.ObjectId `bson:"_id" json:"id"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
Email string `bson:"email" json:"email"` // TODO(jlw) enforce uniqueness
|
||||
Roles []string `bson:"roles" json:"roles"`
|
||||
ID string `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Email string `db:"email" json:"email"`
|
||||
|
||||
PasswordHash []byte `bson:"password_hash" json:"-"`
|
||||
PasswordSalt string `db:"password_salt" json:"-"`
|
||||
PasswordHash []byte `db:"password_hash" json:"-"`
|
||||
PasswordReset sql.NullString `db:"password_reset" json:"-"`
|
||||
|
||||
DateModified time.Time `bson:"date_modified" json:"date_modified"`
|
||||
DateCreated time.Time `bson:"date_created,omitempty" json:"date_created"`
|
||||
Status UserStatus `db:"status" json:"status"`
|
||||
Timezone string `db:"timezone" json:"timezone"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ArchivedAt pq.NullTime `db:"archived_at" json:"archived_at"`
|
||||
}
|
||||
|
||||
// NewUser contains information needed to create a new User.
|
||||
type NewUser struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Email string `json:"email" validate:"required"` // TODO(jlw) enforce uniqueness.
|
||||
Roles []string `json:"roles" validate:"required"` // TODO(jlw) Ensure only includes valid roles.
|
||||
Password string `json:"password" validate:"required"`
|
||||
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password"`
|
||||
// CreateUserRequest contains information needed to create a new User.
|
||||
type CreateUserRequest 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"`
|
||||
Status *UserStatus `json:"status" validate:"oneof=active disabled"`
|
||||
Timezone *string `json:"timezone"`
|
||||
}
|
||||
|
||||
// UpdateUser defines what information may be provided to modify an existing
|
||||
// UpdateUserRequest defines what information may be provided to modify an existing
|
||||
// User. All fields are optional so clients can send just the fields they want
|
||||
// changed. It uses pointer fields so we can differentiate between a field that
|
||||
// was not provided and a field that was provided as explicitly blank. Normally
|
||||
// we do not want to use pointers to basic types but we make exceptions around
|
||||
// marshalling/unmarshalling.
|
||||
type UpdateUser struct {
|
||||
Name *string `json:"name"`
|
||||
Email *string `json:"email"` // TODO(jlw) enforce uniqueness.
|
||||
Roles []string `json:"roles"` // TODO(jlw) Ensure only includes valid roles.
|
||||
Password *string `json:"password"`
|
||||
PasswordConfirm *string `json:"password_confirm" validate:"omitempty,eqfield=Password"`
|
||||
type UpdateUserRequest struct {
|
||||
ID string `validate:"required,uuid"`
|
||||
Name *string `json:"name"`
|
||||
Email *string `json:"email" validate:"email,unique"`
|
||||
Status *UserStatus `json:"status" validate:"oneof=active disabled"`
|
||||
Timezone *string `json:"timezone"`
|
||||
}
|
||||
|
||||
// UpdatePassword defines what information may be provided to update user password.
|
||||
type UpdatePasswordRequest struct {
|
||||
ID string `validate:"required,uuid"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
PasswordConfirm string `json:"password_confirm" validate:"omitempty,eqfield=Password"`
|
||||
}
|
||||
|
||||
// UserFindRequest defines the possible options for search for users
|
||||
type UserFindRequest struct {
|
||||
Where *string
|
||||
Args []interface{}
|
||||
Order []string
|
||||
Limit *uint
|
||||
Offset *uint
|
||||
IncludedArchived bool
|
||||
}
|
||||
|
||||
// UserAccount defines the one to many relationship of an user to an account.
|
||||
// Each association of an user to an account has a set of roles defined for the user
|
||||
// that will be applied when accessing the account.
|
||||
type UserAccount struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
UserID string `db:"user_id" json:"user_id"`
|
||||
AccountID string `db:"account_id" json:"account_id"`
|
||||
Roles []string `db:"roles" json:"roles"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ArchivedAt pq.NullTime `db:"archived_at" json:"archived_at"`
|
||||
}
|
||||
|
||||
// AddAccountRequest defines the information needed to add a new account to a user.
|
||||
type AddAccountRequest struct {
|
||||
UserID string `validate:"required,uuid"`
|
||||
AccountID string `validate:"required,uuid"`
|
||||
Roles []string `json:"roles" validate:"oneof=admin user"`
|
||||
}
|
||||
|
||||
// UpdateAccountRequest defines the information needed to update the roles for
|
||||
// an existing user account.
|
||||
type UpdateAccountRequest struct {
|
||||
UserID string `validate:"required,uuid"`
|
||||
AccountID string `validate:"required,uuid"`
|
||||
Roles []string `json:"roles" validate:"oneof=admin user"`
|
||||
unArchive bool
|
||||
}
|
||||
|
||||
// RemoveAccountRequest defines the information needed to remove an existing
|
||||
// account for a user. This will archive (soft-delete) the existing database entry.
|
||||
type RemoveAccountRequest struct {
|
||||
UserID string `validate:"required,uuid"`
|
||||
AccountID string `validate:"required,uuid"`
|
||||
}
|
||||
|
||||
// DeleteAccountRequest defines the information needed to delete an existing
|
||||
// account for a user. This will hard delete the existing database entry.
|
||||
type DeleteAccountRequest struct {
|
||||
UserID string `validate:"required,uuid"`
|
||||
AccountID string `validate:"required,uuid"`
|
||||
}
|
||||
|
||||
// UserAccountFindRequest defines the possible options for search for users accounts
|
||||
type UserAccountFindRequest struct {
|
||||
Where *string
|
||||
Args []interface{}
|
||||
Order []string
|
||||
Limit *uint
|
||||
Offset *uint
|
||||
IncludedArchived bool
|
||||
}
|
||||
|
||||
// UserStatus values
|
||||
const (
|
||||
UserStatus_Active UserStatus = "active"
|
||||
UserStatus_Disabled UserStatus = "disabled"
|
||||
)
|
||||
|
||||
// UserStatus_Values provides list of valid UserStatus values
|
||||
var UserStatus_Values = []UserStatus{
|
||||
UserStatus_Active,
|
||||
UserStatus_Disabled,
|
||||
}
|
||||
|
||||
// UserStatus represents the status of a user.
|
||||
type UserStatus string
|
||||
|
||||
// Scan supports reading the UserStatus value from the database.
|
||||
func (s *UserStatus) Scan(value interface{}) error {
|
||||
asBytes, ok := value.([]byte)
|
||||
if !ok {
|
||||
return errors.New("Scan source is not []byte")
|
||||
}
|
||||
*s = UserStatus(string(asBytes))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value converts the UserStatus value to be stored in the database.
|
||||
func (s UserStatus) Value() (driver.Value, error) {
|
||||
v := validator.New()
|
||||
|
||||
errs := v.Var(s, "required,oneof=active disabled")
|
||||
if errs != nil {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
// validation would go here
|
||||
return string(s), nil
|
||||
}
|
||||
|
||||
// String converts the UserStatus value to a string.
|
||||
func (s UserStatus) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// Token is the payload we deliver to users when they authenticate.
|
||||
|
@@ -2,19 +2,21 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||
mgo "gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
const usersCollection = "users"
|
||||
// The database table for User
|
||||
const usersTableName = "users"
|
||||
|
||||
var (
|
||||
// ErrNotFound abstracts the mgo not found error.
|
||||
@@ -31,113 +33,463 @@ var (
|
||||
ErrForbidden = errors.New("Attempted action is not allowed")
|
||||
)
|
||||
|
||||
// List retrieves a list of existing users from the database.
|
||||
func List(ctx context.Context, dbConn *sqlx.DB) ([]User, error) {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.List")
|
||||
defer span.Finish()
|
||||
// usersMapColumns is the list of columns needed for mapRowsToUser
|
||||
var usersMapColumns = "id,name,email,password_salt,password_hash,password_reset,status,timezone,created_at,updated_at,archived_at"
|
||||
|
||||
u := []User{}
|
||||
|
||||
f := func(collection *mgo.Collection) error {
|
||||
return collection.Find(nil).All(&u)
|
||||
}
|
||||
if _, err := dbConn.ExecContext(ctx, usersCollection, f); err != nil {
|
||||
return nil, errors.Wrap(err, "db.users.find()")
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Retrieve gets the specified user from the database.
|
||||
func Retrieve(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string) (*User, error) {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Retrieve")
|
||||
defer span.Finish()
|
||||
|
||||
if !bson.IsObjectIdHex(id) {
|
||||
return nil, ErrInvalidID
|
||||
}
|
||||
|
||||
// If you are not an admin and looking to retrieve someone else then you are rejected.
|
||||
if !claims.HasRole(auth.RoleAdmin) && claims.Subject != id {
|
||||
return nil, ErrForbidden
|
||||
}
|
||||
|
||||
q := bson.M{"_id": bson.ObjectIdHex(id)}
|
||||
|
||||
var u *User
|
||||
f := func(collection *mgo.Collection) error {
|
||||
return collection.Find(q).One(&u)
|
||||
}
|
||||
if _, err := dbConn.ExecContext(ctx, usersCollection, f); err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("db.users.find(%s)", q))
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Create inserts a new user into the database.
|
||||
func Create(ctx context.Context, dbConn *sqlx.DB, nu *NewUser, now time.Time) (*User, error) {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Create")
|
||||
defer span.Finish()
|
||||
|
||||
// Mongo truncates times to milliseconds when storing. We and do the same
|
||||
// here so the value we return is consistent with what we store.
|
||||
now = now.Truncate(time.Millisecond)
|
||||
|
||||
pw, err := bcrypt.GenerateFromPassword([]byte(nu.Password), bcrypt.DefaultCost)
|
||||
// mapRowsToUser takes the SQL rows and maps it to the UserAccount struct
|
||||
// with the columns defined by usersMapColumns
|
||||
func mapRowsToUser(rows *sql.Rows) (*User, error) {
|
||||
var (
|
||||
u User
|
||||
err error
|
||||
)
|
||||
err = rows.Scan(&u.ID, &u.Email, &u.PasswordSalt, &u.PasswordHash, &u.PasswordReset, &u.Status, &u.Timezone, &u.CreatedAt, &u.UpdatedAt, &u.ArchivedAt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "generating password hash")
|
||||
}
|
||||
|
||||
u := User{
|
||||
ID: bson.NewObjectId(),
|
||||
Name: nu.Name,
|
||||
Email: nu.Email,
|
||||
PasswordHash: pw,
|
||||
Roles: nu.Roles,
|
||||
DateCreated: now,
|
||||
DateModified: now,
|
||||
}
|
||||
|
||||
f := func(collection *mgo.Collection) error {
|
||||
return collection.Insert(&u)
|
||||
}
|
||||
if _, err := dbConn.ExecContext(ctx, usersCollection, f); err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("db.users.insert(%s)", &u))
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
// Update replaces a user document in the database.
|
||||
func Update(ctx context.Context, dbConn *sqlx.DB, id string, upd *UpdateUser, now time.Time) error {
|
||||
// CanReadUserId determines if claims has the authority to access the specified user ID.
|
||||
func CanReadUserId(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID string) error {
|
||||
// If the request has claims from a specific user, ensure that the user
|
||||
// has the correct access to the user.
|
||||
if claims.Subject != "" {
|
||||
// When the claims Subject - UserId - does not match the requested user, the
|
||||
// claims audience - AccountId - should have a record.
|
||||
if claims.Subject != userID {
|
||||
|
||||
query := sqlbuilder.NewSelectBuilder().Select("id").From(usersAccountsTableName)
|
||||
query.Where(query.And(
|
||||
query.Equal("account_id", claims.Audience),
|
||||
query.Equal("user_id", userID),
|
||||
))
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
|
||||
var userAccountId string
|
||||
err := dbConn.QueryRowContext(ctx, sql, args...).Scan(&userAccountId)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
return err
|
||||
}
|
||||
|
||||
// When there is now userAccount ID returned, then the current user does not have access
|
||||
// to the specified user.
|
||||
if userAccountId == "" {
|
||||
return errors.WithStack(ErrForbidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanModifyUserId determines if claims has the authority to modify the specified user ID.
|
||||
func CanModifyUserId(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID string) error {
|
||||
// First check to see if claims can read the user ID
|
||||
err := CanReadUserId(ctx, claims, dbConn, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the request has claims from a specific user, ensure that the user
|
||||
// has the correct role for updating an existing user.
|
||||
if claims.Subject != "" {
|
||||
if claims.Subject == userID {
|
||||
// All users are allowed to update their own record
|
||||
} else if claims.HasRole(auth.RoleAdmin) {
|
||||
// Admin users can update users they have access to.
|
||||
} else {
|
||||
return errors.WithStack(ErrForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// claimsSql applies a sub-query to the provided query to enforce ACL based on
|
||||
// the claims provided.
|
||||
// 1. All role types can access their user ID
|
||||
// 2. Any user with the same account ID
|
||||
// 3. No claims, request is internal, no ACL applied
|
||||
func applyClaimsUserSelect(ctx context.Context, claims auth.Claims, query *sqlbuilder.SelectBuilder) error {
|
||||
// Claims are empty, don't apply any ACL
|
||||
if claims.Audience == "" && claims.Subject == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build select statement for users_accounts table
|
||||
subQuery := sqlbuilder.NewSelectBuilder().Select("user_id").From(usersAccountsTableName)
|
||||
|
||||
var or []string
|
||||
if claims.Audience != "" {
|
||||
or = append(or, subQuery.Equal("account_id", claims.Audience))
|
||||
}
|
||||
if claims.Subject != "" {
|
||||
or = append(or, subQuery.Equal("user_id", claims.Subject))
|
||||
}
|
||||
subQuery.Where(or...)
|
||||
|
||||
// Append sub query
|
||||
query.Where(query.In("id", subQuery))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectQuery constructs a base select query for User
|
||||
func selectQuery() *sqlbuilder.SelectBuilder {
|
||||
query := sqlbuilder.NewSelectBuilder()
|
||||
query.Select(usersMapColumns)
|
||||
query.From(usersTableName)
|
||||
return query
|
||||
}
|
||||
|
||||
// userFindRequestQuery generates the select query for the given find request.
|
||||
func userFindRequestQuery(req UserFindRequest) *sqlbuilder.SelectBuilder {
|
||||
query := selectQuery()
|
||||
if req.Where != nil {
|
||||
query.Where(*req.Where)
|
||||
}
|
||||
if len(req.Order) > 0 {
|
||||
query.OrderBy(req.Order...)
|
||||
}
|
||||
if req.Limit != nil {
|
||||
query.Limit(int(*req.Limit))
|
||||
}
|
||||
if req.Offset != nil {
|
||||
query.Offset(int(*req.Offset))
|
||||
}
|
||||
|
||||
b := sqlbuilder.Buildf(query.String(), req.Args...)
|
||||
query.BuilderAs(b, usersTableName)
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// List enables streaming retrieval of Users from the database. The query results
|
||||
// will be written to the interface{} resultReceiver channel enabling processing the results while
|
||||
// they're still being fetched. After all pages have been processed the channel is closed
|
||||
// Possible types sent to the channel are limited to:
|
||||
// - error
|
||||
// - User
|
||||
//
|
||||
// rr := make(chan interface{})
|
||||
//
|
||||
// go List(ctx, claims, db, rr)
|
||||
//
|
||||
// for r := range rr {
|
||||
// switch v := r.(type) {
|
||||
// case User:
|
||||
// // v is of type User
|
||||
// // process the user here
|
||||
// case error:
|
||||
// // v is of type error
|
||||
// // handle the error here
|
||||
// }
|
||||
// }
|
||||
func List(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserFindRequest, results chan<- interface{}) {
|
||||
query := userFindRequestQuery(req)
|
||||
list(ctx, claims, dbConn, query, req.IncludedArchived, results)
|
||||
}
|
||||
|
||||
// List enables streaming retrieval of Users from the database for the supplied query.
|
||||
func list(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbuilder.SelectBuilder, includedArchived bool, results chan<- interface{}) {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.List")
|
||||
defer span.Finish()
|
||||
|
||||
// Close the channel on complete
|
||||
defer close(results)
|
||||
|
||||
query.Select(usersMapColumns)
|
||||
query.From(usersTableName)
|
||||
|
||||
if !includedArchived {
|
||||
query.Where(query.IsNull("archived_at"))
|
||||
}
|
||||
|
||||
// Check to see if a sub query needs to be applied for the claims
|
||||
err := applyClaimsUserSelect(ctx, claims, query)
|
||||
if err != nil {
|
||||
results <- err
|
||||
return
|
||||
}
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
|
||||
// fetch all places from the db
|
||||
rows, err := dbConn.QueryContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
results <- errors.WithMessage(err, "list users failed")
|
||||
return
|
||||
}
|
||||
|
||||
// iterate over each row
|
||||
for rows.Next() {
|
||||
u, err := mapRowsToUser(rows)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
results <- err
|
||||
return
|
||||
}
|
||||
results <- u
|
||||
}
|
||||
}
|
||||
|
||||
// Find gets all the users from the database based on the request params
|
||||
func Find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserFindRequest) ([]*User, error) {
|
||||
query := userFindRequestQuery(req)
|
||||
return find(ctx, claims, dbConn, query, req.IncludedArchived)
|
||||
}
|
||||
|
||||
// find gets all the users from the database based on the query
|
||||
func find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbuilder.SelectBuilder, includedArchived bool) ([]*User, error) {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Find")
|
||||
defer span.Finish()
|
||||
|
||||
query.Select(usersMapColumns)
|
||||
query.From(usersTableName)
|
||||
|
||||
if !includedArchived {
|
||||
query.Where(query.IsNull("archived_at"))
|
||||
}
|
||||
|
||||
// Check to see if a sub query needs to be applied for the claims
|
||||
err := applyClaimsUserSelect(ctx, claims, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
|
||||
// fetch all places from the db
|
||||
rows, err := dbConn.QueryContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessage(err, "find users failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// iterate over each row
|
||||
resp := []*User{}
|
||||
for rows.Next() {
|
||||
u, err := mapRowsToUser(rows)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
return nil, err
|
||||
}
|
||||
resp = append(resp, u)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Retrieve gets the specified user from the database.
|
||||
func FindById(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, includedArchived bool) (*User, error) {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.FindById")
|
||||
defer span.Finish()
|
||||
|
||||
// Filter base select query by ID
|
||||
query := selectQuery()
|
||||
query.Where(query.Equal("id", id))
|
||||
|
||||
res, err := find(ctx, claims, dbConn, query, includedArchived)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if res == nil || len(res) == 0 {
|
||||
err = errors.WithMessagef(ErrNotFound, "user %s not found", id)
|
||||
return nil, err
|
||||
}
|
||||
u := res[0]
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Validation an email address is unique excluding the current user ID.
|
||||
func uniqueEmail(ctx context.Context, dbConn *sqlx.DB, email, userId string) (bool, error) {
|
||||
|
||||
query := sqlbuilder.NewSelectBuilder().Select("id").From(usersTableName)
|
||||
query.Where(query.And(
|
||||
query.Equal("email", email),
|
||||
query.NotEqual("id", userId),
|
||||
))
|
||||
queryStr, args := query.Build()
|
||||
queryStr = dbConn.Rebind(queryStr)
|
||||
|
||||
var existingId string
|
||||
err := dbConn.QueryRowContext(ctx, queryStr, args...).Scan(&existingId)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
return false, err
|
||||
}
|
||||
|
||||
// When an ID was found in the db, the email is not unique.
|
||||
if existingId != "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Create inserts a new user into the database.
|
||||
func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req CreateUserRequest, now time.Time) (*User, error) {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Create")
|
||||
defer span.Finish()
|
||||
|
||||
v := validator.New()
|
||||
|
||||
// Validation email address is unique in the database.
|
||||
uniq, err := uniqueEmail(ctx, dbConn, req.Email, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := func(fl validator.FieldLevel) bool {
|
||||
if fl.Field().String() == "invalid" {
|
||||
return false
|
||||
}
|
||||
return uniq
|
||||
}
|
||||
v.RegisterValidation("unique", f)
|
||||
|
||||
// Validate the request.
|
||||
err = v.Struct(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the request has claims from a specific user, ensure that the user
|
||||
// has the correct role for creating a new user.
|
||||
if claims.Subject != "" {
|
||||
// Users with the role of admin are ony allows to create users.
|
||||
if !claims.HasRole(auth.RoleAdmin) {
|
||||
err = errors.WithStack(ErrForbidden)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If now empty set it to the current time.
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
// Always store the time as UTC.
|
||||
now = now.UTC()
|
||||
|
||||
// Postgres truncates times to milliseconds when storing. We and do the same
|
||||
// here so the value we return is consistent with what we store.
|
||||
now = now.Truncate(time.Millisecond)
|
||||
|
||||
passwordSalt := uuid.NewRandom().String()
|
||||
saltedPassword := req.Password + passwordSalt
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(saltedPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "generating password hash")
|
||||
}
|
||||
|
||||
u := User{
|
||||
ID: uuid.NewRandom().String(),
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
PasswordHash: passwordHash,
|
||||
PasswordSalt: passwordSalt,
|
||||
Status: UserStatus_Active,
|
||||
Timezone: "America/Anchorage",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if req.Status != nil {
|
||||
u.Status = *req.Status
|
||||
}
|
||||
if req.Timezone != nil {
|
||||
u.Timezone = *req.Timezone
|
||||
}
|
||||
|
||||
// Build the insert SQL statement.
|
||||
query := sqlbuilder.NewInsertBuilder()
|
||||
query.InsertInto(usersTableName)
|
||||
query.Cols("id", "name", "email", "password_hash", "password_salt", "status", "timezone", "created_at", "updated_at")
|
||||
query.Values(u.ID, u.Name, u.Email, u.PasswordHash, u.PasswordSalt, u.Status.String(), u.Timezone, u.CreatedAt, u.UpdatedAt)
|
||||
|
||||
// Execute the query with the provided context.
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
_, err = dbConn.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessage(err, "create user failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
// Update replaces a user in the database.
|
||||
func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UpdateUserRequest, now time.Time) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Update")
|
||||
defer span.Finish()
|
||||
|
||||
if !bson.IsObjectIdHex(id) {
|
||||
return ErrInvalidID
|
||||
}
|
||||
v := validator.New()
|
||||
|
||||
fields := make(bson.M)
|
||||
|
||||
if upd.Name != nil {
|
||||
fields["name"] = *upd.Name
|
||||
}
|
||||
if upd.Email != nil {
|
||||
fields["email"] = *upd.Email
|
||||
}
|
||||
if upd.Roles != nil {
|
||||
fields["roles"] = upd.Roles
|
||||
}
|
||||
if upd.Password != nil {
|
||||
pw, err := bcrypt.GenerateFromPassword([]byte(*upd.Password), bcrypt.DefaultCost)
|
||||
// Validation email address is unique in the database.
|
||||
if req.Email != nil {
|
||||
uniq, err := uniqueEmail(ctx, dbConn, *req.Email, req.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "generating password hash")
|
||||
return err
|
||||
}
|
||||
fields["password_hash"] = pw
|
||||
f := func(fl validator.FieldLevel) bool {
|
||||
if fl.Field().String() == "invalid" {
|
||||
return false
|
||||
}
|
||||
return uniq
|
||||
}
|
||||
v.RegisterValidation("unique", f)
|
||||
}
|
||||
|
||||
// Validate the request.
|
||||
err := v.Struct(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the claims can modify the user specified in the request.
|
||||
err = CanModifyUserId(ctx, claims, dbConn, req.ID)
|
||||
if err != nil {
|
||||
err = errors.WithMessagef(err, "Update %s failed", usersTableName)
|
||||
return err
|
||||
}
|
||||
|
||||
// If now empty set it to the current time.
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
// Always store the time as UTC.
|
||||
now = now.UTC()
|
||||
|
||||
// Postgres truncates times to milliseconds when storing. We and do the same
|
||||
// here so the value we return is consistent with what we store.
|
||||
now = now.Truncate(time.Millisecond)
|
||||
|
||||
// Build the update SQL statement.
|
||||
query := sqlbuilder.NewUpdateBuilder()
|
||||
query.Update(usersTableName)
|
||||
|
||||
fields := []string{}
|
||||
if req.Name != nil {
|
||||
fields = append(fields, query.Assign("name", req.Name))
|
||||
}
|
||||
if req.Email != nil {
|
||||
fields = append(fields, query.Assign("email", req.Email))
|
||||
}
|
||||
if req.Status != nil {
|
||||
fields = append(fields, query.Assign("status", req.Status))
|
||||
}
|
||||
if req.Timezone != nil {
|
||||
fields = append(fields, query.Assign("timezone", req.Timezone))
|
||||
}
|
||||
|
||||
// If there's nothing to update we can quit early.
|
||||
@@ -145,90 +497,221 @@ func Update(ctx context.Context, dbConn *sqlx.DB, id string, upd *UpdateUser, no
|
||||
return nil
|
||||
}
|
||||
|
||||
fields["date_modified"] = now
|
||||
// Append the updated_at field
|
||||
fields = append(fields, query.Assign("updated_at", now))
|
||||
|
||||
m := bson.M{"$set": fields}
|
||||
q := bson.M{"_id": bson.ObjectIdHex(id)}
|
||||
query.Set(fields...)
|
||||
query.Where(query.Equal("id", req.ID))
|
||||
|
||||
f := func(collection *mgo.Collection) error {
|
||||
return collection.Update(q, m)
|
||||
// Execute the query with the provided context.
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
_, err = dbConn.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessagef(err, "update user %s failed", req.ID)
|
||||
return err
|
||||
}
|
||||
if _, err := dbConn.ExecContext(ctx, usersCollection, f); err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return ErrNotFound
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update replaces a user in the database.
|
||||
func UpdatePassword(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UpdatePasswordRequest, now time.Time) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Update")
|
||||
defer span.Finish()
|
||||
|
||||
// Validate the request.
|
||||
err := validator.New().Struct(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the claims can modify the user specified in the request.
|
||||
err = CanModifyUserId(ctx, claims, dbConn, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If now empty set it to the current time.
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
// Always store the time as UTC.
|
||||
now = now.UTC()
|
||||
|
||||
// Postgres truncates times to milliseconds when storing. We and do the same
|
||||
// here so the value we return is consistent with what we store.
|
||||
now = now.Truncate(time.Millisecond)
|
||||
|
||||
// Generate new password hash for the provided password.
|
||||
passwordSalt := uuid.NewRandom()
|
||||
saltedPassword := req.Password + passwordSalt.String()
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(saltedPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "generating password hash")
|
||||
}
|
||||
|
||||
// Build the update SQL statement.
|
||||
query := sqlbuilder.NewUpdateBuilder()
|
||||
query.Update(usersTableName)
|
||||
query.Set(
|
||||
query.Assign("password_hash", passwordHash),
|
||||
query.Assign("password_salt", passwordSalt),
|
||||
query.Assign("updated_at", now),
|
||||
)
|
||||
query.Where(query.Equal("id", req.ID))
|
||||
|
||||
// Execute the query with the provided context.
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
_, err = dbConn.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessagef(err, "update password for user %s failed", req.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Archive soft deleted the user from the database.
|
||||
func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID string, now time.Time) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Archive")
|
||||
defer span.Finish()
|
||||
|
||||
// Defines the struct to apply validation
|
||||
req := struct {
|
||||
ID string `validate:"required,uuid"`
|
||||
}{
|
||||
ID: userID,
|
||||
}
|
||||
|
||||
// Validate the request.
|
||||
err := validator.New().Struct(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the claims can modify the user specified in the request.
|
||||
err = CanModifyUserId(ctx, claims, dbConn, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If now empty set it to the current time.
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
// Always store the time as UTC.
|
||||
now = now.UTC()
|
||||
|
||||
// Postgres truncates times to milliseconds when storing. We and do the same
|
||||
// here so the value we return is consistent with what we store.
|
||||
now = now.Truncate(time.Millisecond)
|
||||
|
||||
// Build the update SQL statement.
|
||||
query := sqlbuilder.NewUpdateBuilder()
|
||||
query.Update(usersTableName)
|
||||
query.Set(
|
||||
query.Assign("archived_at", now),
|
||||
)
|
||||
query.Where(query.Equal("id", req.ID))
|
||||
|
||||
// Execute the query with the provided context.
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
_, err = dbConn.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessagef(err, "archive user %s failed", req.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// Archive all the associated user accounts
|
||||
{
|
||||
// Build the update SQL statement.
|
||||
query := sqlbuilder.NewUpdateBuilder()
|
||||
query.Update(usersAccountsTableName)
|
||||
query.Set(query.Assign("archived_at", now))
|
||||
query.Where(query.And(
|
||||
query.Equal("user_id", req.ID),
|
||||
))
|
||||
|
||||
// Execute the query with the provided context.
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
_, err = dbConn.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessagef(err, "archive accounts for user %s failed", req.ID)
|
||||
return err
|
||||
}
|
||||
return errors.Wrap(err, fmt.Sprintf("db.customers.update(%s, %s)", q, m))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a user from the database.
|
||||
func Delete(ctx context.Context, dbConn *sqlx.DB, id string) error {
|
||||
func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID string) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Delete")
|
||||
defer span.Finish()
|
||||
|
||||
if !bson.IsObjectIdHex(id) {
|
||||
return ErrInvalidID
|
||||
// Defines the struct to apply validation
|
||||
req := struct {
|
||||
ID string `validate:"required,uuid"`
|
||||
}{
|
||||
ID: userID,
|
||||
}
|
||||
|
||||
q := bson.M{"_id": bson.ObjectIdHex(id)}
|
||||
|
||||
f := func(collection *mgo.Collection) error {
|
||||
return collection.Remove(q)
|
||||
// Validate the request.
|
||||
err := validator.New().Struct(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := dbConn.ExecContext(ctx, usersCollection, f); err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return ErrNotFound
|
||||
|
||||
// Ensure the claims can modify the user specified in the request.
|
||||
err = CanModifyUserId(ctx, claims, dbConn, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Build the delete SQL statement.
|
||||
query := sqlbuilder.NewDeleteBuilder()
|
||||
query.DeleteFrom(usersTableName)
|
||||
query.Where(query.Equal("id", req.ID))
|
||||
|
||||
// Execute the query with the provided context.
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
_, err = dbConn.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessagef(err, "delete user %s failed", req.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete all the associated user accounts
|
||||
{
|
||||
// Build the delete SQL statement.
|
||||
query := sqlbuilder.NewDeleteBuilder()
|
||||
query.DeleteFrom(usersAccountsTableName)
|
||||
query.Where(query.And(
|
||||
query.Equal("user_id", req.ID),
|
||||
))
|
||||
|
||||
// Execute the query with the provided context.
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
_, err = dbConn.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessagef(err, "delete accounts for user %s failed", req.ID)
|
||||
return err
|
||||
}
|
||||
return errors.Wrap(err, fmt.Sprintf("db.users.remove(%s)", q))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TokenGenerator is the behavior we need in our Authenticate to generate
|
||||
// tokens for authenticated users.
|
||||
type TokenGenerator interface {
|
||||
GenerateToken(auth.Claims) (string, error)
|
||||
}
|
||||
|
||||
// Authenticate finds a user by their email and verifies their password. On
|
||||
// success it returns a Token that can be used to authenticate in the future.
|
||||
func Authenticate(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator, now time.Time, email, password string) (Token, error) {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Authenticate")
|
||||
defer span.Finish()
|
||||
|
||||
q := bson.M{"email": email}
|
||||
|
||||
var u *User
|
||||
f := func(collection *mgo.Collection) error {
|
||||
return collection.Find(q).One(&u)
|
||||
}
|
||||
if _, err := dbConn.ExecContext(ctx, usersCollection, f); err != nil {
|
||||
|
||||
// Normally we would return ErrNotFound in this scenario but we do not want
|
||||
// to leak to an unauthenticated user which emails are in the system.
|
||||
if err == mgo.ErrNotFound {
|
||||
return Token{}, ErrAuthenticationFailure
|
||||
}
|
||||
return Token{}, errors.Wrap(err, fmt.Sprintf("db.users.find(%s)", q))
|
||||
}
|
||||
|
||||
// Compare the provided password with the saved hash. Use the bcrypt
|
||||
// comparison function so it is cryptographically secure.
|
||||
if err := bcrypt.CompareHashAndPassword(u.PasswordHash, []byte(password)); err != nil {
|
||||
return Token{}, ErrAuthenticationFailure
|
||||
}
|
||||
|
||||
// If we are this far the request is valid. Create some claims for the user
|
||||
// and generate their token.
|
||||
claims := auth.NewClaims(u.ID.Hex(), u.Roles, now, time.Hour)
|
||||
|
||||
tkn, err := tknGen.GenerateToken(claims)
|
||||
if err != nil {
|
||||
return Token{}, errors.Wrap(err, "generating token")
|
||||
}
|
||||
|
||||
return Token{Token: tkn}, nil
|
||||
}
|
||||
|
388
example-project/internal/user/user_account.go
Normal file
388
example-project/internal/user/user_account.go
Normal file
@@ -0,0 +1,388 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||
"gopkg.in/go-playground/validator.v9"
|
||||
)
|
||||
|
||||
// The database table for UserAccount
|
||||
const usersAccountsTableName = "users_accounts"
|
||||
|
||||
// The list of columns needed for mapRowsToUserAccount
|
||||
var usersAccountsMapColumns = "id,user_id,account_id,roles,created_at,updated_at,archived_at"
|
||||
|
||||
// mapRowsToUserAccount takes the SQL rows and maps it to the UserAccount struct
|
||||
// with the columns defined by usersAccountsMapColumns
|
||||
func mapRowsToUserAccount(rows *sql.Rows) (*UserAccount, error) {
|
||||
var (
|
||||
ua UserAccount
|
||||
err error
|
||||
)
|
||||
err = rows.Scan(&ua.ID, &ua.UserID, &ua.AccountID, &ua.Roles, &ua.CreatedAt, &ua.UpdatedAt, &ua.ArchivedAt)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &ua, nil
|
||||
}
|
||||
|
||||
// applyClaimsUserAccountSelect applies a sub query to enforce ACL for
|
||||
// the supplied claims. If claims is empty then request must be internal and
|
||||
// no sub-query is applied. Else a list of user IDs is found all associated
|
||||
// user accounts.
|
||||
func applyClaimsUserAccountSelect(ctx context.Context, claims auth.Claims, query *sqlbuilder.SelectBuilder) error {
|
||||
if claims.Audience == "" && claims.Subject == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build select statement for users_accounts table
|
||||
subQuery := sqlbuilder.NewSelectBuilder().Select("user_id").From(usersAccountsTableName)
|
||||
|
||||
var or []string
|
||||
if claims.Audience != "" {
|
||||
or = append(or, subQuery.Equal("account_id", claims.Audience))
|
||||
}
|
||||
if claims.Subject != "" {
|
||||
or = append(or, subQuery.Equal("user_id", claims.Subject))
|
||||
}
|
||||
subQuery.Where(or...)
|
||||
|
||||
// Append sub query
|
||||
query.Where(query.In("user_id", subQuery))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AccountSelectQuery
|
||||
func accountSelectQuery() *sqlbuilder.SelectBuilder {
|
||||
query := sqlbuilder.NewSelectBuilder()
|
||||
query.Select(usersAccountsMapColumns)
|
||||
query.From(usersAccountsTableName)
|
||||
return query
|
||||
}
|
||||
|
||||
// userFindRequestQuery generates the select query for the given find request.
|
||||
func accountFindRequestQuery(req UserAccountFindRequest) *sqlbuilder.SelectBuilder {
|
||||
query := accountSelectQuery()
|
||||
if req.Where != nil {
|
||||
query.Where(*req.Where)
|
||||
}
|
||||
if len(req.Order) > 0 {
|
||||
query.OrderBy(req.Order...)
|
||||
}
|
||||
if req.Limit != nil {
|
||||
query.Limit(int(*req.Limit))
|
||||
}
|
||||
if req.Offset != nil {
|
||||
query.Limit(int(*req.Offset))
|
||||
}
|
||||
|
||||
b := sqlbuilder.Buildf(query.String(), req.Args...)
|
||||
query.BuilderAs(b, usersAccountsMapColumns)
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// Find gets all the users from the database based on the request params
|
||||
func FindAccounts(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserAccountFindRequest) ([]*UserAccount, error) {
|
||||
query := accountFindRequestQuery(req)
|
||||
return findAccounts(ctx, claims, dbConn, query, req.IncludedArchived)
|
||||
}
|
||||
|
||||
// Find gets all the users from the database based on the select query
|
||||
func findAccounts(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbuilder.SelectBuilder, includedArchived bool) ([]*UserAccount, error) {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.FindAccounts")
|
||||
defer span.Finish()
|
||||
|
||||
query.Select(usersAccountsMapColumns)
|
||||
query.From(usersAccountsTableName)
|
||||
|
||||
if !includedArchived {
|
||||
query.Where(query.IsNull("archived_at"))
|
||||
}
|
||||
|
||||
// Check to see if a sub query needs to be applied for the claims
|
||||
err := applyClaimsUserAccountSelect(ctx, claims, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
|
||||
// fetch all places from the db
|
||||
rows, err := dbConn.QueryContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessage(err, "find accounts failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// iterate over each row
|
||||
resp := []*UserAccount{}
|
||||
for rows.Next() {
|
||||
ua, err := mapRowsToUserAccount(rows)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
return nil, err
|
||||
}
|
||||
resp = append(resp, ua)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Retrieve gets the specified user from the database.
|
||||
func FindAccountsByUserID(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID string, includedArchived bool) ([]*UserAccount, error) {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.FindAccountsByUserId")
|
||||
defer span.Finish()
|
||||
|
||||
// Filter base select query by ID
|
||||
query := sqlbuilder.NewSelectBuilder()
|
||||
query.Where(query.Equal("user_id", userID))
|
||||
query.OrderBy("id")
|
||||
|
||||
// Execute the find accounts method.
|
||||
res, err := findAccounts(ctx, claims, dbConn, query, includedArchived)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// AddAccount an account for a given user with specified roles.
|
||||
func AddAccount(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req AddAccountRequest, now time.Time) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.AddAccount")
|
||||
defer span.Finish()
|
||||
|
||||
// Validate the request.
|
||||
err := validator.New().Struct(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the claims can modify the user specified in the request.
|
||||
err = CanModifyUserId(ctx, claims, dbConn, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If now empty set it to the current time.
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
// Always store the time as UTC.
|
||||
now = now.UTC()
|
||||
|
||||
// Postgres truncates times to milliseconds when storing. We and do the same
|
||||
// here so the value we return is consistent with what we store.
|
||||
now = now.Truncate(time.Millisecond)
|
||||
|
||||
// Check to see if there is an existing user account, including archived.
|
||||
existQuery := accountSelectQuery()
|
||||
existQuery.Where(existQuery.And(
|
||||
existQuery.Equal("account_id", req.AccountID),
|
||||
existQuery.Equal("user_id", req.UserID),
|
||||
))
|
||||
existing, err := findAccounts(ctx, claims, dbConn, existQuery, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If there is an existing entry, then update instead of insert.
|
||||
if len(existing) > 0 {
|
||||
upReq := UpdateAccountRequest{
|
||||
UserID: req.UserID,
|
||||
AccountID: req.AccountID,
|
||||
Roles: req.Roles,
|
||||
unArchive: true,
|
||||
}
|
||||
return UpdateAccount(ctx, claims, dbConn, upReq, now)
|
||||
}
|
||||
|
||||
// New auto-generated uuid for the record
|
||||
id := uuid.NewRandom().String()
|
||||
|
||||
// Build the insert SQL statement.
|
||||
query := sqlbuilder.NewInsertBuilder()
|
||||
query.InsertInto(usersAccountsTableName)
|
||||
query.Cols("id", "user_id", "account_id", "roles", "created_at", "updated_at")
|
||||
query.Values(1, id, req.UserID, req.AccountID, req.Roles, now, now)
|
||||
|
||||
// Execute the query with the provided context.
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
_, err = dbConn.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessagef(err, "add account %s to user %s failed", req.AccountID, req.UserID)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAccount...
|
||||
func UpdateAccount(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UpdateAccountRequest, now time.Time) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Update")
|
||||
defer span.Finish()
|
||||
|
||||
// Validate the request.
|
||||
err := validator.New().Struct(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the claims can modify the user specified in the request.
|
||||
err = CanModifyUserId(ctx, claims, dbConn, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If now empty set it to the current time.
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
// Always store the time as UTC.
|
||||
now = now.UTC()
|
||||
|
||||
// Postgres truncates times to milliseconds when storing. We and do the same
|
||||
// here so the value we return is consistent with what we store.
|
||||
now = now.Truncate(time.Millisecond)
|
||||
|
||||
// Build the update SQL statement.
|
||||
query := sqlbuilder.NewUpdateBuilder()
|
||||
query.Update(usersTableName)
|
||||
query.Set(
|
||||
query.Assign("roles", req.Roles),
|
||||
query.Assign("updated_at", now),
|
||||
)
|
||||
query.Where(query.And(
|
||||
query.Equal("user_id", req.UserID),
|
||||
query.Equal("account_id", req.AccountID),
|
||||
))
|
||||
|
||||
// Execute the query with the provided context.
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
_, err = dbConn.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessagef(err, "update account %s for user %s failed", req.AccountID, req.UserID)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAccount...
|
||||
func RemoveAccount(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req RemoveAccountRequest, now time.Time) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.RemoveAccount")
|
||||
defer span.Finish()
|
||||
|
||||
// Validate the request.
|
||||
err := validator.New().Struct(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the claims can modify the user specified in the request.
|
||||
err = CanModifyUserId(ctx, claims, dbConn, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If now empty set it to the current time.
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
// Always store the time as UTC.
|
||||
now = now.UTC()
|
||||
|
||||
// Postgres truncates times to milliseconds when storing. We and do the same
|
||||
// here so the value we return is consistent with what we store.
|
||||
now = now.Truncate(time.Millisecond)
|
||||
|
||||
// Build the update SQL statement.
|
||||
query := sqlbuilder.NewUpdateBuilder()
|
||||
query.Update(usersAccountsTableName)
|
||||
query.Set(query.Assign("archived_at", now))
|
||||
query.Where(query.And(
|
||||
query.Equal("user_id", req.UserID),
|
||||
query.Equal("account_id", req.AccountID),
|
||||
))
|
||||
|
||||
// Execute the query with the provided context.
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
_, err = dbConn.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessagef(err, "remove account %s from user %s failed", req.AccountID, req.UserID)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAccount...
|
||||
func DeleteAccount(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req DeleteAccountRequest, now time.Time) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.RemoveAccount")
|
||||
defer span.Finish()
|
||||
|
||||
// Validate the request.
|
||||
err := validator.New().Struct(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the claims can modify the user specified in the request.
|
||||
err = CanModifyUserId(ctx, claims, dbConn, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If now empty set it to the current time.
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
// Always store the time as UTC.
|
||||
now = now.UTC()
|
||||
|
||||
// Postgres truncates times to milliseconds when storing. We and do the same
|
||||
// here so the value we return is consistent with what we store.
|
||||
now = now.Truncate(time.Millisecond)
|
||||
|
||||
// Build the delete SQL statement.
|
||||
query := sqlbuilder.NewDeleteBuilder()
|
||||
query.DeleteFrom(usersAccountsTableName)
|
||||
query.Where(query.And(
|
||||
query.Equal("user_id", req.UserID),
|
||||
query.Equal("account_id", req.AccountID),
|
||||
))
|
||||
|
||||
// Execute the query with the provided context.
|
||||
sql, args := query.Build()
|
||||
sql = dbConn.Rebind(sql)
|
||||
_, err = dbConn.ExecContext(ctx, sql, args...)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "query - %s", query.String())
|
||||
err = errors.WithMessagef(err, "delete account %s for user %s failed", req.AccountID, req.UserID)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -1,17 +1,16 @@
|
||||
package user_test
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/pborman/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
var test *tests.Test
|
||||
@@ -27,6 +26,387 @@ func testMain(m *testing.M) int {
|
||||
return m.Run()
|
||||
}
|
||||
|
||||
// TestUserFindRequestQuery validates userFindRequestQuery
|
||||
func TestUserFindRequestQuery(t *testing.T) {
|
||||
where := "name = ? or email = ?"
|
||||
var (
|
||||
limit uint = 12
|
||||
offset uint = 34
|
||||
)
|
||||
|
||||
req := UserFindRequest{
|
||||
Where: &where,
|
||||
Args: []interface{}{
|
||||
"lee brown",
|
||||
"lee@geeksinthewoods.com",
|
||||
},
|
||||
Order: []string{
|
||||
"id asc",
|
||||
"created_at desc",
|
||||
},
|
||||
Limit: &limit,
|
||||
Offset: &offset,
|
||||
}
|
||||
expected := "SELECT " + usersMapColumns + " FROM " + usersTableName + " WHERE name = ? or email = ? ORDER BY id asc, created_at desc LIMIT 12 OFFSET 34"
|
||||
|
||||
res := userFindRequestQuery(req)
|
||||
|
||||
if diff := cmp.Diff(res.String(), expected); diff != "" {
|
||||
t.Fatalf("\t%s\tExpected result query to match. Diff:\n%s", tests.Failed, diff)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyClaimsUserSelect validates applyClaimsUserSelect
|
||||
func TestApplyClaimsUserSelect(t *testing.T) {
|
||||
var claimTests = []struct {
|
||||
name string
|
||||
claims auth.Claims
|
||||
expectedSql string
|
||||
error error
|
||||
}{
|
||||
{"EmptyClaims",
|
||||
auth.Claims{},
|
||||
"SELECT " + usersMapColumns + " FROM " + usersTableName,
|
||||
nil,
|
||||
},
|
||||
{"RoleUser",
|
||||
auth.Claims{
|
||||
Roles: []string{auth.RoleUser},
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Subject: "user1",
|
||||
Audience: "acc1",
|
||||
},
|
||||
},
|
||||
"SELECT " + usersMapColumns + " FROM " + usersTableName + " WHERE id IN (SELECT user_id FROM " + usersAccountsTableName + " WHERE account_id = 'acc1' AND user_id = 'user1')",
|
||||
nil,
|
||||
},
|
||||
{"RoleAdmin",
|
||||
auth.Claims{
|
||||
Roles: []string{auth.RoleAdmin},
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Subject: "user1",
|
||||
Audience: "acc1",
|
||||
},
|
||||
},
|
||||
"SELECT " + usersMapColumns + " FROM " + usersTableName + " WHERE id IN (SELECT user_id FROM " + usersAccountsTableName + " WHERE account_id = 'acc1' AND user_id = 'user1')",
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
t.Log("Given the need to validate ACLs are enforced by claims to a select query.")
|
||||
{
|
||||
for i, tt := range claimTests {
|
||||
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
|
||||
{
|
||||
ctx := tests.Context()
|
||||
|
||||
query := selectQuery()
|
||||
|
||||
err := applyClaimsUserSelect(ctx, tt.claims, query)
|
||||
if err != tt.error {
|
||||
t.Logf("\t\tGot : %+v", err)
|
||||
t.Logf("\t\tWant: %+v", tt.error)
|
||||
t.Fatalf("\t%s\tapplyClaimsUserSelect failed.", tests.Failed)
|
||||
}
|
||||
|
||||
sql, args := query.Build()
|
||||
|
||||
// Use mysql flavor so placeholders will get replaced for comparison.
|
||||
sql, err = sqlbuilder.MySQL.Interpolate(sql, args)
|
||||
if err != nil {
|
||||
t.Log("\t\tGot :", err)
|
||||
t.Fatalf("\t%s\tapplyClaimsUserSelect failed.", tests.Failed)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(sql, tt.expectedSql); diff != "" {
|
||||
t.Fatalf("\t%s\tExpected result query to match. Diff:\n%s", tests.Failed, diff)
|
||||
}
|
||||
|
||||
t.Logf("\t%s\tapplyClaimsUserSelect ok.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateUser validates CreateUser
|
||||
func TestCreateUser(t *testing.T) {
|
||||
|
||||
now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Use disabled status since default is active
|
||||
us := UserStatus_Disabled
|
||||
utz := "America/Santiago"
|
||||
|
||||
dupEmail := uuid.NewRandom().String() + "@geeksinthewoods.com"
|
||||
|
||||
var userTests = []struct {
|
||||
name string
|
||||
claims auth.Claims
|
||||
req CreateUserRequest
|
||||
error error
|
||||
}{
|
||||
{"EmptyClaims",
|
||||
auth.Claims{},
|
||||
CreateUserRequest{
|
||||
Name: "Lee Brown",
|
||||
Email: dupEmail,
|
||||
Password: "akTechFr0n!ier",
|
||||
PasswordConfirm: "akTechFr0n!ier",
|
||||
Status: &us,
|
||||
Timezone: &utz,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{"DuplicateEmailValidation",
|
||||
auth.Claims{},
|
||||
CreateUserRequest{
|
||||
Name: "Lee Brown",
|
||||
Email: dupEmail,
|
||||
Password: "akTechFr0n!ier",
|
||||
PasswordConfirm: "akTechFr0n!ier",
|
||||
Status: &us,
|
||||
Timezone: &utz,
|
||||
},
|
||||
errors.New("Key: 'CreateUserRequest.Email' Error:Field validation for 'Email' failed on the 'unique' tag"),
|
||||
},
|
||||
{"RoleUser",
|
||||
auth.Claims{
|
||||
Roles: []string{auth.RoleUser},
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Subject: "user1",
|
||||
Audience: "acc1",
|
||||
},
|
||||
},
|
||||
CreateUserRequest{
|
||||
Name: "Lee Brown",
|
||||
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
|
||||
Password: "akTechFr0n!ier",
|
||||
PasswordConfirm: "akTechFr0n!ier",
|
||||
Status: &us,
|
||||
Timezone: &utz,
|
||||
},
|
||||
ErrForbidden,
|
||||
},
|
||||
{"RoleAdmin",
|
||||
auth.Claims{
|
||||
Roles: []string{auth.RoleAdmin},
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Subject: "user1",
|
||||
Audience: "acc1",
|
||||
},
|
||||
},
|
||||
CreateUserRequest{
|
||||
Name: "Lee Brown",
|
||||
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
|
||||
Password: "akTechFr0n!ier",
|
||||
PasswordConfirm: "akTechFr0n!ier",
|
||||
Status: &us,
|
||||
Timezone: &utz,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
t.Log("Given the need to validate ACLs are enforced by claims for user create.")
|
||||
{
|
||||
for i, tt := range userTests {
|
||||
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
|
||||
{
|
||||
ctx := tests.Context()
|
||||
|
||||
dbConn := test.MasterDB
|
||||
defer dbConn.Close()
|
||||
|
||||
res, err := Create(ctx, tt.claims, dbConn, tt.req, now)
|
||||
if err != tt.error {
|
||||
// TODO: need a better way to handle validation errors as they are
|
||||
// of type interface validator.ValidationErrorsTranslations
|
||||
var errStr string
|
||||
if err != nil {
|
||||
errStr = err.Error()
|
||||
}
|
||||
var expectStr string
|
||||
if tt.error != nil {
|
||||
expectStr = tt.error.Error()
|
||||
}
|
||||
if errStr != expectStr {
|
||||
t.Logf("\t\tGot : %+v", err)
|
||||
t.Logf("\t\tWant: %+v", tt.error)
|
||||
t.Fatalf("\t%s\tapplyClaimsUserSelect failed.", tests.Failed)
|
||||
}
|
||||
}
|
||||
|
||||
// If there was an error that was expected, then don't go any further
|
||||
if tt.error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
expected := &User{
|
||||
Name: tt.req.Name,
|
||||
Email: tt.req.Email,
|
||||
Status: *tt.req.Status,
|
||||
Timezone: *tt.req.Timezone,
|
||||
|
||||
// Copy this fields from the result.
|
||||
ID: res.ID,
|
||||
PasswordSalt: res.PasswordSalt,
|
||||
PasswordHash: res.PasswordHash,
|
||||
PasswordReset: res.PasswordReset,
|
||||
CreatedAt: res.CreatedAt,
|
||||
UpdatedAt: res.UpdatedAt,
|
||||
//ArchivedAt: nil,
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(res, expected); diff != "" {
|
||||
t.Fatalf("\t%s\tExpected result should match. Diff:\n%s", tests.Failed, diff)
|
||||
}
|
||||
|
||||
t.Logf("\t%s\tapplyClaimsUserSelect ok.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateUser validates Update
|
||||
func TestUpdateUser(t *testing.T) {
|
||||
|
||||
now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Use disabled status since default is active
|
||||
us := UserStatus_Disabled
|
||||
utz := "America/Santiago"
|
||||
|
||||
create := CreateUserRequest{
|
||||
Name: "Lee Brown",
|
||||
Password: "akTechFr0n!ier",
|
||||
PasswordConfirm: "akTechFr0n!ier",
|
||||
Status: &us,
|
||||
Timezone: &utz,
|
||||
}
|
||||
|
||||
dupEmail := uuid.NewRandom().String() + "@geeksinthewoods.com"
|
||||
|
||||
var userTests = []struct {
|
||||
name string
|
||||
claims auth.Claims
|
||||
req UpdateUserRequest
|
||||
error error
|
||||
}{
|
||||
{"EmptyClaims",
|
||||
auth.Claims{},
|
||||
UpdateUserRequest{
|
||||
Name: "Lee Brown",
|
||||
Email: dupEmail,
|
||||
Status: &us,
|
||||
Timezone: &utz,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{"DuplicateEmailValidation",
|
||||
auth.Claims{},
|
||||
UpdateUserRequest{
|
||||
Name: "Lee Brown",
|
||||
Email: dupEmail,
|
||||
Status: &us,
|
||||
Timezone: &utz,
|
||||
},
|
||||
errors.New("Key: 'CreateUserRequest.Email' Error:Field validation for 'Email' failed on the 'unique' tag"),
|
||||
},
|
||||
{"RoleUser",
|
||||
auth.Claims{
|
||||
Roles: []string{auth.RoleUser},
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Subject: "user1",
|
||||
Audience: "acc1",
|
||||
},
|
||||
},
|
||||
UpdateUserRequest{
|
||||
Name: "Lee Brown",
|
||||
Email: &uuid.NewRandom().String(),
|
||||
Status: &us,
|
||||
Timezone: &utz,
|
||||
},
|
||||
ErrForbidden,
|
||||
},
|
||||
{"RoleAdmin",
|
||||
auth.Claims{
|
||||
Roles: []string{auth.RoleAdmin},
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Subject: "user1",
|
||||
Audience: "acc1",
|
||||
},
|
||||
},
|
||||
UpdateUserRequest{
|
||||
Name: "Lee Brown",
|
||||
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
|
||||
Status: &us,
|
||||
Timezone: &utz,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
t.Log("Given the need to validate ACLs are enforced by claims for user update.")
|
||||
{
|
||||
for i, tt := range userTests {
|
||||
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
|
||||
{
|
||||
ctx := tests.Context()
|
||||
|
||||
dbConn := test.MasterDB
|
||||
defer dbConn.Close()
|
||||
|
||||
err := Update(ctx, tt.claims, dbConn, tt.req, now)
|
||||
if err != tt.error {
|
||||
// TODO: need a better way to handle validation errors as they are
|
||||
// of type interface validator.ValidationErrorsTranslations
|
||||
var errStr string
|
||||
if err != nil {
|
||||
errStr = err.Error()
|
||||
}
|
||||
var expectStr string
|
||||
if tt.error != nil {
|
||||
expectStr = tt.error.Error()
|
||||
}
|
||||
if errStr != expectStr {
|
||||
t.Logf("\t\tGot : %+v", err)
|
||||
t.Logf("\t\tWant: %+v", tt.error)
|
||||
t.Fatalf("\t%s\tapplyClaimsUserSelect failed.", tests.Failed)
|
||||
}
|
||||
}
|
||||
|
||||
// If there was an error that was expected, then don't go any further
|
||||
if tt.error != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
expected := &User{
|
||||
Name: tt.req.Name,
|
||||
Email: tt.req.Email,
|
||||
Status: *tt.req.Status,
|
||||
Timezone: *tt.req.Timezone,
|
||||
|
||||
// Copy this fields from the result.
|
||||
ID: res.ID,
|
||||
PasswordSalt: res.PasswordSalt,
|
||||
PasswordHash: res.PasswordHash,
|
||||
PasswordReset: res.PasswordReset,
|
||||
CreatedAt: res.CreatedAt,
|
||||
UpdatedAt: res.UpdatedAt,
|
||||
//ArchivedAt: nil,
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(res, expected); diff != "" {
|
||||
t.Fatalf("\t%s\tExpected result should match. Diff:\n%s", tests.Failed, diff)
|
||||
}
|
||||
|
||||
t.Logf("\t%s\tapplyClaimsUserSelect ok.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// TestUser validates the full set of CRUD operations on User values.
|
||||
func TestUser(t *testing.T) {
|
||||
defer tests.Recover(t)
|
||||
@@ -37,28 +417,23 @@ func TestUser(t *testing.T) {
|
||||
{
|
||||
ctx := tests.Context()
|
||||
|
||||
dbConn := test.MasterDB.Copy()
|
||||
dbConn := test.MasterDB
|
||||
defer dbConn.Close()
|
||||
|
||||
now := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// claims is information about the person making the request.
|
||||
claims := auth.NewClaims(bson.NewObjectId().Hex(), []string{auth.RoleAdmin}, now, time.Hour)
|
||||
|
||||
nu := user.NewUser{
|
||||
Name: "Bill Kennedy",
|
||||
Email: "bill@ardanlabs.com",
|
||||
Roles: []string{auth.RoleAdmin},
|
||||
Password: "gophers",
|
||||
PasswordConfirm: "gophers",
|
||||
}
|
||||
|
||||
u, err := user.Create(ctx, dbConn, &nu, now)
|
||||
u, err := Create(ctx, dbConn, &nu, now)
|
||||
if err != nil {
|
||||
t.Fatalf("\t%s\tShould be able to create user : %s.", tests.Failed, err)
|
||||
}
|
||||
t.Logf("\t%s\tShould be able to create user.", tests.Success)
|
||||
|
||||
|
||||
|
||||
// claims is information about the person making the request.
|
||||
claims := auth.NewClaims(bson.NewObjectId().Hex(), []string{auth.RoleAdmin}, now, time.Hour)
|
||||
|
||||
|
||||
savedU, err := user.Retrieve(ctx, claims, dbConn, u.ID.Hex())
|
||||
if err != nil {
|
||||
t.Fatalf("\t%s\tShould be able to retrieve user by ID: %s.", tests.Failed, err)
|
||||
@@ -112,10 +487,13 @@ func TestUser(t *testing.T) {
|
||||
t.Fatalf("\t%s\tShould NOT be able to retrieve user : %s.", tests.Failed, err)
|
||||
}
|
||||
t.Logf("\t%s\tShould NOT be able to retrieve user.", tests.Success)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// mockTokenGenerator is used for testing that Authenticate calls its provided
|
||||
// token generator in a specific way.
|
||||
type mockTokenGenerator struct{}
|
||||
@@ -177,3 +555,4 @@ func TestAuthenticate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@@ -1,129 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
|
||||
"github.com/gitwak/sqlxmigrate"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// migrationList returns a list of migrations to be executed. If the id of the
|
||||
// migration already exists in the migrations table it will be skipped.
|
||||
func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
|
||||
return []*sqlxmigrate.Migration{
|
||||
// create table users
|
||||
{
|
||||
ID: "20190522-01a",
|
||||
Migrate: func(tx *sql.Tx) error {
|
||||
q := `CREATE TABLE IF NOT EXISTS users (
|
||||
id char(36) NOT NULL,
|
||||
email varchar(200) NOT NULL,
|
||||
title varchar(100) NOT NULL DEFAULT '',
|
||||
first_name varchar(200) NOT NULL DEFAULT '',
|
||||
last_name varchar(200) NOT NULL DEFAULT '',
|
||||
password_hash varchar(200) NOT NULL,
|
||||
password_reset varchar(200) DEFAULT NULL,
|
||||
password_salt varchar(200) NOT NULL,
|
||||
phone varchar(20) NOT NULL DEFAULT '',
|
||||
status enum('active','disabled') NOT NULL DEFAULT 'active',
|
||||
timezone varchar(128) NOT NULL DEFAULT 'America/Anchorage',
|
||||
created_at timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamp(0) DEFAULT NULL,
|
||||
deleted_at timestamp(0) DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT email UNIQUE (email)
|
||||
) ;`
|
||||
if _, err := tx.Exec(q); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *sql.Tx) error {
|
||||
q := `DROP TABLE IF EXISTS users`
|
||||
if _, err := tx.Exec(q); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
// create new table accounts
|
||||
{
|
||||
ID: "20190522-01b",
|
||||
Migrate: func(tx *sql.Tx) error {
|
||||
q := `CREATE TABLE IF NOT EXISTS accounts (
|
||||
id char(36) NOT NULL,
|
||||
name varchar(255) NOT NULL,
|
||||
address1 varchar(255) NOT NULL DEFAULT '',
|
||||
address2 varchar(255) NOT NULL DEFAULT '',
|
||||
city varchar(100) NOT NULL DEFAULT '',
|
||||
region varchar(255) NOT NULL DEFAULT '',
|
||||
country varchar(255) NOT NULL DEFAULT '',
|
||||
zipcode varchar(20) NOT NULL DEFAULT '',
|
||||
status enum('active','pending','disabled') NOT NULL DEFAULT 'active',
|
||||
timezone varchar(128) NOT NULL DEFAULT 'America/Anchorage',
|
||||
signup_user_id char(36) DEFAULT NULL,
|
||||
billing_user_id char(36) DEFAULT NULL,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime DEFAULT NULL,
|
||||
deleted_at datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY name (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`
|
||||
if _, err := tx.Exec(q); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *sql.Tx) error {
|
||||
q := `DROP TABLE IF EXISTS accounts`
|
||||
if _, err := tx.Exec(q); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
// create new table user_accounts
|
||||
{
|
||||
ID: "20190522-01c",
|
||||
Migrate: func(tx *sql.Tx) error {
|
||||
q1 := `CREATE TYPE IF NOT EXISTS role_t as enum('admin', 'user');`
|
||||
if _, err := tx.Exec(q1); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q1)
|
||||
}
|
||||
|
||||
q2 := `CREATE TABLE IF NOT EXISTS users_accounts (
|
||||
id char(36) NOT NULL,
|
||||
account_id char(36) NOT NULL,
|
||||
user_id ichar(36) NOT NULL,
|
||||
roles role_t[] NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime DEFAULT NULL,
|
||||
deleted_at datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY user_account (user_id,account_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`
|
||||
if _, err := tx.Exec(q1); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q2)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *sql.Tx) error {
|
||||
q1 := `DROP TYPE IF EXISTS role_t`
|
||||
if _, err := tx.Exec(q1); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q1)
|
||||
}
|
||||
|
||||
q2 := `DROP TABLE IF EXISTS users_accounts`
|
||||
if _, err := tx.Exec(q2); err != nil {
|
||||
return errors.WithMessagef(err, "Query failed %s", q2)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user