├── .gitignore
├── files
├── sql
│ ├── migration_20210402.sql
│ ├── migration_20190714.sql
│ ├── migration_20190904.sql
│ └── db.sql
└── template
│ ├── text
│ └── request_password_recovery.txt
│ └── html
│ └── request_password_recovery.html
├── domain
├── string_generator.go
├── hasher.go
├── mailer.go
├── user_storage.go
├── storage_provider.go
├── link.go
├── user.go
├── user
│ ├── storage_provider.go
│ ├── user.go
│ └── user_test.go
└── link
│ ├── link.go
│ └── link_test.go
├── gqlgen.yml
├── infrastructure
├── stringgenerator
│ ├── uuid.go
│ └── mock.go
├── hasher
│ ├── plain.go
│ └── bcrypt.go
├── auth
│ ├── jwt_mock.go
│ └── jwt.go
├── database
│ ├── mysql
│ │ ├── mysql.go
│ │ ├── user.go
│ │ ├── link.go
│ │ └── user_storage.go
│ └── inmemory
│ │ ├── user.go
│ │ ├── link.go
│ │ ├── mem.go
│ │ └── user_storage.go
├── mailer
│ ├── mailtrap.go
│ ├── mock.go
│ └── sendgrid.go
└── storageprovider
│ ├── mock.go
│ └── dropbox.go
├── README.MD
├── config.sample.yaml
├── Dockerfile
├── CONTRIBUTING.MD
├── models_gen.go
├── .gitlab-ci.yml
├── schema.graphql
├── go.mod
├── server
├── fileupload.go
└── server.go
├── resolver.go
└── go.sum
/.gitignore:
--------------------------------------------------------------------------------
1 | config.yaml
2 | cover.out
--------------------------------------------------------------------------------
/files/sql/migration_20210402.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE bcc_drophere.users ADD password varchar(80) NULL;
--------------------------------------------------------------------------------
/domain/string_generator.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | // StringGenerator abstraction
4 | type StringGenerator interface {
5 | Generate() string
6 | }
--------------------------------------------------------------------------------
/files/sql/migration_20190714.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `users`
2 | ADD `recover_password_token` varchar(255) NULL,
3 | ADD `recover_password_token_expiry` datetime NULL;
--------------------------------------------------------------------------------
/domain/hasher.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | // Hasher abstraction
4 | type Hasher interface {
5 | Hash(s string) (string, error)
6 | Verify(hashed, plain string) bool
7 | }
--------------------------------------------------------------------------------
/files/template/text/request_password_recovery.txt:
--------------------------------------------------------------------------------
1 | {{define "request_password_recovery_text"}}
2 | Password Recovery Token: {{.Token}}
3 | {{.ResetPasswordLink}}
4 | {{end}}
--------------------------------------------------------------------------------
/gqlgen.yml:
--------------------------------------------------------------------------------
1 | # .gqlgen.yml example
2 | #
3 | # Refer to https://gqlgen.com/config/
4 | # for detailed .gqlgen.yml documentation.
5 |
6 | schema:
7 | - schema.graphql
8 | exec:
9 | filename: generated.go
10 | model:
11 | filename: models_gen.go
12 | resolver:
13 | filename: resolver.go
14 | type: Resolver
15 |
--------------------------------------------------------------------------------
/domain/mailer.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import "errors"
4 |
5 | // ErrTemplateNotFound error
6 | var ErrTemplateNotFound = errors.New("Template not found")
7 |
8 | // MailAddress model
9 | type MailAddress struct {
10 | Address string
11 | Name string
12 | }
13 |
14 | // Mailer abstraction
15 | type Mailer interface {
16 | Send(from, to MailAddress, subject, messagePlain, messageHTML string) error
17 | }
18 |
--------------------------------------------------------------------------------
/infrastructure/stringgenerator/uuid.go:
--------------------------------------------------------------------------------
1 | package stringgenerator
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/bccfilkom/drophere-go/domain"
7 | "github.com/gofrs/uuid"
8 | )
9 |
10 | type myUUID struct{}
11 |
12 | // NewUUID returns uuid token generator
13 | func NewUUID() domain.StringGenerator {
14 | return &myUUID{}
15 | }
16 |
17 | // Generate generates random string
18 | func (u *myUUID) Generate() string {
19 | token := uuid.Must(uuid.NewV4())
20 | return strings.ReplaceAll(token.String(), "-", "")
21 | }
22 |
--------------------------------------------------------------------------------
/infrastructure/hasher/plain.go:
--------------------------------------------------------------------------------
1 | package hasher
2 |
3 | import "github.com/bccfilkom/drophere-go/domain"
4 |
5 | type notAHasher struct{}
6 |
7 | // NewNotAHasher returns notAHasher instance for testing purpose
8 | func NewNotAHasher() domain.Hasher {
9 | return ¬AHasher{}
10 | }
11 |
12 | // Hash implementation
13 | func (n *notAHasher) Hash(s string) (string, error) {
14 | return s, nil
15 | }
16 |
17 | // Verify implementation
18 | func (n *notAHasher) Verify(hashed, plain string) bool {
19 | return hashed == plain
20 | }
21 |
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 | # Drophere
2 | Drophere is a platform for submitting your task quickly.
3 |
4 | ## How to run locally
5 | 1. Copy the config file from ``config.sample.yaml`` to ``config.yaml``.
6 | 2. Execute db.sql from this [file][migration-file].
7 | 3. Run ``go run server/*.go`` to start the app.
8 | 4. Browse to ``localhost:8080`` by your own browser.
9 |
10 | ## Contributing to this project
11 | Interested in contributing? please check out [the Contributing Guide](CONTRIBUTING.MD) to get started
12 |
13 | [migration-file]: files/sql/db.sql
14 |
--------------------------------------------------------------------------------
/infrastructure/stringgenerator/mock.go:
--------------------------------------------------------------------------------
1 | package stringgenerator
2 |
3 | import "github.com/bccfilkom/drophere-go/domain"
4 |
5 | var preset string
6 |
7 | type mockStringGenerator struct{}
8 |
9 | // SetMockResult set the string that Generate returns
10 | func SetMockResult(s string) {
11 | preset = s
12 | }
13 |
14 | // NewMock returns new mockStringGenerator
15 | func NewMock() domain.StringGenerator {
16 | return &mockStringGenerator{}
17 | }
18 |
19 | // Generate returns pre-set string
20 | func (m *mockStringGenerator) Generate() string {
21 | return preset
22 | }
23 |
--------------------------------------------------------------------------------
/infrastructure/auth/jwt_mock.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "strconv"
5 | "time"
6 |
7 | "github.com/bccfilkom/drophere-go/domain"
8 | )
9 |
10 | type jwtAuthenticatorMock struct{}
11 |
12 | // NewJWTMock func
13 | func NewJWTMock() domain.Authenticator {
14 | return &jwtAuthenticatorMock{}
15 | }
16 |
17 | // Authenticate mock
18 | func (j *jwtAuthenticatorMock) Authenticate(u *domain.User) (*domain.UserCredentials, error) {
19 | t := time.Now().Add(time.Hour)
20 | return &domain.UserCredentials{
21 | Token: "user_token_"+strconv.Itoa(int(u.ID)),
22 | Expiry: &t,
23 | }, nil
24 | }
--------------------------------------------------------------------------------
/infrastructure/database/mysql/mysql.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/jinzhu/gorm"
7 | )
8 |
9 | // New creates new GORM database instance
10 | func New(dsn string) (*gorm.DB, error) {
11 | db, err := gorm.Open("mysql", dsn)
12 | if err != nil {
13 | return nil, err
14 | }
15 |
16 | db = db.
17 | Set("gorm:table_options", "DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci ENGINE=InnoDB").
18 | Set("gorm:auto_preload", false)
19 |
20 | db.DB().SetMaxIdleConns(10)
21 | db.DB().SetMaxOpenConns(100)
22 | db.DB().SetConnMaxLifetime(time.Minute)
23 |
24 | // db = db.Set("gorm:auto_preload", true)
25 |
26 | return db, nil
27 | }
28 |
--------------------------------------------------------------------------------
/files/template/html/request_password_recovery.html:
--------------------------------------------------------------------------------
1 | {{define "request_password_recovery_html"}}
2 |
3 |
Recover Password
4 |
5 |
6 |
17 | {{end}}
--------------------------------------------------------------------------------
/config.sample.yaml:
--------------------------------------------------------------------------------
1 | app:
2 | debug: false
3 | storageRootDirectoryName: "drophere"
4 | templatePath: "files/template"
5 | passwordRecovery:
6 | tokenExpiryDuration: 5 # in minutes
7 | webURL: "http://localhost:3000/reset-password"
8 | mailer:
9 | email: "bot@comeapp.id"
10 | name: "Drophere Bot"
11 |
12 | db:
13 | dsn: "user:pwd@tcp(localhost:3306)/drophere?charset=utf8mb4&collation=utf8mb4_general_ci&parseTime=true"
14 |
15 | jwt:
16 | secret: "please-put-your-secret-key-here"
17 | duration: 8760 # in hours (token duration)
18 | signingAlgorithm: HS256
19 |
20 | mailer:
21 | mailtrap:
22 | host: "smtp.mailtrap.io"
23 | port: 587
24 | username: ""
25 | password:
26 | encryption: "tls"
27 | sendgrid:
28 | apiKey: ""
29 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:stretch-slim
2 | ARG SOURCE_LOCATION=./build
3 | EXPOSE 8888
4 |
5 | # install dependencies
6 | RUN apt-get update && \
7 | apt-get install -y --no-install-recommends \
8 | apt-transport-https \
9 | curl \
10 | ca-certificates \
11 | && apt-get clean \
12 | && apt-get autoremove \
13 | && rm -rf /var/lib/apt/lists/*
14 |
15 | # create new user
16 | RUN useradd --create-home drophere
17 |
18 | # create new directory
19 | RUN mkdir -p /home/drophere/drophere-service
20 |
21 | # specify directory
22 | WORKDIR /home/drophere/drophere-service
23 | COPY ${SOURCE_LOCATION} .
24 |
25 | # change owner to user "drophere"
26 | RUN chown -R drophere:drophere .
27 |
28 | USER drophere
29 | RUN chmod +x drophere-service
30 |
31 | CMD ["./drophere-service"]
32 |
--------------------------------------------------------------------------------
/infrastructure/hasher/bcrypt.go:
--------------------------------------------------------------------------------
1 | package hasher
2 |
3 | import (
4 | "github.com/bccfilkom/drophere-go/domain"
5 | "golang.org/x/crypto/bcrypt"
6 | )
7 |
8 | type bcryptHasher struct {
9 | cost int
10 | }
11 |
12 | // NewBcryptHasher bcrypt hasher that implements Hasher interface
13 | func NewBcryptHasher() domain.Hasher {
14 | return &bcryptHasher{
15 | cost: 12,
16 | }
17 | }
18 |
19 | // Hash implementation
20 | func (b *bcryptHasher) Hash(s string) (string, error) {
21 | hashed, err := bcrypt.GenerateFromPassword([]byte(s), b.cost)
22 | if err != nil {
23 | return "", err
24 | }
25 | return string(hashed), nil
26 | }
27 |
28 | // Verify implementation
29 | func (b *bcryptHasher) Verify(hashed, plain string) bool {
30 | return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain)) == nil
31 | }
32 |
--------------------------------------------------------------------------------
/infrastructure/mailer/mailtrap.go:
--------------------------------------------------------------------------------
1 | package mailer
2 |
3 | import (
4 | "github.com/bccfilkom/drophere-go/domain"
5 |
6 | "gopkg.in/gomail.v2"
7 | )
8 |
9 | type mailtrap struct {
10 | dialer *gomail.Dialer
11 | }
12 |
13 | // NewMailtrap returns new mailtrap instance
14 | func NewMailtrap(user, password string) domain.Mailer {
15 | dialer := gomail.NewDialer("smtp.mailtrap.io", 587, user, password)
16 | return &mailtrap{dialer}
17 | }
18 |
19 | // Send sends the email to mailtrap server
20 | func (m *mailtrap) Send(from, to domain.MailAddress, subject, messagePlain, messageHTML string) error {
21 | msg := gomail.NewMessage()
22 | msg.SetAddressHeader("From", from.Address, from.Name)
23 | msg.SetAddressHeader("To", to.Address, to.Name)
24 | msg.SetHeader("Subject", subject)
25 | msg.SetBody("text/html", messageHTML)
26 | msg.AddAlternative("text/plain", messagePlain)
27 |
28 | return m.dialer.DialAndSend(msg)
29 | }
30 |
--------------------------------------------------------------------------------
/infrastructure/storageprovider/mock.go:
--------------------------------------------------------------------------------
1 | package storageprovider
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/bccfilkom/drophere-go/domain"
7 | )
8 |
9 | var sharedAccountInfo domain.StorageProviderAccountInfo
10 |
11 | type mock struct{}
12 |
13 | // SetSharedAccountInfo set the sharedAccountInfo object
14 | func SetSharedAccountInfo(accountInfo domain.StorageProviderAccountInfo) {
15 | sharedAccountInfo = accountInfo
16 | }
17 |
18 | // NewMock returns new mock
19 | func NewMock() domain.StorageProviderService {
20 | return &mock{}
21 | }
22 |
23 | // ID returns provider ID
24 | func (m *mock) ID() uint {
25 | return 1
26 | }
27 |
28 | // AccountInfo mock
29 | func (m *mock) AccountInfo(cred domain.StorageProviderCredential) (domain.StorageProviderAccountInfo, error) {
30 | return sharedAccountInfo, nil
31 | }
32 |
33 | // Upload mock
34 | func (m *mock) Upload(cred domain.StorageProviderCredential, file io.Reader, fileName, slug string) error {
35 | return nil
36 | }
--------------------------------------------------------------------------------
/files/sql/migration_20190904.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `user_storage_credentials` (
2 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
3 | `user_id` int(10) unsigned NOT NULL,
4 | `provider_id` int(10) unsigned NOT NULL,
5 | `provider_credential` varchar(255) NOT NULL,
6 | `email` varchar(255) NOT NULL DEFAULT '',
7 | `photo` varchar(255) NOT NULL DEFAULT '',
8 | PRIMARY KEY (`id`),
9 | UNIQUE KEY `user_provider_unique` (`user_id`, `provider_id`),
10 | KEY `usc_user_id_users_id_foreign` (`user_id`),
11 | CONSTRAINT `usc_user_id_users_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
12 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
13 |
14 | ALTER TABLE `links`
15 | ADD `user_storage_credential_id` int(10) unsigned NULL,
16 | ADD KEY `links_usc_id_foreign` (`user_storage_credential_id`),
17 | ADD CONSTRAINT `links_usc_id_foreign` FOREIGN KEY (`user_storage_credential_id`) REFERENCES `user_storage_credentials` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
--------------------------------------------------------------------------------
/files/sql/db.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `users` (
2 | `id` int unsigned NOT NULL AUTO_INCREMENT,
3 | `email` varchar(255) NOT NULL,
4 | `name` varchar(255) NOT NULL,
5 | `dropbox_token` varchar(255) DEFAULT NULL,
6 | `drive_token` varchar(255) DEFAULT NULL,
7 | PRIMARY KEY (`id`),
8 | UNIQUE KEY `email` (`email`)
9 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
10 |
11 | CREATE TABLE `links` (
12 | `id` int unsigned NOT NULL AUTO_INCREMENT,
13 | `user_id` int unsigned NOT NULL,
14 | `title` varchar(255) CHARACTER SET utf8mb4 NOT NULL,
15 | `password` varchar(255) NOT NULL,
16 | `slug` varchar(255) NOT NULL,
17 | `description` text CHARACTER SET utf8mb4 NOT NULL,
18 | `deadline` datetime NULL DEFAULT NULL,
19 | PRIMARY KEY (`id`),
20 | UNIQUE KEY `slug` (`slug`),
21 | KEY `links_user_id_users_id_foreign` (`user_id`),
22 | CONSTRAINT `links_user_id_users_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
23 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
24 |
--------------------------------------------------------------------------------
/infrastructure/mailer/mock.go:
--------------------------------------------------------------------------------
1 | package mailer
2 |
3 | import "github.com/bccfilkom/drophere-go/domain"
4 |
5 | // MockMessage is for testing purpose
6 | type MockMessage struct {
7 | From string
8 | To string
9 | Title string
10 | MessagePlain string
11 | MessageHTML string
12 | }
13 |
14 | // MockMessages is an in-memory storage for testing purpose
15 | var MockMessages []MockMessage
16 |
17 | func init() {
18 | MockMessages = make([]MockMessage, 0)
19 | }
20 |
21 | // ClearMessages reset the MockMessages
22 | func ClearMessages() {
23 | MockMessages = make([]MockMessage, 0)
24 | }
25 |
26 | type mockMailer struct{}
27 |
28 | // NewMockMailer returns new mockMailer instance
29 | func NewMockMailer() domain.Mailer {
30 | return &mockMailer{}
31 | }
32 |
33 | // Send sends the email to mockMailer server
34 | func (m *mockMailer) Send(from, to domain.MailAddress, subject, messagePlain, messageHTML string) error {
35 | MockMessages = append(MockMessages, MockMessage{from.Address, to.Address, subject, messagePlain, messageHTML})
36 | return nil
37 | }
38 |
--------------------------------------------------------------------------------
/infrastructure/database/inmemory/user.go:
--------------------------------------------------------------------------------
1 | package inmemory
2 |
3 | import "github.com/bccfilkom/drophere-go/domain"
4 |
5 | type userRepository struct {
6 | db *DB
7 | }
8 |
9 | // NewUserRepository func
10 | func NewUserRepository(db *DB) domain.UserRepository {
11 | return &userRepository{db}
12 | }
13 |
14 | // Create implementation
15 | func (repo *userRepository) Create(user *domain.User) (*domain.User, error) {
16 | return repo.db.CreateUser(user)
17 | }
18 |
19 | // FindByEmail implementation
20 | func (repo *userRepository) FindByEmail(email string) (*domain.User, error) {
21 | return repo.db.FindUserByEmail(email)
22 | }
23 |
24 | // FindByID implementation
25 | func (repo *userRepository) FindByID(id uint) (*domain.User, error) {
26 | return repo.db.FindUserByID(id)
27 | }
28 |
29 | // Update implementation
30 | func (repo *userRepository) Update(u *domain.User) (*domain.User, error) {
31 | updated := false
32 | for i := range repo.db.users {
33 | if repo.db.users[i].ID == u.ID {
34 | repo.db.users[i] = *u
35 | updated = true
36 | break
37 | }
38 | }
39 |
40 | if !updated {
41 | repo.db.users = append(repo.db.users, *u)
42 | }
43 | return u, nil
44 | }
45 |
--------------------------------------------------------------------------------
/infrastructure/mailer/sendgrid.go:
--------------------------------------------------------------------------------
1 | package mailer
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/bccfilkom/drophere-go/domain"
7 |
8 | sendgridClient "github.com/sendgrid/sendgrid-go"
9 | mailHelper "github.com/sendgrid/sendgrid-go/helpers/mail"
10 | )
11 |
12 | type sendgrid struct {
13 | apiKey string
14 | debug bool
15 | }
16 |
17 | // NewSendgrid returns new sendgrid instance
18 | func NewSendgrid(apiKey string, debug bool) domain.Mailer {
19 | return &sendgrid{apiKey, debug}
20 | }
21 |
22 | // Send sends the email to sendgrid server
23 | func (s *sendgrid) Send(from, to domain.MailAddress, subject, messagePlain, messageHTML string) error {
24 |
25 | f := mailHelper.NewEmail(
26 | from.Name,
27 | from.Address,
28 | )
29 | t := mailHelper.NewEmail(
30 | to.Name,
31 | to.Address,
32 | )
33 | msg := mailHelper.NewSingleEmail(f, subject, t, messagePlain, messageHTML)
34 | c := sendgridClient.NewSendClient(s.apiKey)
35 |
36 | // log the response
37 | resp, err := c.Send(msg)
38 | if s.debug {
39 | log.Println("Sendgrid: StatusCode:", resp.StatusCode)
40 | log.Println("Sendgrid: Body:", resp.Body)
41 | log.Println("Sendgrid: Headers:", resp.Headers)
42 | }
43 |
44 | return err
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/domain/user_storage.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import "errors"
4 |
5 | var (
6 | // ErrUserStorageCredentialNotFound error
7 | ErrUserStorageCredentialNotFound = errors.New("User Storage Credential not found")
8 | )
9 |
10 | // UserStorageCredential stores information about user's account on
11 | // a storage provider (e.g. Dropbox)
12 | type UserStorageCredential struct {
13 | ID uint
14 | UserID uint
15 | User User
16 | ProviderID uint
17 | ProviderCredential string
18 | Email string
19 | Photo string
20 | }
21 |
22 | // UserStorageCredentialFilters stores filters to be used by
23 | // Find function in UserStorageCredentialRepository
24 | type UserStorageCredentialFilters struct {
25 | UserIDs []uint
26 | ProviderIDs []uint
27 | }
28 |
29 | // UserStorageCredentialRepository abstraction
30 | type UserStorageCredentialRepository interface {
31 | Find(filters UserStorageCredentialFilters, withUserRelation bool) ([]UserStorageCredential, error)
32 | FindByID(id uint, withUserRelation bool) (UserStorageCredential, error)
33 | Create(cred UserStorageCredential) (UserStorageCredential, error)
34 | Update(cred UserStorageCredential) (UserStorageCredential, error)
35 | Delete(cred UserStorageCredential) error
36 | }
37 |
--------------------------------------------------------------------------------
/CONTRIBUTING.MD:
--------------------------------------------------------------------------------
1 | # Contributing guide
2 |
3 | Interested in contributing to this project? As an open source project, we'd really appreciate any help and contributions!
4 |
5 | ## Contribute code for this project
6 | Here's how to submit a Pull Request (PR):
7 |
8 | 1. [Fork this repository on GitHub][fork].
9 | * If you have forked this repository, please follow the following guide.
10 | * Setup an [Upstream remote][configure-upstream] to this repository
11 | `https://github.com/bccfilkom/drophere-backend.git`
12 | * [Sync your fork][sync-fork] with the upstream.
13 | 2. Clone your fork of the repository to your local computer.
14 | 3. Create a branch with descriptive name to work (i.e., `git checkout -b feature/your-feature-name`).
15 | 4. Make changes, commit them, and push the branch to your repository fork.
16 | 5. Writing unit test would be appreciated.
17 | 6. [Submit a pull request][pull-req] to the master branch.
18 | 7. Address review comments if any.
19 |
20 | [fork]: https://help.github.com/articles/fork-a-repo
21 | [configure-upstream]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/configuring-a-remote-for-a-fork
22 | [sync-fork]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/syncing-a-fork
23 | [pull-req]: https://help.github.com/articles/using-pull-requests
24 |
--------------------------------------------------------------------------------
/models_gen.go:
--------------------------------------------------------------------------------
1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
2 |
3 | package drophere_go
4 |
5 | import (
6 | "time"
7 | )
8 |
9 | type Link struct {
10 | ID int `json:"id"`
11 | Title string `json:"title"`
12 | IsProtected bool `json:"isProtected"`
13 | Slug *string `json:"slug"`
14 | Description *string `json:"description"`
15 | Deadline *time.Time `json:"deadline"`
16 | StorageProvider *StorageProvider `json:"storageProvider"`
17 | }
18 |
19 | type Message struct {
20 | Message string `json:"message"`
21 | }
22 |
23 | type StorageProvider struct {
24 | ID int `json:"id"`
25 | ProviderID int `json:"providerId"`
26 | Email string `json:"email"`
27 | Photo string `json:"photo"`
28 | }
29 |
30 | type Token struct {
31 | LoginToken string `json:"loginToken"`
32 | }
33 |
34 | type User struct {
35 | ID int `json:"id"`
36 | Email string `json:"email"`
37 | Name string `json:"name"`
38 | DropboxAuthorized bool `json:"dropboxAuthorized"`
39 | DropboxEmail *string `json:"dropboxEmail"`
40 | DropboxAvatar *string `json:"dropboxAvatar"`
41 | ConnectedStorageProviders []*StorageProvider `json:"connectedStorageProviders"`
42 | }
43 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: docker:stable
2 |
3 | variables:
4 | DOCKER_DRIVER: overlay2
5 | VERSION: 0.1.0
6 |
7 | stages:
8 | - build
9 | - package
10 |
11 | compile binary:
12 | stage: build
13 | image: golang:1.12.6-stretch
14 | before_script:
15 | - go version
16 | # installing dependencies
17 | - go get ./...
18 | variables:
19 | CGO_ENABLED: "1"
20 | script:
21 | - ls -hal
22 | - go test -v -coverprofile cover.out ./...
23 | - mkdir build
24 | - go build -v -tags netgo -o build/drophere-service server/*.go
25 | - cp -vr files build/
26 | - ls -hal build
27 | artifacts:
28 | paths:
29 | - build/drophere-service
30 | - build/files
31 | only:
32 | - develop
33 | - tags
34 |
35 | package staging:
36 | stage: package
37 | services:
38 | - docker:dind
39 | variables:
40 | IMAGE_TAG: $CI_REGISTRY_IMAGE:staging-$VERSION
41 | IMAGE_TAG_LATEST: $CI_REGISTRY_IMAGE:staging-latest
42 | before_script:
43 | - docker info
44 | script:
45 | # Copy configuration file
46 | - cp -vr config.sample.yaml build/config.yaml
47 | - ls -hal build
48 | # Build Docker image
49 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
50 | - docker build -t $IMAGE_TAG .
51 | - docker tag $IMAGE_TAG $IMAGE_TAG_LATEST
52 | - docker push $CI_REGISTRY_IMAGE
53 | artifacts:
54 | paths:
55 | - build/config.yaml
56 | only:
57 | - develop
58 |
--------------------------------------------------------------------------------
/infrastructure/database/mysql/user.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "github.com/bccfilkom/drophere-go/domain"
5 | "github.com/jinzhu/gorm"
6 | )
7 |
8 | type userRepository struct {
9 | db *gorm.DB
10 | }
11 |
12 | // NewUserRepository func
13 | func NewUserRepository(db *gorm.DB) domain.UserRepository {
14 | return &userRepository{db}
15 | }
16 |
17 | // Create implementation
18 | func (repo *userRepository) Create(user *domain.User) (*domain.User, error) {
19 | if err := repo.db.Create(user).Error; err != nil {
20 | return nil, err
21 | }
22 | return user, nil
23 | }
24 |
25 | // FindByEmail implementation
26 | func (repo *userRepository) FindByEmail(email string) (*domain.User, error) {
27 | user := domain.User{}
28 | if q := repo.db.
29 | Where("`email` = ? ", email).
30 | Find(&user); q.RecordNotFound() {
31 | return nil, domain.ErrUserNotFound
32 | } else if q.Error != nil {
33 | return nil, q.Error
34 | }
35 | return &user, nil
36 | }
37 |
38 | // FindByID implementation
39 | func (repo *userRepository) FindByID(id uint) (*domain.User, error) {
40 | user := domain.User{}
41 | if q := repo.db.
42 | Find(&user, id); q.RecordNotFound() {
43 | return nil, domain.ErrUserNotFound
44 | } else if q.Error != nil {
45 | return nil, q.Error
46 | }
47 | return &user, nil
48 | }
49 |
50 | // Update implementation
51 | func (repo *userRepository) Update(u *domain.User) (*domain.User, error) {
52 | if err := repo.db.Save(u).Error; err != nil {
53 | return nil, err
54 | }
55 | return u, nil
56 | }
57 |
--------------------------------------------------------------------------------
/domain/storage_provider.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import (
4 | "errors"
5 | "io"
6 | )
7 |
8 | var (
9 | // ErrStorageProviderInvalid error
10 | ErrStorageProviderInvalid = errors.New("Invalid Storage Provider ID")
11 | )
12 |
13 | // StorageProvider domain model
14 | // type StorageProvider struct {
15 | // ID uint
16 | // Name string
17 | // }
18 |
19 | // StorageProviderCredential stores data needed to access
20 | // storage provider API
21 | type StorageProviderCredential struct {
22 | UserAccessToken string
23 | }
24 |
25 | // StorageProviderAccountInfo domain model
26 | type StorageProviderAccountInfo struct {
27 | Email string
28 | Photo string
29 | }
30 |
31 | // StorageProviderService abstraction
32 | type StorageProviderService interface {
33 | ID() uint
34 | AccountInfo(creds StorageProviderCredential) (StorageProviderAccountInfo, error)
35 | Upload(creds StorageProviderCredential, file io.Reader, fileName, slug string) error
36 | }
37 |
38 | // StorageProviderPool stores a collection of Storage Provider Service
39 | // with provider ID as the key
40 | type StorageProviderPool struct {
41 | pool map[uint]StorageProviderService
42 | }
43 |
44 | // Get returns a StorageProviderService instance identified by its provider ID
45 | func (p *StorageProviderPool) Get(providerID uint) (StorageProviderService, error) {
46 | sps, ok := p.pool[providerID]
47 | if !ok {
48 | return nil, ErrStorageProviderInvalid
49 | }
50 | return sps, nil
51 | }
52 |
53 | // Register stores a StorageProviderService instance to the pool
54 | func (p *StorageProviderPool) Register(sps StorageProviderService) {
55 | if p.pool == nil {
56 | p.pool = make(map[uint]StorageProviderService)
57 | }
58 |
59 | if sps != nil {
60 | p.pool[sps.ID()] = sps
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/domain/link.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import (
4 | "errors"
5 | "time"
6 | )
7 |
8 | var (
9 | // ErrLinkDuplicatedSlug error
10 | ErrLinkDuplicatedSlug = errors.New("Duplicated slug")
11 | // ErrLinkInvalidPassword error
12 | ErrLinkInvalidPassword = errors.New("Invalid password")
13 | // ErrLinkNotFound error
14 | ErrLinkNotFound = errors.New("Not found")
15 | )
16 |
17 | // Link domain model
18 | type Link struct {
19 | ID uint
20 | UserID uint
21 | User *User
22 | Title string
23 | Password string
24 | Slug string
25 | Deadline *time.Time
26 | Description string
27 | UserStorageCredentialID *uint
28 | UserStorageCredential *UserStorageCredential
29 | }
30 |
31 | // IsProtected checks if the link is protected with password
32 | func (l *Link) IsProtected() bool {
33 | return l.Password != ""
34 | }
35 |
36 | // LinkService abstraction
37 | type LinkService interface {
38 | CheckLinkPassword(l *Link, password string) bool
39 | CreateLink(title, slug, description string, deadline *time.Time, password *string, user *User, providerID *uint) (*Link, error)
40 | UpdateLink(id uint, title, slug string, description *string, deadline *time.Time, password *string, providerID *uint) (*Link, error)
41 | DeleteLink(id uint) error
42 | FetchLink(id uint) (*Link, error)
43 | FindLinkBySlug(slug string) (*Link, error)
44 | ListLinks(userID uint) ([]Link, error)
45 | }
46 |
47 | // LinkRepository abstraction
48 | type LinkRepository interface {
49 | Create(l *Link) (*Link, error)
50 | Delete(l *Link) error
51 | FindByID(id uint) (*Link, error)
52 | FindBySlug(slug string) (*Link, error)
53 | ListByUser(userID uint) ([]Link, error)
54 | Update(l *Link) (*Link, error)
55 | }
56 |
--------------------------------------------------------------------------------
/schema.graphql:
--------------------------------------------------------------------------------
1 | scalar Time
2 |
3 | type StorageProvider {
4 | id: Int!
5 | providerId: Int!
6 | email: String!
7 | photo: String!
8 | }
9 |
10 | type User {
11 | id: Int!
12 | email: String!
13 | name: String!
14 | dropboxAuthorized: Boolean!
15 | dropboxEmail: String
16 | dropboxAvatar: String
17 | connectedStorageProviders: [StorageProvider!]!
18 | }
19 | type Token {
20 | loginToken: String!
21 | #expiry: Time
22 | }
23 | type Link {
24 | id: Int!
25 | title: String!
26 | isProtected: Boolean!
27 | slug: String
28 | description: String
29 | deadline: Time
30 | storageProvider: StorageProvider
31 | ## storageProvider is null if the link is not connected to any storage provider
32 | }
33 | type Message {
34 | message: String!
35 | }
36 |
37 | # the schema allows the following query:
38 | type Query {
39 | ## TODO: change links type below to [Link!]!
40 | links: [Link]
41 | me: User
42 | link(slug: String!): Link
43 | }
44 | type Mutation {
45 | # Register new user
46 | register(email: String!, password: String!, name: String!): Token
47 | login(email: String!, password: String!): Token
48 | requestPasswordRecovery(email: String!): Message
49 | recoverPassword(email: String!, recoverToken: String!, newPassword: String!): Token
50 | updatePassword(oldPassword: String!, newPassword: String!): Message
51 | updateProfile(newName: String!): Message
52 | connectStorageProvider(providerId: Int!, providerToken: String!): Message
53 | disconnectStorageProvider(providerId: Int!): Message
54 | createLink(title: String!, slug: String!, description: String, deadline: Time, password: String, providerId: Int): Link
55 | updateLink(linkId: Int!, title: String!, slug: String!, description: String, deadline: Time, password: String, providerId: Int): Link
56 | deleteLink(linkId: Int!): Message
57 | checkLinkPassword(linkId: Int!, password: String!): Message
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/domain/user.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import (
4 | "errors"
5 | "time"
6 | )
7 |
8 | var (
9 | // ErrUserDuplicated error
10 | ErrUserDuplicated = errors.New("Duplicated email")
11 | // ErrUserInvalidPassword error
12 | ErrUserInvalidPassword = errors.New("Invalid password")
13 | // ErrUserNotFound error
14 | ErrUserNotFound = errors.New("User not found")
15 | // ErrUserPasswordRecoveryTokenExpired error
16 | ErrUserPasswordRecoveryTokenExpired = errors.New("Password recovery token is expired")
17 | )
18 |
19 | // User model
20 | type User struct {
21 | ID uint
22 | Email string
23 | Name string
24 | Password string
25 | DropboxToken *string
26 | DriveToken *string
27 | RecoverPasswordToken *string
28 | RecoverPasswordTokenExpiry *time.Time
29 | }
30 |
31 | // UserCredentials model
32 | type UserCredentials struct {
33 | Token string
34 | Expiry *time.Time
35 | }
36 |
37 | // UserService abstraction
38 | type UserService interface {
39 | Register(email, name, password string) (*User, error)
40 | Auth(email, password string) (*UserCredentials, error)
41 | Update(userID uint, name, password, oldPassword *string) (*User, error)
42 | ConnectStorageProvider(userID, providerID uint, providerCredential string) error
43 | DisconnectStorageProvider(userID, providerID uint) error
44 | ListStorageProviders(userID uint) ([]UserStorageCredential, error)
45 | UpdateStorageToken(userID uint, dropboxToken *string) (*User, error)
46 | RequestPasswordRecovery(email string) error
47 | RecoverPassword(email, token, newPassword string) error
48 | }
49 |
50 | // UserRepository abstraction
51 | type UserRepository interface {
52 | Create(u *User) (*User, error)
53 | FindByEmail(email string) (*User, error)
54 | FindByID(id uint) (*User, error)
55 | Update(u *User) (*User, error)
56 | }
57 |
58 | // Authenticator is external authentication service
59 | type Authenticator interface {
60 | Authenticate(u *User) (*UserCredentials, error)
61 | }
62 |
--------------------------------------------------------------------------------
/infrastructure/database/inmemory/link.go:
--------------------------------------------------------------------------------
1 | package inmemory
2 |
3 | import "github.com/bccfilkom/drophere-go/domain"
4 |
5 | type linkRepository struct {
6 | db *DB
7 | }
8 |
9 | // NewLinkRepository func
10 | func NewLinkRepository(db *DB) domain.LinkRepository {
11 | return &linkRepository{db}
12 | }
13 |
14 | // Create implementation
15 | func (repo *linkRepository) Create(l *domain.Link) (*domain.Link, error) {
16 | l.ID = uint(len(repo.db.links) + 1)
17 | repo.db.links = append(repo.db.links, *l)
18 | return l, nil
19 | }
20 |
21 | // Delete implementation
22 | func (repo *linkRepository) Delete(l *domain.Link) error {
23 |
24 | for i := range repo.db.links {
25 | if repo.db.links[i].ID == l.ID {
26 | repo.db.links = append(repo.db.links[:i], repo.db.links[i+1:]...)
27 | break
28 | }
29 | }
30 |
31 | return nil
32 | }
33 |
34 | // FindByID implementation
35 | func (repo *linkRepository) FindByID(id uint) (*domain.Link, error) {
36 | for i := range repo.db.links {
37 | if repo.db.links[i].ID == id {
38 | return &repo.db.links[i], nil
39 | }
40 | }
41 |
42 | return nil, domain.ErrLinkNotFound
43 | }
44 |
45 | // FindBySlug implementation
46 | func (repo *linkRepository) FindBySlug(slug string) (*domain.Link, error) {
47 | for i := range repo.db.links {
48 | if repo.db.links[i].Slug == slug {
49 | return &repo.db.links[i], nil
50 | }
51 | }
52 |
53 | return nil, domain.ErrLinkNotFound
54 | }
55 |
56 | // ListByUser implementation
57 | func (repo *linkRepository) ListByUser(userID uint) ([]domain.Link, error) {
58 | links := make([]domain.Link, 0, len(repo.db.links))
59 | for _, link := range repo.db.links {
60 | if link.UserID == userID {
61 | links = append(links, link)
62 | }
63 | }
64 |
65 | return links, nil
66 | }
67 |
68 | // Update implementation
69 | func (repo *linkRepository) Update(l *domain.Link) (link *domain.Link, err error) {
70 | link = l
71 | for i := range repo.db.links {
72 | if repo.db.links[i].ID == l.ID {
73 | repo.db.links[i] = *l
74 | return
75 | }
76 | }
77 | repo.db.links = append(repo.db.links, *l)
78 | return
79 | }
80 |
--------------------------------------------------------------------------------
/infrastructure/database/mysql/link.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "github.com/bccfilkom/drophere-go/domain"
5 | "github.com/jinzhu/gorm"
6 | )
7 |
8 | type linkRepository struct {
9 | db *gorm.DB
10 | }
11 |
12 | // NewLinkRepository func
13 | func NewLinkRepository(db *gorm.DB) domain.LinkRepository {
14 | return &linkRepository{db}
15 | }
16 |
17 | // Create implementation
18 | func (repo *linkRepository) Create(l *domain.Link) (*domain.Link, error) {
19 | if err := repo.db.Create(l).Error; err != nil {
20 | return nil, err
21 | }
22 | return l, nil
23 | }
24 |
25 | // Delete implementation
26 | func (repo *linkRepository) Delete(l *domain.Link) error {
27 | return repo.db.Delete(l).Error
28 | }
29 |
30 | // FindByID implementation
31 | func (repo *linkRepository) FindByID(id uint) (*domain.Link, error) {
32 | l := domain.Link{}
33 | if q := repo.db.
34 | Preload("User").
35 | Preload("UserStorageCredential").
36 | Find(&l, id); q.RecordNotFound() {
37 | return nil, domain.ErrLinkNotFound
38 | } else if q.Error != nil {
39 | return nil, q.Error
40 | }
41 |
42 | return &l, nil
43 | }
44 |
45 | // FindBySlug implementation
46 | func (repo *linkRepository) FindBySlug(slug string) (*domain.Link, error) {
47 | l := domain.Link{}
48 | if q := repo.db.
49 | Where("`slug` = ? ", slug).
50 | Preload("User").
51 | Preload("UserStorageCredential").
52 | Find(&l); q.RecordNotFound() {
53 | return nil, domain.ErrLinkNotFound
54 | } else if q.Error != nil {
55 | return nil, q.Error
56 | }
57 |
58 | return &l, nil
59 | }
60 |
61 | // ListByUser implementation
62 | func (repo *linkRepository) ListByUser(userID uint) ([]domain.Link, error) {
63 | var links []domain.Link
64 | if err := repo.db.
65 | Where("`user_id` = ? ", userID).
66 | Preload("User").
67 | Preload("UserStorageCredential").
68 | Find(&links).
69 | Error; err != nil {
70 | return nil, err
71 | }
72 |
73 | return links, nil
74 | }
75 |
76 | // Update implementation
77 | func (repo *linkRepository) Update(l *domain.Link) (link *domain.Link, err error) {
78 | if err := repo.db.Save(l).Error; err != nil {
79 | return nil, err
80 | }
81 |
82 | return l, nil
83 | }
84 |
--------------------------------------------------------------------------------
/domain/user/storage_provider.go:
--------------------------------------------------------------------------------
1 | package user
2 |
3 | import "github.com/bccfilkom/drophere-go/domain"
4 |
5 | // ConnectStorageProvider implementation
6 | func (s *service) ConnectStorageProvider(userID, providerID uint, providerCredential string) error {
7 | storageProvider, err := s.storageProviderPool.Get(providerID)
8 | if err != nil {
9 | return err
10 | }
11 |
12 | u, err := s.userRepo.FindByID(userID)
13 | if err != nil {
14 | return err
15 | }
16 |
17 | storageProviderAccount, err := storageProvider.AccountInfo(
18 | domain.StorageProviderCredential{
19 | UserAccessToken: providerCredential,
20 | },
21 | )
22 | if err != nil {
23 | return err
24 | }
25 |
26 | var cred domain.UserStorageCredential
27 |
28 | creds, err := s.userStorageCredRepo.Find(domain.UserStorageCredentialFilters{
29 | UserIDs: []uint{u.ID},
30 | ProviderIDs: []uint{providerID},
31 | }, false)
32 | if err != nil {
33 | return err
34 | }
35 |
36 | if len(creds) > 0 {
37 | cred = creds[0]
38 | cred.ProviderCredential = providerCredential
39 | cred.Email = storageProviderAccount.Email
40 | cred.Photo = storageProviderAccount.Photo
41 | cred, err = s.userStorageCredRepo.Update(cred)
42 | } else {
43 | cred, err = s.userStorageCredRepo.Create(domain.UserStorageCredential{
44 | UserID: u.ID,
45 | ProviderID: providerID,
46 | ProviderCredential: providerCredential,
47 | Email: storageProviderAccount.Email,
48 | Photo: storageProviderAccount.Photo,
49 | })
50 | }
51 |
52 | return err
53 |
54 | }
55 |
56 | // DisconnectStorageProvider implementation
57 | func (s *service) DisconnectStorageProvider(userID, providerID uint) error {
58 | storageProvider, err := s.storageProviderPool.Get(providerID)
59 | if err != nil {
60 | return err
61 | }
62 |
63 | u, err := s.userRepo.FindByID(userID)
64 | if err != nil {
65 | return err
66 | }
67 |
68 | creds, err := s.userStorageCredRepo.Find(domain.UserStorageCredentialFilters{
69 | UserIDs: []uint{u.ID},
70 | ProviderIDs: []uint{storageProvider.ID()},
71 | }, false)
72 | if err != nil {
73 | return err
74 | }
75 |
76 | if len(creds) > 0 {
77 | err = s.userStorageCredRepo.Delete(creds[0])
78 | if err != nil {
79 | return err
80 | }
81 | }
82 |
83 | return nil
84 |
85 | }
86 |
87 | // ListStorageProviders implementation
88 | func (s *service) ListStorageProviders(userID uint) ([]domain.UserStorageCredential, error) {
89 | return s.userStorageCredRepo.Find(domain.UserStorageCredentialFilters{
90 | UserIDs: []uint{userID},
91 | }, false)
92 | }
93 |
--------------------------------------------------------------------------------
/infrastructure/database/mysql/user_storage.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "github.com/bccfilkom/drophere-go/domain"
5 | "github.com/jinzhu/gorm"
6 | )
7 |
8 | type userStorageCredentialRepository struct {
9 | db *gorm.DB
10 | }
11 |
12 | // NewUserStorageCredentialRepository func
13 | func NewUserStorageCredentialRepository(db *gorm.DB) domain.UserStorageCredentialRepository {
14 | return &userStorageCredentialRepository{db}
15 | }
16 |
17 | // Find implementation
18 | func (repo *userStorageCredentialRepository) Find(filters domain.UserStorageCredentialFilters, withUserRelation bool) ([]domain.UserStorageCredential, error) {
19 | var (
20 | creds []domain.UserStorageCredential
21 | dbQuery = repo.db
22 | )
23 |
24 | if withUserRelation {
25 | dbQuery = dbQuery.Preload("User")
26 | }
27 |
28 | if filters.UserIDs != nil && len(filters.UserIDs) > 0 {
29 | dbQuery = dbQuery.Where("`user_id` IN (?)", filters.UserIDs)
30 | }
31 |
32 | if filters.ProviderIDs != nil && len(filters.ProviderIDs) > 0 {
33 | dbQuery = dbQuery.Where("`provider_id` IN (?)", filters.ProviderIDs)
34 | }
35 |
36 | err := dbQuery.Find(&creds).Error
37 | if err != nil {
38 | return nil, err
39 | }
40 |
41 | return creds, nil
42 | }
43 |
44 | // FindByID implementation
45 | func (repo *userStorageCredentialRepository) FindByID(id uint, withUserRelation bool) (domain.UserStorageCredential, error) {
46 | var (
47 | cred domain.UserStorageCredential
48 | dbQuery = repo.db
49 | )
50 |
51 | if withUserRelation {
52 | dbQuery = dbQuery.Preload("User")
53 | }
54 |
55 | if q := dbQuery.Find(&cred, id); q.RecordNotFound() {
56 | return cred, domain.ErrUserStorageCredentialNotFound
57 | } else if q.Error != nil {
58 | return cred, q.Error
59 | }
60 |
61 | return cred, nil
62 | }
63 |
64 | // Create implementation
65 | func (repo *userStorageCredentialRepository) Create(cred domain.UserStorageCredential) (domain.UserStorageCredential, error) {
66 | err := repo.db.Create(&cred).Error
67 | if err != nil {
68 | return domain.UserStorageCredential{}, err
69 | }
70 |
71 | return cred, nil
72 | }
73 |
74 | // Update implementation
75 | func (repo *userStorageCredentialRepository) Update(cred domain.UserStorageCredential) (domain.UserStorageCredential, error) {
76 | err := repo.db.Save(&cred).Error
77 | if err != nil {
78 | return domain.UserStorageCredential{}, err
79 | }
80 |
81 | return cred, nil
82 | }
83 |
84 | // Delete implementation
85 | func (repo *userStorageCredentialRepository) Delete(cred domain.UserStorageCredential) error {
86 | return repo.db.Delete(&cred).Error
87 | }
88 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/bccfilkom/drophere-go
2 |
3 | go 1.12
4 |
5 | require (
6 | cloud.google.com/go v0.39.0 // indirect
7 | github.com/99designs/gqlgen v0.9.0
8 | github.com/OneOfOne/xxhash v1.2.5 // indirect
9 | github.com/coreos/etcd v3.3.13+incompatible // indirect
10 | github.com/coreos/go-semver v0.3.0 // indirect
11 | github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 // indirect
12 | github.com/dgrijalva/jwt-go v3.2.0+incompatible
13 | github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b // indirect
14 | github.com/go-chi/chi v4.0.2+incompatible
15 | github.com/go-sql-driver/mysql v1.4.1
16 | github.com/gofrs/uuid v3.2.0+incompatible
17 | github.com/golang/mock v1.3.1 // indirect
18 | github.com/google/go-cmp v0.3.0 // indirect
19 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f // indirect
20 | github.com/hashicorp/golang-lru v0.5.1 // indirect
21 | github.com/jinzhu/gorm v1.9.8
22 | github.com/kisielk/errcheck v1.2.0 // indirect
23 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
24 | github.com/kr/pty v1.1.4 // indirect
25 | github.com/lib/pq v1.1.1 // indirect
26 | github.com/magiconair/properties v1.8.1 // indirect
27 | github.com/pelletier/go-toml v1.4.0 // indirect
28 | github.com/prometheus/common v0.4.1 // indirect
29 | github.com/prometheus/procfs v0.0.0-20190523193104-a7aeb8df3389 // indirect
30 | github.com/prometheus/tsdb v0.8.0 // indirect
31 | github.com/rogpeppe/fastuuid v1.1.0 // indirect
32 | github.com/rs/cors v1.6.0
33 | github.com/sendgrid/rest v2.4.1+incompatible // indirect
34 | github.com/sendgrid/sendgrid-go v3.5.0+incompatible
35 | github.com/sirupsen/logrus v1.4.2 // indirect
36 | github.com/spaolacci/murmur3 v1.1.0 // indirect
37 | github.com/spf13/afero v1.2.2 // indirect
38 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
39 | github.com/spf13/viper v1.4.0
40 | github.com/stretchr/objx v0.2.0 // indirect
41 | github.com/stretchr/testify v1.4.0
42 | github.com/vektah/gqlparser v1.1.2
43 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
44 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522 // indirect
45 | golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff // indirect
46 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422 // indirect
47 | golang.org/x/mobile v0.0.0-20190509164839-32b2708ab171 // indirect
48 | golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 // indirect
49 | golang.org/x/sys v0.0.0-20190528183647-3626398d7749 // indirect
50 | golang.org/x/text v0.3.2 // indirect
51 | golang.org/x/tools v0.0.0-20190529010454-aa71c3f32488 // indirect
52 | google.golang.org/appengine v1.6.0 // indirect
53 | google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69 // indirect
54 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
55 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc // indirect
56 | )
57 |
--------------------------------------------------------------------------------
/infrastructure/database/inmemory/mem.go:
--------------------------------------------------------------------------------
1 | package inmemory
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/bccfilkom/drophere-go/domain"
7 | )
8 |
9 | // DB struct
10 | type DB struct {
11 | users []domain.User
12 | links []domain.Link
13 | userStorageCreds []domain.UserStorageCredential
14 | }
15 |
16 | // New func
17 | func New() *DB {
18 | db := &DB{}
19 | db.populate()
20 | return db
21 | }
22 |
23 | func str2ptr(s string) *string {
24 | return &s
25 | }
26 |
27 | func time2ptr(t time.Time) *time.Time {
28 | return &t
29 | }
30 |
31 | func (db *DB) populate() {
32 | db.users = []domain.User{
33 | {ID: 1, Email: "user@drophere.link", Name: "User", Password: "123456", DropboxToken: nil, DriveToken: nil},
34 | {ID: 357, Email: "user_357@drophere.link", Name: "User 357", Password: "123456", DropboxToken: nil, DriveToken: nil},
35 | {
36 | ID: 6631,
37 | Email: "reset+pwd+expired_token@drophere.link",
38 | Name: "Token is set but expired",
39 | Password: "123456",
40 | RecoverPasswordToken: str2ptr("expired_recover_password_token"),
41 | RecoverPasswordTokenExpiry: time2ptr(time.Now().Add(time.Minute * -30)),
42 | },
43 | {
44 | ID: 12368,
45 | Email: "reset+pwd@drophere.link",
46 | Name: "Token is set",
47 | Password: "123456",
48 | RecoverPasswordToken: str2ptr("recover_password_token"),
49 | RecoverPasswordTokenExpiry: time2ptr(time.Now().Add(time.Minute * 30)),
50 | },
51 | }
52 |
53 | db.links = []domain.Link{
54 | {ID: 1, UserID: 1, User: &db.users[0], Title: "Drop file here", Slug: "drop-here", Password: "123098", Description: "drop a file here"},
55 | {ID: 2, UserID: 1, User: &db.users[0], Title: "Test Link 2", Slug: "test-link-2", Password: "", Description: "no description"},
56 | {ID: 3, UserID: 357, User: &db.users[1], Title: "Another link", Slug: "another-link", Password: "999", Description: "nil here"},
57 | }
58 |
59 | db.userStorageCreds = []domain.UserStorageCredential{
60 | {
61 | ID: 2000,
62 | UserID: 1,
63 | ProviderID: 1,
64 | ProviderCredential: "user_1_mock_token",
65 | Email: "user@drophere.link",
66 | Photo: "http://my.photo/user1.jpg",
67 | },
68 | }
69 | }
70 |
71 | // FindUserByEmail func
72 | func (db *DB) FindUserByEmail(email string) (*domain.User, error) {
73 | for i, u := range db.users {
74 | if u.Email == email {
75 | return &db.users[i], nil
76 | }
77 | }
78 | return nil, domain.ErrUserNotFound
79 | }
80 |
81 | // FindUserByID func
82 | func (db *DB) FindUserByID(id uint) (*domain.User, error) {
83 | for i, u := range db.users {
84 | if u.ID == id {
85 | return &db.users[i], nil
86 | }
87 | }
88 | return nil, domain.ErrUserNotFound
89 | }
90 |
91 | // CreateUser func
92 | func (db *DB) CreateUser(u *domain.User) (*domain.User, error) {
93 | db.users = append(db.users, *u)
94 | return u, nil
95 | }
96 |
--------------------------------------------------------------------------------
/infrastructure/database/inmemory/user_storage.go:
--------------------------------------------------------------------------------
1 | package inmemory
2 |
3 | import "github.com/bccfilkom/drophere-go/domain"
4 |
5 | type userStorageCredentialRepository struct {
6 | db *DB
7 | }
8 |
9 | // NewUserStorageCredentialRepository func
10 | func NewUserStorageCredentialRepository(db *DB) domain.UserStorageCredentialRepository {
11 | return &userStorageCredentialRepository{db}
12 | }
13 |
14 | func isInUintSlice(u uint, slice []uint) bool {
15 | for _, el := range slice {
16 | if el == u {
17 | return true
18 | }
19 | }
20 | return false
21 | }
22 |
23 | // Find impl
24 | func (repo *userStorageCredentialRepository) Find(filters domain.UserStorageCredentialFilters, withUserRelation bool) ([]domain.UserStorageCredential, error) {
25 | creds := make([]domain.UserStorageCredential, 0)
26 | usersCache := make(map[uint]domain.User)
27 |
28 | // load users first
29 | if withUserRelation && len(filters.UserIDs) > 0 {
30 | for _, u := range repo.db.users {
31 | if isInUintSlice(u.ID, filters.UserIDs) {
32 | usersCache[u.ID] = u
33 | }
34 | }
35 | }
36 |
37 | for _, usc := range repo.db.userStorageCreds {
38 | if filters.UserIDs != nil && (len(filters.UserIDs) == 0 ||
39 | !isInUintSlice(usc.UserID, filters.UserIDs)) {
40 | continue
41 | }
42 |
43 | if filters.ProviderIDs != nil && (len(filters.ProviderIDs) == 0 ||
44 | !isInUintSlice(usc.ProviderID, filters.ProviderIDs)) {
45 | continue
46 | }
47 |
48 | if withUserRelation {
49 | usc.User = usersCache[usc.UserID]
50 | }
51 |
52 | creds = append(creds, usc)
53 | }
54 |
55 | return creds, nil
56 | }
57 |
58 | // FindByID impl
59 | func (repo *userStorageCredentialRepository) FindByID(id uint, withUserRelation bool) (domain.UserStorageCredential, error) {
60 | cred := domain.UserStorageCredential{}
61 | found := false
62 | for _, usc := range repo.db.userStorageCreds {
63 | if usc.ID == id {
64 | cred = usc
65 | found = true
66 | break
67 | }
68 | }
69 |
70 | if found {
71 | if withUserRelation {
72 | for _, u := range repo.db.users {
73 | if u.ID == cred.UserID {
74 | cred.User = u
75 | break
76 | }
77 | }
78 | }
79 |
80 | return cred, nil
81 | }
82 | return cred, domain.ErrUserStorageCredentialNotFound
83 | }
84 |
85 | // Create impl
86 | func (repo *userStorageCredentialRepository) Create(cred domain.UserStorageCredential) (domain.UserStorageCredential, error) {
87 | repo.db.userStorageCreds = append(repo.db.userStorageCreds, cred)
88 | return cred, nil
89 | }
90 |
91 | // Update impl
92 | func (repo *userStorageCredentialRepository) Update(cred domain.UserStorageCredential) (domain.UserStorageCredential, error) {
93 |
94 | for i := range repo.db.userStorageCreds {
95 | if repo.db.userStorageCreds[i].ID == cred.ID {
96 | repo.db.userStorageCreds[i] = cred
97 | break
98 | }
99 | }
100 |
101 | return cred, nil
102 | }
103 |
104 | // Delete impl
105 | func (repo *userStorageCredentialRepository) Delete(cred domain.UserStorageCredential) error {
106 |
107 | for i := range repo.db.userStorageCreds {
108 | if repo.db.userStorageCreds[i].ID == cred.ID {
109 | repo.db.userStorageCreds = append(repo.db.userStorageCreds[:i], repo.db.userStorageCreds[i+1:]...)
110 | break
111 | }
112 | }
113 |
114 | return nil
115 | }
116 |
--------------------------------------------------------------------------------
/server/fileupload.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 | "strconv"
8 | "time"
9 |
10 | "github.com/bccfilkom/drophere-go/domain"
11 | )
12 |
13 | func writeError(w http.ResponseWriter, msg string) {
14 | json.NewEncoder(w).Encode(map[string]interface{}{
15 | "errors": []map[string]string{
16 | {
17 | "message": msg,
18 | },
19 | },
20 | })
21 | }
22 |
23 | func fileUploadHandler(
24 | userSvc domain.UserService,
25 | linkSvc domain.LinkService,
26 | storageProviderPool domain.StorageProviderPool,
27 | ) http.HandlerFunc {
28 | return func(w http.ResponseWriter, r *http.Request) {
29 | w.Header().Set("Content-Type", "application/json")
30 |
31 | // get file
32 | f, fileHeader, err := r.FormFile("file")
33 | if err != nil {
34 | if debug {
35 | log.Println("read file: ", err)
36 | }
37 | writeError(w, "Invalid File")
38 | w.WriteHeader(http.StatusBadRequest)
39 | return
40 | }
41 |
42 | // get linkID
43 | linkID, err := strconv.Atoi(r.FormValue("linkId"))
44 | if err != nil {
45 | if debug {
46 | log.Println("parsing link ID: ", err)
47 | }
48 | writeError(w, "Invalid Link ID")
49 | w.WriteHeader(http.StatusBadRequest)
50 | return
51 | }
52 |
53 | // fetch link from database
54 | l, err := linkSvc.FetchLink(uint(linkID))
55 | if err != nil {
56 | if err == domain.ErrLinkNotFound {
57 | writeError(w, err.Error())
58 | w.WriteHeader(http.StatusNotFound)
59 | } else {
60 | if debug {
61 | log.Println("file upload: ", err)
62 | }
63 | writeError(w, "Server Error")
64 | w.WriteHeader(http.StatusInternalServerError)
65 | }
66 | return
67 | }
68 |
69 | // check if the link is connected to a Storage Provider
70 | if l.UserStorageCredentialID == nil || *l.UserStorageCredentialID < 1 || l.UserStorageCredential == nil {
71 | writeError(w, "The link is unavailable")
72 | w.WriteHeader(http.StatusServiceUnavailable)
73 | return
74 | }
75 |
76 | // check for password
77 | if l.IsProtected() {
78 | password := r.FormValue("password")
79 |
80 | if !linkSvc.CheckLinkPassword(l, password) {
81 | writeError(w, "Invalid Password")
82 | w.WriteHeader(http.StatusUnprocessableEntity)
83 | return
84 | }
85 | }
86 |
87 | // check for deadline
88 | if l.Deadline != nil && l.Deadline.Before(time.Now()) {
89 | writeError(w, "Link is Expired")
90 | w.WriteHeader(http.StatusForbidden)
91 | return
92 | }
93 |
94 | storageProviderService, err := storageProviderPool.Get(l.UserStorageCredential.ProviderID)
95 | if err != nil {
96 | if debug {
97 | log.Println("get storage provider service: ", err)
98 | }
99 | writeError(w, "Sorry, but the Storage Provider is unavailable at the time")
100 | w.WriteHeader(http.StatusServiceUnavailable)
101 | return
102 | }
103 |
104 | err = storageProviderService.Upload(
105 | domain.StorageProviderCredential{
106 | UserAccessToken: l.UserStorageCredential.ProviderCredential,
107 | },
108 | f,
109 | fileHeader.Filename,
110 | l.Slug,
111 | )
112 | if err != nil {
113 | if debug {
114 | log.Println("file upload: ", err)
115 | }
116 | writeError(w, "Server Error")
117 | w.WriteHeader(http.StatusInternalServerError)
118 | return
119 | }
120 |
121 | json.NewEncoder(w).Encode(map[string]string{
122 | "message": "File is successfully uploaded",
123 | })
124 | w.WriteHeader(http.StatusOK)
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/infrastructure/auth/jwt.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "net/http"
8 | "strings"
9 | "time"
10 |
11 | "github.com/bccfilkom/drophere-go/domain"
12 | jwt "github.com/dgrijalva/jwt-go"
13 | )
14 |
15 | var (
16 | // A private key for context that only this package can access. This is important
17 | // to prevent collisions between different context uses
18 | userCtxKey = &contextKey{"user"}
19 | errInvalidToken = errors.New("jwt: invalid token")
20 | )
21 |
22 | type contextKey struct {
23 | name string
24 | }
25 |
26 | // JWTAuthenticator struct
27 | type JWTAuthenticator struct {
28 | key []byte
29 | duration time.Duration
30 | algo string
31 | userRepo domain.UserRepository
32 | }
33 |
34 | // NewJWT func
35 | func NewJWT(secret string, duration time.Duration, algo string, userRepo domain.UserRepository) *JWTAuthenticator {
36 | return &JWTAuthenticator{
37 | key: []byte(secret),
38 | duration: duration,
39 | algo: algo,
40 | userRepo: userRepo,
41 | }
42 | }
43 |
44 | // Authenticate func
45 | func (j *JWTAuthenticator) Authenticate(u *domain.User) (*domain.UserCredentials, error) {
46 | expiry := time.Now().Add(j.duration)
47 | token := jwt.NewWithClaims(jwt.GetSigningMethod(j.algo), jwt.MapClaims{
48 | "user_id": u.ID,
49 | "exp": expiry.Unix(),
50 | })
51 |
52 | tokenS, err := token.SignedString(j.key)
53 | if err != nil {
54 | return nil, err
55 | }
56 | return &domain.UserCredentials{
57 | Token: tokenS,
58 | Expiry: &expiry,
59 | }, nil
60 | }
61 |
62 | func (j *JWTAuthenticator) validateAndGetUserID(token string) (uint, error) {
63 | payloadI, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
64 | if jwt.GetSigningMethod(j.algo) != token.Method {
65 | return nil, errInvalidToken
66 | }
67 |
68 | return j.key, nil
69 | })
70 |
71 | if err != nil {
72 | return 0, err
73 | }
74 |
75 | if !payloadI.Valid {
76 | return 0, errInvalidToken
77 | }
78 |
79 | claims := payloadI.Claims.(jwt.MapClaims)
80 |
81 | userID, ok := claims["user_id"].(float64)
82 | if !ok {
83 | return 0, errInvalidToken
84 | }
85 |
86 | return uint(userID), nil
87 | }
88 |
89 | func writeGqlError(w http.ResponseWriter, msg string) {
90 | json.NewEncoder(w).Encode(map[string]interface{}{
91 | "errors": []map[string]string{
92 | {
93 | "message": msg,
94 | },
95 | },
96 | })
97 | }
98 |
99 | // Middleware func
100 | func (j *JWTAuthenticator) Middleware() func(http.Handler) http.Handler {
101 | return func(next http.Handler) http.Handler {
102 | // cast inner function to HandlerFunc
103 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
104 | authHeader := r.Header.Get("Authorization")
105 |
106 | // Allow unauthenticated users in
107 | if authHeader == "" {
108 | next.ServeHTTP(w, r)
109 | return
110 | }
111 |
112 | spaceIdx := strings.IndexByte(authHeader, ' ')
113 | if spaceIdx < 0 {
114 | writeGqlError(w, "Invalid Authorization header")
115 | return
116 | }
117 | authHeaderPrefix := authHeader[:spaceIdx]
118 | authToken := authHeader[spaceIdx+1:]
119 |
120 | if authHeaderPrefix != "bearer" && authHeaderPrefix != "Bearer" {
121 | writeGqlError(w, "Invalid Authorization header")
122 | return
123 | }
124 |
125 | userID, err := j.validateAndGetUserID(authToken)
126 | if err != nil {
127 | writeGqlError(w, "Invalid or expired token")
128 | return
129 | }
130 |
131 | // get the user from the database
132 | user, err := j.userRepo.FindByID(userID)
133 | if err != nil {
134 | writeGqlError(w, "Server Error")
135 | return
136 | }
137 |
138 | // put it in context
139 | ctx := context.WithValue(r.Context(), userCtxKey, user)
140 |
141 | // and call the next with our new context
142 | r = r.WithContext(ctx)
143 | next.ServeHTTP(w, r)
144 | })
145 | }
146 | }
147 |
148 | // GetAuthenticatedUser finds the user from the context. REQUIRES Middleware to have run.
149 | func (j *JWTAuthenticator) GetAuthenticatedUser(ctx context.Context) *domain.User {
150 | raw, _ := ctx.Value(userCtxKey).(*domain.User)
151 | return raw
152 | }
153 |
--------------------------------------------------------------------------------
/domain/link/link.go:
--------------------------------------------------------------------------------
1 | package link
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/bccfilkom/drophere-go/domain"
7 | )
8 |
9 | type service struct {
10 | linkRepo domain.LinkRepository
11 | uscRepo domain.UserStorageCredentialRepository
12 | passwordHasher domain.Hasher
13 | }
14 |
15 | // NewService returns new service instance
16 | func NewService(
17 | linkRepo domain.LinkRepository,
18 | uscRepo domain.UserStorageCredentialRepository,
19 | passwordHasher domain.Hasher,
20 | ) domain.LinkService {
21 | return &service{
22 | linkRepo: linkRepo,
23 | uscRepo: uscRepo,
24 | passwordHasher: passwordHasher,
25 | }
26 | }
27 |
28 | // CheckLinkPassword checks if user-inputted password match the hashed password
29 | func (s *service) CheckLinkPassword(l *domain.Link, password string) bool {
30 | // skip password checking if link is not protected
31 | if !l.IsProtected() {
32 | return true
33 | }
34 |
35 | return s.passwordHasher.Verify(l.Password, password)
36 | }
37 |
38 | // CreateLink creates new Link and store it to repository
39 | func (s *service) CreateLink(title, slug, description string, deadline *time.Time, password *string, user *domain.User, providerID *uint) (*domain.Link, error) {
40 | l, err := s.linkRepo.FindBySlug(slug)
41 | if err != nil && err != domain.ErrLinkNotFound {
42 | return nil, err
43 | }
44 |
45 | if l != nil {
46 | return nil, domain.ErrLinkDuplicatedSlug
47 | }
48 |
49 | l = &domain.Link{
50 | UserID: user.ID,
51 | Title: title,
52 | Slug: slug,
53 | Description: description,
54 | Deadline: deadline,
55 | }
56 |
57 | if password != nil && *password != "" {
58 | l.Password, err = s.passwordHasher.Hash(*password)
59 | if err != nil {
60 | return nil, err
61 | }
62 | }
63 |
64 | if providerID != nil && *providerID > 0 {
65 | uscs, err := s.uscRepo.Find(
66 | domain.UserStorageCredentialFilters{
67 | UserIDs: []uint{user.ID},
68 | ProviderIDs: []uint{*providerID},
69 | },
70 | false,
71 | )
72 | if err != nil {
73 | return nil, err
74 | }
75 |
76 | if len(uscs) < 1 {
77 | return nil, domain.ErrUserStorageCredentialNotFound
78 | }
79 | l.UserStorageCredentialID = &(uscs[0].ID)
80 | l.UserStorageCredential = &uscs[0]
81 | }
82 |
83 | return s.linkRepo.Create(l)
84 | }
85 |
86 | // UpdateLink updates existing Link and save it to repository
87 | func (s *service) UpdateLink(linkID uint, title, slug string, description *string, deadline *time.Time, password *string, providerID *uint) (*domain.Link, error) {
88 | l, err := s.linkRepo.FindByID(linkID)
89 | if err != nil {
90 | return nil, err
91 | }
92 |
93 | // check duplicated slug
94 | link2, err := s.linkRepo.FindBySlug(slug)
95 | if err != nil && err != domain.ErrLinkNotFound {
96 | return nil, err
97 | }
98 |
99 | if link2 != nil && link2.ID != l.ID {
100 | return nil, domain.ErrLinkDuplicatedSlug
101 | }
102 |
103 | l.Title = title
104 | l.Slug = slug
105 | l.Deadline = deadline // set null if the user want to remove the deadline
106 | if description != nil {
107 | l.Description = *description
108 | }
109 |
110 | if password != nil {
111 | if *password == "" {
112 | l.Password = *password
113 | } else {
114 | l.Password, err = s.passwordHasher.Hash(*password)
115 | if err != nil {
116 | return nil, err
117 | }
118 | }
119 | }
120 |
121 | // user can unset the UserStorageProviderID by passing 0 to providerID
122 | if providerID != nil {
123 | if *providerID <= 0 {
124 | l.UserStorageCredentialID = nil
125 | l.UserStorageCredential = nil
126 | } else {
127 | uscs, err := s.uscRepo.Find(
128 | domain.UserStorageCredentialFilters{
129 | UserIDs: []uint{l.UserID},
130 | ProviderIDs: []uint{*providerID},
131 | },
132 | false,
133 | )
134 |
135 | if err != nil {
136 | return nil, err
137 | }
138 |
139 | if len(uscs) < 1 {
140 | return nil, domain.ErrUserStorageCredentialNotFound
141 | }
142 | l.UserStorageCredentialID = &(uscs[0].ID)
143 | l.UserStorageCredential = &uscs[0]
144 |
145 | }
146 | }
147 |
148 | return s.linkRepo.Update(l)
149 | }
150 |
151 | // DeleteLink delete existing Link specified by its ID
152 | func (s *service) DeleteLink(id uint) error {
153 | l, err := s.linkRepo.FindByID(id)
154 | if err != nil {
155 | return err
156 | }
157 |
158 | return s.linkRepo.Delete(l)
159 | }
160 |
161 | // FetchLink returns single Link identified by its ID
162 | func (s *service) FetchLink(id uint) (*domain.Link, error) {
163 | return s.linkRepo.FindByID(id)
164 | }
165 |
166 | // FindLinkBySlug returns single Link identified by its slug
167 | func (s *service) FindLinkBySlug(slug string) (*domain.Link, error) {
168 | return s.linkRepo.FindBySlug(slug)
169 | }
170 |
171 | // ListLinks returns list of Link which belongs to a user
172 | func (s *service) ListLinks(userID uint) ([]domain.Link, error) {
173 | return s.linkRepo.ListByUser(userID)
174 | }
175 |
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "path/filepath"
8 | "time"
9 |
10 | htmlTemplate "html/template"
11 | textTemplate "text/template"
12 |
13 | drophere_go "github.com/bccfilkom/drophere-go"
14 | "github.com/bccfilkom/drophere-go/domain"
15 | "github.com/bccfilkom/drophere-go/domain/link"
16 | "github.com/bccfilkom/drophere-go/domain/user"
17 | "github.com/bccfilkom/drophere-go/infrastructure/auth"
18 | "github.com/bccfilkom/drophere-go/infrastructure/database/mysql"
19 | "github.com/bccfilkom/drophere-go/infrastructure/hasher"
20 | "github.com/bccfilkom/drophere-go/infrastructure/mailer"
21 | "github.com/bccfilkom/drophere-go/infrastructure/storageprovider"
22 | "github.com/bccfilkom/drophere-go/infrastructure/stringgenerator"
23 |
24 | "github.com/99designs/gqlgen/handler"
25 | "github.com/go-chi/chi"
26 | "github.com/go-chi/chi/middleware"
27 | "github.com/rs/cors"
28 | "github.com/spf13/viper"
29 |
30 | _ "github.com/go-sql-driver/mysql"
31 | )
32 |
33 | const defaultPort = "8080"
34 |
35 | var debug bool
36 |
37 | func main() {
38 | viper.SetConfigName("config")
39 | viper.SetConfigType("yaml")
40 | viper.AddConfigPath(".")
41 |
42 | err := viper.ReadInConfig()
43 | if err != nil {
44 | panic(fmt.Errorf("config: %s", err))
45 | }
46 |
47 | viper.SetEnvPrefix("DROPHERE")
48 | viper.AutomaticEnv()
49 |
50 | // set debug mode
51 | debug = viper.GetBool("app.debug")
52 |
53 | port := viper.GetString("PORT")
54 | if port == "" {
55 | port = defaultPort
56 | }
57 |
58 | // setup
59 | db, err := mysql.New(viper.GetString("db.dsn"))
60 | if err != nil {
61 | panic(err)
62 | }
63 |
64 | // initialize repositories
65 | userRepo := mysql.NewUserRepository(db)
66 | linkRepo := mysql.NewLinkRepository(db)
67 | userStorageCredRepo := mysql.NewUserStorageCredentialRepository(db)
68 |
69 | // initialize infrastructures
70 | authenticator := auth.NewJWT(
71 | viper.GetString("jwt.secret"),
72 | time.Duration(viper.GetInt("jwt.duration"))*time.Hour,
73 | viper.GetString("jwt.signingAlgorithm"),
74 | userRepo,
75 | )
76 | bcryptHasher := hasher.NewBcryptHasher()
77 | // mailtrap := mailer.NewMailtrap(
78 | // viper.GetString("mailer.mailtrap.username"),
79 | // viper.GetString("mailer.mailtrap.password"),
80 | // )
81 | sendgridMailer := mailer.NewSendgrid(
82 | viper.GetString("mailer.sendgrid.apiKey"),
83 | debug,
84 | )
85 | uuidGenerator := stringgenerator.NewUUID()
86 |
87 | remoteDirectory := "drophere"
88 | if remoteDirCfg := viper.GetString("app.storageRootDirectoryName"); remoteDirCfg != "" {
89 | remoteDirectory = remoteDirCfg
90 | }
91 |
92 | dropboxService := storageprovider.NewDropboxStorageProvider(remoteDirectory)
93 | storageProviderPool := domain.StorageProviderPool{}
94 | storageProviderPool.Register(dropboxService)
95 |
96 | basePath := viper.GetString("app.templatePath")
97 | htmlTemplates, err := htmlTemplate.ParseGlob(filepath.Join(basePath, "html", "*.html"))
98 | if err != nil {
99 | panic(err)
100 | }
101 |
102 | textTemplates, err := textTemplate.ParseGlob(filepath.Join(basePath, "text", "*.txt"))
103 | if err != nil {
104 | panic(err)
105 | }
106 |
107 | // initialize services
108 | userSvc := user.NewService(
109 | userRepo,
110 | userStorageCredRepo,
111 | authenticator,
112 | sendgridMailer,
113 | bcryptHasher,
114 | uuidGenerator,
115 | storageProviderPool,
116 | htmlTemplates,
117 | textTemplates,
118 | user.Config{
119 | PasswordRecoveryTokenExpiryDuration: viper.GetInt("app.passwordRecovery.tokenExpiryDuration"),
120 | RecoverPasswordWebURL: viper.GetString("app.passwordRecovery.webURL"),
121 | MailerEmail: viper.GetString("app.passwordRecovery.mailer.email"),
122 | MailerName: viper.GetString("app.passwordRecovery.mailer.name"),
123 | },
124 | )
125 | linkSvc := link.NewService(linkRepo, userStorageCredRepo, bcryptHasher)
126 |
127 | resolver := drophere_go.NewResolver(userSvc, authenticator, linkSvc)
128 |
129 | // setup router
130 | router := chi.NewRouter()
131 |
132 | // A good base middleware stack
133 | router.Use(cors.New(cors.Options{
134 | AllowedOrigins: []string{"*"},
135 | AllowCredentials: true,
136 | AllowedHeaders: []string{"*"},
137 | Debug: debug,
138 | }).Handler)
139 | router.Use(authenticator.Middleware())
140 | router.Use(middleware.RequestID)
141 | router.Use(middleware.RealIP)
142 | router.Use(middleware.Logger)
143 | router.Use(middleware.Recoverer)
144 |
145 | router.Handle("/", handler.Playground("GraphQL playground", "/query"))
146 | router.Handle("/query", handler.GraphQL(drophere_go.NewExecutableSchema(drophere_go.Config{Resolvers: resolver})))
147 | router.Post("/uploadfile", fileUploadHandler(userSvc, linkSvc, storageProviderPool))
148 |
149 | log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
150 | err = http.ListenAndServe(":"+port, router)
151 | if err != nil {
152 | log.Fatal(err)
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/infrastructure/storageprovider/dropbox.go:
--------------------------------------------------------------------------------
1 | package storageprovider
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "net/http"
10 | "strings"
11 | "time"
12 |
13 | "github.com/bccfilkom/drophere-go/domain"
14 | )
15 |
16 | var (
17 | errNotEnoughScope = errors.New("Not enough scope given from the Dropbox access token. Please grant the required scope 'files.content.write' and reset the access token.")
18 | )
19 |
20 | const dropboxProviderID uint = 12345678
21 |
22 | type dropbox struct {
23 | remoteDirectory string
24 | }
25 |
26 | type dropboxError struct {
27 | HttpCode int
28 | Message string
29 | Json dropboxErrorJson
30 | }
31 |
32 | type dropboxErrorJson struct {
33 | ErrorSummary string `json:"error_summary"`
34 | ErrorStructured map[string]interface{} `json:"error"`
35 | UserMessage string `json:"user_message"`
36 | }
37 |
38 | // NewDropboxStorageProvider returns new StorageProviderService
39 | func NewDropboxStorageProvider(remoteDirectory string) domain.StorageProviderService {
40 | return &dropbox{
41 | remoteDirectory: remoteDirectory,
42 | }
43 | }
44 |
45 | // ID returns provider ID
46 | func (d *dropbox) ID() uint {
47 | return dropboxProviderID
48 | }
49 |
50 | // AccountInfo fetches Dropbox account information
51 | func (d *dropbox) AccountInfo(cred domain.StorageProviderCredential) (domain.StorageProviderAccountInfo, error) {
52 | var accountInfo domain.StorageProviderAccountInfo
53 |
54 | req, err := http.NewRequest(
55 | http.MethodPost,
56 | "https://api.dropboxapi.com/2/users/get_current_account",
57 | nil,
58 | )
59 | if err != nil {
60 | return accountInfo, err
61 | }
62 |
63 | // prepare header (no need to set content-type)
64 | req.Header.Set("Authorization", "Bearer "+cred.UserAccessToken)
65 |
66 | client := http.Client{
67 | Timeout: 5 * time.Second,
68 | }
69 |
70 | // do http request
71 | resp, err := client.Do(req)
72 | if err != nil {
73 | return accountInfo, err
74 | }
75 |
76 | defer resp.Body.Close()
77 |
78 | // read body
79 | respBodyBytes, err := ioutil.ReadAll(resp.Body)
80 | if err != nil {
81 | return accountInfo, err
82 | }
83 |
84 | var respBody map[string]interface{}
85 |
86 | err = json.Unmarshal(respBodyBytes, &respBody)
87 | if err != nil {
88 | return accountInfo, err
89 | }
90 |
91 | accountInfo.Email, _ = respBody["email"].(string)
92 | accountInfo.Photo, _ = respBody["profile_photo_url"].(string)
93 |
94 | return accountInfo, nil
95 | }
96 |
97 | // Upload sends the file to Dropbox server
98 | func (d *dropbox) Upload(cred domain.StorageProviderCredential, file io.Reader, fileName, slug string) error {
99 |
100 | req, err := d.prepareRequest(cred.UserAccessToken, file, fileName, slug)
101 | if err != nil {
102 | return err
103 | }
104 |
105 | client := http.Client{
106 | Timeout: 10 * time.Second,
107 | }
108 |
109 | // do the request
110 | res, err := client.Do(req)
111 | if err != nil {
112 | return err
113 | }
114 |
115 | if res.StatusCode != http.StatusOK {
116 | dropboxError, err := d.mapToDropboxError(res.Body, res.StatusCode)
117 | defer res.Body.Close()
118 | if err != nil {
119 | return err
120 | }
121 |
122 | regularError := d.mapToRegularError(dropboxError)
123 |
124 | return regularError
125 | }
126 |
127 | return nil
128 | }
129 |
130 | func (d *dropbox) prepareRequest(accessToken string, file io.Reader, fileName, slug string) (*http.Request, error) {
131 |
132 | req, err := http.NewRequest(
133 | http.MethodPost,
134 | "https://content.dropboxapi.com/2/files/upload",
135 | file,
136 | )
137 | if err != nil {
138 | return nil, err
139 | }
140 |
141 | // construct Dropbox API arguments
142 | dropboxAPIArg := fmt.Sprintf(
143 | `{"path": "/%s/%s/%s","mode": "add","autorename": true,"mute": false}`,
144 | d.remoteDirectory,
145 | slug,
146 | fileName,
147 | )
148 |
149 | // prepare header
150 | req.Header.Set("Authorization", "Bearer "+accessToken)
151 | req.Header.Set("Content-Type", "application/octet-stream")
152 | req.Header.Set("Dropbox-API-Arg", dropboxAPIArg)
153 | return req, nil
154 | }
155 |
156 | func (d *dropbox) mapToDropboxError(responseReader io.Reader, httpStatusCode int) (dropboxError, error) {
157 | byteResponse, err := io.ReadAll(responseReader)
158 | if err != nil {
159 | return dropboxError{}, err
160 | }
161 | errorRes := dropboxError{HttpCode: httpStatusCode}
162 | errorRes.Message = string(byteResponse)
163 | if httpStatusCode == http.StatusUnauthorized {
164 | errorRes.Json = dropboxErrorJson{}
165 | json.Unmarshal(byteResponse, &errorRes.Json)
166 | }
167 |
168 | return errorRes, nil
169 | }
170 |
171 | func (d *dropbox) mapToRegularError(dropboxError dropboxError) error {
172 | if dropboxError.HttpCode == http.StatusBadRequest {
173 | if strings.Contains(dropboxError.Message, "files.content.write") {
174 | return errNotEnoughScope
175 | }
176 | } else if dropboxError.HttpCode == http.StatusUnauthorized {
177 | if dropboxError.Json.ErrorStructured[".tag"].(string) == "missing_scope" && dropboxError.Json.ErrorStructured["required_scope"].(string) == "files.content.write" {
178 | return errNotEnoughScope
179 | }
180 | }
181 |
182 | return errors.New("Unknown dropbox error.\n" + dropboxError.Message)
183 | }
184 |
--------------------------------------------------------------------------------
/domain/user/user.go:
--------------------------------------------------------------------------------
1 | package user
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "time"
7 |
8 | htmlTemplate "html/template"
9 | textTemplate "text/template"
10 |
11 | "github.com/bccfilkom/drophere-go/domain"
12 | )
13 |
14 | const defaultTokenExpiryDuration int = 5
15 |
16 | // Config model
17 | type Config struct {
18 | PasswordRecoveryTokenExpiryDuration int
19 | RecoverPasswordWebURL string
20 | MailerEmail string
21 | MailerName string
22 | }
23 |
24 | type service struct {
25 | userRepo domain.UserRepository
26 | userStorageCredRepo domain.UserStorageCredentialRepository
27 | authenticator domain.Authenticator
28 | mailer domain.Mailer
29 | passwordHasher domain.Hasher
30 | stringGenerator domain.StringGenerator
31 |
32 | storageProviderPool domain.StorageProviderPool
33 |
34 | htmlTemplates *htmlTemplate.Template
35 | textTemplates *textTemplate.Template
36 |
37 | config Config
38 | }
39 |
40 | // NewService returns service instance
41 | func NewService(
42 | userRepo domain.UserRepository,
43 | userStorageCredRepo domain.UserStorageCredentialRepository,
44 | authenticator domain.Authenticator,
45 | mailer domain.Mailer,
46 | passwordHasher domain.Hasher,
47 | stringGenerator domain.StringGenerator,
48 | storageProviderPool domain.StorageProviderPool,
49 | htmlTemplates *htmlTemplate.Template,
50 | textTemplates *textTemplate.Template,
51 | config Config,
52 | ) domain.UserService {
53 | return &service{
54 | userRepo: userRepo,
55 | userStorageCredRepo: userStorageCredRepo,
56 | authenticator: authenticator,
57 | mailer: mailer,
58 | passwordHasher: passwordHasher,
59 | stringGenerator: stringGenerator,
60 |
61 | storageProviderPool: storageProviderPool,
62 |
63 | htmlTemplates: htmlTemplates,
64 | textTemplates: textTemplates,
65 |
66 | config: config,
67 | }
68 | }
69 |
70 | // Register implementation
71 | func (s *service) Register(email, name, password string) (*domain.User, error) {
72 | // check for existing email prior to creating new user
73 | user, err := s.userRepo.FindByEmail(email)
74 | if err != nil && err != domain.ErrUserNotFound {
75 | return nil, err
76 | }
77 |
78 | if user != nil {
79 | return nil, domain.ErrUserDuplicated
80 | }
81 |
82 | user = &domain.User{
83 | Email: email,
84 | Name: name,
85 | }
86 |
87 | user.Password, err = s.passwordHasher.Hash(password)
88 | if err != nil {
89 | return nil, err
90 | }
91 | return s.userRepo.Create(user)
92 | }
93 |
94 | // Auth implementation
95 | func (s *service) Auth(email, password string) (*domain.UserCredentials, error) {
96 | user, err := s.userRepo.FindByEmail(email)
97 | if err != nil {
98 | return nil, err
99 | }
100 |
101 | if !s.passwordHasher.Verify(user.Password, password) {
102 | return nil, domain.ErrUserInvalidPassword
103 | }
104 |
105 | return s.authenticator.Authenticate(user)
106 | }
107 |
108 | // Update implementation
109 | func (s *service) Update(userID uint, name, newPassword, oldPassword *string) (*domain.User, error) {
110 | u, err := s.userRepo.FindByID(userID)
111 | if err != nil {
112 | return nil, err
113 | }
114 |
115 | if newPassword != nil {
116 | if oldPassword == nil || !s.passwordHasher.Verify(u.Password, *oldPassword) {
117 | return nil, domain.ErrUserInvalidPassword
118 | }
119 |
120 | u.Password, err = s.passwordHasher.Hash(*newPassword)
121 | if err != nil {
122 | return nil, err
123 | }
124 | }
125 |
126 | if name != nil {
127 | u.Name = *name
128 | }
129 |
130 | return s.userRepo.Update(u)
131 | }
132 |
133 | // UpdateStorageToken implementation
134 | func (s *service) UpdateStorageToken(userID uint, dropboxToken *string) (*domain.User, error) {
135 | u, err := s.userRepo.FindByID(userID)
136 | if err != nil {
137 | return nil, err
138 | }
139 |
140 | u.DropboxToken = dropboxToken
141 |
142 | return s.userRepo.Update(u)
143 | }
144 |
145 | // RequestPasswordRecovery implementation
146 | func (s *service) RequestPasswordRecovery(email string) error {
147 | u, err := s.userRepo.FindByEmail(email)
148 | if err != nil {
149 | return err
150 | }
151 |
152 | // TODO: check if user has already requested password recovery to avoid spam
153 | tokenExpiryDuration := defaultTokenExpiryDuration
154 | if s.config.PasswordRecoveryTokenExpiryDuration > 0 {
155 | tokenExpiryDuration = s.config.PasswordRecoveryTokenExpiryDuration
156 | }
157 |
158 | token := s.stringGenerator.Generate()
159 | tokenExpiry := time.Now().Add(time.Minute * time.Duration(tokenExpiryDuration))
160 | u.RecoverPasswordToken = &token
161 | u.RecoverPasswordTokenExpiry = &tokenExpiry
162 |
163 | // save the user
164 | u, err = s.userRepo.Update(u)
165 | if err != nil {
166 | return err
167 | }
168 |
169 | // send email
170 | err = s.sendPasswordRecoveryTokenToEmail(
171 | domain.MailAddress{
172 | Address: u.Email,
173 | Name: u.Name,
174 | },
175 | "Recover Password",
176 | u.Email,
177 | token,
178 | )
179 | if err != nil {
180 | return err
181 | }
182 |
183 | return nil
184 | }
185 |
186 | func (s *service) sendPasswordRecoveryTokenToEmail(to domain.MailAddress, subject, email, token string) error {
187 |
188 | // preparing template
189 | htmlTmpl := s.htmlTemplates.Lookup("request_password_recovery_html")
190 | if htmlTmpl == nil {
191 | return domain.ErrTemplateNotFound
192 | }
193 |
194 | textTmpl := s.textTemplates.Lookup("request_password_recovery_text")
195 | if textTmpl == nil {
196 | return domain.ErrTemplateNotFound
197 | }
198 |
199 | // preparing template content
200 | messageData := map[string]string{
201 | "ResetPasswordLink": fmt.Sprintf(
202 | "%s?token=%s&email=%s",
203 | s.config.RecoverPasswordWebURL,
204 | token,
205 | email,
206 | ),
207 | "Token": token,
208 | }
209 |
210 | // injecting data to template
211 | htmlMessage := &bytes.Buffer{}
212 | htmlTmpl.Execute(htmlMessage, messageData)
213 |
214 | textMessage := &bytes.Buffer{}
215 | textTmpl.Execute(textMessage, messageData)
216 |
217 | from := domain.MailAddress{
218 | Address: "admin@drophere.link",
219 | Name: "Drophere Bot",
220 | }
221 |
222 | if s.config.MailerEmail != "" {
223 | from.Address = s.config.MailerEmail
224 | }
225 |
226 | if s.config.MailerName != "" {
227 | from.Name = s.config.MailerName
228 | }
229 |
230 | // send email
231 | return s.mailer.Send(
232 | from,
233 | to,
234 | subject,
235 | textMessage.String(),
236 | htmlMessage.String(),
237 | )
238 | }
239 |
240 | func (s *service) RecoverPassword(email, token, newPassword string) error {
241 | u, err := s.userRepo.FindByEmail(email)
242 | if err != nil {
243 | return err
244 | }
245 |
246 | if token == "" || u.RecoverPasswordToken == nil || *u.RecoverPasswordToken != token {
247 | return domain.ErrUserNotFound
248 | }
249 |
250 | if u.RecoverPasswordTokenExpiry == nil || time.Now().After(*u.RecoverPasswordTokenExpiry) {
251 | return domain.ErrUserPasswordRecoveryTokenExpired
252 | }
253 |
254 | u.Password, err = s.passwordHasher.Hash(newPassword)
255 | if err != nil {
256 | return err
257 | }
258 |
259 | u.RecoverPasswordToken, u.RecoverPasswordTokenExpiry = nil, nil
260 |
261 | u, err = s.userRepo.Update(u)
262 | if err != nil {
263 | return err
264 | }
265 |
266 | return nil
267 | }
268 |
--------------------------------------------------------------------------------
/resolver.go:
--------------------------------------------------------------------------------
1 | package drophere_go
2 |
3 | //go:generate go run github.com/99designs/gqlgen
4 |
5 | import (
6 | "context"
7 | "errors"
8 | "time"
9 |
10 | "github.com/bccfilkom/drophere-go/domain"
11 | ) // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.
12 |
13 | var (
14 | errUnauthenticated = errors.New("Access denied")
15 | errUnauthorized = errors.New("You are not allowed to do this operation")
16 | )
17 |
18 | type authenticator interface {
19 | GetAuthenticatedUser(context.Context) *domain.User
20 | }
21 |
22 | // Resolver resolves given query from client
23 | type Resolver struct {
24 | linkSvc domain.LinkService
25 | userSvc domain.UserService
26 | authenticator authenticator
27 | }
28 |
29 | // NewResolver func
30 | func NewResolver(
31 | userSvc domain.UserService,
32 | authenticator authenticator,
33 | linkSvc domain.LinkService,
34 | ) *Resolver {
35 | return &Resolver{
36 | linkSvc: linkSvc,
37 | userSvc: userSvc,
38 | authenticator: authenticator,
39 | }
40 | }
41 |
42 | // Mutation returns a group of resolvers for mutation query
43 | func (r *Resolver) Mutation() MutationResolver {
44 | return &mutationResolver{r}
45 | }
46 |
47 | // Query returns a group of resolvers for query
48 | func (r *Resolver) Query() QueryResolver {
49 | return &queryResolver{r}
50 | }
51 |
52 | type mutationResolver struct{ *Resolver }
53 |
54 | // Register resolver
55 | func (r *mutationResolver) Register(ctx context.Context, email string, password string, name string) (*Token, error) {
56 | user, err := r.userSvc.Register(email, name, password)
57 | if err != nil {
58 | return nil, err
59 | }
60 |
61 | userCreds, err := r.userSvc.Auth(user.Email, password)
62 | if err != nil {
63 | return nil, err
64 | }
65 | return &Token{LoginToken: userCreds.Token}, nil
66 | }
67 |
68 | // Login resolver
69 | func (r *mutationResolver) Login(ctx context.Context, email string, password string) (*Token, error) {
70 | userCreds, err := r.userSvc.Auth(email, password)
71 | if err != nil {
72 | return nil, err
73 | }
74 | return &Token{LoginToken: userCreds.Token}, nil
75 | }
76 |
77 | // RequestPasswordRecovery resolver
78 | func (r *mutationResolver) RequestPasswordRecovery(ctx context.Context, email string) (*Message, error) {
79 | err := r.userSvc.RequestPasswordRecovery(email)
80 | if err != nil {
81 | return nil, err
82 | }
83 |
84 | return &Message{"Recover Password instruction has been sent to your email"}, nil
85 | }
86 |
87 | // RecoverPassword resolver
88 | func (r *mutationResolver) RecoverPassword(ctx context.Context, email, recoverToken, newPassword string) (*Token, error) {
89 | err := r.userSvc.RecoverPassword(email, recoverToken, newPassword)
90 | if err != nil {
91 | return nil, err
92 | }
93 |
94 | userCreds, err := r.userSvc.Auth(email, newPassword)
95 | if err != nil {
96 | return nil, err
97 | }
98 |
99 | return &Token{LoginToken: userCreds.Token}, nil
100 | }
101 |
102 | // UpdatePassword resolver
103 | func (r *mutationResolver) UpdatePassword(ctx context.Context, oldPassword string, newPassword string) (*Message, error) {
104 | user := r.authenticator.GetAuthenticatedUser(ctx)
105 | if user == nil {
106 | return nil, errUnauthenticated
107 | }
108 |
109 | _, err := r.userSvc.Update(user.ID, nil, &newPassword, &oldPassword)
110 | if err != nil {
111 | return nil, err
112 | }
113 |
114 | return &Message{Message: "You password successfully updated"}, nil
115 | }
116 |
117 | // UpdateProfile resolver
118 | func (r *mutationResolver) UpdateProfile(ctx context.Context, newName string) (*Message, error) {
119 | user := r.authenticator.GetAuthenticatedUser(ctx)
120 | if user == nil {
121 | return nil, errUnauthenticated
122 | }
123 |
124 | _, err := r.userSvc.Update(user.ID, &newName, nil, nil)
125 | if err != nil {
126 | return nil, err
127 | }
128 |
129 | return &Message{Message: "Your profile successfully updated"}, nil
130 | }
131 |
132 | // CreateLink resolver
133 | func (r *mutationResolver) CreateLink(ctx context.Context, title string, slug string, description *string, deadline *time.Time, password *string, providerID *int) (*Link, error) {
134 | user := r.authenticator.GetAuthenticatedUser(ctx)
135 | if user == nil {
136 | return nil, errUnauthenticated
137 | }
138 |
139 | desc := ""
140 | if description != nil {
141 | desc = *description
142 | }
143 |
144 | var providerIDUintPtr *uint
145 | if providerID != nil {
146 | providerIDUint := uint(*providerID)
147 | providerIDUintPtr = &providerIDUint
148 | }
149 |
150 | l, err := r.linkSvc.CreateLink(title, slug, desc, deadline, password, user, providerIDUintPtr)
151 | if err != nil {
152 | return nil, err
153 | }
154 |
155 | return formatLink(*l), nil
156 | }
157 |
158 | // UpdateLink resolver
159 | func (r *mutationResolver) UpdateLink(ctx context.Context, linkID int, title string, slug string, description *string, deadline *time.Time, password *string, providerID *int) (*Link, error) {
160 | user := r.authenticator.GetAuthenticatedUser(ctx)
161 | if user == nil {
162 | return nil, errUnauthenticated
163 | }
164 |
165 | l, err := r.linkSvc.FetchLink(uint(linkID))
166 | if err != nil {
167 | return nil, err
168 | }
169 |
170 | if l.UserID != user.ID {
171 | return nil, errUnauthorized
172 | }
173 |
174 | var providerIDUintPtr *uint
175 | if providerID != nil {
176 | providerIDUint := uint(*providerID)
177 | providerIDUintPtr = &providerIDUint
178 | }
179 |
180 | l, err = r.linkSvc.UpdateLink(
181 | uint(linkID),
182 | title,
183 | slug,
184 | description,
185 | deadline,
186 | password,
187 | providerIDUintPtr,
188 | )
189 |
190 | if err != nil {
191 | return nil, err
192 | }
193 |
194 | return formatLink(*l), nil
195 | }
196 |
197 | // DeleteLink resolver
198 | func (r *mutationResolver) DeleteLink(ctx context.Context, linkID int) (*Message, error) {
199 | user := r.authenticator.GetAuthenticatedUser(ctx)
200 | if user == nil {
201 | return nil, errUnauthenticated
202 | }
203 |
204 | l, err := r.linkSvc.FetchLink(uint(linkID))
205 | if err != nil {
206 | return nil, err
207 | }
208 |
209 | if l.UserID != user.ID {
210 | return nil, errUnauthorized
211 | }
212 |
213 | err = r.linkSvc.DeleteLink(uint(linkID))
214 | if err != nil {
215 | return nil, err
216 | }
217 |
218 | return &Message{Message: "Link Deleted!"}, nil
219 | }
220 |
221 | // CheckLinkPassword resolver
222 | func (r *mutationResolver) CheckLinkPassword(ctx context.Context, linkID int, password string) (*Message, error) {
223 | // this is for public use, no need to check user auth
224 | l, err := r.linkSvc.FetchLink(uint(linkID))
225 | if err != nil {
226 | return nil, err
227 | }
228 |
229 | msg := "Invalid Password"
230 | if r.linkSvc.CheckLinkPassword(l, password) {
231 | msg = "Valid Password"
232 | }
233 |
234 | return &Message{Message: msg}, nil
235 | }
236 |
237 | // ConnectStorageProvider resolver
238 | func (r *mutationResolver) ConnectStorageProvider(ctx context.Context, providerID int, providerToken string) (*Message, error) {
239 | user := r.authenticator.GetAuthenticatedUser(ctx)
240 | if user == nil {
241 | return nil, errUnauthenticated
242 | }
243 |
244 | err := r.userSvc.ConnectStorageProvider(user.ID, uint(providerID), providerToken)
245 | if err != nil {
246 | return nil, err
247 | }
248 |
249 | return &Message{Message: "Storage Provider successfully connected"}, nil
250 | }
251 |
252 | // DisconnectStorageProvider resolver
253 | func (r *mutationResolver) DisconnectStorageProvider(ctx context.Context, providerID int) (*Message, error) {
254 | user := r.authenticator.GetAuthenticatedUser(ctx)
255 | if user == nil {
256 | return nil, errUnauthenticated
257 | }
258 |
259 | err := r.userSvc.DisconnectStorageProvider(user.ID, uint(providerID))
260 | if err != nil {
261 | return nil, err
262 | }
263 |
264 | return &Message{Message: "Storage Provider disconnected"}, nil
265 | }
266 |
267 | type queryResolver struct{ *Resolver }
268 |
269 | // Links resolver
270 | func (r *queryResolver) Links(ctx context.Context) ([]*Link, error) {
271 | user := r.authenticator.GetAuthenticatedUser(ctx)
272 | if user == nil {
273 | return nil, errUnauthenticated
274 | }
275 |
276 | links, err := r.linkSvc.ListLinks(user.ID)
277 | if err != nil {
278 | return nil, err
279 | }
280 |
281 | return formatLinks(links), nil
282 | }
283 |
284 | // Me resolver
285 | func (r *queryResolver) Me(ctx context.Context) (*User, error) {
286 | user := r.authenticator.GetAuthenticatedUser(ctx)
287 | if user == nil {
288 | return nil, errUnauthenticated
289 | }
290 |
291 | uscs, err := r.userSvc.ListStorageProviders(user.ID)
292 | if err != nil {
293 | return nil, err
294 | }
295 |
296 | // map from domain.UserStorageProviderCredential to StorageProvider
297 | storageProviders := make([]*StorageProvider, len(uscs))
298 | for i, usc := range uscs {
299 | storageProviders[i] = &StorageProvider{
300 | ID: int(usc.ID),
301 | ProviderID: int(usc.ProviderID),
302 | Email: usc.Email,
303 | Photo: usc.Photo,
304 | }
305 | }
306 |
307 | return &User{
308 | ID: int(user.ID),
309 | Email: user.Email,
310 | Name: user.Name,
311 | ConnectedStorageProviders: storageProviders,
312 | }, nil
313 | }
314 |
315 | // Link resolver
316 | func (r *queryResolver) Link(ctx context.Context, slug string) (*Link, error) {
317 | // this is for public use, no need to check user auth
318 | link, err := r.linkSvc.FindLinkBySlug(slug)
319 | if err != nil {
320 | return nil, err
321 | }
322 |
323 | return formatLink(*link), nil
324 | }
325 |
326 | func formatLink(link domain.Link) *Link {
327 | formattedLink := &Link{
328 | ID: int(link.ID),
329 | Title: link.Title,
330 | IsProtected: link.IsProtected(),
331 | Slug: &link.Slug,
332 | Description: &link.Description,
333 | Deadline: link.Deadline,
334 | }
335 |
336 | if link.UserStorageCredential != nil {
337 | formattedLink.StorageProvider = &StorageProvider{
338 | ID: int(link.UserStorageCredential.ID),
339 | ProviderID: int(link.UserStorageCredential.ProviderID),
340 | Email: link.UserStorageCredential.Email,
341 | Photo: link.UserStorageCredential.Photo,
342 | }
343 | }
344 |
345 | return formattedLink
346 | }
347 |
348 | func formatLinks(links []domain.Link) []*Link {
349 | formattedLinks := make([]*Link, len(links))
350 | for i, link := range links {
351 | formattedLinks[i] = &Link{
352 | ID: int(link.ID),
353 | Title: link.Title,
354 | IsProtected: link.IsProtected(),
355 | Slug: &links[i].Slug,
356 | Description: &links[i].Description,
357 | Deadline: link.Deadline,
358 | }
359 |
360 | if link.UserStorageCredential != nil {
361 | formattedLinks[i].StorageProvider = &StorageProvider{
362 | ID: int(link.UserStorageCredential.ID),
363 | ProviderID: int(link.UserStorageCredential.ProviderID),
364 | Email: link.UserStorageCredential.Email,
365 | Photo: link.UserStorageCredential.Photo,
366 | }
367 | }
368 | }
369 | return formattedLinks
370 | }
371 |
--------------------------------------------------------------------------------
/domain/link/link_test.go:
--------------------------------------------------------------------------------
1 | package link_test
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | "time"
7 |
8 | "github.com/bccfilkom/drophere-go/domain"
9 | "github.com/bccfilkom/drophere-go/domain/link"
10 | "github.com/bccfilkom/drophere-go/infrastructure/database/inmemory"
11 | "github.com/bccfilkom/drophere-go/infrastructure/hasher"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | var dummyHasher domain.Hasher
17 |
18 | func init() {
19 | dummyHasher = hasher.NewNotAHasher()
20 | }
21 |
22 | func newRepo() (domain.LinkRepository, domain.UserRepository, domain.UserStorageCredentialRepository) {
23 | memdb := inmemory.New()
24 | return inmemory.NewLinkRepository(memdb), inmemory.NewUserRepository(memdb), inmemory.NewUserStorageCredentialRepository(memdb)
25 | }
26 |
27 | func str2ptr(s string) *string {
28 | return &s
29 | }
30 |
31 | func time2ptr(t time.Time) *time.Time {
32 | return &t
33 | }
34 |
35 | func uint2ptr(u uint) *uint {
36 | return &u
37 | }
38 |
39 | func TestCheckLinkPassword(t *testing.T) {
40 | type test struct {
41 | link *domain.Link
42 | password string
43 | wantResult bool
44 | }
45 |
46 | linkRepo, _, uscRepo := newRepo()
47 | getLink := func(id uint) *domain.Link {
48 | l, _ := linkRepo.FindByID(id)
49 | return l
50 | }
51 | tests := []test{
52 | {
53 | link: getLink(1),
54 | password: "",
55 | wantResult: false,
56 | },
57 | {
58 | link: getLink(1),
59 | password: "abcdef",
60 | wantResult: false,
61 | },
62 | {
63 | link: getLink(1),
64 | password: "123098",
65 | wantResult: true,
66 | },
67 | {
68 | link: getLink(2),
69 | password: "123098",
70 | wantResult: true,
71 | },
72 | {
73 | link: getLink(2),
74 | password: "",
75 | wantResult: true,
76 | },
77 | }
78 |
79 | linkSvc := link.NewService(linkRepo, uscRepo, dummyHasher)
80 |
81 | for i, tc := range tests {
82 | gotResult := linkSvc.CheckLinkPassword(tc.link, tc.password)
83 | if gotResult != tc.wantResult {
84 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantResult, gotResult)
85 | }
86 | }
87 |
88 | }
89 |
90 | func TestCreateLink(t *testing.T) {
91 | type test struct {
92 | title string
93 | slug string
94 | description string
95 | deadline *time.Time
96 | password *string
97 | user *domain.User
98 | providerID *uint
99 | wantLink *domain.Link
100 | wantErr error
101 | }
102 |
103 | linkRepo, userRepo, uscRepo := newRepo()
104 | user, _ := userRepo.FindByID(1)
105 | uscUser1, _ := uscRepo.FindByID(2000, false)
106 |
107 | linkDeadline := time.Date(2020, time.November, 11, 1, 2, 3, 0, time.UTC)
108 |
109 | tests := []test{
110 | {
111 | title: "Drop file here",
112 | slug: "drop-here",
113 | description: "drop a file here",
114 | user: user,
115 | wantErr: domain.ErrLinkDuplicatedSlug,
116 | },
117 | {
118 | title: "Drop CV",
119 | slug: "yoursummerintern",
120 | description: "Drop your CV for summer internship",
121 | user: user,
122 | wantLink: &domain.Link{
123 | ID: 4,
124 | UserID: user.ID,
125 | Title: "Drop CV",
126 | Slug: "yoursummerintern",
127 | Description: "Drop your CV for summer internship",
128 | },
129 | wantErr: nil,
130 | },
131 | {
132 | title: "Link with associated storage provider",
133 | slug: "linktomockbox",
134 | description: "hello there, please upload a file",
135 | user: user,
136 | providerID: uint2ptr(1234),
137 | wantErr: domain.ErrUserStorageCredentialNotFound,
138 | },
139 | {
140 | title: "Link with associated storage provider",
141 | slug: "linktomockbox",
142 | description: "hello there, please upload a file",
143 | user: user,
144 | providerID: uint2ptr(1),
145 | wantLink: &domain.Link{
146 | ID: 5,
147 | UserID: user.ID,
148 | Title: "Link with associated storage provider",
149 | Slug: "linktomockbox",
150 | Description: "hello there, please upload a file",
151 | Password: "",
152 | UserStorageCredentialID: uint2ptr(2000),
153 | UserStorageCredential: &uscUser1,
154 | },
155 | wantErr: nil,
156 | },
157 | {
158 | title: "Link with associated storage provider",
159 | slug: "guarded-with-pwd-and-deadline",
160 | description: "hello there, please upload a file",
161 | deadline: &linkDeadline,
162 | password: str2ptr("abcdef"),
163 | user: user,
164 | providerID: uint2ptr(1),
165 | wantLink: &domain.Link{
166 | ID: 6,
167 | UserID: user.ID,
168 | Title: "Link with associated storage provider",
169 | Slug: "guarded-with-pwd-and-deadline",
170 | Description: "hello there, please upload a file",
171 | Deadline: &linkDeadline,
172 | Password: "abcdef",
173 | UserStorageCredentialID: uint2ptr(2000),
174 | UserStorageCredential: &uscUser1,
175 | },
176 | wantErr: nil,
177 | },
178 | }
179 |
180 | linkSvc := link.NewService(linkRepo, uscRepo, dummyHasher)
181 |
182 | for _, tc := range tests {
183 | gotLink, gotErr := linkSvc.CreateLink(tc.title, tc.slug, tc.description, tc.deadline, tc.password, tc.user, tc.providerID)
184 |
185 | assert.Equal(t, tc.wantErr, gotErr)
186 | assert.Equal(t, tc.wantLink, gotLink)
187 | }
188 |
189 | }
190 |
191 | func TestUpdateLink(t *testing.T) {
192 | type test struct {
193 | linkID uint
194 | title string
195 | slug string
196 | description *string
197 | deadline *time.Time
198 | password *string
199 | providerID *uint
200 | wantLink *domain.Link
201 | wantErr error
202 | }
203 |
204 | linkRepo, userRepo, uscRepo := newRepo()
205 | user, _ := userRepo.FindByID(1)
206 | uscUser1, _ := uscRepo.FindByID(2000, false)
207 |
208 | tests := []test{
209 | {
210 | linkID: 123,
211 | title: "Drop file here",
212 | slug: "drop-here",
213 | wantErr: domain.ErrLinkNotFound,
214 | },
215 | {
216 | linkID: 2,
217 | title: "Drop file here",
218 | slug: "drop-here",
219 | wantErr: domain.ErrLinkDuplicatedSlug,
220 | },
221 | {
222 | linkID: 1,
223 | title: "Drop CV 2",
224 | slug: "yoursummerintern2",
225 | description: str2ptr("Drop your CV for summer internship 2019"),
226 | deadline: time2ptr(time.Date(2019, 1, 2, 3, 0, 0, 0, time.Local)),
227 | password: str2ptr("123098"),
228 | wantErr: nil,
229 | wantLink: &domain.Link{
230 | ID: 1,
231 | Title: "Drop CV 2",
232 | Slug: "yoursummerintern2",
233 | Description: "Drop your CV for summer internship 2019",
234 | Deadline: time2ptr(time.Date(2019, 1, 2, 3, 0, 0, 0, time.Local)),
235 | Password: "123098",
236 | UserID: user.ID,
237 | User: user,
238 | },
239 | },
240 | {
241 | linkID: 1,
242 | title: "Drop CV 2",
243 | slug: "yoursummerintern2",
244 | description: str2ptr("Drop your CV for summer internship 2019"),
245 | deadline: time2ptr(time.Date(2019, 1, 2, 3, 0, 0, 0, time.Local)),
246 | password: str2ptr("123098"),
247 | providerID: uint2ptr(1234),
248 | wantErr: domain.ErrUserStorageCredentialNotFound,
249 | },
250 | {
251 | linkID: 1,
252 | title: "Drop CV 2 With MockBox",
253 | slug: "yoursummerintern2mockbox",
254 | description: str2ptr("Drop your CV for summer internship 2019"),
255 | deadline: time2ptr(time.Date(2019, 1, 2, 3, 0, 0, 0, time.Local)),
256 | password: str2ptr("123098"),
257 | providerID: uint2ptr(1),
258 | wantErr: nil,
259 | wantLink: &domain.Link{
260 | ID: 1,
261 | Title: "Drop CV 2 With MockBox",
262 | Slug: "yoursummerintern2mockbox",
263 | Description: "Drop your CV for summer internship 2019",
264 | Deadline: time2ptr(time.Date(2019, 1, 2, 3, 0, 0, 0, time.Local)),
265 | Password: "123098",
266 | UserID: user.ID,
267 | User: user,
268 | UserStorageCredentialID: uint2ptr(uscUser1.ID),
269 | UserStorageCredential: &uscUser1,
270 | },
271 | },
272 | }
273 |
274 | linkSvc := link.NewService(linkRepo, uscRepo, dummyHasher)
275 |
276 | for _, tc := range tests {
277 | gotLink, gotErr := linkSvc.UpdateLink(tc.linkID, tc.title, tc.slug, tc.description, tc.deadline, tc.password, tc.providerID)
278 |
279 | assert.Equal(t, tc.wantErr, gotErr)
280 | assert.Equal(t, tc.wantLink, gotLink)
281 | }
282 |
283 | }
284 |
285 | func TestDeleteLink(t *testing.T) {
286 | type test struct {
287 | linkID uint
288 | wantErr error
289 | }
290 |
291 | linkRepo, _, uscRepo := newRepo()
292 |
293 | tests := []test{
294 | {
295 | linkID: 123,
296 | wantErr: domain.ErrLinkNotFound,
297 | },
298 | {
299 | linkID: 1,
300 | wantErr: nil,
301 | },
302 | }
303 |
304 | linkSvc := link.NewService(linkRepo, uscRepo, dummyHasher)
305 |
306 | for i, tc := range tests {
307 | gotErr := linkSvc.DeleteLink(tc.linkID)
308 | if gotErr != tc.wantErr {
309 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantErr, gotErr)
310 | }
311 | }
312 |
313 | }
314 |
315 | func TestFetchLink(t *testing.T) {
316 | type test struct {
317 | linkID uint
318 | wantErr error
319 | wantLink *domain.Link
320 | }
321 |
322 | linkRepo, userRepo, uscRepo := newRepo()
323 |
324 | user, _ := userRepo.FindByID(1)
325 |
326 | tests := []test{
327 | {
328 | linkID: 123,
329 | wantErr: domain.ErrLinkNotFound,
330 | },
331 | {
332 | linkID: 1,
333 | wantErr: nil,
334 | wantLink: &domain.Link{
335 | ID: 1,
336 | UserID: user.ID,
337 | User: user,
338 | Title: "Drop file here",
339 | Slug: "drop-here",
340 | Password: "123098",
341 | Description: "drop a file here",
342 | },
343 | },
344 | }
345 |
346 | linkSvc := link.NewService(linkRepo, uscRepo, dummyHasher)
347 |
348 | for i, tc := range tests {
349 | gotLink, gotErr := linkSvc.FetchLink(tc.linkID)
350 | if gotErr != tc.wantErr {
351 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantErr, gotErr)
352 | }
353 |
354 | if !reflect.DeepEqual(gotLink, tc.wantLink) {
355 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantLink, gotLink)
356 | }
357 | }
358 |
359 | }
360 |
361 | func TestFindLinkBySlug(t *testing.T) {
362 | type test struct {
363 | slug string
364 | wantErr error
365 | wantLink *domain.Link
366 | }
367 |
368 | linkRepo, userRepo, uscRepo := newRepo()
369 |
370 | user, _ := userRepo.FindByID(1)
371 |
372 | tests := []test{
373 | {
374 | slug: "123",
375 | wantErr: domain.ErrLinkNotFound,
376 | },
377 | {
378 | slug: "drop-here",
379 | wantErr: nil,
380 | wantLink: &domain.Link{
381 | ID: 1,
382 | UserID: user.ID,
383 | User: user,
384 | Title: "Drop file here",
385 | Slug: "drop-here",
386 | Password: "123098",
387 | Description: "drop a file here",
388 | },
389 | },
390 | }
391 |
392 | linkSvc := link.NewService(linkRepo, uscRepo, dummyHasher)
393 |
394 | for i, tc := range tests {
395 | gotLink, gotErr := linkSvc.FindLinkBySlug(tc.slug)
396 | if gotErr != tc.wantErr {
397 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantErr, gotErr)
398 | }
399 |
400 | if !reflect.DeepEqual(gotLink, tc.wantLink) {
401 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantLink, gotLink)
402 | }
403 | }
404 |
405 | }
406 |
407 | func TestListLinks(t *testing.T) {
408 | type test struct {
409 | userID uint
410 | wantErr error
411 | wantLinks []domain.Link
412 | }
413 |
414 | linkRepo, userRepo, uscRepo := newRepo()
415 |
416 | user, _ := userRepo.FindByID(1)
417 |
418 | tests := []test{
419 | {
420 | userID: 123,
421 | wantErr: nil,
422 | wantLinks: []domain.Link{},
423 | },
424 | {
425 | userID: 1,
426 | wantErr: nil,
427 | wantLinks: []domain.Link{
428 | {
429 | ID: 1,
430 | UserID: user.ID,
431 | User: user,
432 | Title: "Drop file here",
433 | Slug: "drop-here",
434 | Password: "123098",
435 | Description: "drop a file here",
436 | },
437 | {
438 | ID: 2,
439 | UserID: user.ID,
440 | User: user,
441 | Title: "Test Link 2",
442 | Slug: "test-link-2",
443 | Password: "",
444 | Description: "no description",
445 | },
446 | },
447 | },
448 | }
449 |
450 | linkSvc := link.NewService(linkRepo, uscRepo, dummyHasher)
451 |
452 | for _, tc := range tests {
453 | gotLinks, gotErr := linkSvc.ListLinks(tc.userID)
454 |
455 | assert.Equal(t, tc.wantErr, gotErr)
456 | assert.Equal(t, tc.wantLinks, gotLinks)
457 |
458 | }
459 |
460 | }
461 |
--------------------------------------------------------------------------------
/domain/user/user_test.go:
--------------------------------------------------------------------------------
1 | package user_test
2 |
3 | import (
4 | "bytes"
5 | "reflect"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stretchr/testify/assert"
10 |
11 | htmlTemplate "html/template"
12 | textTemplate "text/template"
13 |
14 | "github.com/bccfilkom/drophere-go/domain"
15 | "github.com/bccfilkom/drophere-go/domain/user"
16 | "github.com/bccfilkom/drophere-go/infrastructure/auth"
17 | "github.com/bccfilkom/drophere-go/infrastructure/database/inmemory"
18 | "github.com/bccfilkom/drophere-go/infrastructure/hasher"
19 | "github.com/bccfilkom/drophere-go/infrastructure/mailer"
20 | "github.com/bccfilkom/drophere-go/infrastructure/storageprovider"
21 | "github.com/bccfilkom/drophere-go/infrastructure/stringgenerator"
22 | )
23 |
24 | var (
25 | authenticator domain.Authenticator
26 | dummyHasher domain.Hasher
27 | mockMailer domain.Mailer
28 | strGen domain.StringGenerator
29 | htmlTemplates *htmlTemplate.Template
30 | textTemplates *textTemplate.Template
31 |
32 | storageProviderPool domain.StorageProviderPool
33 | )
34 |
35 | func init() {
36 | authenticator = auth.NewJWTMock()
37 | dummyHasher = hasher.NewNotAHasher()
38 | strGen = stringgenerator.NewMock()
39 | mockMailer = mailer.NewMockMailer()
40 | stringgenerator.SetMockResult("this_is_not_a_random_string")
41 | mockStorageProvider := storageprovider.NewMock()
42 | storageProviderPool.Register(mockStorageProvider)
43 |
44 | var err error
45 |
46 | htmlTemplates, err = htmlTemplate.
47 | New("request_password_recovery_html").
48 | Parse("{{.Token}}")
49 | if err != nil {
50 | panic(err)
51 | }
52 |
53 | textTemplates, err = textTemplate.
54 | New("request_password_recovery_text").
55 | Parse("{{.Token}}")
56 | if err != nil {
57 | panic(err)
58 | }
59 | }
60 |
61 | func newRepo() (domain.UserRepository, domain.UserStorageCredentialRepository) {
62 | memdb := inmemory.New()
63 | return inmemory.NewUserRepository(memdb), inmemory.NewUserStorageCredentialRepository(memdb)
64 | }
65 |
66 | func str2ptr(s string) *string {
67 | return &s
68 | }
69 |
70 | func time2ptr(t time.Time) *time.Time {
71 | return &t
72 | }
73 |
74 | func TestRegister(t *testing.T) {
75 | type test struct {
76 | email string
77 | name string
78 | password string
79 | wantUser *domain.User
80 | wantErr error
81 | }
82 |
83 | tests := []test{
84 | {email: "user@drophere.link", name: "User", password: "123456", wantErr: domain.ErrUserDuplicated},
85 | {email: "new_user@drophere.link", name: "New User", password: "123456", wantErr: nil},
86 | }
87 |
88 | userRepo, userStorageCredRepo := newRepo()
89 | userSvc := user.NewService(
90 | userRepo,
91 | userStorageCredRepo,
92 | authenticator,
93 | mockMailer,
94 | dummyHasher,
95 | strGen,
96 | storageProviderPool,
97 | htmlTemplates,
98 | textTemplates,
99 | user.Config{},
100 | )
101 |
102 | for i, tc := range tests {
103 | _, gotErr := userSvc.Register(tc.email, tc.name, tc.password)
104 | if gotErr != tc.wantErr {
105 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantErr, gotErr)
106 | }
107 | }
108 | }
109 |
110 | func TestAuth(t *testing.T) {
111 | type test struct {
112 | email string
113 | password string
114 | wantCreds *domain.UserCredentials
115 | wantErr error
116 | }
117 |
118 | tests := []test{
119 | {email: "", password: "", wantErr: domain.ErrUserNotFound},
120 | {email: "user@drophere.link", password: "", wantErr: domain.ErrUserInvalidPassword},
121 | {email: "user@drophere.link", password: "123456", wantCreds: &domain.UserCredentials{Token: "user_token_1"}},
122 | }
123 |
124 | userRepo, userStorageCredRepo := newRepo()
125 | userSvc := user.NewService(
126 | userRepo,
127 | userStorageCredRepo,
128 | authenticator,
129 | mockMailer,
130 | dummyHasher,
131 | strGen,
132 | storageProviderPool,
133 | htmlTemplates,
134 | textTemplates,
135 | user.Config{},
136 | )
137 |
138 | for i, tc := range tests {
139 | gotCreds, gotErr := userSvc.Auth(tc.email, tc.password)
140 | if gotErr != tc.wantErr {
141 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantErr, gotErr)
142 | }
143 | if gotCreds != nil && gotCreds.Token != tc.wantCreds.Token {
144 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantCreds.Token, gotCreds.Token)
145 | }
146 | }
147 | }
148 |
149 | func TestUpdateStorageToken(t *testing.T) {
150 | type test struct {
151 | userID uint
152 | dropboxToken *string
153 | wantUser *domain.User
154 | wantErr error
155 | }
156 |
157 | userRepo, userStorageCredRepo := newRepo()
158 | u, _ := userRepo.FindByID(1)
159 |
160 | tests := []test{
161 | {userID: 123, wantErr: domain.ErrUserNotFound, wantUser: nil},
162 | {
163 | userID: 1,
164 | dropboxToken: str2ptr("my_dropbox_token_here"),
165 | wantUser: &domain.User{
166 | ID: u.ID,
167 | Email: u.Email,
168 | Name: u.Name,
169 | Password: u.Password,
170 | DropboxToken: str2ptr("my_dropbox_token_here"),
171 | },
172 | },
173 | }
174 |
175 | userSvc := user.NewService(
176 | userRepo,
177 | userStorageCredRepo,
178 | authenticator,
179 | mockMailer,
180 | dummyHasher,
181 | strGen,
182 | storageProviderPool,
183 | htmlTemplates,
184 | textTemplates,
185 | user.Config{},
186 | )
187 |
188 | for i, tc := range tests {
189 | gotUser, gotErr := userSvc.UpdateStorageToken(tc.userID, tc.dropboxToken)
190 | if gotErr != tc.wantErr {
191 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantErr, gotErr)
192 | }
193 | if !reflect.DeepEqual(gotUser, tc.wantUser) {
194 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantUser, gotUser)
195 | }
196 | }
197 | }
198 |
199 | func TestUpdate(t *testing.T) {
200 | type test struct {
201 | userID uint
202 | name *string
203 | password *string
204 | oldPassword *string
205 | wantUser *domain.User
206 | wantErr error
207 | }
208 |
209 | userRepo, userStorageCredRepo := newRepo()
210 | u, _ := userRepo.FindByID(1)
211 |
212 | tests := []test{
213 | {userID: 123, wantErr: domain.ErrUserNotFound, wantUser: nil},
214 | {userID: 1, password: str2ptr("new_password123"), oldPassword: nil, wantErr: domain.ErrUserInvalidPassword},
215 | {userID: 1, password: str2ptr("new_password123"), oldPassword: str2ptr(""), wantErr: domain.ErrUserInvalidPassword},
216 | {
217 | userID: 1,
218 | name: str2ptr("new name 123"),
219 | password: str2ptr("new_password123"),
220 | oldPassword: str2ptr("123456"),
221 | wantUser: &domain.User{
222 | ID: u.ID,
223 | Email: u.Email,
224 | Name: "new name 123",
225 | Password: "new_password123",
226 | },
227 | },
228 | }
229 |
230 | userSvc := user.NewService(
231 | userRepo,
232 | userStorageCredRepo,
233 | authenticator,
234 | mockMailer,
235 | dummyHasher,
236 | strGen,
237 | storageProviderPool,
238 | htmlTemplates,
239 | textTemplates,
240 | user.Config{},
241 | )
242 |
243 | for i, tc := range tests {
244 | gotUser, gotErr := userSvc.Update(tc.userID, tc.name, tc.password, tc.oldPassword)
245 | if gotErr != tc.wantErr {
246 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantErr, gotErr)
247 | }
248 | if !reflect.DeepEqual(gotUser, tc.wantUser) {
249 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantUser, gotUser)
250 | }
251 | }
252 | }
253 |
254 | func TestRequestPasswordRecovery(t *testing.T) {
255 | type test struct {
256 | email string
257 | wantErr error
258 | }
259 |
260 | userRepo, userStorageCredRepo := newRepo()
261 | u, _ := userRepo.FindByEmail("reset+pwd@drophere.link")
262 | expectedToken := str2ptr("this_is_not_a_random_string")
263 | emailHTMLTemplate := htmlTemplates.Lookup("request_password_recovery_html")
264 | emailTextTemplate := textTemplates.Lookup("request_password_recovery_text")
265 | templateContent := map[string]string{
266 | "Token": *expectedToken,
267 | }
268 |
269 | expectedHTMLEmailMessage := &bytes.Buffer{}
270 | emailHTMLTemplate.Execute(expectedHTMLEmailMessage, templateContent)
271 | expectedTextEmailMessage := &bytes.Buffer{}
272 | emailTextTemplate.Execute(expectedTextEmailMessage, templateContent)
273 |
274 | expectedMail := mailer.MockMessage{
275 | From: "admin@drophere.link",
276 | To: "reset+pwd@drophere.link",
277 | Title: "Recover Password",
278 | MessagePlain: expectedTextEmailMessage.String(),
279 | MessageHTML: expectedHTMLEmailMessage.String(),
280 | }
281 |
282 | tests := []test{
283 | {email: "", wantErr: domain.ErrUserNotFound},
284 | {email: "reset+pwd@drophere.link", wantErr: nil},
285 | }
286 |
287 | userSvc := user.NewService(
288 | userRepo,
289 | userStorageCredRepo,
290 | authenticator,
291 | mockMailer,
292 | dummyHasher,
293 | strGen,
294 | storageProviderPool,
295 | htmlTemplates,
296 | textTemplates,
297 | user.Config{},
298 | )
299 |
300 | for i, tc := range tests {
301 | // reset inbox
302 | mailer.ClearMessages()
303 |
304 | gotErr := userSvc.RequestPasswordRecovery(tc.email)
305 | if gotErr != tc.wantErr {
306 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantErr, gotErr)
307 | }
308 |
309 | if gotErr == nil {
310 | if !reflect.DeepEqual(u.RecoverPasswordToken, expectedToken) {
311 | t.Fatalf("test %d: expected: %v, got: %v", i, expectedToken, u.RecoverPasswordToken)
312 | }
313 | // TODO: Mock time using https://github.com/bouk/monkey
314 | if !reflect.DeepEqual(mailer.MockMessages[0], expectedMail) {
315 | t.Fatalf("test %d: expected: %v, got: %v", i, expectedMail, mailer.MockMessages[0])
316 | }
317 | }
318 |
319 | }
320 | }
321 |
322 | func TestRecoverPassword(t *testing.T) {
323 | type test struct {
324 | email string
325 | token string
326 | newPassword string
327 | expectedUser *domain.User
328 | wantErr error
329 | }
330 |
331 | recoverPasswordToken := "this_is_a_recover_password_token"
332 |
333 | userRepo, userStorageCredRepo := newRepo()
334 | u, _ := userRepo.FindByEmail("reset+pwd@drophere.link")
335 | u.RecoverPasswordToken = str2ptr(recoverPasswordToken)
336 | u.RecoverPasswordTokenExpiry = time2ptr(time.Now().Add(30 * time.Minute))
337 |
338 | expiredRPTUser, _ := userRepo.FindByEmail("reset+pwd+expired_token@drophere.link")
339 | expiredRPTUser.RecoverPasswordToken = str2ptr(recoverPasswordToken)
340 |
341 | tests := []test{
342 | {email: "", wantErr: domain.ErrUserNotFound},
343 | {email: "reset+pwd@drophere.link", token: "", wantErr: domain.ErrUserNotFound},
344 | // {email: "reset+pwd@drophere.link", token: recoverPasswordToken, newPassword: "", wantErr: domain.ErrUserNotFound},
345 | {
346 | email: "reset+pwd+expired_token@drophere.link",
347 | token: recoverPasswordToken,
348 | newPassword: "new_password_for_this_user",
349 | wantErr: domain.ErrUserPasswordRecoveryTokenExpired,
350 | },
351 | {
352 | email: "reset+pwd@drophere.link",
353 | token: recoverPasswordToken,
354 | newPassword: "new_password_for_this_user",
355 | expectedUser: &domain.User{
356 | ID: u.ID,
357 | Email: u.Email,
358 | Name: u.Name,
359 | Password: "new_password_for_this_user",
360 | RecoverPasswordToken: nil,
361 | RecoverPasswordTokenExpiry: nil,
362 | },
363 | wantErr: nil,
364 | },
365 | }
366 |
367 | userSvc := user.NewService(
368 | userRepo,
369 | userStorageCredRepo,
370 | authenticator,
371 | mockMailer,
372 | dummyHasher,
373 | strGen,
374 | storageProviderPool,
375 | htmlTemplates,
376 | textTemplates,
377 | user.Config{},
378 | )
379 |
380 | for i, tc := range tests {
381 |
382 | gotErr := userSvc.RecoverPassword(tc.email, tc.token, tc.newPassword)
383 | if gotErr != tc.wantErr {
384 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantErr, gotErr)
385 | }
386 |
387 | if gotErr == nil {
388 | if !reflect.DeepEqual(u, tc.expectedUser) {
389 | t.Fatalf("test %d: expected: %+v, got: %+v", i, tc.expectedUser, u)
390 | }
391 | }
392 |
393 | }
394 | }
395 |
396 | func TestConnectStorageProvider(t *testing.T) {
397 | type test struct {
398 | userID uint
399 | providerID uint
400 | providerCredential string
401 | accountInfo domain.StorageProviderAccountInfo
402 | wantErr error
403 | }
404 |
405 | userRepo, userStorageCredRepo := newRepo()
406 | // user1, _ := userRepo.FindByID(1)
407 |
408 | tests := []test{
409 | {
410 | userID: 123,
411 | wantErr: domain.ErrStorageProviderInvalid,
412 | },
413 | {
414 | userID: 123,
415 | providerID: 1,
416 | wantErr: domain.ErrUserNotFound,
417 | },
418 | {
419 | // update existing token
420 | userID: 1,
421 | providerID: 1,
422 | providerCredential: "dropboxToken+0xbadc0de",
423 | accountInfo: domain.StorageProviderAccountInfo{
424 | Email: "user_1_another_email@drophere.link",
425 | Photo: "https://my.photo/user_1.jpg",
426 | },
427 | wantErr: nil,
428 | },
429 | {
430 | // create new record
431 | userID: 357,
432 | providerID: 1,
433 | providerCredential: "mockStorageToken+0xbadc0de",
434 | accountInfo: domain.StorageProviderAccountInfo{
435 | Email: "user_357_another_email@drophere.link",
436 | Photo: "https://my.photo/user_357.jpg",
437 | },
438 | wantErr: nil,
439 | },
440 | }
441 |
442 | userSvc := user.NewService(
443 | userRepo,
444 | userStorageCredRepo,
445 | authenticator,
446 | mockMailer,
447 | dummyHasher,
448 | strGen,
449 | storageProviderPool,
450 | htmlTemplates,
451 | textTemplates,
452 | user.Config{},
453 | )
454 |
455 | for i, tc := range tests {
456 |
457 | storageprovider.SetSharedAccountInfo(tc.accountInfo)
458 |
459 | gotErr := userSvc.ConnectStorageProvider(tc.userID, tc.providerID, tc.providerCredential)
460 | if gotErr != tc.wantErr {
461 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantErr, gotErr)
462 | }
463 |
464 | if gotErr == nil {
465 | ucs, _ := userStorageCredRepo.Find(domain.UserStorageCredentialFilters{
466 | UserIDs: []uint{tc.userID},
467 | }, false)
468 |
469 | if tc.providerCredential != ucs[0].ProviderCredential {
470 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.providerCredential, ucs[0].ProviderCredential)
471 | }
472 |
473 | if tc.accountInfo.Email != ucs[0].Email {
474 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.accountInfo.Email, ucs[0].Email)
475 | }
476 |
477 | if tc.accountInfo.Photo != ucs[0].Photo {
478 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.accountInfo.Photo, ucs[0].Photo)
479 | }
480 |
481 | }
482 |
483 | }
484 | }
485 |
486 | func TestDisconnectStorageProvider(t *testing.T) {
487 | type test struct {
488 | userID uint
489 | providerID uint
490 | wantErr error
491 | }
492 |
493 | userRepo, userStorageCredRepo := newRepo()
494 | // user1, _ := userRepo.FindByID(1)
495 |
496 | tests := []test{
497 | {
498 | userID: 123,
499 | wantErr: domain.ErrStorageProviderInvalid,
500 | },
501 | {
502 | userID: 123,
503 | providerID: 1,
504 | wantErr: domain.ErrUserNotFound,
505 | },
506 | {
507 | // delete existing token
508 | userID: 1,
509 | providerID: 1,
510 | wantErr: nil,
511 | },
512 | {
513 | // delete empty record
514 | userID: 357,
515 | providerID: 1,
516 | wantErr: nil,
517 | },
518 | }
519 |
520 | userSvc := user.NewService(
521 | userRepo,
522 | userStorageCredRepo,
523 | authenticator,
524 | mockMailer,
525 | dummyHasher,
526 | strGen,
527 | storageProviderPool,
528 | htmlTemplates,
529 | textTemplates,
530 | user.Config{},
531 | )
532 |
533 | for i, tc := range tests {
534 |
535 | gotErr := userSvc.DisconnectStorageProvider(tc.userID, tc.providerID)
536 | if gotErr != tc.wantErr {
537 | t.Fatalf("test %d: expected: %v, got: %v", i, tc.wantErr, gotErr)
538 | }
539 |
540 | if gotErr == nil {
541 | ucs, _ := userStorageCredRepo.Find(domain.UserStorageCredentialFilters{
542 | UserIDs: []uint{tc.userID},
543 | ProviderIDs: []uint{tc.providerID},
544 | }, false)
545 |
546 | if len(ucs) > 0 {
547 | t.Fatalf("test %d: expected: %v, got: %v", i, nil, ucs)
548 | }
549 |
550 | }
551 |
552 | }
553 | }
554 |
555 | func TestListStorageProviders(t *testing.T) {
556 | type test struct {
557 | userID uint
558 | expectedProviders []domain.UserStorageCredential
559 | wantErr error
560 | }
561 |
562 | userRepo, userStorageCredRepo := newRepo()
563 | uscsUser1, _ := userStorageCredRepo.Find(domain.UserStorageCredentialFilters{
564 | UserIDs: []uint{1},
565 | }, false)
566 |
567 | tests := []test{
568 | {
569 | // expect empty
570 | userID: 123,
571 | expectedProviders: []domain.UserStorageCredential{},
572 | wantErr: nil,
573 | },
574 | {
575 | // expect non-empty credential
576 | userID: 1,
577 | expectedProviders: uscsUser1,
578 | wantErr: nil,
579 | },
580 | }
581 |
582 | userSvc := user.NewService(
583 | userRepo,
584 | userStorageCredRepo,
585 | authenticator,
586 | mockMailer,
587 | dummyHasher,
588 | strGen,
589 | storageProviderPool,
590 | htmlTemplates,
591 | textTemplates,
592 | user.Config{},
593 | )
594 |
595 | for _, tc := range tests {
596 |
597 | uscs, gotErr := userSvc.ListStorageProviders(tc.userID)
598 | assert.ElementsMatch(t, tc.expectedProviders, uscs)
599 | assert.Equal(t, tc.wantErr, gotErr)
600 |
601 | }
602 | }
603 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
4 | cloud.google.com/go v0.39.0 h1:UgQP9na6OTfp4dsAiz/eFpFA1C6tPdH5wiRdi19tuMw=
5 | cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts=
6 | github.com/99designs/gqlgen v0.9.0 h1:g1arBPML74Vqv0L3Q+TqIhGXLspV+2MYtRLkBxuZrlE=
7 | github.com/99designs/gqlgen v0.9.0/go.mod h1:HrrG7ic9EgLPsULxsZh/Ti+p0HNWgR3XRuvnD0pb5KY=
8 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
9 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
10 | github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
11 | github.com/DataDog/zstd v1.4.0/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
12 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
13 | github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
14 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
15 | github.com/Shopify/sarama v1.22.1/go.mod h1:FRzlvRpMFO/639zY1SDxUxkqH97Y0ndM5CbGj6oG3As=
16 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
17 | github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
18 | github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
19 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
20 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
21 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
22 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
23 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
24 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
25 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
26 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
27 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
28 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
29 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
30 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
31 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
32 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
33 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
34 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
35 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
36 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
37 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
38 | github.com/denisenkom/go-mssqldb v0.0.0-20190423183735-731ef375ac02/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
39 | github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA=
40 | github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
41 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
42 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
43 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
44 | github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
45 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
46 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
47 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
48 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
49 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
50 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
51 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
52 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
53 | github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ=
54 | github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
55 | github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
56 | github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
57 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
58 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
59 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
60 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
61 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
62 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
63 | github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
64 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
65 | github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
66 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
67 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
68 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
69 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
70 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
71 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
72 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
73 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
74 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
75 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
76 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
77 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
78 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
79 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
80 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
81 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
82 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
83 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
84 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
85 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
86 | github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
87 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
88 | github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
89 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
90 | github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
91 | github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
92 | github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
93 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
94 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
95 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
96 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
97 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
98 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
99 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
100 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
101 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
102 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
103 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
104 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
105 | github.com/jinzhu/gorm v1.9.8 h1:n5uvxqLepIP2R1XF7pudpt9Rv8I3m7G9trGxJVjLZ5k=
106 | github.com/jinzhu/gorm v1.9.8/go.mod h1:bdqTT3q6dhSph2K3pWxrHP6nqxuAp2yQ3KFtc3U3F84=
107 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
108 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
109 | github.com/jinzhu/now v1.0.0 h1:6WV8LvwPpDhKjo5U9O6b4+xdG/jTXNPwlDme/MTo8Ns=
110 | github.com/jinzhu/now v1.0.0/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
111 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
112 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
113 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
114 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
115 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
116 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
117 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
118 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
119 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
120 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
121 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
122 | github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
123 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
124 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
125 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
126 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
127 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
128 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
129 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
130 | github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
131 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
132 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
133 | github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
134 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
135 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
136 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
137 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
138 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
139 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
140 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
141 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
142 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
143 | github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
144 | github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
145 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
146 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
147 | github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
148 | github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
149 | github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
150 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
151 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
152 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
153 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
154 | github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
155 | github.com/pkg/profile v1.3.0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
156 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
157 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
158 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
159 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
160 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
161 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
162 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
163 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
164 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
165 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
166 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
167 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
168 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
169 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
170 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
171 | github.com/prometheus/procfs v0.0.0-20190523193104-a7aeb8df3389/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
172 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
173 | github.com/prometheus/tsdb v0.8.0/go.mod h1:fSI0j+IUQrDd7+ZtR9WKIGtoYAYAJUKcKhYLG25tN4g=
174 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
175 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
176 | github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
177 | github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
178 | github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
179 | github.com/sendgrid/rest v2.4.1+incompatible h1:HDib/5xzQREPq34lN3YMhQtMkdXxS/qLp5G3k9a5++4=
180 | github.com/sendgrid/rest v2.4.1+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
181 | github.com/sendgrid/sendgrid-go v3.5.0+incompatible h1:kosbgHyNVYVaqECDYvFVLVD9nvThweBd6xp7vaCT3GI=
182 | github.com/sendgrid/sendgrid-go v3.5.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
183 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
184 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
185 | github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
186 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
187 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
188 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
189 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
190 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
191 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
192 | github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
193 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
194 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
195 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
196 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
197 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
198 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
199 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
200 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
201 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
202 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
203 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
204 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
205 | github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
206 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
207 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
208 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
209 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
210 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
211 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
212 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
213 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
214 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
215 | github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
216 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
217 | github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
218 | github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68=
219 | github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
220 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
221 | github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
222 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
223 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
224 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
225 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
226 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
227 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
228 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
229 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
230 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
231 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
232 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
233 | golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
234 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
235 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
236 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
237 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
238 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
239 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
240 | golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
241 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
242 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
243 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
244 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
245 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
246 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
247 | golang.org/x/mobile v0.0.0-20190509164839-32b2708ab171/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
248 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
249 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
250 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
251 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
252 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
253 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
254 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
255 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
256 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
257 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
258 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
259 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
260 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
261 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
262 | golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
263 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
264 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
265 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
266 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
267 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
268 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
269 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
270 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
271 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
272 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
273 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
274 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
275 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
276 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
277 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
278 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
279 | golang.org/x/sys v0.0.0-20190527104216-9cd6430ef91e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
280 | golang.org/x/sys v0.0.0-20190528012530-adf421d2caf4 h1:gd52YanAQJ4UkvuNi/7z63JEyc6ejHh9QwdzbTiEtAY=
281 | golang.org/x/sys v0.0.0-20190528012530-adf421d2caf4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
282 | golang.org/x/sys v0.0.0-20190528183647-3626398d7749 h1:oG2HS+e2B9VqK95y67B5MgJIJhOPY27/m5uJKJhHzus=
283 | golang.org/x/sys v0.0.0-20190528183647-3626398d7749/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
284 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
285 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
286 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
287 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
288 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
289 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
290 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
291 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
292 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
293 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
294 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
295 | golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
296 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
297 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
298 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
299 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
300 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
301 | golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
302 | golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
303 | golang.org/x/tools v0.0.0-20190525145741-7be61e1b0e51 h1:RhYYBLDB5MoVkvoNGMNk+DSj7WoGhySvIvtEjTyiP74=
304 | golang.org/x/tools v0.0.0-20190525145741-7be61e1b0e51/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
305 | golang.org/x/tools v0.0.0-20190529010454-aa71c3f32488 h1:vBgi/AgEje1rNScpWGJqe+RPHHZvBqrk9UH+LOXWN6Q=
306 | golang.org/x/tools v0.0.0-20190529010454-aa71c3f32488/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
307 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
308 | google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
309 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
310 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
311 | google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
312 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
313 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
314 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
315 | google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
316 | google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
317 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
318 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
319 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
320 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
321 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
322 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
323 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
324 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
325 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
326 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
327 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
328 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
329 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
330 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
331 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
332 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
333 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
334 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
335 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
336 | sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
337 | sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
338 |
--------------------------------------------------------------------------------