├── .gitignore ├── .travis.yml ├── README.md ├── client ├── app.jsx ├── application.js ├── home.js └── home.scss ├── config.yaml ├── configuration.go ├── db.go ├── email.go ├── email_test.go ├── encrypt-decrypt.go ├── encrypt-decrypt_test.go ├── error.go ├── go.mod ├── go.sum ├── graphql.go ├── handlers.go ├── hippo_suite_test.go ├── log.go ├── login_test.go ├── migrations └── 1539997214585_hippo.up.sql ├── models ├── boil_main_test.go ├── boil_queries.go ├── boil_queries_test.go ├── boil_suites_test.go ├── boil_table_names.go ├── boil_types.go ├── psql_main_test.go ├── psql_suites_test.go ├── psql_upsert.go ├── roles.go ├── roles_test.go ├── subscriptions.go ├── subscriptions_test.go ├── tenants.go ├── tenants_test.go ├── users.go └── users_test.go ├── password-reset-email.go ├── public-schema.sql ├── reset_password_test.go ├── signup-email.go ├── signup_test.go ├── sqlboiler.toml ├── tenant.go ├── tenant_test.go ├── testing.go ├── user.go ├── user_jwt.go ├── user_role_test.go ├── user_roles.go ├── user_test.go ├── views ├── application.html ├── error.html ├── forgot-password-success.html ├── forgot-password.html ├── home.html ├── invite-sent.html └── login.html ├── web.go └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | public/webpack 3 | sqlboiler.toml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.11.x" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hippo 2 | 3 | A multi-tenant Golang framework for writing SaaS type apps 4 | 5 | Built on top of gin, gorm, webpacking and a few other libraries. 6 | -------------------------------------------------------------------------------- /client/app.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathanstitt/hippo/8558b0c78d4ed766e54965c8171a5652da3ce52d/client/app.jsx -------------------------------------------------------------------------------- /client/application.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import whenDomReady from 'when-dom-ready'; 3 | import { render } from 'react-dom'; 4 | import ApolloClient from 'apollo-boost'; 5 | 6 | import { ApolloProvider } from 'react-apollo'; 7 | import { gql } from 'apollo-boost'; 8 | import { Query } from 'react-apollo'; 9 | import { 10 | App, Split, Sidebar, Header, TItle, Box, Menu, Anchor, 11 | } from 'grommet' 12 | 13 | 14 | const Application = () => ( 15 | 16 | 17 | 18 |
19 | brand 20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | {this.props.children} 29 | 30 |
31 |
32 | ) 33 | 34 | 35 | const GET_USER = gql` 36 | query { 37 | users(where: {name: {_eq: "nathan"}}) { 38 | id 39 | name 40 | created_at 41 | } 42 | } 43 | ` 44 | 45 | // const App = () => ( 46 | // 47 | // {({ loading, error, data }) => { 48 | // console.log(data) 49 | // if (loading) return
Loading...
; 50 | // if (error) return
Error...
; 51 | // 52 | // return data.users.map(u =>
{u.id}: {u.name}
) 53 | // }} 54 | //
55 | // ) 56 | 57 | 58 | const ApolloApp = (AppComponent, JWT) => { 59 | // Pass your GraphQL endpoint to uri 60 | const client = new ApolloClient({ 61 | uri: '/v1alpha1/graphql', 62 | headers: { 63 | Authorization: `Bearer ${JWT}`, 64 | }, 65 | }); 66 | return ( 67 | 68 | 69 | 70 | ); 71 | }; 72 | 73 | whenDomReady(() => { 74 | const bootstrapData = JSON.parse( 75 | document.getElementById('bootstrapData') 76 | .getAttribute('content') 77 | ); 78 | console.log(bootstrapData) 79 | const JWT = bootstrapData.JWT; 80 | render(ApolloApp(Application, JWT), document.getElementById('root')); 81 | }); 82 | -------------------------------------------------------------------------------- /client/home.js: -------------------------------------------------------------------------------- 1 | import "./home.scss"; 2 | import $ from 'domtastic'; 3 | 4 | $(document).ready(() => { 5 | $('input, textarea').on('keyup blur focus', function (e) { 6 | 7 | 8 | var $this = $(e.target), 9 | label = $(e.target.parentElement.querySelector('label')); 10 | if (e.type === 'keyup') { 11 | if ($this.val() === '') { 12 | label.removeClass('active highlight'); 13 | } else { 14 | label.addClass('active highlight'); 15 | } 16 | } else if (e.type === 'blur') { 17 | if( $this.val() === '' ) { 18 | label.removeClass('active highlight'); 19 | } else { 20 | label.removeClass('highlight'); 21 | } 22 | } else if (e.type === 'focus') { 23 | 24 | if( $this.val() === '' ) { 25 | label.removeClass('highlight'); 26 | } 27 | else if( $this.val() !== '' ) { 28 | label.addClass('highlight'); 29 | } 30 | } 31 | }); 32 | 33 | 34 | $('.tab a').on('click', function (e) { 35 | e.preventDefault(); 36 | $(this).parent().addClass('active'); 37 | $(this).parent().siblings().removeClass('active'); 38 | var target = $(this).attr('href'); 39 | $('.tab-content > div').forEach((d) => { 40 | if ($.matches(d, target)) { 41 | $(d).removeClass('hidden') 42 | } else { 43 | $(d).addClass('hidden') 44 | } 45 | }) 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /client/home.scss: -------------------------------------------------------------------------------- 1 | $body-bg: #c1bdba; 2 | $form-bg: #13232f; 3 | $white: #ffffff; 4 | 5 | $main: #1ab188; 6 | $main-light: lighten($main,5%); 7 | $main-dark: darken($main,5%); 8 | 9 | $gray-light: #a0b3b0; 10 | $gray: #ddd; 11 | 12 | $thin: 300; 13 | $normal: 400; 14 | $bold: 600; 15 | $br: 4px; 16 | 17 | *, *:before, *:after { 18 | box-sizing: border-box; 19 | } 20 | 21 | html { 22 | overflow-y: scroll; 23 | } 24 | 25 | body { 26 | background: $body-bg; 27 | font-family: Verdana, sans-serif; 28 | } 29 | 30 | .error { 31 | color: #721c24; 32 | background-color: #f8d7da; 33 | border-color: #f5c6cb; 34 | padding: .75rem 1.25rem; 35 | margin-bottom: 1rem; 36 | border: 1px solid transparent; 37 | border-radius: .25rem; 38 | } 39 | 40 | .home, 41 | .forgot-password { 42 | 43 | a { 44 | text-decoration:none; 45 | color:$main; 46 | transition:.5s ease; 47 | &:hover { 48 | color:$main-dark; 49 | } 50 | } 51 | 52 | .content { 53 | background:rgba($form-bg,.9); 54 | padding: 40px; 55 | max-width:600px; 56 | margin:40px auto; 57 | border-radius:$br; 58 | box-shadow:0 4px 10px 4px rgba($form-bg,.3); 59 | } 60 | 61 | .tab-group { 62 | list-style:none; 63 | padding:0; 64 | margin:0 0 40px 0; 65 | &:after { 66 | content: ""; 67 | display: table; 68 | clear: both; 69 | } 70 | li a { 71 | display:block; 72 | text-decoration:none; 73 | padding:15px; 74 | background:rgba($gray-light,.25); 75 | color:$gray-light; 76 | font-size:20px; 77 | float:left; 78 | width:50%; 79 | text-align:center; 80 | cursor:pointer; 81 | transition:.5s ease; 82 | &:hover { 83 | background:$main-dark; 84 | color:$white; 85 | } 86 | } 87 | .active a { 88 | background:$main; 89 | color:$white; 90 | } 91 | } 92 | 93 | .tab-content > .hidden { 94 | display:none; 95 | } 96 | 97 | 98 | h1 { 99 | text-align:center; 100 | color:$white; 101 | font-weight:$thin; 102 | margin:0 0 40px; 103 | } 104 | 105 | label { 106 | position:absolute; 107 | transform:translateY(6px); 108 | left:13px; 109 | color:rgba($white,.5); 110 | transition:all 0.25s ease; 111 | -webkit-backface-visibility: hidden; 112 | pointer-events: none; 113 | font-size:22px; 114 | .req { 115 | margin:2px; 116 | color:$main; 117 | } 118 | &.active { 119 | transform:translateY(-25px); 120 | left:2px; 121 | font-size:14px; 122 | .req { 123 | opacity:0; 124 | } 125 | } 126 | &.highlight { 127 | color:$white; 128 | } 129 | } 130 | 131 | input, textarea { 132 | font-size:22px; 133 | display:block; 134 | width:100%; 135 | height:100%; 136 | padding:5px 10px; 137 | background:none; 138 | background-image:none; 139 | border:1px solid $gray-light; 140 | color:$white; 141 | border-radius:0; 142 | transition:border-color .25s ease, box-shadow .25s ease; 143 | &:focus { 144 | outline:0; 145 | border-color:$main; 146 | } 147 | } 148 | 149 | textarea { 150 | border:2px solid $gray-light; 151 | resize: vertical; 152 | } 153 | 154 | .field-wrap { 155 | position:relative; 156 | margin-bottom:40px; 157 | } 158 | 159 | .top-row { 160 | &:after { 161 | content: ""; 162 | display: table; 163 | clear: both; 164 | } 165 | 166 | > div { 167 | float:left; 168 | width:48%; 169 | margin-right:4%; 170 | &:last-child { 171 | margin:0; 172 | } 173 | } 174 | } 175 | 176 | .button { 177 | border:0; 178 | outline:none; 179 | border-radius:0; 180 | padding:15px 0; 181 | font-size:2rem; 182 | font-weight:$bold; 183 | text-transform:uppercase; 184 | letter-spacing:.1em; 185 | background:$main; 186 | color:$white; 187 | transition:all.5s ease; 188 | -webkit-appearance: none; 189 | &:hover, &:focus { 190 | background:$main-dark; 191 | } 192 | } 193 | 194 | .button-block { 195 | display:block; 196 | width:100%; 197 | } 198 | 199 | .forgot { 200 | margin-top:-20px; 201 | text-align:right; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | endpoint: http://localhost:8081 2 | port: 12345 3 | db_conn_url: "postgres://nas@localhost/hippo_dev?sslmode=disable" 4 | session_secret: 32-byte-long-auth-key-123-45-712 5 | graphql_access_key: mysecretkey 6 | -------------------------------------------------------------------------------- /configuration.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "strings" 5 | "github.com/gin-gonic/gin" 6 | "github.com/urfave/cli" 7 | "github.com/urfave/cli/altsrc" 8 | ) 9 | 10 | type Configuration interface { 11 | String(string) string 12 | Bool(name string) bool 13 | Int(name string) int 14 | } 15 | 16 | var IsDevMode = false 17 | var SessionsKeyValue = []byte("32-byte-long-auth-key-123-45-712") 18 | 19 | func Initialize() *cli.App { 20 | IsDevMode = 0 != strings.Compare(gin.Mode(), "release") 21 | 22 | app := cli.NewApp() 23 | app.Flags = []cli.Flag { 24 | altsrc.NewIntFlag(cli.IntFlag{ 25 | Name: "port", 26 | Value: 8080, 27 | Usage: "port to listen to", 28 | }), 29 | altsrc.NewStringFlag(cli.StringFlag{ 30 | Name: "verbose", 31 | Value: "info", 32 | Usage: "verbosity to log at", 33 | }), 34 | altsrc.NewStringFlag(cli.StringFlag{ 35 | Name: "vmodule", 36 | Value: "", 37 | Usage: "comma-separated list of pattern=N settings for file-filtered logging", 38 | }), 39 | altsrc.NewBoolFlag(cli.BoolFlag{ 40 | Name: "logtostderr", 41 | Usage: "log to standard error instead of files", 42 | }), 43 | altsrc.NewStringFlag(cli.StringFlag{ 44 | Name: "log_dir", 45 | Value: ".", 46 | Usage: "directory to log to", 47 | }), 48 | altsrc.NewIntFlag(cli.IntFlag{ 49 | Name: "webpack_dev_port", 50 | Value: 8089, 51 | Usage: "port for webpack dev server to listen on", 52 | }), 53 | altsrc.NewIntFlag(cli.IntFlag{ 54 | Name: "graphql_port", 55 | Value: 8091, 56 | Usage: "port to listen to", 57 | }), 58 | altsrc.NewStringFlag(cli.StringFlag{ 59 | Name: "product_name", 60 | Value: "Hippo Fun Time!", 61 | Usage: "The name of the product", 62 | }), 63 | altsrc.NewStringFlag(cli.StringFlag{ 64 | Name: "product_email", 65 | Usage: "The email address to use for transactional email for the product", 66 | }), 67 | altsrc.NewStringFlag(cli.StringFlag{ 68 | Name: "logo_url", 69 | Value: "", 70 | Usage: "url for logo", 71 | }), 72 | altsrc.NewStringFlag(cli.StringFlag{ 73 | Name: "server_url", 74 | Value: "http://localhost:8080", 75 | Usage: "The domain to use for URLs", 76 | }), 77 | altsrc.NewStringFlag(cli.StringFlag{ 78 | Name: "bind_address", 79 | Value: "localhost", 80 | Usage: "ip address to bind to", 81 | }), 82 | altsrc.NewStringFlag(cli.StringFlag{ 83 | Name: "session_cookie_name", 84 | Value: "hippo", 85 | Usage: "name of session cookie", 86 | }), 87 | altsrc.NewStringFlag(cli.StringFlag{ 88 | Name: "db_connection_url", 89 | Value: "", 90 | Usage: "PG database connection string", 91 | }), 92 | altsrc.NewStringFlag(cli.StringFlag{ 93 | Name: "session_secret", 94 | Value: "", 95 | Usage: "32 char long string to use for encrypting session", 96 | }), 97 | altsrc.NewStringFlag(cli.StringFlag{ 98 | Name: "graphql_access_key", 99 | Value: "mysecretkey", 100 | Usage: "32 char long string to use for encrypting session", 101 | }), 102 | altsrc.NewStringFlag(cli.StringFlag{ 103 | Name: "email_server", 104 | Value: "localhost", 105 | Usage: "address of email server", 106 | }), 107 | altsrc.NewStringFlag(cli.StringFlag{ 108 | Name: "administrator_uuid", 109 | Value: "", 110 | Usage: "uuid of administrator tenant", 111 | }), 112 | cli.StringFlag{ 113 | Name: "config", 114 | Value: "./config.yaml", 115 | Usage: "read configuration from file", 116 | }, 117 | } 118 | 119 | app.Before = altsrc.InitInputSourceWithContext( 120 | app.Flags, 121 | altsrc.NewYamlSourceFromFlagFunc("config"), 122 | ) 123 | 124 | return app; 125 | } 126 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package hippo; 2 | 3 | import ( 4 | "fmt" 5 | "database/sql" 6 | _ "github.com/lib/pq" 7 | "github.com/nathanstitt/hippo/models" 8 | "github.com/volatiletech/sqlboiler/boil" 9 | ) 10 | 11 | type DB = *sql.Tx 12 | 13 | // as a convience re-export models/func from models 14 | // so that other users of Hippo don't have to import models 15 | type Tenant = hm.Tenant 16 | var Tenants = hm.Tenants 17 | var FindTenantP = hm.FindTenantP 18 | 19 | type User = hm.User 20 | var Users = hm.Users 21 | var FindUserP = hm.FindUserP 22 | 23 | type Subscription = hm.Subscription 24 | var Subscriptions = hm.Subscriptions 25 | 26 | func ConnectDB(c Configuration) *sql.DB { 27 | conn := c.String("db_connection_url") 28 | db, err := sql.Open("postgres", conn) 29 | if err != nil { 30 | panic(fmt.Sprintf("invalid syntx for db_conn_url %s: %s\n", conn, err)) 31 | } 32 | pingErr := db.Ping() 33 | if pingErr != nil { 34 | panic(fmt.Sprintf("unable to connect to DB using %s: %s\n", conn, pingErr)) 35 | } 36 | 37 | hm.AddUserHook(boil.BeforeDeleteHook, ensureOwnerAndGuest) 38 | 39 | return db 40 | } 41 | -------------------------------------------------------------------------------- /email.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "time" 7 | "github.com/go-mail/mail" 8 | "github.com/matcornic/hermes/v2" 9 | "github.com/nathanstitt/hippo/models" 10 | ) 11 | 12 | 13 | type Email struct { 14 | Message *mail.Message 15 | Body *hermes.Body 16 | Tenant *hm.Tenant 17 | Product hermes.Product 18 | Configuration Configuration 19 | } 20 | 21 | func NewEmailMessage(tenant *hm.Tenant, config Configuration) *Email { 22 | product := hermes.Product{ 23 | // Appears in header & footer of e-mails 24 | Name: tenant.Name, 25 | Link: tenant.HomepageURL.String, 26 | Logo: tenant.LogoURL.String, 27 | Copyright: fmt.Sprintf( 28 | "Copyright © %d %s. All rights reserved.", 29 | time.Now().Year(), 30 | config.String("product_name"), 31 | ), 32 | } 33 | email := &Email{ 34 | Message: mail.NewMessage(), 35 | Configuration: config, 36 | Tenant: tenant, 37 | Product: product, 38 | } 39 | email.SetFrom(tenant.Email, tenant.Name) 40 | return email 41 | } 42 | 43 | func decodeInviteToken(token string) (string, error) { 44 | return DecryptStringProperty(token, "email") 45 | } 46 | 47 | 48 | 49 | type EmailSenderInterface interface { 50 | SendEmail(Configuration, *mail.Message) error 51 | } 52 | 53 | // Mail sender 54 | type LocalhostEmailSender struct {} 55 | 56 | func (s *LocalhostEmailSender) SendEmail(config Configuration, m *mail.Message) error { 57 | host := config.String("email_server") 58 | d := mail.Dialer{Host: host, Port: 25} 59 | d.StartTLSPolicy = mail.NoStartTLS 60 | if IsDevMode { 61 | m.WriteTo(os.Stdout) 62 | return nil 63 | } else { 64 | return d.DialAndSend(m) 65 | } 66 | } 67 | 68 | var EmailSender EmailSenderInterface = &LocalhostEmailSender{} 69 | 70 | func setAddress(mail *mail.Message, header string, address string, names []string) { 71 | if len(names) > 0 { 72 | mail.SetAddressHeader(header, address, names[0]) 73 | } else { 74 | mail.SetHeader(header, address) 75 | } 76 | } 77 | 78 | func (email *Email) SetSubject(subject string, a ...interface{}) { 79 | email.Message.SetHeader("Subject", fmt.Sprintf(subject, a...)) 80 | } 81 | func (email *Email) SetFrom(address string, name ...string) { 82 | setAddress(email.Message, "From", address, name) 83 | } 84 | 85 | func (email *Email) SetTo(address string, name ...string) { 86 | setAddress(email.Message, "To", address, name) 87 | } 88 | func (email *Email) SetReplyTo(address string, name ...string) { 89 | setAddress(email.Message, "ReplyTo", address, name) 90 | } 91 | func (email *Email) FormatAddress(address, name string) string { 92 | return email.Message.FormatAddress(address, name) 93 | } 94 | 95 | func (email *Email) BuildMessage() error { 96 | h := hermes.Hermes{ 97 | Product: email.Product, 98 | } 99 | if email.Body == nil { 100 | return fmt.Errorf("Unable to send email without body") 101 | } 102 | contents := hermes.Email{ Body: *email.Body} 103 | htmlEmailBody, err := h.GenerateHTML(contents) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | textEmailBody, err := h.GeneratePlainText(contents) 109 | if err != nil { 110 | return err 111 | } 112 | email.Message.SetBody("text/plain", textEmailBody) 113 | email.Message.AddAlternative("text/html", htmlEmailBody) 114 | return nil 115 | } 116 | 117 | func (email *Email) Deliver() error { 118 | err := email.BuildMessage() 119 | if err != nil { 120 | return err 121 | } 122 | return EmailSender.SendEmail(email.Configuration, email.Message) 123 | } 124 | -------------------------------------------------------------------------------- /email_test.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | "github.com/matcornic/hermes/v2" 7 | ) 8 | 9 | var _ = Describe("Sending Email", func() { 10 | 11 | Test("can send", &TestFlags{}, func(env *TestEnv) { 12 | email := NewEmailMessage(env.Tenant, env.Config) 13 | email.SetTo("test@test.com") 14 | email.SetSubject("Hello %s, you have %d things", "bob", 33) 15 | email.SetFrom("foo-test@test.com") 16 | email.Body = &hermes.Body{ 17 | Name: "test@test.com", 18 | Signature: "GO AWAY!", 19 | } 20 | err := email.Deliver() 21 | Expect(err).To(BeNil()) 22 | Expect(LastEmailDelivery.Subject).To(Equal("Hello bob, you have 33 things")) 23 | Expect(LastEmailDelivery.To).To(Equal("test@test.com")) 24 | Expect(LastEmailDelivery.Contents).To( 25 | ContainSubstring("GO AWAY!"), 26 | ) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /encrypt-decrypt.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "fmt" 5 | "errors" 6 | "strings" 7 | "net/http" 8 | "github.com/dgrijalva/jwt-go" 9 | "github.com/nathanstitt/hippo/models" 10 | ) 11 | 12 | type UserClaims struct { 13 | hm.User 14 | jwt.StandardClaims 15 | } 16 | 17 | func JWTforUser(user *hm.User) (string, error) { 18 | 19 | claims := UserClaims{ 20 | *user, 21 | jwt.StandardClaims{}, 22 | } 23 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 24 | tokenString, err := token.SignedString(SessionsKeyValue) 25 | return tokenString, err 26 | } 27 | 28 | func UserforJWT(tokenString string) (*hm.User, error) { 29 | token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { 30 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 31 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) 32 | } 33 | return SessionsKeyValue, nil 34 | }) 35 | 36 | if claims, ok := token.Claims.(*UserClaims); ok && token.Valid { 37 | return &claims.User, nil; 38 | } else { 39 | return nil, err; 40 | } 41 | } 42 | 43 | func Encrypt(contents jwt.MapClaims) (string, error) { 44 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, contents) 45 | tokenString, err := token.SignedString(SessionsKeyValue) 46 | return tokenString, err 47 | } 48 | 49 | func userFromRequest(r *http.Request) (*hm.User, error) { 50 | reqToken := r.Header.Get("Authorization") 51 | parts := strings.Split(reqToken, "Bearer ") 52 | if len(parts) != 2 { 53 | return nil, errors.New("failed to parse Authorization Bearer token") 54 | } 55 | user, err := UserforJWT(parts[1]) 56 | if err != nil { 57 | return nil, fmt.Errorf("Failed to decode auth token: %v", err); 58 | } 59 | return user, nil 60 | } 61 | 62 | func Decrypt(tokenString string) (map[string]interface{}, error) { 63 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 64 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 65 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) 66 | } 67 | 68 | return SessionsKeyValue, nil 69 | }) 70 | 71 | if err != nil { 72 | return nil, err; 73 | } 74 | 75 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 76 | return claims, nil 77 | } else { 78 | return nil, errors.New("failed to extract claims from token") 79 | } 80 | } 81 | 82 | func EncryptStringProperty(property string, value string) (string, error) { 83 | return Encrypt(map[string]interface{}{property: value}); 84 | } 85 | 86 | func DecryptStringProperty(tokenString string, property string) (string, error) { 87 | claims, err := Decrypt(tokenString) 88 | if err == nil { 89 | property, ok := claims[property].(string) 90 | if ok { 91 | return property, nil 92 | } else { 93 | return "", fmt.Errorf("Unable to decode property %s", property) 94 | } 95 | } 96 | return "", err 97 | } 98 | 99 | 100 | 101 | // // Encrypts data using 256-bit AES-GCM. This both hides the content of 102 | // // the data and provides a check that it hasn't been altered. Output takes the 103 | // // form of a urlencoded base64 string 104 | // func Encrypt(plaintext string) (string, error) { 105 | // //plaintext []byte 106 | // block, err := aes.NewCipher(SessionsKeyValue) 107 | // if err != nil { 108 | // return "", err 109 | // } 110 | // gcm, err := cipher.NewGCM(block) 111 | // if err != nil { 112 | // return "", err 113 | // } 114 | // nonce := make([]byte, gcm.NonceSize()) 115 | // _, err = io.ReadFull(rand.Reader, nonce) 116 | // if err != nil { 117 | // return "", err 118 | // } 119 | // encrypted, err := gcm.Seal(nonce, nonce, []byte(plaintext), nil), nil 120 | // if err != nil { 121 | // return "", err 122 | // } 123 | // return base64.URLEncoding.EncodeToString(encrypted), nil 124 | // } 125 | 126 | // // Decrypts urlencoded b64 data using 256-bit AES-GCM and returns it as as a string. 127 | // func Decrypt(b64text string) (string, error) { 128 | // block, err := aes.NewCipher(SessionsKeyValue) 129 | // if err != nil { 130 | // return "", err 131 | // } 132 | // gcm, err := cipher.NewGCM(block) 133 | // if err != nil { 134 | // return "", err 135 | // } 136 | // ciphertext, err := base64.URLEncoding.DecodeString(b64text) 137 | // if len(ciphertext) < gcm.NonceSize() { 138 | // return "", errors.New("ciphertext to short") 139 | // } 140 | // bytes, err := gcm.Open(nil, 141 | // ciphertext[:gcm.NonceSize()], 142 | // ciphertext[gcm.NonceSize():], 143 | // nil, 144 | // ) 145 | // return string(bytes[:]), err 146 | // } 147 | -------------------------------------------------------------------------------- /encrypt-decrypt_test.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | _ "fmt" // for adhoc printing 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "github.com/nathanstitt/hippo/models" 8 | ) 9 | 10 | var _ = Describe("Encryption-Decryption", func() { 11 | 12 | Test("can encrypt/decrypt", &TestFlags{}, func(env *TestEnv) { 13 | domain := "test.com" 14 | encrypted, _ := Encrypt(map[string]interface{}{"d": domain}) 15 | decrypted, _ := DecryptStringProperty(encrypted, "d") 16 | Expect(domain).To(Equal(decrypted)) 17 | }) 18 | 19 | Test("can encrypt/decrypt users", &TestFlags{}, func(env *TestEnv) { 20 | user := hm.User{ 21 | Name: "My Name", Email:"test@test.com", 22 | } 23 | encrypted, _ := JWTforUser(&user) 24 | decrypted, _ := UserforJWT(encrypted) 25 | Expect(user.ID).To(Equal(decrypted.ID)) 26 | Expect(user.Name).To(Equal(decrypted.Name)) 27 | Expect(user.Email).To(Equal(decrypted.Email)) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | func CheckError(err error) { 8 | if err != nil { 9 | log.Fatalf("Error: %s", err) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nathanstitt/hippo 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/ericlagergren/decimal v0.0.0-20191206042408-88212e6cfca9 // indirect 8 | github.com/friendsofgo/errors v0.9.2 // indirect 9 | github.com/gin-contrib/sessions v0.0.3 10 | github.com/gin-gonic/gin v1.6.2 11 | github.com/go-mail/mail v2.3.1+incompatible 12 | github.com/gofrs/uuid v3.2.0+incompatible // indirect 13 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 14 | github.com/gosimple/slug v1.9.0 15 | github.com/lib/pq v1.4.0 16 | github.com/matcornic/hermes/v2 v2.1.0 17 | github.com/nathanstitt/webpacking v0.0.0-20181127003608-c4aefc428777 18 | github.com/onsi/ginkgo v1.12.0 19 | github.com/pkg/errors v0.9.1 20 | github.com/spf13/cast v1.3.1 // indirect 21 | github.com/szuecs/gin-glog v1.1.1 22 | github.com/urfave/cli v1.22.4 23 | github.com/urfave/cli/v2 v2.2.0 24 | github.com/volatiletech/inflect v0.0.0-20170731032912-e7201282ae8d // indirect 25 | github.com/volatiletech/null v8.0.0+incompatible 26 | github.com/volatiletech/sqlboiler v3.7.0+incompatible 27 | golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 28 | gopkg.in/urfave/cli.v1 v1.20.0 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= 4 | github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 5 | github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88/EUUG3qmxwtDmPsY= 6 | github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 7 | github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk= 8 | github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= 9 | github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= 10 | github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 11 | github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg= 12 | github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= 13 | github.com/apmckinlay/gsuneido v0.0.0-20190404155041-0b6cd442a18f/go.mod h1:JU2DOj5Fc6rol0yaT79Csr47QR0vONGwJtBNGRD7jmc= 14 | github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= 15 | github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= 16 | github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= 17 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 18 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 19 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM= 24 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 25 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 26 | github.com/ericlagergren/decimal v0.0.0-20191206042408-88212e6cfca9 h1:mMVotm9OVwoOS2IFGRRS5AfMTFWhtf8wj34JEYh47/k= 27 | github.com/ericlagergren/decimal v0.0.0-20191206042408-88212e6cfca9/go.mod h1:ZWP59etEywfyMG2lAqnoi3t8uoiZCiTmLtwt6iESIsQ= 28 | github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk= 29 | github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI= 30 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 31 | github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI= 32 | github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I= 33 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 34 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 35 | github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= 36 | github.com/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM= 37 | github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 38 | github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= 39 | github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y= 40 | github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM= 41 | github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M= 42 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 43 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 44 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 45 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 46 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 47 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 48 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 49 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 50 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 51 | github.com/gofrs/uuid v1.2.0 h1:coDhrjgyJaglxSjxuJdqQSSdUpG3w6p1OwN2od6frBU= 52 | github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= 53 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 54 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 55 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 56 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 57 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 58 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 59 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 60 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 61 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 62 | github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= 63 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 64 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 65 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 66 | github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= 67 | github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= 68 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 69 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 70 | github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= 71 | github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= 72 | github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= 73 | github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs= 74 | github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg= 75 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 76 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 77 | github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= 78 | github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= 79 | github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= 80 | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 81 | github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ= 82 | github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= 83 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 84 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 85 | github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= 86 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 87 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 88 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 89 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 90 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 91 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 92 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 93 | github.com/lib/pq v1.4.0 h1:TmtCFbH+Aw0AixwyttznSMQDgbR5Yed/Gg6S8Funrhc= 94 | github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 95 | github.com/matcornic/hermes v1.2.0 h1:AuqZpYcTOtTB7cahdevLfnhIpfzmpqw5Czv8vpdnFDU= 96 | github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc= 97 | github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI= 98 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 99 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 100 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 101 | github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= 102 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 103 | github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= 104 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 105 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 106 | github.com/nathanstitt/webpacking v0.0.0-20181127003608-c4aefc428777 h1:++TqzXzd3NppdrjHzbvQuH73yc2IGUGHn9ttNQF3b3s= 107 | github.com/nathanstitt/webpacking v0.0.0-20181127003608-c4aefc428777/go.mod h1:z4wowTQhX9NC1eVD44UtfgppsZ9ElhGLNI1swrvxLe0= 108 | github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= 109 | github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 110 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 111 | github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= 112 | github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= 113 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 114 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 115 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 116 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 117 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 118 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 119 | github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= 120 | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= 121 | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= 122 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 123 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 124 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 125 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 126 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 127 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 128 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 129 | github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= 130 | github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= 131 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 132 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 133 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 134 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 135 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 136 | github.com/szuecs/gin-glog v1.1.1 h1:YwewjwcnxTVJeB6U7zcJ82FohUzxR4wzSTuspkj6BRE= 137 | github.com/szuecs/gin-glog v1.1.1/go.mod h1:eFFtHjaaO5lc0ich5AZPMsu3i8rn1TvwmpQnJb+3HP4= 138 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 139 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 140 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 141 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 142 | github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= 143 | github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 144 | github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= 145 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 146 | github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 h1:L0rPdfzq43+NV8rfIx2kA4iSSLRj2jN5ijYHoeXRwvQ= 147 | github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w= 148 | github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe h1:9YnI5plmy+ad6BM+JCLJb2ZV7/TNiE5l7SNKfumYKgc= 149 | github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe/go.mod h1:JTFJA/t820uFDoyPpErFQ3rb3amdZoPtxcKervG0OE4= 150 | github.com/volatiletech/inflect v0.0.0-20170731032912-e7201282ae8d h1:gI4/tqP6lCY5k6Sg+4k9qSoBXmPwG+xXgMpK7jivD4M= 151 | github.com/volatiletech/inflect v0.0.0-20170731032912-e7201282ae8d/go.mod h1:jspfvgf53t5NLUT4o9L1IX0kIBNKamGq1tWc/MgWK9Q= 152 | github.com/volatiletech/null v8.0.0+incompatible h1:7wP8m5d/gZ6kW/9GnrLtMCRre2dlEnaQ9Km5OXlK4zg= 153 | github.com/volatiletech/null v8.0.0+incompatible/go.mod h1:0wD98JzdqB+rLyZ70fN05VDbXbafIb0KU0MdVhCzmOQ= 154 | github.com/volatiletech/sqlboiler v1.0.0 h1:p0aCjD4tyDfjDjL6jakkfnrWWC4Z2wd1SNM7sS3rzAQ= 155 | github.com/volatiletech/sqlboiler v3.7.0+incompatible h1:tgc0UL8e1YW2g01ic472UMtSqrert5cSyz7tWWDIJbU= 156 | github.com/volatiletech/sqlboiler v3.7.0+incompatible/go.mod h1:jLfDkkHWPbS2cWRLkyC20vQWaIQsASEY7gM7zSo11Yw= 157 | golang.org/x/crypto v0.0.0-20181029175232-7e6ffbd03851/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 158 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 159 | golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 h1:Q7tZBpemrlsc2I7IyODzhtallWRSm4Q0d09pL6XbQtU= 160 | golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 161 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 162 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 163 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 164 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 165 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 166 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 167 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 168 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 169 | golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 170 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 172 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 174 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 176 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 177 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 178 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 179 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 180 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 181 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 182 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 183 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 184 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 185 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 186 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 187 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 188 | gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= 189 | gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= 190 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 191 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 192 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 193 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 194 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 195 | -------------------------------------------------------------------------------- /graphql.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "fmt" 7 | "os/exec" 8 | "encoding/json" 9 | ) 10 | 11 | 12 | func StartGraphql(c Configuration) *exec.Cmd { 13 | jwtSecret, _ := json.Marshal( 14 | map[string]string{ 15 | "type": "HS256", 16 | "key": c.String("session_secret"), 17 | "claims_namespace": "graphql_claims", 18 | }, 19 | ) 20 | 21 | hasura := exec.Command( 22 | "graphql-engine", "serve", 23 | "--server-port", fmt.Sprintf("%d", c.Int("graphql_port")), 24 | "--database-url", c.String("db_conn_url"), 25 | ) 26 | 27 | hasura.Env = append(os.Environ(), 28 | fmt.Sprintf("HASURA_GRAPHQL_ACCESS_KEY=%s", c.String("graphql_access_key")), 29 | fmt.Sprintf("HASURA_GRAPHQL_JWT_SECRET=%s", jwtSecret), 30 | ) 31 | // Create stdout, stderr streams of type io.Reader 32 | stdout, err := hasura.StdoutPipe() 33 | CheckError(err) 34 | stderr, err := hasura.StderrPipe() 35 | CheckError(err) 36 | 37 | // Start command 38 | err = hasura.Start() 39 | CheckError(err) 40 | 41 | // Don't let main() exit before our command has finished running 42 | //defer hasura.Wait() // Doesn't block 43 | 44 | // Non-blockingly echo command output to terminal 45 | go io.Copy(os.Stdout, stdout) 46 | go io.Copy(os.Stderr, stderr) 47 | 48 | return hasura; 49 | } 50 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "log" 5 | "fmt" 6 | "net/http" 7 | "database/sql" 8 | "html/template" 9 | "encoding/json" 10 | "net/http/httputil" 11 | "github.com/gin-gonic/gin" 12 | "github.com/nathanstitt/hippo/models" 13 | ) 14 | 15 | func GetConfig(c *gin.Context) Configuration { 16 | config, ok := c.MustGet("config").(Configuration) 17 | if ok { 18 | return config 19 | } 20 | panic("config isn't the correct type") 21 | } 22 | 23 | func GetDB(c *gin.Context) DB { 24 | tx, ok := c.MustGet("dbTx").(DB) 25 | if ok { 26 | return tx 27 | } 28 | panic("config isn't the correct type") 29 | } 30 | 31 | func RoutingMiddleware(config Configuration, db *sql.DB) gin.HandlerFunc { 32 | return func(c *gin.Context) { 33 | tx, err := db.Begin() 34 | if err != nil { 35 | panic(err) 36 | } 37 | c.Set("dbTx", tx) 38 | c.Set("config", config) 39 | defer func() { 40 | status := c.Writer.Status() 41 | if status >= 400 { 42 | log.Printf("Transaction is being rolled back; status = %d\n", status) 43 | tx.Rollback(); 44 | } 45 | return 46 | }() 47 | c.Next() 48 | if (c.Writer.Status() < 400) { 49 | tx.Commit(); 50 | } 51 | } 52 | } 53 | 54 | func RenderErrorPage(message string, c *gin.Context, err *error) { 55 | if err != nil { 56 | log.Printf("Error occured: %s", *err) 57 | } 58 | c.HTML(http.StatusInternalServerError, "error.html", gin.H{ 59 | "message": message, 60 | }) 61 | } 62 | 63 | func RenderNotFoundPage(message string, c *gin.Context, err *error) { 64 | c.HTML(http.StatusNotFound , "not-found.html", gin.H{ 65 | "message": message, 66 | }) 67 | } 68 | 69 | func allowCorsReply(c *gin.Context) { 70 | c.Header("Content-Type", "application/json") 71 | c.Header("Access-Control-Allow-Origin", "*") 72 | c.Header("Access-Control-Max-Age", "86400") 73 | c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") 74 | c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Max, X-HASURA-ACCESS-KEY") 75 | c.Header("Access-Control-Allow-Credentials", "true") 76 | } 77 | 78 | func reverseProxy(port int) gin.HandlerFunc { 79 | target := fmt.Sprintf("localhost:%d", port) 80 | director := func(req *http.Request) { 81 | req.URL.Scheme = "http" 82 | req.URL.Host = target 83 | } 84 | return func(c *gin.Context) { 85 | proxy := &httputil.ReverseProxy{Director: director} 86 | proxy.ServeHTTP(c.Writer, c.Request) 87 | } 88 | } 89 | 90 | func RenderHomepage(signup *SignupData, err *error, c *gin.Context) { 91 | c.HTML(http.StatusOK, "home.html", gin.H{ 92 | "signup": signup, 93 | "error": err, 94 | }) 95 | } 96 | 97 | func RenderApplication(user *hm.User, c *gin.Context) { 98 | cfg := GetConfig(c) 99 | c.HTML(http.StatusOK, "application.html", gin.H{ 100 | "bootstrapData": BootstrapData(user, cfg), 101 | }) 102 | } 103 | 104 | func deliverResetEmail(user *hm.User, token string, db DB, config Configuration) error { 105 | email := NewEmailMessage(user.Tenant().OneP(db), config) 106 | email.Body = passwordResetEmail(user, token, db, config) 107 | email.SetTo(user.Email, user.Name) 108 | email.SetSubject("Password Reset for %s", config.String("product_name")) 109 | return email.Deliver() 110 | } 111 | 112 | func deliverLoginEmail(emailAddress string, tenant *hm.Tenant, config Configuration) error { 113 | email := NewEmailMessage(tenant, config) 114 | email.Body = signupEmail(emailAddress, tenant, config) 115 | email.SetTo(emailAddress, "") 116 | email.SetSubject("Login to %s", config.String("product_name")) 117 | return email.Deliver() 118 | } 119 | 120 | func BootstrapData(user *hm.User, cfg Configuration) template.JS { 121 | type BootstrapDataT map[string]interface{} 122 | bootstrapData, err := json.Marshal( 123 | BootstrapDataT{ 124 | "serverUrl": cfg.String("server_url"), 125 | "user": user, 126 | "graphql" : BootstrapDataT{ 127 | "token": JWTForUser(user, cfg), 128 | "endpoint": cfg.String("server_url"), 129 | }, 130 | }) 131 | if err != nil { 132 | panic(err) 133 | } 134 | return template.JS(string(bootstrapData)) 135 | } 136 | -------------------------------------------------------------------------------- /hippo_suite_test.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "testing" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | func TestDis(t *testing.T) { 10 | BeforeEach(func() { 11 | TestingEnvironment.DBConnectionUrl = "postgres://localhost/hippo_dev?sslmode=disable" 12 | }) 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "Hippo Test Suite") 15 | } 16 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | // "flag" 5 | "time" 6 | 7 | "github.com/golang/glog" 8 | "github.com/szuecs/gin-glog" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | 13 | type LogT struct { 14 | Info func(args ...interface{}) 15 | Warn func(args ...interface{}) 16 | Error func(args ...interface{}) 17 | } 18 | 19 | var Log = LogT{ 20 | Info: glog.Info, 21 | Warn: glog.Warning, 22 | Error: glog.Error, 23 | } 24 | 25 | func InitLoggging(router *gin.Engine, config Configuration) { 26 | // IsDevMode 27 | // flag.Parse() 28 | router.Use(ginglog.Logger(3 * time.Second)) 29 | } 30 | -------------------------------------------------------------------------------- /login_test.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | "net/http" 8 | "net/http/httptest" 9 | "github.com/gin-gonic/gin" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | "github.com/nathanstitt/webpacking" 13 | ) 14 | 15 | func prepareLoginRequest(db DB) url.Values { 16 | data := SignupData{ 17 | Name: "Bob", 18 | Email: "test123@test.com", 19 | Password: "password1234", 20 | Tenant: "Acme Inc", 21 | } 22 | 23 | _, err := CreateTenant(&data, db) 24 | if err != nil { 25 | panic(fmt.Sprintf("add tenant failed: %s", err)) 26 | } 27 | form := url.Values{} 28 | form.Add("email", data.Email) 29 | form.Add("tenant", "acme-inc") 30 | form.Add("password", data.Password) 31 | return form; 32 | } 33 | 34 | func addLoginRoute( 35 | r *gin.Engine, 36 | config Configuration, 37 | webpack *webpacking.WebPacking, 38 | ) { 39 | r.GET("/login", func(c *gin.Context) { 40 | c.HTML(http.StatusOK, "login.html", gin.H{}) 41 | }) 42 | r.POST("/login", UserLoginHandler("/")) 43 | } 44 | 45 | var _ = Describe("Login", func() { 46 | 47 | Test("can log in", &TestFlags{WithRoutes: addLoginRoute}, func(env *TestEnv) { 48 | r := env.Router 49 | db := env.DB 50 | 51 | form := prepareLoginRequest(db); 52 | req, _ := http.NewRequest( "POST", "/login", strings.NewReader(form.Encode())) 53 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 54 | resp := httptest.NewRecorder() 55 | r.ServeHTTP(resp, req) 56 | Expect(resp.Header().Get("Set-Cookie")).To(Not(BeEmpty())) 57 | Expect(resp.Header().Get("Location")).To(Equal("/")) 58 | }) 59 | 60 | Test("it rejects invalid logins", &TestFlags{WithRoutes: addLoginRoute}, func(env *TestEnv) { 61 | 62 | 63 | r := env.Router 64 | db := env.DB 65 | form := prepareLoginRequest(db); 66 | form.Set("password", "foo") 67 | req, _ := http.NewRequest( "POST", "/login", strings.NewReader(form.Encode())) 68 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 69 | resp := httptest.NewRecorder() 70 | r.ServeHTTP(resp, req) 71 | 72 | Expect(resp.Header().Get("Location")).To(BeEmpty()) 73 | Expect(resp.Body.String()).To(ContainSubstring("email or password is incorrect")) 74 | }) 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /migrations/1539997214585_hippo.up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 2 | 3 | CREATE TABLE subscriptions ( 4 | id serial primary key 5 | ,subscription_id character varying 6 | ,name character varying NOT NULL 7 | ,description character varying NOT NULL 8 | ,price numeric(10,2) NOT NULL 9 | ,trial_duration integer DEFAULT 0 10 | ); 11 | 12 | insert into subscriptions (subscription_id, name, description, price) values ( 'free', 'Free', 'Free', 0); 13 | 14 | create table tenants ( 15 | id uuid primary key default gen_random_uuid() 16 | ,identifier text not null unique 17 | ,name text not null 18 | ,subscription_id integer not null references subscriptions(id) default 1 19 | ,logo_url text 20 | ,homepage_url text 21 | ,email text not null 22 | ,metadata jsonb 23 | ,created_at timestamp without time zone NOT NULL default now() 24 | ,updated_at timestamp without time zone NOT NULL default now() 25 | ); 26 | 27 | create table roles ( 28 | id serial primary key 29 | ,name text not null 30 | ); 31 | 32 | create table users ( 33 | id uuid primary key default gen_random_uuid() 34 | ,tenant_id uuid references tenants(id) not null 35 | ,role_id integer not null references roles(id) 36 | ,metadata jsonb 37 | ,password_digest character varying NOT NULL 38 | ,name text not null 39 | ,email text not null 40 | ,created_at timestamp without time zone NOT NULL default now() 41 | ,updated_at timestamp without time zone NOT NULL default now() 42 | ); 43 | 44 | CREATE INDEX user_roles ON users(role_id); 45 | 46 | insert into roles ( name ) values ( 'guest' ); 47 | insert into roles ( name ) values ( 'user' ); 48 | insert into roles ( name ) values ( 'manager' ); 49 | insert into roles ( name ) values ( 'admin' ); 50 | -------------------------------------------------------------------------------- /models/boil_main_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | import ( 7 | "database/sql" 8 | "flag" 9 | "fmt" 10 | "math/rand" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "testing" 15 | "time" 16 | 17 | "github.com/spf13/viper" 18 | "github.com/volatiletech/sqlboiler/boil" 19 | ) 20 | 21 | var flagDebugMode = flag.Bool("test.sqldebug", false, "Turns on debug mode for SQL statements") 22 | var flagConfigFile = flag.String("test.config", "", "Overrides the default config") 23 | 24 | const outputDirDepth = 1 25 | 26 | var ( 27 | dbMain tester 28 | ) 29 | 30 | type tester interface { 31 | setup() error 32 | conn() (*sql.DB, error) 33 | teardown() error 34 | } 35 | 36 | func TestMain(m *testing.M) { 37 | if dbMain == nil { 38 | fmt.Println("no dbMain tester interface was ready") 39 | os.Exit(-1) 40 | } 41 | 42 | rand.Seed(time.Now().UnixNano()) 43 | 44 | flag.Parse() 45 | 46 | var err error 47 | 48 | // Load configuration 49 | err = initViper() 50 | if err != nil { 51 | fmt.Println("unable to load config file") 52 | os.Exit(-2) 53 | } 54 | 55 | // Set DebugMode so we can see generated sql statements 56 | boil.DebugMode = *flagDebugMode 57 | 58 | if err = dbMain.setup(); err != nil { 59 | fmt.Println("Unable to execute setup:", err) 60 | os.Exit(-4) 61 | } 62 | 63 | conn, err := dbMain.conn() 64 | if err != nil { 65 | fmt.Println("failed to get connection:", err) 66 | } 67 | 68 | var code int 69 | boil.SetDB(conn) 70 | code = m.Run() 71 | 72 | if err = dbMain.teardown(); err != nil { 73 | fmt.Println("Unable to execute teardown:", err) 74 | os.Exit(-5) 75 | } 76 | 77 | os.Exit(code) 78 | } 79 | 80 | func initViper() error { 81 | if flagConfigFile != nil && *flagConfigFile != "" { 82 | viper.SetConfigFile(*flagConfigFile) 83 | if err := viper.ReadInConfig(); err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | 89 | var err error 90 | 91 | viper.SetConfigName("sqlboiler") 92 | 93 | configHome := os.Getenv("XDG_CONFIG_HOME") 94 | homePath := os.Getenv("HOME") 95 | wd, err := os.Getwd() 96 | if err != nil { 97 | wd = strings.Repeat("../", outputDirDepth) 98 | } else { 99 | wd = wd + strings.Repeat("/..", outputDirDepth) 100 | } 101 | 102 | configPaths := []string{wd} 103 | if len(configHome) > 0 { 104 | configPaths = append(configPaths, filepath.Join(configHome, "sqlboiler")) 105 | } else { 106 | configPaths = append(configPaths, filepath.Join(homePath, ".config/sqlboiler")) 107 | } 108 | 109 | for _, p := range configPaths { 110 | viper.AddConfigPath(p) 111 | } 112 | 113 | // Ignore errors here, fall back to defaults and validation to provide errs 114 | _ = viper.ReadInConfig() 115 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 116 | viper.AutomaticEnv() 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /models/boil_queries.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | import ( 7 | "github.com/volatiletech/sqlboiler/drivers" 8 | "github.com/volatiletech/sqlboiler/queries" 9 | "github.com/volatiletech/sqlboiler/queries/qm" 10 | ) 11 | 12 | var dialect = drivers.Dialect{ 13 | LQ: 0x22, 14 | RQ: 0x22, 15 | 16 | UseIndexPlaceholders: true, 17 | UseLastInsertID: false, 18 | UseSchema: false, 19 | UseDefaultKeyword: true, 20 | UseAutoColumns: false, 21 | UseTopClause: false, 22 | UseOutputClause: false, 23 | UseCaseWhenExistsClause: false, 24 | } 25 | 26 | // NewQuery initializes a new Query using the passed in QueryMods 27 | func NewQuery(mods ...qm.QueryMod) *queries.Query { 28 | q := &queries.Query{} 29 | queries.SetDialect(q, &dialect) 30 | qm.Apply(q, mods...) 31 | 32 | return q 33 | } 34 | -------------------------------------------------------------------------------- /models/boil_queries_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "math/rand" 12 | "regexp" 13 | 14 | "github.com/volatiletech/sqlboiler/boil" 15 | ) 16 | 17 | var dbNameRand *rand.Rand 18 | 19 | func MustTx(transactor boil.Transactor, err error) boil.Transactor { 20 | if err != nil { 21 | panic(fmt.Sprintf("Cannot create a transactor: %s", err)) 22 | } 23 | return transactor 24 | } 25 | 26 | func newFKeyDestroyer(regex *regexp.Regexp, reader io.Reader) io.Reader { 27 | return &fKeyDestroyer{ 28 | reader: reader, 29 | rgx: regex, 30 | } 31 | } 32 | 33 | type fKeyDestroyer struct { 34 | reader io.Reader 35 | buf *bytes.Buffer 36 | rgx *regexp.Regexp 37 | } 38 | 39 | func (f *fKeyDestroyer) Read(b []byte) (int, error) { 40 | if f.buf == nil { 41 | all, err := ioutil.ReadAll(f.reader) 42 | if err != nil { 43 | return 0, err 44 | } 45 | 46 | all = bytes.Replace(all, []byte{'\r', '\n'}, []byte{'\n'}, -1) 47 | all = f.rgx.ReplaceAll(all, []byte{}) 48 | f.buf = bytes.NewBuffer(all) 49 | } 50 | 51 | return f.buf.Read(b) 52 | } 53 | -------------------------------------------------------------------------------- /models/boil_suites_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | import "testing" 7 | 8 | // This test suite runs each operation test in parallel. 9 | // Example, if your database has 3 tables, the suite will run: 10 | // table1, table2 and table3 Delete in parallel 11 | // table1, table2 and table3 Insert in parallel, and so forth. 12 | // It does NOT run each operation group in parallel. 13 | // Separating the tests thusly grants avoidance of Postgres deadlocks. 14 | func TestParent(t *testing.T) { 15 | t.Run("Roles", testRoles) 16 | t.Run("Subscriptions", testSubscriptions) 17 | t.Run("Tenants", testTenants) 18 | t.Run("Users", testUsers) 19 | } 20 | 21 | func TestDelete(t *testing.T) { 22 | t.Run("Roles", testRolesDelete) 23 | t.Run("Subscriptions", testSubscriptionsDelete) 24 | t.Run("Tenants", testTenantsDelete) 25 | t.Run("Users", testUsersDelete) 26 | } 27 | 28 | func TestQueryDeleteAll(t *testing.T) { 29 | t.Run("Roles", testRolesQueryDeleteAll) 30 | t.Run("Subscriptions", testSubscriptionsQueryDeleteAll) 31 | t.Run("Tenants", testTenantsQueryDeleteAll) 32 | t.Run("Users", testUsersQueryDeleteAll) 33 | } 34 | 35 | func TestSliceDeleteAll(t *testing.T) { 36 | t.Run("Roles", testRolesSliceDeleteAll) 37 | t.Run("Subscriptions", testSubscriptionsSliceDeleteAll) 38 | t.Run("Tenants", testTenantsSliceDeleteAll) 39 | t.Run("Users", testUsersSliceDeleteAll) 40 | } 41 | 42 | func TestExists(t *testing.T) { 43 | t.Run("Roles", testRolesExists) 44 | t.Run("Subscriptions", testSubscriptionsExists) 45 | t.Run("Tenants", testTenantsExists) 46 | t.Run("Users", testUsersExists) 47 | } 48 | 49 | func TestFind(t *testing.T) { 50 | t.Run("Roles", testRolesFind) 51 | t.Run("Subscriptions", testSubscriptionsFind) 52 | t.Run("Tenants", testTenantsFind) 53 | t.Run("Users", testUsersFind) 54 | } 55 | 56 | func TestBind(t *testing.T) { 57 | t.Run("Roles", testRolesBind) 58 | t.Run("Subscriptions", testSubscriptionsBind) 59 | t.Run("Tenants", testTenantsBind) 60 | t.Run("Users", testUsersBind) 61 | } 62 | 63 | func TestOne(t *testing.T) { 64 | t.Run("Roles", testRolesOne) 65 | t.Run("Subscriptions", testSubscriptionsOne) 66 | t.Run("Tenants", testTenantsOne) 67 | t.Run("Users", testUsersOne) 68 | } 69 | 70 | func TestAll(t *testing.T) { 71 | t.Run("Roles", testRolesAll) 72 | t.Run("Subscriptions", testSubscriptionsAll) 73 | t.Run("Tenants", testTenantsAll) 74 | t.Run("Users", testUsersAll) 75 | } 76 | 77 | func TestCount(t *testing.T) { 78 | t.Run("Roles", testRolesCount) 79 | t.Run("Subscriptions", testSubscriptionsCount) 80 | t.Run("Tenants", testTenantsCount) 81 | t.Run("Users", testUsersCount) 82 | } 83 | 84 | func TestHooks(t *testing.T) { 85 | t.Run("Roles", testRolesHooks) 86 | t.Run("Subscriptions", testSubscriptionsHooks) 87 | t.Run("Tenants", testTenantsHooks) 88 | t.Run("Users", testUsersHooks) 89 | } 90 | 91 | func TestInsert(t *testing.T) { 92 | t.Run("Roles", testRolesInsert) 93 | t.Run("Roles", testRolesInsertWhitelist) 94 | t.Run("Subscriptions", testSubscriptionsInsert) 95 | t.Run("Subscriptions", testSubscriptionsInsertWhitelist) 96 | t.Run("Tenants", testTenantsInsert) 97 | t.Run("Tenants", testTenantsInsertWhitelist) 98 | t.Run("Users", testUsersInsert) 99 | t.Run("Users", testUsersInsertWhitelist) 100 | } 101 | 102 | // TestToOne tests cannot be run in parallel 103 | // or deadlocks can occur. 104 | func TestToOne(t *testing.T) { 105 | t.Run("TenantToSubscriptionUsingSubscription", testTenantToOneSubscriptionUsingSubscription) 106 | t.Run("UserToRoleUsingRole", testUserToOneRoleUsingRole) 107 | t.Run("UserToTenantUsingTenant", testUserToOneTenantUsingTenant) 108 | } 109 | 110 | // TestOneToOne tests cannot be run in parallel 111 | // or deadlocks can occur. 112 | func TestOneToOne(t *testing.T) {} 113 | 114 | // TestToMany tests cannot be run in parallel 115 | // or deadlocks can occur. 116 | func TestToMany(t *testing.T) { 117 | t.Run("RoleToUsers", testRoleToManyUsers) 118 | t.Run("SubscriptionToTenants", testSubscriptionToManyTenants) 119 | t.Run("TenantToUsers", testTenantToManyUsers) 120 | } 121 | 122 | // TestToOneSet tests cannot be run in parallel 123 | // or deadlocks can occur. 124 | func TestToOneSet(t *testing.T) { 125 | t.Run("TenantToSubscriptionUsingTenants", testTenantToOneSetOpSubscriptionUsingSubscription) 126 | t.Run("UserToRoleUsingUsers", testUserToOneSetOpRoleUsingRole) 127 | t.Run("UserToTenantUsingUsers", testUserToOneSetOpTenantUsingTenant) 128 | } 129 | 130 | // TestToOneRemove tests cannot be run in parallel 131 | // or deadlocks can occur. 132 | func TestToOneRemove(t *testing.T) {} 133 | 134 | // TestOneToOneSet tests cannot be run in parallel 135 | // or deadlocks can occur. 136 | func TestOneToOneSet(t *testing.T) {} 137 | 138 | // TestOneToOneRemove tests cannot be run in parallel 139 | // or deadlocks can occur. 140 | func TestOneToOneRemove(t *testing.T) {} 141 | 142 | // TestToManyAdd tests cannot be run in parallel 143 | // or deadlocks can occur. 144 | func TestToManyAdd(t *testing.T) { 145 | t.Run("RoleToUsers", testRoleToManyAddOpUsers) 146 | t.Run("SubscriptionToTenants", testSubscriptionToManyAddOpTenants) 147 | t.Run("TenantToUsers", testTenantToManyAddOpUsers) 148 | } 149 | 150 | // TestToManySet tests cannot be run in parallel 151 | // or deadlocks can occur. 152 | func TestToManySet(t *testing.T) {} 153 | 154 | // TestToManyRemove tests cannot be run in parallel 155 | // or deadlocks can occur. 156 | func TestToManyRemove(t *testing.T) {} 157 | 158 | func TestReload(t *testing.T) { 159 | t.Run("Roles", testRolesReload) 160 | t.Run("Subscriptions", testSubscriptionsReload) 161 | t.Run("Tenants", testTenantsReload) 162 | t.Run("Users", testUsersReload) 163 | } 164 | 165 | func TestReloadAll(t *testing.T) { 166 | t.Run("Roles", testRolesReloadAll) 167 | t.Run("Subscriptions", testSubscriptionsReloadAll) 168 | t.Run("Tenants", testTenantsReloadAll) 169 | t.Run("Users", testUsersReloadAll) 170 | } 171 | 172 | func TestSelect(t *testing.T) { 173 | t.Run("Roles", testRolesSelect) 174 | t.Run("Subscriptions", testSubscriptionsSelect) 175 | t.Run("Tenants", testTenantsSelect) 176 | t.Run("Users", testUsersSelect) 177 | } 178 | 179 | func TestUpdate(t *testing.T) { 180 | t.Run("Roles", testRolesUpdate) 181 | t.Run("Subscriptions", testSubscriptionsUpdate) 182 | t.Run("Tenants", testTenantsUpdate) 183 | t.Run("Users", testUsersUpdate) 184 | } 185 | 186 | func TestSliceUpdateAll(t *testing.T) { 187 | t.Run("Roles", testRolesSliceUpdateAll) 188 | t.Run("Subscriptions", testSubscriptionsSliceUpdateAll) 189 | t.Run("Tenants", testTenantsSliceUpdateAll) 190 | t.Run("Users", testUsersSliceUpdateAll) 191 | } 192 | -------------------------------------------------------------------------------- /models/boil_table_names.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | var TableNames = struct { 7 | Roles string 8 | Subscriptions string 9 | Tenants string 10 | Users string 11 | }{ 12 | Roles: "roles", 13 | Subscriptions: "subscriptions", 14 | Tenants: "tenants", 15 | Users: "users", 16 | } 17 | -------------------------------------------------------------------------------- /models/boil_types.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | import ( 7 | "strconv" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/volatiletech/sqlboiler/boil" 11 | "github.com/volatiletech/sqlboiler/strmangle" 12 | ) 13 | 14 | // M type is for providing columns and column values to UpdateAll. 15 | type M map[string]interface{} 16 | 17 | // ErrSyncFail occurs during insert when the record could not be retrieved in 18 | // order to populate default value information. This usually happens when LastInsertId 19 | // fails or there was a primary key configuration that was not resolvable. 20 | var ErrSyncFail = errors.New("hm: failed to synchronize data after insert") 21 | 22 | type insertCache struct { 23 | query string 24 | retQuery string 25 | valueMapping []uint64 26 | retMapping []uint64 27 | } 28 | 29 | type updateCache struct { 30 | query string 31 | valueMapping []uint64 32 | } 33 | 34 | func makeCacheKey(cols boil.Columns, nzDefaults []string) string { 35 | buf := strmangle.GetBuffer() 36 | 37 | buf.WriteString(strconv.Itoa(cols.Kind)) 38 | for _, w := range cols.Cols { 39 | buf.WriteString(w) 40 | } 41 | 42 | if len(nzDefaults) != 0 { 43 | buf.WriteByte('.') 44 | } 45 | for _, nz := range nzDefaults { 46 | buf.WriteString(nz) 47 | } 48 | 49 | str := buf.String() 50 | strmangle.PutBuffer(buf) 51 | return str 52 | } 53 | -------------------------------------------------------------------------------- /models/psql_main_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | import ( 7 | "bytes" 8 | "database/sql" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "os" 13 | "os/exec" 14 | "regexp" 15 | "strings" 16 | 17 | "github.com/kat-co/vala" 18 | _ "github.com/lib/pq" 19 | "github.com/pkg/errors" 20 | "github.com/spf13/viper" 21 | "github.com/volatiletech/sqlboiler/drivers/sqlboiler-psql/driver" 22 | "github.com/volatiletech/sqlboiler/randomize" 23 | ) 24 | 25 | var rgxPGFkey = regexp.MustCompile(`(?m)^ALTER TABLE ONLY .*\n\s+ADD CONSTRAINT .*? FOREIGN KEY .*?;\n`) 26 | 27 | type pgTester struct { 28 | dbConn *sql.DB 29 | 30 | dbName string 31 | host string 32 | user string 33 | pass string 34 | sslmode string 35 | port int 36 | 37 | pgPassFile string 38 | 39 | testDBName string 40 | } 41 | 42 | func init() { 43 | dbMain = &pgTester{} 44 | } 45 | 46 | // setup dumps the database schema and imports it into a temporary randomly 47 | // generated test database so that tests can be run against it using the 48 | // generated sqlboiler ORM package. 49 | func (p *pgTester) setup() error { 50 | var err error 51 | 52 | viper.SetDefault("psql.schema", "public") 53 | viper.SetDefault("psql.port", 5432) 54 | viper.SetDefault("psql.sslmode", "require") 55 | 56 | p.dbName = viper.GetString("psql.dbname") 57 | p.host = viper.GetString("psql.host") 58 | p.user = viper.GetString("psql.user") 59 | p.pass = viper.GetString("psql.pass") 60 | p.port = viper.GetInt("psql.port") 61 | p.sslmode = viper.GetString("psql.sslmode") 62 | 63 | err = vala.BeginValidation().Validate( 64 | vala.StringNotEmpty(p.user, "psql.user"), 65 | vala.StringNotEmpty(p.host, "psql.host"), 66 | vala.Not(vala.Equals(p.port, 0, "psql.port")), 67 | vala.StringNotEmpty(p.dbName, "psql.dbname"), 68 | vala.StringNotEmpty(p.sslmode, "psql.sslmode"), 69 | ).Check() 70 | 71 | if err != nil { 72 | return err 73 | } 74 | 75 | // Create a randomized db name. 76 | p.testDBName = randomize.StableDBName(p.dbName) 77 | 78 | if err = p.makePGPassFile(); err != nil { 79 | return err 80 | } 81 | 82 | if err = p.dropTestDB(); err != nil { 83 | return err 84 | } 85 | if err = p.createTestDB(); err != nil { 86 | return err 87 | } 88 | 89 | dumpCmd := exec.Command("pg_dump", "--schema-only", p.dbName) 90 | dumpCmd.Env = append(os.Environ(), p.pgEnv()...) 91 | createCmd := exec.Command("psql", p.testDBName) 92 | createCmd.Env = append(os.Environ(), p.pgEnv()...) 93 | 94 | r, w := io.Pipe() 95 | dumpCmd.Stdout = w 96 | createCmd.Stdin = newFKeyDestroyer(rgxPGFkey, r) 97 | 98 | if err = dumpCmd.Start(); err != nil { 99 | return errors.Wrap(err, "failed to start pg_dump command") 100 | } 101 | if err = createCmd.Start(); err != nil { 102 | return errors.Wrap(err, "failed to start psql command") 103 | } 104 | 105 | if err = dumpCmd.Wait(); err != nil { 106 | fmt.Println(err) 107 | return errors.Wrap(err, "failed to wait for pg_dump command") 108 | } 109 | 110 | _ = w.Close() // After dumpCmd is done, close the write end of the pipe 111 | 112 | if err = createCmd.Wait(); err != nil { 113 | fmt.Println(err) 114 | return errors.Wrap(err, "failed to wait for psql command") 115 | } 116 | 117 | return nil 118 | } 119 | 120 | func (p *pgTester) runCmd(stdin, command string, args ...string) error { 121 | cmd := exec.Command(command, args...) 122 | cmd.Env = append(os.Environ(), p.pgEnv()...) 123 | 124 | if len(stdin) != 0 { 125 | cmd.Stdin = strings.NewReader(stdin) 126 | } 127 | 128 | stdout := &bytes.Buffer{} 129 | stderr := &bytes.Buffer{} 130 | cmd.Stdout = stdout 131 | cmd.Stderr = stderr 132 | if err := cmd.Run(); err != nil { 133 | fmt.Println("failed running:", command, args) 134 | fmt.Println(stdout.String()) 135 | fmt.Println(stderr.String()) 136 | return err 137 | } 138 | 139 | return nil 140 | } 141 | 142 | func (p *pgTester) pgEnv() []string { 143 | return []string{ 144 | fmt.Sprintf("PGHOST=%s", p.host), 145 | fmt.Sprintf("PGPORT=%d", p.port), 146 | fmt.Sprintf("PGUSER=%s", p.user), 147 | fmt.Sprintf("PGPASSFILE=%s", p.pgPassFile), 148 | } 149 | } 150 | 151 | func (p *pgTester) makePGPassFile() error { 152 | tmp, err := ioutil.TempFile("", "pgpass") 153 | if err != nil { 154 | return errors.Wrap(err, "failed to create option file") 155 | } 156 | 157 | fmt.Fprintf(tmp, "%s:%d:postgres:%s", p.host, p.port, p.user) 158 | if len(p.pass) != 0 { 159 | fmt.Fprintf(tmp, ":%s", p.pass) 160 | } 161 | fmt.Fprintln(tmp) 162 | 163 | fmt.Fprintf(tmp, "%s:%d:%s:%s", p.host, p.port, p.dbName, p.user) 164 | if len(p.pass) != 0 { 165 | fmt.Fprintf(tmp, ":%s", p.pass) 166 | } 167 | fmt.Fprintln(tmp) 168 | 169 | fmt.Fprintf(tmp, "%s:%d:%s:%s", p.host, p.port, p.testDBName, p.user) 170 | if len(p.pass) != 0 { 171 | fmt.Fprintf(tmp, ":%s", p.pass) 172 | } 173 | fmt.Fprintln(tmp) 174 | 175 | p.pgPassFile = tmp.Name() 176 | return tmp.Close() 177 | } 178 | 179 | func (p *pgTester) createTestDB() error { 180 | return p.runCmd("", "createdb", p.testDBName) 181 | } 182 | 183 | func (p *pgTester) dropTestDB() error { 184 | return p.runCmd("", "dropdb", "--if-exists", p.testDBName) 185 | } 186 | 187 | // teardown executes cleanup tasks when the tests finish running 188 | func (p *pgTester) teardown() error { 189 | var err error 190 | if err = p.dbConn.Close(); err != nil { 191 | return err 192 | } 193 | p.dbConn = nil 194 | 195 | if err = p.dropTestDB(); err != nil { 196 | return err 197 | } 198 | 199 | return os.Remove(p.pgPassFile) 200 | } 201 | 202 | func (p *pgTester) conn() (*sql.DB, error) { 203 | if p.dbConn != nil { 204 | return p.dbConn, nil 205 | } 206 | 207 | var err error 208 | p.dbConn, err = sql.Open("postgres", driver.PSQLBuildQueryString(p.user, p.pass, p.testDBName, p.host, p.port, p.sslmode)) 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | return p.dbConn, nil 214 | } 215 | -------------------------------------------------------------------------------- /models/psql_suites_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | import "testing" 7 | 8 | func TestUpsert(t *testing.T) { 9 | t.Run("Roles", testRolesUpsert) 10 | 11 | t.Run("Subscriptions", testSubscriptionsUpsert) 12 | 13 | t.Run("Tenants", testTenantsUpsert) 14 | 15 | t.Run("Users", testUsersUpsert) 16 | } 17 | -------------------------------------------------------------------------------- /models/psql_upsert.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/volatiletech/sqlboiler/drivers" 11 | "github.com/volatiletech/sqlboiler/strmangle" 12 | ) 13 | 14 | // buildUpsertQueryPostgres builds a SQL statement string using the upsertData provided. 15 | func buildUpsertQueryPostgres(dia drivers.Dialect, tableName string, updateOnConflict bool, ret, update, conflict, whitelist []string) string { 16 | conflict = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, conflict) 17 | whitelist = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, whitelist) 18 | ret = strmangle.IdentQuoteSlice(dia.LQ, dia.RQ, ret) 19 | 20 | buf := strmangle.GetBuffer() 21 | defer strmangle.PutBuffer(buf) 22 | 23 | columns := "DEFAULT VALUES" 24 | if len(whitelist) != 0 { 25 | columns = fmt.Sprintf("(%s) VALUES (%s)", 26 | strings.Join(whitelist, ", "), 27 | strmangle.Placeholders(dia.UseIndexPlaceholders, len(whitelist), 1, 1)) 28 | } 29 | 30 | fmt.Fprintf( 31 | buf, 32 | "INSERT INTO %s %s ON CONFLICT ", 33 | tableName, 34 | columns, 35 | ) 36 | 37 | if !updateOnConflict || len(update) == 0 { 38 | buf.WriteString("DO NOTHING") 39 | } else { 40 | buf.WriteByte('(') 41 | buf.WriteString(strings.Join(conflict, ", ")) 42 | buf.WriteString(") DO UPDATE SET ") 43 | 44 | for i, v := range update { 45 | if i != 0 { 46 | buf.WriteByte(',') 47 | } 48 | quoted := strmangle.IdentQuote(dia.LQ, dia.RQ, v) 49 | buf.WriteString(quoted) 50 | buf.WriteString(" = EXCLUDED.") 51 | buf.WriteString(quoted) 52 | } 53 | } 54 | 55 | if len(ret) != 0 { 56 | buf.WriteString(" RETURNING ") 57 | buf.WriteString(strings.Join(ret, ", ")) 58 | } 59 | 60 | return buf.String() 61 | } 62 | -------------------------------------------------------------------------------- /models/roles_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | import ( 7 | "bytes" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/volatiletech/sqlboiler/boil" 12 | "github.com/volatiletech/sqlboiler/queries" 13 | "github.com/volatiletech/sqlboiler/randomize" 14 | "github.com/volatiletech/sqlboiler/strmangle" 15 | ) 16 | 17 | var ( 18 | // Relationships sometimes use the reflection helper queries.Equal/queries.Assign 19 | // so force a package dependency in case they don't. 20 | _ = queries.Equal 21 | ) 22 | 23 | func testRoles(t *testing.T) { 24 | t.Parallel() 25 | 26 | query := Roles() 27 | 28 | if query.Query == nil { 29 | t.Error("expected a query, got nothing") 30 | } 31 | } 32 | 33 | func testRolesDelete(t *testing.T) { 34 | t.Parallel() 35 | 36 | seed := randomize.NewSeed() 37 | var err error 38 | o := &Role{} 39 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 40 | t.Errorf("Unable to randomize Role struct: %s", err) 41 | } 42 | 43 | tx := MustTx(boil.Begin()) 44 | defer func() { _ = tx.Rollback() }() 45 | if err = o.Insert(tx, boil.Infer()); err != nil { 46 | t.Error(err) 47 | } 48 | 49 | if rowsAff, err := o.Delete(tx); err != nil { 50 | t.Error(err) 51 | } else if rowsAff != 1 { 52 | t.Error("should only have deleted one row, but affected:", rowsAff) 53 | } 54 | 55 | count, err := Roles().Count(tx) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | 60 | if count != 0 { 61 | t.Error("want zero records, got:", count) 62 | } 63 | } 64 | 65 | func testRolesQueryDeleteAll(t *testing.T) { 66 | t.Parallel() 67 | 68 | seed := randomize.NewSeed() 69 | var err error 70 | o := &Role{} 71 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 72 | t.Errorf("Unable to randomize Role struct: %s", err) 73 | } 74 | 75 | tx := MustTx(boil.Begin()) 76 | defer func() { _ = tx.Rollback() }() 77 | if err = o.Insert(tx, boil.Infer()); err != nil { 78 | t.Error(err) 79 | } 80 | 81 | if rowsAff, err := Roles().DeleteAll(tx); err != nil { 82 | t.Error(err) 83 | } else if rowsAff != 1 { 84 | t.Error("should only have deleted one row, but affected:", rowsAff) 85 | } 86 | 87 | count, err := Roles().Count(tx) 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | 92 | if count != 0 { 93 | t.Error("want zero records, got:", count) 94 | } 95 | } 96 | 97 | func testRolesSliceDeleteAll(t *testing.T) { 98 | t.Parallel() 99 | 100 | seed := randomize.NewSeed() 101 | var err error 102 | o := &Role{} 103 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 104 | t.Errorf("Unable to randomize Role struct: %s", err) 105 | } 106 | 107 | tx := MustTx(boil.Begin()) 108 | defer func() { _ = tx.Rollback() }() 109 | if err = o.Insert(tx, boil.Infer()); err != nil { 110 | t.Error(err) 111 | } 112 | 113 | slice := RoleSlice{o} 114 | 115 | if rowsAff, err := slice.DeleteAll(tx); err != nil { 116 | t.Error(err) 117 | } else if rowsAff != 1 { 118 | t.Error("should only have deleted one row, but affected:", rowsAff) 119 | } 120 | 121 | count, err := Roles().Count(tx) 122 | if err != nil { 123 | t.Error(err) 124 | } 125 | 126 | if count != 0 { 127 | t.Error("want zero records, got:", count) 128 | } 129 | } 130 | 131 | func testRolesExists(t *testing.T) { 132 | t.Parallel() 133 | 134 | seed := randomize.NewSeed() 135 | var err error 136 | o := &Role{} 137 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 138 | t.Errorf("Unable to randomize Role struct: %s", err) 139 | } 140 | 141 | tx := MustTx(boil.Begin()) 142 | defer func() { _ = tx.Rollback() }() 143 | if err = o.Insert(tx, boil.Infer()); err != nil { 144 | t.Error(err) 145 | } 146 | 147 | e, err := RoleExists(tx, o.ID) 148 | if err != nil { 149 | t.Errorf("Unable to check if Role exists: %s", err) 150 | } 151 | if !e { 152 | t.Errorf("Expected RoleExists to return true, but got false.") 153 | } 154 | } 155 | 156 | func testRolesFind(t *testing.T) { 157 | t.Parallel() 158 | 159 | seed := randomize.NewSeed() 160 | var err error 161 | o := &Role{} 162 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 163 | t.Errorf("Unable to randomize Role struct: %s", err) 164 | } 165 | 166 | tx := MustTx(boil.Begin()) 167 | defer func() { _ = tx.Rollback() }() 168 | if err = o.Insert(tx, boil.Infer()); err != nil { 169 | t.Error(err) 170 | } 171 | 172 | roleFound, err := FindRole(tx, o.ID) 173 | if err != nil { 174 | t.Error(err) 175 | } 176 | 177 | if roleFound == nil { 178 | t.Error("want a record, got nil") 179 | } 180 | } 181 | 182 | func testRolesBind(t *testing.T) { 183 | t.Parallel() 184 | 185 | seed := randomize.NewSeed() 186 | var err error 187 | o := &Role{} 188 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 189 | t.Errorf("Unable to randomize Role struct: %s", err) 190 | } 191 | 192 | tx := MustTx(boil.Begin()) 193 | defer func() { _ = tx.Rollback() }() 194 | if err = o.Insert(tx, boil.Infer()); err != nil { 195 | t.Error(err) 196 | } 197 | 198 | if err = Roles().Bind(nil, tx, o); err != nil { 199 | t.Error(err) 200 | } 201 | } 202 | 203 | func testRolesOne(t *testing.T) { 204 | t.Parallel() 205 | 206 | seed := randomize.NewSeed() 207 | var err error 208 | o := &Role{} 209 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 210 | t.Errorf("Unable to randomize Role struct: %s", err) 211 | } 212 | 213 | tx := MustTx(boil.Begin()) 214 | defer func() { _ = tx.Rollback() }() 215 | if err = o.Insert(tx, boil.Infer()); err != nil { 216 | t.Error(err) 217 | } 218 | 219 | if x, err := Roles().One(tx); err != nil { 220 | t.Error(err) 221 | } else if x == nil { 222 | t.Error("expected to get a non nil record") 223 | } 224 | } 225 | 226 | func testRolesAll(t *testing.T) { 227 | t.Parallel() 228 | 229 | seed := randomize.NewSeed() 230 | var err error 231 | roleOne := &Role{} 232 | roleTwo := &Role{} 233 | if err = randomize.Struct(seed, roleOne, roleDBTypes, false, roleColumnsWithDefault...); err != nil { 234 | t.Errorf("Unable to randomize Role struct: %s", err) 235 | } 236 | if err = randomize.Struct(seed, roleTwo, roleDBTypes, false, roleColumnsWithDefault...); err != nil { 237 | t.Errorf("Unable to randomize Role struct: %s", err) 238 | } 239 | 240 | tx := MustTx(boil.Begin()) 241 | defer func() { _ = tx.Rollback() }() 242 | if err = roleOne.Insert(tx, boil.Infer()); err != nil { 243 | t.Error(err) 244 | } 245 | if err = roleTwo.Insert(tx, boil.Infer()); err != nil { 246 | t.Error(err) 247 | } 248 | 249 | slice, err := Roles().All(tx) 250 | if err != nil { 251 | t.Error(err) 252 | } 253 | 254 | if len(slice) != 2 { 255 | t.Error("want 2 records, got:", len(slice)) 256 | } 257 | } 258 | 259 | func testRolesCount(t *testing.T) { 260 | t.Parallel() 261 | 262 | var err error 263 | seed := randomize.NewSeed() 264 | roleOne := &Role{} 265 | roleTwo := &Role{} 266 | if err = randomize.Struct(seed, roleOne, roleDBTypes, false, roleColumnsWithDefault...); err != nil { 267 | t.Errorf("Unable to randomize Role struct: %s", err) 268 | } 269 | if err = randomize.Struct(seed, roleTwo, roleDBTypes, false, roleColumnsWithDefault...); err != nil { 270 | t.Errorf("Unable to randomize Role struct: %s", err) 271 | } 272 | 273 | tx := MustTx(boil.Begin()) 274 | defer func() { _ = tx.Rollback() }() 275 | if err = roleOne.Insert(tx, boil.Infer()); err != nil { 276 | t.Error(err) 277 | } 278 | if err = roleTwo.Insert(tx, boil.Infer()); err != nil { 279 | t.Error(err) 280 | } 281 | 282 | count, err := Roles().Count(tx) 283 | if err != nil { 284 | t.Error(err) 285 | } 286 | 287 | if count != 2 { 288 | t.Error("want 2 records, got:", count) 289 | } 290 | } 291 | 292 | func roleBeforeInsertHook(e boil.Executor, o *Role) error { 293 | *o = Role{} 294 | return nil 295 | } 296 | 297 | func roleAfterInsertHook(e boil.Executor, o *Role) error { 298 | *o = Role{} 299 | return nil 300 | } 301 | 302 | func roleAfterSelectHook(e boil.Executor, o *Role) error { 303 | *o = Role{} 304 | return nil 305 | } 306 | 307 | func roleBeforeUpdateHook(e boil.Executor, o *Role) error { 308 | *o = Role{} 309 | return nil 310 | } 311 | 312 | func roleAfterUpdateHook(e boil.Executor, o *Role) error { 313 | *o = Role{} 314 | return nil 315 | } 316 | 317 | func roleBeforeDeleteHook(e boil.Executor, o *Role) error { 318 | *o = Role{} 319 | return nil 320 | } 321 | 322 | func roleAfterDeleteHook(e boil.Executor, o *Role) error { 323 | *o = Role{} 324 | return nil 325 | } 326 | 327 | func roleBeforeUpsertHook(e boil.Executor, o *Role) error { 328 | *o = Role{} 329 | return nil 330 | } 331 | 332 | func roleAfterUpsertHook(e boil.Executor, o *Role) error { 333 | *o = Role{} 334 | return nil 335 | } 336 | 337 | func testRolesHooks(t *testing.T) { 338 | t.Parallel() 339 | 340 | var err error 341 | 342 | empty := &Role{} 343 | o := &Role{} 344 | 345 | seed := randomize.NewSeed() 346 | if err = randomize.Struct(seed, o, roleDBTypes, false); err != nil { 347 | t.Errorf("Unable to randomize Role object: %s", err) 348 | } 349 | 350 | AddRoleHook(boil.BeforeInsertHook, roleBeforeInsertHook) 351 | if err = o.doBeforeInsertHooks(nil); err != nil { 352 | t.Errorf("Unable to execute doBeforeInsertHooks: %s", err) 353 | } 354 | if !reflect.DeepEqual(o, empty) { 355 | t.Errorf("Expected BeforeInsertHook function to empty object, but got: %#v", o) 356 | } 357 | roleBeforeInsertHooks = []RoleHook{} 358 | 359 | AddRoleHook(boil.AfterInsertHook, roleAfterInsertHook) 360 | if err = o.doAfterInsertHooks(nil); err != nil { 361 | t.Errorf("Unable to execute doAfterInsertHooks: %s", err) 362 | } 363 | if !reflect.DeepEqual(o, empty) { 364 | t.Errorf("Expected AfterInsertHook function to empty object, but got: %#v", o) 365 | } 366 | roleAfterInsertHooks = []RoleHook{} 367 | 368 | AddRoleHook(boil.AfterSelectHook, roleAfterSelectHook) 369 | if err = o.doAfterSelectHooks(nil); err != nil { 370 | t.Errorf("Unable to execute doAfterSelectHooks: %s", err) 371 | } 372 | if !reflect.DeepEqual(o, empty) { 373 | t.Errorf("Expected AfterSelectHook function to empty object, but got: %#v", o) 374 | } 375 | roleAfterSelectHooks = []RoleHook{} 376 | 377 | AddRoleHook(boil.BeforeUpdateHook, roleBeforeUpdateHook) 378 | if err = o.doBeforeUpdateHooks(nil); err != nil { 379 | t.Errorf("Unable to execute doBeforeUpdateHooks: %s", err) 380 | } 381 | if !reflect.DeepEqual(o, empty) { 382 | t.Errorf("Expected BeforeUpdateHook function to empty object, but got: %#v", o) 383 | } 384 | roleBeforeUpdateHooks = []RoleHook{} 385 | 386 | AddRoleHook(boil.AfterUpdateHook, roleAfterUpdateHook) 387 | if err = o.doAfterUpdateHooks(nil); err != nil { 388 | t.Errorf("Unable to execute doAfterUpdateHooks: %s", err) 389 | } 390 | if !reflect.DeepEqual(o, empty) { 391 | t.Errorf("Expected AfterUpdateHook function to empty object, but got: %#v", o) 392 | } 393 | roleAfterUpdateHooks = []RoleHook{} 394 | 395 | AddRoleHook(boil.BeforeDeleteHook, roleBeforeDeleteHook) 396 | if err = o.doBeforeDeleteHooks(nil); err != nil { 397 | t.Errorf("Unable to execute doBeforeDeleteHooks: %s", err) 398 | } 399 | if !reflect.DeepEqual(o, empty) { 400 | t.Errorf("Expected BeforeDeleteHook function to empty object, but got: %#v", o) 401 | } 402 | roleBeforeDeleteHooks = []RoleHook{} 403 | 404 | AddRoleHook(boil.AfterDeleteHook, roleAfterDeleteHook) 405 | if err = o.doAfterDeleteHooks(nil); err != nil { 406 | t.Errorf("Unable to execute doAfterDeleteHooks: %s", err) 407 | } 408 | if !reflect.DeepEqual(o, empty) { 409 | t.Errorf("Expected AfterDeleteHook function to empty object, but got: %#v", o) 410 | } 411 | roleAfterDeleteHooks = []RoleHook{} 412 | 413 | AddRoleHook(boil.BeforeUpsertHook, roleBeforeUpsertHook) 414 | if err = o.doBeforeUpsertHooks(nil); err != nil { 415 | t.Errorf("Unable to execute doBeforeUpsertHooks: %s", err) 416 | } 417 | if !reflect.DeepEqual(o, empty) { 418 | t.Errorf("Expected BeforeUpsertHook function to empty object, but got: %#v", o) 419 | } 420 | roleBeforeUpsertHooks = []RoleHook{} 421 | 422 | AddRoleHook(boil.AfterUpsertHook, roleAfterUpsertHook) 423 | if err = o.doAfterUpsertHooks(nil); err != nil { 424 | t.Errorf("Unable to execute doAfterUpsertHooks: %s", err) 425 | } 426 | if !reflect.DeepEqual(o, empty) { 427 | t.Errorf("Expected AfterUpsertHook function to empty object, but got: %#v", o) 428 | } 429 | roleAfterUpsertHooks = []RoleHook{} 430 | } 431 | 432 | func testRolesInsert(t *testing.T) { 433 | t.Parallel() 434 | 435 | seed := randomize.NewSeed() 436 | var err error 437 | o := &Role{} 438 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 439 | t.Errorf("Unable to randomize Role struct: %s", err) 440 | } 441 | 442 | tx := MustTx(boil.Begin()) 443 | defer func() { _ = tx.Rollback() }() 444 | if err = o.Insert(tx, boil.Infer()); err != nil { 445 | t.Error(err) 446 | } 447 | 448 | count, err := Roles().Count(tx) 449 | if err != nil { 450 | t.Error(err) 451 | } 452 | 453 | if count != 1 { 454 | t.Error("want one record, got:", count) 455 | } 456 | } 457 | 458 | func testRolesInsertWhitelist(t *testing.T) { 459 | t.Parallel() 460 | 461 | seed := randomize.NewSeed() 462 | var err error 463 | o := &Role{} 464 | if err = randomize.Struct(seed, o, roleDBTypes, true); err != nil { 465 | t.Errorf("Unable to randomize Role struct: %s", err) 466 | } 467 | 468 | tx := MustTx(boil.Begin()) 469 | defer func() { _ = tx.Rollback() }() 470 | if err = o.Insert(tx, boil.Whitelist(roleColumnsWithoutDefault...)); err != nil { 471 | t.Error(err) 472 | } 473 | 474 | count, err := Roles().Count(tx) 475 | if err != nil { 476 | t.Error(err) 477 | } 478 | 479 | if count != 1 { 480 | t.Error("want one record, got:", count) 481 | } 482 | } 483 | 484 | func testRoleToManyUsers(t *testing.T) { 485 | var err error 486 | 487 | tx := MustTx(boil.Begin()) 488 | defer func() { _ = tx.Rollback() }() 489 | 490 | var a Role 491 | var b, c User 492 | 493 | seed := randomize.NewSeed() 494 | if err = randomize.Struct(seed, &a, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 495 | t.Errorf("Unable to randomize Role struct: %s", err) 496 | } 497 | 498 | if err := a.Insert(tx, boil.Infer()); err != nil { 499 | t.Fatal(err) 500 | } 501 | 502 | if err = randomize.Struct(seed, &b, userDBTypes, false, userColumnsWithDefault...); err != nil { 503 | t.Fatal(err) 504 | } 505 | if err = randomize.Struct(seed, &c, userDBTypes, false, userColumnsWithDefault...); err != nil { 506 | t.Fatal(err) 507 | } 508 | 509 | b.RoleID = a.ID 510 | c.RoleID = a.ID 511 | 512 | if err = b.Insert(tx, boil.Infer()); err != nil { 513 | t.Fatal(err) 514 | } 515 | if err = c.Insert(tx, boil.Infer()); err != nil { 516 | t.Fatal(err) 517 | } 518 | 519 | user, err := a.Users().All(tx) 520 | if err != nil { 521 | t.Fatal(err) 522 | } 523 | 524 | bFound, cFound := false, false 525 | for _, v := range user { 526 | if v.RoleID == b.RoleID { 527 | bFound = true 528 | } 529 | if v.RoleID == c.RoleID { 530 | cFound = true 531 | } 532 | } 533 | 534 | if !bFound { 535 | t.Error("expected to find b") 536 | } 537 | if !cFound { 538 | t.Error("expected to find c") 539 | } 540 | 541 | slice := RoleSlice{&a} 542 | if err = a.L.LoadUsers(tx, false, (*[]*Role)(&slice), nil); err != nil { 543 | t.Fatal(err) 544 | } 545 | if got := len(a.R.Users); got != 2 { 546 | t.Error("number of eager loaded records wrong, got:", got) 547 | } 548 | 549 | a.R.Users = nil 550 | if err = a.L.LoadUsers(tx, true, &a, nil); err != nil { 551 | t.Fatal(err) 552 | } 553 | if got := len(a.R.Users); got != 2 { 554 | t.Error("number of eager loaded records wrong, got:", got) 555 | } 556 | 557 | if t.Failed() { 558 | t.Logf("%#v", user) 559 | } 560 | } 561 | 562 | func testRoleToManyAddOpUsers(t *testing.T) { 563 | var err error 564 | 565 | tx := MustTx(boil.Begin()) 566 | defer func() { _ = tx.Rollback() }() 567 | 568 | var a Role 569 | var b, c, d, e User 570 | 571 | seed := randomize.NewSeed() 572 | if err = randomize.Struct(seed, &a, roleDBTypes, false, strmangle.SetComplement(rolePrimaryKeyColumns, roleColumnsWithoutDefault)...); err != nil { 573 | t.Fatal(err) 574 | } 575 | foreigners := []*User{&b, &c, &d, &e} 576 | for _, x := range foreigners { 577 | if err = randomize.Struct(seed, x, userDBTypes, false, strmangle.SetComplement(userPrimaryKeyColumns, userColumnsWithoutDefault)...); err != nil { 578 | t.Fatal(err) 579 | } 580 | } 581 | 582 | if err := a.Insert(tx, boil.Infer()); err != nil { 583 | t.Fatal(err) 584 | } 585 | if err = b.Insert(tx, boil.Infer()); err != nil { 586 | t.Fatal(err) 587 | } 588 | if err = c.Insert(tx, boil.Infer()); err != nil { 589 | t.Fatal(err) 590 | } 591 | 592 | foreignersSplitByInsertion := [][]*User{ 593 | {&b, &c}, 594 | {&d, &e}, 595 | } 596 | 597 | for i, x := range foreignersSplitByInsertion { 598 | err = a.AddUsers(tx, i != 0, x...) 599 | if err != nil { 600 | t.Fatal(err) 601 | } 602 | 603 | first := x[0] 604 | second := x[1] 605 | 606 | if a.ID != first.RoleID { 607 | t.Error("foreign key was wrong value", a.ID, first.RoleID) 608 | } 609 | if a.ID != second.RoleID { 610 | t.Error("foreign key was wrong value", a.ID, second.RoleID) 611 | } 612 | 613 | if first.R.Role != &a { 614 | t.Error("relationship was not added properly to the foreign slice") 615 | } 616 | if second.R.Role != &a { 617 | t.Error("relationship was not added properly to the foreign slice") 618 | } 619 | 620 | if a.R.Users[i*2] != first { 621 | t.Error("relationship struct slice not set to correct value") 622 | } 623 | if a.R.Users[i*2+1] != second { 624 | t.Error("relationship struct slice not set to correct value") 625 | } 626 | 627 | count, err := a.Users().Count(tx) 628 | if err != nil { 629 | t.Fatal(err) 630 | } 631 | if want := int64((i + 1) * 2); count != want { 632 | t.Error("want", want, "got", count) 633 | } 634 | } 635 | } 636 | 637 | func testRolesReload(t *testing.T) { 638 | t.Parallel() 639 | 640 | seed := randomize.NewSeed() 641 | var err error 642 | o := &Role{} 643 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 644 | t.Errorf("Unable to randomize Role struct: %s", err) 645 | } 646 | 647 | tx := MustTx(boil.Begin()) 648 | defer func() { _ = tx.Rollback() }() 649 | if err = o.Insert(tx, boil.Infer()); err != nil { 650 | t.Error(err) 651 | } 652 | 653 | if err = o.Reload(tx); err != nil { 654 | t.Error(err) 655 | } 656 | } 657 | 658 | func testRolesReloadAll(t *testing.T) { 659 | t.Parallel() 660 | 661 | seed := randomize.NewSeed() 662 | var err error 663 | o := &Role{} 664 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 665 | t.Errorf("Unable to randomize Role struct: %s", err) 666 | } 667 | 668 | tx := MustTx(boil.Begin()) 669 | defer func() { _ = tx.Rollback() }() 670 | if err = o.Insert(tx, boil.Infer()); err != nil { 671 | t.Error(err) 672 | } 673 | 674 | slice := RoleSlice{o} 675 | 676 | if err = slice.ReloadAll(tx); err != nil { 677 | t.Error(err) 678 | } 679 | } 680 | 681 | func testRolesSelect(t *testing.T) { 682 | t.Parallel() 683 | 684 | seed := randomize.NewSeed() 685 | var err error 686 | o := &Role{} 687 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 688 | t.Errorf("Unable to randomize Role struct: %s", err) 689 | } 690 | 691 | tx := MustTx(boil.Begin()) 692 | defer func() { _ = tx.Rollback() }() 693 | if err = o.Insert(tx, boil.Infer()); err != nil { 694 | t.Error(err) 695 | } 696 | 697 | slice, err := Roles().All(tx) 698 | if err != nil { 699 | t.Error(err) 700 | } 701 | 702 | if len(slice) != 1 { 703 | t.Error("want one record, got:", len(slice)) 704 | } 705 | } 706 | 707 | var ( 708 | roleDBTypes = map[string]string{`ID`: `integer`, `Name`: `text`} 709 | _ = bytes.MinRead 710 | ) 711 | 712 | func testRolesUpdate(t *testing.T) { 713 | t.Parallel() 714 | 715 | if 0 == len(rolePrimaryKeyColumns) { 716 | t.Skip("Skipping table with no primary key columns") 717 | } 718 | if len(roleColumns) == len(rolePrimaryKeyColumns) { 719 | t.Skip("Skipping table with only primary key columns") 720 | } 721 | 722 | seed := randomize.NewSeed() 723 | var err error 724 | o := &Role{} 725 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 726 | t.Errorf("Unable to randomize Role struct: %s", err) 727 | } 728 | 729 | tx := MustTx(boil.Begin()) 730 | defer func() { _ = tx.Rollback() }() 731 | if err = o.Insert(tx, boil.Infer()); err != nil { 732 | t.Error(err) 733 | } 734 | 735 | count, err := Roles().Count(tx) 736 | if err != nil { 737 | t.Error(err) 738 | } 739 | 740 | if count != 1 { 741 | t.Error("want one record, got:", count) 742 | } 743 | 744 | if err = randomize.Struct(seed, o, roleDBTypes, true, rolePrimaryKeyColumns...); err != nil { 745 | t.Errorf("Unable to randomize Role struct: %s", err) 746 | } 747 | 748 | if rowsAff, err := o.Update(tx, boil.Infer()); err != nil { 749 | t.Error(err) 750 | } else if rowsAff != 1 { 751 | t.Error("should only affect one row but affected", rowsAff) 752 | } 753 | } 754 | 755 | func testRolesSliceUpdateAll(t *testing.T) { 756 | t.Parallel() 757 | 758 | if len(roleColumns) == len(rolePrimaryKeyColumns) { 759 | t.Skip("Skipping table with only primary key columns") 760 | } 761 | 762 | seed := randomize.NewSeed() 763 | var err error 764 | o := &Role{} 765 | if err = randomize.Struct(seed, o, roleDBTypes, true, roleColumnsWithDefault...); err != nil { 766 | t.Errorf("Unable to randomize Role struct: %s", err) 767 | } 768 | 769 | tx := MustTx(boil.Begin()) 770 | defer func() { _ = tx.Rollback() }() 771 | if err = o.Insert(tx, boil.Infer()); err != nil { 772 | t.Error(err) 773 | } 774 | 775 | count, err := Roles().Count(tx) 776 | if err != nil { 777 | t.Error(err) 778 | } 779 | 780 | if count != 1 { 781 | t.Error("want one record, got:", count) 782 | } 783 | 784 | if err = randomize.Struct(seed, o, roleDBTypes, true, rolePrimaryKeyColumns...); err != nil { 785 | t.Errorf("Unable to randomize Role struct: %s", err) 786 | } 787 | 788 | // Remove Primary keys and unique columns from what we plan to update 789 | var fields []string 790 | if strmangle.StringSliceMatch(roleColumns, rolePrimaryKeyColumns) { 791 | fields = roleColumns 792 | } else { 793 | fields = strmangle.SetComplement( 794 | roleColumns, 795 | rolePrimaryKeyColumns, 796 | ) 797 | } 798 | 799 | value := reflect.Indirect(reflect.ValueOf(o)) 800 | typ := reflect.TypeOf(o).Elem() 801 | n := typ.NumField() 802 | 803 | updateMap := M{} 804 | for _, col := range fields { 805 | for i := 0; i < n; i++ { 806 | f := typ.Field(i) 807 | if f.Tag.Get("boil") == col { 808 | updateMap[col] = value.Field(i).Interface() 809 | } 810 | } 811 | } 812 | 813 | slice := RoleSlice{o} 814 | if rowsAff, err := slice.UpdateAll(tx, updateMap); err != nil { 815 | t.Error(err) 816 | } else if rowsAff != 1 { 817 | t.Error("wanted one record updated but got", rowsAff) 818 | } 819 | } 820 | 821 | func testRolesUpsert(t *testing.T) { 822 | t.Parallel() 823 | 824 | if len(roleColumns) == len(rolePrimaryKeyColumns) { 825 | t.Skip("Skipping table with only primary key columns") 826 | } 827 | 828 | seed := randomize.NewSeed() 829 | var err error 830 | // Attempt the INSERT side of an UPSERT 831 | o := Role{} 832 | if err = randomize.Struct(seed, &o, roleDBTypes, true); err != nil { 833 | t.Errorf("Unable to randomize Role struct: %s", err) 834 | } 835 | 836 | tx := MustTx(boil.Begin()) 837 | defer func() { _ = tx.Rollback() }() 838 | if err = o.Upsert(tx, false, nil, boil.Infer(), boil.Infer()); err != nil { 839 | t.Errorf("Unable to upsert Role: %s", err) 840 | } 841 | 842 | count, err := Roles().Count(tx) 843 | if err != nil { 844 | t.Error(err) 845 | } 846 | if count != 1 { 847 | t.Error("want one record, got:", count) 848 | } 849 | 850 | // Attempt the UPDATE side of an UPSERT 851 | if err = randomize.Struct(seed, &o, roleDBTypes, false, rolePrimaryKeyColumns...); err != nil { 852 | t.Errorf("Unable to randomize Role struct: %s", err) 853 | } 854 | 855 | if err = o.Upsert(tx, true, nil, boil.Infer(), boil.Infer()); err != nil { 856 | t.Errorf("Unable to upsert Role: %s", err) 857 | } 858 | 859 | count, err = Roles().Count(tx) 860 | if err != nil { 861 | t.Error(err) 862 | } 863 | if count != 1 { 864 | t.Error("want one record, got:", count) 865 | } 866 | } 867 | -------------------------------------------------------------------------------- /models/subscriptions_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | import ( 7 | "bytes" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/volatiletech/sqlboiler/boil" 12 | "github.com/volatiletech/sqlboiler/queries" 13 | "github.com/volatiletech/sqlboiler/randomize" 14 | "github.com/volatiletech/sqlboiler/strmangle" 15 | ) 16 | 17 | var ( 18 | // Relationships sometimes use the reflection helper queries.Equal/queries.Assign 19 | // so force a package dependency in case they don't. 20 | _ = queries.Equal 21 | ) 22 | 23 | func testSubscriptions(t *testing.T) { 24 | t.Parallel() 25 | 26 | query := Subscriptions() 27 | 28 | if query.Query == nil { 29 | t.Error("expected a query, got nothing") 30 | } 31 | } 32 | 33 | func testSubscriptionsDelete(t *testing.T) { 34 | t.Parallel() 35 | 36 | seed := randomize.NewSeed() 37 | var err error 38 | o := &Subscription{} 39 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 40 | t.Errorf("Unable to randomize Subscription struct: %s", err) 41 | } 42 | 43 | tx := MustTx(boil.Begin()) 44 | defer func() { _ = tx.Rollback() }() 45 | if err = o.Insert(tx, boil.Infer()); err != nil { 46 | t.Error(err) 47 | } 48 | 49 | if rowsAff, err := o.Delete(tx); err != nil { 50 | t.Error(err) 51 | } else if rowsAff != 1 { 52 | t.Error("should only have deleted one row, but affected:", rowsAff) 53 | } 54 | 55 | count, err := Subscriptions().Count(tx) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | 60 | if count != 0 { 61 | t.Error("want zero records, got:", count) 62 | } 63 | } 64 | 65 | func testSubscriptionsQueryDeleteAll(t *testing.T) { 66 | t.Parallel() 67 | 68 | seed := randomize.NewSeed() 69 | var err error 70 | o := &Subscription{} 71 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 72 | t.Errorf("Unable to randomize Subscription struct: %s", err) 73 | } 74 | 75 | tx := MustTx(boil.Begin()) 76 | defer func() { _ = tx.Rollback() }() 77 | if err = o.Insert(tx, boil.Infer()); err != nil { 78 | t.Error(err) 79 | } 80 | 81 | if rowsAff, err := Subscriptions().DeleteAll(tx); err != nil { 82 | t.Error(err) 83 | } else if rowsAff != 1 { 84 | t.Error("should only have deleted one row, but affected:", rowsAff) 85 | } 86 | 87 | count, err := Subscriptions().Count(tx) 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | 92 | if count != 0 { 93 | t.Error("want zero records, got:", count) 94 | } 95 | } 96 | 97 | func testSubscriptionsSliceDeleteAll(t *testing.T) { 98 | t.Parallel() 99 | 100 | seed := randomize.NewSeed() 101 | var err error 102 | o := &Subscription{} 103 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 104 | t.Errorf("Unable to randomize Subscription struct: %s", err) 105 | } 106 | 107 | tx := MustTx(boil.Begin()) 108 | defer func() { _ = tx.Rollback() }() 109 | if err = o.Insert(tx, boil.Infer()); err != nil { 110 | t.Error(err) 111 | } 112 | 113 | slice := SubscriptionSlice{o} 114 | 115 | if rowsAff, err := slice.DeleteAll(tx); err != nil { 116 | t.Error(err) 117 | } else if rowsAff != 1 { 118 | t.Error("should only have deleted one row, but affected:", rowsAff) 119 | } 120 | 121 | count, err := Subscriptions().Count(tx) 122 | if err != nil { 123 | t.Error(err) 124 | } 125 | 126 | if count != 0 { 127 | t.Error("want zero records, got:", count) 128 | } 129 | } 130 | 131 | func testSubscriptionsExists(t *testing.T) { 132 | t.Parallel() 133 | 134 | seed := randomize.NewSeed() 135 | var err error 136 | o := &Subscription{} 137 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 138 | t.Errorf("Unable to randomize Subscription struct: %s", err) 139 | } 140 | 141 | tx := MustTx(boil.Begin()) 142 | defer func() { _ = tx.Rollback() }() 143 | if err = o.Insert(tx, boil.Infer()); err != nil { 144 | t.Error(err) 145 | } 146 | 147 | e, err := SubscriptionExists(tx, o.ID) 148 | if err != nil { 149 | t.Errorf("Unable to check if Subscription exists: %s", err) 150 | } 151 | if !e { 152 | t.Errorf("Expected SubscriptionExists to return true, but got false.") 153 | } 154 | } 155 | 156 | func testSubscriptionsFind(t *testing.T) { 157 | t.Parallel() 158 | 159 | seed := randomize.NewSeed() 160 | var err error 161 | o := &Subscription{} 162 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 163 | t.Errorf("Unable to randomize Subscription struct: %s", err) 164 | } 165 | 166 | tx := MustTx(boil.Begin()) 167 | defer func() { _ = tx.Rollback() }() 168 | if err = o.Insert(tx, boil.Infer()); err != nil { 169 | t.Error(err) 170 | } 171 | 172 | subscriptionFound, err := FindSubscription(tx, o.ID) 173 | if err != nil { 174 | t.Error(err) 175 | } 176 | 177 | if subscriptionFound == nil { 178 | t.Error("want a record, got nil") 179 | } 180 | } 181 | 182 | func testSubscriptionsBind(t *testing.T) { 183 | t.Parallel() 184 | 185 | seed := randomize.NewSeed() 186 | var err error 187 | o := &Subscription{} 188 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 189 | t.Errorf("Unable to randomize Subscription struct: %s", err) 190 | } 191 | 192 | tx := MustTx(boil.Begin()) 193 | defer func() { _ = tx.Rollback() }() 194 | if err = o.Insert(tx, boil.Infer()); err != nil { 195 | t.Error(err) 196 | } 197 | 198 | if err = Subscriptions().Bind(nil, tx, o); err != nil { 199 | t.Error(err) 200 | } 201 | } 202 | 203 | func testSubscriptionsOne(t *testing.T) { 204 | t.Parallel() 205 | 206 | seed := randomize.NewSeed() 207 | var err error 208 | o := &Subscription{} 209 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 210 | t.Errorf("Unable to randomize Subscription struct: %s", err) 211 | } 212 | 213 | tx := MustTx(boil.Begin()) 214 | defer func() { _ = tx.Rollback() }() 215 | if err = o.Insert(tx, boil.Infer()); err != nil { 216 | t.Error(err) 217 | } 218 | 219 | if x, err := Subscriptions().One(tx); err != nil { 220 | t.Error(err) 221 | } else if x == nil { 222 | t.Error("expected to get a non nil record") 223 | } 224 | } 225 | 226 | func testSubscriptionsAll(t *testing.T) { 227 | t.Parallel() 228 | 229 | seed := randomize.NewSeed() 230 | var err error 231 | subscriptionOne := &Subscription{} 232 | subscriptionTwo := &Subscription{} 233 | if err = randomize.Struct(seed, subscriptionOne, subscriptionDBTypes, false, subscriptionColumnsWithDefault...); err != nil { 234 | t.Errorf("Unable to randomize Subscription struct: %s", err) 235 | } 236 | if err = randomize.Struct(seed, subscriptionTwo, subscriptionDBTypes, false, subscriptionColumnsWithDefault...); err != nil { 237 | t.Errorf("Unable to randomize Subscription struct: %s", err) 238 | } 239 | 240 | tx := MustTx(boil.Begin()) 241 | defer func() { _ = tx.Rollback() }() 242 | if err = subscriptionOne.Insert(tx, boil.Infer()); err != nil { 243 | t.Error(err) 244 | } 245 | if err = subscriptionTwo.Insert(tx, boil.Infer()); err != nil { 246 | t.Error(err) 247 | } 248 | 249 | slice, err := Subscriptions().All(tx) 250 | if err != nil { 251 | t.Error(err) 252 | } 253 | 254 | if len(slice) != 2 { 255 | t.Error("want 2 records, got:", len(slice)) 256 | } 257 | } 258 | 259 | func testSubscriptionsCount(t *testing.T) { 260 | t.Parallel() 261 | 262 | var err error 263 | seed := randomize.NewSeed() 264 | subscriptionOne := &Subscription{} 265 | subscriptionTwo := &Subscription{} 266 | if err = randomize.Struct(seed, subscriptionOne, subscriptionDBTypes, false, subscriptionColumnsWithDefault...); err != nil { 267 | t.Errorf("Unable to randomize Subscription struct: %s", err) 268 | } 269 | if err = randomize.Struct(seed, subscriptionTwo, subscriptionDBTypes, false, subscriptionColumnsWithDefault...); err != nil { 270 | t.Errorf("Unable to randomize Subscription struct: %s", err) 271 | } 272 | 273 | tx := MustTx(boil.Begin()) 274 | defer func() { _ = tx.Rollback() }() 275 | if err = subscriptionOne.Insert(tx, boil.Infer()); err != nil { 276 | t.Error(err) 277 | } 278 | if err = subscriptionTwo.Insert(tx, boil.Infer()); err != nil { 279 | t.Error(err) 280 | } 281 | 282 | count, err := Subscriptions().Count(tx) 283 | if err != nil { 284 | t.Error(err) 285 | } 286 | 287 | if count != 2 { 288 | t.Error("want 2 records, got:", count) 289 | } 290 | } 291 | 292 | func subscriptionBeforeInsertHook(e boil.Executor, o *Subscription) error { 293 | *o = Subscription{} 294 | return nil 295 | } 296 | 297 | func subscriptionAfterInsertHook(e boil.Executor, o *Subscription) error { 298 | *o = Subscription{} 299 | return nil 300 | } 301 | 302 | func subscriptionAfterSelectHook(e boil.Executor, o *Subscription) error { 303 | *o = Subscription{} 304 | return nil 305 | } 306 | 307 | func subscriptionBeforeUpdateHook(e boil.Executor, o *Subscription) error { 308 | *o = Subscription{} 309 | return nil 310 | } 311 | 312 | func subscriptionAfterUpdateHook(e boil.Executor, o *Subscription) error { 313 | *o = Subscription{} 314 | return nil 315 | } 316 | 317 | func subscriptionBeforeDeleteHook(e boil.Executor, o *Subscription) error { 318 | *o = Subscription{} 319 | return nil 320 | } 321 | 322 | func subscriptionAfterDeleteHook(e boil.Executor, o *Subscription) error { 323 | *o = Subscription{} 324 | return nil 325 | } 326 | 327 | func subscriptionBeforeUpsertHook(e boil.Executor, o *Subscription) error { 328 | *o = Subscription{} 329 | return nil 330 | } 331 | 332 | func subscriptionAfterUpsertHook(e boil.Executor, o *Subscription) error { 333 | *o = Subscription{} 334 | return nil 335 | } 336 | 337 | func testSubscriptionsHooks(t *testing.T) { 338 | t.Parallel() 339 | 340 | var err error 341 | 342 | empty := &Subscription{} 343 | o := &Subscription{} 344 | 345 | seed := randomize.NewSeed() 346 | if err = randomize.Struct(seed, o, subscriptionDBTypes, false); err != nil { 347 | t.Errorf("Unable to randomize Subscription object: %s", err) 348 | } 349 | 350 | AddSubscriptionHook(boil.BeforeInsertHook, subscriptionBeforeInsertHook) 351 | if err = o.doBeforeInsertHooks(nil); err != nil { 352 | t.Errorf("Unable to execute doBeforeInsertHooks: %s", err) 353 | } 354 | if !reflect.DeepEqual(o, empty) { 355 | t.Errorf("Expected BeforeInsertHook function to empty object, but got: %#v", o) 356 | } 357 | subscriptionBeforeInsertHooks = []SubscriptionHook{} 358 | 359 | AddSubscriptionHook(boil.AfterInsertHook, subscriptionAfterInsertHook) 360 | if err = o.doAfterInsertHooks(nil); err != nil { 361 | t.Errorf("Unable to execute doAfterInsertHooks: %s", err) 362 | } 363 | if !reflect.DeepEqual(o, empty) { 364 | t.Errorf("Expected AfterInsertHook function to empty object, but got: %#v", o) 365 | } 366 | subscriptionAfterInsertHooks = []SubscriptionHook{} 367 | 368 | AddSubscriptionHook(boil.AfterSelectHook, subscriptionAfterSelectHook) 369 | if err = o.doAfterSelectHooks(nil); err != nil { 370 | t.Errorf("Unable to execute doAfterSelectHooks: %s", err) 371 | } 372 | if !reflect.DeepEqual(o, empty) { 373 | t.Errorf("Expected AfterSelectHook function to empty object, but got: %#v", o) 374 | } 375 | subscriptionAfterSelectHooks = []SubscriptionHook{} 376 | 377 | AddSubscriptionHook(boil.BeforeUpdateHook, subscriptionBeforeUpdateHook) 378 | if err = o.doBeforeUpdateHooks(nil); err != nil { 379 | t.Errorf("Unable to execute doBeforeUpdateHooks: %s", err) 380 | } 381 | if !reflect.DeepEqual(o, empty) { 382 | t.Errorf("Expected BeforeUpdateHook function to empty object, but got: %#v", o) 383 | } 384 | subscriptionBeforeUpdateHooks = []SubscriptionHook{} 385 | 386 | AddSubscriptionHook(boil.AfterUpdateHook, subscriptionAfterUpdateHook) 387 | if err = o.doAfterUpdateHooks(nil); err != nil { 388 | t.Errorf("Unable to execute doAfterUpdateHooks: %s", err) 389 | } 390 | if !reflect.DeepEqual(o, empty) { 391 | t.Errorf("Expected AfterUpdateHook function to empty object, but got: %#v", o) 392 | } 393 | subscriptionAfterUpdateHooks = []SubscriptionHook{} 394 | 395 | AddSubscriptionHook(boil.BeforeDeleteHook, subscriptionBeforeDeleteHook) 396 | if err = o.doBeforeDeleteHooks(nil); err != nil { 397 | t.Errorf("Unable to execute doBeforeDeleteHooks: %s", err) 398 | } 399 | if !reflect.DeepEqual(o, empty) { 400 | t.Errorf("Expected BeforeDeleteHook function to empty object, but got: %#v", o) 401 | } 402 | subscriptionBeforeDeleteHooks = []SubscriptionHook{} 403 | 404 | AddSubscriptionHook(boil.AfterDeleteHook, subscriptionAfterDeleteHook) 405 | if err = o.doAfterDeleteHooks(nil); err != nil { 406 | t.Errorf("Unable to execute doAfterDeleteHooks: %s", err) 407 | } 408 | if !reflect.DeepEqual(o, empty) { 409 | t.Errorf("Expected AfterDeleteHook function to empty object, but got: %#v", o) 410 | } 411 | subscriptionAfterDeleteHooks = []SubscriptionHook{} 412 | 413 | AddSubscriptionHook(boil.BeforeUpsertHook, subscriptionBeforeUpsertHook) 414 | if err = o.doBeforeUpsertHooks(nil); err != nil { 415 | t.Errorf("Unable to execute doBeforeUpsertHooks: %s", err) 416 | } 417 | if !reflect.DeepEqual(o, empty) { 418 | t.Errorf("Expected BeforeUpsertHook function to empty object, but got: %#v", o) 419 | } 420 | subscriptionBeforeUpsertHooks = []SubscriptionHook{} 421 | 422 | AddSubscriptionHook(boil.AfterUpsertHook, subscriptionAfterUpsertHook) 423 | if err = o.doAfterUpsertHooks(nil); err != nil { 424 | t.Errorf("Unable to execute doAfterUpsertHooks: %s", err) 425 | } 426 | if !reflect.DeepEqual(o, empty) { 427 | t.Errorf("Expected AfterUpsertHook function to empty object, but got: %#v", o) 428 | } 429 | subscriptionAfterUpsertHooks = []SubscriptionHook{} 430 | } 431 | 432 | func testSubscriptionsInsert(t *testing.T) { 433 | t.Parallel() 434 | 435 | seed := randomize.NewSeed() 436 | var err error 437 | o := &Subscription{} 438 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 439 | t.Errorf("Unable to randomize Subscription struct: %s", err) 440 | } 441 | 442 | tx := MustTx(boil.Begin()) 443 | defer func() { _ = tx.Rollback() }() 444 | if err = o.Insert(tx, boil.Infer()); err != nil { 445 | t.Error(err) 446 | } 447 | 448 | count, err := Subscriptions().Count(tx) 449 | if err != nil { 450 | t.Error(err) 451 | } 452 | 453 | if count != 1 { 454 | t.Error("want one record, got:", count) 455 | } 456 | } 457 | 458 | func testSubscriptionsInsertWhitelist(t *testing.T) { 459 | t.Parallel() 460 | 461 | seed := randomize.NewSeed() 462 | var err error 463 | o := &Subscription{} 464 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true); err != nil { 465 | t.Errorf("Unable to randomize Subscription struct: %s", err) 466 | } 467 | 468 | tx := MustTx(boil.Begin()) 469 | defer func() { _ = tx.Rollback() }() 470 | if err = o.Insert(tx, boil.Whitelist(subscriptionColumnsWithoutDefault...)); err != nil { 471 | t.Error(err) 472 | } 473 | 474 | count, err := Subscriptions().Count(tx) 475 | if err != nil { 476 | t.Error(err) 477 | } 478 | 479 | if count != 1 { 480 | t.Error("want one record, got:", count) 481 | } 482 | } 483 | 484 | func testSubscriptionToManyTenants(t *testing.T) { 485 | var err error 486 | 487 | tx := MustTx(boil.Begin()) 488 | defer func() { _ = tx.Rollback() }() 489 | 490 | var a Subscription 491 | var b, c Tenant 492 | 493 | seed := randomize.NewSeed() 494 | if err = randomize.Struct(seed, &a, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 495 | t.Errorf("Unable to randomize Subscription struct: %s", err) 496 | } 497 | 498 | if err := a.Insert(tx, boil.Infer()); err != nil { 499 | t.Fatal(err) 500 | } 501 | 502 | if err = randomize.Struct(seed, &b, tenantDBTypes, false, tenantColumnsWithDefault...); err != nil { 503 | t.Fatal(err) 504 | } 505 | if err = randomize.Struct(seed, &c, tenantDBTypes, false, tenantColumnsWithDefault...); err != nil { 506 | t.Fatal(err) 507 | } 508 | 509 | b.SubscriptionID = a.ID 510 | c.SubscriptionID = a.ID 511 | 512 | if err = b.Insert(tx, boil.Infer()); err != nil { 513 | t.Fatal(err) 514 | } 515 | if err = c.Insert(tx, boil.Infer()); err != nil { 516 | t.Fatal(err) 517 | } 518 | 519 | tenant, err := a.Tenants().All(tx) 520 | if err != nil { 521 | t.Fatal(err) 522 | } 523 | 524 | bFound, cFound := false, false 525 | for _, v := range tenant { 526 | if v.SubscriptionID == b.SubscriptionID { 527 | bFound = true 528 | } 529 | if v.SubscriptionID == c.SubscriptionID { 530 | cFound = true 531 | } 532 | } 533 | 534 | if !bFound { 535 | t.Error("expected to find b") 536 | } 537 | if !cFound { 538 | t.Error("expected to find c") 539 | } 540 | 541 | slice := SubscriptionSlice{&a} 542 | if err = a.L.LoadTenants(tx, false, (*[]*Subscription)(&slice), nil); err != nil { 543 | t.Fatal(err) 544 | } 545 | if got := len(a.R.Tenants); got != 2 { 546 | t.Error("number of eager loaded records wrong, got:", got) 547 | } 548 | 549 | a.R.Tenants = nil 550 | if err = a.L.LoadTenants(tx, true, &a, nil); err != nil { 551 | t.Fatal(err) 552 | } 553 | if got := len(a.R.Tenants); got != 2 { 554 | t.Error("number of eager loaded records wrong, got:", got) 555 | } 556 | 557 | if t.Failed() { 558 | t.Logf("%#v", tenant) 559 | } 560 | } 561 | 562 | func testSubscriptionToManyAddOpTenants(t *testing.T) { 563 | var err error 564 | 565 | tx := MustTx(boil.Begin()) 566 | defer func() { _ = tx.Rollback() }() 567 | 568 | var a Subscription 569 | var b, c, d, e Tenant 570 | 571 | seed := randomize.NewSeed() 572 | if err = randomize.Struct(seed, &a, subscriptionDBTypes, false, strmangle.SetComplement(subscriptionPrimaryKeyColumns, subscriptionColumnsWithoutDefault)...); err != nil { 573 | t.Fatal(err) 574 | } 575 | foreigners := []*Tenant{&b, &c, &d, &e} 576 | for _, x := range foreigners { 577 | if err = randomize.Struct(seed, x, tenantDBTypes, false, strmangle.SetComplement(tenantPrimaryKeyColumns, tenantColumnsWithoutDefault)...); err != nil { 578 | t.Fatal(err) 579 | } 580 | } 581 | 582 | if err := a.Insert(tx, boil.Infer()); err != nil { 583 | t.Fatal(err) 584 | } 585 | if err = b.Insert(tx, boil.Infer()); err != nil { 586 | t.Fatal(err) 587 | } 588 | if err = c.Insert(tx, boil.Infer()); err != nil { 589 | t.Fatal(err) 590 | } 591 | 592 | foreignersSplitByInsertion := [][]*Tenant{ 593 | {&b, &c}, 594 | {&d, &e}, 595 | } 596 | 597 | for i, x := range foreignersSplitByInsertion { 598 | err = a.AddTenants(tx, i != 0, x...) 599 | if err != nil { 600 | t.Fatal(err) 601 | } 602 | 603 | first := x[0] 604 | second := x[1] 605 | 606 | if a.ID != first.SubscriptionID { 607 | t.Error("foreign key was wrong value", a.ID, first.SubscriptionID) 608 | } 609 | if a.ID != second.SubscriptionID { 610 | t.Error("foreign key was wrong value", a.ID, second.SubscriptionID) 611 | } 612 | 613 | if first.R.Subscription != &a { 614 | t.Error("relationship was not added properly to the foreign slice") 615 | } 616 | if second.R.Subscription != &a { 617 | t.Error("relationship was not added properly to the foreign slice") 618 | } 619 | 620 | if a.R.Tenants[i*2] != first { 621 | t.Error("relationship struct slice not set to correct value") 622 | } 623 | if a.R.Tenants[i*2+1] != second { 624 | t.Error("relationship struct slice not set to correct value") 625 | } 626 | 627 | count, err := a.Tenants().Count(tx) 628 | if err != nil { 629 | t.Fatal(err) 630 | } 631 | if want := int64((i + 1) * 2); count != want { 632 | t.Error("want", want, "got", count) 633 | } 634 | } 635 | } 636 | 637 | func testSubscriptionsReload(t *testing.T) { 638 | t.Parallel() 639 | 640 | seed := randomize.NewSeed() 641 | var err error 642 | o := &Subscription{} 643 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 644 | t.Errorf("Unable to randomize Subscription struct: %s", err) 645 | } 646 | 647 | tx := MustTx(boil.Begin()) 648 | defer func() { _ = tx.Rollback() }() 649 | if err = o.Insert(tx, boil.Infer()); err != nil { 650 | t.Error(err) 651 | } 652 | 653 | if err = o.Reload(tx); err != nil { 654 | t.Error(err) 655 | } 656 | } 657 | 658 | func testSubscriptionsReloadAll(t *testing.T) { 659 | t.Parallel() 660 | 661 | seed := randomize.NewSeed() 662 | var err error 663 | o := &Subscription{} 664 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 665 | t.Errorf("Unable to randomize Subscription struct: %s", err) 666 | } 667 | 668 | tx := MustTx(boil.Begin()) 669 | defer func() { _ = tx.Rollback() }() 670 | if err = o.Insert(tx, boil.Infer()); err != nil { 671 | t.Error(err) 672 | } 673 | 674 | slice := SubscriptionSlice{o} 675 | 676 | if err = slice.ReloadAll(tx); err != nil { 677 | t.Error(err) 678 | } 679 | } 680 | 681 | func testSubscriptionsSelect(t *testing.T) { 682 | t.Parallel() 683 | 684 | seed := randomize.NewSeed() 685 | var err error 686 | o := &Subscription{} 687 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 688 | t.Errorf("Unable to randomize Subscription struct: %s", err) 689 | } 690 | 691 | tx := MustTx(boil.Begin()) 692 | defer func() { _ = tx.Rollback() }() 693 | if err = o.Insert(tx, boil.Infer()); err != nil { 694 | t.Error(err) 695 | } 696 | 697 | slice, err := Subscriptions().All(tx) 698 | if err != nil { 699 | t.Error(err) 700 | } 701 | 702 | if len(slice) != 1 { 703 | t.Error("want one record, got:", len(slice)) 704 | } 705 | } 706 | 707 | var ( 708 | subscriptionDBTypes = map[string]string{`Description`: `character varying`, `ID`: `integer`, `Name`: `character varying`, `Price`: `numeric`, `SubscriptionID`: `character varying`, `TrialDuration`: `integer`} 709 | _ = bytes.MinRead 710 | ) 711 | 712 | func testSubscriptionsUpdate(t *testing.T) { 713 | t.Parallel() 714 | 715 | if 0 == len(subscriptionPrimaryKeyColumns) { 716 | t.Skip("Skipping table with no primary key columns") 717 | } 718 | if len(subscriptionColumns) == len(subscriptionPrimaryKeyColumns) { 719 | t.Skip("Skipping table with only primary key columns") 720 | } 721 | 722 | seed := randomize.NewSeed() 723 | var err error 724 | o := &Subscription{} 725 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 726 | t.Errorf("Unable to randomize Subscription struct: %s", err) 727 | } 728 | 729 | tx := MustTx(boil.Begin()) 730 | defer func() { _ = tx.Rollback() }() 731 | if err = o.Insert(tx, boil.Infer()); err != nil { 732 | t.Error(err) 733 | } 734 | 735 | count, err := Subscriptions().Count(tx) 736 | if err != nil { 737 | t.Error(err) 738 | } 739 | 740 | if count != 1 { 741 | t.Error("want one record, got:", count) 742 | } 743 | 744 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionPrimaryKeyColumns...); err != nil { 745 | t.Errorf("Unable to randomize Subscription struct: %s", err) 746 | } 747 | 748 | if rowsAff, err := o.Update(tx, boil.Infer()); err != nil { 749 | t.Error(err) 750 | } else if rowsAff != 1 { 751 | t.Error("should only affect one row but affected", rowsAff) 752 | } 753 | } 754 | 755 | func testSubscriptionsSliceUpdateAll(t *testing.T) { 756 | t.Parallel() 757 | 758 | if len(subscriptionColumns) == len(subscriptionPrimaryKeyColumns) { 759 | t.Skip("Skipping table with only primary key columns") 760 | } 761 | 762 | seed := randomize.NewSeed() 763 | var err error 764 | o := &Subscription{} 765 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionColumnsWithDefault...); err != nil { 766 | t.Errorf("Unable to randomize Subscription struct: %s", err) 767 | } 768 | 769 | tx := MustTx(boil.Begin()) 770 | defer func() { _ = tx.Rollback() }() 771 | if err = o.Insert(tx, boil.Infer()); err != nil { 772 | t.Error(err) 773 | } 774 | 775 | count, err := Subscriptions().Count(tx) 776 | if err != nil { 777 | t.Error(err) 778 | } 779 | 780 | if count != 1 { 781 | t.Error("want one record, got:", count) 782 | } 783 | 784 | if err = randomize.Struct(seed, o, subscriptionDBTypes, true, subscriptionPrimaryKeyColumns...); err != nil { 785 | t.Errorf("Unable to randomize Subscription struct: %s", err) 786 | } 787 | 788 | // Remove Primary keys and unique columns from what we plan to update 789 | var fields []string 790 | if strmangle.StringSliceMatch(subscriptionColumns, subscriptionPrimaryKeyColumns) { 791 | fields = subscriptionColumns 792 | } else { 793 | fields = strmangle.SetComplement( 794 | subscriptionColumns, 795 | subscriptionPrimaryKeyColumns, 796 | ) 797 | } 798 | 799 | value := reflect.Indirect(reflect.ValueOf(o)) 800 | typ := reflect.TypeOf(o).Elem() 801 | n := typ.NumField() 802 | 803 | updateMap := M{} 804 | for _, col := range fields { 805 | for i := 0; i < n; i++ { 806 | f := typ.Field(i) 807 | if f.Tag.Get("boil") == col { 808 | updateMap[col] = value.Field(i).Interface() 809 | } 810 | } 811 | } 812 | 813 | slice := SubscriptionSlice{o} 814 | if rowsAff, err := slice.UpdateAll(tx, updateMap); err != nil { 815 | t.Error(err) 816 | } else if rowsAff != 1 { 817 | t.Error("wanted one record updated but got", rowsAff) 818 | } 819 | } 820 | 821 | func testSubscriptionsUpsert(t *testing.T) { 822 | t.Parallel() 823 | 824 | if len(subscriptionColumns) == len(subscriptionPrimaryKeyColumns) { 825 | t.Skip("Skipping table with only primary key columns") 826 | } 827 | 828 | seed := randomize.NewSeed() 829 | var err error 830 | // Attempt the INSERT side of an UPSERT 831 | o := Subscription{} 832 | if err = randomize.Struct(seed, &o, subscriptionDBTypes, true); err != nil { 833 | t.Errorf("Unable to randomize Subscription struct: %s", err) 834 | } 835 | 836 | tx := MustTx(boil.Begin()) 837 | defer func() { _ = tx.Rollback() }() 838 | if err = o.Upsert(tx, false, nil, boil.Infer(), boil.Infer()); err != nil { 839 | t.Errorf("Unable to upsert Subscription: %s", err) 840 | } 841 | 842 | count, err := Subscriptions().Count(tx) 843 | if err != nil { 844 | t.Error(err) 845 | } 846 | if count != 1 { 847 | t.Error("want one record, got:", count) 848 | } 849 | 850 | // Attempt the UPDATE side of an UPSERT 851 | if err = randomize.Struct(seed, &o, subscriptionDBTypes, false, subscriptionPrimaryKeyColumns...); err != nil { 852 | t.Errorf("Unable to randomize Subscription struct: %s", err) 853 | } 854 | 855 | if err = o.Upsert(tx, true, nil, boil.Infer(), boil.Infer()); err != nil { 856 | t.Errorf("Unable to upsert Subscription: %s", err) 857 | } 858 | 859 | count, err = Subscriptions().Count(tx) 860 | if err != nil { 861 | t.Error(err) 862 | } 863 | if count != 1 { 864 | t.Error("want one record, got:", count) 865 | } 866 | } 867 | -------------------------------------------------------------------------------- /models/users_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package hm 5 | 6 | import ( 7 | "bytes" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/volatiletech/sqlboiler/boil" 12 | "github.com/volatiletech/sqlboiler/queries" 13 | "github.com/volatiletech/sqlboiler/randomize" 14 | "github.com/volatiletech/sqlboiler/strmangle" 15 | ) 16 | 17 | var ( 18 | // Relationships sometimes use the reflection helper queries.Equal/queries.Assign 19 | // so force a package dependency in case they don't. 20 | _ = queries.Equal 21 | ) 22 | 23 | func testUsers(t *testing.T) { 24 | t.Parallel() 25 | 26 | query := Users() 27 | 28 | if query.Query == nil { 29 | t.Error("expected a query, got nothing") 30 | } 31 | } 32 | 33 | func testUsersDelete(t *testing.T) { 34 | t.Parallel() 35 | 36 | seed := randomize.NewSeed() 37 | var err error 38 | o := &User{} 39 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 40 | t.Errorf("Unable to randomize User struct: %s", err) 41 | } 42 | 43 | tx := MustTx(boil.Begin()) 44 | defer func() { _ = tx.Rollback() }() 45 | if err = o.Insert(tx, boil.Infer()); err != nil { 46 | t.Error(err) 47 | } 48 | 49 | if rowsAff, err := o.Delete(tx); err != nil { 50 | t.Error(err) 51 | } else if rowsAff != 1 { 52 | t.Error("should only have deleted one row, but affected:", rowsAff) 53 | } 54 | 55 | count, err := Users().Count(tx) 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | 60 | if count != 0 { 61 | t.Error("want zero records, got:", count) 62 | } 63 | } 64 | 65 | func testUsersQueryDeleteAll(t *testing.T) { 66 | t.Parallel() 67 | 68 | seed := randomize.NewSeed() 69 | var err error 70 | o := &User{} 71 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 72 | t.Errorf("Unable to randomize User struct: %s", err) 73 | } 74 | 75 | tx := MustTx(boil.Begin()) 76 | defer func() { _ = tx.Rollback() }() 77 | if err = o.Insert(tx, boil.Infer()); err != nil { 78 | t.Error(err) 79 | } 80 | 81 | if rowsAff, err := Users().DeleteAll(tx); err != nil { 82 | t.Error(err) 83 | } else if rowsAff != 1 { 84 | t.Error("should only have deleted one row, but affected:", rowsAff) 85 | } 86 | 87 | count, err := Users().Count(tx) 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | 92 | if count != 0 { 93 | t.Error("want zero records, got:", count) 94 | } 95 | } 96 | 97 | func testUsersSliceDeleteAll(t *testing.T) { 98 | t.Parallel() 99 | 100 | seed := randomize.NewSeed() 101 | var err error 102 | o := &User{} 103 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 104 | t.Errorf("Unable to randomize User struct: %s", err) 105 | } 106 | 107 | tx := MustTx(boil.Begin()) 108 | defer func() { _ = tx.Rollback() }() 109 | if err = o.Insert(tx, boil.Infer()); err != nil { 110 | t.Error(err) 111 | } 112 | 113 | slice := UserSlice{o} 114 | 115 | if rowsAff, err := slice.DeleteAll(tx); err != nil { 116 | t.Error(err) 117 | } else if rowsAff != 1 { 118 | t.Error("should only have deleted one row, but affected:", rowsAff) 119 | } 120 | 121 | count, err := Users().Count(tx) 122 | if err != nil { 123 | t.Error(err) 124 | } 125 | 126 | if count != 0 { 127 | t.Error("want zero records, got:", count) 128 | } 129 | } 130 | 131 | func testUsersExists(t *testing.T) { 132 | t.Parallel() 133 | 134 | seed := randomize.NewSeed() 135 | var err error 136 | o := &User{} 137 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 138 | t.Errorf("Unable to randomize User struct: %s", err) 139 | } 140 | 141 | tx := MustTx(boil.Begin()) 142 | defer func() { _ = tx.Rollback() }() 143 | if err = o.Insert(tx, boil.Infer()); err != nil { 144 | t.Error(err) 145 | } 146 | 147 | e, err := UserExists(tx, o.ID) 148 | if err != nil { 149 | t.Errorf("Unable to check if User exists: %s", err) 150 | } 151 | if !e { 152 | t.Errorf("Expected UserExists to return true, but got false.") 153 | } 154 | } 155 | 156 | func testUsersFind(t *testing.T) { 157 | t.Parallel() 158 | 159 | seed := randomize.NewSeed() 160 | var err error 161 | o := &User{} 162 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 163 | t.Errorf("Unable to randomize User struct: %s", err) 164 | } 165 | 166 | tx := MustTx(boil.Begin()) 167 | defer func() { _ = tx.Rollback() }() 168 | if err = o.Insert(tx, boil.Infer()); err != nil { 169 | t.Error(err) 170 | } 171 | 172 | userFound, err := FindUser(tx, o.ID) 173 | if err != nil { 174 | t.Error(err) 175 | } 176 | 177 | if userFound == nil { 178 | t.Error("want a record, got nil") 179 | } 180 | } 181 | 182 | func testUsersBind(t *testing.T) { 183 | t.Parallel() 184 | 185 | seed := randomize.NewSeed() 186 | var err error 187 | o := &User{} 188 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 189 | t.Errorf("Unable to randomize User struct: %s", err) 190 | } 191 | 192 | tx := MustTx(boil.Begin()) 193 | defer func() { _ = tx.Rollback() }() 194 | if err = o.Insert(tx, boil.Infer()); err != nil { 195 | t.Error(err) 196 | } 197 | 198 | if err = Users().Bind(nil, tx, o); err != nil { 199 | t.Error(err) 200 | } 201 | } 202 | 203 | func testUsersOne(t *testing.T) { 204 | t.Parallel() 205 | 206 | seed := randomize.NewSeed() 207 | var err error 208 | o := &User{} 209 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 210 | t.Errorf("Unable to randomize User struct: %s", err) 211 | } 212 | 213 | tx := MustTx(boil.Begin()) 214 | defer func() { _ = tx.Rollback() }() 215 | if err = o.Insert(tx, boil.Infer()); err != nil { 216 | t.Error(err) 217 | } 218 | 219 | if x, err := Users().One(tx); err != nil { 220 | t.Error(err) 221 | } else if x == nil { 222 | t.Error("expected to get a non nil record") 223 | } 224 | } 225 | 226 | func testUsersAll(t *testing.T) { 227 | t.Parallel() 228 | 229 | seed := randomize.NewSeed() 230 | var err error 231 | userOne := &User{} 232 | userTwo := &User{} 233 | if err = randomize.Struct(seed, userOne, userDBTypes, false, userColumnsWithDefault...); err != nil { 234 | t.Errorf("Unable to randomize User struct: %s", err) 235 | } 236 | if err = randomize.Struct(seed, userTwo, userDBTypes, false, userColumnsWithDefault...); err != nil { 237 | t.Errorf("Unable to randomize User struct: %s", err) 238 | } 239 | 240 | tx := MustTx(boil.Begin()) 241 | defer func() { _ = tx.Rollback() }() 242 | if err = userOne.Insert(tx, boil.Infer()); err != nil { 243 | t.Error(err) 244 | } 245 | if err = userTwo.Insert(tx, boil.Infer()); err != nil { 246 | t.Error(err) 247 | } 248 | 249 | slice, err := Users().All(tx) 250 | if err != nil { 251 | t.Error(err) 252 | } 253 | 254 | if len(slice) != 2 { 255 | t.Error("want 2 records, got:", len(slice)) 256 | } 257 | } 258 | 259 | func testUsersCount(t *testing.T) { 260 | t.Parallel() 261 | 262 | var err error 263 | seed := randomize.NewSeed() 264 | userOne := &User{} 265 | userTwo := &User{} 266 | if err = randomize.Struct(seed, userOne, userDBTypes, false, userColumnsWithDefault...); err != nil { 267 | t.Errorf("Unable to randomize User struct: %s", err) 268 | } 269 | if err = randomize.Struct(seed, userTwo, userDBTypes, false, userColumnsWithDefault...); err != nil { 270 | t.Errorf("Unable to randomize User struct: %s", err) 271 | } 272 | 273 | tx := MustTx(boil.Begin()) 274 | defer func() { _ = tx.Rollback() }() 275 | if err = userOne.Insert(tx, boil.Infer()); err != nil { 276 | t.Error(err) 277 | } 278 | if err = userTwo.Insert(tx, boil.Infer()); err != nil { 279 | t.Error(err) 280 | } 281 | 282 | count, err := Users().Count(tx) 283 | if err != nil { 284 | t.Error(err) 285 | } 286 | 287 | if count != 2 { 288 | t.Error("want 2 records, got:", count) 289 | } 290 | } 291 | 292 | func userBeforeInsertHook(e boil.Executor, o *User) error { 293 | *o = User{} 294 | return nil 295 | } 296 | 297 | func userAfterInsertHook(e boil.Executor, o *User) error { 298 | *o = User{} 299 | return nil 300 | } 301 | 302 | func userAfterSelectHook(e boil.Executor, o *User) error { 303 | *o = User{} 304 | return nil 305 | } 306 | 307 | func userBeforeUpdateHook(e boil.Executor, o *User) error { 308 | *o = User{} 309 | return nil 310 | } 311 | 312 | func userAfterUpdateHook(e boil.Executor, o *User) error { 313 | *o = User{} 314 | return nil 315 | } 316 | 317 | func userBeforeDeleteHook(e boil.Executor, o *User) error { 318 | *o = User{} 319 | return nil 320 | } 321 | 322 | func userAfterDeleteHook(e boil.Executor, o *User) error { 323 | *o = User{} 324 | return nil 325 | } 326 | 327 | func userBeforeUpsertHook(e boil.Executor, o *User) error { 328 | *o = User{} 329 | return nil 330 | } 331 | 332 | func userAfterUpsertHook(e boil.Executor, o *User) error { 333 | *o = User{} 334 | return nil 335 | } 336 | 337 | func testUsersHooks(t *testing.T) { 338 | t.Parallel() 339 | 340 | var err error 341 | 342 | empty := &User{} 343 | o := &User{} 344 | 345 | seed := randomize.NewSeed() 346 | if err = randomize.Struct(seed, o, userDBTypes, false); err != nil { 347 | t.Errorf("Unable to randomize User object: %s", err) 348 | } 349 | 350 | AddUserHook(boil.BeforeInsertHook, userBeforeInsertHook) 351 | if err = o.doBeforeInsertHooks(nil); err != nil { 352 | t.Errorf("Unable to execute doBeforeInsertHooks: %s", err) 353 | } 354 | if !reflect.DeepEqual(o, empty) { 355 | t.Errorf("Expected BeforeInsertHook function to empty object, but got: %#v", o) 356 | } 357 | userBeforeInsertHooks = []UserHook{} 358 | 359 | AddUserHook(boil.AfterInsertHook, userAfterInsertHook) 360 | if err = o.doAfterInsertHooks(nil); err != nil { 361 | t.Errorf("Unable to execute doAfterInsertHooks: %s", err) 362 | } 363 | if !reflect.DeepEqual(o, empty) { 364 | t.Errorf("Expected AfterInsertHook function to empty object, but got: %#v", o) 365 | } 366 | userAfterInsertHooks = []UserHook{} 367 | 368 | AddUserHook(boil.AfterSelectHook, userAfterSelectHook) 369 | if err = o.doAfterSelectHooks(nil); err != nil { 370 | t.Errorf("Unable to execute doAfterSelectHooks: %s", err) 371 | } 372 | if !reflect.DeepEqual(o, empty) { 373 | t.Errorf("Expected AfterSelectHook function to empty object, but got: %#v", o) 374 | } 375 | userAfterSelectHooks = []UserHook{} 376 | 377 | AddUserHook(boil.BeforeUpdateHook, userBeforeUpdateHook) 378 | if err = o.doBeforeUpdateHooks(nil); err != nil { 379 | t.Errorf("Unable to execute doBeforeUpdateHooks: %s", err) 380 | } 381 | if !reflect.DeepEqual(o, empty) { 382 | t.Errorf("Expected BeforeUpdateHook function to empty object, but got: %#v", o) 383 | } 384 | userBeforeUpdateHooks = []UserHook{} 385 | 386 | AddUserHook(boil.AfterUpdateHook, userAfterUpdateHook) 387 | if err = o.doAfterUpdateHooks(nil); err != nil { 388 | t.Errorf("Unable to execute doAfterUpdateHooks: %s", err) 389 | } 390 | if !reflect.DeepEqual(o, empty) { 391 | t.Errorf("Expected AfterUpdateHook function to empty object, but got: %#v", o) 392 | } 393 | userAfterUpdateHooks = []UserHook{} 394 | 395 | AddUserHook(boil.BeforeDeleteHook, userBeforeDeleteHook) 396 | if err = o.doBeforeDeleteHooks(nil); err != nil { 397 | t.Errorf("Unable to execute doBeforeDeleteHooks: %s", err) 398 | } 399 | if !reflect.DeepEqual(o, empty) { 400 | t.Errorf("Expected BeforeDeleteHook function to empty object, but got: %#v", o) 401 | } 402 | userBeforeDeleteHooks = []UserHook{} 403 | 404 | AddUserHook(boil.AfterDeleteHook, userAfterDeleteHook) 405 | if err = o.doAfterDeleteHooks(nil); err != nil { 406 | t.Errorf("Unable to execute doAfterDeleteHooks: %s", err) 407 | } 408 | if !reflect.DeepEqual(o, empty) { 409 | t.Errorf("Expected AfterDeleteHook function to empty object, but got: %#v", o) 410 | } 411 | userAfterDeleteHooks = []UserHook{} 412 | 413 | AddUserHook(boil.BeforeUpsertHook, userBeforeUpsertHook) 414 | if err = o.doBeforeUpsertHooks(nil); err != nil { 415 | t.Errorf("Unable to execute doBeforeUpsertHooks: %s", err) 416 | } 417 | if !reflect.DeepEqual(o, empty) { 418 | t.Errorf("Expected BeforeUpsertHook function to empty object, but got: %#v", o) 419 | } 420 | userBeforeUpsertHooks = []UserHook{} 421 | 422 | AddUserHook(boil.AfterUpsertHook, userAfterUpsertHook) 423 | if err = o.doAfterUpsertHooks(nil); err != nil { 424 | t.Errorf("Unable to execute doAfterUpsertHooks: %s", err) 425 | } 426 | if !reflect.DeepEqual(o, empty) { 427 | t.Errorf("Expected AfterUpsertHook function to empty object, but got: %#v", o) 428 | } 429 | userAfterUpsertHooks = []UserHook{} 430 | } 431 | 432 | func testUsersInsert(t *testing.T) { 433 | t.Parallel() 434 | 435 | seed := randomize.NewSeed() 436 | var err error 437 | o := &User{} 438 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 439 | t.Errorf("Unable to randomize User struct: %s", err) 440 | } 441 | 442 | tx := MustTx(boil.Begin()) 443 | defer func() { _ = tx.Rollback() }() 444 | if err = o.Insert(tx, boil.Infer()); err != nil { 445 | t.Error(err) 446 | } 447 | 448 | count, err := Users().Count(tx) 449 | if err != nil { 450 | t.Error(err) 451 | } 452 | 453 | if count != 1 { 454 | t.Error("want one record, got:", count) 455 | } 456 | } 457 | 458 | func testUsersInsertWhitelist(t *testing.T) { 459 | t.Parallel() 460 | 461 | seed := randomize.NewSeed() 462 | var err error 463 | o := &User{} 464 | if err = randomize.Struct(seed, o, userDBTypes, true); err != nil { 465 | t.Errorf("Unable to randomize User struct: %s", err) 466 | } 467 | 468 | tx := MustTx(boil.Begin()) 469 | defer func() { _ = tx.Rollback() }() 470 | if err = o.Insert(tx, boil.Whitelist(userColumnsWithoutDefault...)); err != nil { 471 | t.Error(err) 472 | } 473 | 474 | count, err := Users().Count(tx) 475 | if err != nil { 476 | t.Error(err) 477 | } 478 | 479 | if count != 1 { 480 | t.Error("want one record, got:", count) 481 | } 482 | } 483 | 484 | func testUserToOneRoleUsingRole(t *testing.T) { 485 | 486 | tx := MustTx(boil.Begin()) 487 | defer func() { _ = tx.Rollback() }() 488 | 489 | var local User 490 | var foreign Role 491 | 492 | seed := randomize.NewSeed() 493 | if err := randomize.Struct(seed, &local, userDBTypes, false, userColumnsWithDefault...); err != nil { 494 | t.Errorf("Unable to randomize User struct: %s", err) 495 | } 496 | if err := randomize.Struct(seed, &foreign, roleDBTypes, false, roleColumnsWithDefault...); err != nil { 497 | t.Errorf("Unable to randomize Role struct: %s", err) 498 | } 499 | 500 | if err := foreign.Insert(tx, boil.Infer()); err != nil { 501 | t.Fatal(err) 502 | } 503 | 504 | local.RoleID = foreign.ID 505 | if err := local.Insert(tx, boil.Infer()); err != nil { 506 | t.Fatal(err) 507 | } 508 | 509 | check, err := local.Role().One(tx) 510 | if err != nil { 511 | t.Fatal(err) 512 | } 513 | 514 | if check.ID != foreign.ID { 515 | t.Errorf("want: %v, got %v", foreign.ID, check.ID) 516 | } 517 | 518 | slice := UserSlice{&local} 519 | if err = local.L.LoadRole(tx, false, (*[]*User)(&slice), nil); err != nil { 520 | t.Fatal(err) 521 | } 522 | if local.R.Role == nil { 523 | t.Error("struct should have been eager loaded") 524 | } 525 | 526 | local.R.Role = nil 527 | if err = local.L.LoadRole(tx, true, &local, nil); err != nil { 528 | t.Fatal(err) 529 | } 530 | if local.R.Role == nil { 531 | t.Error("struct should have been eager loaded") 532 | } 533 | } 534 | 535 | func testUserToOneTenantUsingTenant(t *testing.T) { 536 | 537 | tx := MustTx(boil.Begin()) 538 | defer func() { _ = tx.Rollback() }() 539 | 540 | var local User 541 | var foreign Tenant 542 | 543 | seed := randomize.NewSeed() 544 | if err := randomize.Struct(seed, &local, userDBTypes, false, userColumnsWithDefault...); err != nil { 545 | t.Errorf("Unable to randomize User struct: %s", err) 546 | } 547 | if err := randomize.Struct(seed, &foreign, tenantDBTypes, false, tenantColumnsWithDefault...); err != nil { 548 | t.Errorf("Unable to randomize Tenant struct: %s", err) 549 | } 550 | 551 | if err := foreign.Insert(tx, boil.Infer()); err != nil { 552 | t.Fatal(err) 553 | } 554 | 555 | local.TenantID = foreign.ID 556 | if err := local.Insert(tx, boil.Infer()); err != nil { 557 | t.Fatal(err) 558 | } 559 | 560 | check, err := local.Tenant().One(tx) 561 | if err != nil { 562 | t.Fatal(err) 563 | } 564 | 565 | if check.ID != foreign.ID { 566 | t.Errorf("want: %v, got %v", foreign.ID, check.ID) 567 | } 568 | 569 | slice := UserSlice{&local} 570 | if err = local.L.LoadTenant(tx, false, (*[]*User)(&slice), nil); err != nil { 571 | t.Fatal(err) 572 | } 573 | if local.R.Tenant == nil { 574 | t.Error("struct should have been eager loaded") 575 | } 576 | 577 | local.R.Tenant = nil 578 | if err = local.L.LoadTenant(tx, true, &local, nil); err != nil { 579 | t.Fatal(err) 580 | } 581 | if local.R.Tenant == nil { 582 | t.Error("struct should have been eager loaded") 583 | } 584 | } 585 | 586 | func testUserToOneSetOpRoleUsingRole(t *testing.T) { 587 | var err error 588 | 589 | tx := MustTx(boil.Begin()) 590 | defer func() { _ = tx.Rollback() }() 591 | 592 | var a User 593 | var b, c Role 594 | 595 | seed := randomize.NewSeed() 596 | if err = randomize.Struct(seed, &a, userDBTypes, false, strmangle.SetComplement(userPrimaryKeyColumns, userColumnsWithoutDefault)...); err != nil { 597 | t.Fatal(err) 598 | } 599 | if err = randomize.Struct(seed, &b, roleDBTypes, false, strmangle.SetComplement(rolePrimaryKeyColumns, roleColumnsWithoutDefault)...); err != nil { 600 | t.Fatal(err) 601 | } 602 | if err = randomize.Struct(seed, &c, roleDBTypes, false, strmangle.SetComplement(rolePrimaryKeyColumns, roleColumnsWithoutDefault)...); err != nil { 603 | t.Fatal(err) 604 | } 605 | 606 | if err := a.Insert(tx, boil.Infer()); err != nil { 607 | t.Fatal(err) 608 | } 609 | if err = b.Insert(tx, boil.Infer()); err != nil { 610 | t.Fatal(err) 611 | } 612 | 613 | for i, x := range []*Role{&b, &c} { 614 | err = a.SetRole(tx, i != 0, x) 615 | if err != nil { 616 | t.Fatal(err) 617 | } 618 | 619 | if a.R.Role != x { 620 | t.Error("relationship struct not set to correct value") 621 | } 622 | 623 | if x.R.Users[0] != &a { 624 | t.Error("failed to append to foreign relationship struct") 625 | } 626 | if a.RoleID != x.ID { 627 | t.Error("foreign key was wrong value", a.RoleID) 628 | } 629 | 630 | zero := reflect.Zero(reflect.TypeOf(a.RoleID)) 631 | reflect.Indirect(reflect.ValueOf(&a.RoleID)).Set(zero) 632 | 633 | if err = a.Reload(tx); err != nil { 634 | t.Fatal("failed to reload", err) 635 | } 636 | 637 | if a.RoleID != x.ID { 638 | t.Error("foreign key was wrong value", a.RoleID, x.ID) 639 | } 640 | } 641 | } 642 | func testUserToOneSetOpTenantUsingTenant(t *testing.T) { 643 | var err error 644 | 645 | tx := MustTx(boil.Begin()) 646 | defer func() { _ = tx.Rollback() }() 647 | 648 | var a User 649 | var b, c Tenant 650 | 651 | seed := randomize.NewSeed() 652 | if err = randomize.Struct(seed, &a, userDBTypes, false, strmangle.SetComplement(userPrimaryKeyColumns, userColumnsWithoutDefault)...); err != nil { 653 | t.Fatal(err) 654 | } 655 | if err = randomize.Struct(seed, &b, tenantDBTypes, false, strmangle.SetComplement(tenantPrimaryKeyColumns, tenantColumnsWithoutDefault)...); err != nil { 656 | t.Fatal(err) 657 | } 658 | if err = randomize.Struct(seed, &c, tenantDBTypes, false, strmangle.SetComplement(tenantPrimaryKeyColumns, tenantColumnsWithoutDefault)...); err != nil { 659 | t.Fatal(err) 660 | } 661 | 662 | if err := a.Insert(tx, boil.Infer()); err != nil { 663 | t.Fatal(err) 664 | } 665 | if err = b.Insert(tx, boil.Infer()); err != nil { 666 | t.Fatal(err) 667 | } 668 | 669 | for i, x := range []*Tenant{&b, &c} { 670 | err = a.SetTenant(tx, i != 0, x) 671 | if err != nil { 672 | t.Fatal(err) 673 | } 674 | 675 | if a.R.Tenant != x { 676 | t.Error("relationship struct not set to correct value") 677 | } 678 | 679 | if x.R.Users[0] != &a { 680 | t.Error("failed to append to foreign relationship struct") 681 | } 682 | if a.TenantID != x.ID { 683 | t.Error("foreign key was wrong value", a.TenantID) 684 | } 685 | 686 | zero := reflect.Zero(reflect.TypeOf(a.TenantID)) 687 | reflect.Indirect(reflect.ValueOf(&a.TenantID)).Set(zero) 688 | 689 | if err = a.Reload(tx); err != nil { 690 | t.Fatal("failed to reload", err) 691 | } 692 | 693 | if a.TenantID != x.ID { 694 | t.Error("foreign key was wrong value", a.TenantID, x.ID) 695 | } 696 | } 697 | } 698 | 699 | func testUsersReload(t *testing.T) { 700 | t.Parallel() 701 | 702 | seed := randomize.NewSeed() 703 | var err error 704 | o := &User{} 705 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 706 | t.Errorf("Unable to randomize User struct: %s", err) 707 | } 708 | 709 | tx := MustTx(boil.Begin()) 710 | defer func() { _ = tx.Rollback() }() 711 | if err = o.Insert(tx, boil.Infer()); err != nil { 712 | t.Error(err) 713 | } 714 | 715 | if err = o.Reload(tx); err != nil { 716 | t.Error(err) 717 | } 718 | } 719 | 720 | func testUsersReloadAll(t *testing.T) { 721 | t.Parallel() 722 | 723 | seed := randomize.NewSeed() 724 | var err error 725 | o := &User{} 726 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 727 | t.Errorf("Unable to randomize User struct: %s", err) 728 | } 729 | 730 | tx := MustTx(boil.Begin()) 731 | defer func() { _ = tx.Rollback() }() 732 | if err = o.Insert(tx, boil.Infer()); err != nil { 733 | t.Error(err) 734 | } 735 | 736 | slice := UserSlice{o} 737 | 738 | if err = slice.ReloadAll(tx); err != nil { 739 | t.Error(err) 740 | } 741 | } 742 | 743 | func testUsersSelect(t *testing.T) { 744 | t.Parallel() 745 | 746 | seed := randomize.NewSeed() 747 | var err error 748 | o := &User{} 749 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 750 | t.Errorf("Unable to randomize User struct: %s", err) 751 | } 752 | 753 | tx := MustTx(boil.Begin()) 754 | defer func() { _ = tx.Rollback() }() 755 | if err = o.Insert(tx, boil.Infer()); err != nil { 756 | t.Error(err) 757 | } 758 | 759 | slice, err := Users().All(tx) 760 | if err != nil { 761 | t.Error(err) 762 | } 763 | 764 | if len(slice) != 1 { 765 | t.Error("want one record, got:", len(slice)) 766 | } 767 | } 768 | 769 | var ( 770 | userDBTypes = map[string]string{`CreatedAt`: `timestamp without time zone`, `Email`: `text`, `ID`: `uuid`, `Metadata`: `jsonb`, `Name`: `text`, `PasswordDigest`: `character varying`, `RoleID`: `integer`, `TenantID`: `uuid`, `UpdatedAt`: `timestamp without time zone`} 771 | _ = bytes.MinRead 772 | ) 773 | 774 | func testUsersUpdate(t *testing.T) { 775 | t.Parallel() 776 | 777 | if 0 == len(userPrimaryKeyColumns) { 778 | t.Skip("Skipping table with no primary key columns") 779 | } 780 | if len(userColumns) == len(userPrimaryKeyColumns) { 781 | t.Skip("Skipping table with only primary key columns") 782 | } 783 | 784 | seed := randomize.NewSeed() 785 | var err error 786 | o := &User{} 787 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 788 | t.Errorf("Unable to randomize User struct: %s", err) 789 | } 790 | 791 | tx := MustTx(boil.Begin()) 792 | defer func() { _ = tx.Rollback() }() 793 | if err = o.Insert(tx, boil.Infer()); err != nil { 794 | t.Error(err) 795 | } 796 | 797 | count, err := Users().Count(tx) 798 | if err != nil { 799 | t.Error(err) 800 | } 801 | 802 | if count != 1 { 803 | t.Error("want one record, got:", count) 804 | } 805 | 806 | if err = randomize.Struct(seed, o, userDBTypes, true, userPrimaryKeyColumns...); err != nil { 807 | t.Errorf("Unable to randomize User struct: %s", err) 808 | } 809 | 810 | if rowsAff, err := o.Update(tx, boil.Infer()); err != nil { 811 | t.Error(err) 812 | } else if rowsAff != 1 { 813 | t.Error("should only affect one row but affected", rowsAff) 814 | } 815 | } 816 | 817 | func testUsersSliceUpdateAll(t *testing.T) { 818 | t.Parallel() 819 | 820 | if len(userColumns) == len(userPrimaryKeyColumns) { 821 | t.Skip("Skipping table with only primary key columns") 822 | } 823 | 824 | seed := randomize.NewSeed() 825 | var err error 826 | o := &User{} 827 | if err = randomize.Struct(seed, o, userDBTypes, true, userColumnsWithDefault...); err != nil { 828 | t.Errorf("Unable to randomize User struct: %s", err) 829 | } 830 | 831 | tx := MustTx(boil.Begin()) 832 | defer func() { _ = tx.Rollback() }() 833 | if err = o.Insert(tx, boil.Infer()); err != nil { 834 | t.Error(err) 835 | } 836 | 837 | count, err := Users().Count(tx) 838 | if err != nil { 839 | t.Error(err) 840 | } 841 | 842 | if count != 1 { 843 | t.Error("want one record, got:", count) 844 | } 845 | 846 | if err = randomize.Struct(seed, o, userDBTypes, true, userPrimaryKeyColumns...); err != nil { 847 | t.Errorf("Unable to randomize User struct: %s", err) 848 | } 849 | 850 | // Remove Primary keys and unique columns from what we plan to update 851 | var fields []string 852 | if strmangle.StringSliceMatch(userColumns, userPrimaryKeyColumns) { 853 | fields = userColumns 854 | } else { 855 | fields = strmangle.SetComplement( 856 | userColumns, 857 | userPrimaryKeyColumns, 858 | ) 859 | } 860 | 861 | value := reflect.Indirect(reflect.ValueOf(o)) 862 | typ := reflect.TypeOf(o).Elem() 863 | n := typ.NumField() 864 | 865 | updateMap := M{} 866 | for _, col := range fields { 867 | for i := 0; i < n; i++ { 868 | f := typ.Field(i) 869 | if f.Tag.Get("boil") == col { 870 | updateMap[col] = value.Field(i).Interface() 871 | } 872 | } 873 | } 874 | 875 | slice := UserSlice{o} 876 | if rowsAff, err := slice.UpdateAll(tx, updateMap); err != nil { 877 | t.Error(err) 878 | } else if rowsAff != 1 { 879 | t.Error("wanted one record updated but got", rowsAff) 880 | } 881 | } 882 | 883 | func testUsersUpsert(t *testing.T) { 884 | t.Parallel() 885 | 886 | if len(userColumns) == len(userPrimaryKeyColumns) { 887 | t.Skip("Skipping table with only primary key columns") 888 | } 889 | 890 | seed := randomize.NewSeed() 891 | var err error 892 | // Attempt the INSERT side of an UPSERT 893 | o := User{} 894 | if err = randomize.Struct(seed, &o, userDBTypes, true); err != nil { 895 | t.Errorf("Unable to randomize User struct: %s", err) 896 | } 897 | 898 | tx := MustTx(boil.Begin()) 899 | defer func() { _ = tx.Rollback() }() 900 | if err = o.Upsert(tx, false, nil, boil.Infer(), boil.Infer()); err != nil { 901 | t.Errorf("Unable to upsert User: %s", err) 902 | } 903 | 904 | count, err := Users().Count(tx) 905 | if err != nil { 906 | t.Error(err) 907 | } 908 | if count != 1 { 909 | t.Error("want one record, got:", count) 910 | } 911 | 912 | // Attempt the UPDATE side of an UPSERT 913 | if err = randomize.Struct(seed, &o, userDBTypes, false, userPrimaryKeyColumns...); err != nil { 914 | t.Errorf("Unable to randomize User struct: %s", err) 915 | } 916 | 917 | if err = o.Upsert(tx, true, nil, boil.Infer(), boil.Infer()); err != nil { 918 | t.Errorf("Unable to upsert User: %s", err) 919 | } 920 | 921 | count, err = Users().Count(tx) 922 | if err != nil { 923 | t.Error(err) 924 | } 925 | if count != 1 { 926 | t.Error("want one record, got:", count) 927 | } 928 | } 929 | -------------------------------------------------------------------------------- /password-reset-email.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "fmt" 5 | "github.com/matcornic/hermes/v2" 6 | "github.com/nathanstitt/hippo/models" 7 | ) 8 | 9 | func passwordResetEmail(user *hm.User, token string, db DB, config Configuration) *hermes.Body { 10 | // email string 11 | productName := config.String("product_name") 12 | domain := config.String("domain") 13 | 14 | return &hermes.Body{ 15 | Name: user.Email, 16 | Intros: []string{ 17 | fmt.Sprintf("You have received this email because someone requested to reset the password for email address %s at %s", user.Email, productName), 18 | }, 19 | Actions: []hermes.Action{ 20 | { 21 | Instructions: fmt.Sprintf("Click the button below to reset your password for %s", productName), 22 | Button: hermes.Button{ 23 | Color: "#DC4D2F", 24 | Text: "Reset Password", 25 | Link: fmt.Sprintf("https://%s/forgot-password?t=%s", 26 | domain, token), 27 | }, 28 | }, 29 | }, 30 | Outros: []string{ 31 | fmt.Sprintf("If you did not request a password reset for %s, please ignore this email. No further action is required on your part.", productName), 32 | }, 33 | Signature: "Thanks!", 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public-schema.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 10.1 6 | -- Dumped by pg_dump version 10.1 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET idle_in_transaction_session_timeout = 0; 11 | SET client_encoding = 'UTF8'; 12 | SET standard_conforming_strings = on; 13 | SET check_function_bodies = false; 14 | SET client_min_messages = warning; 15 | SET row_security = off; 16 | 17 | SET search_path = public, pg_catalog; 18 | 19 | -- 20 | -- Name: entry_tagger_func(uuid, character varying, boolean, numeric); Type: FUNCTION; Schema: public; Owner: - 21 | -- 22 | 23 | CREATE FUNCTION entry_tagger_func(v_uid uuid, v_tag_name character varying, v_add_tag boolean, v_tag_amount numeric) RETURNS boolean 24 | LANGUAGE plpgsql 25 | AS $$ 26 | DECLARE 27 | v_tag RECORD; 28 | v_tag_id integer; 29 | v_account_id integer; 30 | BEGIN 31 | IF v_add_tag THEN 32 | SELECT account_id into v_account_id from entries where id = v_uid; 33 | IF NOT FOUND THEN 34 | RAISE EXCEPTION 'entry % not found', v_uid; 35 | END IF; 36 | 37 | SELECT * into v_tag from tags where name = v_tag_name and account_id = v_account_id; 38 | IF NOT FOUND THEN 39 | insert into tags ( account_id, deleted, name ) values ( v_account_id, 'f', v_tag_name ) RETURNING ID into v_tag_id; 40 | ELSE 41 | v_tag_id := v_tag.id; 42 | IF v_tag.deleted THEN 43 | update tags set deleted = 'f' where id = v_tag.id; 44 | END IF; 45 | END IF; 46 | BEGIN 47 | insert into entry_tags ( tag_id, entry_id, amount ) values ( v_tag_id, v_uid, v_tag_amount ); 48 | EXCEPTION WHEN unique_violation THEN 49 | -- do nothing, tag already exists 50 | END; 51 | return true; 52 | ELSE 53 | DELETE from entry_tags using tags where entry_id=v_uid and tags.id=entry_tags.tag_id and tags.name=v_tag_name; 54 | return false; 55 | END IF; 56 | UPDATE entries set last_mod = ( CURRENT_TIMESTAMP at time zone 'Z' ) where id = v_uid; 57 | END; 58 | $$; 59 | 60 | 61 | -- 62 | -- Name: nearby_entries(integer, character varying, character varying); Type: FUNCTION; Schema: public; Owner: - 63 | -- 64 | 65 | CREATE FUNCTION nearby_entries(p_tenant_id integer, p_longitude character varying, p_latitude character varying) RETURNS TABLE(name text, distance double precision, usage_count bigint) 66 | LANGUAGE plpgsql 67 | AS $$ 68 | DECLARE 69 | geo geography := ST_GeographyFromText('SRID=4326;POINT('|| p_longitude || ' ' || p_latitude || ')'); 70 | BEGIN 71 | RETURN QUERY 72 | SELECT 73 | entries.name 74 | ,st_distance(geo, 75 | ST_GeographyFromText( 76 | 'SRID=4326;POINT('|| avg(entries.longitude) || ' ' || avg(entries.latitude) || ')' 77 | ) 78 | ) as distance 79 | ,count(entries.*) 80 | FROM entries WHERE 81 | tenant_id = p_tenant_id and 82 | ST_DWithin( 83 | ST_GeographyFromText( 84 | 'SRID=4326;POINT('|| entries.longitude || ' ' || entries.latitude || ')' 85 | ) 86 | ,geo 87 | ,1000 88 | ) 89 | group by entries.name 90 | having count(entries.*) > 2; 91 | END 92 | $$; 93 | 94 | 95 | -- 96 | -- Name: random_string(integer); Type: FUNCTION; Schema: public; Owner: - 97 | -- 98 | 99 | CREATE FUNCTION random_string(length integer) RETURNS text 100 | LANGUAGE plpgsql 101 | AS $$ 102 | declare 103 | chars text[] := '{2,3,4,5,6,7,8,9,A,B,C,D,E,F,G,H,J,K,L,M,N,P,Q,R,S,T,U,V,W,X,Y,Z}'; 104 | result text := ''; 105 | i integer := 0; 106 | begin 107 | if length < 0 then 108 | raise exception 'Given length cannot be less than 0'; 109 | end if; 110 | for i in 1..length loop 111 | result := result || chars[1+random()*(array_length(chars, 1)-1)]; 112 | end loop; 113 | return result; 114 | end; 115 | $$; 116 | 117 | 118 | SET default_tablespace = ''; 119 | 120 | SET default_with_oids = false; 121 | 122 | -- 123 | -- Name: accounts; Type: TABLE; Schema: public; Owner: - 124 | -- 125 | 126 | CREATE TABLE accounts ( 127 | name text NOT NULL, 128 | tenant_id integer NOT NULL, 129 | is_deleted boolean DEFAULT false NOT NULL, 130 | created_at timestamp without time zone, 131 | updated_at timestamp without time zone, 132 | id uuid DEFAULT gen_random_uuid() NOT NULL 133 | ); 134 | 135 | 136 | -- 137 | -- Name: entries; Type: TABLE; Schema: public; Owner: - 138 | -- 139 | 140 | CREATE TABLE entries ( 141 | id uuid NOT NULL, 142 | created_by_id integer NOT NULL, 143 | name text, 144 | notes text, 145 | amount double precision NOT NULL, 146 | occurred_at timestamp without time zone NOT NULL, 147 | created_at timestamp without time zone, 148 | latitude numeric, 149 | longitude numeric, 150 | has_cleared boolean DEFAULT false NOT NULL, 151 | account_id uuid, 152 | tenant_id integer NOT NULL 153 | ); 154 | 155 | 156 | -- 157 | -- Name: account_details; Type: VIEW; Schema: public; Owner: - 158 | -- 159 | 160 | CREATE VIEW account_details AS 161 | SELECT accounts.id AS account_id, 162 | COALESCE(calulated.balance, (0)::double precision) AS balance, 163 | COALESCE(calulated.count, (0)::bigint) AS num_entries 164 | FROM (accounts 165 | LEFT JOIN ( SELECT entries.account_id, 166 | count(*) AS count, 167 | sum(entries.amount) AS balance 168 | FROM entries 169 | GROUP BY entries.account_id) calulated ON ((calulated.account_id = accounts.id))); 170 | 171 | 172 | -- 173 | -- Name: ar_internal_metadata; Type: TABLE; Schema: public; Owner: - 174 | -- 175 | 176 | CREATE TABLE ar_internal_metadata ( 177 | key character varying NOT NULL, 178 | value character varying, 179 | created_at timestamp without time zone NOT NULL, 180 | updated_at timestamp without time zone NOT NULL 181 | ); 182 | 183 | 184 | -- 185 | -- Name: assets; Type: TABLE; Schema: public; Owner: - 186 | -- 187 | 188 | CREATE TABLE assets ( 189 | id integer NOT NULL, 190 | tenant_id integer NOT NULL, 191 | owner_type character varying NOT NULL, 192 | owner_id integer NOT NULL, 193 | "order" integer, 194 | file_data jsonb DEFAULT '{}'::jsonb NOT NULL 195 | ); 196 | 197 | 198 | -- 199 | -- Name: assets_id_seq; Type: SEQUENCE; Schema: public; Owner: - 200 | -- 201 | 202 | CREATE SEQUENCE assets_id_seq 203 | AS integer 204 | START WITH 1 205 | INCREMENT BY 1 206 | NO MINVALUE 207 | NO MAXVALUE 208 | CACHE 1; 209 | 210 | 211 | -- 212 | -- Name: assets_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 213 | -- 214 | 215 | ALTER SEQUENCE assets_id_seq OWNED BY assets.id; 216 | 217 | 218 | -- 219 | -- Name: entry_running_balance; Type: VIEW; Schema: public; Owner: - 220 | -- 221 | 222 | CREATE VIEW entry_running_balance AS 223 | SELECT ent.id AS entry_id, 224 | sum(ent.amount) OVER (PARTITION BY ent.account_id ORDER BY ent.occurred_at) AS running_balance 225 | FROM entries ent; 226 | 227 | 228 | -- 229 | -- Name: entry_tags; Type: TABLE; Schema: public; Owner: - 230 | -- 231 | 232 | CREATE TABLE entry_tags ( 233 | id integer NOT NULL, 234 | tag_id integer NOT NULL, 235 | entry_id uuid NOT NULL 236 | ); 237 | 238 | 239 | -- 240 | -- Name: tags; Type: TABLE; Schema: public; Owner: - 241 | -- 242 | 243 | CREATE TABLE tags ( 244 | id integer NOT NULL, 245 | name character varying(255), 246 | tenant_id integer NOT NULL 247 | ); 248 | 249 | 250 | -- 251 | -- Name: entry_tag_details; Type: VIEW; Schema: public; Owner: - 252 | -- 253 | 254 | CREATE VIEW entry_tag_details AS 255 | SELECT entries.id AS entry_id, 256 | array_agg(entry_tags.tag_id) AS tag_ids, 257 | jsonb_agg(json_build_object('name', tags.name, 'id', tags.id)) AS tag 258 | FROM (entries 259 | JOIN (entry_tags 260 | JOIN tags ON ((tags.id = entry_tags.tag_id))) ON ((entry_tags.entry_id = entries.id))) 261 | GROUP BY entries.id; 262 | 263 | 264 | -- 265 | -- Name: entry_tags_id_seq; Type: SEQUENCE; Schema: public; Owner: - 266 | -- 267 | 268 | CREATE SEQUENCE entry_tags_id_seq 269 | START WITH 1 270 | INCREMENT BY 1 271 | NO MINVALUE 272 | NO MAXVALUE 273 | CACHE 1; 274 | 275 | 276 | -- 277 | -- Name: entry_tags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 278 | -- 279 | 280 | ALTER SEQUENCE entry_tags_id_seq OWNED BY entry_tags.id; 281 | 282 | 283 | -- 284 | -- Name: pages; Type: TABLE; Schema: public; Owner: - 285 | -- 286 | 287 | CREATE TABLE pages ( 288 | id integer NOT NULL, 289 | tenant_id integer NOT NULL, 290 | owner_type character varying, 291 | owner_id integer, 292 | html text NOT NULL, 293 | contents jsonb NOT NULL 294 | ); 295 | 296 | 297 | -- 298 | -- Name: pages_id_seq; Type: SEQUENCE; Schema: public; Owner: - 299 | -- 300 | 301 | CREATE SEQUENCE pages_id_seq 302 | AS integer 303 | START WITH 1 304 | INCREMENT BY 1 305 | NO MINVALUE 306 | NO MAXVALUE 307 | CACHE 1; 308 | 309 | 310 | -- 311 | -- Name: pages_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 312 | -- 313 | 314 | ALTER SEQUENCE pages_id_seq OWNED BY pages.id; 315 | 316 | 317 | -- 318 | -- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - 319 | -- 320 | 321 | CREATE TABLE schema_migrations ( 322 | version character varying(255) NOT NULL 323 | ); 324 | 325 | 326 | -- 327 | -- Name: subscriptions; Type: TABLE; Schema: public; Owner: - 328 | -- 329 | 330 | CREATE TABLE subscriptions ( 331 | id integer NOT NULL, 332 | subscription_id character varying, 333 | name character varying NOT NULL, 334 | description character varying NOT NULL, 335 | price numeric(10,2) NOT NULL, 336 | trial_duration integer DEFAULT 0 337 | ); 338 | 339 | 340 | -- 341 | -- Name: subscriptions_id_seq; Type: SEQUENCE; Schema: public; Owner: - 342 | -- 343 | 344 | CREATE SEQUENCE subscriptions_id_seq 345 | AS integer 346 | START WITH 1 347 | INCREMENT BY 1 348 | NO MINVALUE 349 | NO MAXVALUE 350 | CACHE 1; 351 | 352 | 353 | -- 354 | -- Name: subscriptions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 355 | -- 356 | 357 | ALTER SEQUENCE subscriptions_id_seq OWNED BY subscriptions.id; 358 | 359 | 360 | -- 361 | -- Name: system_settings; Type: TABLE; Schema: public; Owner: - 362 | -- 363 | 364 | CREATE TABLE system_settings ( 365 | id integer NOT NULL, 366 | tenant_id integer NOT NULL, 367 | settings jsonb DEFAULT '{}'::jsonb NOT NULL 368 | ); 369 | 370 | 371 | -- 372 | -- Name: system_settings_id_seq; Type: SEQUENCE; Schema: public; Owner: - 373 | -- 374 | 375 | CREATE SEQUENCE system_settings_id_seq 376 | AS integer 377 | START WITH 1 378 | INCREMENT BY 1 379 | NO MINVALUE 380 | NO MAXVALUE 381 | CACHE 1; 382 | 383 | 384 | -- 385 | -- Name: system_settings_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 386 | -- 387 | 388 | ALTER SEQUENCE system_settings_id_seq OWNED BY system_settings.id; 389 | 390 | 391 | -- 392 | -- Name: tags_id_seq; Type: SEQUENCE; Schema: public; Owner: - 393 | -- 394 | 395 | CREATE SEQUENCE tags_id_seq 396 | START WITH 1 397 | INCREMENT BY 1 398 | NO MINVALUE 399 | NO MAXVALUE 400 | CACHE 1; 401 | 402 | 403 | -- 404 | -- Name: tags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 405 | -- 406 | 407 | ALTER SEQUENCE tags_id_seq OWNED BY tags.id; 408 | 409 | 410 | -- 411 | -- Name: tenants; Type: TABLE; Schema: public; Owner: - 412 | -- 413 | 414 | CREATE TABLE tenants ( 415 | id integer NOT NULL, 416 | slug character varying NOT NULL, 417 | email character varying NOT NULL, 418 | identifier text NOT NULL, 419 | name text NOT NULL, 420 | address text, 421 | phone_number text, 422 | created_at timestamp without time zone NOT NULL, 423 | updated_at timestamp without time zone NOT NULL, 424 | metadata jsonb DEFAULT '{}'::jsonb NOT NULL, 425 | subscription_id integer 426 | ); 427 | 428 | 429 | -- 430 | -- Name: tenants_id_seq; Type: SEQUENCE; Schema: public; Owner: - 431 | -- 432 | 433 | CREATE SEQUENCE tenants_id_seq 434 | AS integer 435 | START WITH 1 436 | INCREMENT BY 1 437 | NO MINVALUE 438 | NO MAXVALUE 439 | CACHE 1; 440 | 441 | 442 | -- 443 | -- Name: tenants_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 444 | -- 445 | 446 | ALTER SEQUENCE tenants_id_seq OWNED BY tenants.id; 447 | 448 | 449 | -- 450 | -- Name: users; Type: TABLE; Schema: public; Owner: - 451 | -- 452 | 453 | CREATE TABLE users ( 454 | id integer NOT NULL, 455 | tenant_id integer NOT NULL, 456 | login character varying NOT NULL, 457 | name character varying NOT NULL, 458 | email character varying NOT NULL, 459 | password_digest character varying NOT NULL, 460 | role_names character varying[] DEFAULT '{}'::character varying[] NOT NULL, 461 | options jsonb DEFAULT '{}'::jsonb NOT NULL, 462 | created_at timestamp without time zone NOT NULL, 463 | updated_at timestamp without time zone NOT NULL 464 | ); 465 | 466 | 467 | -- 468 | -- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: - 469 | -- 470 | 471 | CREATE SEQUENCE users_id_seq 472 | AS integer 473 | START WITH 1 474 | INCREMENT BY 1 475 | NO MINVALUE 476 | NO MAXVALUE 477 | CACHE 1; 478 | 479 | 480 | -- 481 | -- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 482 | -- 483 | 484 | ALTER SEQUENCE users_id_seq OWNED BY users.id; 485 | 486 | 487 | -- 488 | -- Name: assets id; Type: DEFAULT; Schema: public; Owner: - 489 | -- 490 | 491 | ALTER TABLE ONLY assets ALTER COLUMN id SET DEFAULT nextval('assets_id_seq'::regclass); 492 | 493 | 494 | -- 495 | -- Name: entry_tags id; Type: DEFAULT; Schema: public; Owner: - 496 | -- 497 | 498 | ALTER TABLE ONLY entry_tags ALTER COLUMN id SET DEFAULT nextval('entry_tags_id_seq'::regclass); 499 | 500 | 501 | -- 502 | -- Name: pages id; Type: DEFAULT; Schema: public; Owner: - 503 | -- 504 | 505 | ALTER TABLE ONLY pages ALTER COLUMN id SET DEFAULT nextval('pages_id_seq'::regclass); 506 | 507 | 508 | -- 509 | -- Name: subscriptions id; Type: DEFAULT; Schema: public; Owner: - 510 | -- 511 | 512 | ALTER TABLE ONLY subscriptions ALTER COLUMN id SET DEFAULT nextval('subscriptions_id_seq'::regclass); 513 | 514 | 515 | -- 516 | -- Name: system_settings id; Type: DEFAULT; Schema: public; Owner: - 517 | -- 518 | 519 | ALTER TABLE ONLY system_settings ALTER COLUMN id SET DEFAULT nextval('system_settings_id_seq'::regclass); 520 | 521 | 522 | -- 523 | -- Name: tags id; Type: DEFAULT; Schema: public; Owner: - 524 | -- 525 | 526 | ALTER TABLE ONLY tags ALTER COLUMN id SET DEFAULT nextval('tags_id_seq'::regclass); 527 | 528 | 529 | -- 530 | -- Name: tenants id; Type: DEFAULT; Schema: public; Owner: - 531 | -- 532 | 533 | ALTER TABLE ONLY tenants ALTER COLUMN id SET DEFAULT nextval('tenants_id_seq'::regclass); 534 | 535 | 536 | -- 537 | -- Name: users id; Type: DEFAULT; Schema: public; Owner: - 538 | -- 539 | 540 | ALTER TABLE ONLY users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regclass); 541 | 542 | 543 | -- 544 | -- Name: accounts accounts_pkey; Type: CONSTRAINT; Schema: public; Owner: - 545 | -- 546 | 547 | ALTER TABLE ONLY accounts 548 | ADD CONSTRAINT accounts_pkey PRIMARY KEY (id); 549 | 550 | 551 | -- 552 | -- Name: ar_internal_metadata ar_internal_metadata_pkey; Type: CONSTRAINT; Schema: public; Owner: - 553 | -- 554 | 555 | ALTER TABLE ONLY ar_internal_metadata 556 | ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key); 557 | 558 | 559 | -- 560 | -- Name: assets assets_pkey; Type: CONSTRAINT; Schema: public; Owner: - 561 | -- 562 | 563 | ALTER TABLE ONLY assets 564 | ADD CONSTRAINT assets_pkey PRIMARY KEY (id); 565 | 566 | 567 | -- 568 | -- Name: entry_tags entry_tags_pkey; Type: CONSTRAINT; Schema: public; Owner: - 569 | -- 570 | 571 | ALTER TABLE ONLY entry_tags 572 | ADD CONSTRAINT entry_tags_pkey PRIMARY KEY (id); 573 | 574 | 575 | -- 576 | -- Name: pages pages_pkey; Type: CONSTRAINT; Schema: public; Owner: - 577 | -- 578 | 579 | ALTER TABLE ONLY pages 580 | ADD CONSTRAINT pages_pkey PRIMARY KEY (id); 581 | 582 | 583 | -- 584 | -- Name: subscriptions subscriptions_pkey; Type: CONSTRAINT; Schema: public; Owner: - 585 | -- 586 | 587 | ALTER TABLE ONLY subscriptions 588 | ADD CONSTRAINT subscriptions_pkey PRIMARY KEY (id); 589 | 590 | 591 | -- 592 | -- Name: system_settings system_settings_pkey; Type: CONSTRAINT; Schema: public; Owner: - 593 | -- 594 | 595 | ALTER TABLE ONLY system_settings 596 | ADD CONSTRAINT system_settings_pkey PRIMARY KEY (id); 597 | 598 | 599 | -- 600 | -- Name: tags tags_pkey; Type: CONSTRAINT; Schema: public; Owner: - 601 | -- 602 | 603 | ALTER TABLE ONLY tags 604 | ADD CONSTRAINT tags_pkey PRIMARY KEY (id); 605 | 606 | 607 | -- 608 | -- Name: tenants tenants_pkey; Type: CONSTRAINT; Schema: public; Owner: - 609 | -- 610 | 611 | ALTER TABLE ONLY tenants 612 | ADD CONSTRAINT tenants_pkey PRIMARY KEY (id); 613 | 614 | 615 | -- 616 | -- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - 617 | -- 618 | 619 | ALTER TABLE ONLY users 620 | ADD CONSTRAINT users_pkey PRIMARY KEY (id); 621 | 622 | 623 | -- 624 | -- Name: index_assets_on_owner_id_and_owner_type; Type: INDEX; Schema: public; Owner: - 625 | -- 626 | 627 | CREATE INDEX index_assets_on_owner_id_and_owner_type ON assets USING btree (owner_id, owner_type); 628 | 629 | 630 | -- 631 | -- Name: index_assets_on_owner_type_and_owner_id; Type: INDEX; Schema: public; Owner: - 632 | -- 633 | 634 | CREATE INDEX index_assets_on_owner_type_and_owner_id ON assets USING btree (owner_type, owner_id); 635 | 636 | 637 | -- 638 | -- Name: index_assets_on_tenant_id; Type: INDEX; Schema: public; Owner: - 639 | -- 640 | 641 | CREATE INDEX index_assets_on_tenant_id ON assets USING btree (tenant_id); 642 | 643 | 644 | -- 645 | -- Name: index_entries_location; Type: INDEX; Schema: public; Owner: - 646 | -- 647 | 648 | CREATE INDEX index_entries_location ON entries USING gist (st_geographyfromtext((((('SRID=4326;POINT('::text || longitude) || ' '::text) || latitude) || ')'::text))); 649 | 650 | 651 | -- 652 | -- Name: index_entries_on_account_id_and_occurred_at; Type: INDEX; Schema: public; Owner: - 653 | -- 654 | 655 | CREATE INDEX index_entries_on_account_id_and_occurred_at ON entries USING btree (account_id, occurred_at); 656 | 657 | 658 | -- 659 | -- Name: index_entries_on_id; Type: INDEX; Schema: public; Owner: - 660 | -- 661 | 662 | CREATE UNIQUE INDEX index_entries_on_id ON entries USING btree (id); 663 | 664 | 665 | -- 666 | -- Name: index_entries_on_occurred_at; Type: INDEX; Schema: public; Owner: - 667 | -- 668 | 669 | CREATE INDEX index_entries_on_occurred_at ON entries USING btree (occurred_at); 670 | 671 | 672 | -- 673 | -- Name: index_entries_on_tenant_id; Type: INDEX; Schema: public; Owner: - 674 | -- 675 | 676 | CREATE INDEX index_entries_on_tenant_id ON entries USING btree (tenant_id); 677 | 678 | 679 | -- 680 | -- Name: index_entry_tags_on_entry_id; Type: INDEX; Schema: public; Owner: - 681 | -- 682 | 683 | CREATE INDEX index_entry_tags_on_entry_id ON entry_tags USING btree (entry_id); 684 | 685 | 686 | -- 687 | -- Name: index_entry_tags_on_tag_id_and_entry_id; Type: INDEX; Schema: public; Owner: - 688 | -- 689 | 690 | CREATE UNIQUE INDEX index_entry_tags_on_tag_id_and_entry_id ON entry_tags USING btree (tag_id, entry_id); 691 | 692 | 693 | -- 694 | -- Name: index_pages_on_owner_type_and_owner_id; Type: INDEX; Schema: public; Owner: - 695 | -- 696 | 697 | CREATE INDEX index_pages_on_owner_type_and_owner_id ON pages USING btree (owner_type, owner_id); 698 | 699 | 700 | -- 701 | -- Name: index_pages_on_tenant_id; Type: INDEX; Schema: public; Owner: - 702 | -- 703 | 704 | CREATE INDEX index_pages_on_tenant_id ON pages USING btree (tenant_id); 705 | 706 | 707 | -- 708 | -- Name: index_subscriptions_on_subscription_id; Type: INDEX; Schema: public; Owner: - 709 | -- 710 | 711 | CREATE UNIQUE INDEX index_subscriptions_on_subscription_id ON subscriptions USING btree (subscription_id); 712 | 713 | 714 | -- 715 | -- Name: index_system_settings_on_tenant_id; Type: INDEX; Schema: public; Owner: - 716 | -- 717 | 718 | CREATE INDEX index_system_settings_on_tenant_id ON system_settings USING btree (tenant_id); 719 | 720 | 721 | -- 722 | -- Name: index_tags_on_account_id; Type: INDEX; Schema: public; Owner: - 723 | -- 724 | 725 | CREATE INDEX index_tags_on_account_id ON tags USING btree (tenant_id); 726 | 727 | 728 | -- 729 | -- Name: index_tags_on_name_and_account_id; Type: INDEX; Schema: public; Owner: - 730 | -- 731 | 732 | CREATE UNIQUE INDEX index_tags_on_name_and_account_id ON tags USING btree (name, tenant_id); 733 | 734 | 735 | -- 736 | -- Name: index_tenants_on_identifier; Type: INDEX; Schema: public; Owner: - 737 | -- 738 | 739 | CREATE UNIQUE INDEX index_tenants_on_identifier ON tenants USING btree (identifier); 740 | 741 | 742 | -- 743 | -- Name: index_tenants_on_slug; Type: INDEX; Schema: public; Owner: - 744 | -- 745 | 746 | CREATE UNIQUE INDEX index_tenants_on_slug ON tenants USING btree (slug); 747 | 748 | 749 | -- 750 | -- Name: index_tenants_on_subscription_id; Type: INDEX; Schema: public; Owner: - 751 | -- 752 | 753 | CREATE INDEX index_tenants_on_subscription_id ON tenants USING btree (subscription_id); 754 | 755 | 756 | -- 757 | -- Name: index_users_on_login_and_tenant_id; Type: INDEX; Schema: public; Owner: - 758 | -- 759 | 760 | CREATE UNIQUE INDEX index_users_on_login_and_tenant_id ON users USING btree (lower((login)::text), tenant_id); 761 | 762 | 763 | -- 764 | -- Name: index_users_on_tenant_id; Type: INDEX; Schema: public; Owner: - 765 | -- 766 | 767 | CREATE INDEX index_users_on_tenant_id ON users USING btree (tenant_id); 768 | 769 | 770 | -- 771 | -- Name: unique_schema_migrations; Type: INDEX; Schema: public; Owner: - 772 | -- 773 | 774 | CREATE UNIQUE INDEX unique_schema_migrations ON schema_migrations USING btree (version); 775 | 776 | 777 | -- 778 | -- Name: accounts accounts_tenant_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - 779 | -- 780 | 781 | ALTER TABLE ONLY accounts 782 | ADD CONSTRAINT accounts_tenant_id_fk FOREIGN KEY (tenant_id) REFERENCES tenants(id); 783 | 784 | 785 | -- 786 | -- Name: entries entries_created_by_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - 787 | -- 788 | 789 | ALTER TABLE ONLY entries 790 | ADD CONSTRAINT entries_created_by_id_fk FOREIGN KEY (created_by_id) REFERENCES users(id); 791 | 792 | 793 | -- 794 | -- Name: entry_tags entry_tags_entry_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - 795 | -- 796 | 797 | ALTER TABLE ONLY entry_tags 798 | ADD CONSTRAINT entry_tags_entry_id_fk FOREIGN KEY (entry_id) REFERENCES entries(id); 799 | 800 | 801 | -- 802 | -- Name: entry_tags entry_tags_tag_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - 803 | -- 804 | 805 | ALTER TABLE ONLY entry_tags 806 | ADD CONSTRAINT entry_tags_tag_id_fk FOREIGN KEY (tag_id) REFERENCES tags(id); 807 | 808 | 809 | -- 810 | -- Name: tenants fk_rails_503e0d703f; Type: FK CONSTRAINT; Schema: public; Owner: - 811 | -- 812 | 813 | ALTER TABLE ONLY tenants 814 | ADD CONSTRAINT fk_rails_503e0d703f FOREIGN KEY (subscription_id) REFERENCES subscriptions(id); 815 | 816 | 817 | -- 818 | -- Name: pages fk_rails_c7f006a55b; Type: FK CONSTRAINT; Schema: public; Owner: - 819 | -- 820 | 821 | ALTER TABLE ONLY pages 822 | ADD CONSTRAINT fk_rails_c7f006a55b FOREIGN KEY (tenant_id) REFERENCES tenants(id); 823 | 824 | 825 | -- 826 | -- Name: tags tags_tenant_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - 827 | -- 828 | 829 | ALTER TABLE ONLY tags 830 | ADD CONSTRAINT tags_tenant_id_fk FOREIGN KEY (tenant_id) REFERENCES tenants(id); 831 | 832 | 833 | -- 834 | -- PostgreSQL database dump complete 835 | -- 836 | 837 | -------------------------------------------------------------------------------- /reset_password_test.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | "net/http/httptest" 9 | "github.com/gin-gonic/gin" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | "github.com/nathanstitt/webpacking" 13 | ) 14 | 15 | func prepareResetRequest(db DB) *SignupData { 16 | data := SignupData{ 17 | Name: "Bob", 18 | Email: "test-invite-123@test.com", 19 | Password: "password1234", 20 | Tenant: "Acme Inc", 21 | } 22 | CreateTenant(&data, db) 23 | return &data 24 | } 25 | 26 | func addPasswordResetRoute( 27 | r *gin.Engine, 28 | config Configuration, 29 | webpack *webpacking.WebPacking, 30 | ) { 31 | r.GET("/reset-password", func(c *gin.Context) { 32 | c.HTML(http.StatusOK, "login.html", gin.H{}) 33 | }) 34 | r.POST("/reset-password", UserPasswordResetHandler()) 35 | r.GET("/forgot-password", UserDisplayPasswordResetHandler) 36 | } 37 | 38 | var _ = Describe("Resetting Password", func() { 39 | 40 | Test("sends reset link", &TestFlags{WithRoutes: addPasswordResetRoute}, func(env *TestEnv) { 41 | 42 | r := env.Router 43 | db := env.DB 44 | 45 | // initiate reset 46 | data := prepareResetRequest(db) 47 | form := url.Values{} 48 | form.Add("email", data.Email) 49 | 50 | req, _ := http.NewRequest( "POST", "/reset-password", strings.NewReader(form.Encode())) 51 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 52 | resp := httptest.NewRecorder() 53 | r.ServeHTTP(resp, req) 54 | 55 | Expect(resp.Body.String()).To(ContainSubstring( 56 | fmt.Sprintf("emailed a login link to you at %s", data.Email))) 57 | Expect(LastEmailDelivery).ToNot(BeNil()) 58 | Expect(LastEmailDelivery.Contents).To(ContainSubstring( 59 | fmt.Sprintf("reset the password for email address %s", data.Email))) 60 | 61 | // follow link from email 62 | Expect(LastEmailDelivery.Contents).To(ContainSubstring("/forgot-password")) 63 | token, _ := EncryptStringProperty("email", data.Email) 64 | path := fmt.Sprintf("/forgot-password?t=%s", token) 65 | req, _ = http.NewRequest("GET", path, nil) 66 | resp = httptest.NewRecorder() 67 | r.ServeHTTP(resp, req) 68 | Expect(resp.Body.String()).To( 69 | ContainSubstring("Set New Password"), 70 | ) 71 | 72 | form = url.Values{} 73 | form.Add("password", "test-123-reset") 74 | req, _ = http.NewRequest("POST", "/reset-password", strings.NewReader(form.Encode())) 75 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 76 | req.Header.Set("Cookie", resp.Header().Get("Set-Cookie")) 77 | resp = httptest.NewRecorder() 78 | r.ServeHTTP(resp, req) 79 | Expect(resp.Header().Get("Location")).To(Equal("/")) 80 | user := FindUserByEmail(data.Email, db) 81 | Expect(IsValidPassword(user, "test-123-reset")).To(BeTrue()) 82 | }) 83 | 84 | 85 | }) 86 | -------------------------------------------------------------------------------- /signup-email.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "fmt" 5 | "github.com/matcornic/hermes/v2" 6 | "github.com/nathanstitt/hippo/models" 7 | ) 8 | 9 | func signupEmail(email string, tenant *hm.Tenant, config Configuration) *hermes.Body { 10 | productName := config.String("product_name") 11 | domain := config.String("domain") 12 | return &hermes.Body{ 13 | Name: email, 14 | Intros: []string{ 15 | fmt.Sprintf("You have received this email because %s was used to sign up for TheScrumGame.com", email), 16 | }, 17 | Actions: []hermes.Action{ 18 | { 19 | Instructions: fmt.Sprintf("Click the button below to access %s", 20 | productName), 21 | Button: hermes.Button{ 22 | Color: "#DC4D2F", 23 | Text: "Log me in", 24 | Link: fmt.Sprintf("https://%s/login", domain), 25 | }, 26 | }, 27 | }, 28 | Outros: []string{ 29 | fmt.Sprintf("If you did not request an account with %s, please ignore this email. No further action is required on your part.", productName), 30 | }, 31 | Signature: "Thanks!", 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /signup_test.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "net/url" 7 | "testing" 8 | "net/http" 9 | "net/http/httptest" 10 | "github.com/gin-gonic/gin" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func formData(email string) url.Values { 15 | data := url.Values{} 16 | data.Add("name", "Bob") 17 | data.Add("email", email) 18 | data.Add("password", "password1234") 19 | data.Add("tenant", "Acme Inc") 20 | return data 21 | } 22 | 23 | func makeRequest(data url.Values, router *gin.Engine) *httptest.ResponseRecorder { 24 | req, _ := http.NewRequest("POST", "/signup", 25 | strings.NewReader(data.Encode())) 26 | req.PostForm = data 27 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 28 | 29 | resp := httptest.NewRecorder() 30 | router.ServeHTTP(resp, req) 31 | return resp 32 | } 33 | 34 | func TestSignupHandler(t *testing.T) { 35 | 36 | 37 | Test("it emails a login link", &TestFlags{WithRoutes: addLoginRoute}, func(env *TestEnv) { 38 | r := env.Router 39 | db := env.DB 40 | email := "test1234@test.com" 41 | 42 | data := formData(email) 43 | resp := makeRequest(data, r) 44 | user := FindUserByEmail(email, db) 45 | So(user.ID, ShouldNotEqual, 0) 46 | So(resp.Code, ShouldEqual, http.StatusOK) 47 | if user.ID == "" { 48 | fmt.Printf("BODY: %s", resp.Body.String()) 49 | } 50 | So(resp.Header().Get("Set-Cookie"), ShouldNotBeEmpty) 51 | So(resp.Header().Get("Location"), ShouldEqual, "/") 52 | So(resp.Body.String(), ShouldContainSubstring, email) 53 | }) 54 | 55 | Test("errors when email is duplicate", &TestFlags{WithRoutes: addLoginRoute}, func(env *TestEnv) { 56 | email := "nathan1234@stitt.org" 57 | form := &SignupData{ 58 | Name: "Nathan", 59 | Email: email, 60 | Password: "password", 61 | Tenant: "Acme", 62 | } 63 | CreateTenant(form, env.DB) 64 | data := formData(email) 65 | resp := makeRequest(data, env.Router) 66 | So(resp.Code, ShouldEqual, http.StatusOK) 67 | So(resp.Body.String(), ShouldContainSubstring, "email is in use") 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /sqlboiler.toml: -------------------------------------------------------------------------------- 1 | add-panic-variants = "true" 2 | no-context = "true" 3 | pkgname = "hm" 4 | output = "models" 5 | 6 | [psql] 7 | dbname = "hippo_dev" 8 | host = "localhost" 9 | port = 5432 10 | user = "nas" 11 | sslmode = "disable" 12 | -------------------------------------------------------------------------------- /tenant.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | // "fmt" 5 | "errors" 6 | "strings" 7 | "net/http" 8 | "github.com/gosimple/slug" 9 | "github.com/gin-gonic/gin" 10 | "github.com/nathanstitt/hippo/models" 11 | "github.com/volatiletech/sqlboiler/boil" 12 | . "github.com/volatiletech/sqlboiler/queries/qm" 13 | ) 14 | 15 | type SignupData struct { 16 | Name string `form:"name"` 17 | Email string `form:"email"` 18 | Password string `form:"password"` 19 | Tenant string `form:"tenant"` 20 | } 21 | 22 | type ApplicationBootstrapData struct { 23 | User *hm.User 24 | JWT string 25 | WebDomain string 26 | } 27 | 28 | func IsEmailInUse(email string, db DB) bool { 29 | lowerEmail := strings.ToLower(email) 30 | m := Where("email = ?", lowerEmail) 31 | if (hm.Tenants(m).ExistsP(db) || 32 | hm.Users(m).ExistsP(db)) { 33 | return true 34 | } 35 | return false 36 | } 37 | 38 | func CreateTenant(data *SignupData, db DB) (*hm.Tenant, error) { 39 | email := strings.ToLower(data.Email) 40 | if IsEmailInUse(email, db) { 41 | return nil, errors.New("email is in use") 42 | } 43 | tenant := hm.Tenant{ 44 | Name: data.Tenant, 45 | Email: email, 46 | Identifier: slug.Make(data.Tenant), 47 | } 48 | var err error 49 | var admin *hm.User 50 | 51 | if err = tenant.Insert(db, boil.Infer()); err != nil { 52 | return nil, err; 53 | } 54 | 55 | admin = &hm.User{ 56 | Name: data.Name, 57 | Email: data.Email, 58 | RoleID: UserOwnerRoleID, 59 | } 60 | SetUserPassword(admin, data.Password) 61 | if err = tenant.AddUsers(db, true, admin); err != nil { 62 | return nil, err; 63 | } 64 | 65 | err = tenant.AddUsers(db, true, &hm.User{ 66 | Name: "Anonymous", 67 | RoleID: UserGuestRoleID, 68 | }); 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return &tenant, nil 74 | } 75 | 76 | func TenantSignupHandler(afterSignUp string) func(c *gin.Context) { 77 | return func(c *gin.Context) { 78 | var form SignupData 79 | if err := c.ShouldBind(&form); err != nil { 80 | RenderErrorPage("Failed to read signup data, please retry", c, &err) 81 | return 82 | } 83 | tx := GetDB(c) 84 | tenant, err := CreateTenant(&form, tx) 85 | if err != nil { 86 | RenderHomepage(&form, &err, c); 87 | return 88 | } 89 | admin := tenant.R.Users[0] 90 | LoginUser(admin, c) 91 | c.Redirect(http.StatusFound, afterSignUp) 92 | RenderApplication(admin, c) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tenant_test.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | _ "fmt" // for adhoc printing 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | q "github.com/volatiletech/sqlboiler/queries/qm" 8 | ) 9 | 10 | var _ = Describe("Tenant methods", func() { 11 | 12 | Test("can query", &TestFlags{}, func(env *TestEnv) { 13 | Expect( 14 | Tenants(q.Where("id=?", env.Tenant.ID)).ExistsP(env.DB), 15 | ).To(Equal(true)) 16 | 17 | }) 18 | 19 | }) 20 | -------------------------------------------------------------------------------- /testing.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "io" 5 | "fmt" 6 | "log" 7 | "flag" 8 | "bytes" 9 | "strings" 10 | "context" 11 | "net/http" 12 | "io/ioutil" 13 | "database/sql" 14 | "html/template" 15 | "net/http/httptest" 16 | "gopkg.in/urfave/cli.v1" 17 | "github.com/onsi/ginkgo" 18 | "github.com/go-mail/mail" 19 | "github.com/gin-gonic/gin" 20 | "github.com/nathanstitt/webpacking" 21 | "github.com/nathanstitt/hippo/models" 22 | "github.com/volatiletech/sqlboiler/boil" 23 | ) 24 | 25 | type TestEmailDelivery struct { 26 | To string 27 | Subject string 28 | Contents string 29 | } 30 | 31 | func (f *TestEmailDelivery) SendEmail(config Configuration, m *mail.Message) error { 32 | to := m.GetHeader("To") 33 | if len(to) > 0 { 34 | f.To = to[0]; 35 | } 36 | subj := m.GetHeader("Subject") 37 | if len(subj) > 0 { 38 | f.Subject = subj[0]; 39 | } 40 | buf := new(bytes.Buffer) 41 | _, err := m.WriteTo(buf) 42 | if err == nil { 43 | f.Contents = buf.String() 44 | } 45 | return err 46 | } 47 | 48 | var LastEmailDelivery *TestEmailDelivery 49 | 50 | func testingContextMiddleware(config Configuration, tx DB) gin.HandlerFunc { 51 | return func(c *gin.Context) { 52 | c.Set("dbTx", tx) 53 | c.Set("config", config) 54 | c.Next() 55 | } 56 | } 57 | 58 | type TestEnv struct { 59 | Router *gin.Engine 60 | DB DB 61 | Config Configuration 62 | Tenant *hm.Tenant 63 | } 64 | 65 | type RequestOptions struct { 66 | Body *string 67 | SessionCookie string 68 | ContentType string 69 | User *hm.User 70 | } 71 | 72 | func contentType(method string, options *RequestOptions) string { 73 | if options != nil && options.ContentType != "" { 74 | return options.ContentType 75 | } else { 76 | if method == "POST" { 77 | return "application/x-www-form-urlencoded" 78 | } else { 79 | return "application/json" 80 | } 81 | } 82 | } 83 | 84 | func (env *TestEnv) MakeRequest( 85 | method string, 86 | path string, 87 | options *RequestOptions, 88 | ) *httptest.ResponseRecorder { 89 | var body io.Reader 90 | if options != nil { 91 | if options.Body != nil { 92 | body = strings.NewReader(*options.Body) 93 | } 94 | } 95 | req, _ := http.NewRequest(method, path, body) 96 | req.Header.Set("Content-Type", contentType(method, options)) 97 | // fmt.Printf("CT: %s\nBody: %s\n", 98 | // contentType(method, options), 99 | // *options.Body, 100 | // ) 101 | if options != nil { 102 | if options.User != nil { 103 | req.Header.Set("Cookie", 104 | TestingCookieForUser( 105 | options.User, env.Config, 106 | ), 107 | ) 108 | } 109 | } 110 | resp := httptest.NewRecorder() 111 | env.Router.ServeHTTP(resp, req) 112 | return resp 113 | } 114 | 115 | type TestFlags struct { 116 | DebugDB bool 117 | WithRoutes func( 118 | *gin.Engine, 119 | Configuration, 120 | *webpacking.WebPacking, 121 | ) 122 | } 123 | 124 | type TestSetupEnv struct { 125 | SessionSecret string 126 | DBConnectionUrl string 127 | } 128 | 129 | func TestingCookieForUser(u *hm.User, config Configuration) string { 130 | r := gin.Default() 131 | InitSessions("test", r, config) 132 | r.GET("/", func(c *gin.Context) { 133 | LoginUser(u, c) 134 | c.String(200, "") 135 | }) 136 | res := httptest.NewRecorder() 137 | req, _ := http.NewRequest("GET", "/", nil) 138 | r.ServeHTTP(res, req) 139 | return res.Header().Get("Set-Cookie") 140 | } 141 | 142 | var TestingEnvironment = &TestSetupEnv{ 143 | SessionSecret: "32-byte-long-auth-key-123-45-712", 144 | DBConnectionUrl: "postgres://nas@localhost", 145 | } 146 | 147 | var testingDBConn *sql.DB = nil; 148 | 149 | func RunSpec(flags *TestFlags, testFunc func(*TestEnv)) { 150 | boil.DebugMode = flags != nil && flags.DebugDB 151 | 152 | LastEmailDelivery = &TestEmailDelivery{} 153 | EmailSender = LastEmailDelivery; 154 | gin.SetMode(gin.ReleaseMode) 155 | gin.DefaultWriter = ioutil.Discard 156 | log.SetOutput(ioutil.Discard) 157 | 158 | set := flag.NewFlagSet("test", 0) 159 | set.String( 160 | "session_secret", TestingEnvironment.SessionSecret, "doc", 161 | ) 162 | set.String( 163 | "db_conn_url", TestingEnvironment.DBConnectionUrl, "doc", 164 | ) 165 | 166 | var config Configuration 167 | config = cli.NewContext(nil, set, nil) 168 | 169 | if testingDBConn == nil || testingDBConn.Ping() != nil { 170 | testingDBConn = ConnectDB(config) 171 | } 172 | 173 | ctx := context.Background() 174 | tx, _ := testingDBConn.BeginTx(ctx, nil) 175 | 176 | var router *gin.Engine 177 | var webpack *webpacking.WebPacking 178 | 179 | tenant, err := CreateTenant( 180 | &SignupData{ 181 | Name: "Tester Testing", 182 | Email: fmt.Sprintf("test@test.com"), 183 | Password: "password", 184 | Tenant: "testing", 185 | }, tx, 186 | ) 187 | if err != nil { 188 | panic(err) 189 | } 190 | 191 | 192 | if flags != nil && flags.WithRoutes != nil { 193 | router = gin.New() 194 | 195 | router.Use(testingContextMiddleware(config, tx)) 196 | InitSessions("test", router, config) 197 | IsDevMode = true 198 | fake := webpacking.InstallFakeAssetReader() 199 | defer fake.Restore() 200 | router.SetFuncMap(template.FuncMap{ 201 | "asset": func(asset string) (template.HTML, error) { 202 | return template.HTML(fmt.Sprintf("", asset)), nil 203 | }, 204 | }) 205 | router.LoadHTMLGlob("views/*") 206 | InitViews(router, config) 207 | flags.WithRoutes(router, config, webpack) 208 | } 209 | 210 | defer func() { 211 | tx.Rollback() 212 | }() 213 | 214 | testFunc(&TestEnv{ 215 | Router: router, 216 | DB: tx, 217 | Config: config, 218 | Tenant: tenant, 219 | }) 220 | } 221 | 222 | 223 | func Test(description string, flags *TestFlags, testFunc func(*TestEnv)) { 224 | ginkgo.It(description, func() { 225 | RunSpec(flags, testFunc) 226 | }) 227 | } 228 | 229 | func XTest(description string, flags *TestFlags, testFunc func(*TestEnv)) { 230 | ginkgo.XIt(description, func() {}) 231 | 232 | } 233 | 234 | func FTest(description string, flags *TestFlags, testFunc func(*TestEnv)) { 235 | ginkgo.FIt(description, func() { 236 | RunSpec(flags, testFunc) 237 | }) 238 | } 239 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | "net/http" 8 | "encoding/json" 9 | "github.com/gin-gonic/gin" 10 | "golang.org/x/crypto/bcrypt" 11 | "github.com/gin-contrib/sessions" 12 | "github.com/volatiletech/sqlboiler/boil" 13 | . "github.com/volatiletech/sqlboiler/queries/qm" 14 | "github.com/nathanstitt/hippo/models" 15 | ) 16 | 17 | func IsValidPassword(u *hm.User, password string) bool { 18 | err := bcrypt.CompareHashAndPassword([]byte(u.PasswordDigest), []byte(password)) 19 | return err == nil; 20 | } 21 | 22 | func SetUserPassword(u *hm.User, password string) { 23 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 24 | if err != nil { 25 | panic(err) 26 | } 27 | u.PasswordDigest = string(hashedPassword) 28 | } 29 | 30 | func FindUserByEmail(email string, tx DB) *hm.User { 31 | user, err := hm.Users( 32 | Where("email = ?", strings.ToLower(email)), 33 | ).One(tx) 34 | if err != nil { 35 | return user 36 | } 37 | return user 38 | } 39 | 40 | func CreateUser(email string, tx DB) *hm.User { 41 | var user = &hm.User{ Name: email, Email: strings.ToLower(email) } 42 | user.InsertP(tx, boil.Infer()) 43 | return user 44 | } 45 | 46 | 47 | func SaveUserToSession(user *hm.User, session sessions.Session) { 48 | out, err := json.Marshal(user) 49 | if err != nil { 50 | panic("failed to encode user") 51 | } 52 | session.Set("u", out) 53 | session.Save() 54 | } 55 | 56 | func LoginUser(user *hm.User, c *gin.Context) { 57 | session := sessions.Default(c) 58 | SaveUserToSession(user, session) 59 | } 60 | 61 | func UserFromSession(c *gin.Context) *hm.User { 62 | session := sessions.Default(c) 63 | val := session.Get("u") 64 | if val == nil { 65 | return nil 66 | } 67 | var user *hm.User 68 | err := json.Unmarshal(val.([]byte), &user) 69 | if err != nil { 70 | return nil 71 | } 72 | return user 73 | } 74 | 75 | func TenantAdminUser(tenantID string, db DB) *hm.User { 76 | return hm.Users( 77 | Where("tenant_id=? and role_id=?", tenantID, UserOwnerRoleID), 78 | ).OneP(db) 79 | } 80 | 81 | func TenantGuestUser(tenantID string, db DB) *hm.User { 82 | return hm.Users( 83 | Where("tenant_id=? and role_id=?", tenantID, UserGuestRoleID), 84 | ).OneP(db) 85 | } 86 | 87 | func userForInviteToken(token string, c *gin.Context) (*hm.User, error) { 88 | email, err := decodeInviteToken(token) 89 | if (err != nil) { 90 | log.Printf("Failed to decode token %s: %s", token, err.Error()) 91 | return nil, fmt.Errorf("Failed to authenticate, please retry") 92 | } 93 | db := GetDB(c) 94 | user := FindUserByEmail(email, db) 95 | if user == nil { 96 | user = CreateUser(email, db) 97 | } 98 | LoginUser(user, c) 99 | return user, nil 100 | } 101 | 102 | 103 | 104 | func UserDisplayPasswordResetHandler(c *gin.Context) { 105 | token := c.Query("t") 106 | vars := gin.H{} 107 | if token != "" { 108 | user, err := userForInviteToken(token, c) 109 | if err == nil { 110 | LoginUser(user, c) 111 | vars["user"] = user 112 | } else { 113 | vars["error"] = err 114 | } 115 | } 116 | c.HTML(http.StatusOK, "forgot-password.html", vars) 117 | } 118 | 119 | func UserPasswordResetHandler() func (c *gin.Context) { 120 | return func (c *gin.Context) { 121 | db := GetDB(c) 122 | password := c.PostForm("password") 123 | if password != "" { 124 | user := UserFromSession(c) 125 | if user != nil { 126 | SetUserPassword(user, password) 127 | user.UpdateP(db, boil.Infer()) 128 | c.Redirect(http.StatusFound, "/") 129 | return 130 | } 131 | } 132 | email := c.PostForm("email") 133 | token, _ := EncryptStringProperty("email", email) 134 | user := FindUserByEmail(email, db) 135 | if user != nil { 136 | err := deliverResetEmail(user, token, db, GetConfig(c)) 137 | if err != nil { 138 | RenderErrorPage("Failed to deliver email, please retry", c, &err) 139 | return 140 | } 141 | } 142 | if IsDevMode { 143 | log.Printf("used reset password link: /forgot-password?t=%s\n", token) 144 | } 145 | 146 | c.HTML(http.StatusOK, "invite-sent.html", gin.H{ "email": email}) 147 | } 148 | } 149 | 150 | type SigninData struct { 151 | Tenant string `form:"tenant"` 152 | Email string `form:"email"` 153 | Password string `form:"password"` 154 | } 155 | 156 | func UserLoginHandler(successUrl string) func(c *gin.Context) { 157 | return func(c *gin.Context) { 158 | var form SigninData 159 | if err := c.ShouldBind(&form); err != nil { 160 | RenderErrorPage("Failed to read signin data, please retry", c, &err) 161 | return 162 | } 163 | db := GetDB(c) 164 | 165 | email := strings.ToLower(form.Email) 166 | 167 | user := hm.Users( 168 | InnerJoin("tenants on tenants.id = users.tenant_id and tenants.identifier=?", form.Tenant), 169 | Where("users.email = ?", email), 170 | ).OneP(db) 171 | 172 | if user.ID == "" { 173 | c.HTML(http.StatusOK, "login.html", gin.H{ 174 | "signin": form, 175 | "error": "email or password is incorrect", 176 | }) 177 | return 178 | } 179 | 180 | if !IsValidPassword(user, form.Password) { 181 | c.HTML(http.StatusOK, "login.html", gin.H{ 182 | "signin": form, 183 | "error": "email or password is incorrect", 184 | }) 185 | return 186 | } 187 | LoginUser(user, c) 188 | c.Redirect(http.StatusSeeOther, successUrl) 189 | } 190 | } 191 | 192 | func UserLogoutHandler(returnTo string) func(c *gin.Context) { 193 | return func(c *gin.Context) { 194 | session := sessions.Default(c) 195 | session.Delete("u") 196 | session.Save() 197 | c.Redirect(http.StatusSeeOther, returnTo) 198 | RenderHomepage(&SignupData{}, nil, c) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /user_jwt.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "github.com/dgrijalva/jwt-go" 5 | "github.com/nathanstitt/hippo/models" 6 | ) 7 | 8 | func JWTForUser(u *hm.User, config Configuration) string { 9 | // Create a new token object, specifying signing method 10 | // and the claims you would like it to contain. 11 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 12 | "name": u.Name, 13 | "admin": UserIsAdmin(u, config), 14 | "graphql_claims": jwt.MapClaims{ 15 | "x-hasura-default-role": UserRoleName(u), 16 | "x-hasura-allowed-roles": UserAllowedRoleNames(u), 17 | "x-hasura-user-id": u.ID, 18 | "x-hasura-org-id": u.TenantID, 19 | }, 20 | }) 21 | 22 | // Sign and get the complete encoded token as a string using the secret 23 | tokenString, err := token.SignedString([]byte(config.String("session_secret"))) 24 | if err != nil { 25 | panic(err) 26 | } 27 | return tokenString 28 | } 29 | -------------------------------------------------------------------------------- /user_role_test.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | _ "fmt" // for adhoc printing 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | q "github.com/volatiletech/sqlboiler/queries/qm" 8 | ) 9 | 10 | var _ = Describe("User Role", func() { 11 | 12 | Test("prevents removing the last admin/guest", nil, func(env *TestEnv) { 13 | admin := env.Tenant.Users( 14 | q.Where("role_id = ?", UserAdminRoleID), 15 | ).OneP(env.DB) 16 | var err error 17 | _, err = admin.Delete(env.DB) 18 | Expect(err).To(HaveOccurred()) 19 | 20 | env.Tenant.AddUsersP(env.DB, true, &User{ 21 | Name: "Bob", 22 | Email: "bob@test.com", 23 | RoleID: UserAdminRoleID, 24 | }) 25 | _, err = admin.Delete(env.DB) 26 | Expect(err).ToNot(HaveOccurred()) 27 | 28 | }) 29 | 30 | }) 31 | -------------------------------------------------------------------------------- /user_roles.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "fmt" 5 | "github.com/nathanstitt/hippo/models" 6 | "github.com/volatiletech/sqlboiler/boil" 7 | . "github.com/volatiletech/sqlboiler/queries/qm" 8 | ) 9 | 10 | 11 | const ( 12 | UserGuestRoleID = 1 13 | UserMemberRoleID = 2 14 | UserManagerRoleID = 3 15 | UserOwnerRoleID = 4 16 | ) 17 | 18 | func UserIsGuest(u *hm.User) bool { 19 | return u.RoleID == UserOwnerRoleID 20 | } 21 | func UserIsMember(u *hm.User) bool { 22 | return u.RoleID == UserMemberRoleID 23 | } 24 | func UserIsManager(u *hm.User) bool { 25 | return u.RoleID == UserManagerRoleID 26 | } 27 | func UserIsOwner(u *hm.User) bool { 28 | return u.RoleID == UserOwnerRoleID 29 | } 30 | 31 | func UserIsAdmin(u *hm.User, config Configuration) bool { 32 | return u.RoleID == UserOwnerRoleID && 33 | u.TenantID == config.String("administrator_uuid") 34 | } 35 | 36 | func UserRoleName(u *hm.User) string { 37 | switch u.RoleID { 38 | case UserOwnerRoleID: 39 | return "owner" 40 | case UserManagerRoleID: 41 | return "manager" 42 | case UserMemberRoleID: 43 | return "user" 44 | case UserGuestRoleID: 45 | return "guest" 46 | default: 47 | return "invalid" 48 | } 49 | } 50 | 51 | 52 | func UserAllowedRoleNames(u *hm.User) []string { 53 | switch u.RoleID { 54 | case UserOwnerRoleID: 55 | return []string{"owner", "manager", "member", "guest"} 56 | case UserManagerRoleID: 57 | return []string{"manager", "member", "guest"} 58 | case UserMemberRoleID: 59 | return []string{"member", "guest"} 60 | case UserGuestRoleID: 61 | return []string{"guest"} 62 | default: 63 | return []string{} 64 | } 65 | } 66 | 67 | // Define hook to prevent deleing last owner or guest account 68 | func ensureOwnerAndGuest(exec boil.Executor, u *hm.User) error { 69 | 70 | count := hm.Users(Where("tenant_id = ? and role_id = ?", 71 | u.TenantID, u.RoleID)).CountP(exec) 72 | 73 | if ((UserIsOwner(u) || UserIsGuest(u)) && (count < 2)) { 74 | return fmt.Errorf("all accounts must have at least 1 user with role %s present", UserRoleName(u)); 75 | } 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /user_test.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | // "fmt" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "github.com/dgrijalva/jwt-go" 8 | ) 9 | 10 | var _ = Describe("User", func() { 11 | 12 | Test("creates a JWT token", &TestFlags{WithRoutes: addLoginRoute}, func(env *TestEnv) { 13 | user := env.Tenant.R.Users[0] 14 | tokenString := JWTForUser(user, env.Config) 15 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 16 | return []byte(env.Config.String("session_secret")), nil 17 | }) 18 | Expect(err).To(BeNil()) 19 | claims, ok := token.Claims.(jwt.MapClaims); 20 | Expect(ok).To(BeTrue()) 21 | Expect(token.Valid).To(BeTrue()) 22 | Expect(claims["name"]).To(Equal(user.Name)) 23 | }) 24 | 25 | Test("can get/set user role", &TestFlags{WithRoutes: addLoginRoute}, func(env *TestEnv) { 26 | tenant := env.Tenant 27 | Expect(tenant.R.Users).To(HaveLen(2)) 28 | user := tenant.R.Users[0] 29 | Expect(user.TenantID).To(Equal(tenant.ID)) 30 | 31 | Expect(UserIsOwner(user)).To(BeTrue()) 32 | Expect(UserAllowedRoleNames(user)).Should(ConsistOf( 33 | []string{"admin", "manager", "member", "guest"}, 34 | )) 35 | }) 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /views/application.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Spendily 9 | {{ asset "application.js" }} 10 | 11 | 12 | {{ .bootstrapData }} 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /views/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | We've got some trouble 7 | 9 | 10 | 11 |
12 |

