From c3df2396d3745c96c56d879f26dbd74372173277 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 13 Jun 2015 19:13:50 +0200 Subject: [PATCH 01/27] File rework --- .travis.yml | 1 + db/setup.go | 10 ++- models/file.go | 5 +- routes/files.go | 160 ++++++++++++++++++++++++++---------------------- setup/setup.go | 9 +-- 5 files changed, 105 insertions(+), 80 deletions(-) diff --git a/.travis.yml b/.travis.yml index f22bd85..d5c9f5a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ script: - godep go test -cpu=2 -race -v ./... - godep go test -cpu=2 -covermode=atomic ./... after_success: + - go install -v std - ./run 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then docker login --username="$DOCKER_USER" --password="$DOCKER_PASS" --email="$DOCKER_EMAIL" && docker build -t lavab/api:$TRAVIS_BRANCH . && docker push lavab/api; fi' diff --git a/db/setup.go b/db/setup.go index 3a1d4dd..0e2798f 100644 --- a/db/setup.go +++ b/db/setup.go @@ -72,7 +72,15 @@ func Setup(opts r.ConnectOpts) error { r.DB(d).Table("files").IndexCreate("name").Exec(ss) r.DB(d).Table("files").IndexCreate("date_created").Exec(ss) r.DB(d).Table("files").IndexCreate("date_modified").Exec(ss) - + r.DB(d).Table("files").IndexCreate("tags", r.IndexCreateOpts{Multi: true}).Exec(ss) + r.DB(d).Table("files").IndexCreateFunc("ownerTags", func(row r.Term) r.Term { + return row.Field("tags").Map(func(tag r.Term) []interface{} { + return []interface{}{ + row.Field("owner"), + tag, + } + }) + }, r.IndexCreateOpts{Multi: true}) r.DB(d).TableCreate("keys").Exec(ss) r.DB(d).Table("keys").IndexCreate("owner").Exec(ss) r.DB(d).Table("keys").IndexCreate("date_created").Exec(ss) diff --git a/models/file.go b/models/file.go index 0414578..953d302 100644 --- a/models/file.go +++ b/models/file.go @@ -2,6 +2,9 @@ package models // File is an encrypted file stored by Lavaboom type File struct { - Encrypted Resource + + Meta interface{} `json:"meta" gorethink:"meta"` + Body []byte `json:"body" gorethink:"body"` + Tags []string `json:"tags" gorethink:"tags"` } diff --git a/routes/files.go b/routes/files.go index 3730f31..feabd6c 100644 --- a/routes/files.go +++ b/routes/files.go @@ -1,9 +1,12 @@ package routes import ( + "encoding/base64" "net/http" + "strings" "github.com/Sirupsen/logrus" + r "github.com/dancannon/gorethink" "github.com/zenazn/goji/web" "github.com/lavab/api/env" @@ -17,60 +20,83 @@ type FilesListResponse struct { Files *[]*models.File `json:"files,omitempty"` } -func FilesList(c web.C, w http.ResponseWriter, r *http.Request) { +func FilesList(c web.C, w http.ResponseWriter, req *http.Request) { session := c.Env["token"].(*models.Token) - query := r.URL.Query() - email := query.Get("email") - name := query.Get("name") - - if email == "" || name == "" { - utils.JSONResponse(w, 400, &FilesListResponse{ - Success: false, - Message: "No email or name in get params", - }) - return - } - - files, err := env.Files.GetInEmail(session.Owner, email, name) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to fetch files") - - utils.JSONResponse(w, 500, &FilesListResponse{ - Success: false, - Message: "Internal error (code FI/LI/01)", - }) - return + var ( + query = req.URL.Query() + sTags = query.Get("sTags") + result []*models.File + ) + + if sTags == "" { + cursor, err := r.Table("files").GetAllByIndex("owner", session.Owner).Run(env.Rethink) + if err != nil { + utils.JSONResponse(w, 500, &FilesListResponse{ + Success: false, + Message: "Internal error (code FI/LI/01)", + }) + return + } + defer cursor.Close() + if err := cursor.All(&result); err != nil { + utils.JSONResponse(w, 500, &FilesListResponse{ + Success: false, + Message: "Internal error (code FI/LI/02)", + }) + return + } + } else { + tags := strings.Split(sTags, ",") + ids := []interface{}{} + for _, tag := range tags { + ids = append(ids, []interface{}{ + session.Owner, + tag, + }) + } + cursor, err := r.Table("files").GetAllByIndex("ownerTags", ids...).Run(env.Rethink) + if err != nil { + utils.JSONResponse(w, 500, &FilesListResponse{ + Success: false, + Message: "Internal error (code FI/LI/03)", + }) + return + } + defer cursor.Close() + if err := cursor.All(&result); err != nil { + utils.JSONResponse(w, 500, &FilesListResponse{ + Success: false, + Message: "Internal error (code FI/LI/04)", + }) + return + } } utils.JSONResponse(w, 200, &FilesListResponse{ Success: true, - Files: &files, + Files: &result, }) } type FilesCreateRequest struct { - Data string `json:"data" schema:"data"` - Name string `json:"name" schema:"name"` - Encoding string `json:"encoding" schema:"encoding"` - VersionMajor int `json:"version_major" schema:"version_major"` - VersionMinor int `json:"version_minor" schema:"version_minor"` - PGPFingerprints []string `json:"pgp_fingerprints" schema:"pgp_fingerprints"` + Name string `json:"name" schema:"name"` + Meta interface{} `json:"meta" schema:"meta"` + Body string `json:"body" schema:"body"` + Tags []string `json:"tags" schema:"tags"` } type FilesCreateResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - File *models.File `json:"file,omitempty"` + Success bool `json:"success"` + Message string `json:"message"` + ID *string `json:"file,omitempty"` } // FilesCreate creates a new file -func FilesCreate(c web.C, w http.ResponseWriter, r *http.Request) { +func FilesCreate(c web.C, w http.ResponseWriter, req *http.Request) { // Decode the request var input FilesCreateRequest - err := utils.ParseRequest(r, &input) + err := utils.ParseRequest(req, &input) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), @@ -86,26 +112,22 @@ func FilesCreate(c web.C, w http.ResponseWriter, r *http.Request) { // Fetch the current session from the middleware session := c.Env["token"].(*models.Token) - // Ensure that the input data isn't empty - if input.Data == "" || input.Name == "" || input.Encoding == "" { + // Decode the body + body, err := base64.StdEncoding.DecodeString(input.Body) + if err != nil { utils.JSONResponse(w, 400, &FilesCreateResponse{ Success: false, - Message: "Invalid request", + Message: "Invalid input format, " + err.Error(), }) return } // Create a new file struct file := &models.File{ - Encrypted: models.Encrypted{ - Encoding: input.Encoding, - Data: input.Data, - Schema: "file", - VersionMajor: input.VersionMajor, - VersionMinor: input.VersionMinor, - PGPFingerprints: input.PGPFingerprints, - }, Resource: models.MakeResource(session.Owner, input.Name), + Meta: input.Meta, + Body: body, + Tags: input.Tags, } // Insert the file into the database @@ -124,7 +146,7 @@ func FilesCreate(c web.C, w http.ResponseWriter, r *http.Request) { utils.JSONResponse(w, 201, &FilesCreateResponse{ Success: true, Message: "A new file was successfully created", - File: file, + ID: &file.ID, }) } @@ -136,7 +158,7 @@ type FilesGetResponse struct { } // FilesGet gets the requested file from the database -func FilesGet(c web.C, w http.ResponseWriter, r *http.Request) { +func FilesGet(c web.C, w http.ResponseWriter, req *http.Request) { // Get the file from the database file, err := env.Files.GetFile(c.URLParams["id"]) if err != nil { @@ -168,12 +190,10 @@ func FilesGet(c web.C, w http.ResponseWriter, r *http.Request) { // FilesUpdateRequest is the payload passed to PUT /files/:id type FilesUpdateRequest struct { - Data string `json:"data" schema:"data"` - Name string `json:"name" schema:"name"` - Encoding string `json:"encoding" schema:"encoding"` - VersionMajor *int `json:"version_major" schema:"version_major"` - VersionMinor *int `json:"version_minor" schema:"version_minor"` - PGPFingerprints []string `json:"pgp_fingerprints" schema:"pgp_fingerprints"` + Name *string `json:"name" schema:"name"` + Meta interface{} `json:"meta" schema:"meta"` + Body []byte `json:"body" schema:"body"` + Tags []string `json:"tags" schema:"tags"` } // FilesUpdateResponse contains the result of the FilesUpdate request. @@ -184,10 +204,10 @@ type FilesUpdateResponse struct { } // FilesUpdate updates an existing file in the database -func FilesUpdate(c web.C, w http.ResponseWriter, r *http.Request) { +func FilesUpdate(c web.C, w http.ResponseWriter, req *http.Request) { // Decode the request var input FilesUpdateRequest - err := utils.ParseRequest(r, &input) + err := utils.ParseRequest(req, &input) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err.Error(), @@ -222,28 +242,20 @@ func FilesUpdate(c web.C, w http.ResponseWriter, r *http.Request) { return } - if input.Data != "" { - file.Data = input.Data - } - - if input.Name != "" { - file.Name = input.Name - } - - if input.Encoding != "" { - file.Encoding = input.Encoding + if input.Name != nil { + file.Name = *input.Name } - if input.VersionMajor != nil { - file.VersionMajor = *input.VersionMajor + if input.Meta != nil { + file.Meta = input.Meta } - if input.VersionMinor != nil { - file.VersionMinor = *input.VersionMinor + if input.Body != nil { + file.Body = input.Body } - if input.PGPFingerprints != nil { - file.PGPFingerprints = input.PGPFingerprints + if input.Tags != nil { + file.Tags = input.Tags } // Perform the update @@ -275,7 +287,7 @@ type FilesDeleteResponse struct { } // FilesDelete removes a file from the database -func FilesDelete(c web.C, w http.ResponseWriter, r *http.Request) { +func FilesDelete(c web.C, w http.ResponseWriter, req *http.Request) { // Get the file from the database file, err := env.Files.GetFile(c.URLParams["id"]) if err != nil { diff --git a/setup/setup.go b/setup/setup.go index 314273a..07b6f5f 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -138,10 +138,11 @@ func PrepareMux(flags *env.Flags) *web.Mux { // Set up the database rethinkOpts := gorethink.ConnectOpts{ - Address: flags.RethinkDBAddress, - AuthKey: flags.RethinkDBKey, - MaxIdle: 10, - Timeout: time.Second * 10, + Address: flags.RethinkDBAddress, + AuthKey: flags.RethinkDBKey, + Database: flags.RethinkDBDatabase, + MaxIdle: 10, + Timeout: time.Second * 10, } err = db.Setup(rethinkOpts) if err != nil { From 1a12f87ebfd1f0c4fb7f0f12b724d3d5f81f3af2 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sun, 14 Jun 2015 14:08:44 +0200 Subject: [PATCH 02/27] Recoverer didn't call the next function in the chain oh shit --- setup/recoverer.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup/recoverer.go b/setup/recoverer.go index 8955bfa..ecdae72 100644 --- a/setup/recoverer.go +++ b/setup/recoverer.go @@ -37,5 +37,7 @@ func recoverer(c *web.C, h http.Handler) http.Handler { "request_id": id, }) }() + + h.ServeHTTP(w, r) }) } From 3b6ae228b6fa5a68260f1fa9103f8f5aca0b1e98 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sun, 14 Jun 2015 15:17:56 +0200 Subject: [PATCH 03/27] Email.Secure property --- models/email.go | 2 ++ routes/emails.go | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/models/email.go b/models/email.go index eccc7dd..4ad468b 100644 --- a/models/email.go +++ b/models/email.go @@ -41,4 +41,6 @@ type Email struct { // received or (queued|processed) Status string `json:"status" gorethink:"status"` + + Secure bool `json:"secure" gorethink:"secure"` } diff --git a/routes/emails.go b/routes/emails.go index 89ca0eb..f743e69 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -393,6 +393,12 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { idHash := sha256.Sum256([]byte(resource.ID)) messageID := hex.EncodeToString(idHash[:]) + "@" + env.Config.EmailDomain + // Determine if email is secure + secure := true + if input.Kind == "raw" { + secure = false + } + // Create a new email struct email := &models.Email{ Resource: resource, @@ -415,6 +421,7 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { ReplyTo: input.ReplyTo, Status: "queued", + Secure: secure, } // Insert the email into the database From 25255cd780012b66c95d31d9702d3af18d20b453 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Mon, 15 Jun 2015 17:41:38 +0200 Subject: [PATCH 04/27] Files typo --- routes/files.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/files.go b/routes/files.go index feabd6c..ae61679 100644 --- a/routes/files.go +++ b/routes/files.go @@ -25,7 +25,7 @@ func FilesList(c web.C, w http.ResponseWriter, req *http.Request) { var ( query = req.URL.Query() - sTags = query.Get("sTags") + sTags = query.Get("tags") result []*models.File ) From e8d8cdf01f9673db5f1341259382890f2e9fa72a Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Mon, 15 Jun 2015 21:16:31 +0200 Subject: [PATCH 05/27] threadAndDate index function --- db/setup.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/db/setup.go b/db/setup.go index 0e2798f..bd29d66 100644 --- a/db/setup.go +++ b/db/setup.go @@ -115,6 +115,12 @@ func Setup(opts r.ConnectOpts) error { row.Field("owner"), } }).Exec(ss) + r.DB(d).Table("threads").IndexCreateFunc("threadAndDate", func(row r.Term) interface{} { + return []interface{}{ + row.Field("thread"), + row.Field("date_created"), + } + }).Exec(ss) r.DB(d).TableCreate("tokens").Exec(ss) r.DB(d).Table("tokens").IndexCreate("name").Exec(ss) From c4e01c1cf86dfbbbded3ccba6de7b969390983cf Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Tue, 16 Jun 2015 00:18:48 +0200 Subject: [PATCH 06/27] File metadata transformed into a custom struct based on map[string]interface{} --- models/file.go | 18 +++++++++++++++--- routes/files.go | 16 ++++++++-------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/models/file.go b/models/file.go index 953d302..daa6247 100644 --- a/models/file.go +++ b/models/file.go @@ -4,7 +4,19 @@ package models type File struct { Resource - Meta interface{} `json:"meta" gorethink:"meta"` - Body []byte `json:"body" gorethink:"body"` - Tags []string `json:"tags" gorethink:"tags"` + Meta FileMeta `json:"meta" gorethink:"meta"` + Body []byte `json:"body" gorethink:"body"` + Tags []string `json:"tags" gorethink:"tags"` +} + +type FileMeta map[string]interface{} + +func (f FileMeta) ContentType() string { + if a, ok := f["content_type"]; ok { + if b, ok := a.(string); ok { + return b + } + } + + return "" } diff --git a/routes/files.go b/routes/files.go index ae61679..de3fda3 100644 --- a/routes/files.go +++ b/routes/files.go @@ -80,10 +80,10 @@ func FilesList(c web.C, w http.ResponseWriter, req *http.Request) { } type FilesCreateRequest struct { - Name string `json:"name" schema:"name"` - Meta interface{} `json:"meta" schema:"meta"` - Body string `json:"body" schema:"body"` - Tags []string `json:"tags" schema:"tags"` + Name string `json:"name" schema:"name"` + Meta map[string]interface{} `json:"meta" schema:"meta"` + Body string `json:"body" schema:"body"` + Tags []string `json:"tags" schema:"tags"` } type FilesCreateResponse struct { @@ -190,10 +190,10 @@ func FilesGet(c web.C, w http.ResponseWriter, req *http.Request) { // FilesUpdateRequest is the payload passed to PUT /files/:id type FilesUpdateRequest struct { - Name *string `json:"name" schema:"name"` - Meta interface{} `json:"meta" schema:"meta"` - Body []byte `json:"body" schema:"body"` - Tags []string `json:"tags" schema:"tags"` + Name *string `json:"name" schema:"name"` + Meta map[string]interface{} `json:"meta" schema:"meta"` + Body []byte `json:"body" schema:"body"` + Tags []string `json:"tags" schema:"tags"` } // FilesUpdateResponse contains the result of the FilesUpdate request. From 23797654015ce7bb5bfb429660baaf07bf51b70f Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Wed, 17 Jun 2015 18:34:11 +0200 Subject: [PATCH 07/27] Fixed threadAndDate welp --- db/setup.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/db/setup.go b/db/setup.go index bd29d66..2e74685 100644 --- a/db/setup.go +++ b/db/setup.go @@ -66,6 +66,12 @@ func Setup(opts r.ConnectOpts) error { row.Field("status"), } }).Exec(ss) + r.DB(d).Table("emails").IndexCreateFunc("threadAndDate", func(row r.Term) interface{} { + return []interface{}{ + row.Field("thread"), + row.Field("date_created"), + } + }).Exec(ss) r.DB(d).TableCreate("files").Exec(ss) r.DB(d).Table("files").IndexCreate("owner").Exec(ss) @@ -115,12 +121,6 @@ func Setup(opts r.ConnectOpts) error { row.Field("owner"), } }).Exec(ss) - r.DB(d).Table("threads").IndexCreateFunc("threadAndDate", func(row r.Term) interface{} { - return []interface{}{ - row.Field("thread"), - row.Field("date_created"), - } - }).Exec(ss) r.DB(d).TableCreate("tokens").Exec(ss) r.DB(d).Table("tokens").IndexCreate("name").Exec(ss) From a6a1c95b6b72a0c81f4f54c5d789fae43503aad9 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Thu, 18 Jun 2015 21:56:34 +0200 Subject: [PATCH 08/27] Removed base64 from the files endpoint --- routes/files.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/routes/files.go b/routes/files.go index de3fda3..e2d34e0 100644 --- a/routes/files.go +++ b/routes/files.go @@ -1,7 +1,6 @@ package routes import ( - "encoding/base64" "net/http" "strings" @@ -112,16 +111,6 @@ func FilesCreate(c web.C, w http.ResponseWriter, req *http.Request) { // Fetch the current session from the middleware session := c.Env["token"].(*models.Token) - // Decode the body - body, err := base64.StdEncoding.DecodeString(input.Body) - if err != nil { - utils.JSONResponse(w, 400, &FilesCreateResponse{ - Success: false, - Message: "Invalid input format, " + err.Error(), - }) - return - } - // Create a new file struct file := &models.File{ Resource: models.MakeResource(session.Owner, input.Name), From 9552a674ed08844a83189cc8f49e3e23126b835e Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Thu, 18 Jun 2015 22:12:11 +0200 Subject: [PATCH 09/27] Switched to native []byte decoding --- routes/files.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/files.go b/routes/files.go index e2d34e0..715ed6f 100644 --- a/routes/files.go +++ b/routes/files.go @@ -81,7 +81,7 @@ func FilesList(c web.C, w http.ResponseWriter, req *http.Request) { type FilesCreateRequest struct { Name string `json:"name" schema:"name"` Meta map[string]interface{} `json:"meta" schema:"meta"` - Body string `json:"body" schema:"body"` + Body []byte `json:"body" schema:"body"` Tags []string `json:"tags" schema:"tags"` } @@ -115,7 +115,7 @@ func FilesCreate(c web.C, w http.ResponseWriter, req *http.Request) { file := &models.File{ Resource: models.MakeResource(session.Owner, input.Name), Meta: input.Meta, - Body: body, + Body: input.Body, Tags: input.Tags, } From aee045f3024db255546d7b1abdcd752d66b2e106 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 10:33:52 +0200 Subject: [PATCH 10/27] oops --- db/setup.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/setup.go b/db/setup.go index 2e74685..f5ab048 100644 --- a/db/setup.go +++ b/db/setup.go @@ -86,7 +86,7 @@ func Setup(opts r.ConnectOpts) error { tag, } }) - }, r.IndexCreateOpts{Multi: true}) + }, r.IndexCreateOpts{Multi: true}).Exec(ss) r.DB(d).TableCreate("keys").Exec(ss) r.DB(d).Table("keys").IndexCreate("owner").Exec(ss) r.DB(d).Table("keys").IndexCreate("date_created").Exec(ss) From 49d19a6326d1a44e9510095d0baecc1262f8e8a1 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 13:29:47 +0200 Subject: [PATCH 11/27] Removed old 2FA code, introduced new error handling --- README.md | 2 - env/config.go | 3 - env/env.go | 3 - factor/authenticator.go | 38 --- factor/method.go | 7 - factor/yubicloud.go | 58 ---- main.go | 6 - models/account.go | 24 -- routes/accounts.go | 549 +++++++++--------------------- routes/accounts_test.go_ | 705 --------------------------------------- routes/tokens.go | 50 +-- setup/setup.go | 16 - utils/errors.go | 120 +++++++ utils/requests.go | 23 ++ 14 files changed, 302 insertions(+), 1302 deletions(-) delete mode 100644 factor/authenticator.go delete mode 100644 factor/method.go delete mode 100644 factor/yubicloud.go delete mode 100644 routes/accounts_test.go_ create mode 100644 utils/errors.go diff --git a/README.md b/README.md index 0c24654..bc2f197 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,6 @@ Usage of api: -slack_level="warning": minimal level required to have messages sent to slack -slack_url="": URL of the Slack Incoming webhook -slack_username="API": username of the Slack bot - -yubicloud_id="": YubiCloud API id - -yubicloud_key="": YubiCloud API key ``` diff --git a/env/config.go b/env/config.go index 038b45d..4028584 100644 --- a/env/config.go +++ b/env/config.go @@ -21,9 +21,6 @@ type Flags struct { LookupdAddress string NSQdAddress string - YubiCloudID string - YubiCloudKey string - SlackURL string SlackLevels string SlackChannel string diff --git a/env/env.go b/env/env.go index ab67e39..15f4bbd 100644 --- a/env/env.go +++ b/env/env.go @@ -9,7 +9,6 @@ import ( "github.com/lavab/api/cache" "github.com/lavab/api/db" - "github.com/lavab/api/factor" ) var ( @@ -41,8 +40,6 @@ var ( Files *db.FilesTable // Threads is the global instance of ThreadsTable Threads *db.ThreadsTable - // Factors contains all currently registered factors - Factors map[string]factor.Factor // Producer is the nsq producer used to send messages to other components of the system Producer *nsq.Producer // PasswordBF is the bloom filter used for leaked password matching diff --git a/factor/authenticator.go b/factor/authenticator.go deleted file mode 100644 index ba747b3..0000000 --- a/factor/authenticator.go +++ /dev/null @@ -1,38 +0,0 @@ -package factor - -import ( - "github.com/gokyle/hotp" -) - -type Authenticator struct { - length int -} - -func NewAuthenticator(length int) *Authenticator { - return &Authenticator{ - length: length, - } -} - -func (a *Authenticator) Type() string { - return "authenticator" -} - -func (a *Authenticator) Request(data string) (string, error) { - otp, err := hotp.GenerateHOTP(a.length, false) - if err != nil { - return "", err - } - - return otp.URL(data), nil -} - -func (a *Authenticator) Verify(data []string, input string) (bool, error) { - // obviously broken - hotp, err := hotp.Unmarshal([]byte(data[0])) - if err != nil { - return false, err - } - - return hotp.Check(input), nil -} diff --git a/factor/method.go b/factor/method.go deleted file mode 100644 index df869ed..0000000 --- a/factor/method.go +++ /dev/null @@ -1,7 +0,0 @@ -package factor - -type Factor interface { - Type() string - Request(data string) (string, error) - Verify(data []string, input string) (bool, error) -} diff --git a/factor/yubicloud.go b/factor/yubicloud.go deleted file mode 100644 index 8bd0927..0000000 --- a/factor/yubicloud.go +++ /dev/null @@ -1,58 +0,0 @@ -package factor - -import "github.com/GeertJohan/yubigo" - -// YubiCloud is an implementation of Factor to authenticate with YubiCloud -type YubiCloud struct { - client *yubigo.YubiAuth -} - -// NewYubiCloud set ups a new Factor that supports authing using YubiCloud -func NewYubiCloud(id string, key string) (*YubiCloud, error) { - client, err := yubigo.NewYubiAuth(id, key) - if err != nil { - return nil, err - } - - return &YubiCloud{ - client: client, - }, nil -} - -// Type returns factor's type -func (y *YubiCloud) Type() string { - return "yubicloud" -} - -// Request does nothing in this driver -func (y *YubiCloud) Request(data string) (string, error) { - return "", nil -} - -// Verify checks if the token is valid -func (y *YubiCloud) Verify(data []string, input string) (bool, error) { - publicKey := input[:12] - - found := false - for _, prefix := range data { - if publicKey == prefix { - found = true - break - } - } - - if !found { - return false, nil - } - - _, ok, err := y.client.Verify(input) - if err != nil { - return false, err - } - - if !ok { - return false, nil - } - - return true, nil -} diff --git a/main.go b/main.go index 3593d79..50638c0 100644 --- a/main.go +++ b/main.go @@ -69,9 +69,6 @@ var ( } return address + ":4160" }(), "Address of the lookupd server") - // YubiCloud params - yubiCloudID = flag.String("yubicloud_id", "", "YubiCloud API id") - yubiCloudKey = flag.String("yubicloud_key", "", "YubiCloud API key") // etcd etcdAddress = flag.String("etcd_address", "", "etcd peer addresses split by commas") etcdCAFile = flag.String("etcd_ca_file", "", "etcd path to server cert's ca") @@ -116,9 +113,6 @@ func main() { NSQdAddress: *nsqdAddress, LookupdAddress: *lookupdAddress, - YubiCloudID: *yubiCloudID, - YubiCloudKey: *yubiCloudKey, - SlackURL: *slackURL, SlackLevels: *slackLevels, SlackChannel: *slackChannel, diff --git a/models/account.go b/models/account.go index 8b11d61..13df04f 100644 --- a/models/account.go +++ b/models/account.go @@ -3,7 +3,6 @@ package models import ( "github.com/gyepisam/mcf" _ "github.com/gyepisam/mcf/scrypt" // Required to have mcf hash the password into scrypt - "github.com/lavab/api/factor" "golang.org/x/crypto/openpgp" ) @@ -37,9 +36,6 @@ type Account struct { AltEmail string `json:"alt_email" gorethink:"alt_email"` - FactorType string `json:"-" gorethink:"factor_type"` - FactorValue []string `json:"-" gorethink:"factor_value"` - Status string `json:"status" gorethink:"status"` Key *openpgp.Entity `json:"-" gorethink:"-"` @@ -86,26 +82,6 @@ func (a *Account) VerifyPassword(password string) (bool, bool, error) { return true, false, nil } -// Verify2FA verifies the 2FA token with the account settings. -// Returns verified, challenge, error -func (a *Account) Verify2FA(factor factor.Factor, token string) (bool, string, error) { - if token == "" { - req, err := factor.Request(a.ID) - if err != nil { - return false, "", err - } - - return false, req, nil - } - - ok, err := factor.Verify(a.FactorValue, token) - if err != nil { - return false, "", err - } - - return ok, "", nil -} - // SettingsData TODO type SettingsData struct { } diff --git a/routes/accounts.go b/routes/accounts.go index c1ff734..f0066f0 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -5,7 +5,6 @@ import ( "net/http" "time" - "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" "github.com/lavab/api/env" @@ -13,18 +12,11 @@ import ( "github.com/lavab/api/utils" ) -// AccountsListResponse contains the result of the AccountsList request. -type AccountsListResponse struct { - Success bool `json:"success"` - Message string `json:"message"` -} - // AccountsList returns a list of accounts visible to an user func AccountsList(w http.ResponseWriter, r *http.Request) { - utils.JSONResponse(w, 501, &AccountsListResponse{ - Success: false, - Message: "Sorry, not implemented yet", - }) + utils.JSONResponse(w, 501, utils.NewError( + utils.AccountsListUnknown, "Account not implemented yet", false, + )) } // AccountsCreateRequest contains the input for the AccountsCreate endpoint. @@ -48,14 +40,9 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { var input AccountsCreateRequest err := utils.ParseRequest(r, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.AccountsCreateInvalidInput, err, false, + )) return } @@ -77,10 +64,9 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // "unknown" requests are empty and invalid if requestType == "unknown" { - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Invalid request", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.AccountsCreateUnknownStep, "Unable to recognize the step", false, + )) return } @@ -90,43 +76,33 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // Validate the username if len(input.Username) < 3 || len(utils.RemoveDots(input.Username)) < 3 || len(input.Username) > 32 { - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Invalid username - it has to be at least 3 and at max 32 characters long", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.AccountsCreateInvalidLength, "Invalid username - it has to be at least 3 and at max 32 characters long", false, + )) return } // Ensure that the username is not used in address table if used, err := env.Addresses.GetAddress(utils.RemoveDots(input.Username)); err == nil || used != nil { - utils.JSONResponse(w, 409, &AccountsCreateResponse{ - Success: false, - Message: "Username already used", - }) + utils.JSONResponse(w, 409, utils.NewError( + utils.AccountsCreateUsernameTaken, "Username already taken", false, + )) return } // Then check it in the accounts table if ok, err := env.Accounts.IsUsernameUsed(utils.RemoveDots(input.Username)); ok || err != nil { - utils.JSONResponse(w, 409, &AccountsCreateResponse{ - Success: false, - Message: "Username already used", - }) + utils.JSONResponse(w, 409, utils.NewError( + utils.AccountsCreateUsernameTaken, "Username already taken", false, + )) return } // Also check that the email is unique if used, err := env.Accounts.IsEmailUsed(input.AltEmail); err != nil || used { - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to lookup registered accounts for emails") - } - - utils.JSONResponse(w, 409, &AccountsCreateResponse{ - Success: false, - Message: "Email already used", - }) + utils.JSONResponse(w, 409, utils.NewError( + utils.AccountsCreateEmailUsed, "Email already used", false, + )) return } @@ -141,14 +117,9 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // Try to save it in the database if err := env.Accounts.Insert(account); err != nil { - utils.JSONResponse(w, 500, &AccountsCreateResponse{ - Success: false, - Message: "Internal server error - AC/CR/02", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Could not insert an user into the database") + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsCreateUnableToInsertAccount, err, true, + )) return } @@ -170,70 +141,50 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // Fetch the user from database account, err := env.Accounts.FindAccountByName(input.Username) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "username": input.Username, - }).Warn("User not found in the database") - - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Invalid username", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateUserNotFound, err, false, + )) return } // Fetch the token from the database token, err := env.Tokens.GetToken(input.InviteCode) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to fetch a registration token from the database") - - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Invalid invitation code", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateInvalidToken, err, false, + )) return } // Ensure that the invite code was given to this particular user. if token.Owner != account.ID { - env.Log.WithFields(logrus.Fields{ - "user_id": account.ID, - "owner": token.Owner, - }).Warn("Not owned invitation code used by an user") - - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Invalid invitation code", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateInvalidTokenOwner, "You don't own this invitation code", false, + )) return } // Ensure that the token's type is valid if token.Type != "verify" { - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Invalid invitation code", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateInvalidTokenType, "Invalid token type - "+token.Type, false, + )) return } // Check if it's expired if token.Expired() { - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Expired invitation code", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateExpiredToken, "This token has expired", false, + )) return } // Ensure that the account is "registered" if account.Status != "registered" { - utils.JSONResponse(w, 403, &AccountsCreateResponse{ - Success: true, - Message: "This account was already configured", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateAlreadyConfigured, "This account is already configured", false, + )) return } @@ -253,70 +204,50 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // Fetch the user from database account, err := env.Accounts.FindAccountByName(input.Username) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "username": input.Username, - }).Warn("User not found in the database") - - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Invalid username", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateUserNotFound, err, false, + )) return } // Fetch the token from the database token, err := env.Tokens.GetToken(input.InviteCode) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to fetch a registration token from the database") - - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Invalid invitation code", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateInvalidToken, err, false, + )) return } // Ensure that the invite code was given to this particular user. if token.Owner != account.ID { - env.Log.WithFields(logrus.Fields{ - "user_id": account.ID, - "owner": token.Owner, - }).Warn("Not owned invitation code used by an user") - - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Invalid invitation code", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateInvalidTokenOwner, "You don't own this invitation code", false, + )) return } // Ensure that the token's type is valid if token.Type != "verify" { - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Invalid invitation code", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateInvalidTokenType, "Invalid token type - "+token.Type, false, + )) return } // Check if it's expired if token.Expired() { - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Expired invitation code", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateExpiredToken, "This token has expired", false, + )) return } // Ensure that the account is "registered" if account.Status != "registered" { - utils.JSONResponse(w, 403, &AccountsCreateResponse{ - Success: true, - Message: "This account was already configured", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateAlreadyConfigured, "This account is already configured", false, + )) return } @@ -324,29 +255,23 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // Ensure that user has chosen a secure password (check against 10k most used) if env.PasswordBF.TestString(input.Password) { - utils.JSONResponse(w, 403, &AccountsCreateResponse{ - Success: false, - Message: "Weak password", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsCreateWeakPassword, "Weak password", false, + )) return } // We can't really make more checks on the password, user could as well send us a hash // of a simple password, but we assume that no developer is that stupid (actually, - // considering how many people upload their private keys and AWS credentials, I'm starting - // to doubt the competence of some so-called "web deyvelopayrs") + // considering how many people upload their private keys and AWS credentials to GitHub, + // I'm starting to doubt the competence of some so-called "web deyvelopayrs") // Set the password err = account.SetPassword(input.Password) if err != nil { - utils.JSONResponse(w, 500, &AccountsCreateResponse{ - Success: false, - Message: "Internal server error - AC/CR/01", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to hash the password") + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsCreateUnableToHash, err, true, + )) return } @@ -380,14 +305,9 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { }, }) if err != nil { - utils.JSONResponse(w, 500, &AccountsCreateResponse{ - Success: false, - Message: "Internal server error - AC/CR/03", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Could not insert labels into the database") + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsCreateUnableToPrepareLabels, err, true, + )) return } @@ -401,39 +321,25 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { }, }) if err != nil { - utils.JSONResponse(w, 500, &AccountsCreateResponse{ - Success: false, - Message: "Unable to create a new address mapping", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Could not insert an address mapping into db") + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsCreateUnableToCreateAddress, err, true, + )) return } // Update the account err = env.Accounts.UpdateID(account.ID, account) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": account.ID, - }).Error("Unable to update an account") - - utils.JSONResponse(w, 500, &AccountsCreateResponse{ - Success: false, - Message: "Unable to update the account", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsCreateUnableToUpdateAccount, err, true, + )) return } // Remove the token and return a response err = env.Tokens.DeleteID(input.InviteCode) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": input.InviteCode, - }).Error("Could not remove the token from database") + env.Raven.CaptureError(err, nil) } utils.JSONResponse(w, 200, &AccountsCreateResponse{ @@ -459,10 +365,9 @@ func AccountsGet(c web.C, w http.ResponseWriter, r *http.Request) { // Right now we only support "me" as the ID if id != "me" { - utils.JSONResponse(w, 501, &AccountsGetResponse{ - Success: false, - Message: `Only the "me" user is implemented`, - }) + utils.JSONResponse(w, 501, utils.NewError( + utils.AccountsGetOnlyMe, "You can only get your own account's details", false, + )) return } @@ -472,10 +377,9 @@ func AccountsGet(c web.C, w http.ResponseWriter, r *http.Request) { // Fetch the user object from the database user, err := env.Accounts.GetAccount(session.Owner) if err != nil { - utils.JSONResponse(w, 500, &AccountsDeleteResponse{ - Success: false, - Message: "Unable to resolve the account", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsGetUnableToGet, err, true, + )) return } @@ -491,8 +395,6 @@ type AccountsUpdateRequest struct { AltEmail string `json:"alt_email" schema:"alt_email"` CurrentPassword string `json:"current_password" schema:"current_password"` NewPassword string `json:"new_password" schema:"new_password"` - FactorType string `json:"factor_type" schema:"factor_type"` - FactorValue []string `json:"factor_value" schema:"factor_value"` Token string `json:"token" schema:"token"` Settings interface{} `json:"settings" schema:"settings"` PublicKey string `json:"public_key" schema:"public_key"` @@ -500,11 +402,9 @@ type AccountsUpdateRequest struct { // AccountsUpdateResponse contains the result of the AccountsUpdate request. type AccountsUpdateResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Account *models.Account `json:"account,omitempty"` - FactorType string `json:"factor_type,omitempty"` - FactorChallenge string `json:"factor_challenge,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Account *models.Account `json:"account,omitempty"` } // AccountsUpdate allows changing the account's information (password etc.) @@ -513,14 +413,9 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { var input AccountsUpdateRequest err := utils.ParseRequest(r, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 400, &AccountsUpdateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.AccountsUpdateOnlyMe, err, false, + )) return } @@ -529,10 +424,9 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { // Right now we only support "me" as the ID if id != "me" { - utils.JSONResponse(w, 501, &AccountsUpdateResponse{ - Success: false, - Message: `Only the "me" user is implemented`, - }) + utils.JSONResponse(w, 501, utils.NewError( + utils.AccountsUpdateOnlyMe, "You can only update your own account", false, + )) return } @@ -542,84 +436,34 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { // Fetch the user object from the database user, err := env.Accounts.GetAccount(session.Owner) if err != nil { - utils.JSONResponse(w, 500, &AccountsDeleteResponse{ - Success: false, - Message: "Unable to resolve the account", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsUpdateUnableToGet, err, true, + )) return } if input.NewPassword != "" { if valid, _, err := user.VerifyPassword(input.CurrentPassword); err != nil || !valid { - utils.JSONResponse(w, 403, &AccountsUpdateResponse{ - Success: false, - Message: "Invalid current password", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsUpdateInvalidCurrentPassword, err, false, + )) return } } - // Check for 2nd factor - if user.FactorType != "" { - factor, ok := env.Factors[user.FactorType] - if ok { - // Verify the 2FA - verified, challenge, err := user.Verify2FA(factor, input.Token) - if err != nil { - utils.JSONResponse(w, 500, &AccountsUpdateResponse{ - Success: false, - Message: "Internal 2FA error", - }) - - env.Log.WithFields(logrus.Fields{ - "err": err.Error(), - "factor": user.FactorType, - }).Warn("2FA authentication error") - return - } - - // Token was probably empty. Return the challenge. - if !verified && challenge != "" { - utils.JSONResponse(w, 403, &AccountsUpdateResponse{ - Success: false, - Message: "2FA token was not passed", - FactorType: user.FactorType, - FactorChallenge: challenge, - }) - return - } - - // Token was incorrect - if !verified { - utils.JSONResponse(w, 403, &AccountsUpdateResponse{ - Success: false, - Message: "Invalid token passed", - FactorType: user.FactorType, - }) - return - } - } - } - if input.NewPassword != "" && env.PasswordBF.TestString(input.NewPassword) { - utils.JSONResponse(w, 400, &AccountsUpdateResponse{ - Success: false, - Message: "Weak new password", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.AccountsUpdateWeakPassword, "Weak new password", false, + )) return } if input.NewPassword != "" { err = user.SetPassword(input.NewPassword) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to hash a password") - - utils.JSONResponse(w, 500, &AccountsUpdateResponse{ - Success: false, - Message: "Internal error (code AC/UP/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsUpdateUnableToHash, err, true, + )) return } } @@ -635,64 +479,29 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { if input.PublicKey != "" { key, err := env.Keys.FindByFingerprint(input.PublicKey) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "fingerprint": input.PublicKey, - }).Error("Unable to find a key") - - utils.JSONResponse(w, 400, &AccountsUpdateResponse{ - Success: false, - Message: "Invalid public key", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsUpdateInvalidPublicKey, err, false, + )) return } if key.Owner != user.ID { - env.Log.WithFields(logrus.Fields{ - "user_id:": user.ID, - "owner": key.Owner, - "fingerprint": input.PublicKey, - }).Error("Unable to find a key") - - utils.JSONResponse(w, 400, &AccountsUpdateResponse{ - Success: false, - Message: "Invalid public key", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsUpdateInvalidPublicKeyOwner, "You're not the owner of that public key", false, + )) return } user.PublicKey = input.PublicKey } - if input.FactorType != "" { - // Check if such factor exists - if _, exists := env.Factors[input.FactorType]; !exists { - utils.JSONResponse(w, 400, &AccountsUpdateResponse{ - Success: false, - Message: "Invalid new 2FA type", - }) - return - } - - user.FactorType = input.FactorType - } - - if input.FactorValue != nil && len(input.FactorValue) > 0 { - user.FactorValue = input.FactorValue - } - user.DateModified = time.Now() err = env.Accounts.UpdateID(session.Owner, user) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to update an account") - - utils.JSONResponse(w, 500, &AccountsUpdateResponse{ - Success: false, - Message: "Internal error (code AC/UP/02)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsUpdateUnableToUpdate, err, true, + )) return } @@ -716,10 +525,9 @@ func AccountsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Right now we only support "me" as the ID if id != "me" { - utils.JSONResponse(w, 501, &AccountsDeleteResponse{ - Success: false, - Message: `Only the "me" user is implemented`, - }) + utils.JSONResponse(w, 501, utils.NewError( + utils.AccountsDeleteOnlyMe, "You can only delete your own account", false, + )) return } @@ -729,10 +537,9 @@ func AccountsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Fetch the user object from the database user, err := env.Accounts.GetAccount(session.Owner) if err != nil { - utils.JSONResponse(w, 500, &AccountsDeleteResponse{ - Success: false, - Message: "Unable to resolve the account", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsDeleteUnableToGet, err, true, + )) return } @@ -747,29 +554,18 @@ func AccountsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Delete tokens err = env.Tokens.DeleteOwnedBy(user.ID) if err != nil { - env.Log.WithFields(logrus.Fields{ - "id": user.ID, - "error": err.Error(), - }).Error("Unable to remove account's tokens") - - utils.JSONResponse(w, 500, &AccountsDeleteResponse{ - Success: false, - Message: "Internal error (code AC/DE/05)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsDeleteUnableToDelete, err, true, + )) return } // Delete account err = env.Accounts.DeleteID(user.ID) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to delete an account") - - utils.JSONResponse(w, 500, &AccountsDeleteResponse{ - Success: false, - Message: "Internal error (code AC/DE/06)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsDeleteUnableToDelete, err, true, + )) return } @@ -792,10 +588,9 @@ func AccountsWipeData(c web.C, w http.ResponseWriter, r *http.Request) { // Right now we only support "me" as the ID if id != "me" { - utils.JSONResponse(w, 501, &AccountsWipeDataResponse{ - Success: false, - Message: `Only the "me" user is implemented`, - }) + utils.JSONResponse(w, 501, utils.NewError( + utils.AccountsWipeDataOnlyMe, "You can only delete your own account", false, + )) return } @@ -806,15 +601,9 @@ func AccountsWipeData(c web.C, w http.ResponseWriter, r *http.Request) { user, err := env.Accounts.GetTokenOwner(session) if err != nil { // The session refers to a non-existing user - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - "error": err.Error(), - }).Warn("Valid session referred to a removed account") - - utils.JSONResponse(w, 410, &AccountsWipeDataResponse{ - Success: false, - Message: "Account disabled", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsWipeDataUnableToGet, err, true, + )) return } @@ -829,15 +618,9 @@ func AccountsWipeData(c web.C, w http.ResponseWriter, r *http.Request) { // Delete tokens err = env.Tokens.DeleteOwnedBy(user.ID) if err != nil { - env.Log.WithFields(logrus.Fields{ - "id": user.ID, - "error": err.Error(), - }).Error("Unable to remove account's tokens") - - utils.JSONResponse(w, 500, &AccountsWipeDataResponse{ - Success: false, - Message: "Internal error (code AC/WD/05)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsWipeDataUnableToDelete, err, true, + )) return } @@ -858,10 +641,9 @@ func AccountsStartOnboarding(c web.C, w http.ResponseWriter, r *http.Request) { // Right now we only support "me" as the ID if id != "me" { - utils.JSONResponse(w, 501, &AccountsStartOnboardingResponse{ - Success: false, - Message: `Only the "me" user is implemented`, - }) + utils.JSONResponse(w, 501, utils.NewError( + utils.AccountsStartOnboardingOnlyMe, "You can only start onboarding for your own account", false, + )) return } @@ -871,65 +653,46 @@ func AccountsStartOnboarding(c web.C, w http.ResponseWriter, r *http.Request) { // Fetch the user object from the database account, err := env.Accounts.GetTokenOwner(session) if err != nil { - // The session refers to a non-existing user - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - "error": err.Error(), - }).Warn("Valid session referred to a removed account") - - utils.JSONResponse(w, 410, &AccountsStartOnboardingResponse{ - Success: false, - Message: "Account disabled", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsStartOnboardingUnableToGet, err, true, + )) return } x1, ok := account.Settings.(map[string]interface{}) if !ok { - utils.JSONResponse(w, 403, &AccountsStartOnboardingResponse{ - Success: false, - Message: "Account misconfigured #1", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsStartOnboardingMisconfigured, "Account settings are not an array", true, + )) return } x2, ok := x1["firstName"] if !ok { - utils.JSONResponse(w, 403, &AccountsStartOnboardingResponse{ - Success: false, - Message: "Account misconfigured #2", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsStartOnboardingMisconfigured, "Account settings do not have a first name property", true, + )) return } x3, ok := x2.(string) if !ok { - utils.JSONResponse(w, 403, &AccountsStartOnboardingResponse{ - Success: false, - Message: "Account misconfigured #3", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.AccountsStartOnboardingMisconfigured, "First name in account settings is not a string", true, + )) return } - data, err := json.Marshal(map[string]interface{}{ - "type": "onboarding", - "email": account.Name + "@lavaboom.com", - // polish roulette + data, _ := json.Marshal(map[string]interface{}{ + "type": "onboarding", + "email": account.Name + "@lavaboom.com", "first_name": x3, }) - if !ok { - utils.JSONResponse(w, 500, &AccountsStartOnboardingResponse{ - Success: false, - Message: "Unable to encode a message", - }) - return - } if err := env.Producer.Publish("hub", data); err != nil { - utils.JSONResponse(w, 500, &AccountsCreateResponse{ - Success: false, - Message: "Unable to initialize onboarding emails", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AccountsStartOnboardingUnableToInit, err, true, + )) return } diff --git a/routes/accounts_test.go_ b/routes/accounts_test.go_ deleted file mode 100644 index f104af7..0000000 --- a/routes/accounts_test.go_ +++ /dev/null @@ -1,705 +0,0 @@ -package routes_test - -import ( - "crypto/sha256" - "encoding/hex" - "testing" - "time" - - "github.com/dchest/uniuri" - "github.com/franela/goreq" - . "github.com/smartystreets/goconvey/convey" - - "github.com/lavab/api/env" - "github.com/lavab/api/models" - "github.com/lavab/api/routes" -) - -func TestAccountsRoute(t *testing.T) { - Convey("When creating a new account", t, func() { - Convey("Misformatted body should fail", func() { - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: "!@#!@#", - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid input format") - }) - - Convey("Invalid set of data should fail", func() { - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid request") - }) - - Convey("Account creation should succeed", func() { - var ( - username = uniuri.New() - password = uniuri.New() - email = uniuri.New() + "@potato.org" - ) - - passwordHash := sha256.Sum256([]byte(password)) - accountPassword := hex.EncodeToString(passwordHash[:]) - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + username + `", - "alt_email": "` + email + `" -}`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Your account has been added to the beta queue") - So(response.Success, ShouldBeTrue) - So(response.Account.ID, ShouldNotBeEmpty) - - account := response.Account - - Convey("Duplicating the username should fail", func() { - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + username + `", - "alt_email": "` + email + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Username already used") - }) - - Convey("Duplicating the email should fail", func() { - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + uniuri.New() + `", - "alt_email": "` + email + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Email already used") - }) - - Convey("Verification with an invalid username should fail", func() { - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + uniuri.New() + `", - "invite_code": "` + uniuri.New() + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid username") - So(response.Success, ShouldBeFalse) - }) - - Convey("Verification with an invalid code should fail", func() { - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + account.Name + `", - "invite_code": "` + uniuri.New() + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid invitation code") - So(response.Success, ShouldBeFalse) - }) - - Convey("Verification with a not owned code should fail", func() { - verificationToken := models.Token{ - Resource: models.MakeResource("top kek", "test verification token"), - Type: "verify", - } - verificationToken.ExpireSoon() - - err := env.Tokens.Insert(verificationToken) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + account.Name + `", - "invite_code": "` + verificationToken.ID + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid invitation code") - So(response.Success, ShouldBeFalse) - }) - - Convey("Verification with a token that is not a verification token should fail", func() { - verificationToken := models.Token{ - Resource: models.MakeResource(account.ID, "test verification token"), - Type: "notverify", - } - verificationToken.ExpireSoon() - - err := env.Tokens.Insert(verificationToken) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + account.Name + `", - "invite_code": "` + verificationToken.ID + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid invitation code") - So(response.Success, ShouldBeFalse) - }) - - Convey("Verification with an expired invitation code should fail", func() { - verificationToken := models.Token{ - Resource: models.MakeResource(account.ID, "test verification token"), - Type: "verify", - } - verificationToken.ExpiryDate = time.Now().Truncate(time.Hour * 24) - - err := env.Tokens.Insert(verificationToken) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + account.Name + `", - "invite_code": "` + verificationToken.ID + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Expired invitation code") - So(response.Success, ShouldBeFalse) - }) - - Convey("Verification of the account should succeed", func() { - verificationToken := models.Token{ - Resource: models.MakeResource(account.ID, "test verification token"), - Type: "verify", - } - verificationToken.ExpireSoon() - - err := env.Tokens.Insert(verificationToken) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + username + `", - "invite_code": "` + verificationToken.ID + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Valid token was provided") - So(response.Success, ShouldBeTrue) - - Convey("Setup with a weak password should fail", func() { - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + account.Name + `", - "invite_code": "` + verificationToken.ID + `", - "password": "d0cfc2e5319b82cdc71a33873e826c93d7ee11363f8ac91c4fa3a2cfcd2286e5" - }`, - }.Do() - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Weak password") - So(response.Success, ShouldBeFalse) - }) - - Convey("Setup with an invalid username should fail", func() { - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + uniuri.New() + `", - "invite_code": "` + verificationToken.ID + `", - "password": "` + accountPassword + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid username") - So(response.Success, ShouldBeFalse) - }) - - Convey("Setup with an invalid code should fail", func() { - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + account.Name + `", - "invite_code": "` + uniuri.New() + `", - "password": "` + accountPassword + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid invitation code") - So(response.Success, ShouldBeFalse) - }) - - Convey("Setup with a code that user does not own should fail", func() { - verificationToken := models.Token{ - Resource: models.MakeResource(uniuri.New(), "test verification token"), - Type: "verify", - } - verificationToken.ExpireSoon() - - err := env.Tokens.Insert(verificationToken) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + account.Name + `", - "invite_code": "` + verificationToken.ID + `", - "password": "` + accountPassword + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid invitation code") - So(response.Success, ShouldBeFalse) - }) - - Convey("Setup with a token that is not a verification token should fail", func() { - verificationToken := models.Token{ - Resource: models.MakeResource(account.ID, "test verification token"), - Type: "notverify", - } - verificationToken.ExpireSoon() - - err := env.Tokens.Insert(verificationToken) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + account.Name + `", - "invite_code": "` + verificationToken.ID + `", - "password": "` + accountPassword + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid invitation code") - So(response.Success, ShouldBeFalse) - }) - - Convey("Setup with a token that expired should fail", func() { - verificationToken := models.Token{ - Resource: models.MakeResource(account.ID, "test verification token"), - Type: "verify", - } - verificationToken.ExpiryDate = time.Now().Truncate(time.Hour * 24) - - err := env.Tokens.Insert(verificationToken) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + account.Name + `", - "invite_code": "` + verificationToken.ID + `", - "password": "` + accountPassword + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Expired invitation code") - So(response.Success, ShouldBeFalse) - }) - - Convey("Setup with proper data should succeed", func() { - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts", - ContentType: "application/json", - Body: `{ - "username": "` + account.Name + `", - "invite_code": "` + verificationToken.ID + `", - "password": "` + accountPassword + `" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Your account has been initialized successfully") - So(response.Success, ShouldBeTrue) - - Convey("After acquiring an authentication token", func() { - request, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/tokens", - ContentType: "application/json", - Body: `{ - "username": "` + account.Name + `", - "password": "` + accountPassword + `", - "type": "auth" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.TokensCreateResponse - err = request.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Authentication successful") - So(response.Success, ShouldBeTrue) - So(response.Token.ID, ShouldNotBeEmpty) - - authToken := response.Token.ID - - Convey("Accounts list query should return a proper response", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/accounts", - } - request.AddHeader("Authorization", "Bearer "+authToken) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsListResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Sorry, not implemented yet") - }) - - Convey("Getting own account information should return the account information", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/accounts/me", - } - request.AddHeader("Authorization", "Bearer "+authToken) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(response.Account.Name, ShouldEqual, account.Name) - }) - - Convey("Getting any non-me account should return a proper response", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/accounts/not-me", - } - request.AddHeader("Authorization", "Bearer "+authToken) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, `Only the "me" user is implemented`) - }) - - Convey("Updating own account should succeed", func() { - newPasswordHashBytes := sha256.Sum256([]byte("cabbage123")) - newPasswordHash := hex.EncodeToString(newPasswordHashBytes[:]) - - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/accounts/me", - ContentType: "application/json", - Body: `{ - "current_password": "` + accountPassword + `", - "new_password": "` + newPasswordHash + `", - "alt_email": "john.cabbage@example.com" - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Your account has been successfully updated") - So(response.Success, ShouldBeTrue) - So(response.Account.AltEmail, ShouldEqual, "john.cabbage@example.com") - }) - - Convey("Updating with an invalid body should fail", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/accounts/me", - ContentType: "application/json", - Body: "123123123!@#!@#!@#", - } - request.AddHeader("Authorization", "Bearer "+authToken) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid input format") - So(response.Success, ShouldBeFalse) - }) - - Convey("Trying to update not own account should fail", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/accounts/not-me", - ContentType: "application/json", - Body: `{ - "current_password": "potato", - "new_password": "cabbage", - "alt_email": "john.cabbage@example.com" - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, `Only the "me" user is implemented`) - So(response.Success, ShouldBeFalse) - }) - - Convey("Trying to update with an invalid password should fail", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/accounts/me", - ContentType: "application/json", - Body: `{ - "current_password": "potato2", - "new_password": "cabbage", - "alt_email": "john.cabbage@example.com" - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid current password") - So(response.Success, ShouldBeFalse) - }) - - Convey("Wiping not own account should fail", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts/not-me/wipe-data", - } - request.AddHeader("Authorization", "Bearer "+authToken) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsWipeDataResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, `Only the "me" user is implemented`) - So(response.Success, ShouldBeFalse) - }) - - Convey("Wiping own account should succeed", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/accounts/me/wipe-data", - } - request.AddHeader("Authorization", "Bearer "+authToken) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsWipeDataResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Your account has been successfully wiped") - So(response.Success, ShouldBeTrue) - }) - - Convey("Deleting not own account should fail", func() { - token := models.Token{ - Resource: models.MakeResource(account.ID, "test invite token"), - Type: "auth", - } - token.ExpireSoon() - - err := env.Tokens.Insert(token) - So(err, ShouldBeNil) - - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/accounts/not-me", - } - request.AddHeader("Authorization", "Bearer "+token.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsWipeDataResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, `Only the "me" user is implemented`) - So(response.Success, ShouldBeFalse) - }) - - Convey("Deleting own account should succeed", func() { - token := models.Token{ - Resource: models.MakeResource(account.ID, "test invite token"), - Type: "auth", - } - token.ExpireSoon() - - err := env.Tokens.Insert(token) - So(err, ShouldBeNil) - - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/accounts/me", - } - request.AddHeader("Authorization", "Bearer "+token.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsWipeDataResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Your account has been successfully deleted") - So(response.Success, ShouldBeTrue) - }) - }) - }) - }) - }) - }) -} diff --git a/routes/tokens.go b/routes/tokens.go index d4b3c1a..7ba21e8 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -64,11 +64,9 @@ type TokensCreateRequest struct { // TokensCreateResponse contains the result of the TokensCreate request. type TokensCreateResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Token *models.Token `json:"token,omitempty"` - FactorType string `json:"factor_type,omitempty"` - FactorChallenge string `json:"factor_challenge,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Token *models.Token `json:"token,omitempty"` } // TokensCreate allows logging in to an account. @@ -144,48 +142,6 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { } } - // Check for 2nd factor - if user.FactorType != "" { - factor, ok := env.Factors[user.FactorType] - if ok { - // Verify the 2FA - verified, challenge, err := user.Verify2FA(factor, input.Token) - if err != nil { - utils.JSONResponse(w, 500, &TokensCreateResponse{ - Success: false, - Message: "Internal 2FA error", - }) - - env.Log.WithFields(logrus.Fields{ - "err": err.Error(), - "factor": user.FactorType, - }).Warn("2FA authentication error") - return - } - - // Token was probably empty. Return the challenge. - if !verified && challenge != "" { - utils.JSONResponse(w, 403, &TokensCreateResponse{ - Success: false, - Message: "2FA token was not passed", - FactorType: user.FactorType, - FactorChallenge: challenge, - }) - return - } - - // Token was incorrect - if !verified { - utils.JSONResponse(w, 403, &TokensCreateResponse{ - Success: false, - Message: "Invalid token passed", - FactorType: user.FactorType, - }) - return - } - } - } - // Calculate the expiry date expDate := time.Now().Add(time.Hour * time.Duration(env.Config.SessionDuration)) diff --git a/setup/setup.go b/setup/setup.go index 07b6f5f..b74784a 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -27,7 +27,6 @@ import ( "github.com/lavab/api/cache" "github.com/lavab/api/db" "github.com/lavab/api/env" - "github.com/lavab/api/factor" "github.com/lavab/api/routes" "github.com/lavab/api/utils" ) @@ -163,21 +162,6 @@ func PrepareMux(flags *env.Flags) *web.Mux { // Put the RethinkDB session into the environment package env.Rethink = rethinkSession - // Initialize factors - env.Factors = make(map[string]factor.Factor) - if flags.YubiCloudID != "" { - yubicloud, err := factor.NewYubiCloud(flags.YubiCloudID, flags.YubiCloudKey) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Unable to initiate YubiCloud") - } - env.Factors[yubicloud.Type()] = yubicloud - } - - authenticator := factor.NewAuthenticator(6) - env.Factors[authenticator.Type()] = authenticator - // Initialize the tables env.Tokens = &db.TokensTable{ RethinkCRUD: db.NewCRUDTable( diff --git a/utils/errors.go b/utils/errors.go new file mode 100644 index 0000000..0c1bea4 --- /dev/null +++ b/utils/errors.go @@ -0,0 +1,120 @@ +package utils + +import ( + "bytes" + "fmt" + "path/filepath" + "runtime" + "strconv" +) + +type Error struct { + Success bool `json:"success"` + Code int `json:"code,omitempty"` + Location string `json:"location,omitempty"` + Error interface{} `json:"error"` + Severe bool `json:"-"` +} + +func NewError(code int, input interface{}, severe bool) *Error { + _, file, line, _ := runtime.Caller(1) + + return &Error{ + Code: code, + Error: input, + Location: filepath.Base(file) + ":" + strconv.Itoa(line), + Severe: severe, + } +} + +func (e *Error) String() string { + buf := &bytes.Buffer{} + + if e.Code != 0 { + buf.WriteString("[") + buf.WriteString(strconv.Itoa(e.Code)) + buf.WriteString("] ") + } + + if e.Location != "" { + buf.WriteString(e.Location) + buf.WriteString(": ") + } + + switch v := e.Error.(type) { + case error: + buf.WriteString(v.Error()) + case string: + buf.WriteString(v) + default: + buf.WriteString(fmt.Sprintf("%+v", v)) + } + + return buf.String() +} + +const ( + AccountsListUnknown = 10000 + iota +) + +const ( + AccountsCreateUnknown = 10100 + iota + AccountsCreateInvalidInput + AccountsCreateUnknownStep + AccountsCreateInvalidLength + AccountsCreateUsernameTaken + AccountsCreateEmailUsed + AccountsCreateUnableToInsertAccount + AccountsCreateUserNotFound + AccountsCreateInvalidToken + AccountsCreateInvalidTokenOwner + AccountsCreateInvalidTokenType + AccountsCreateExpiredToken + AccountsCreateAlreadyConfigured + AccountsCreateWeakPassword + AccountsCreateUnableToHash + AccountsCreateUnableToPrepareLabels + AccountsCreateUnableToCreateAddress + AccountsCreateUnableToUpdateAccount +) + +const ( + AccountsGetUnknown = 10200 + iota + AccountsGetOnlyMe + AccountsGetUnableToGet +) + +const ( + AccountsUpdateUnknown = 10300 + iota + AccountsUpdateInvalidInput + AccountsUpdateOnlyMe + AccountsUpdateUnableToGet + AccountsUpdateInvalidCurrentPassword + AccountsUpdateWeakPassword + AccountsUpdateUnableToHash + AccountsUpdateInvalidPublicKey + AccountsUpdateInvalidPublicKeyOwner + AccountsUpdateUnableToUpdate +) + +const ( + AccountsDeleteUnknown = 10400 + iota + AccountsDeleteOnlyMe + AccountsDeleteUnableToGet + AccountsDeleteUnableToDelete +) + +const ( + AccountsWipeDataUnknown = 10500 + iota + AccountsWipeDataOnlyMe + AccountsWipeDataUnableToGet + AccountsWipeDataUnableToDelete +) + +const ( + AccountsStartOnboardingUnknown = 10600 + iota + AccountsStartOnboardingOnlyMe + AccountsStartOnboardingUnableToGet + AccountsStartOnboardingMisconfigured + AccountsStartOnboardingUnableToInit +) diff --git a/utils/requests.go b/utils/requests.go index 0cf08e6..dc3dcef 100644 --- a/utils/requests.go +++ b/utils/requests.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/Sirupsen/logrus" + "github.com/getsentry/raven-go" "github.com/gorilla/schema" "github.com/lavab/api/env" @@ -28,6 +29,28 @@ func JSONResponse(w http.ResponseWriter, status int, data interface{}) { status = 200 } + // Check if the data is an error + if err, ok := data.(*Error); ok { + if err.Severe { + packet := raven.NewPacket( + err.String(), + raven.NewException(errors.New(err.String()), raven.NewStacktrace(1, 3, nil)), + ) + eid, _ := env.Raven.Capture(packet, nil) + + env.Log.WithFields(logrus.Fields{ + "location": err.Location, + "code": err.Code, + "event_id": eid, + }).Error(err.Error) + } else { + env.Log.WithFields(logrus.Fields{ + "location": err.Location, + "code": err.Code, + }).Error(err.Error) + } + } + // Try to marshal the input result, err := json.Marshal(data) if err != nil { From 64deabac1fa0901b231e1e5bc7ecd3c874987fd9 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 17:29:23 +0200 Subject: [PATCH 12/27] Removed tests, new error logging for addresses, avatars and contacts --- routes/addresses.go | 12 +- routes/avatars.go | 7 +- routes/avatars_test.go_ | 73 -------- routes/contacts.go | 158 ++++++---------- routes/contacts_test.go_ | 359 ------------------------------------- routes/emails_test.go_ | 298 ------------------------------ routes/files_test.go_ | 323 --------------------------------- routes/hello_test.go_ | 25 --- routes/init_test.go_ | 72 -------- routes/keys_test.go_ | 230 ------------------------ routes/labels_test.go_ | 314 -------------------------------- routes/middleware_test.go_ | 105 ----------- routes/tokens_test.go_ | 193 -------------------- utils/errors.go | 42 +++++ 14 files changed, 103 insertions(+), 2108 deletions(-) delete mode 100644 routes/avatars_test.go_ delete mode 100644 routes/contacts_test.go_ delete mode 100644 routes/emails_test.go_ delete mode 100644 routes/files_test.go_ delete mode 100644 routes/hello_test.go_ delete mode 100644 routes/init_test.go_ delete mode 100644 routes/keys_test.go_ delete mode 100644 routes/labels_test.go_ delete mode 100644 routes/middleware_test.go_ delete mode 100644 routes/tokens_test.go_ diff --git a/routes/addresses.go b/routes/addresses.go index ca91043..864db9b 100644 --- a/routes/addresses.go +++ b/routes/addresses.go @@ -3,7 +3,6 @@ package routes import ( "net/http" - "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" "github.com/lavab/api/env" @@ -21,14 +20,9 @@ func AddressesList(c web.C, w http.ResponseWriter, r *http.Request) { session := c.Env["token"].(*models.Token) addresses, err := env.Addresses.GetOwnedBy(session.Owner) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to fetch addresses") - - utils.JSONResponse(w, 500, &AddressesListResponse{ - Success: false, - Message: "Internal error (code AD/LI/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.AddressesListUnableToGet, err, true, + )) return } diff --git a/routes/avatars.go b/routes/avatars.go index 944d843..edddeb5 100644 --- a/routes/avatars.go +++ b/routes/avatars.go @@ -45,10 +45,9 @@ func Avatars(c web.C, w http.ResponseWriter, r *http.Request) { var err error width, err = strconv.Atoi(widthString) if err != nil { - utils.JSONResponse(w, 400, map[string]interface{}{ - "succes": false, - "message": "Invalid width", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.AvatarsInvalidWidth, "Invalid width", false, + )) return } } diff --git a/routes/avatars_test.go_ b/routes/avatars_test.go_ deleted file mode 100644 index ef5bbba..0000000 --- a/routes/avatars_test.go_ +++ /dev/null @@ -1,73 +0,0 @@ -package routes_test - -import ( - "crypto/md5" - "encoding/hex" - "testing" - - "github.com/dchest/uniuri" - "github.com/franela/goreq" - . "github.com/smartystreets/goconvey/convey" -) - -func TestAvatarsRoute(t *testing.T) { - Convey("While querying avatars generator", t, func() { - Convey("Default settings PNG avatar using a hash", func() { - email := uniuri.New() + "@lavaboom.io" - rawHashedEmail := md5.Sum([]byte(email)) - hashedEmail := hex.EncodeToString(rawHashedEmail[:]) - - result, err := goreq.Request{ - Method: "GET", - Uri: server.URL + "/avatars/" + hashedEmail + ".png", - }.Do() - So(err, ShouldBeNil) - - avatarFromHash, err := result.Body.ToString() - So(err, ShouldBeNil) - So(avatarFromHash, ShouldNotBeEmpty) - - Convey("A non-hashed avatar should be the same", func() { - result, err := goreq.Request{ - Method: "GET", - Uri: server.URL + "/avatars/" + email + ".png", - }.Do() - So(err, ShouldBeNil) - - avatarFromEmail, err := result.Body.ToString() - So(err, ShouldBeNil) - So(avatarFromEmail, ShouldEqual, avatarFromHash) - }) - }) - - Convey("A 150px-wide SVG avatar should have a proper size", func() { - email := uniuri.New() + "@lavaboom.io" - - result, err := goreq.Request{ - Method: "GET", - Uri: server.URL + "/avatars/" + email + ".svg?width=150", - }.Do() - So(err, ShouldBeNil) - - avatar, err := result.Body.ToString() - So(err, ShouldBeNil) - So(avatar, ShouldNotBeEmpty) - So(avatar, ShouldContainSubstring, `width="150"`) - }) - - Convey("Invalid custom width should fail", func() { - email := uniuri.New() + "@lavaboom.io" - - result, err := goreq.Request{ - Method: "GET", - Uri: server.URL + "/avatars/" + email + ".svg?width=ayylmao", - }.Do() - So(err, ShouldBeNil) - - avatar, err := result.Body.ToString() - So(err, ShouldBeNil) - So(avatar, ShouldNotBeEmpty) - So(avatar, ShouldContainSubstring, "Invalid width") - }) - }) -} diff --git a/routes/contacts.go b/routes/contacts.go index 6fa712a..4d6d531 100644 --- a/routes/contacts.go +++ b/routes/contacts.go @@ -3,7 +3,6 @@ package routes import ( "net/http" - "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" "github.com/lavab/api/env" @@ -26,14 +25,9 @@ func ContactsList(c web.C, w http.ResponseWriter, r *http.Request) { // Get contacts from the database contacts, err := env.Contacts.GetOwnedBy(session.Owner) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to fetch contacts") - - utils.JSONResponse(w, 500, &ContactsListResponse{ - Success: false, - Message: "Internal error (code CO/LI/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.ContactsListUnableToGet, err, true, + )) return } @@ -45,12 +39,11 @@ func ContactsList(c web.C, w http.ResponseWriter, r *http.Request) { // ContactsCreateRequest is the payload that user should pass to POST /contacts type ContactsCreateRequest struct { - Data string `json:"data" schema:"data"` - Name string `json:"name" schema:"name"` - Encoding string `json:"encoding" schema:"encoding"` - VersionMajor int `json:"version_major" schema:"version_major"` - VersionMinor int `json:"version_minor" schema:"version_minor"` - PGPFingerprints []string `json:"pgp_fingerprints" schema:"pgp_fingerprints"` + Data string `json:"data" schema:"data"` + Name string `json:"name" schema:"name"` + Encoding string `json:"encoding" schema:"encoding"` + VersionMajor int `json:"version_major" schema:"version_major"` + VersionMinor int `json:"version_minor" schema:"version_minor"` } // ContactsCreateResponse contains the result of the ContactsCreate request. @@ -66,14 +59,9 @@ func ContactsCreate(c web.C, w http.ResponseWriter, r *http.Request) { var input ContactsCreateRequest err := utils.ParseRequest(r, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 400, &ContactsCreateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.ContactsCreateInvalidInput, err, false, + )) return } @@ -81,38 +69,30 @@ func ContactsCreate(c web.C, w http.ResponseWriter, r *http.Request) { session := c.Env["token"].(*models.Token) // Ensure that the input data isn't empty - if input.Data == "" || input.Name == "" || input.Encoding == "" || - input.PGPFingerprints == nil || len(input.PGPFingerprints) == 0 { - utils.JSONResponse(w, 400, &ContactsCreateResponse{ - Success: false, - Message: "Invalid request", - }) + if input.Data == "" || input.Name == "" || input.Encoding == "" { + utils.JSONResponse(w, 400, utils.NewError( + utils.ContactsCreateInvalidInput, "One of the input fields is empty", false, + )) return } // Create a new contact struct contact := &models.Contact{ Encrypted: models.Encrypted{ - Encoding: input.Encoding, - Data: input.Data, - Schema: "contact", - VersionMajor: input.VersionMajor, - VersionMinor: input.VersionMinor, - PGPFingerprints: input.PGPFingerprints, + Encoding: input.Encoding, + Data: input.Data, + Schema: "contact", + VersionMajor: input.VersionMajor, + VersionMinor: input.VersionMinor, }, Resource: models.MakeResource(session.Owner, input.Name), } // Insert the contact into the database if err := env.Contacts.Insert(contact); err != nil { - utils.JSONResponse(w, 500, &ContactsCreateResponse{ - Success: false, - Message: "internal server error - CO/CR/01", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Could not insert a contact into the database") + utils.JSONResponse(w, 500, utils.NewError( + utils.ContactsCreateUnableToInsert, err, true, + )) return } @@ -135,10 +115,9 @@ func ContactsGet(c web.C, w http.ResponseWriter, r *http.Request) { // Get the contact from the database contact, err := env.Contacts.GetContact(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &ContactsGetResponse{ - Success: false, - Message: "Contact not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.ContactsGetUnableToGet, err, false, + )) return } @@ -147,10 +126,9 @@ func ContactsGet(c web.C, w http.ResponseWriter, r *http.Request) { // Check for ownership if contact.Owner != session.Owner { - utils.JSONResponse(w, 404, &ContactsGetResponse{ - Success: false, - Message: "Contact not found", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.ContactsGetNotOwned, "You're not the owner of this contact", false, + )) return } @@ -163,12 +141,11 @@ func ContactsGet(c web.C, w http.ResponseWriter, r *http.Request) { // ContactsUpdateRequest is the payload passed to PUT /contacts/:id type ContactsUpdateRequest struct { - Data string `json:"data" schema:"data"` - Name string `json:"name" schema:"name"` - Encoding string `json:"encoding" schema:"encoding"` - VersionMajor *int `json:"version_major" schema:"version_major"` - VersionMinor *int `json:"version_minor" schema:"version_minor"` - PGPFingerprints []string `json:"pgp_fingerprints" schema:"pgp_fingerprints"` + Data string `json:"data" schema:"data"` + Name string `json:"name" schema:"name"` + Encoding string `json:"encoding" schema:"encoding"` + VersionMajor *int `json:"version_major" schema:"version_major"` + VersionMinor *int `json:"version_minor" schema:"version_minor"` } // ContactsUpdateResponse contains the result of the ContactsUpdate request. @@ -184,24 +161,18 @@ func ContactsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { var input ContactsUpdateRequest err := utils.ParseRequest(r, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 400, &ContactsUpdateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.ContactsUpdateInvalidInput, err, false, + )) return } // Get the contact from the database contact, err := env.Contacts.GetContact(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &ContactsUpdateResponse{ - Success: false, - Message: "Contact not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.ContactsUpdateUnableToGet, err, false, + )) return } @@ -210,10 +181,9 @@ func ContactsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { // Check for ownership if contact.Owner != session.Owner { - utils.JSONResponse(w, 404, &ContactsUpdateResponse{ - Success: false, - Message: "Contact not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.ContactsUpdateNotOwned, "You're not the owner of this contact", false, + )) return } @@ -237,22 +207,12 @@ func ContactsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { contact.VersionMinor = *input.VersionMinor } - if input.PGPFingerprints != nil { - contact.PGPFingerprints = input.PGPFingerprints - } - // Perform the update err = env.Contacts.UpdateID(c.URLParams["id"], contact) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": c.URLParams["id"], - }).Error("Unable to update a contact") - - utils.JSONResponse(w, 500, &ContactsUpdateResponse{ - Success: false, - Message: "Internal error (code CO/UP/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.ContactsUpdateUnableToUpdate, err, true, + )) return } @@ -274,10 +234,9 @@ func ContactsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Get the contact from the database contact, err := env.Contacts.GetContact(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &ContactsDeleteResponse{ - Success: false, - Message: "Contact not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.ContactsDeleteUnableToGet, err, false, + )) return } @@ -286,25 +245,18 @@ func ContactsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Check for ownership if contact.Owner != session.Owner { - utils.JSONResponse(w, 404, &ContactsDeleteResponse{ - Success: false, - Message: "Contact not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.ContactsDeleteNotOwned, "You're not the owner of this contact", false, + )) return } // Perform the deletion err = env.Contacts.DeleteID(c.URLParams["id"]) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": c.URLParams["id"], - }).Error("Unable to delete a contact") - - utils.JSONResponse(w, 500, &ContactsDeleteResponse{ - Success: false, - Message: "Internal error (code CO/DE/01)", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.ContactsDeleteUnableToDelete, "You're not the owner of this contact", true, + )) return } diff --git a/routes/contacts_test.go_ b/routes/contacts_test.go_ deleted file mode 100644 index 8ec2038..0000000 --- a/routes/contacts_test.go_ +++ /dev/null @@ -1,359 +0,0 @@ -package routes_test - -import ( - "testing" - - "github.com/dchest/uniuri" - "github.com/franela/goreq" - . "github.com/smartystreets/goconvey/convey" - - "github.com/lavab/api/env" - "github.com/lavab/api/models" - "github.com/lavab/api/routes" -) - -func TestContactsRoute(t *testing.T) { - Convey("Given a working account", t, func() { - account := &models.Account{ - Resource: models.MakeResource("", "johnorange2"), - Status: "complete", - AltEmail: "john2@orange.org", - } - err := account.SetPassword("fruityloops") - So(err, ShouldBeNil) - - err = env.Accounts.Insert(account) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/tokens", - ContentType: "application/json", - Body: `{ - "type": "auth", - "username": "johnorange2", - "password": "fruityloops" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.TokensCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - authToken := response.Token - - Convey("Creating a contact with missing parts should fail", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/contacts", - ContentType: "application/json", - Body: `{ - "data": "` + uniuri.NewLen(64) + `", - "encoding": "json", - "version_major": 1, - "version_minor": 0, - "pgp_fingerprints": ["` + uniuri.New() + `"] - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid request") - So(response.Success, ShouldBeFalse) - }) - - Convey("Creating a contact with invalid input data should fail", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/contacts", - ContentType: "application/json", - Body: "!@#!@#!@#", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid input format") - So(response.Success, ShouldBeFalse) - }) - - Convey("Getting a non-owned contact should fail", func() { - contact := &models.Contact{ - Encrypted: models.Encrypted{ - Encoding: "json", - Data: uniuri.NewLen(64), - Schema: "contact", - VersionMajor: 1, - VersionMinor: 0, - }, - Resource: models.MakeResource("not", uniuri.New()), - } - - err := env.Contacts.Insert(contact) - So(err, ShouldBeNil) - - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/contacts/" + contact.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Contact not found") - - Convey("Update of a not-owned contact should fail", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/contacts/" + contact.ID, - ContentType: "application/json", - Body: `{ - "data": "` + uniuri.NewLen(64) + `", - "name": "` + uniuri.New() + `", - "encoding": "xml", - "version_major": 8, - "version_minor": 3 - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Contact not found") - }) - - Convey("Deleting it should fail", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/contacts/" + contact.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Contact not found") - }) - }) - - Convey("Getting a non-existing contact should fail", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/contacts/" + uniuri.New(), - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Contact not found") - }) - - Convey("Creating a contact should succeed", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/contacts", - ContentType: "application/json", - Body: `{ - "data": "` + uniuri.NewLen(64) + `", - "name": "` + uniuri.New() + `", - "encoding": "json", - "version_major": 1, - "version_minor": 0, - "pgp_fingerprints": ["` + uniuri.New() + `"] - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "A new contact was successfully created") - So(response.Success, ShouldBeTrue) - So(response.Contact.ID, ShouldNotBeNil) - - contact := response.Contact - - Convey("The contact should be visible on the list", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/contacts", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsListResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(len(*response.Contacts), ShouldBeGreaterThan, 0) - So(response.Success, ShouldBeTrue) - - found := false - for _, c := range *response.Contacts { - if c.ID == contact.ID { - found = true - break - } - } - - So(found, ShouldBeTrue) - }) - - Convey("Getting that contact should succeed", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/contacts/" + contact.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(response.Contact.Name, ShouldEqual, contact.Name) - }) - - Convey("Updating that contact should succeed", func() { - newName := uniuri.New() - - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/contacts/" + contact.ID, - ContentType: "application/json", - Body: `{ - "data": "` + uniuri.NewLen(64) + `", - "name": "` + newName + `", - "encoding": "xml", - "version_major": 8, - "version_minor": 3 - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(response.Contact.Name, ShouldEqual, newName) - }) - - Convey("Deleting that contact should succeed", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/contacts/" + contact.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(response.Message, ShouldEqual, "Contact successfully removed") - }) - }) - - Convey("Update with invalid input should fail", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/contacts/" + uniuri.New(), - ContentType: "application/json", - Body: "123123!@#!@#", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid input format") - }) - - Convey("Update of a non-existing contact should fail", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/contacts/gibberish", - ContentType: "application/json", - Body: `{ - "data": "` + uniuri.NewLen(64) + `", - "name": "` + uniuri.New() + `", - "encoding": "xml", - "version_major": 8, - "version_minor": 3 - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Contact not found") - }) - - Convey("Deleting a non-existing contact should fail", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/contacts/gibberish", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.ContactsDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Contact not found") - }) - }) -} diff --git a/routes/emails_test.go_ b/routes/emails_test.go_ deleted file mode 100644 index 3c72234..0000000 --- a/routes/emails_test.go_ +++ /dev/null @@ -1,298 +0,0 @@ -package routes_test - -import ( - "testing" - - "github.com/dchest/uniuri" - "github.com/franela/goreq" - . "github.com/smartystreets/goconvey/convey" - - "github.com/lavab/api/env" - "github.com/lavab/api/models" - "github.com/lavab/api/routes" -) - -func TestEmailsRoute(t *testing.T) { - Convey("Given a working account", t, func() { - account := &models.Account{ - Resource: models.MakeResource("", "johnorange3"), - Status: "complete", - AltEmail: "john3@orange.org", - } - err := account.SetPassword("fruityloops") - So(err, ShouldBeNil) - - err = env.Accounts.Insert(account) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/tokens", - ContentType: "application/json", - Body: `{ - "type": "auth", - "username": "johnorange3", - "password": "fruityloops" - }`, - }.Do() - So(err, ShouldBeNil) - - err = env.Labels.Insert([]*models.Label{ - &models.Label{ - Resource: models.MakeResource(account.ID, "Inbox"), - Builtin: true, - }, - &models.Label{ - Resource: models.MakeResource(account.ID, "Sent"), - Builtin: true, - }, - &models.Label{ - Resource: models.MakeResource(account.ID, "Trash"), - Builtin: true, - }, - &models.Label{ - Resource: models.MakeResource(account.ID, "Spam"), - Builtin: true, - }, - &models.Label{ - Resource: models.MakeResource(account.ID, "Starred"), - Builtin: true, - }, - }) - So(err, ShouldBeNil) - - var response routes.TokensCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - authToken := response.Token - - Convey("Creating a new email using invalid JSON input should fail", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/emails", - ContentType: "application/json", - Body: "!@#!@#!@#", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid input format") - So(response.Success, ShouldBeFalse) - }) - - Convey("Creating a new email with missing data should fail", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/emails", - ContentType: "application/json", - Body: routes.EmailsCreateRequest{ - To: []string{"piotr@zduniak.net"}, - Subject: "hello world", - }, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid request") - So(response.Success, ShouldBeFalse) - }) - - Convey("Getting a non-existing email should fail", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/emails/nonexisting", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Email not found") - So(response.Success, ShouldBeFalse) - }) - - Convey("Getting a non-owned email should fail", func() { - email := &models.Email{ - Resource: models.MakeResource("not", uniuri.New()), - } - - err := env.Emails.Insert(email) - So(err, ShouldBeNil) - - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/emails/" + email.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Email not found") - So(response.Success, ShouldBeFalse) - - Convey("Deleting it should fail", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/emails/" + email.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Email not found") - }) - }) - - Convey("Listing emails with invalid offset should fail", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/emails?offset=pppp&limit=1&sort=+date", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsListResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid offset") - }) - - Convey("Listing emails with invalid limit should fail", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/emails?offset=0&limit=pppp&sort=+date", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsListResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid limit") - }) - - Convey("Delete a non-existing email should fail", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/emails/nonexisting", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Email not found") - }) - - Convey("Creating a new email should succeed", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/emails", - ContentType: "application/json", - Body: routes.EmailsCreateRequest{ - To: []string{"test@lavaboom.io"}, - Subject: "hello world", - Body: "raw meaty email", - }, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldBeEmpty) - So(len(response.Created), ShouldBeGreaterThan, 0) - So(response.Success, ShouldBeTrue) - - emailID := response.Created[0] - - Convey("Getting that email should succeed", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/emails/" + emailID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(response.Email.Name, ShouldEqual, "hello world") - }) - - Convey("That email should be visible on the emails list", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/emails?offset=0&limit=1&sort=+date", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsListResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So((*response.Emails)[0].Name, ShouldEqual, "hello world") - }) - - Convey("Deleting that email should succeed", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/emails/" + emailID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.EmailsDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(response.Message, ShouldEqual, "Email successfully removed") - }) - }) - }) -} diff --git a/routes/files_test.go_ b/routes/files_test.go_ deleted file mode 100644 index 91897ac..0000000 --- a/routes/files_test.go_ +++ /dev/null @@ -1,323 +0,0 @@ -package routes_test - -import ( - "testing" - - "github.com/dchest/uniuri" - "github.com/franela/goreq" - . "github.com/smartystreets/goconvey/convey" - - "github.com/lavab/api/env" - "github.com/lavab/api/models" - "github.com/lavab/api/routes" -) - -func TestFilesRoute(t *testing.T) { - Convey("When uploading a new file", t, func() { - account := &models.Account{ - Resource: models.MakeResource("", "johnorange"), - Status: "complete", - AltEmail: "john@orange.org", - } - err := account.SetPassword("fruityloops") - So(err, ShouldBeNil) - - err = env.Accounts.Insert(account) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/tokens", - ContentType: "application/json", - Body: `{ - "type": "auth", - "username": "johnorange", - "password": "fruityloops" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.TokensCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - authToken := response.Token - - Convey("Misformatted body should fail", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/files", - ContentType: "application/json", - Body: "!@#!@#", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid input format") - }) - - Convey("Invalid set of data should fail", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/files", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AccountsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid request") - }) - - Convey("File upload should succeed", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/files", - ContentType: "application/json", - Body: `{ - "data": "` + uniuri.NewLen(64) + `", - "name": "` + uniuri.New() + `", - "encoding": "json", - "version_major": 1, - "version_minor": 0, - "pgp_fingerprints": ["` + uniuri.New() + `"] -}`, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "A new file was successfully created") - So(response.Success, ShouldBeTrue) - So(response.File.ID, ShouldNotBeEmpty) - - file := response.File - - Convey("Getting that file should succeed", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/files/" + file.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.File.ID, ShouldNotBeNil) - So(response.Success, ShouldBeTrue) - }) - - Convey("The file should be visible on the list", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/files", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesListResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(len(*response.Files), ShouldBeGreaterThan, 0) - So(response.Success, ShouldBeTrue) - - found := false - for _, a := range *response.Files { - if a.ID == file.ID { - found = true - break - } - } - - So(found, ShouldBeTrue) - }) - - Convey("Updating it should succeed", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/files/" + file.ID, - ContentType: "application/json", - Body: `{ - "data": "` + uniuri.NewLen(64) + `", - "name": "` + uniuri.New() + `", - "encoding": "xml", - "version_major": 2, - "version_minor": 1, - "pgp_fingerprints": ["` + uniuri.New() + `"] - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(response.File.ID, ShouldEqual, file.ID) - So(response.File.Encoding, ShouldEqual, "xml") - }) - - Convey("Deleting it should succeed", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/files/" + file.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "File successfully removed") - So(response.Success, ShouldBeTrue) - }) - }) - - Convey("Getting a non-existing file should fail", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/files/doesntexist", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "File not found") - So(response.Success, ShouldBeFalse) - - Convey("Updating it should fail too", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/files/doesntexist", - ContentType: "application/json", - Body: "{}", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "File not found") - So(response.Success, ShouldBeFalse) - }) - }) - - Convey("Getting a non-owned file should fail", func() { - file := &models.File{ - Encrypted: models.Encrypted{ - Encoding: "json", - Data: uniuri.NewLen(64), - Schema: "file", - VersionMajor: 1, - VersionMinor: 0, - PGPFingerprints: []string{uniuri.New()}, - }, - Resource: models.MakeResource("nonowned", "photo.jpg"), - } - - err := env.Files.Insert(file) - So(err, ShouldBeNil) - - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/files/" + file.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "File not found") - So(response.Success, ShouldBeFalse) - }) - - Convey("Updating using a misformatted body should fail", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/files/shizzle", - ContentType: "application/json", - Body: "!@#!@#", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid input format") - }) - - Convey("Updating a non-existing file should fail", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/files/shizzle", - ContentType: "application/json", - Body: "{}", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "File not found") - }) - - Convey("Deleting a non-existing file should fail", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/files/shizzle", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.FilesDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "File not found") - }) - }) -} diff --git a/routes/hello_test.go_ b/routes/hello_test.go_ deleted file mode 100644 index 7f05bf4..0000000 --- a/routes/hello_test.go_ +++ /dev/null @@ -1,25 +0,0 @@ -package routes_test - -import ( - "testing" - - "github.com/franela/goreq" - "github.com/stretchr/testify/require" - - "github.com/lavab/api/routes" -) - -func TestHello(t *testing.T) { - // Request the / route - helloResult, err := goreq.Request{ - Method: "GET", - Uri: server.URL, - }.Do() - require.Nil(t, err) - - // Unmarshal the response - var helloResponse routes.HelloResponse - err = helloResult.Body.FromJsonTo(&helloResponse) - require.Nil(t, err) - require.Equal(t, "Lavaboom API", helloResponse.Message) -} diff --git a/routes/init_test.go_ b/routes/init_test.go_ deleted file mode 100644 index 1dbb6ff..0000000 --- a/routes/init_test.go_ +++ /dev/null @@ -1,72 +0,0 @@ -package routes_test - -import ( - "fmt" - "net/http/httptest" - "time" - - "github.com/dancannon/gorethink" - - "github.com/lavab/api/env" - "github.com/lavab/api/setup" -) - -var ( - server *httptest.Server - authToken string -) - -func init() { - // Mock data - env.Config = &env.Flags{ - APIVersion: "v0", - LogFormatterType: "text", - ForceColors: true, - - SessionDuration: 72, - - RedisAddress: "127.0.0.1:6379", - - NSQdAddress: "127.0.0.1:4150", - LookupdAddress: "127.0.0.1:4160", - - RethinkDBAddress: "127.0.0.1:28015", - RethinkDBKey: "", - RethinkDBDatabase: "test", - } - - // Connect to the RethinkDB server - rdbSession, err := gorethink.Connect(gorethink.ConnectOpts{ - Address: env.Config.RethinkDBAddress, - AuthKey: env.Config.RethinkDBKey, - MaxIdle: 10, - Timeout: time.Second * 10, - }) - if err != nil { - panic("connecting to RethinkDB should not return an error, got " + err.Error()) - } - - // Clear the test database - err = gorethink.DbDrop("test").Exec(rdbSession) - if err != nil { - fmt.Println("removing the test database should not return an error, got " + err.Error()) - } - - // Disconnect - err = rdbSession.Close() - if err != nil { - panic("closing the RethinkDB session should not return an error, got " + err.Error()) - } - - // Prepare a new mux (initialize the API) - mux := setup.PrepareMux(env.Config) - if mux == nil { - panic("returned mux was nil") - } - - // Set up a new temporary HTTP test server - server = httptest.NewServer(mux) - if server == nil { - panic("returned httptest server was nil") - } -} diff --git a/routes/keys_test.go_ b/routes/keys_test.go_ deleted file mode 100644 index 8650c48..0000000 --- a/routes/keys_test.go_ +++ /dev/null @@ -1,230 +0,0 @@ -package routes_test - -import ( - "strings" - "testing" - - "github.com/franela/goreq" - . "github.com/smartystreets/goconvey/convey" - - "github.com/lavab/api/env" - "github.com/lavab/api/models" - "github.com/lavab/api/routes" -) - -func TestKeysRoute(t *testing.T) { - Convey("Given a working account", t, func() { - account := &models.Account{ - Resource: models.MakeResource("", "johnorange4"), - Status: "complete", - AltEmail: "john4@orange.org", - } - err := account.SetPassword("fruityloops") - So(err, ShouldBeNil) - - err = env.Accounts.Insert(account) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/tokens", - ContentType: "application/json", - Body: `{ - "type": "auth", - "username": "johnorange4", - "password": "fruityloops" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.TokensCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - authToken := response.Token - - Convey("Uploading a new key using an invalid JSON format should fail", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/keys", - ContentType: "application/json", - Body: "!@#!@!@#", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.KeysCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid input format") - So(response.Success, ShouldBeFalse) - }) - - Convey("Uploading an invalid key should fail", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/keys", - ContentType: "application/json", - Body: `{ - "key": "hbnjmvnbhvm nbhm jhbjmnghnbgjvgbhvf bgvmj gvhnft" - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.KeysCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid key format") - So(response.Success, ShouldBeFalse) - }) - - Convey("Uploading a key should succeed", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/keys", - ContentType: "application/json", - Body: `{ - "key": "` + strings.Join(strings.Split(`-----BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1 - -mQINBFR6JFoBEADoLOVi5NEkIELYOIfOsztAuPqNPiJcDXCsKuprjNj7n2vxyNim -WbArRZ4TJereG0H2skCQlKMx26EiHHdK3je4i6erD+OT4NolAsxVsl4PpkEDZnzz -tIwVb7FymahIrqwP9YPrXc0tr07HgnE3+it828ZJlCMfGUgJJrn12p+UetlBoFwr -OEgaCl4fOfAuUQUzD156AGV/S0H4ge8H7yngSxNTMCqypX6SaX+O0uhKqa3CxiiG -HxIGo+lNdM72Xm3Ym9sNKtfsflkqZdlWfdpit1mgveZMx2CpuYI1aS+FRzQczCDn -fDnSVqErIWUv64daC5qU3pPWjqRuOr4WXEdxXSCgi2oXVP+2hVyqgPk6ch64TodR -lKxFN2wvrJVYJd/5XQrojBtf/F/ZnlYq0rze+snZ5R1lBMZMU2oBnWtRQMSO/+8b -iHY/7mjyT+LGLXhbGGmgtycYsuujR54Smtzx1tc7CsoVLJ3JB4629YT6RtDnd85R -f7oUnjtd714e6k6zLIkppsSDse8WOPGtnfHxswrNRGnEPFYxQhCN+PbYdwGmSfmA -kzoJFumJF8KIXflGBZ0s2JdAx4G1aMhPR3rUNiJdh+DXXseLn/PAbDj2O4uMVi5F -/ai6U/vhNOatrt5syOwWZnShuIBj5VwwyJOdGjC9uwYrfocDtx7IdbaokQARAQAB -tCFQaW90ciBaZHVuaWFrIDxwaW90ckB6ZHVuaWFrLm5ldD6JAjgEEwECACIFAlR6 -JFoCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEN9g3PR+HyAlZigP/3H2 -l9icK0tazF5B4jcPaKJ4cToe/XiTU1eNNzTGftlbtCgb2e2TMuzcY7LpiK3zHO5z -0NlVKWxAoD7JHEaG5vwL74gB1324VbW08dWcz/a/jMyTAUhGIZ1WBIJGa9dVkN98 -GZp6i8q2DfsvflQI5Q9s3+Y6nbl2FEDFc3U+UXyN3M7x94NEc+3BUPvds/CwD/L0 -rjatqusCf1lo2GNZvVcoluerKjSR0/LryTbQwSlW0rDIVAoc5AB1ezpJKfW6O22i -4h8MpNGNJ3XVrMIX4/Tu4ESE75WQSVqThd1Zy3y9bVvhL8UxKV3qviuBRDtlk/7N -QznUBTJ0RFegebTDp6+jVaVt+RBJg8rnwXOT0iSEBionCjjuIWX7hzM3mRg8FnnJ -RUudJxN2b1mJHKCHEG3/SIbl6m32HesJahfNnmGV8xs7YpZWHQU+DXoTJN8+t/2E -kZ7+4X38jdWfLfw4Z+Cb3J+J4yf0uipUQ8+6f7zm2p0BINlt5TQczZpWYQolhKoK -Xhd+Sd2XieaAkxUQqaYjCbr5fC5QouWYlwqnghCVSs1MLCPdHDI2FOXB5Sh8hOHN -sxar+5r9iWLkAvr5k+QoR8fQgarIQKcXQc+NQR65D8eneGo/apVknvRVMLrtC1ZI -QLi8aLMFaM6HReXsHD6PJUsuuHys2fhT+6vD4ujjuQINBFR6JFoBEADMa8xp8O1W -WvRxBZ0Bd0EOm+znhCsDhdHxrq3x74k3229NVJ42tfRunegP+s+/nFQuSV/FXxiL -NFb7cfTL2ZlibNbOwbZ6RQ66BdPaBKyIc0QdIsaR/+ehGqbG0dN1aAiQJBustPzX -RQJBhzHKx4FpdJLrFppe5JLp2pcmI9CoMHdirIh3uFF85sNBTa0MAHNBHzXBeZbv -jZDCxTkFBPmUEbNiUWDOPDQnZlJAG9VvXzSLilsZ4Cgj/jN0/MUJ+vEOb1NvOWNH -Wo0/uFqmMhAFHxFSUETnZ4Q/6ZU2bdCeAp9uo1oEFvaEbmRdW1BkjMOXqJ4V5bXj -p9qREraEargj3+FKQHIiKDEz6p4C9y0RsJROIj8oZmvZsynzsnrmU5Gme5V8a4sS -ruPkm3kmdPCWq1SSZ/3V293NnE73KKdy6XinuyZBWVN1y8jSd/lJpyIZzIIMAQSp -OwWBYnVwTIlbFi0Ad1BGvMMSCM15AdrN9Ywb7xfnlkXEMHTQk4czwJUDKYodIw1u -KnGm/N/SPlgm1sk59rlMTQk0/TFT6KsYEoDdEJP934lldG+11vgpcicV8owM0AQ4 -PYtVTKhHv7QNK0FCIHWIWq/QMLJn73X7kotgLB/1M94eTgcWasg4ENI/ZCCRelnL -6cs4Ggo4/j/bd5QhogdiJYHUlEDqUL+a0QARAQABiQIfBBgBAgAJBQJUeiRaAhsM -AAoJEN9g3PR+HyAled0P/0J9gp48UOSWmkoMOPbGCIyABYMmaoDKdYYf1rToP3wp -O2nOwG48ZFW9Q4r6LAiOmPjPtMsvjtFeHDQ5FjnXpbFI2NBn3YwB2fulim8TZL03 -SvpiZD7TUiZKmUAOmVPoZJ+GUIE9lJtBrlOS5n0TkhmS14G3xPlex7jdJ63JFmME -XZ9gDcgUOzG7pSneCYyHOLKGwTmLV3HXUSIAm/8bW2xJ7g+j9qr/c78D8ThUY+I0 -0edCq+tL5rpnPYIusI3lzh4xeSMSSVCKB+Fhz9DFdD6pZC6E6KWlaoUgw1DdvfFC -KFrEhGFPu80Y7zl77nME9Yg9JYrKlISZHtbT8mDduOXlJIyZxsIlg/bDhsN38HOE -3ZoAsJh/8Ui44b58x/u4P9uKDroCua/6sOb0JFuxPNZHc7Sjdy1S0md7YEW3vFyT -1H1XzRAOPLwJFoz4ymRz9COHTyzExycr/TIjoBG7v1nYOGUdqaTNU2/802LRQaE2 -eUftQWTTiFoES4Z0vTKmKwq3CoP80Z5zTrcQf8CdMmTd9bu9kE3AvrK6OD0amxKw -LNHuuVgP/KuG0U4M8A641mUjCt0ZvtDCcAgO90cQKdHsuiCkX/wFYGg+lCzwjtRZ -UZSWZtUmAO12vjmUwGtRbp5xfdbV+PmIBRYe0iikrykoBy+FLw9yHlSCoey2ih6W -=r/yh ------END PGP PUBLIC KEY BLOCK-----`, "\n"), "\\n") + `" - }`, - } - - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.KeysCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "A new key has been successfully inserted") - So(response.Success, ShouldBeTrue) - So(response.Key.ID, ShouldNotBeNil) - - key := response.Key - - Convey("Key should be visible on the user's key list", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/keys?user=johnorange4", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.KeysListResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(len(*response.Keys), ShouldBeGreaterThan, 0) - }) - - Convey("Getting that key should succeed", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/keys/" + key.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.KeysGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(response.Key.ID, ShouldEqual, key.ID) - }) - }) - - Convey("Listing keys without passing a username should fail", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/keys", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.KeysListResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid username") - }) - - Convey("Getting a non-existing key should fail", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/keys/123", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.KeysGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Requested key does not exist on our server") - }) - }) -} diff --git a/routes/labels_test.go_ b/routes/labels_test.go_ deleted file mode 100644 index b5ade9a..0000000 --- a/routes/labels_test.go_ +++ /dev/null @@ -1,314 +0,0 @@ -package routes_test - -import ( - "testing" - - "github.com/dchest/uniuri" - "github.com/franela/goreq" - . "github.com/smartystreets/goconvey/convey" - - "github.com/lavab/api/env" - "github.com/lavab/api/models" - "github.com/lavab/api/routes" -) - -func TestLabelsRoute(t *testing.T) { - Convey("Given a working account", t, func() { - account := &models.Account{ - Resource: models.MakeResource("", "johnorange2"), - Status: "complete", - AltEmail: "john2@orange.org", - } - err := account.SetPassword("fruityloops") - So(err, ShouldBeNil) - - err = env.Accounts.Insert(account) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/tokens", - ContentType: "application/json", - Body: `{ - "type": "auth", - "username": "johnorange2", - "password": "fruityloops" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.TokensCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - authToken := response.Token - - Convey("Creating a new label using invalid input should fail", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/labels", - ContentType: "application/json", - Body: "!@#!@!@#", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid input format") - So(response.Success, ShouldBeFalse) - }) - - Convey("Updating a label using invalid input should fail", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/labels/anything", - ContentType: "application/json", - Body: "!@#!@!@#", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid input format") - So(response.Success, ShouldBeFalse) - }) - - Convey("Creating a new label without enough information should fail", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/labels", - ContentType: "application/json", - Body: "{}", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Invalid request") - So(response.Success, ShouldBeFalse) - }) - - Convey("Getting a non-existing label should fail", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/labels/nonexisting", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Label not found") - So(response.Success, ShouldBeFalse) - }) - - Convey("Updating a non-existing label should fail", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/labels/anything", - ContentType: "application/json", - Body: "{}", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Label not found") - So(response.Success, ShouldBeFalse) - }) - - Convey("Deleting a non-existing label should fail", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/labels/anything", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Label not found") - So(response.Success, ShouldBeFalse) - }) - - Convey("Getting a non-owned label should fail", func() { - label := &models.Label{ - Resource: models.MakeResource("not", uniuri.New()), - Builtin: false, - } - - err := env.Labels.Insert(label) - So(err, ShouldBeNil) - - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/labels/" + label.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Label not found") - So(response.Success, ShouldBeFalse) - - Convey("Updating it should fail", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/labels/" + label.ID, - ContentType: "application/json", - Body: "{}", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Label not found") - So(response.Success, ShouldBeFalse) - }) - - Convey("Deleting it should fail", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/labels/" + label.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Label not found") - So(response.Success, ShouldBeFalse) - }) - }) - - Convey("Creating a label should succeed", func() { - request := goreq.Request{ - Method: "POST", - Uri: server.URL + "/labels", - ContentType: "application/json", - Body: `{ - "name": "` + uniuri.New() + `" - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Label.Name, ShouldNotBeEmpty) - So(response.Success, ShouldBeTrue) - - label := response.Label - - Convey("That label should be visible on the labels list", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/labels", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsListResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldBeEmpty) - So(len(*response.Labels), ShouldBeGreaterThan, 0) - So(response.Success, ShouldBeTrue) - }) - - Convey("Getting that label should succeed", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/labels/" + label.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Label.ID, ShouldEqual, label.ID) - So(response.Success, ShouldBeTrue) - }) - - Convey("Updating that label should succeed", func() { - request := goreq.Request{ - Method: "PUT", - Uri: server.URL + "/labels/" + label.ID, - ContentType: "application/json", - Body: `{ - "name": "test123" - }`, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsUpdateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Label.Name, ShouldEqual, "test123") - So(response.Success, ShouldBeTrue) - }) - - Convey("Deleting that label should succeed", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/labels/" + label.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.LabelsDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Message, ShouldEqual, "Label successfully removed") - So(response.Success, ShouldBeTrue) - }) - }) - }) -} diff --git a/routes/middleware_test.go_ b/routes/middleware_test.go_ deleted file mode 100644 index d398349..0000000 --- a/routes/middleware_test.go_ +++ /dev/null @@ -1,105 +0,0 @@ -package routes_test - -import ( - "testing" - "time" - - "github.com/franela/goreq" - . "github.com/smartystreets/goconvey/convey" - - "github.com/lavab/api/env" - "github.com/lavab/api/models" - "github.com/lavab/api/routes" -) - -func TestMiddleware(t *testing.T) { - Convey("While querying a secure endpoint", t, func() { - Convey("No header should fail", func() { - result, err := goreq.Request{ - Method: "GET", - Uri: server.URL + "/accounts/me", - }.Do() - So(err, ShouldBeNil) - - var response routes.AuthMiddlewareResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Missing auth token") - }) - - Convey("An invalid header should fail", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/accounts/me", - } - request.AddHeader("Authorization", "123") - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AuthMiddlewareResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid authorization header") - }) - - Convey("Invalid token should fail", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/accounts/me", - } - request.AddHeader("Authorization", "Bearer 123") - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AuthMiddlewareResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid authorization token") - }) - - Convey("Expired token should fail", func() { - account := &models.Account{ - Resource: models.MakeResource("", "johnorange"), - Status: "complete", - AltEmail: "john@orange.org", - } - err := account.SetPassword("fruityloops") - So(err, ShouldBeNil) - - err = env.Accounts.Insert(account) - So(err, ShouldBeNil) - - token := models.Token{ - Resource: models.MakeResource(account.ID, "test invite token"), - Expiring: models.Expiring{ - ExpiryDate: time.Now().UTC().Truncate(time.Hour * 8), - }, - Type: "auth", - } - - err = env.Tokens.Insert(token) - So(err, ShouldBeNil) - - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/accounts/me", - } - request.AddHeader("Authorization", "Bearer "+token.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.AuthMiddlewareResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Authorization token has expired") - }) - }) -} diff --git a/routes/tokens_test.go_ b/routes/tokens_test.go_ deleted file mode 100644 index fd32b0b..0000000 --- a/routes/tokens_test.go_ +++ /dev/null @@ -1,193 +0,0 @@ -package routes_test - -import ( - "testing" - "time" - - "github.com/franela/goreq" - . "github.com/smartystreets/goconvey/convey" - - "github.com/lavab/api/env" - "github.com/lavab/api/models" - "github.com/lavab/api/routes" -) - -func TestTokensRoute(t *testing.T) { - Convey("Given a working account", t, func() { - account := &models.Account{ - Resource: models.MakeResource("", "johnorange5"), - Status: "complete", - AltEmail: "john5@orange.org", - } - err := account.SetPassword("fruityloops") - So(err, ShouldBeNil) - - err = env.Accounts.Insert(account) - So(err, ShouldBeNil) - - result, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/tokens", - ContentType: "application/json", - Body: `{ - "type": "auth", - "username": "johnorange5", - "password": "fruityloops" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.TokensCreateResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - authToken := response.Token - - Convey("Creating a non-auth token should fail", func() { - request, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/tokens", - ContentType: "application/json", - Body: `{ - "type": "not-auth" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.TokensCreateResponse - err = request.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Only auth tokens are implemented") - }) - - Convey("Trying to sign in using wrong username should fail", func() { - request, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/tokens", - ContentType: "application/json", - Body: `{ - "type": "auth", - "username": "not-johnorange5", - "password": "fruityloops" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.TokensCreateResponse - err = request.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Wrong username or password") - }) - - Convey("Trying to sign in using wrong password should fail", func() { - request, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/tokens", - ContentType: "application/json", - Body: `{ - "type": "auth", - "username": "johnorange5", - "password": "not-fruityloops" - }`, - }.Do() - So(err, ShouldBeNil) - - var response routes.TokensCreateResponse - err = request.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Wrong username or password") - }) - - Convey("Trying to sign in using an invalid JSON input should fail", func() { - request, err := goreq.Request{ - Method: "POST", - Uri: server.URL + "/tokens", - ContentType: "application/json", - Body: "123123123###434$#$", - }.Do() - So(err, ShouldBeNil) - - var response routes.TokensCreateResponse - err = request.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid input format") - }) - - Convey("Getting the currently used token should succeed", func() { - request := goreq.Request{ - Method: "GET", - Uri: server.URL + "/tokens", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.TokensGetResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(response.Token.ExpiryDate.After(time.Now().UTC()), ShouldBeTrue) - }) - - Convey("Deleting the token by ID should succeed", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/tokens/" + authToken.ID, - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.TokensDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(response.Message, ShouldEqual, "Successfully logged out") - }) - - Convey("Deleting a non-existing token should fail", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/tokens/123", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.TokensDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeFalse) - So(response.Message, ShouldEqual, "Invalid token ID") - }) - - Convey("Deleting current token should succeed", func() { - request := goreq.Request{ - Method: "DELETE", - Uri: server.URL + "/tokens", - } - request.AddHeader("Authorization", "Bearer "+authToken.ID) - result, err := request.Do() - So(err, ShouldBeNil) - - var response routes.TokensDeleteResponse - err = result.Body.FromJsonTo(&response) - So(err, ShouldBeNil) - - So(response.Success, ShouldBeTrue) - So(response.Message, ShouldEqual, "Successfully logged out") - }) - }) -} diff --git a/utils/errors.go b/utils/errors.go index 0c1bea4..a9cd369 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -118,3 +118,45 @@ const ( AccountsStartOnboardingMisconfigured AccountsStartOnboardingUnableToInit ) + +const ( + AddressesListUnknown = 11000 + iota + AddressesListUnableToGet +) + +const ( + AvatarsUnknown = 12000 + iota + AvatarsInvalidWidth +) + +const ( + ContactsListUnknown = 13000 + iota + ContactsListUnableToGet +) + +const ( + ContactsCreateUnknown = 13100 + iota + ContactsCreateInvalidInput + ContactsCreateUnableToInsert +) + +const ( + ContactsGetUnknown = 13200 + iota + ContactsGetUnableToGet + ContactsGetNotOwned +) + +const ( + ContactsUpdateUnknown = 13300 + iota + ContactsUpdateInvalidInput + ContactsUpdateUnableToGet + ContactsUpdateNotOwned + ContactsUpdateUnableToUpdate +) + +const ( + ContactsDeleteUnknown = 13400 + iota + ContactsDeleteUnableToGet + ContactsDeleteNotOwned + ContactsDeleteUnableToDelete +) From 50b650aa0d52ffa61abbb42a3044d38f83cc53c8 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 18:17:28 +0200 Subject: [PATCH 13/27] Removed commented out internal mailing code --- routes/emails.go | 251 ----------------------------------------------- utils/errors.go | 104 ++++++++++++++++++++ 2 files changed, 104 insertions(+), 251 deletions(-) diff --git a/routes/emails.go b/routes/emails.go index f743e69..264b4e3 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -437,32 +437,6 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { return } - // I'm going to whine at this part, as we are doubling the email sending code - - // Check if To contains lavaboom emails - /*for _, address := range email.To { - parts := strings.SplitN(address, "@", 2) - if parts[1] == env.Config.EmailDomain { - go sendEmail(parts[0], email) - } - } - - // Check if CC contains lavaboom emails - for _, address := range email.CC { - parts := strings.SplitN(address, "@", 2) - if parts[1] == env.Config.EmailDomain { - go sendEmail(parts[0], email) - } - } - - // Check if BCC contains lavaboom emails - for _, address := range email.BCC { - parts := strings.SplitN(address, "@", 2) - if parts[1] == env.Config.EmailDomain { - go sendEmail(parts[0], email) - } - }*/ - // Add a send request to the queue err = env.Producer.Publish("send_email", []byte(`"`+email.ID+`"`)) if err != nil { @@ -572,228 +546,3 @@ func EmailsDelete(c web.C, w http.ResponseWriter, r *http.Request) { Message: "Email successfully removed", }) } - -/*func sendEmail(account string, email *models.Email) { - // find recipient's account - recipient, err := env.Accounts.FindAccountByName(account) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "name": account, - }).Warn("Unable to fetch recipent's account") - return - } - - newEmail := *email - - // check if the email is unencrypted - if newEmail.Body.PGPFingerprints == nil || len(newEmail.Body.PGPFingerprints) == 0 { - // check if the acc has a pkey set - if recipient.PublicKey == "" { - env.Log.WithFields(logrus.Fields{ - "name": account, - }).Warn("Recipient has no public key set") - return - } - - // fetch the pkey - key, err := env.Keys.FindByFingerprint(recipient.PublicKey) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "fingerprint": recipient.PublicKey, - "name": account, - }).Warn("Recipient's public key does not exist") - return - } - - // parse the armored key - entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key.Key)) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "fingerprint": recipient.PublicKey, - }).Warn("Cannot parse an armored key") - return - } - - // first key should be the pkey - publicKey := entityList[0] - - // prepare a buffer for ciphertext and initialize openpgp - output := &bytes.Buffer{} - input, err := openpgp.Encrypt(output, []*openpgp.Entity{publicKey}, nil, nil, nil) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "fingerprint": recipient.PublicKey, - }).Warn("Cannot set up an OpenPGP encrypter") - return - } - - // write email's contents into input - _, err = input.Write([]byte(newEmail.Body.Data)) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "fingerprint": recipient.PublicKey, - }).Warn("Cannot write into the OpenPGP's input") - return - } - - // close the input - if err := input.Close(); err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "fingerprint": recipient.PublicKey, - }).Warn("Cannot close OpenPGP's input") - return - } - - // encode output into armor - armoredOutput := &bytes.Buffer{} - armoredInput, err := armor.Encode(armoredOutput, "PGP MESSAGE", map[string]string{ - "Version": "Lavaboom " + env.Config.APIVersion, - }) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Cannot initialize a new armor encoding") - return - } - - _, err = io.Copy(armoredInput, output) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to copy encrypted ciphertext into the armor processor") - return - } - - if err := armoredInput.Close(); err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "fingerprint": recipient.PublicKey, - }).Warn("Cannot close armoring's input") - return - } - - newEmail.Body.PGPFingerprints = []string{recipient.PublicKey} - newEmail.Body.Data = armoredOutput.String() - } - - // Get the "Inbox" label's ID - var inbox *models.Label - err = env.Labels.WhereAndFetchOne(map[string]interface{}{ - "name": "Inbox", - "builtin": true, - "owner": recipient.ID, - }, &inbox) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "id": recipient.ID, - "error": err.Error(), - }).Warn("Account has no inbox label") - return - } - - // strip prefixes from the subject - rawSubject := prefixesRegex.ReplaceAllString(newEmail.Name, "") - - emailResource := models.MakeResource(recipient.ID, newEmail.Name) - - var thread *models.Thread - err = env.Threads.WhereAndFetchOne(map[string]interface{}{ - "name": rawSubject, - "owner": recipient.ID, - }, &thread) - if err != nil { - thread = &models.Thread{ - Resource: models.MakeResource(recipient.ID, rawSubject), - Emails: []string{emailResource.ID}, - Labels: []string{inbox.ID}, - Members: append(append(newEmail.To, newEmail.CC...), newEmail.BCC...), - IsRead: false, - } - - err := env.Threads.Insert(thread) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to create a new thread") - return - } - } else { - existingMembers := make(map[string]struct{}) - - for _, member := range thread.Members { - existingMembers[member] = struct{}{} - } - - for _, member := range newEmail.To { - if _, ok := existingMembers[member]; !ok { - thread.Members = append(thread.Members, member) - existingMembers[member] = struct{}{} - } - } - - for _, member := range newEmail.CC { - if _, ok := existingMembers[member]; !ok { - thread.Members = append(thread.Members, member) - existingMembers[member] = struct{}{} - } - } - - for _, member := range newEmail.BCC { - if _, ok := existingMembers[member]; !ok { - thread.Members = append(thread.Members, member) - existingMembers[member] = struct{}{} - } - } - - err := env.Threads.UpdateID(thread.ID, thread) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "id": thread.ID, - "error": err.Error(), - }).Error("Unable to update an existing thread") - return - } - } - - // Insert the new email - newEmail.Resource = emailResource - newEmail.Status = "processed" - newEmail.Thread = thread.ID - - err = env.Emails.Insert(newEmail) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to create a new email") - return - } - - // Send notifications - err = env.Producer.Publish("email_delivery", map[string]interface{}{ - "id": email.ID, - "owner": email.Owner, - }) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "id": email.ID, - "error": err.Error(), - }).Error("Unable to publish a delivery message") - } - - err = env.NATS.Publish("email_receipt", map[string]interface{}{ - "id": newEmail.ID, - "owner": newEmail.Owner, - }) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "id": newEmail.ID, - "error": err.Error(), - }).Error("Unable to publish a receipt message") - } -}*/ diff --git a/utils/errors.go b/utils/errors.go index a9cd369..c481844 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -160,3 +160,107 @@ const ( ContactsDeleteNotOwned ContactsDeleteUnableToDelete ) + +const ( + EmailsListUnknown = 14000 + iota +) + +const ( + EmailsCreateUnknown = 14100 + iota +) + +const ( + EmailsGetUnknown = 14200 + iota +) + +const ( + EmailsDeleteUnknown = 14300 + iota +) + +const ( + FilesListUnknown = 15000 + iota +) + +const ( + FilesCreateUnknown = 15100 + iota +) + +const ( + FilesGetUnknown = 15200 + iota +) + +const ( + FilesUpdateUnknown = 15300 + iota +) + +const ( + FilesDeleteUnknown = 15400 + iota +) + +const ( + KeysListUnknown = 16000 + iota +) + +const ( + KeysCreateUnknown = 16100 + iota +) + +const ( + KeysGetUnknown = 16200 + iota +) + +const ( + KeysVoteUnknown = 16300 + iota +) + +const ( + LabelsListUnknown = 17000 + iota +) + +const ( + LabelsCreateUnknown = 17100 + iota +) + +const ( + LabelsGetUnknown = 17200 + iota +) + +const ( + LabelsUpdateUnknown = 17300 + iota +) + +const ( + LabelsDeleteUnknown = 17400 + iota +) + +const ( + MiddlewareUnknown = 18000 + iota +) + +const ( + ThreadsListUnknown = 19000 + iota +) + +const ( + ThreadsGetUnknown = 19100 + iota +) + +const ( + ThreadsUpdateUnknown = 19200 + iota +) + +const ( + ThreadsDeleteUnknown = 19300 + iota +) + +const ( + TokensGetUnknown = 20000 + iota +) + +const ( + TokensCreateUnknown = 20100 + iota +) + +const ( + TokensDeleteUnknown = 20200 + iota +) From 2c88c5af48121e774421eaf0b06bdd690947f3d3 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 19:06:13 +0200 Subject: [PATCH 14/27] Fixed a crash if a thread in your inbox is empty, new error handling for /emails --- db/table_threads.go | 4 +- routes/contacts.go | 4 +- routes/emails.go | 247 +++++++++++++++----------------------------- utils/errors.go | 21 ++++ 4 files changed, 109 insertions(+), 167 deletions(-) diff --git a/db/table_threads.go b/db/table_threads.go index 150291a..d17fb2d 100644 --- a/db/table_threads.go +++ b/db/table_threads.go @@ -140,7 +140,9 @@ func (t *ThreadsTable) List( }, gorethink.BetweenOpts{ Index: "threadAndDate", }).OrderBy(gorethink.OrderByOpts{Index: "threadAndDate"}). - Nth(0).Pluck("manifest")) + Nth(0).Default(map[string]interface{}{ + "interface": nil, + }).Pluck("manifest")) }) // Run the query diff --git a/routes/contacts.go b/routes/contacts.go index 4d6d531..9164f9b 100644 --- a/routes/contacts.go +++ b/routes/contacts.go @@ -254,8 +254,8 @@ func ContactsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Perform the deletion err = env.Contacts.DeleteID(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, utils.NewError( - utils.ContactsDeleteUnableToDelete, "You're not the owner of this contact", true, + utils.JSONResponse(w, 500, utils.NewError( + utils.ContactsDeleteUnableToDelete, err, true, )) return } diff --git a/routes/emails.go b/routes/emails.go index 264b4e3..2d4a77c 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -1,8 +1,6 @@ package routes import ( - //"bytes" - //"io" "crypto/sha256" "encoding/hex" "net/http" @@ -13,8 +11,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" - //"golang.org/x/crypto/openpgp" - //"golang.org/x/crypto/openpgp/armor" _ "golang.org/x/crypto/ripemd160" "github.com/lavab/api/env" @@ -51,15 +47,9 @@ func EmailsList(c web.C, w http.ResponseWriter, r *http.Request) { if offsetRaw != "" { o, err := strconv.Atoi(offsetRaw) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err, - "offset": offset, - }).Error("Invalid offset") - - utils.JSONResponse(w, 400, &EmailsListResponse{ - Success: false, - Message: "Invalid offset", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.EmailsListInvalidOffset, err, false, + )) return } offset = o @@ -68,15 +58,9 @@ func EmailsList(c web.C, w http.ResponseWriter, r *http.Request) { if limitRaw != "" { l, err := strconv.Atoi(limitRaw) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "limit": limit, - }).Error("Invalid limit") - - utils.JSONResponse(w, 400, &EmailsListResponse{ - Success: false, - Message: "Invalid limit", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.EmailsListInvalidLimit, err, false, + )) return } limit = l @@ -89,28 +73,18 @@ func EmailsList(c web.C, w http.ResponseWriter, r *http.Request) { // Get contacts from the database emails, err := env.Emails.List(session.Owner, sort, offset, limit, thread) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to fetch emails") - - utils.JSONResponse(w, 500, &EmailsListResponse{ - Success: false, - Message: "Internal error (code EM/LI/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.EmailsListUnableToGet, err, true, + )) return } if offsetRaw != "" || limitRaw != "" { count, err := env.Emails.CountOwnedBy(session.Owner) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to count emails") - - utils.JSONResponse(w, 500, &EmailsListResponse{ - Success: false, - Message: "Internal error (code EM/LI/02)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.EmailsListUnableToCount, err, true, + )) return } w.Header().Set("X-Total-Count", strconv.Itoa(count)) @@ -165,14 +139,9 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { var input EmailsCreateRequest err := utils.ParseRequest(r, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 400, &EmailsCreateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.EmailsCreateInvalidInput, err, false, + )) return } @@ -181,19 +150,17 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { // Ensure that the kind is valid if input.Kind != "raw" && input.Kind != "manifest" && input.Kind != "pgpmime" { - utils.JSONResponse(w, 400, &EmailsCreateResponse{ - Success: false, - Message: "Invalid email encryption kind", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.EmailsCreateInvalidInput, "Invalid email encryption kind", false, + )) return } // Ensure that there's at least one recipient and that there's body - if len(input.To) == 0 || input.Body == "" { - utils.JSONResponse(w, 400, &EmailsCreateResponse{ - Success: false, - Message: "Invalid email", - }) + if input.To == nil || len(input.To) == 0 || input.Body == "" { + utils.JSONResponse(w, 400, utils.NewError( + utils.EmailsCreateInvalidInput, "Invalid to field or empty body", false, + )) return } @@ -201,18 +168,16 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { // Check rights to files files, err := env.Files.GetFiles(input.Files...) if err != nil { - utils.JSONResponse(w, 500, &EmailsCreateResponse{ - Success: false, - Message: "Unable to fetch emails", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.EmailsCreateUnableToFetchFiles, err, false, + )) return } for _, file := range files { if file.Owner != session.Owner { - utils.JSONResponse(w, 403, &EmailsCreateResponse{ - Success: false, - Message: "You are not the owner of file " + file.ID, - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.EmailsCreateFileNotOwned, "You're not the owner of file "+file.ID, false, + )) return } } @@ -230,15 +195,9 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { account, err := env.Accounts.GetTokenOwner(c.Env["token"].(*models.Token)) if err != nil { // The session refers to a non-existing user - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - "error": err.Error(), - }).Warn("Valid session referred to a removed account") - - utils.JSONResponse(w, 410, &EmailsCreateResponse{ - Success: false, - Message: "Account disabled", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.EmailsCreateUnableToFetchAccount, err, true, + )) return } @@ -250,15 +209,9 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { "owner": account.ID, }, &label) if err != nil { - env.Log.WithFields(logrus.Fields{ - "id": account.ID, - "error": err.Error(), - }).Warn("Account has no sent label") - - utils.JSONResponse(w, 410, &EmailsCreateResponse{ - Success: false, - Message: "Misconfigured account", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.EmailsCreateUnableToFetchLabel, err, true, + )) return } @@ -266,10 +219,9 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { // Parse the from field from, err := mail.ParseAddress(input.From) if err != nil { - utils.JSONResponse(w, 400, &EmailsCreateResponse{ - Success: false, - Message: "Invalid email.From", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.EmailsCreateInvalidFromAddress, err, false, + )) return } @@ -278,27 +230,24 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { parts := strings.SplitN(from.Address, "@", 2) if parts[1] != env.Config.EmailDomain { - utils.JSONResponse(w, 400, &EmailsCreateResponse{ - Success: false, - Message: "Invalid email.From (invalid domain)", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.EmailsCreateInvalidFromAddress, "Invalid from domain", false, + )) return } address, err := env.Addresses.GetAddress(parts[0]) if err != nil { - utils.JSONResponse(w, 400, &EmailsCreateResponse{ - Success: false, - Message: "Invalid email.From (invalid username)", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.EmailsCreateInvalidFromAddress, err, false, + )) return } if address.Owner != account.ID { - utils.JSONResponse(w, 400, &EmailsCreateResponse{ - Success: false, - Message: "Invalid email.From (address not owned)", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.EmailsCreateUnableToParseAddress, "You're not the owner of that address.", false, + )) return } } @@ -326,15 +275,16 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { // todo: make it an actual exists check to reduce lan bandwidth thread, err := env.Threads.GetThread(input.Thread) if err != nil { - env.Log.WithFields(logrus.Fields{ - "id": input.Thread, - "error": err.Error(), - }).Warn("Cannot retrieve a thread") - - utils.JSONResponse(w, 400, &EmailsCreateResponse{ - Success: false, - Message: "Invalid thread", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.EmailsCreateUnableToFetchThread, err, false, + )) + return + } + + if thread.Owner != account.Owner { + utils.JSONResponse(w, 403, utils.NewError( + utils.EmailsCreateThreadNotOwned, "You're not the owner of this thread", false, + )) return } @@ -345,15 +295,9 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { if err := env.Threads.UpdateID(thread.ID, map[string]interface{}{ "secure": "some", }); err != nil { - env.Log.WithFields(logrus.Fields{ - "id": input.Thread, - "error": err.Error(), - }).Warn("Cannot update a thread") - - utils.JSONResponse(w, 400, &EmailsCreateResponse{ - Success: false, - Message: "Unable to update the thread", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.EmailsCreateUnableToUpdateThread, err, true, + )) return } } @@ -375,14 +319,9 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { err := env.Threads.Insert(thread) if err != nil { - utils.JSONResponse(w, 500, &EmailsCreateResponse{ - Success: false, - Message: "Unable to create a new thread", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to create a new thread") + utils.JSONResponse(w, 500, utils.NewError( + utils.EmailsCreateUnableToInsertThread, err, true, + )) return } @@ -426,28 +365,18 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { // Insert the email into the database if err := env.Emails.Insert(email); err != nil { - utils.JSONResponse(w, 500, &EmailsCreateResponse{ - Success: false, - Message: "internal server error - EM/CR/01", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Could not insert an email into the database") + utils.JSONResponse(w, 500, utils.NewError( + utils.EmailsCreateUnableToInsertEmail, err, true, + )) return } // Add a send request to the queue err = env.Producer.Publish("send_email", []byte(`"`+email.ID+`"`)) if err != nil { - utils.JSONResponse(w, 500, &EmailsCreateResponse{ - Success: false, - Message: "internal server error - EM/CR/03", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Could not publish an email send request") + utils.JSONResponse(w, 500, utils.NewError( + utils.EmailsCreateUnableToQueue, err, true, + )) return } @@ -469,10 +398,9 @@ func EmailsGet(c web.C, w http.ResponseWriter, r *http.Request) { // Get the email from the database email, err := env.Emails.GetEmail(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &EmailsGetResponse{ - Success: false, - Message: "Email not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.EmailsGetUnableToGet, err, false, + )) return } @@ -481,10 +409,9 @@ func EmailsGet(c web.C, w http.ResponseWriter, r *http.Request) { // Check for ownership if email.Owner != session.Owner { - utils.JSONResponse(w, 404, &EmailsGetResponse{ - Success: false, - Message: "Email not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.EmailsGetNotOwned, "You're not the owner of this email", false, + )) return } @@ -506,10 +433,9 @@ func EmailsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Get the email from the database email, err := env.Emails.GetEmail(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &EmailsDeleteResponse{ - Success: false, - Message: "Email not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.EmailsDeleteUnableToGet, err, false, + )) return } @@ -518,25 +444,18 @@ func EmailsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Check for ownership if email.Owner != session.Owner { - utils.JSONResponse(w, 404, &EmailsDeleteResponse{ - Success: false, - Message: "Email not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.EmailsDeleteNotOwned, "You're not the owner of this email", false, + )) return } // Perform the deletion err = env.Emails.DeleteID(c.URLParams["id"]) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": c.URLParams["id"], - }).Error("Unable to delete a email") - - utils.JSONResponse(w, 500, &EmailsDeleteResponse{ - Success: false, - Message: "Internal error (code EM/DE/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.EmailsDeleteUnableToDelete, err, false, + )) return } diff --git a/utils/errors.go b/utils/errors.go index c481844..39e9c81 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -163,18 +163,39 @@ const ( const ( EmailsListUnknown = 14000 + iota + EmailsListInvalidOffset + EmailsListInvalidLimit + EmailsListUnableToGet + EmailsListUnableToCount ) const ( EmailsCreateUnknown = 14100 + iota + EmailsCreateInvalidInput + EmailsCreateUnableToFetchFiles + EmailsCreateFileNotOwned + EmailsCreateUnableToFetchAccount + EmailsCreateUnableToFetchLabel + EmailsCreateInvalidFromAddress + EmailsCreateUnableToFetchThread + EmailsCreateThreadNotOwned + EmailsCreateUnableToUpdateThread + EmailsCreateUnableToInsertThread + EmailsCreateUnableToInsertEmail + EmailsCreateUnableToQueue ) const ( EmailsGetUnknown = 14200 + iota + EmailsGetUnableToGet + EmailsGetNotOwned ) const ( EmailsDeleteUnknown = 14300 + iota + EmailsDeleteUnableToGet + EmailsDeleteNotOwned + EmailsDeleteUnableToDelete ) const ( From 1ea559ad4b5bb11db39a4b9e63adcee1bd93b719 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 19:34:45 +0200 Subject: [PATCH 15/27] Exclude bodies in files and new error handling in files.go --- routes/emails.go | 3 +- routes/files.go | 163 ++++++++++++++++++----------------------------- utils/errors.go | 12 ++++ 3 files changed, 74 insertions(+), 104 deletions(-) diff --git a/routes/emails.go b/routes/emails.go index 2d4a77c..c2953d9 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -9,7 +9,6 @@ import ( "strconv" "strings" - "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" _ "golang.org/x/crypto/ripemd160" @@ -246,7 +245,7 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { if address.Owner != account.ID { utils.JSONResponse(w, 403, utils.NewError( - utils.EmailsCreateUnableToParseAddress, "You're not the owner of that address.", false, + utils.EmailsCreateInvalidFromAddress, "You're not the owner of that address.", false, )) return } diff --git a/routes/files.go b/routes/files.go index 715ed6f..79ddbf8 100644 --- a/routes/files.go +++ b/routes/files.go @@ -4,7 +4,6 @@ import ( "net/http" "strings" - "github.com/Sirupsen/logrus" r "github.com/dancannon/gorethink" "github.com/zenazn/goji/web" @@ -23,28 +22,16 @@ func FilesList(c web.C, w http.ResponseWriter, req *http.Request) { session := c.Env["token"].(*models.Token) var ( - query = req.URL.Query() - sTags = query.Get("tags") - result []*models.File + query = req.URL.Query() + sTags = query.Get("tags") + excludeBodies = query.Get("exclude_bodies") + result []*models.File ) + q := r.Table("files") + if sTags == "" { - cursor, err := r.Table("files").GetAllByIndex("owner", session.Owner).Run(env.Rethink) - if err != nil { - utils.JSONResponse(w, 500, &FilesListResponse{ - Success: false, - Message: "Internal error (code FI/LI/01)", - }) - return - } - defer cursor.Close() - if err := cursor.All(&result); err != nil { - utils.JSONResponse(w, 500, &FilesListResponse{ - Success: false, - Message: "Internal error (code FI/LI/02)", - }) - return - } + q = q.GetAllByIndex("owner", session.Owner) } else { tags := strings.Split(sTags, ",") ids := []interface{}{} @@ -54,22 +41,27 @@ func FilesList(c web.C, w http.ResponseWriter, req *http.Request) { tag, }) } - cursor, err := r.Table("files").GetAllByIndex("ownerTags", ids...).Run(env.Rethink) - if err != nil { - utils.JSONResponse(w, 500, &FilesListResponse{ - Success: false, - Message: "Internal error (code FI/LI/03)", - }) - return - } - defer cursor.Close() - if err := cursor.All(&result); err != nil { - utils.JSONResponse(w, 500, &FilesListResponse{ - Success: false, - Message: "Internal error (code FI/LI/04)", - }) - return - } + + q = q.GetAllByIndex("ownerTags", ids...) + } + + if excludeBodies == "true" { + q = q.Without("body") + } + + cursor, err := q.Run(env.Rethink) + if err != nil { + utils.JSONResponse(w, 500, utils.NewError( + utils.FilesListUnableToGet, err, false, + )) + return + } + defer cursor.Close() + if err := cursor.All(&result); err != nil { + utils.JSONResponse(w, 500, utils.NewError( + utils.FilesListUnableToGet, err, false, + )) + return } utils.JSONResponse(w, 200, &FilesListResponse{ @@ -97,14 +89,9 @@ func FilesCreate(c web.C, w http.ResponseWriter, req *http.Request) { var input FilesCreateRequest err := utils.ParseRequest(req, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 400, &FilesCreateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.FilesCreateInvalidInput, err, false, + )) return } @@ -121,14 +108,9 @@ func FilesCreate(c web.C, w http.ResponseWriter, req *http.Request) { // Insert the file into the database if err := env.Files.Insert(file); err != nil { - utils.JSONResponse(w, 500, &FilesCreateResponse{ - Success: false, - Message: "internal server error - FI/CR/01", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Could not insert a file into the database") + utils.JSONResponse(w, 500, utils.NewError( + utils.FilesCreateUnableToInsert, err, true, + )) return } @@ -151,10 +133,9 @@ func FilesGet(c web.C, w http.ResponseWriter, req *http.Request) { // Get the file from the database file, err := env.Files.GetFile(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &FilesGetResponse{ - Success: false, - Message: "File not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.FilesGetUnableToGet, err, false, + )) return } @@ -163,10 +144,9 @@ func FilesGet(c web.C, w http.ResponseWriter, req *http.Request) { // Check for ownership if file.Owner != session.Owner { - utils.JSONResponse(w, 404, &FilesGetResponse{ - Success: false, - Message: "File not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.FilesGetNotOwned, "You're not the owner of this file", false, + )) return } @@ -198,24 +178,18 @@ func FilesUpdate(c web.C, w http.ResponseWriter, req *http.Request) { var input FilesUpdateRequest err := utils.ParseRequest(req, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 400, &FilesUpdateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.FilesUpdateInvalidInput, err, false, + )) return } // Get the file from the database file, err := env.Files.GetFile(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &FilesUpdateResponse{ - Success: false, - Message: "File not found", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.FilesUpdateUnableToGet, err, false, + )) return } @@ -224,10 +198,9 @@ func FilesUpdate(c web.C, w http.ResponseWriter, req *http.Request) { // Check for ownership if file.Owner != session.Owner { - utils.JSONResponse(w, 404, &FilesUpdateResponse{ - Success: false, - Message: "File not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.FilesUpdateNotOwned, "You're not the owner of this file", false, + )) return } @@ -250,15 +223,9 @@ func FilesUpdate(c web.C, w http.ResponseWriter, req *http.Request) { // Perform the update err = env.Files.UpdateID(c.URLParams["id"], file) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": c.URLParams["id"], - }).Error("Unable to update a file") - - utils.JSONResponse(w, 500, &FilesUpdateResponse{ - Success: false, - Message: "Internal error (code FI/UP/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.FilesUpdateUnableToUpdate, err, true, + )) return } @@ -280,10 +247,9 @@ func FilesDelete(c web.C, w http.ResponseWriter, req *http.Request) { // Get the file from the database file, err := env.Files.GetFile(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &FilesDeleteResponse{ - Success: false, - Message: "File not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.FilesDeleteUnableToGet, err, false, + )) return } @@ -292,25 +258,18 @@ func FilesDelete(c web.C, w http.ResponseWriter, req *http.Request) { // Check for ownership if file.Owner != session.Owner { - utils.JSONResponse(w, 404, &FilesDeleteResponse{ - Success: false, - Message: "File not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.FilesDeleteNotOwned, "You're not the owner of this file", false, + )) return } // Perform the deletion err = env.Files.DeleteID(c.URLParams["id"]) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": c.URLParams["id"], - }).Error("Unable to delete a file") - - utils.JSONResponse(w, 500, &FilesDeleteResponse{ - Success: false, - Message: "Internal error (code FI/DE/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.FilesDeleteUnableToDelete, err, true, + )) return } diff --git a/utils/errors.go b/utils/errors.go index 39e9c81..afdf286 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -200,22 +200,34 @@ const ( const ( FilesListUnknown = 15000 + iota + FilesListUnableToGet ) const ( FilesCreateUnknown = 15100 + iota + FilesCreateInvalidInput + FilesCreateUnableToInsert ) const ( FilesGetUnknown = 15200 + iota + FilesGetUnableToGet + FilesGetNotOwned ) const ( FilesUpdateUnknown = 15300 + iota + FilesUpdateInvalidInput + FilesUpdateUnableToGet + FilesUpdateNotOwned + FilesUpdateUnableToUpdate ) const ( FilesDeleteUnknown = 15400 + iota + FilesDeleteUnableToGet + FilesDeleteNotOwned + FilesDeleteUnableToDelete ) const ( From d1d1028a2a7acaab04b91130483fe9e5f05d440a Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 19:49:23 +0200 Subject: [PATCH 16/27] keys.go: New error handling --- routes/keys.go | 152 ++++++++++++++---------------------------------- utils/errors.go | 12 ++++ 2 files changed, 57 insertions(+), 107 deletions(-) diff --git a/routes/keys.go b/routes/keys.go index cfb6048..8363bb0 100644 --- a/routes/keys.go +++ b/routes/keys.go @@ -6,7 +6,6 @@ import ( "net/http" "strings" - "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/armor" @@ -28,10 +27,9 @@ func KeysList(w http.ResponseWriter, r *http.Request) { // Get the username from the GET query user := r.URL.Query().Get("user") if user == "" { - utils.JSONResponse(w, 409, &KeysListResponse{ - Success: false, - Message: "Invalid username", - }) + utils.JSONResponse(w, 409, utils.NewError( + utils.KeysListInvalidUsername, "Invalid username", false, + )) return } @@ -39,20 +37,18 @@ func KeysList(w http.ResponseWriter, r *http.Request) { address, err := env.Addresses.GetAddress(user) if err != nil { - utils.JSONResponse(w, 409, &KeysListResponse{ - Success: false, - Message: "Invalid address", - }) + utils.JSONResponse(w, 409, utils.NewError( + utils.KeysListUnableToFetchAddress, err, false, + )) return } // Find all keys owner by user keys, err := env.Keys.FindByOwner(address.Owner) if err != nil { - utils.JSONResponse(w, 500, &KeysListResponse{ - Success: false, - Message: "Internal server error (KE//LI/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.KeysListUnableToFetchKeys, err, true, + )) return } @@ -81,14 +77,9 @@ func KeysCreate(c web.C, w http.ResponseWriter, r *http.Request) { var input KeysCreateRequest err := utils.ParseRequest(r, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 409, &KeysCreateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 409, utils.NewError( + utils.KeysCreateInvalidInput, err, false, + )) return } @@ -98,45 +89,27 @@ func KeysCreate(c web.C, w http.ResponseWriter, r *http.Request) { // Parse the armored key entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(input.Key)) if err != nil { - utils.JSONResponse(w, 409, &KeysCreateResponse{ - Success: false, - Message: "Invalid key format", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "list": entityList, - }).Warn("Cannot parse an armored key") + utils.JSONResponse(w, 409, utils.NewError( + utils.KeysCreateInvalidFormat, err, false, + )) return } // Parse using armor pkg block, err := armor.Decode(strings.NewReader(input.Key)) if err != nil { - utils.JSONResponse(w, 409, &KeysCreateResponse{ - Success: false, - Message: "Invalid key format", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "list": entityList, - }).Warn("Cannot parse an armored key #2") + utils.JSONResponse(w, 409, utils.NewError( + utils.KeysCreateInvalidFormat, err, false, + )) return } // Get the account from db account, err := env.Accounts.GetAccount(session.Owner) if err != nil { - utils.JSONResponse(w, 500, &KeysCreateResponse{ - Success: false, - Message: "Internal server error - KE/CR/01", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": session.Owner, - }).Error("Cannot fetch user from database") + utils.JSONResponse(w, 500, utils.NewError( + utils.KeysCreateUnableToFetchAccount, err, true, + )) return } @@ -174,14 +147,9 @@ func KeysCreate(c web.C, w http.ResponseWriter, r *http.Request) { // Try to insert it into the database if err := env.Keys.Insert(key); err != nil { - utils.JSONResponse(w, 500, &KeysCreateResponse{ - Success: false, - Message: "Internal server error - KE/CR/02", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Could not insert a key to the database") + utils.JSONResponse(w, 500, utils.NewError( + utils.KeysCreateUnableToInsert, err, true, + )) return } @@ -221,30 +189,18 @@ func KeysGet(c web.C, w http.ResponseWriter, r *http.Request) { // Resolve address address, err := env.Addresses.GetAddress(username) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "name": username, - }).Warn("Unable to fetch the requested address from the database") - - utils.JSONResponse(w, 404, &KeysGetResponse{ - Success: false, - Message: "No such address", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.KeysGetUnableToFetchAddress, err, false, + )) return } // Get its owner account, err := env.Accounts.GetAccount(address.Owner) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "name": username, - }).Warn("Unable to fetch the requested account from the database") - - utils.JSONResponse(w, 404, &KeysGetResponse{ - Success: false, - Message: "No such account", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.KeysGetUnableToFetchAccount, err, true, + )) return } @@ -253,14 +209,9 @@ func KeysGet(c web.C, w http.ResponseWriter, r *http.Request) { // Fetch the requested key from the database key2, err := env.Keys.FindByFingerprint(account.PublicKey) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to fetch the requested key from the database") - - utils.JSONResponse(w, 500, &KeysGetResponse{ - Success: false, - Message: "Invalid user public key ID", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.KeysGetUnableToFetchKeyByFingerprint, err, true, + )) return } @@ -268,23 +219,16 @@ func KeysGet(c web.C, w http.ResponseWriter, r *http.Request) { } else { keys, err := env.Keys.FindByOwner(account.ID) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "owner": account.ID, - }).Warn("Unable to fetch user's keys from the database") - - utils.JSONResponse(w, 500, &KeysGetResponse{ - Success: false, - Message: "Cannot find keys assigned to the account", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.KeysGetUnableToFetchKeysByOwner, err, true, + )) return } if len(keys) == 0 { - utils.JSONResponse(w, 500, &KeysGetResponse{ - Success: false, - Message: "Account has no keys assigned to itself", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.KeysGetAccountHasNoKeys, "This account has no keys", false, + )) return } @@ -295,14 +239,9 @@ func KeysGet(c web.C, w http.ResponseWriter, r *http.Request) { // Fetch the requested key from the database key2, err := env.Keys.FindByFingerprint(id) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to fetch the requested key from the database") - - utils.JSONResponse(w, 404, &KeysGetResponse{ - Success: false, - Message: "Requested key does not exist on our server", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.KeysGetUnableToFetchKeyByFingerprint, err, false, + )) return } @@ -324,8 +263,7 @@ type KeysVoteResponse struct { // KeysVote does *something* - TODO func KeysVote(w http.ResponseWriter, r *http.Request) { - utils.JSONResponse(w, 501, &KeysVoteResponse{ - Success: false, - Message: "Sorry, not implemented yet", - }) + utils.JSONResponse(w, 501, utils.NewError( + utils.KeysVoteUnknown, "Sorry, not implemented yet", false, + )) } diff --git a/utils/errors.go b/utils/errors.go index afdf286..53a64f1 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -232,14 +232,26 @@ const ( const ( KeysListUnknown = 16000 + iota + KeysListInvalidUsername + KeysListUnableToFetchAddress + KeysListUnableToFetchKeys ) const ( KeysCreateUnknown = 16100 + iota + KeysCreateInvalidInput + KeysCreateInvalidFormat + KeysCreateUnableToFetchAccount + KeysCreateUnableToInsert ) const ( KeysGetUnknown = 16200 + iota + KeysGetUnableToFetchAddress + KeysGetUnableToFetchAccount + KeysGetUnableToFetchKeyByFingerprint + KeysGetUnableToFetchKeysByOwner + KeysGetAccountHasNoKeys ) const ( From 1be64c338ca2bdc8c7ce9db067b2c73e321d2f68 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 21:22:27 +0200 Subject: [PATCH 17/27] labels.go: New error handling, optimized queries. Fixes #145 --- models/label.go | 4 +- routes/labels.go | 291 +++++++++++++++++++++-------------------------- utils/errors.go | 15 +++ 3 files changed, 146 insertions(+), 164 deletions(-) diff --git a/models/label.go b/models/label.go index cf19fc6..e0e96d2 100644 --- a/models/label.go +++ b/models/label.go @@ -16,6 +16,6 @@ type Label struct { // Examples: inbox, trash, spam, drafts, starred, etc. Builtin bool `json:"builtin" gorethink:"builtin"` - UnreadThreadsCount int `json:"unread_threads_count" gorethink:"unread_threads_count"` - TotalThreadsCount int `json:"total_threads_count" gorethink:"total_threads_count"` + UnreadThreadsCount int `json:"unread_threads_count" gorethink:"unread_threads_count,omitempty"` + TotalThreadsCount int `json:"total_threads_count" gorethink:"total_threads_count,omitempty"` } diff --git a/routes/labels.go b/routes/labels.go index 2de8862..2582a5e 100644 --- a/routes/labels.go +++ b/routes/labels.go @@ -3,7 +3,6 @@ package routes import ( "net/http" - "github.com/Sirupsen/logrus" r "github.com/dancannon/gorethink" "github.com/lavab/api/env" "github.com/lavab/api/models" @@ -34,83 +33,62 @@ func LabelsList(c web.C, w http.ResponseWriter, req *http.Request) { "Sent", session.Owner, true, + }).Map(func(row r.Term) r.Term { + return row.Field("id") }).Run(env.Rethink) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to get account's specified labels") - - utils.JSONResponse(w, 500, &LabelsListResponse{ - Success: false, - Message: err.Error(), - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.LabelsListUnableToFetchBuiltinLabels, err, true, + )) return } defer cursor.Close() - var spamTrashSent []*models.Label + var spamTrashSent []string if err := cursor.All(&spamTrashSent); err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to unmarshal account's specified labels") - - utils.JSONResponse(w, 500, &LabelsListResponse{ - Success: false, - Message: err.Error(), - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.LabelsListUnableToFetchBuiltinLabels, err, true, + )) return } if len(spamTrashSent) != 3 { - env.Log.WithFields(logrus.Fields{ - "count": len(spamTrashSent), - "account": session.Owner, - }).Error("Invalid count of Trash, Sent and Spam labels") - - utils.JSONResponse(w, 500, &LabelsListResponse{ - Success: false, - Message: "Misconfigured account", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.LabelsListInvalidBuiltinLabels, "Account's builtin labels are missing", true, + )) return } cursor, err = env.Labels.GetTable().GetAllByIndex("owner", session.Owner).Map(func(label r.Term) r.Term { - return label.Merge(map[string]interface{}{ - "total_threads_count": env.Threads.GetTable().GetAllByIndex("labels", label.Field("id")).Count(), - "unread_threads_count": env.Threads.GetTable().GetAllByIndex("labels", label.Field("id")).Filter(func(thread r.Term) r.Term { - return r.Not(thread.Field("is_read")).And( - r.Not( - thread.Field("labels").Contains(spamTrashSent[0].ID).Or( - thread.Field("labels").Contains(spamTrashSent[1].ID).Or( - thread.Field("labels").Contains(spamTrashSent[2].ID), - ), - ), - ), - ) - }).Count(), + return env.Threads.GetTable(). + GetAllByIndex("labels", label.Field("id")). + CoerceTo("array"). + Do(func(threads r.Term) r.Term { + return label.Merge(map[string]interface{}{ + "total_threads_count": threads.Count(), + "unread_threads_count": threads.Filter(func(thread r.Term) r.Term { + return thread.Field("is_read").Not().And( + thread.Field("labels").Map(func(label r.Term) r.Term { + return r.Expr(spamTrashSent).Contains(label) + }).Reduce(func(left r.Term, right r.Term) r.Term { + return left.Or(right) + }).Not(), + ) + }).Count(), + }) }) }).Run(env.Rethink) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to get account's all labels") - - utils.JSONResponse(w, 500, &LabelsListResponse{ - Success: false, - Message: err.Error(), - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.LabelsListUnableToFetchAllLabels, err, true, + )) return } defer cursor.Close() var labels []*models.Label if err := cursor.All(&labels); err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to unmarshal account's labels") - - utils.JSONResponse(w, 500, &LabelsListResponse{ - Success: false, - Message: err.Error(), - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.LabelsListUnableToFetchAllLabels, err, true, + )) return } @@ -137,14 +115,9 @@ func LabelsCreate(c web.C, w http.ResponseWriter, req *http.Request) { var input LabelsCreateRequest err := utils.ParseRequest(req, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 400, &LabelsCreateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.LabelsCreateInvalidInput, err, false, + )) return } @@ -153,18 +126,16 @@ func LabelsCreate(c web.C, w http.ResponseWriter, req *http.Request) { // Ensure that the input data isn't empty if input.Name == "" { - utils.JSONResponse(w, 400, &LabelsCreateResponse{ - Success: false, - Message: "Invalid request", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.LabelsCreateInvalidInput, "Name is empty", false, + )) return } if _, err := env.Labels.GetLabelByNameAndOwner(session.Owner, input.Name); err == nil { - utils.JSONResponse(w, 409, &LabelsCreateResponse{ - Success: false, - Message: "Label with such name already exists", - }) + utils.JSONResponse(w, 409, utils.NewError( + utils.LabelsCreateAlreadyExists, "A label with such name already exists", false, + )) return } @@ -176,14 +147,9 @@ func LabelsCreate(c web.C, w http.ResponseWriter, req *http.Request) { // Insert the label into the database if err := env.Labels.Insert(label); err != nil { - utils.JSONResponse(w, 500, &LabelsCreateResponse{ - Success: false, - Message: "internal server error - LA/CR/01", - }) - - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Could not insert a label into the database") + utils.JSONResponse(w, 400, utils.NewError( + utils.LabelsCreateUnableToInsert, err, true, + )) return } @@ -202,58 +168,80 @@ type LabelsGetResponse struct { // LabelsGet does *something* - TODO func LabelsGet(c web.C, w http.ResponseWriter, req *http.Request) { - // Get the label from the database - label, err := env.Labels.GetLabel(c.URLParams["id"]) + session := c.Env["token"].(*models.Token) + + // Fetch spam, trash and id + cursor, err := env.Labels.GetTable().GetAllByIndex("nameOwnerBuiltin", []interface{}{ + "Spam", + session.Owner, + true, + }, []interface{}{ + "Trash", + session.Owner, + true, + }, []interface{}{ + "Sent", + session.Owner, + true, + }).Map(func(row r.Term) r.Term { + return row.Field("id") + }).Run(env.Rethink) if err != nil { - utils.JSONResponse(w, 404, &LabelsGetResponse{ - Success: false, - Message: "Label not found", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.LabelsListUnableToFetchBuiltinLabels, err, true, + )) return } - - // Fetch the current session from the middleware - session := c.Env["token"].(*models.Token) - - // Check for ownership - if label.Owner != session.Owner { - utils.JSONResponse(w, 404, &LabelsGetResponse{ - Success: false, - Message: "Label not found", - }) + defer cursor.Close() + var spamTrashSent []string + if err := cursor.All(&spamTrashSent); err != nil { + utils.JSONResponse(w, 500, utils.NewError( + utils.LabelsListUnableToFetchBuiltinLabels, err, true, + )) return } - totalCount, err := env.Threads.CountByLabel(label.ID) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "label": label.ID, - }).Error("Unable to fetch total threads count") - - utils.JSONResponse(w, 500, &LabelsListResponse{ - Success: false, - Message: "Internal error (code LA/GE/01)", + // Fetch the label + cursor, err = env.Labels.GetTable().Get(c.URLParams["id"]).Do(func(label r.Term) r.Term { + return env.Threads.GetTable(). + GetAllByIndex("labels", label.Field("id")). + CoerceTo("array"). + Do(func(threads r.Term) r.Term { + return label.Merge(map[string]interface{}{ + "total_threads_count": threads.Count(), + "unread_threads_count": threads.Filter(func(thread r.Term) r.Term { + return thread.Field("is_read").Not().And( + thread.Field("labels").Map(func(label r.Term) r.Term { + return r.Expr(spamTrashSent).Contains(label) + }).Reduce(func(left r.Term, right r.Term) r.Term { + return left.Or(right) + }).Not(), + ) + }).Count(), + }) }) + }).Run(env.Rethink) + if err != nil { + utils.JSONResponse(w, 404, utils.NewError( + utils.LabelsGetUnableToGet, err, true, + )) return } - - unreadCount, err := env.Threads.CountByLabelUnread(label.ID) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "label": label.ID, - }).Error("Unable to fetch unread threads count") - - utils.JSONResponse(w, 500, &LabelsListResponse{ - Success: false, - Message: "Internal error (code LA/GE/01)", - }) + var label *models.Label + if err := cursor.One(&label); err != nil { + utils.JSONResponse(w, 404, utils.NewError( + utils.LabelsGetUnableToGet, err, true, + )) return } - label.TotalThreadsCount = totalCount - label.UnreadThreadsCount = unreadCount + // Check for ownership + if label.Owner != session.Owner { + utils.JSONResponse(w, 403, utils.NewError( + utils.LabelsGetNotOwned, "You're not the owner of this label", false, + )) + return + } // Write the label to the response utils.JSONResponse(w, 200, &LabelsGetResponse{ @@ -279,24 +267,18 @@ func LabelsUpdate(c web.C, w http.ResponseWriter, req *http.Request) { var input LabelsUpdateRequest err := utils.ParseRequest(req, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 400, &LabelsUpdateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.LabelsUpdateInvalidInput, err, false, + )) return } // Get the label from the database label, err := env.Labels.GetLabel(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &LabelsUpdateResponse{ - Success: false, - Message: "Label not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.LabelsUpdateUnableToGet, err, false, + )) return } @@ -305,10 +287,9 @@ func LabelsUpdate(c web.C, w http.ResponseWriter, req *http.Request) { // Check for ownership if label.Owner != session.Owner { - utils.JSONResponse(w, 404, &LabelsUpdateResponse{ - Success: false, - Message: "Label not found", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.LabelsUpdateUnableToGet, "You're not the owner of this label", false, + )) return } @@ -319,15 +300,9 @@ func LabelsUpdate(c web.C, w http.ResponseWriter, req *http.Request) { // Perform the update err = env.Labels.UpdateID(c.URLParams["id"], label) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": c.URLParams["id"], - }).Error("Unable to update a contact") - - utils.JSONResponse(w, 500, &LabelsUpdateResponse{ - Success: false, - Message: "Internal error (code LA/UP/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.LabelsUpdateUnableToUpdate, err, true, + )) return } @@ -349,10 +324,9 @@ func LabelsDelete(c web.C, w http.ResponseWriter, req *http.Request) { // Get the label from the database label, err := env.Labels.GetLabel(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &LabelsDeleteResponse{ - Success: false, - Message: "Label not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.LabelsDeleteUnableToGet, err, false, + )) return } @@ -361,25 +335,18 @@ func LabelsDelete(c web.C, w http.ResponseWriter, req *http.Request) { // Check for ownership if label.Owner != session.Owner { - utils.JSONResponse(w, 404, &LabelsDeleteResponse{ - Success: false, - Message: "Label not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.LabelsDeleteNotOwned, "You're not the owner of this label", false, + )) return } // Perform the deletion err = env.Labels.DeleteID(c.URLParams["id"]) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": c.URLParams["id"], - }).Error("Unable to delete a label") - - utils.JSONResponse(w, 500, &LabelsDeleteResponse{ - Success: false, - Message: "Internal error (code LA/DE/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.LabelsDeleteUnableToDelete, err, true, + )) return } diff --git a/utils/errors.go b/utils/errors.go index 53a64f1..e93e117 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -260,22 +260,37 @@ const ( const ( LabelsListUnknown = 17000 + iota + LabelsListUnableToFetchBuiltinLabels + LabelsListInvalidBuiltinLabels + LabelsListUnableToFetchAllLabels ) const ( LabelsCreateUnknown = 17100 + iota + LabelsCreateInvalidInput + LabelsCreateAlreadyExists + LabelsCreateUnableToInsert ) const ( LabelsGetUnknown = 17200 + iota + LabelsGetUnableToGet + LabelsGetNotOwned ) const ( LabelsUpdateUnknown = 17300 + iota + LabelsUpdateInvalidInput + LabelsUpdateUnableToGet + LabelsUpdateNotOwned + LabelsUpdateUnableToUpdate ) const ( LabelsDeleteUnknown = 17400 + iota + LabelsDeleteUnableToGet + LabelsDeleteNotOwned + LabelsDeleteUnableToDelete ) const ( From da49c5da10eae23a1bc7b782b498da900dcc7308 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 21:28:19 +0200 Subject: [PATCH 18/27] middleware.go: New error handling --- routes/middleware.go | 33 ++++++++++++--------------------- utils/errors.go | 4 ++++ 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/routes/middleware.go b/routes/middleware.go index c923c6e..900a016 100644 --- a/routes/middleware.go +++ b/routes/middleware.go @@ -4,7 +4,6 @@ import ( "net/http" "strings" - "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" "github.com/lavab/api/env" @@ -23,43 +22,35 @@ func AuthMiddleware(c *web.C, h http.Handler) http.Handler { // Read the Authorization header header := r.Header.Get("Authorization") if header == "" { - utils.JSONResponse(w, 401, &AuthMiddlewareResponse{ - Success: false, - Message: "Missing auth token", - }) + utils.JSONResponse(w, 401, utils.NewError( + utils.MiddlewareMissingToken, "Missing auth token", false, + )) return } // Split it into two parts headerParts := strings.Split(header, " ") if len(headerParts) != 2 || headerParts[0] != "Bearer" { - utils.JSONResponse(w, 401, &AuthMiddlewareResponse{ - Success: false, - Message: "Invalid authorization header", - }) + utils.JSONResponse(w, 401, utils.NewError( + utils.MiddlewareInvalidFormat, "Invalid authorization header's format", false, + )) return } // Get the token from the database token, err := env.Tokens.GetToken(headerParts[1]) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Cannot retrieve session from the database") - - utils.JSONResponse(w, 401, &AuthMiddlewareResponse{ - Success: false, - Message: "Invalid authorization token", - }) + utils.JSONResponse(w, 401, utils.NewError( + utils.MiddlewareInvalidToken, err, false, + )) return } // Check if it's expired if token.Expired() { - utils.JSONResponse(w, 419, &AuthMiddlewareResponse{ - Success: false, - Message: "Authorization token has expired", - }) + utils.JSONResponse(w, 401, utils.NewError( + utils.MiddlewareExpiredToken, "Your authentication token has expired", false, + )) env.Tokens.DeleteID(token.ID) return } diff --git a/utils/errors.go b/utils/errors.go index e93e117..82d3034 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -295,6 +295,10 @@ const ( const ( MiddlewareUnknown = 18000 + iota + MiddlewareMissingToken + MiddlewareInvalidFormat + MiddlewareInvalidToken + MiddlewareExpiredToken ) const ( From 172bc9a6a060b748694ad54dde408d774dcaaf3b Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 22:08:30 +0200 Subject: [PATCH 19/27] threads.go: New error logging --- routes/threads.go | 157 +++++++++++++++------------------------------- utils/errors.go | 16 +++++ 2 files changed, 66 insertions(+), 107 deletions(-) diff --git a/routes/threads.go b/routes/threads.go index a533e1b..41cde28 100644 --- a/routes/threads.go +++ b/routes/threads.go @@ -40,15 +40,9 @@ func ThreadsList(c web.C, w http.ResponseWriter, r *http.Request) { if offsetRaw != "" { o, err := strconv.Atoi(offsetRaw) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err, - "offset": offset, - }).Error("Invalid offset") - - utils.JSONResponse(w, 400, &ThreadsListResponse{ - Success: false, - Message: "Invalid offset", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.ThreadsListInvalidOffset, err, false, + )) return } offset = o @@ -57,15 +51,9 @@ func ThreadsList(c web.C, w http.ResponseWriter, r *http.Request) { if limitRaw != "" { l, err := strconv.Atoi(limitRaw) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "limit": limit, - }).Error("Invalid limit") - - utils.JSONResponse(w, 400, &ThreadsListResponse{ - Success: false, - Message: "Invalid limit", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.ThreadsListInvalidLimit, err, false, + )) return } limit = l @@ -81,28 +69,18 @@ func ThreadsList(c web.C, w http.ResponseWriter, r *http.Request) { threads, err := env.Threads.List(session.Owner, sort, offset, limit, labels) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to fetch threads") - - utils.JSONResponse(w, 500, &ThreadsListResponse{ - Success: false, - Message: "Internal error (code TH/LI/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.ThreadsListUnableToGet, err, true, + )) return } if offsetRaw != "" || limitRaw != "" { count, err := env.Threads.CountOwnedBy(session.Owner) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to count threads") - - utils.JSONResponse(w, 500, &ThreadsListResponse{ - Success: false, - Message: "Internal error (code TH/LI/02)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.ThreadsListUnableToCount, err, true, + )) return } w.Header().Set("X-Total-Count", strconv.Itoa(count)) @@ -116,30 +94,28 @@ func ThreadsList(c web.C, w http.ResponseWriter, r *http.Request) { // ThreadsGetResponse contains the result of the ThreadsGet request. type ThreadsGetResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Thread *models.Thread `json:"thread,omitempty"` - Emails *[]*models.Email `json:"emails,omitempty"` + Success bool `json:"success"` + Message string `json:"message"` + Thread *models.Thread `json:"thread,omitempty"` + Emails []*models.Email `json:"emails,omitempty"` } // ThreadsGet returns information about a single thread. func ThreadsGet(c web.C, w http.ResponseWriter, r *http.Request) { thread, err := env.Threads.GetThread(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &ThreadsGetResponse{ - Success: false, - Message: "Thread not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.ThreadsGetUnableToGet, err, false, + )) return } session := c.Env["token"].(*models.Token) if thread.Owner != session.Owner { - utils.JSONResponse(w, 404, &ThreadsGetResponse{ - Success: false, - Message: "Thread not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.ThreadsGetNotOwned, "You're not the owner of this thread", false, + )) return } @@ -157,15 +133,9 @@ func ThreadsGet(c web.C, w http.ResponseWriter, r *http.Request) { if ok := r.URL.Query().Get("list_emails"); ok == "true" || ok == "1" { emails, err = env.Emails.GetByThread(thread.ID) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": thread.ID, - }).Error("Unable to fetch emails linked to a thread") - - utils.JSONResponse(w, 500, &ThreadsGetResponse{ - Success: false, - Message: "Unable to retrieve emails", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.ThreadsGetUnableToFetchEmails, err, true, + )) return } } @@ -173,7 +143,7 @@ func ThreadsGet(c web.C, w http.ResponseWriter, r *http.Request) { utils.JSONResponse(w, 200, &ThreadsGetResponse{ Success: true, Thread: thread, - Emails: &emails, + Emails: emails, }) } @@ -194,24 +164,18 @@ func ThreadsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { var input ThreadsUpdateRequest err := utils.ParseRequest(r, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 400, &ThreadsUpdateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.ThreadsUpdateInvalidInput, err, false, + )) return } // Get the thread from the database thread, err := env.Threads.GetThread(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &ThreadsUpdateResponse{ - Success: false, - Message: "Thread not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.ThreadsUpdateUnableToGet, err, false, + )) return } @@ -220,10 +184,9 @@ func ThreadsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { // Check for ownership if thread.Owner != session.Owner { - utils.JSONResponse(w, 404, &ContactsUpdateResponse{ - Success: false, - Message: "Contact not found", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.ThreadsUpdateNotOwned, "You're not the owner of this thread", false, + )) return } @@ -244,15 +207,9 @@ func ThreadsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { err = env.Threads.UpdateID(c.URLParams["id"], thread) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": c.URLParams["id"], - }).Error("Unable to update a thread") - - utils.JSONResponse(w, 500, &ThreadsUpdateResponse{ - Success: false, - Message: "Internal error (code TH/UP/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.ThreadsUpdateUnableToUpdate, err, true, + )) return } @@ -274,10 +231,9 @@ func ThreadsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Get the thread from the database thread, err := env.Threads.GetThread(c.URLParams["id"]) if err != nil { - utils.JSONResponse(w, 404, &ThreadsDeleteResponse{ - Success: false, - Message: "Thread not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.ThreadsUpdateUnableToGet, err, false, + )) return } @@ -286,40 +242,27 @@ func ThreadsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // Check for ownership if thread.Owner != session.Owner { - utils.JSONResponse(w, 404, &ThreadsDeleteResponse{ - Success: false, - Message: "Thread not found", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.ThreadsUpdateNotOwned, "You're not the owner of this thread", false, + )) return } // Perform the deletion err = env.Threads.DeleteID(c.URLParams["id"]) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": c.URLParams["id"], - }).Error("Unable to delete a thread") - - utils.JSONResponse(w, 500, &ThreadsDeleteResponse{ - Success: false, - Message: "Internal error (code TH/DE/01)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.ThreadsDeleteUnableToDeleteThread, err, true, + )) return } // Remove dependent emails err = env.Emails.DeleteByThread(c.URLParams["id"]) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": c.URLParams["id"], - }).Error("Unable to delete emails by thread") - - utils.JSONResponse(w, 500, &ThreadsDeleteResponse{ - Success: false, - Message: "Internal error (code TH/DE/02)", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.ThreadsDeleteUnableToDeleteEmails, err, true, + )) return } diff --git a/utils/errors.go b/utils/errors.go index 82d3034..575254f 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -303,18 +303,34 @@ const ( const ( ThreadsListUnknown = 19000 + iota + ThreadsListInvalidOffset + ThreadsListInvalidLimit + ThreadsListUnableToGet + ThreadsListUnableToCount ) const ( ThreadsGetUnknown = 19100 + iota + ThreadsGetUnableToGet + ThreadsGetNotOwned + ThreadsGetUnableToFetchManifest + ThreadsGetUnableToFetchEmails ) const ( ThreadsUpdateUnknown = 19200 + iota + ThreadsUpdateInvalidInput + ThreadsUpdateUnableToGet + ThreadsUpdateNotOwned + ThreadsUpdateUnableToUpdate ) const ( ThreadsDeleteUnknown = 19300 + iota + ThreadsDeleteUnableToGet + ThreadsDeleteNotOwned + ThreadsDeleteUnableToDeleteThread + ThreadsDeleteUnableToDeleteEmails ) const ( From 7f1b8d6c7027325fa57210c9d4533af073c28dd2 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 22:19:46 +0200 Subject: [PATCH 20/27] Changelog update and new errors handling in tokens.go --- CHANGELOG.md | 16 +++++++++ routes/tokens.go | 92 ++++++++++++++++++------------------------------ utils/errors.go | 11 +++++- 3 files changed, 60 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e25caf..1ef2ce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased][unreleased] +### Added + - New file abstraction that allows storing custom plugins' data on + the server. + - Raven-based Sentry panic reporting. + +### Changed + - Replaced awful error messages with a proper error code and message + system. Sending actual errors, as the code is opensource anyways. + +### Fixed + - Fixed `threadAndDate` index creation not being executed. + +### Removed + - Removed old 2FA support. + - Removed tests. + - Removed Loggly support. ## [2.0.2] - 2015-05-19 ### Added diff --git a/routes/tokens.go b/routes/tokens.go index 7ba21e8..a574ce3 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -4,7 +4,6 @@ import ( "net/http" "time" - "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" "github.com/lavab/api/env" @@ -34,15 +33,9 @@ func TokensGet(c web.C, w http.ResponseWriter, r *http.Request) { } else { token, err = env.Tokens.GetToken(id) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": id, - }).Warn("Unable to find the token") - - utils.JSONResponse(w, 404, &TokensGetResponse{ - Success: false, - Message: "Invalid token ID", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.TokensGetUnableToGet, err, false, + )) return } } @@ -75,23 +68,17 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { var input TokensCreateRequest err := utils.ParseRequest(r, &input) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Warn("Unable to decode a request") - - utils.JSONResponse(w, 409, &TokensCreateResponse{ - Success: false, - Message: "Invalid input format", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.TokensCreateInvalidInput, err, false, + )) return } // We can only create "auth" tokens now if input.Type != "auth" { - utils.JSONResponse(w, 409, &TokensCreateResponse{ - Success: false, - Message: "Only auth tokens are implemented", - }) + utils.JSONResponse(w, 400, utils.NewError( + utils.TokensCreateInvalidType, "Only auth tokens are implemented", false, + )) return } @@ -102,29 +89,26 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { // Check if account exists user, err := env.Accounts.FindAccountByName(input.Username) if err != nil { - utils.JSONResponse(w, 403, &TokensCreateResponse{ - Success: false, - Message: "Wrong username or password", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.TokensCreateUnableToGetAccount, err, false, + )) return } // "registered" accounts can't log in if user.Status == "registered" { - utils.JSONResponse(w, 403, &TokensCreateResponse{ - Success: false, - Message: "Your account is not confirmed", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.TokensCreateInvalidStatus, "Your account is not confirmed", false, + )) return } // Verify the password valid, updated, err := user.VerifyPassword(input.Password) if err != nil || !valid { - utils.JSONResponse(w, 403, &TokensCreateResponse{ - Success: false, - Message: "Wrong username or password", - }) + utils.JSONResponse(w, 403, utils.NewError( + utils.TokensCreateInvalidPassword, err, false, + )) return } @@ -133,12 +117,10 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { user.DateModified = time.Now() err := env.Accounts.UpdateID(user.ID, user) if err != nil { - env.Log.WithFields(logrus.Fields{ - "user": user.Name, - "error": err.Error(), - }).Error("Could not update user") - - // DO NOT RETURN! + utils.JSONResponse(w, 500, utils.NewError( + utils.TokensCreateUnableToUpdate, err, true, + )) + return } } @@ -153,7 +135,12 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { } // Insert int into the database - env.Tokens.Insert(token) + if err := env.Tokens.Insert(token); err != nil { + utils.JSONResponse(w, 500, utils.NewError( + utils.TokensCreateUnableToInsert, err, true, + )) + return + } // Respond with the freshly created token utils.JSONResponse(w, 201, &TokensCreateResponse{ @@ -184,29 +171,18 @@ func TokensDelete(c web.C, w http.ResponseWriter, r *http.Request) { } else { token, err = env.Tokens.GetToken(id) if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - "id": id, - }).Warn("Unable to find the token") - - utils.JSONResponse(w, 404, &TokensDeleteResponse{ - Success: false, - Message: "Invalid token ID", - }) + utils.JSONResponse(w, 404, utils.NewError( + utils.TokensDeleteUnableToGet, err, false, + )) return } } // Delete it from the database if err := env.Tokens.DeleteID(token.ID); err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err.Error(), - }).Error("Unable to delete a token") - - utils.JSONResponse(w, 500, &TokensDeleteResponse{ - Success: false, - Message: "Internal server error - TO/DE/02", - }) + utils.JSONResponse(w, 500, utils.NewError( + utils.TokensDeleteUnableToDelete, err, true, + )) return } diff --git a/utils/errors.go b/utils/errors.go index 575254f..691c6d9 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -313,7 +313,6 @@ const ( ThreadsGetUnknown = 19100 + iota ThreadsGetUnableToGet ThreadsGetNotOwned - ThreadsGetUnableToFetchManifest ThreadsGetUnableToFetchEmails ) @@ -335,12 +334,22 @@ const ( const ( TokensGetUnknown = 20000 + iota + TokensGetUnableToGet ) const ( TokensCreateUnknown = 20100 + iota + TokensCreateInvalidInput + TokensCreateInvalidType + TokensCreateUnableToGetAccount + TokensCreateInvalidStatus + TokensCreateInvalidPassword + TokensCreateUnableToUpdate + TokensCreateUnableToInsert ) const ( TokensDeleteUnknown = 20200 + iota + TokensDeleteUnableToGet + TokensDeleteUnableToDelete ) From 5d02b2f09cf904abd1d563ef4ba743685956af4b Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sat, 20 Jun 2015 22:29:23 +0200 Subject: [PATCH 21/27] Casting errors as strings in errors.go --- utils/errors.go | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/utils/errors.go b/utils/errors.go index 691c6d9..a436d14 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -9,19 +9,29 @@ import ( ) type Error struct { - Success bool `json:"success"` - Code int `json:"code,omitempty"` - Location string `json:"location,omitempty"` - Error interface{} `json:"error"` - Severe bool `json:"-"` + Success bool `json:"success"` + Code int `json:"code,omitempty"` + Location string `json:"location,omitempty"` + Error string `json:"error"` + Severe bool `json:"-"` } func NewError(code int, input interface{}, severe bool) *Error { _, file, line, _ := runtime.Caller(1) + var err string + switch v := input.(type) { + case error: + err = v.Error() + case string: + err = v + default: + err = fmt.Sprintf("%+v", v) + } + return &Error{ Code: code, - Error: input, + Error: err, Location: filepath.Base(file) + ":" + strconv.Itoa(line), Severe: severe, } @@ -41,14 +51,7 @@ func (e *Error) String() string { buf.WriteString(": ") } - switch v := e.Error.(type) { - case error: - buf.WriteString(v.Error()) - case string: - buf.WriteString(v) - default: - buf.WriteString(fmt.Sprintf("%+v", v)) - } + buf.WriteString(e.Error) return buf.String() } From 4f4843b8faebe7cf2fb92ede5d2244086d0e293e Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Sun, 21 Jun 2015 18:10:51 +0200 Subject: [PATCH 22/27] Subject hash generation and removed ReplyTo, added InReplyTo --- models/email.go | 2 +- routes/emails.go | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/models/email.go b/models/email.go index 4ad468b..962a5f2 100644 --- a/models/email.go +++ b/models/email.go @@ -34,7 +34,7 @@ type Email struct { // ContentType of the body in unencrypted emails ContentType string `json:"content_type" gorethink:"content_type"` - ReplyTo string `json:"reply_to" gorethink:"reply_to"` + InReplyTo string `json:"in_reply_to" gorethink:"in_reply_to"` // Contains ID of the thread Thread string `json:"thread" gorethink:"thread"` diff --git a/routes/emails.go b/routes/emails.go index c2953d9..f056948 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -120,7 +120,7 @@ type EmailsCreateRequest struct { // Temporary partials if you're sending unencrypted Subject string `json:"subject"` ContentType string `json:"content_type"` - ReplyTo string `json:"reply_to"` + InReplyTo string `json:"in_reply_to"` SubjectHash string `json:"subject_hash"` } @@ -306,6 +306,12 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { secure = "none" } + if input.SubjectHash == "" { + // Generate the subject hash + shr := sha256.Sum256([]byte(prefixesRegex.ReplaceAllString(input.Subject, ""))) + input.SubjectHash = hex.EncodeToString(shr[:]) + } + thread := &models.Thread{ Resource: models.MakeResource(account.ID, "Encrypted thread"), Emails: []string{resource.ID}, @@ -356,7 +362,7 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { Files: input.Files, ContentType: input.ContentType, - ReplyTo: input.ReplyTo, + InReplyTo: input.InReplyTo, Status: "queued", Secure: secure, From 15ac278787283267fc9a3b3ec2973d7d8753887f Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Mon, 22 Jun 2015 11:58:40 +0200 Subject: [PATCH 23/27] Potential fix for no meta/body in GET /file/:id --- db/table_files.go | 4 ++-- models/base_encrypted.go | 3 --- models/email.go | 3 --- routes/emails.go | 14 ++++++-------- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/db/table_files.go b/db/table_files.go index efd7560..96e7cd7 100644 --- a/db/table_files.go +++ b/db/table_files.go @@ -12,13 +12,13 @@ type FilesTable struct { } func (f *FilesTable) GetFile(id string) (*models.File, error) { - var result models.File + var result *models.File if err := f.FindFetchOne(id, &result); err != nil { return nil, err } - return &result, nil + return result, nil } func (f *FilesTable) GetFiles(ids ...string) ([]*models.File, error) { diff --git a/models/base_encrypted.go b/models/base_encrypted.go index adbe7a1..d039f66 100644 --- a/models/base_encrypted.go +++ b/models/base_encrypted.go @@ -5,9 +5,6 @@ type Encrypted struct { // Encoding tells the reader how to decode the data; can be "json", "protobuf", maybe more in the future Encoding string `json:"encoding" gorethink:"encoding"` - // PGPFingerprints contains the fingerprints of the PGP public keys used to encrypt the data. - PGPFingerprints []string `json:"pgp_fingerprints" gorethink:"pgp_fingerprints"` - // Data is the raw, PGP-encrypted data Data string `json:"data" gorethink:"data"` diff --git a/models/email.go b/models/email.go index 962a5f2..1f0ca11 100644 --- a/models/email.go +++ b/models/email.go @@ -20,9 +20,6 @@ type Email struct { // BCC is only visible in sent emails BCC []string `json:"bcc" gorethink:"bcc"` - // Fingerprints used for body and manifest - PGPFingerprints []string `json:"pgp_fingerprints" gorethink:"pgp_fingerprints"` - // Files contains IDs of other files Files []string `json:"files" gorethink:"files"` diff --git a/routes/emails.go b/routes/emails.go index f056948..7b7a91a 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -112,10 +112,9 @@ type EmailsCreateRequest struct { BCC []string `json:"bcc"` // Encrypted parts - PGPFingerprints []string `json:"pgp_fingerprints"` - Manifest string `json:"manifest"` - Body string `json:"body"` - Files []string `json:"files"` + Manifest string `json:"manifest"` + Body string `json:"body"` + Files []string `json:"files"` // Temporary partials if you're sending unencrypted Subject string `json:"subject"` @@ -356,10 +355,9 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { CC: input.CC, BCC: input.BCC, - PGPFingerprints: input.PGPFingerprints, - Manifest: input.Manifest, - Body: input.Body, - Files: input.Files, + Manifest: input.Manifest, + Body: input.Body, + Files: input.Files, ContentType: input.ContentType, InReplyTo: input.InReplyTo, From ec0f832377b2d6c76d1ba05435cd417682a13f56 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Mon, 22 Jun 2015 12:31:47 +0200 Subject: [PATCH 24/27] Purging anything related to Mr Second Developer from files.Get --- routes/files.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/routes/files.go b/routes/files.go index 79ddbf8..dd63b76 100644 --- a/routes/files.go +++ b/routes/files.go @@ -131,10 +131,18 @@ type FilesGetResponse struct { // FilesGet gets the requested file from the database func FilesGet(c web.C, w http.ResponseWriter, req *http.Request) { // Get the file from the database - file, err := env.Files.GetFile(c.URLParams["id"]) + cursor, err := r.Table("files").Get(c.URLParams["id"]).Run(env.Rethink) if err != nil { utils.JSONResponse(w, 404, utils.NewError( - utils.FilesGetUnableToGet, err, false, + utils.FilesGetUnableToGet, err, true, + )) + return + } + defer cursor.Close() + var file *models.File + if err := cursor.One(&file); err != nil { + utils.JSONResponse(w, 404, utils.NewError( + utils.FilesGetUnableToGet, err, true, )) return } From 5df4eaae1cfad08331a71ab6a4e290856eda9e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Mu=CC=88ller-Irion?= Date: Mon, 29 Jun 2015 14:17:49 +0200 Subject: [PATCH 25/27] changed License to GPL V2 --- LICENSE | 360 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 339 insertions(+), 21 deletions(-) diff --git a/LICENSE b/LICENSE index bb484ba..085281c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,339 @@ -The MIT License (MIT) - -Copyright (c) {{{2015}}} {{{Lavaboom GmbH}}} - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + Lavaboom Webmail client + Copyright (C) 2015 Lavaboom GmbH + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. From 4120b1956884494cbf726b2f698a957bdfff3284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Mu=CC=88ller-Irion?= Date: Mon, 29 Jun 2015 14:22:24 +0200 Subject: [PATCH 26/27] updated --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 085281c..ca12fd4 100644 --- a/LICENSE +++ b/LICENSE @@ -290,7 +290,7 @@ to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - Lavaboom Webmail client + Lavaboom API Copyright (C) 2015 Lavaboom GmbH This program is free software; you can redistribute it and/or modify From 2ce8023e924e011845e189e8374b7aeaf9295fd6 Mon Sep 17 00:00:00 2001 From: Felix Mueller-Irion Date: Sun, 16 Aug 2015 16:32:37 +0200 Subject: [PATCH 27/27] Create .codeclimate.yml --- .codeclimate.yml | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .codeclimate.yml diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..749bb88 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,48 @@ +# This is a sample .codeclimate.yml configured for Engine analysis on Code +# Climate Platform. For an overview of the Code Climate Platform, see here: +# http://docs.codeclimate.com/article/300-the-codeclimate-platform + +# Under the engines key, you can configure which engines will analyze your repo. +# Each key is an engine name. For each value, you need to specify enabled: true +# to enable the engine as well as any other engines-specific configuration. + +# For more details, see here: +# http://docs.codeclimate.com/article/289-configuring-your-repository-via-codeclimate-yml#platform + +# For a list of all available engines, see here: +# http://docs.codeclimate.com/article/296-engines-available-engines + +engines: +# to turn on an engine, add it here and set enabled to `true` +# to turn off an engine, set enabled to `false` or remove it + rubocop: + enabled: true + golint: + enabled: true + gofmt: + enabled: true + eslint: + enabled: true + csslint: + enabled: true + +# Engines can analyze files and report issues on them, but you can separately +# decide which files will receive ratings based on those issues. This is +# specified by path patterns under the ratings key. + +# For more details see here: +# http://docs.codeclimate.com/article/289-configuring-your-repository-via-codeclimate-yml#platform + +# ratings: +# paths: +# - app/** +# - lib/** +# - "**.rb" +# - "**.go" + +# You can globally exclude files from being analyzed by any engine using the +# exclude_paths key. + +#exclude_paths: +#- spec/**/* +#- vendor/**/*