diff --git a/db/setup.go b/db/setup.go index 75483a5..5aa049b 100644 --- a/db/setup.go +++ b/db/setup.go @@ -20,6 +20,7 @@ var tableIndexes = map[string][]string{ "reservations": []string{"email", "name"}, "threads": []string{"owner"}, "tokens": []string{"owner"}, + "attachments": []string{"owner"}, } // List of names of databases diff --git a/db/table_attachments.go b/db/table_attachments.go new file mode 100644 index 0000000..3ab8337 --- /dev/null +++ b/db/table_attachments.go @@ -0,0 +1,63 @@ +package db + +import ( + "github.com/lavab/api/models" + + "github.com/dancannon/gorethink" +) + +type AttachmentsTable struct { + RethinkCRUD + Emails *EmailsTable +} + +func (a *AttachmentsTable) GetAttachment(id string) (*models.Attachment, error) { + var result models.Attachment + + if err := a.FindFetchOne(id, &result); err != nil { + return nil, err + } + + return &result, nil +} + +func (a *AttachmentsTable) GetOwnedBy(id string) ([]*models.Attachment, error) { + var result []*models.Attachment + + err := a.WhereAndFetch(map[string]interface{}{ + "owner": id, + }, &result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (a *AttachmentsTable) DeleteOwnedBy(id string) error { + return a.Delete(map[string]interface{}{ + "owner": id, + }) +} + +func (a *AttachmentsTable) GetEmailAttachments(id string) ([]*models.Attachment, error) { + email, err := a.Emails.GetEmail(id) + if err != nil { + return nil, err + } + + query, err := a.Emails.GetTable().Filter(func(row gorethink.Term) gorethink.Term { + return gorethink.Expr(email.AttachmentIDs).Contains(row.Field("id")) + }).GetAll().Run(a.Emails.GetSession()) + if err != nil { + return nil, err + } + + var result []*models.Attachment + err = query.All(&result) + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/env/env.go b/env/env.go index a1fa521..6cfd898 100644 --- a/env/env.go +++ b/env/env.go @@ -33,6 +33,8 @@ var ( Emails *db.EmailsTable // Labels is the global instance of LabelsTable Labels *db.LabelsTable + // Attachments is the global instance of AttachmentsTable + Attachments *db.AttachmentsTable // Factors contains all currently registered factors Factors map[string]factor.Factor // NATS is the encoded connection to the NATS queue diff --git a/models/file.go b/models/attachment.go similarity index 56% rename from models/file.go rename to models/attachment.go index 9e1eb33..b4c0e61 100644 --- a/models/file.go +++ b/models/attachment.go @@ -1,13 +1,13 @@ package models -// File is an encrypted file stored by Lavaboom -type File struct { +// Attachment is an encrypted file stored by Lavaboom +type Attachment struct { Encrypted Resource - // Mime is the Internet media type of the file + // Mime is the Internet media type of the attachment // Format: "type/subtype" – more info: en.wikipedia.org/wiki/Internet_media_type - Mime string `json:"mime" gorethink:"mime"` + MIME string `json:"mime" gorethink:"mime"` // Size is the size of the file in bytes i.e. len(file.Data) Size int `json:"size" gorethink:"size"` diff --git a/routes/accounts.go b/routes/accounts.go index 8134cc4..621c595 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -375,12 +375,13 @@ func AccountsGet(c web.C, w http.ResponseWriter, r *http.Request) { // AccountsUpdateRequest contains the input for the AccountsUpdate endpoint. 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"` + 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"` + AppData interface{} `json:"app_data" schema:"app_data"` } // AccountsUpdateResponse contains the result of the AccountsUpdate request. @@ -511,6 +512,10 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { user.AltEmail = input.AltEmail } + if input.AppData != nil { + user.AppData = input.AppData + } + if input.FactorType != "" { // Check if such factor exists if _, exists := env.Factors[input.FactorType]; !exists { diff --git a/routes/attachments.go b/routes/attachments.go new file mode 100644 index 0000000..df06e94 --- /dev/null +++ b/routes/attachments.go @@ -0,0 +1,310 @@ +package routes + +import ( + "net/http" + + "github.com/Sirupsen/logrus" + "github.com/zenazn/goji/web" + + "github.com/lavab/api/env" + "github.com/lavab/api/models" + "github.com/lavab/api/utils" +) + +type AttachmentsListResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Attachments *[]*models.Attachment `json:"attachments,omitempty"` +} + +func AttachmentsList(c web.C, w http.ResponseWriter, r *http.Request) { + session := c.Env["token"].(*models.Token) + + attachments, err := env.Attachments.GetOwnedBy(session.Owner) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Error("Unable to fetch attachments") + + utils.JSONResponse(w, 500, &AttachmentsListResponse{ + Success: false, + Message: "Internal error (code AT/LI/01)", + }) + return + } + + utils.JSONResponse(w, 200, &AttachmentsListResponse{ + Success: true, + Attachments: &attachments, + }) +} + +type AttachmentsCreateRequest 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"` +} + +type AttachmentsCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Attachment *models.Attachment `json:"attachment,omitempty"` +} + +// AttachmentsCreate creates a new attachment +func AttachmentsCreate(c web.C, w http.ResponseWriter, r *http.Request) { + // Decode the request + var input AttachmentsCreateRequest + err := utils.ParseRequest(r, &input) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Warn("Unable to decode a request") + + utils.JSONResponse(w, 400, &AttachmentsCreateResponse{ + Success: false, + Message: "Invalid input format", + }) + return + } + + // 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 == "" || + input.PGPFingerprints == nil || len(input.PGPFingerprints) == 0 { + utils.JSONResponse(w, 400, &AttachmentsCreateResponse{ + Success: false, + Message: "Invalid request", + }) + return + } + + // Create a new attachment struct + attachment := &models.Attachment{ + Encrypted: models.Encrypted{ + Encoding: input.Encoding, + Data: input.Data, + Schema: "attachment", + VersionMajor: input.VersionMajor, + VersionMinor: input.VersionMinor, + PGPFingerprints: input.PGPFingerprints, + }, + Resource: models.MakeResource(session.Owner, input.Name), + } + + // Insert the attachment into the database + if err := env.Attachments.Insert(attachment); err != nil { + utils.JSONResponse(w, 500, &AttachmentsCreateResponse{ + Success: false, + Message: "internal server error - AT/CR/01", + }) + + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Error("Could not insert a attachment into the database") + return + } + + utils.JSONResponse(w, 201, &AttachmentsCreateResponse{ + Success: true, + Message: "A new attachment was successfully created", + Attachment: attachment, + }) +} + +// AttachmentsGetResponse contains the result of the AttachmentsGet request. +type AttachmentsGetResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Attachment *models.Attachment `json:"attachment,omitempty"` +} + +// AttachmentsGet gets the requested attachment from the database +func AttachmentsGet(c web.C, w http.ResponseWriter, r *http.Request) { + // Get the attachment from the database + attachment, err := env.Attachments.GetAttachment(c.URLParams["id"]) + if err != nil { + utils.JSONResponse(w, 404, &AttachmentsGetResponse{ + Success: false, + Message: "Attachment not found", + }) + return + } + + // Fetch the current session from the middleware + session := c.Env["token"].(*models.Token) + + // Check for ownership + if attachment.Owner != session.Owner { + utils.JSONResponse(w, 404, &AttachmentsGetResponse{ + Success: false, + Message: "Attachment not found", + }) + return + } + + // Write the attachment to the response + utils.JSONResponse(w, 200, &AttachmentsGetResponse{ + Success: true, + Attachment: attachment, + }) +} + +// AttachmentsUpdateRequest is the payload passed to PUT /contacts/:id +type AttachmentsUpdateRequest 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"` +} + +// AttachmentsUpdateResponse contains the result of the AttachmentsUpdate request. +type AttachmentsUpdateResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Attachment *models.Attachment `json:"attachment,omitempty"` +} + +// AttachmentsUpdate updates an existing attachment in the database +func AttachmentsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { + // Decode the request + var input AttachmentsUpdateRequest + err := utils.ParseRequest(r, &input) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Warn("Unable to decode a request") + + utils.JSONResponse(w, 400, &AttachmentsUpdateResponse{ + Success: false, + Message: "Invalid input format", + }) + return + } + + // Get the attachment from the database + attachment, err := env.Attachments.GetAttachment(c.URLParams["id"]) + if err != nil { + utils.JSONResponse(w, 404, &AttachmentsUpdateResponse{ + Success: false, + Message: "Attachment not found", + }) + return + } + + // Fetch the current session from the middleware + session := c.Env["token"].(*models.Token) + + // Check for ownership + if attachment.Owner != session.Owner { + utils.JSONResponse(w, 404, &AttachmentsUpdateResponse{ + Success: false, + Message: "Attachment not found", + }) + return + } + + if input.Data != "" { + attachment.Data = input.Data + } + + if input.Name != "" { + attachment.Name = input.Name + } + + if input.Encoding != "" { + attachment.Encoding = input.Encoding + } + + if input.VersionMajor != nil { + attachment.VersionMajor = *input.VersionMajor + } + + if input.VersionMinor != nil { + attachment.VersionMinor = *input.VersionMinor + } + + if input.PGPFingerprints != nil { + attachment.PGPFingerprints = input.PGPFingerprints + } + + // Perform the update + err = env.Attachments.UpdateID(c.URLParams["id"], input) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + "id": c.URLParams["id"], + }).Error("Unable to update a attachment") + + utils.JSONResponse(w, 500, &AttachmentsUpdateResponse{ + Success: false, + Message: "Internal error (code AT/UP/01)", + }) + return + } + + // Write the attachment to the response + utils.JSONResponse(w, 200, &AttachmentsUpdateResponse{ + Success: true, + Attachment: attachment, + }) +} + +// AttachmentsDeleteResponse contains the result of the Delete request. +type AttachmentsDeleteResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// AttachmentsDelete removes a attachment from the database +func AttachmentsDelete(c web.C, w http.ResponseWriter, r *http.Request) { + // Get the attachment from the database + attachment, err := env.Attachments.GetAttachment(c.URLParams["id"]) + if err != nil { + utils.JSONResponse(w, 404, &AttachmentsDeleteResponse{ + Success: false, + Message: "Attachment not found", + }) + return + } + + // Fetch the current session from the middleware + session := c.Env["token"].(*models.Token) + + // Check for ownership + if attachment.Owner != session.Owner { + utils.JSONResponse(w, 404, &AttachmentsDeleteResponse{ + Success: false, + Message: "Attachment not found", + }) + return + } + + // Perform the deletion + err = env.Attachments.DeleteID(c.URLParams["id"]) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + "id": c.URLParams["id"], + }).Error("Unable to delete a attachment") + + utils.JSONResponse(w, 500, &AttachmentsDeleteResponse{ + Success: false, + Message: "Internal error (code AT/DE/01)", + }) + return + } + + // Write the attachment to the response + utils.JSONResponse(w, 200, &AttachmentsDeleteResponse{ + Success: true, + Message: "Attachment successfully removed", + }) +} diff --git a/setup/setup.go b/setup/setup.go index b3d1a7f..e0afc03 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -163,6 +163,13 @@ func PrepareMux(flags *env.Flags) *web.Mux { Emails: env.Emails, Cache: redis, } + env.Attachments = &db.AttachmentsTable{ + RethinkCRUD: db.NewCRUDTable( + rethinkSession, + rethinkOpts.Database, + "attachments", + ), + } // NATS queue connection nc, err := nats.Connect(flags.NATSAddress) @@ -326,6 +333,13 @@ func PrepareMux(flags *env.Flags) *web.Mux { // Index route mux.Get("/", routes.Hello) + // Attachments + auth.Get("/attachments", routes.AttachmentsList) + auth.Post("/attachments", routes.AttachmentsCreate) + auth.Get("/attachments/:id", routes.AttachmentsGet) + auth.Put("/attachments/:id", routes.AttachmentsUpdate) + auth.Delete("/attachments/:id", routes.AttachmentsDelete) + // Accounts auth.Get("/accounts", routes.AccountsList) mux.Post("/accounts", routes.AccountsCreate)