13 | Webservice currently unavailable 14 | Error 500 15 |

16 |

17 | An unexpected condition was encountered. 18 |

19 |

20 | {{ .message }} 21 |

22 |

23 | Our service team has been dispatched to bring it back online. 24 |

25 |
26 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /views/forgot-password-success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Spendily :: Reset Password 8 | {{ asset "home.js" }} 9 | 10 | 11 |
12 | 13 |

Reset Password

14 | 15 |
16 | 17 |
18 | 21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /views/forgot-password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Spendily :: Reset Password 8 | {{ asset "home.js" }} 9 | 10 | 11 |
12 | 13 |

Reset Password

14 | 15 |
16 |
17 | {{ if .user }} 18 | 19 | 20 | {{ else }} 21 | 22 | 23 | {{ end }} 24 |
25 | 26 |
27 | 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /views/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Spendily 8 | {{ asset "home.js" }} 9 | 10 | 11 | 12 |
13 | 14 | 18 | 19 | {{ if .error }} 20 |

{{ .error }}

21 | {{ end }} 22 | 23 |
24 |
25 |

Sign Up for Free

26 | 27 |
28 | 29 |
30 |
31 | 34 | 35 |
36 | 37 |
38 | 41 | 42 |
43 |
44 | 45 |
46 | 49 | 50 |
51 | 52 |
53 | 56 | 57 |
58 | 59 | 60 | 61 |
62 | 63 |
64 | 65 | 96 | 97 |
98 | 99 |
100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /views/invite-sent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Spendily :: Reset Passwor 8 | {{ asset "home.js" }} 9 | 10 | 11 |
12 |

