diff --git a/db/table_accounts.go b/db/table_accounts.go index d7a1ee9..0cc86f1 100644 --- a/db/table_accounts.go +++ b/db/table_accounts.go @@ -1,12 +1,16 @@ package db import ( + "errors" + "github.com/lavab/api/models" ) // AccountsTable implements the CRUD interface for accounts type AccountsTable struct { RethinkCRUD + + Tokens *TokensTable } // GetAccount returns an account with specified ID @@ -30,3 +34,14 @@ func (users *AccountsTable) FindAccountByName(name string) (*models.Account, err return &result, nil } + +func (a *AccountsTable) GetTokenOwner(token *models.Token) (*models.Account, error) { + user, err := a.GetAccount(token.Owner) + if err != nil { + // Try to remove the orphaned token + a.Tokens.DeleteID(token.ID) + return nil, errors.New("Account disabled") + } + + return user, nil +} diff --git a/db/table_contacts.go b/db/table_contacts.go new file mode 100644 index 0000000..5391fc0 --- /dev/null +++ b/db/table_contacts.go @@ -0,0 +1,42 @@ +package db + +import ( + "github.com/lavab/api/models" +) + +// Contacts implements the CRUD interface for tokens +type ContactsTable struct { + RethinkCRUD +} + +// GetContact returns a token with specified name +func (c *ContactsTable) GetContact(id string) (*models.Contact, error) { + var result models.Contact + + if err := c.FindFetchOne(id, &result); err != nil { + return nil, err + } + + return &result, nil +} + +// GetOwnedBy returns all contacts owned by id +func (c *ContactsTable) GetOwnedBy(id string) ([]*models.Contact, error) { + var result []*models.Contact + + err := c.WhereAndFetch(map[string]interface{}{ + "owner": id, + }, &result) + if err != nil { + return nil, err + } + + return result, nil +} + +// DeleteOwnedBy deletes all contacts owned by id +func (c *ContactsTable) DeleteOwnedBy(id string) error { + return c.Delete(map[string]interface{}{ + "owner": id, + }) +} diff --git a/db/table_tokens.go b/db/table_tokens.go index 83f54f0..8de409e 100644 --- a/db/table_tokens.go +++ b/db/table_tokens.go @@ -20,8 +20,8 @@ func (t *TokensTable) GetToken(id string) (*models.Token, error) { return &result, nil } -// DeleteByOwner deletes all tokens owned by id -func (t *TokensTable) DeleteByOwner(id string) error { +// DeleteOwnedBy deletes all tokens owned by id +func (t *TokensTable) DeleteOwnedBy(id string) error { return t.Delete(map[string]interface{}{ "owner": id, }) diff --git a/env/env.go b/env/env.go index bd05854..18178b6 100644 --- a/env/env.go +++ b/env/env.go @@ -20,4 +20,6 @@ var ( Tokens *db.TokensTable // Keys is the global instance of KeysTable Keys *db.KeysTable + // Contacts is the global instance of ContactsTable + Contacts *db.ContactsTable ) diff --git a/main.go b/main.go index 9cbcef4..1ef2346 100644 --- a/main.go +++ b/main.go @@ -104,19 +104,20 @@ func main() { env.Rethink = rethinkSession // Initialize the tables - env.Accounts = &db.AccountsTable{ + env.Tokens = &db.TokensTable{ RethinkCRUD: db.NewCRUDTable( rethinkSession, rethinkOpts.Database, - "accounts", + "tokens", ), } - env.Tokens = &db.TokensTable{ + env.Accounts = &db.AccountsTable{ RethinkCRUD: db.NewCRUDTable( rethinkSession, rethinkOpts.Database, - "tokens", + "accounts", ), + Tokens: env.Tokens, } env.Keys = &db.KeysTable{ RethinkCRUD: db.NewCRUDTable( @@ -125,6 +126,13 @@ func main() { "keys", ), } + env.Contacts = &db.ContactsTable{ + RethinkCRUD: db.NewCRUDTable( + rethinkSession, + rethinkOpts.Database, + "contacts", + ), + } // Create a new goji mux mux := web.New() diff --git a/models/base_encrypted.go b/models/base_encrypted.go index 359f066..44eec0b 100644 --- a/models/base_encrypted.go +++ b/models/base_encrypted.go @@ -9,7 +9,7 @@ type Encrypted struct { PgpFingerprints []string `json:"pgp_fingerprints" gorethink:"pgp_fingerprints"` // Data is the raw, PGP-encrypted data - Data []byte `json:"raw" gorethink:"raw"` + Data string `json:"raw" gorethink:"raw"` // Schema is the name of the schema used to encode the data // Examples: string, contact, email diff --git a/routes/accounts.go b/routes/accounts.go index 7b77b64..eaf9618 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -183,7 +183,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { env.Log.WithFields(logrus.Fields{ "error": err, - }).Error("Could not insert an user to the database") + }).Error("Could not insert an user into the database") return } @@ -247,7 +247,7 @@ func AccountsGet(c web.C, w http.ResponseWriter, r *http.Request) { } // Fetch the current session from the database - session := c.Env["session"].(*models.Token) + session := c.Env["token"].(*models.Token) // Fetch the user object from the database user, err := env.Accounts.GetAccount(session.Owner) @@ -336,7 +336,7 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { } // Fetch the current session from the database - session := c.Env["session"].(*models.Token) + session := c.Env["token"].(*models.Token) // Fetch the user object from the database user, err := env.Accounts.GetAccount(session.Owner) @@ -439,7 +439,7 @@ func AccountsDelete(c web.C, w http.ResponseWriter, r *http.Request) { } // Fetch the current session from the database - session := c.Env["session"].(*models.Token) + session := c.Env["token"].(*models.Token) // Fetch the user object from the database user, err := env.Accounts.GetAccount(session.Owner) @@ -478,7 +478,7 @@ func AccountsDelete(c web.C, w http.ResponseWriter, r *http.Request) { // TODO: Delete threads // Delete tokens - err = env.Tokens.DeleteByOwner(user.ID) + err = env.Tokens.DeleteOwnedBy(user.ID) if err != nil { env.Log.WithFields(logrus.Fields{ "id": user.ID, @@ -540,10 +540,10 @@ func AccountsWipeData(c web.C, w http.ResponseWriter, r *http.Request) { } // Fetch the current session from the database - session := c.Env["session"].(*models.Token) + session := c.Env["token"].(*models.Token) // Fetch the user object from the database - user, err := env.Accounts.GetAccount(session.Owner) + user, err := env.Accounts.GetTokenOwner(session) if err != nil { // The session refers to a non-existing user env.Log.WithFields(logrus.Fields{ @@ -551,18 +551,6 @@ func AccountsWipeData(c web.C, w http.ResponseWriter, r *http.Request) { "error": err, }).Warn("Valid session referred to a removed account") - // Try to remove the orphaned session - if err := env.Tokens.DeleteID(session.ID); err != nil { - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - "error": err, - }).Error("Unable to remove an orphaned session") - } else { - env.Log.WithFields(logrus.Fields{ - "id": session.ID, - }).Info("Removed an orphaned session") - } - utils.JSONResponse(w, 410, &AccountsWipeDataResponse{ Success: false, Message: "Account disabled", @@ -579,7 +567,7 @@ func AccountsWipeData(c web.C, w http.ResponseWriter, r *http.Request) { // TODO: Delete threads // Delete tokens - err = env.Tokens.DeleteByOwner(user.ID) + err = env.Tokens.DeleteOwnedBy(user.ID) if err != nil { env.Log.WithFields(logrus.Fields{ "id": user.ID, diff --git a/routes/contacts.go b/routes/contacts.go index d0effbf..5d8e18a 100644 --- a/routes/contacts.go +++ b/routes/contacts.go @@ -3,62 +3,248 @@ 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" ) // ContactsListResponse contains the result of the ContactsList request. type ContactsListResponse struct { - Success bool `json:"success"` - Message string `json:"message"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Contacts *[]*models.Contact `json:"contacts,omitempty"` } // ContactsList does *something* - TODO -func ContactsList(w http.ResponseWriter, r *http.Request) { +func ContactsList(c web.C, w http.ResponseWriter, r *http.Request) { + // Fetch the current session from the database + session := c.Env["token"].(*models.Token) + + // Get contacts from the database + contacts, err := env.Contacts.GetOwnedBy(session.Owner) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Error("Unable to fetch contacts") + + utils.JSONResponse(w, 500, &AccountsDeleteResponse{ + Success: false, + Message: "Internal error (code CO/LI/01)", + }) + return + } + utils.JSONResponse(w, 501, &ContactsListResponse{ - Success: false, - Message: "Sorry, not implemented yet", + Success: false, + Contacts: &contacts, }) } +// 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"` +} + // ContactsCreateResponse contains the result of the ContactsCreate request. type ContactsCreateResponse struct { - Success bool `json:"success"` - Message string `json:"message"` + Success bool `json:"success"` + Message string `json:"message"` + Contact *models.Contact `json:"contact,omitempty"` } -// ContactsCreate does *something* - TODO -func ContactsCreate(w http.ResponseWriter, r *http.Request) { - utils.JSONResponse(w, 501, &ContactsCreateResponse{ - Success: false, - Message: "Sorry, not implemented yet", +// ContactsCreate creates a new contact +func ContactsCreate(c web.C, w http.ResponseWriter, r *http.Request) { + // Decode the request + var input ContactsCreateRequest + 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, &ContactsCreateResponse{ + 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.VersionMajor != nil || input.VersionMinor != nil { + utils.JSONResponse(w, 400, &ContactsCreateResponse{ + Success: false, + Message: "Invalid request", + }) + 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, + }, + 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("Could not insert a contact into the database") + return + } + + utils.JSONResponse(w, 201, &ContactsCreateResponse{ + Success: true, + Message: "A new account was successfully created", + Contact: contact, }) } // ContactsGetResponse contains the result of the ContactsGet request. type ContactsGetResponse struct { - Success bool `json:"success"` - Message string `json:"message"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Contact *models.Contact `json:"contact,omitempty"` } -// ContactsGet does *something* - TODO -func ContactsGet(w http.ResponseWriter, r *http.Request) { - utils.JSONResponse(w, 501, &ContactsGetResponse{ - Success: false, - Message: "Sorry, not implemented yet", +// ContactsGet gets the requested contact from the database +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", + }) + return + } + + // Fetch the current session from the middleware + session := c.Env["token"].(*models.Token) + + // Check for ownership + if contact.Owner != session.Owner { + utils.JSONResponse(w, 404, &ContactsGetResponse{ + Success: false, + Message: "Contact not found", + }) + return + } + + // Write the contact to the response + utils.JSONResponse(w, 200, &ContactsGetResponse{ + Success: true, + Contact: contact, }) } +// 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"` +} + // ContactsUpdateResponse contains the result of the ContactsUpdate request. type ContactsUpdateResponse struct { - Success bool `json:"success"` - Message string `json:"message"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Contact *models.Contact `json:"contact,omitempty"` } -// ContactsUpdate does *something* - TODO -func ContactsUpdate(w http.ResponseWriter, r *http.Request) { - utils.JSONResponse(w, 501, &ContactsUpdateResponse{ - Success: false, - Message: "Sorry, not implemented yet", +// ContactsUpdate updates an existing contact in the database +func ContactsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { + // Decode the request + var input ContactsUpdateRequest + 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, &ContactsUpdateResponse{ + Success: false, + Message: "Invalid input format", + }) + 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", + }) + return + } + + // Fetch the current session from the middleware + session := c.Env["token"].(*models.Token) + + // Check for ownership + if contact.Owner != session.Owner { + utils.JSONResponse(w, 404, &ContactsUpdateResponse{ + Success: false, + Message: "Contact not found", + }) + return + } + + // Perform the update + err = env.Contacts.UpdateID(c.URLParams["id"], map[string]interface{}{ + "data": input.Data, + "name": input.Name, + "encoding": input.Encoding, + "version_major": input.VersionMajor, + "version_minor": input.VersionMinor, + }) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + "id": c.URLParams["id"], + }).Error("Unable to update a contact") + + utils.JSONResponse(w, 500, &ContactsUpdateResponse{ + Success: false, + Message: "Internal error (code CO/UP/01)", + }) + return + } + + // Update the original struct for the response + contact.Data = input.Data + contact.Name = input.Name + contact.Encoding = input.Encoding + contact.VersionMajor = input.VersionMajor + contact.VersionMinor = input.VersionMinor + + // Write the contact to the response + utils.JSONResponse(w, 200, &ContactsUpdateResponse{ + Success: true, + Contact: contact, }) } @@ -68,10 +254,48 @@ type ContactsDeleteResponse struct { Message string `json:"message"` } -// ContactsDelete does *something* - TODO -func ContactsDelete(w http.ResponseWriter, r *http.Request) { - utils.JSONResponse(w, 501, &ContactsDeleteResponse{ - Success: false, - Message: "Sorry, not implemented yet", +// ContactsDelete removes a contact from the database +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", + }) + return + } + + // Fetch the current session from the middleware + session := c.Env["token"].(*models.Token) + + // Check for ownership + if contact.Owner != session.Owner { + utils.JSONResponse(w, 404, &ContactsDeleteResponse{ + Success: false, + Message: "Contact not found", + }) + return + } + + // Perform the deletion + err = env.Contacts.DeleteID(c.URLParams["id"]) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + "id": c.URLParams["id"], + }).Error("Unable to delete a contact") + + utils.JSONResponse(w, 500, &ContactsDeleteResponse{ + Success: false, + Message: "Internal error (code CO/DE/01)", + }) + return + } + + // Write the contact to the response + utils.JSONResponse(w, 200, &ContactsDeleteResponse{ + Success: true, + Message: "Contact successfully removed", }) } diff --git a/routes/keys.go b/routes/keys.go index e0308d4..cbd4fce 100644 --- a/routes/keys.go +++ b/routes/keys.go @@ -88,7 +88,7 @@ func KeysCreate(c web.C, w http.ResponseWriter, r *http.Request) { } // Get the session - session := c.Env["session"].(*models.Token) + session := c.Env["token"].(*models.Token) // Parse the armored key entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(input.Key)) diff --git a/routes/middleware.go b/routes/middleware.go index 7ce3317..837ac04 100644 --- a/routes/middleware.go +++ b/routes/middleware.go @@ -65,7 +65,7 @@ func AuthMiddleware(c *web.C, h http.Handler) http.Handler { } // Continue to the next middleware/route - c.Env["session"] = token + c.Env["token"] = token h.ServeHTTP(w, r) }) } diff --git a/routes/tokens.go b/routes/tokens.go index bfc1e8a..54e4ca2 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -23,7 +23,7 @@ type TokensGetResponse struct { // TokensGet returns information about the current token. func TokensGet(c web.C, w http.ResponseWriter, r *http.Request) { // Fetch the current session from the database - session := c.Env["session"].(*models.Token) + session := c.Env["token"].(*models.Token) // Respond with the token information utils.JSONResponse(w, 200, &TokensGetResponse{ @@ -142,7 +142,7 @@ func TokensDelete(c web.C, w http.ResponseWriter, r *http.Request) { id, ok := c.URLParams["id"] if !ok || id == "" { // Get the token from the middleware - token = c.Env["session"].(*models.Token) + token = c.Env["token"].(*models.Token) } else { token, err = env.Tokens.GetToken(id) if err != nil {