├── .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 |
21 |
22 |
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 |
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 |
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 |
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 |
62 |
63 |
64 |
65 |
66 |
Welcome Back!
67 |
68 |
94 |
95 |
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 |
65 |
66 |
67 | {{ end }}
68 |
69 |
70 |
Welcome Back!
71 |
72 |
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 |
--------------------------------------------------------------------------------