We've emailed a login link to you at {{ .email }}

13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /views/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Login 8 | 9 | 10 | 11 |
12 | 13 | {{ if .signupEnabled }} 14 | 18 | {{ end }} 19 | 20 | {{ if .error }} 21 |

{{ .error }}

22 | {{ end }} 23 | 24 |
25 | 26 | {{ if .signupEnabled }} 27 |
28 |

Sign Up for Free

29 | 30 |
31 | 32 |
33 |
34 | 37 | 38 |
39 | 40 |
41 | 44 | 45 |
46 |
47 | 48 |
49 | 52 | 53 |
54 | 55 |
56 | 59 | 60 |
61 | 62 | 63 | 64 |
65 | 66 |
67 | {{ end }} 68 | 69 |
70 |

Welcome Back!

71 | 72 |
73 | 74 |
75 | 78 | 79 |
80 | 81 |
82 | 85 | 86 |
87 | 88 |
89 | 92 | 93 |
94 | 95 |

Forgot Password?

96 | 97 |
98 | 99 |
100 | 101 |
102 | 103 |
104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /web.go: -------------------------------------------------------------------------------- 1 | package hippo 2 | 3 | import ( 4 | "html/template" 5 | "github.com/gin-gonic/gin" 6 | "github.com/nathanstitt/webpacking" 7 | "github.com/gin-contrib/sessions" 8 | "github.com/gin-contrib/sessions/cookie" 9 | ) 10 | 11 | func InitWebpack(router *gin.Engine, config Configuration) *webpacking.WebPacking { 12 | wpConfig := &webpacking.Config{ 13 | IsDev: IsDevMode, 14 | DevPort: config.String("webpack_dev_port"), 15 | } 16 | packager, err := webpacking.New(wpConfig) 17 | CheckError(err) 18 | err = packager.Run() 19 | CheckError(err) 20 | 21 | router.SetFuncMap(template.FuncMap{ 22 | "asset": packager.AssetHelper(), 23 | }) 24 | return packager 25 | } 26 | 27 | func InitSessions(cookie_name string, r *gin.Engine, config Configuration) { 28 | secret := []byte(config.String("session_secret")) 29 | SessionsKeyValue = secret 30 | store := cookie.NewStore(secret) 31 | r.Use(sessions.Sessions(cookie_name, store)); 32 | } 33 | 34 | func CreateRouter() *gin.Engine { 35 | return gin.New() 36 | } 37 | 38 | func InitViews(r *gin.Engine, config Configuration) { 39 | r.LoadHTMLGlob("views/*.html") 40 | } 41 | 42 | func AddGraphqlProxyRoutes(r *gin.Engine, config Configuration) { 43 | graphql_port := config.Int("graphql_port") 44 | r.OPTIONS("/v1/*query", allowCorsReply) 45 | r.OPTIONS("/v1alpha1/*graphql", allowCorsReply) 46 | r.POST("/v1/*query", reverseProxy(graphql_port)) 47 | r.POST("/v1alpha1/*graphql", reverseProxy(graphql_port)) 48 | r.POST("/apis/migrate", reverseProxy(graphql_port)) 49 | r.GET("/v1alpha1/*graphql", reverseProxy(graphql_port)) 50 | r.GET("/console/*q", reverseProxy(graphql_port)) 51 | } 52 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ManifestPlugin = require('webpack-manifest-plugin'); 3 | const path = require('path'); 4 | 5 | const devMode = process.env.NODE_ENV !== 'production'; 6 | //const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 7 | 8 | const production = process.env.NODE_ENV === 'production'; 9 | const devServerPort = 3808; 10 | const host = process.env.HOST || 'localhost' 11 | const publicURLPath = production ? 12 | `https://truelinetitle.com/public/webpack/` : 13 | `http://${host}:${devServerPort}/webpack/`; 14 | 15 | 16 | module.exports = { 17 | entry: { 18 | application: 'application.js', 19 | home: 'home.js', 20 | }, 21 | mode: production ? 'production' : 'development', 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.(sa|sc|c)ss$/, 26 | use: [ 27 | 'style-loader', 28 | 'css-loader', 29 | 'sass-loader', 30 | ], 31 | }, { 32 | test: /\.jsx?$/, 33 | exclude: /node_modules/, 34 | use: { 35 | loader: 'babel-loader', 36 | options: { 37 | presets: ['@babel/preset-env'] 38 | }, 39 | }, 40 | }, 41 | ] 42 | }, 43 | resolve: { 44 | extensions: ['*', '.js', '.jsx', '.css'], 45 | alias: { 46 | mobx: __dirname + "/node_modules/mobx/lib/mobx.es6.js" 47 | }, 48 | modules: [ 49 | 'node_modules', 50 | 'client', 51 | ] 52 | }, 53 | output: { 54 | path: __dirname + '/public/webpack', 55 | publicPath: production ? "/webpack/" : publicURLPath, 56 | filename: '[name]-[hash].js', 57 | }, 58 | plugins: [ 59 | new webpack.HotModuleReplacementPlugin(), 60 | new ManifestPlugin({ 61 | writeToFileEmit: true, 62 | publicPath: production ? "/public/webpack/" : publicURLPath, 63 | }), 64 | ], 65 | devServer: { 66 | contentBase: './dist', 67 | port: devServerPort, 68 | headers: { 'Access-Control-Allow-Origin': '*' }, 69 | hot: true 70 | } 71 | }; 72 | --------------------------------------------------------------------------------