From ca05044541ca1f8f3aa972177cf322d4c4ca53d9 Mon Sep 17 00:00:00 2001 From: "Piotr \"Orange\" Zduniak" Date: Wed, 5 Nov 2014 20:54:04 +0100 Subject: [PATCH 01/20] main.go is now rewritten and well commented --- main.go | 135 +++++++++++++++++++++++++++----------------------------- 1 file changed, 65 insertions(+), 70 deletions(-) diff --git a/main.go b/main.go index e6aaf85..728f453 100644 --- a/main.go +++ b/main.go @@ -8,89 +8,84 @@ import ( "strconv" "time" - "github.com/gorilla/mux" - "github.com/lavab/api/db" - "github.com/lavab/api/utils" - "github.com/stretchr/graceful" + "github.com/Sirupsen/logrus" + "github.com/goji/glogrus" + "github.com/namsral/flag" + "github.com/zenazn/goji" + "github.com/zenazn/goji/graceful" + "github.com/zenazn/goji/web" + "github.com/zenazn/goji/web/middleware" ) // TODO: "Middleware that implements a few quick security wins" // https://github.com/unrolled/secure -const ( - cTlsFilePub = ".tls/pub" - cTlsFilePriv = ".tls/priv" - cTcpPort = 5000 - cApiVersion = "v0" +var ( + bindAddress = flag.String("bind", ":5000", "Network address used to bind") + apiVersion = flag.String("version", "v0", "Shown API version") + logFormatterType = flag.String("log", "text", "Log formatter type. Either \"json\" or \"text\"") ) -var config struct { - Port int - PortString string - Host string - TlsAvailable bool - RootJSON string -} - -func init() { - config.Port = cTcpPort - config.Host = "" - config.TlsAvailable = false - config.RootJSON = rootResponseString() // this avoids an import cycle and also improves perf by caching the response - - if tmp := os.Getenv("API_PORT"); tmp != "" { - tmp2, err := strconv.Atoi(tmp) - if err != nil { - config.Port = tmp2 - } - log.Println("Running on non-default port", config.Port) - } - config.PortString = fmt.Sprintf(":%d", config.Port) - - if utils.FileExists(cTlsFilePub) && utils.FileExists(cTlsFilePriv) { - config.TlsAvailable = true - log.Println("Imported TLS cert/key successfully.") - } else { - log.Printf("TLS cert (%s) and key (%s) not found, serving plain HTTP.\n", cTlsFilePub, cTlsFilePriv) - } - - // Set up RethinkDB - go db.Init() -} - func main() { - setupAndRun() -} + // Parse the flags + flag.Parse() -func setupAndRun() { - r := mux.NewRouter() + // Set up a new logger + log := logrus.New() - if config.TlsAvailable { - r = r.Schemes("https").Subrouter() - } - if tmp := os.Getenv("API_HOST"); tmp != "" { - r = r.Host(tmp).Subrouter() + // Set the formatter depending on the passed flag's value + if *logFormatterType == "text" { + log.Formatter = &logrus.TextFormatter{} + } else if *logFormatterType == "json" { + log.Formatter = &logrus.JSONFormatter{} } - for _, rt := range publicRoutes { - r.HandleFunc(rt.Path, rt.HandleFunc).Methods(rt.Method) + // Create a new goji mux + mux := web.New() + + // Include the most basic middlewares: + // - RequestID assigns an unique ID for each request in order to identify errors. + // - Glogrus logs each request + // - Recoverer prevents panics from crashing the API + // - AutomaticOptions + mux.Use(middleware.RequestID) + mux.Use(glogrus.NewGlogrus(log, "api")) + mux.Use(middleware.Recoverer) + mux.Use(middleware.AutomaticOptions) + + // Compile the routes + mux.Compile() + + // Make the mux handle every request + http.Handle("/", DefaultMux) + + // Log that we're starting the server + log.WithFields(logrus.Fields{ + "address": *bindAddress, + }).Info("Starting the HTTP server") + + // Initialize the goroutine listening to signals passed to the app + graceful.HandleSignals() + + // Pre-graceful shutdown event + graceful.PreHook(func() { + log.Info("Received a singnal, stopping the application") + }) + + // Post-shutdown event + graceful.PostHook(func() { + log.Info("Stopped the application") + }) + + // Start the listening + err := graceful.Serve(listener, http.DefaultServeMux) + if err != nil { + // Don't use .Fatal! We need the code to shut down properly. + log.Error(err) } - for _, rt := range authRoutes { - r.HandleFunc(rt.Path, AuthWrapper(rt.HandleFunc)).Methods(rt.Method) - } + // If code reaches this place, it means that it was forcefully closed. - srv := &graceful.Server{ - Timeout: 10 * time.Second, - Server: &http.Server{ - Addr: config.PortString, - Handler: r, - }, - } - - if config.TlsAvailable { - log.Fatal(srv.ListenAndServeTLS(cTlsFilePub, cTlsFilePriv)) - } else { - log.Fatal(srv.ListenAndServe()) - } + // Wait until open connections close. + graceful.Wait() } From 7c4eb939d05784c0004936bb7c160548f366cbb3 Mon Sep 17 00:00:00 2001 From: "Piotr \"Orange\" Zduniak" Date: Wed, 5 Nov 2014 21:22:30 +0100 Subject: [PATCH 02/20] Added routes to main.go --- main.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/main.go b/main.go index 728f453..a8f58bd 100644 --- a/main.go +++ b/main.go @@ -53,6 +53,60 @@ func main() { mux.Use(middleware.Recoverer) mux.Use(middleware.AutomaticOptions) + // Set up an auth'd mux + auth := web.New() + mux.Use(models.AuthMiddleware) + + // Index route + mux.Get("/", routes.Hello) + + // Accounts + auth.Get("/accounts", routes.AccountsList) + mux.Post("/accounts", routes.AccountsCreate) + auth.Get("/accounts/:id", routes.AccountsGet) + auth.Put("/accounts/:id", routes.AccountsUpdate) + auth.Delete("/accounts/:id", routes.AccountsDelete) + auth.Post("/accounts/:id/wipe-user-data", routes.AccountsWipeUserData) + + // Tokens + auth.Get("/token", routes.TokenGet) + auth.Post("/token", routes.TokensCreate) + auth.Delete("/token", routes.TokensDelete) + + // Threads + auth.Get("/threads", routes.ThreadsList) + auth.Get("/threads/:id", routes.ThreadsGet) + auth.Put("/threads/:id", routes.ThreadsUpdate) + + // Emails + auth.Get("/emails", routes.EmailsList) + auth.Post("/emails", routes.EmailsCreate) + auth.Get("/emails/:id", routes.EmailsGet) + auth.Put("/emails/:id", routes.EmailsUpdate) + auth.Delete("/emails/:id", routes.EmailsDelete) + + // Labels + auth.Get("/labels", routes.LabelsList) + auth.Post("/labels", routes.LabelsCreate) + auth.Get("/labels/:id", routes.LabelsGet) + auth.Put("/labels/:id", routes.LabelsUpdate) + auth.Delete("/labels/:id", routes.LabelsDelete) + + // Contacts + auth.Get("/contacts", routes.ContactsList) + auth.Post("/contacts", routes.ContactsCreate) + auth.Get("/contacts/:id", routes.ContactsGet) + auth.Put("/contacts/:id", routes.ContactsUpdate) + auth.Delete("/contacts/:id", routes.ContactsDelete) + + // Keys + auth.Post("/keys", routes.KeysCreate) + mux.Get("/keys/:id", routes.KeysGet) + auth.Post("/keys/:id/vote", routes.KeysVote) + + // Merge the muxes + mux.Handle("/", auth) + // Compile the routes mux.Compile() From de447097193ff9f9dad42e6d15ca697a697c2e9c Mon Sep 17 00:00:00 2001 From: "Piotr \"Orange\" Zduniak" Date: Wed, 5 Nov 2014 21:51:20 +0100 Subject: [PATCH 03/20] Added an environment object, modified JSON utility response methods, added Hello route handler as an example --- env/config.go | 7 ++++ env/env.go | 12 ++++++ main.go | 14 ++++++- routes.go | 75 ----------------------------------- routes/{me.go => accounts.go} | 0 routes/hello.go | 22 ++++++++++ utils/json.go | 10 ----- utils/responses.go | 29 ++++++++++++-- 8 files changed, 79 insertions(+), 90 deletions(-) create mode 100644 env/config.go create mode 100644 env/env.go delete mode 100644 routes.go rename routes/{me.go => accounts.go} (100%) create mode 100644 routes/hello.go diff --git a/env/config.go b/env/config.go new file mode 100644 index 0000000..d60fb38 --- /dev/null +++ b/env/config.go @@ -0,0 +1,7 @@ +package env + +type Config struct { + BindAddress string + APIVersion string + LogFormatterType string +} diff --git a/env/env.go b/env/env.go new file mode 100644 index 0000000..29bfce7 --- /dev/null +++ b/env/env.go @@ -0,0 +1,12 @@ +package env + +import ( + "github.com/Sirupsen/logrus" +) + +type Environment struct { + Log *logrus.Logger + Config *Config +} + +var G *Environment diff --git a/main.go b/main.go index a8f58bd..3ecc96f 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,8 @@ import ( "github.com/zenazn/goji/graceful" "github.com/zenazn/goji/web" "github.com/zenazn/goji/web/middleware" + + "github.com/lavab/api/env" ) // TODO: "Middleware that implements a few quick security wins" @@ -47,7 +49,7 @@ func main() { // - RequestID assigns an unique ID for each request in order to identify errors. // - Glogrus logs each request // - Recoverer prevents panics from crashing the API - // - AutomaticOptions + // - AutomaticOptions automatically responds to OPTIONS requests mux.Use(middleware.RequestID) mux.Use(glogrus.NewGlogrus(log, "api")) mux.Use(middleware.Recoverer) @@ -113,6 +115,16 @@ func main() { // Make the mux handle every request http.Handle("/", DefaultMux) + // Set up a new environment object + env.G = &env.Environment{ + Log: log, + Config: &env.Config{ + BindAddress: *bindAddress, + APIVersion: *apiVersion, + LogFormatterType: *logFormatterType, + }, + } + // Log that we're starting the server log.WithFields(logrus.Fields{ "address": *bindAddress, diff --git a/routes.go b/routes.go deleted file mode 100644 index b958a8d..0000000 --- a/routes.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "log" - "net/http" - - "github.com/lavab/api/routes" -) - -type handleFunc func(w http.ResponseWriter, r *http.Request) - -type route struct { - Path string `json:"path"` - HandleFunc handleFunc `json:"-"` - Method string `json:"method"` - Description string `json:"desc"` -} - -var publicRoutes = []route{ - route{"/", listRoutes, "GET", "List of public and auth methods"}, - route{"/keys/{id}", routes.Key, "GET", ""}, - route{"/login", routes.Login, "POST", ""}, - route{"/signup", routes.Signup, "POST", ""}, -} - -var authRoutes = []route{ - route{"/logout", routes.Logout, "DELETE", "Destroys the current session"}, - route{"/me", routes.Me, "GET", "Fetch profile data for the current user"}, - route{"/me", routes.UpdateMe, "PUT", "Update data for the current user (settings, billing data, password, etc.)"}, - route{"/me/sessions", routes.Sessions, "GET", "Lists all the active sessions for the current user"}, - route{"/me/wipe-user-data", routes.WipeUserData, "DELETE", "Deletes all personal data of the user, except for basic profile information and billing status"}, - route{"/me/delete-account", routes.DeleteAccount, "DELETE", "Permanently deletes the user account"}, - route{"/threads", routes.Threads, "GET", "List email threads for the current user"}, - route{"/threads/{id}", routes.Thread, "GET", "Fetch a specific email thread"}, - route{"/threads/{id}", routes.UpdateThread, "PUT", "Update an email thread"}, - route{"/emails", routes.Emails, "GET", "List all emails for the current user"}, - route{"/emails", routes.CreateEmail, "POST", "Create and send an email"}, - route{"/emails/{id}", routes.Email, "GET", "Fetch a specific email"}, - route{"/emails/{id}", routes.UpdateEmail, "PUT", "Update a specific email (label, archive, etc)"}, - route{"/emails/{id}", routes.DeleteEmail, "DELETE", "Delete an email"}, - route{"/labels", routes.Labels, "GET", "List labels for the current user"}, - route{"/labels", routes.CreateLabel, "POST", "Create a new label"}, - route{"/labels/{id}", routes.Label, "GET", "Fetch a specific label"}, - route{"/labels/{id}", routes.UpdateLabel, "PUT", "Update a label"}, - route{"/labels/{id}", routes.DeleteLabel, "DELETE", "Delete a label"}, - route{"/contacts", routes.Contacts, "GET", "List all contacts for the current user"}, - route{"/contacts", routes.CreateContact, "POST", "Create a new contact"}, - route{"/contacts/{id}", routes.Contact, "GET", "Fetch a specific contact"}, - route{"/contacts/{id}", routes.UpdateContact, "PUT", "Update a contact"}, - route{"/contacts/{id}", routes.DeleteContact, "DELETE", "Delete a contact"}, - route{"/keys", routes.SubmitKey, "POST", "Submit a key to the Lavaboom private server"}, - route{"/keys/{id}", routes.VoteKey, "POST", "Vote or flag a key"}, -} - -func listRoutes(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, config.RootJSON) -} - -func rootResponseString() string { - tmp, err := json.Marshal( - map[string]interface{}{ - "message": "Lavaboom API", - "docs_url": "http://lavaboom.readme.io/", - "version": cApiVersion, - "routes": map[string]interface{}{ - "public": publicRoutes, - "auth": authRoutes, - }}) - if err != nil { - log.Fatalln("Error! Couldn't marshal JSON.", err) - } - return string(tmp) -} diff --git a/routes/me.go b/routes/accounts.go similarity index 100% rename from routes/me.go rename to routes/accounts.go diff --git a/routes/hello.go b/routes/hello.go new file mode 100644 index 0000000..4e33c0f --- /dev/null +++ b/routes/hello.go @@ -0,0 +1,22 @@ +package routes + +import ( + "net/http" + + "github.com/lavab/api/env" + "github.com/lavab/api/utils" +) + +type HelloResponse struct { + Message string `json:"message"` + DocsURL string `json:"docs_url"` + Version string `json:"version"` +} + +func Hello(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 200, &HelloResponse{ + Message: "Lavaboom API", + DocsURL: "http://lavaboom.readme.io/", + Version: env.G.Config.APIVersion, + }) +} diff --git a/utils/json.go b/utils/json.go index 52bdedd..cc58f91 100644 --- a/utils/json.go +++ b/utils/json.go @@ -6,16 +6,6 @@ import ( "log" ) -// MakeJSON is a wrapper over json.Marshal that returns an error message if an error occured -func MakeJSON(data map[string]interface{}) string { - res, err := json.Marshal(data) - if err != nil { - log.Fatalln("Error marshalling the response body.", err) - return "{\"status\":500,\"message\":\"Error occured while marshalling the response body\"}" - } - return string(res) -} - // ReadJSON reads a JSON string as a generic map string func ReadJSON(r io.Reader) (map[string]interface{}, error) { decoder := json.NewDecoder(r) diff --git a/utils/responses.go b/utils/responses.go index 31c1c15..c1649bc 100644 --- a/utils/responses.go +++ b/utils/responses.go @@ -1,17 +1,38 @@ package utils import ( - "fmt" "net/http" + + "github.com/lavab/api/env" ) +// marshalJSON is a wrapper over json.Marshal that returns an error message if an error occured +func marshalJSON(data interface{}) ([]byte, error) { + result, err := json.Marshal(data) + if err != nil { + return `{"status":500,"message":"Error occured while marshalling the response body"}`, err + } + return result, nil +} + // JSONResponse writes JSON to an http.ResponseWriter with the corresponding status code -func JSONResponse(w http.ResponseWriter, status int, data map[string]interface{}) { +func JSONResponse(w http.ResponseWriter, status int, data interface{}) { + // Get rid of the invalid status codes if status < 100 || status > 599 { status = 200 } + + // Try to marshal the input + result, err := marshalJSON(data) + if err != nil { + env.G.Log.WithFields(logrus.Fields{ + "error": err, + }).Error("Unable to marshal a message") + } + + // Write the result w.WriteHeader(status) - fmt.Fprint(w, MakeJSON(data)) + w.Write(result) } // ErrorResponse TODO @@ -25,7 +46,7 @@ func ErrorResponse(w http.ResponseWriter, code int, message string, debug string if debug == "" { delete(out, "debug") } - fmt.Fprint(w, MakeJSON(out)) + w.Write(marshalJSON(out)) } // FormatNotRecognizedResponse TODO From ee04eab113a354cfeab80d8e6ba40734c1557a50 Mon Sep 17 00:00:00 2001 From: Andrei Simionescu Date: Thu, 6 Nov 2014 17:34:40 +0100 Subject: [PATCH 04/20] Refactoring models Deleted models.base folder (now models.base_*.go files) Renamed sessions to auth tokens Removed some pointless helped methods --- auth.go | 4 +- dbutils/sessions.go | 5 +- models/auth_token.go | 7 +++ models/base/expiring.go | 25 ---------- models/base/resource.go | 42 ----------------- .../{base/encrypted.go => base_encrypted.go} | 26 ++--------- models/base_expiring.go | 22 +++++++++ models/base_resource.go | 46 +++++++++++++++++++ models/contact.go | 6 +-- models/email.go | 8 ++-- models/file.go | 8 ++-- models/label.go | 4 +- models/session.go | 24 ---------- models/thread.go | 13 ++---- models/user.go | 4 +- routes/me.go | 5 +- routes/sessions.go | 14 ++---- 17 files changed, 104 insertions(+), 159 deletions(-) create mode 100644 models/auth_token.go delete mode 100644 models/base/expiring.go delete mode 100644 models/base/resource.go rename models/{base/encrypted.go => base_encrypted.go} (59%) create mode 100644 models/base_expiring.go create mode 100644 models/base_resource.go delete mode 100644 models/session.go diff --git a/auth.go b/auth.go index 4d6079d..eef27cb 100644 --- a/auth.go +++ b/auth.go @@ -23,8 +23,8 @@ func AuthWrapper(next handleFunc) handleFunc { utils.ErrorResponse(w, 401, "Invalid auth token", "") return } - if session.HasExpired() { - utils.ErrorResponse(w, 419, "Authentication token has expired", "Session has expired on "+session.ExpDate) + if session.Expired() { + utils.ErrorResponse(w, 419, "Authentication token has expired", "") db.Delete("sessions", session.ID) return } diff --git a/dbutils/sessions.go b/dbutils/sessions.go index fec1168..8fe4641 100644 --- a/dbutils/sessions.go +++ b/dbutils/sessions.go @@ -7,8 +7,9 @@ import ( "github.com/lavab/api/models" ) -func GetSession(id string) (*models.Session, bool) { - var result models.Session +// TODO change names to auth tokens instead of sessions +func GetSession(id string) (*models.AuthToken, bool) { + var result models.AuthToken response, err := db.Get("sessions", id) if err == nil && response != nil && !response.IsNil() { err := response.One(&result) diff --git a/models/auth_token.go b/models/auth_token.go new file mode 100644 index 0000000..64acd90 --- /dev/null +++ b/models/auth_token.go @@ -0,0 +1,7 @@ +package models + +// AuthToken is a UUID used for user authentication, stored in the "auth_tokens" database +type AuthToken struct { + Expiring + Resource +} diff --git a/models/base/expiring.go b/models/base/expiring.go deleted file mode 100644 index c25c1f6..0000000 --- a/models/base/expiring.go +++ /dev/null @@ -1,25 +0,0 @@ -package base - -import ( - "log" - "time" -) - -// Expiring is a base struct for resources that expires e.g. sessions. -type Expiring struct { - // ExpDate is the RFC3339-encoded time when the resource will expire. - ExpDate string `json:"exp_date" gorethink:"exp_date"` -} - -// HasExpired returns true if the resource has expired (or if the ExpDate string is badly formatted) -func (e *Expiring) HasExpired() bool { - t, err := time.Parse(time.RFC3339, e.ExpDate) - if err != nil { - log.Println("Bad format! The expiry date is not RFC3339-formatted.", err) - return true - } - if time.Now().UTC().After(t) { - return true - } - return false -} diff --git a/models/base/resource.go b/models/base/resource.go deleted file mode 100644 index 315bd0c..0000000 --- a/models/base/resource.go +++ /dev/null @@ -1,42 +0,0 @@ -package base - -import "github.com/lavab/api/utils" - -// Resource is the base struct for every resource that needs to be saved to db and marshalled with json. -type Resource struct { - // ID is the resources ID, used as a primary key by the db. - ID string `json:"id" gorethink:"id"` - - // DateChanged is an RFC3339-encoded time string that lets clients poll whether a resource has changed. - DateChanged string `json:"date_changed" gorethink:"date_changed"` - - // DateCreated is an RFC3339-encoded string that stores the creation date of a resource. - DateCreated string `json:"date_created" gorethink:"date_created"` - - // Name is a human-friendly description of the resource. - // Sometimes it can be essential to the resource, e.g. the `User.Name` field. - Name string `json:"name" gorethink:"name,omitempty"` - - // UserID is the ID of the user to which this resource belongs. - UserID string `json:"user_id" gorethink:"user_id"` -} - -// MakeResource creates a new Resource object with sane defaults. -func MakeResource(userID, name string) Resource { - t := utils.TimeNowString() - return Resource{ - ID: utils.UUID(), - DateChanged: t, - DateCreated: t, - Name: name, - UserID: userID, - } -} - -// Touch sets r.DateChanged to the current time -// It returns the object it's modifying to allow chaining for shorter code: -// r.Touch().PerformSomeOp(args) -func (r *Resource) Touch() *Resource { - r.DateChanged = utils.TimeNowString() - return r -} diff --git a/models/base/encrypted.go b/models/base_encrypted.go similarity index 59% rename from models/base/encrypted.go rename to models/base_encrypted.go index 5671d44..359f066 100644 --- a/models/base/encrypted.go +++ b/models/base_encrypted.go @@ -1,17 +1,12 @@ -package base +package models -import ( - "log" - "strings" -) - -// Encrypted is the base struct for PGP-encrypted resources +// Encrypted is the base struct for PGP-encrypted resources. 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"` + PgpFingerprints []string `json:"pgp_fingerprints" gorethink:"pgp_fingerprints"` // Data is the raw, PGP-encrypted data Data []byte `json:"raw" gorethink:"raw"` @@ -28,18 +23,3 @@ type Encrypted struct { // Schemas with different minor versions should be compatible. VersionMinor int `json:"version_minor" gorethink:"version_minor"` } - -// IsCompatibleWith checks whether the schema versions of two Encrypted objects are compatible -func (e *Encrypted) IsCompatibleWith(res *Encrypted) bool { - if e == nil || res == nil { - log.Printf("[models.IsCompatibleWith] %+v or %+v were nil\n", e, res) - return false - } - if strings.ToLower(e.Schema) != strings.ToLower(res.Schema) { - return false - } - if e.VersionMajor == res.VersionMajor { - return true - } - return false -} diff --git a/models/base_expiring.go b/models/base_expiring.go new file mode 100644 index 0000000..8221358 --- /dev/null +++ b/models/base_expiring.go @@ -0,0 +1,22 @@ +package models + +import "time" + +// Expiring is a base struct for resources that expires e.g. sessions. +type Expiring struct { + // ExpiryDate indicates when an object will expire + ExpiryDate time.Time `json:"expiry_date" gorethink:"expiry_date"` +} + +// Expired checks whether an object has expired. It returns true if ExpiryDate is in the past. +func (e *Expiring) Expired() bool { + if time.Now().UTC().After(e.ExpiryDate) { + return true + } + return false +} + +// ExpireAfterNHours sets e.ExpiryDate to time.Now().UTC() + n hours +func (e *Expiring) ExpireAfterNHours(n int) { + e.ExpiryDate = time.Now().UTC().Add(time.Duration(n) * time.Hour) +} diff --git a/models/base_resource.go b/models/base_resource.go new file mode 100644 index 0000000..5fa4809 --- /dev/null +++ b/models/base_resource.go @@ -0,0 +1,46 @@ +package models + +import ( + "time" + + "github.com/lavab/api/utils" +) + +// Resource is the base type for API resources. +type Resource struct { + // ID is the resources ID, used as a primary key by the db. + // For some resources (invites, auth tokens) this is also the data itself. + ID string `json:"id" gorethink:"id"` + + // DateCreated is, shockingly, the date when the resource was created. + DateCreated time.Time `json:"date_created" gorethink:"date_created"` + + // DateModified records the time of the last change of the resource. + DateModified time.Time `json:"date_modified" gorethink:"date_modified"` + + // Name is a human-friendly description of the resource. + // Sometimes it can be essential to the resource, e.g. the `Account.Name` field. + Name string `json:"name" gorethink:"name,omitempty"` + + // AccountID is the ID of the user account that owns this resource. + AccountID string `json:"user_id" gorethink:"user_id"` +} + +// MakeResource creates a new Resource object with sane defaults. +func MakeResource(userID, name string) Resource { + t := time.Now() + return Resource{ + ID: utils.UUID(), + DateModified: t, + DateCreated: t, + Name: name, + AccountID: userID, + } +} + +// Touch sets the time the resource was last modified to time.Now(). +// For convenience (e.g. chaining) it also returns the resource pointer. +func (r *Resource) Touch() *Resource { + r.DateModified = time.Now() + return r +} diff --git a/models/contact.go b/models/contact.go index e07bc05..9582750 100644 --- a/models/contact.go +++ b/models/contact.go @@ -1,11 +1,9 @@ package models -import "github.com/lavab/api/models/base" - // Contact is the data model for a contact. type Contact struct { - base.Encrypted - base.Resource + Encrypted + Resource // Picture is a profile picture Picture Avatar `json:"picture" gorethink:"picture"` diff --git a/models/email.go b/models/email.go index a24eec6..610570c 100644 --- a/models/email.go +++ b/models/email.go @@ -1,18 +1,16 @@ package models -import "github.com/lavab/api/models/base" - // Email is the cornerstone of our application. // TODO mime info type Email struct { - base.Resource + Resource // AttachmentsIDs is a slice of the FileIDs associated with this email // For uploading attachments see `POST /upload` AttachmentIDs []string `json:"attachments" gorethink:"attachments"` // Body contains all the data needed to send this email - Body base.Encrypted `json:"body" gorethink:"body"` + Body Encrypted `json:"body" gorethink:"body"` LabelIDs []string `json:"label_ids" gorethink:"label_ids"` @@ -21,7 +19,7 @@ type Email struct { // Headers []string // Body string // Snippet string - Preview base.Encrypted `json:"preview" gorethink:"preview"` + Preview Encrypted `json:"preview" gorethink:"preview"` // ThreadID ThreadID string `json:"thread_id" gorethink:"thread_id"` diff --git a/models/file.go b/models/file.go index 273ca0f..47925c6 100644 --- a/models/file.go +++ b/models/file.go @@ -1,11 +1,9 @@ package models -import "github.com/lavab/api/models/base" - // File is an encrypted file stored by Lavaboom type File struct { - base.Encrypted - base.Resource + Encrypted + Resource // Mime is the Internet media type of the file // Check out: http://en.wikipedia.org/wiki/Internet_media_type @@ -21,7 +19,7 @@ type File struct { // Avatar is a picture used to identify contacts type Avatar struct { - base.Resource + Resource Large File `json:"data" gorethink:"data"` Medium File `json:"thumb_small" gorethink:"thumb_small"` Small File `json:"thumb_large" gorethink:"thumb_large"` diff --git a/models/label.go b/models/label.go index f953347..10e383c 100644 --- a/models/label.go +++ b/models/label.go @@ -1,9 +1,7 @@ package models -import "github.com/lavab/api/models/base" - type Label struct { - base.Resource + Resource EmailsUnread int `json:"emails_unread" gorethink:"emails_unread"` EmailsTotal int `json:"emails_total" gorethink:"emails_total"` Hidden bool `json:"hidden" gorethink:"hidden"` diff --git a/models/session.go b/models/session.go deleted file mode 100644 index 4b39d46..0000000 --- a/models/session.go +++ /dev/null @@ -1,24 +0,0 @@ -package models - -import ( - "log" - "net/http" - - "github.com/gorilla/context" - "github.com/lavab/api/models/base" -) - -// Session TODO -type Session struct { - base.Expiring - base.Resource -} - -// CurrentSession returns the current request's session object -func CurrentSession(r *http.Request) *Session { - session, ok := context.Get(r, "session").(*Session) - if !ok { - log.Fatalln("Session data in gorilla/context was not found or malformed.") - } - return session -} diff --git a/models/thread.go b/models/thread.go index 2949f37..0e64e66 100644 --- a/models/thread.go +++ b/models/thread.go @@ -1,19 +1,12 @@ package models -import "github.com/lavab/api/models/base" - // Thread is the data model for a conversation. type Thread struct { - base.Resource + Resource - // Emails is a slice containing email IDs. - // Ideally the array should be ordered by creation date, newest first - EmailIDs []string `json:"email_ids" gorethink:"email_ids"` + // Emails is an array of email IDs belonging to this thread + Emails []string `json:"emails" gorethink:"emails"` // Members is a slice containing userIDs or email addresses for all members of the thread Members []string `json:"members" gorethink:"members"` - - // Preview contains the thread details to be shown in the list of emails - // This should be - Preview base.Encrypted `json:"preview" gorethink:"preview"` } diff --git a/models/user.go b/models/user.go index 5871df3..0f046fb 100644 --- a/models/user.go +++ b/models/user.go @@ -1,10 +1,8 @@ package models -import "github.com/lavab/api/models/base" - // User stores essential data for a Lavaboom user, and is thus not encrypted. type User struct { - base.Resource + Resource // Billing is a struct containing billing information. Billing BillingData `json:"billing" gorethink:"billing"` diff --git a/routes/me.go b/routes/me.go index b25ea6c..43fe1c1 100644 --- a/routes/me.go +++ b/routes/me.go @@ -6,6 +6,7 @@ import ( "log" "net/http" + "github.com/gorilla/context" "github.com/lavab/api/db" "github.com/lavab/api/dbutils" "github.com/lavab/api/models" @@ -14,8 +15,8 @@ import ( // Me returns information about the current user (more exactly, a JSONized models.User) func Me(w http.ResponseWriter, r *http.Request) { - session := models.CurrentSession(r) - user, ok := dbutils.GetUser(session.UserID) + session, _ := context.Get(r, "session").(*models.AuthToken) + user, ok := dbutils.GetUser(session.AccountID) if !ok { debug := fmt.Sprintf("Session %s was deleted", session.ID) if err := db.Delete("sessions", session.ID); err != nil { diff --git a/routes/sessions.go b/routes/sessions.go index 29b2566..2951f7d 100644 --- a/routes/sessions.go +++ b/routes/sessions.go @@ -9,7 +9,6 @@ import ( "github.com/lavab/api/db" "github.com/lavab/api/dbutils" "github.com/lavab/api/models" - "github.com/lavab/api/models/base" "github.com/lavab/api/utils" ) @@ -26,11 +25,8 @@ func Login(w http.ResponseWriter, r *http.Request) { } // TODO check number of sessions for the current user here - session := models.Session{ - Expiring: base.Expiring{utils.HoursFromNowString(SessionDurationInHours)}, - Resource: base.MakeResource(user.ID, ""), - } - session.Name = fmt.Sprintf("Auth session expiring on %s", session.ExpDate) + session := models.AuthToken{Resource: models.MakeResource(user.ID, "")} + session.ExpireAfterNHours(SessionDurationInHours) db.Insert("sessions", session) utils.JSONResponse(w, 200, map[string]interface{}{ @@ -60,7 +56,7 @@ func Signup(w http.ResponseWriter, r *http.Request) { // TODO: sanitize user name (i.e. remove caps, periods) user := models.User{ - Resource: base.MakeResource(utils.UUID(), username), + Resource: models.MakeResource(utils.UUID(), username), Password: string(hash), } @@ -78,13 +74,13 @@ func Signup(w http.ResponseWriter, r *http.Request) { // Logout destroys the current session token func Logout(w http.ResponseWriter, r *http.Request) { - session := context.Get(r, "session").(*models.Session) + session := context.Get(r, "session").(*models.AuthToken) if err := db.Delete("sessions", session.ID); err != nil { utils.ErrorResponse(w, 500, "Internal server error", fmt.Sprint("Couldn't delete session %v. %v", session, err)) } utils.JSONResponse(w, 410, map[string]interface{}{ - "message": fmt.Sprintf("Successfully logged out", session.UserID), + "message": fmt.Sprintf("Successfully logged out", session.AccountID), "success": true, "deleted": session.ID, }) From 48de48ea931f7bd76a3d7c8e414c310d41659679 Mon Sep 17 00:00:00 2001 From: Andrei Simionescu Date: Thu, 6 Nov 2014 18:04:37 +0100 Subject: [PATCH 05/20] Rm avatar, comment --- models/file.go | 8 -------- models/invite.go | 18 ++++++++++++++++++ models/label.go | 5 +++++ 3 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 models/invite.go diff --git a/models/file.go b/models/file.go index 47925c6..a5f85c8 100644 --- a/models/file.go +++ b/models/file.go @@ -16,11 +16,3 @@ type File struct { // Possible values: `file`, `audio`, `video`, `pdf`, `text`, `binary` Type string `json:"type" gorethink:"type"` } - -// Avatar is a picture used to identify contacts -type Avatar struct { - Resource - Large File `json:"data" gorethink:"data"` - Medium File `json:"thumb_small" gorethink:"thumb_small"` - Small File `json:"thumb_large" gorethink:"thumb_large"` -} diff --git a/models/invite.go b/models/invite.go new file mode 100644 index 0000000..b4a1cc2 --- /dev/null +++ b/models/invite.go @@ -0,0 +1,18 @@ +package models + +// Invite is a token (Invite.ID) that allows a user +type Invite struct { + Expiring + Resource + + // AccountCreated is the ID of the account that was created using this invite. + AccountCreated string + + // Username is the desired username. It can be blank. + Username string +} + +// Used returns whether this invitation has been used +func (i *Invite) Used() bool { + return i.DateCreated != i.DateModified +} diff --git a/models/label.go b/models/label.go index 10e383c..a745c98 100644 --- a/models/label.go +++ b/models/label.go @@ -1,5 +1,10 @@ package models +// TODO: nested labels? + +// Label is what IMAP calls folders, some providers call tags, and what we (and Gmail) call labels. +// It's both a simple way for users to organise their emails, but also a way to provide classic folder +// functionality (inbox, spam, drafts, etc). For example, to "archive" an email means to remove the "inbox" label. type Label struct { Resource EmailsUnread int `json:"emails_unread" gorethink:"emails_unread"` From 86585fc3031087009300c35178d4933424b2c71b Mon Sep 17 00:00:00 2001 From: "Piotr \"Orange\" Zduniak" Date: Thu, 6 Nov 2014 22:33:46 +0100 Subject: [PATCH 06/20] routes/accounts.go rewritten, various small changes --- db/setup.go | 2 +- main.go | 3 +- routes/accounts.go | 219 ++++++++++++++++++++++++++++++++++++++++----- utils/requests.go | 85 ++++++++++++++++++ utils/responses.go | 55 ------------ 5 files changed, 287 insertions(+), 77 deletions(-) create mode 100644 utils/requests.go delete mode 100644 utils/responses.go diff --git a/db/setup.go b/db/setup.go index 2fcbfb0..5156c86 100644 --- a/db/setup.go +++ b/db/setup.go @@ -33,7 +33,7 @@ var tablesAndIndexes = map[string][]string{ "keys": []string{}, } -func Init() { +func init() { config.Url = "localhost:28015" config.AuthKey = "" config.Db = "dev" diff --git a/main.go b/main.go index 3ecc96f..148d60d 100644 --- a/main.go +++ b/main.go @@ -68,7 +68,8 @@ func main() { auth.Get("/accounts/:id", routes.AccountsGet) auth.Put("/accounts/:id", routes.AccountsUpdate) auth.Delete("/accounts/:id", routes.AccountsDelete) - auth.Post("/accounts/:id/wipe-user-data", routes.AccountsWipeUserData) + auth.Post("/accounts/:id/wipe-data", routes.AccountsWipeUserData) + auth.Get("/accounts/:id/sessions", routes.AccountsSessionsList) // Tokens auth.Get("/token", routes.TokenGet) diff --git a/routes/accounts.go b/routes/accounts.go index b25ea6c..ebad3ec 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -6,41 +6,220 @@ import ( "log" "net/http" + "github.com/Sirupsen/logrus" + "github.com/zenazn/goji/web" + "github.com/lavab/api/db" "github.com/lavab/api/dbutils" + "github.com/lavab/api/env" "github.com/lavab/api/models" "github.com/lavab/api/utils" ) -// Me returns information about the current user (more exactly, a JSONized models.User) -func Me(w http.ResponseWriter, r *http.Request) { +// Accounts list +type AccountsListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +func AccountsList(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &AccountsListResponse{ + Success: false, + Message: "Method not implemented", + }) +} + +// Account registration +type AccountsCreateRequest struct { + Username string `json:"username" schema:"username"` + Password string `json:"password" schema:"password"` +} + +type AccountsCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + User *models.User `json:"data,omitempty"` +} + +func AccountsCreate(w http.ResponseWriter, r *http.Request) { + // Decode the request + var input AccountsCreateRequest + err := utils.ParseRequest(r, input) + if err != nil { + env.G.Log.WithFields(logrus.Fields{ + "error": err, + }).Warning("Unable to decode a request") + + utils.JSONResponse(w, 409, &AccountsCreateResponse{ + Success: false, + Message: "Invalid input format", + }) + return + } + + // Ensure that the user with requested username doesn't exist + if _, ok := dbutils.FindUserByName(username); ok { + utils.JSONResponse(w, 409, &AccountsCreateResponse{ + Success: false, + Message: "Username already exists", + }) + return + } + + // Try to hash the password + hash, err := utils.BcryptHash(password) + if err != nil { + utils.JSONResponse(w, 500, &AccountsCreateResponse{ + Success: false, + Message: "Internal server error - AC/CR/01", + }) + + env.G.Log.WithFields(logrus.Fields{ + "error": err, + }).Error("Unable to hash a password") + return + } + + // TODO: sanitize user name (i.e. remove caps, periods) + + // Create a new user object + user := &models.User{ + Resource: base.MakeResource(utils.UUID(), username), + Password: string(hash), + } + + // Try to save it in the database + if err := db.Insert("users", user); err != nil { + utils.JSONResponse(w, 500, &AccountsCreateResponse{ + Success: false, + Message: "Internal server error - AC/CR/02", + }) + + env.G.Log.WithFields(logrus.Fields{ + "error": err, + }).Error("Could not insert an user to the database") + return + } + + utils.JSONResponse(w, 201, &AccountsCreateResponse{ + Success: true, + Message: "A new account was successfully created", + User: user, + }) +} + +// AccountsGet returns the information about the specified account +type AccountsGetResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + User *models.User `json:"user,omitempty"` +} + +func AccountsGet(c *web.C, w http.ResponseWriter, r *http.Request) { + // Get the account ID from the request + id, ok := c.URLParams["id"] + if !ok { + utils.JSONResponse(409, &AccountsGetResponse{ + Success: false, + Message: "Invalid user ID", + }) + return + } + + // Right now we only support "me" as the ID + if id != "me" { + utils.JSONResponse(501, &AccountsGetResponse{ + Success: false, + Message: `Only the "me" user is implemented`, + }) + return + } + + // Fetch the current session from the database session := models.CurrentSession(r) + + // Fetch the user object from the database user, ok := dbutils.GetUser(session.UserID) if !ok { - debug := fmt.Sprintf("Session %s was deleted", session.ID) + // The session refers to a non-existing user + env.G.Log.WithFields(logrus.Fields{ + "id": session.ID, + }).Warning("Valid session referred to a removed account") + + // Try to remove the orphaned session if err := db.Delete("sessions", session.ID); err != nil { - debug = "Error when trying to delete session associated with inactive account" - log.Println("[routes.Me]", debug, err) + env.G.Log.WithFields(logrus.Fields{ + "id": session.ID, + "error": err, + }).Error("Unable to remove an orphaned session") + } else { + env.G.Log.WithFields(logrus.Fields{ + "id": session.ID, + }).Info("Removed an orphaned session") } - utils.ErrorResponse(w, 410, "Account deactivated", debug) - return - } - str, err := json.Marshal(user) - if err != nil { - debug := fmt.Sprint("Failed to marshal models.User:", user) - log.Println("[routes.Me]", debug) - utils.ErrorResponse(w, 500, "Internal server error", debug) + + utils.JSONResponse(410, &AccountsGetResponse{ + Success: false, + Message: "Account disabled", + }) return } - fmt.Fprint(w, string(str)) + + // Return the user struct + utils.JSONResponse(200, &AccountsGetResponse{ + Success: true, + User: user, + }) +} + +// AccountsUpdate TODO +type AccountsUpdateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +func AccountsUpdate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(501, &AccountsUpdateResponse{ + Success: false, + Message: `Sorry, not implemented yet`, + }) +} + +// AccountsDelete TODO +type AccountsDeleteResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +func AccountsDelete(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(501, &AccountsDeleteResponse{ + Success: false, + Message: `Sorry, not implemented yet`, + }) +} + +// AccountsWipeData TODO +type AccountsWipeDataResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +func AccountsWipeData(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(501, &AccountsWipeDataResponse{ + Success: false, + Message: `Sorry, not implemented yet`, + }) } -// UpdateMe TODO -func UpdateMe(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// AccountsSessionsList TODO +type AccountsSessionsListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` } -// Sessions lists all active sessions for current user -func Sessions(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +func AccountsSessionsList(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(501, &AccountsSessionsListResponse{ + Success: false, + Message: `Sorry, not implemented yet`, + }) } diff --git a/utils/requests.go b/utils/requests.go new file mode 100644 index 0000000..b1e5d2e --- /dev/null +++ b/utils/requests.go @@ -0,0 +1,85 @@ +package utils + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "strings" + + "github.com/gorilla/schema" + + "github.com/lavab/api/env" +) + +var ( + // Error declarations + ErrInvalidContentType = errors.New("Invalid request content type") + + // gorilla/schema decoder is a shared object, as it caches information about structs + decoder = schema.NewDecoder() +) + +// JSONResponse writes JSON to an http.ResponseWriter with the corresponding status code +func JSONResponse(w http.ResponseWriter, status int, data interface{}) { + // Get rid of the invalid status codes + if status < 100 || status > 599 { + status = 200 + } + + // Try to marshal the input + result, err := json.Marshal(data) + if err != nil { + // Log the error + env.G.Log.WithFields(logrus.Fields{ + "error": err, + }).Error("Unable to marshal a message") + + // Set the result to the default value to prevent empty responses + result = []byte(`{"status":500,"message":"Error occured while marshalling the response body"}`) + } + + // Write the result + w.WriteHeader(status) + w.Write(result) +} + +func ParseRequest(r *http.Request, data interface{}) error { + // Get the contentType for comparsions + contentType = r.Header.Get("Content-Type") + + // Deterimine the passed ContentType + if strings.Contains(contentType, "application/json") { + // It's JSON, so read the body into a variable + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + + // And then unmarshal it into the passed interface + err = json.Unmarshal(body, data) + if err != nil { + return err + } + + return nil + } else if contentType == "" || + strings.Contains(contentType, "application/x-www-form-urlencoded") || + strings.Contains(contentType, "multipart/form-data") { + // net/http should be capable of parsing the form data + err := r.ParseForm() + if err != nil { + return err + } + + // Unmarshal them into the passed interface + err = decoder.Decode(data, r.PostForm) + if err != nil { + return err + } + + return nil + } + + return ErrInvalidContentType +} diff --git a/utils/responses.go b/utils/responses.go deleted file mode 100644 index c1649bc..0000000 --- a/utils/responses.go +++ /dev/null @@ -1,55 +0,0 @@ -package utils - -import ( - "net/http" - - "github.com/lavab/api/env" -) - -// marshalJSON is a wrapper over json.Marshal that returns an error message if an error occured -func marshalJSON(data interface{}) ([]byte, error) { - result, err := json.Marshal(data) - if err != nil { - return `{"status":500,"message":"Error occured while marshalling the response body"}`, err - } - return result, nil -} - -// JSONResponse writes JSON to an http.ResponseWriter with the corresponding status code -func JSONResponse(w http.ResponseWriter, status int, data interface{}) { - // Get rid of the invalid status codes - if status < 100 || status > 599 { - status = 200 - } - - // Try to marshal the input - result, err := marshalJSON(data) - if err != nil { - env.G.Log.WithFields(logrus.Fields{ - "error": err, - }).Error("Unable to marshal a message") - } - - // Write the result - w.WriteHeader(status) - w.Write(result) -} - -// ErrorResponse TODO -func ErrorResponse(w http.ResponseWriter, code int, message string, debug string) { - out := map[string]interface{}{ - "debug": debug, - "message": message, - "status": code, - "success": false, - } - if debug == "" { - delete(out, "debug") - } - w.Write(marshalJSON(out)) -} - -// FormatNotRecognizedResponse TODO -func FormatNotRecognizedResponse(w http.ResponseWriter, err error) { - ErrorResponse(w, 400, "Format not recognized", err.Error()) -} From 5db30c40bb04135417113aa528ed4863159cdc8f Mon Sep 17 00:00:00 2001 From: "Piotr \"Orange\" Zduniak" Date: Thu, 6 Nov 2014 23:04:45 +0100 Subject: [PATCH 07/20] Rewritten tokens.go --- env/config.go | 1 + main.go | 8 ++- models/base/expiring.go | 8 +-- routes/actions.go | 16 ------ routes/sessions.go | 91 ----------------------------- routes/tokens.go | 124 ++++++++++++++++++++++++++++++++++++++++ utils/json.go | 18 ------ 7 files changed, 134 insertions(+), 132 deletions(-) delete mode 100644 routes/actions.go delete mode 100644 routes/sessions.go create mode 100644 routes/tokens.go delete mode 100644 utils/json.go diff --git a/env/config.go b/env/config.go index d60fb38..e0d4bdd 100644 --- a/env/config.go +++ b/env/config.go @@ -4,4 +4,5 @@ type Config struct { BindAddress string APIVersion string LogFormatterType string + SessionDuration int } diff --git a/main.go b/main.go index 148d60d..43df403 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ var ( bindAddress = flag.String("bind", ":5000", "Network address used to bind") apiVersion = flag.String("version", "v0", "Shown API version") logFormatterType = flag.String("log", "text", "Log formatter type. Either \"json\" or \"text\"") + sessionDuration = flag.Int("session_duration", 72, "Session duration expressed in hours") ) func main() { @@ -72,9 +73,9 @@ func main() { auth.Get("/accounts/:id/sessions", routes.AccountsSessionsList) // Tokens - auth.Get("/token", routes.TokenGet) - auth.Post("/token", routes.TokensCreate) - auth.Delete("/token", routes.TokensDelete) + auth.Get("/tokens", routes.TokenGet) + auth.Post("/tokens", routes.TokensCreate) + auth.Delete("/tokens", routes.TokensDelete) // Threads auth.Get("/threads", routes.ThreadsList) @@ -123,6 +124,7 @@ func main() { BindAddress: *bindAddress, APIVersion: *apiVersion, LogFormatterType: *logFormatterType, + SessionDuration: *sessionDuration, }, } diff --git a/models/base/expiring.go b/models/base/expiring.go index c25c1f6..f1bc4d6 100644 --- a/models/base/expiring.go +++ b/models/base/expiring.go @@ -7,13 +7,13 @@ import ( // Expiring is a base struct for resources that expires e.g. sessions. type Expiring struct { - // ExpDate is the RFC3339-encoded time when the resource will expire. - ExpDate string `json:"exp_date" gorethink:"exp_date"` + // ExpirationDate is the RFC3339-encoded time when the resource will expire. + ExpirationDate string `json:"exp_date" gorethink:"exp_date"` } -// HasExpired returns true if the resource has expired (or if the ExpDate string is badly formatted) +// HasExpired returns true if the resource has expired (or if the ExpirationDate string is badly formatted) func (e *Expiring) HasExpired() bool { - t, err := time.Parse(time.RFC3339, e.ExpDate) + t, err := time.Parse(time.RFC3339, e.ExpirationDate) if err != nil { log.Println("Bad format! The expiry date is not RFC3339-formatted.", err) return true diff --git a/routes/actions.go b/routes/actions.go deleted file mode 100644 index 232d3a1..0000000 --- a/routes/actions.go +++ /dev/null @@ -1,16 +0,0 @@ -package routes - -import ( - "fmt" - "net/http" -) - -// WipeUserData TODO -func WipeUserData(w http.ResponseWriter, r *http.Request) { - -} - -// DeleteAccount TODO -func DeleteAccount(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") -} diff --git a/routes/sessions.go b/routes/sessions.go deleted file mode 100644 index 29b2566..0000000 --- a/routes/sessions.go +++ /dev/null @@ -1,91 +0,0 @@ -package routes - -import ( - "fmt" - "log" - "net/http" - - "github.com/gorilla/context" - "github.com/lavab/api/db" - "github.com/lavab/api/dbutils" - "github.com/lavab/api/models" - "github.com/lavab/api/models/base" - "github.com/lavab/api/utils" -) - -const SessionDurationInHours = 72 - -// Login gets a username and password and returns a session token on success -func Login(w http.ResponseWriter, r *http.Request) { - username, password := r.FormValue("username"), r.FormValue("password") - user, ok := dbutils.FindUserByName(username) - if !ok || user == nil || !utils.BcryptVerify(user.Password, password) { - utils.ErrorResponse(w, 403, "Wrong username or password", - fmt.Sprintf("user: %+v", user)) - return - } - - // TODO check number of sessions for the current user here - session := models.Session{ - Expiring: base.Expiring{utils.HoursFromNowString(SessionDurationInHours)}, - Resource: base.MakeResource(user.ID, ""), - } - session.Name = fmt.Sprintf("Auth session expiring on %s", session.ExpDate) - db.Insert("sessions", session) - - utils.JSONResponse(w, 200, map[string]interface{}{ - "message": "Authentication successful", - "success": true, - "session": session, - }) -} - -// Signup gets a username and password and creates a user account on success -func Signup(w http.ResponseWriter, r *http.Request) { - username, password := r.FormValue("username"), r.FormValue("password") - // regt := r.FormValue("reg_token") - - if _, ok := dbutils.FindUserByName(username); ok { - utils.ErrorResponse(w, 409, "Username already exists", "") - return - } - - hash, err := utils.BcryptHash(password) - if err != nil { - msg := "Bcrypt hashing has failed" - utils.ErrorResponse(w, 500, "Internal server error", msg) - log.Fatalln(msg) - } - - // TODO: sanitize user name (i.e. remove caps, periods) - - user := models.User{ - Resource: base.MakeResource(utils.UUID(), username), - Password: string(hash), - } - - if err := db.Insert("users", user); err != nil { - utils.ErrorResponse(w, 500, "Internal server error", - fmt.Sprintf("Couldn't insert %+v to database", user)) - } - - utils.JSONResponse(w, 201, map[string]interface{}{ - "message": "Signup successful", - "success": true, - "data": user, - }) -} - -// Logout destroys the current session token -func Logout(w http.ResponseWriter, r *http.Request) { - session := context.Get(r, "session").(*models.Session) - if err := db.Delete("sessions", session.ID); err != nil { - utils.ErrorResponse(w, 500, "Internal server error", - fmt.Sprint("Couldn't delete session %v. %v", session, err)) - } - utils.JSONResponse(w, 410, map[string]interface{}{ - "message": fmt.Sprintf("Successfully logged out", session.UserID), - "success": true, - "deleted": session.ID, - }) -} diff --git a/routes/tokens.go b/routes/tokens.go new file mode 100644 index 0000000..8acfe7b --- /dev/null +++ b/routes/tokens.go @@ -0,0 +1,124 @@ +package routes + +import ( + "fmt" + "log" + "net/http" + "time" + + "github.com/gorilla/context" + "github.com/zenazn/goji/web" + + "github.com/lavab/api/db" + "github.com/lavab/api/dbutils" + "github.com/lavab/api/env" + "github.com/lavab/api/models" + "github.com/lavab/api/models/base" + "github.com/lavab/api/utils" +) + +type TokensGetResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Created string `json:"created,omitempty"` + Expires string `json:"expires,omitempty"` +} + +func TokensGet(w http.ResponseWriter, r *http.Request) { + // Fetch the current session from the database + session := models.CurrentSession(r) + + // Respond with the token information + utils.JSONResponse(200, &TokensGetResponse{ + Success: true, + Created: session.DateCreated, + Expires: session.ExpirationDate, + }) +} + +type TokensCreateRequest struct { + Username string `json:"username" schema:"username"` + Password string `json:"password" schema:"password"` +} + +type TokensCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Token *models.Session `json:"token,omitempty"` +} + +func TokensCreate(w http.ResponseWriter, r *http.Request) { + // Decode the request + var input TokensCreateRequest + err := utils.ParseRequest(r, input) + if err != nil { + env.G.Log.WithFields(logrus.Fields{ + "error": err, + }).Warning("Unable to decode a request") + + utils.JSONResponse(w, 409, &TokensCreateResponse{ + Success: false, + Message: "Invalid input format", + }) + return + } + + // Authenticate the user + user, ok := dbutils.FindUserByName(username) + if !ok || user == nil || !utils.BcryptVerify(user.Password, password) { + utils.JSONResponse(w, 403, &TokensCreateResponse{ + Success: false, + Message: "Wrong username or password", + }) + return + } + + // Calculate the expiration date + expDate := utils.HoursFromNowString(env.G.Config.SessionDuration) + + // Create a new token + token := &models.Session{ + Expiring: base.Expiring{expDate}, + Resource: base.MakeResource(user.ID, ""), + Name: "Auth token expiring on " + expDate, + } + + // Insert int into the database + db.Insert("sessions", token) + + // Respond with the freshly created token + utils.JSONResponse(w, 201, &TokensCreateResponse{ + Success: true, + Message: "Authentication successful", + Token: token, + }) +} + +// Logout destroys the current session token +type TokensDeleteResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +func TokensDelete(c *web.C, w http.ResponseWriter, r *http.Request) { + // Get the session from the middleware + session := c.Env["session"].(*models.Session) + + // Delete it from the database + if err := db.Delete("sessions", session.ID); err != nil { + env.G.Log.WithFields(logrus.Fields{ + "error": err, + }).Error("Unable to delete a session") + + utils.JSONResponse(w, 500, &TokensDeleteResponse{ + Success: true, + Message: "Internal server error - TO/DE/01", + }) + return + } + + utils.JSONResponse(w, 200, &TokensDeleteResponse{ + Success: true, + Message: "Successfully logged out", + }) +} diff --git a/utils/json.go b/utils/json.go deleted file mode 100644 index cc58f91..0000000 --- a/utils/json.go +++ /dev/null @@ -1,18 +0,0 @@ -package utils - -import ( - "encoding/json" - "io" - "log" -) - -// ReadJSON reads a JSON string as a generic map string -func ReadJSON(r io.Reader) (map[string]interface{}, error) { - decoder := json.NewDecoder(r) - out := map[string]interface{}{} - err := decoder.Decode(&out) - if err != nil { - return out, err - } - return out, nil -} From 8fd532d94fc2c2295314eae9d57668bac5ae732c Mon Sep 17 00:00:00 2001 From: "Piotr \"Orange\" Zduniak" Date: Thu, 6 Nov 2014 23:13:32 +0100 Subject: [PATCH 08/20] golint changes on the rewritten files --- routes/accounts.go | 22 +++++++++++++++------- routes/tokens.go | 8 +++++++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/routes/accounts.go b/routes/accounts.go index ebad3ec..ec72f21 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -16,12 +16,13 @@ import ( "github.com/lavab/api/utils" ) -// Accounts list +// 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, @@ -29,18 +30,20 @@ func AccountsList(w http.ResponseWriter, r *http.Request) { }) } -// Account registration +// AccountsCreateRequest contains the input for the AccountsCreate endpoint. type AccountsCreateRequest struct { Username string `json:"username" schema:"username"` Password string `json:"password" schema:"password"` } +// AccountsCreateResponse contains the output of the AccountsCreate request. type AccountsCreateResponse struct { Success bool `json:"success"` Message string `json:"message"` User *models.User `json:"data,omitempty"` } +// AccountsCreate creates a new account in the system. func AccountsCreate(w http.ResponseWriter, r *http.Request) { // Decode the request var input AccountsCreateRequest @@ -108,13 +111,14 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { }) } -// AccountsGet returns the information about the specified account +// AccountsGetResponse contains the result of the AccountsGet request. type AccountsGetResponse struct { Success bool `json:"success"` Message string `json:"message,omitempty"` User *models.User `json:"user,omitempty"` } +// AccountsGet returns the information about the specified account func AccountsGet(c *web.C, w http.ResponseWriter, r *http.Request) { // Get the account ID from the request id, ok := c.URLParams["id"] @@ -172,12 +176,13 @@ func AccountsGet(c *web.C, w http.ResponseWriter, r *http.Request) { }) } -// AccountsUpdate TODO +// AccountsUpdateResponse contains the result of the AccountsUpdate request. type AccountsUpdateResponse struct { Success bool `json:"success"` Message string `json:"message"` } +// AccountsUpdate allows changing the account's information (password etc.) func AccountsUpdate(w http.ResponseWriter, r *http.Request) { utils.JSONResponse(501, &AccountsUpdateResponse{ Success: false, @@ -185,12 +190,13 @@ func AccountsUpdate(w http.ResponseWriter, r *http.Request) { }) } -// AccountsDelete TODO +// AccountsDeleteResponse contains the result of the AccountsDelete request. type AccountsDeleteResponse struct { Success bool `json:"success"` Message string `json:"message"` } +// AccountsDelete allows deleting an account. func AccountsDelete(w http.ResponseWriter, r *http.Request) { utils.JSONResponse(501, &AccountsDeleteResponse{ Success: false, @@ -198,12 +204,13 @@ func AccountsDelete(w http.ResponseWriter, r *http.Request) { }) } -// AccountsWipeData TODO +// AccountsWipeDataResponse contains the result of the AccountsWipeData request. type AccountsWipeDataResponse struct { Success bool `json:"success"` Message string `json:"message"` } +// AccountsWipeData allows getting rid of the all data related to the account. func AccountsWipeData(w http.ResponseWriter, r *http.Request) { utils.JSONResponse(501, &AccountsWipeDataResponse{ Success: false, @@ -211,12 +218,13 @@ func AccountsWipeData(w http.ResponseWriter, r *http.Request) { }) } -// AccountsSessionsList TODO +// AccountsSessionsListResponse contains the result of the AccountsSessionsList request. type AccountsSessionsListResponse struct { Success bool `json:"success"` Message string `json:"message"` } +// AccountsSessionsList returns a list of all opened sessions. func AccountsSessionsList(w http.ResponseWriter, r *http.Request) { utils.JSONResponse(501, &AccountsSessionsListResponse{ Success: false, diff --git a/routes/tokens.go b/routes/tokens.go index 8acfe7b..1ff6392 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -17,6 +17,7 @@ import ( "github.com/lavab/api/utils" ) +// TokensGetResponse contains the result of the TokensGet request. type TokensGetResponse struct { Success bool `json:"success"` Message string `json:"message,omitempty"` @@ -24,6 +25,7 @@ type TokensGetResponse struct { Expires string `json:"expires,omitempty"` } +// TokensGet returns information about the current token. func TokensGet(w http.ResponseWriter, r *http.Request) { // Fetch the current session from the database session := models.CurrentSession(r) @@ -36,17 +38,20 @@ func TokensGet(w http.ResponseWriter, r *http.Request) { }) } +// TokensCreateRequest contains the input for the TokensCreate endpoint. type TokensCreateRequest struct { Username string `json:"username" schema:"username"` Password string `json:"password" schema:"password"` } +// TokensCreateResponse contains the result of the TokensCreate request. type TokensCreateResponse struct { Success bool `json:"success"` Message string `json:"message,omitempty"` Token *models.Session `json:"token,omitempty"` } +// TokensCreate allows logging in to an account. func TokensCreate(w http.ResponseWriter, r *http.Request) { // Decode the request var input TokensCreateRequest @@ -94,12 +99,13 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { }) } -// Logout destroys the current session token +// TokensDeleteResponse contains the result of the TokensDelete request. type TokensDeleteResponse struct { Success bool `json:"success"` Message string `json:"message"` } +// TokensDelete destroys the current session token. func TokensDelete(c *web.C, w http.ResponseWriter, r *http.Request) { // Get the session from the middleware session := c.Env["session"].(*models.Session) From 6f181a0306aa062540138c802d95b090051af051 Mon Sep 17 00:00:00 2001 From: "Piotr \"Orange\" Zduniak" Date: Thu, 6 Nov 2014 23:36:57 +0100 Subject: [PATCH 09/20] Rewrote other routes --- routes/accounts.go | 16 +++---- routes/contacts.go | 80 +++++++++++++++++++++++-------- routes/emails.go | 114 +++++++++++++++++++++++++++++---------------- routes/hello.go | 2 + routes/keys.go | 46 +++++++++++++----- routes/labels.go | 75 +++++++++++++++++++++++------ routes/threads.go | 48 +++++++++++++++---- routes/tokens.go | 2 +- 8 files changed, 276 insertions(+), 107 deletions(-) diff --git a/routes/accounts.go b/routes/accounts.go index ec72f21..18543b3 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -123,7 +123,7 @@ func AccountsGet(c *web.C, w http.ResponseWriter, r *http.Request) { // Get the account ID from the request id, ok := c.URLParams["id"] if !ok { - utils.JSONResponse(409, &AccountsGetResponse{ + utils.JSONResponse(w, 409, &AccountsGetResponse{ Success: false, Message: "Invalid user ID", }) @@ -132,7 +132,7 @@ 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(501, &AccountsGetResponse{ + utils.JSONResponse(w, 501, &AccountsGetResponse{ Success: false, Message: `Only the "me" user is implemented`, }) @@ -162,7 +162,7 @@ func AccountsGet(c *web.C, w http.ResponseWriter, r *http.Request) { }).Info("Removed an orphaned session") } - utils.JSONResponse(410, &AccountsGetResponse{ + utils.JSONResponse(w, 410, &AccountsGetResponse{ Success: false, Message: "Account disabled", }) @@ -170,7 +170,7 @@ func AccountsGet(c *web.C, w http.ResponseWriter, r *http.Request) { } // Return the user struct - utils.JSONResponse(200, &AccountsGetResponse{ + utils.JSONResponse(w, 200, &AccountsGetResponse{ Success: true, User: user, }) @@ -184,7 +184,7 @@ type AccountsUpdateResponse struct { // AccountsUpdate allows changing the account's information (password etc.) func AccountsUpdate(w http.ResponseWriter, r *http.Request) { - utils.JSONResponse(501, &AccountsUpdateResponse{ + utils.JSONResponse(w, 501, &AccountsUpdateResponse{ Success: false, Message: `Sorry, not implemented yet`, }) @@ -198,7 +198,7 @@ type AccountsDeleteResponse struct { // AccountsDelete allows deleting an account. func AccountsDelete(w http.ResponseWriter, r *http.Request) { - utils.JSONResponse(501, &AccountsDeleteResponse{ + utils.JSONResponse(w, 501, &AccountsDeleteResponse{ Success: false, Message: `Sorry, not implemented yet`, }) @@ -212,7 +212,7 @@ type AccountsWipeDataResponse struct { // AccountsWipeData allows getting rid of the all data related to the account. func AccountsWipeData(w http.ResponseWriter, r *http.Request) { - utils.JSONResponse(501, &AccountsWipeDataResponse{ + utils.JSONResponse(w, 501, &AccountsWipeDataResponse{ Success: false, Message: `Sorry, not implemented yet`, }) @@ -226,7 +226,7 @@ type AccountsSessionsListResponse struct { // AccountsSessionsList returns a list of all opened sessions. func AccountsSessionsList(w http.ResponseWriter, r *http.Request) { - utils.JSONResponse(501, &AccountsSessionsListResponse{ + utils.JSONResponse(w, 501, &AccountsSessionsListResponse{ Success: false, Message: `Sorry, not implemented yet`, }) diff --git a/routes/contacts.go b/routes/contacts.go index b9b8da2..21a24d9 100644 --- a/routes/contacts.go +++ b/routes/contacts.go @@ -7,32 +7,72 @@ import ( "github.com/lavab/api/utils" ) -// Contacts TODO -func Contacts(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// ContactsListResponse contains the result of the ContactsList request. +type ContactsListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` } -// CreateContact TODO -func CreateContact(w http.ResponseWriter, r *http.Request) { - reqData, err := utils.ReadJSON(r.Body) - if err == nil { - utils.FormatNotRecognizedResponse(w, err) - return - } - fmt.Println("TODO", reqData) +// ContactsList does *something* - TODO +func ContactsList(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &ContactsListResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) } -// Contact TODO -func Contact(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// ContactsCreateResponse contains the result of the ContactsCreate request. +type ContactsCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` } -// UpdateContact TODO -func UpdateContact(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// ContactsCreate does *something* - TODO +func ContactsCreate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &ContactsCreateResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) } -// DeleteContact TODO -func DeleteContact(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// ContactsGetResponse contains the result of the ContactsGet request. +type ContactsGetResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// ContactsGet does *something* - TODO +func ContactsGet(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &ContactsGetResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) +} + +// ContactsUpdateResponse contains the result of the ContactsUpdate request. +type ContactsUpdateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// ContactsUpdate does *something* - TODO +func ContactsUpdate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &ContactsUpdateResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) +} + +// ContactsDeleteResponse contains the result of the ContactsDelete request. +type ContactsDeleteResponse struct { + Success bool `json:"success"` + 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", + }) } diff --git a/routes/emails.go b/routes/emails.go index 48ab67e..fe7d458 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -8,45 +8,77 @@ import ( "github.com/lavab/api/utils" ) -// Emails TODO -func Emails(w http.ResponseWriter, r *http.Request) { - // fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") - mock := map[string]interface{}{ - "n_items": 1, - "emails": []models.Email{}, - } - utils.JSONResponse(w, 200, mock) -} - -// CreateEmail TODO -func CreateEmail(w http.ResponseWriter, r *http.Request) { - // reqData, err := utils.ReadJSON(r.Body) - // if err != nil { - // utils.ErrorResponse(w, 400, "Couldn't parse the request body", err.Error()) - // } - // log.Println(reqData) - mock := map[string]interface{}{ - "success": true, - "created": []string{utils.UUID()}, - } - utils.JSONResponse(w, 200, mock) -} - -// Email TODO -func Email(w http.ResponseWriter, r *http.Request) { - // fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") - mock := map[string]interface{}{ - "status": "sending", - } - utils.JSONResponse(w, 200, mock) -} - -// UpdateEmail TODO -func UpdateEmail(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") -} - -// DeleteEmail TODO -func DeleteEmail(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// EmailsListResponse contains the result of the EmailsList request. +type EmailsListResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + ItemsCount int `json:"items_count,omitempty"` + Emails []*models.Email `json:"emails,omitempty"` +} + +// EmailsList sends a list of the emails in the inbox. +func EmailsList(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 200, &EmailsListResponse{ + Success: true, + ItemCount: 1, + Emails: []*models.Email{}, + }) +} + +// EmailsCreateResponse contains the result of the EmailsCreate request. +type EmailsCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Created []string `json:"created,omitempty"` +} + +// EmailsCreate sends a new email +func EmailsCreate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 200, &EmailsCreateResponse{ + Success: true, + Created: []string{utils.UUID()}, + }) +} + +// EmailsGetResponse contains the result of the EmailsGet request. +type EmailsGetResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Status string `json:"status,omitempty"` +} + +// EmailsGet responds with a single email message +func EmailsGet(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 200, &EmailsGetResponse{ + Success: true, + Status: "sending", + }) +} + +// EmailsUpdateResponse contains the result of the EmailsUpdate request. +type EmailsUpdateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// EmailsUpdate does *something* - TODO +func EmailsUpdate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &EmailsUpdateResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) +} + +// EmailsDeleteResponse contains the result of the EmailsDelete request. +type EmailsDeleteResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// EmailsDelete remvoes an email from the system +func EmailsDelete(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &EmailsDeleteResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) } diff --git a/routes/hello.go b/routes/hello.go index 4e33c0f..7594175 100644 --- a/routes/hello.go +++ b/routes/hello.go @@ -7,12 +7,14 @@ import ( "github.com/lavab/api/utils" ) +// HelloResponse contains the result of the Hello request. type HelloResponse struct { Message string `json:"message"` DocsURL string `json:"docs_url"` Version string `json:"version"` } +// Hello shows basic information about the API on its frontpage. func Hello(w http.ResponseWriter, r *http.Request) { utils.JSONResponse(w, 200, &HelloResponse{ Message: "Lavaboom API", diff --git a/routes/keys.go b/routes/keys.go index 5106401..12d3f68 100644 --- a/routes/keys.go +++ b/routes/keys.go @@ -5,22 +5,44 @@ import ( "net/http" ) -// Keys TODO -func Keys(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// KeysCreateResponse contains the result of the KeysCreate request. +type KeysCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` } -// SubmitKey TODO -func SubmitKey(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// KeysCreate does *something* - TODO +func KeysCreate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &KeysCreateResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) } -// Key TODO -func Key(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// KeysGetResponse contains the result of the KeysGet request. +type KeysGetResponse struct { + Success bool `json:"success"` + Message string `json:"message"` } -// VoteKey TODO -func VoteKey(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// KeysGet does *something* - TODO +func KeysGet(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &KeysGetResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) +} + +// KeysVoteResponse contains the result of the KeysVote request. +type KeysVoteResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// KeysVote does *something* - TODO +func KeysVote(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &KeysVoteResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) } diff --git a/routes/labels.go b/routes/labels.go index 5a2e837..4e4dda5 100644 --- a/routes/labels.go +++ b/routes/labels.go @@ -5,27 +5,72 @@ import ( "net/http" ) -// Labels TODO -func Labels(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// LabelsListResponse contains the result of the LabelsList request. +type LabelsListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` } -// CreateLabel TODO -func CreateLabel(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// LabelsList does *something* - TODO +func LabelsList(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &LabelsListResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) } -// Label TODO -func Label(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// LabelsCreateResponse contains the result of the LabelsCreate request. +type LabelsCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` } -// UpdateLabel TODO -func UpdateLabel(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// LabelsCreate does *something* - TODO +func LabelsCreate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &LabelsCreateResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) } -// Label TODO -func DeleteLabel(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// LabelsGetResponse contains the result of the LabelsGet request. +type LabelsGetResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// LabelsGet does *something* - TODO +func LabelsGet(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &LabelsGetResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) +} + +// LabelsUpdateResponse contains the result of the LabelsUpdate request. +type LabelsUpdateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// LabelsUpdate does *something* - TODO +func LabelsUpdate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &LabelsUpdateResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) +} + +// LabelsDeleteResponse contains the result of the LabelsDelete request. +type LabelsDeleteResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// LabelsDelete does *something* - TODO +func LabelsDelete(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &LabelsDeleteResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) } diff --git a/routes/threads.go b/routes/threads.go index ed4bd5a..74907cc 100644 --- a/routes/threads.go +++ b/routes/threads.go @@ -1,21 +1,49 @@ package routes import ( - "fmt" "net/http" + + "github.com/lavab/api/utils" ) -// Threads TODO -func Threads(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// ThreadsListResponse contains the result of the ThreadsList request. +type ThreadsListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// ThreadsList shows all threads +func ThreadsList(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &ThreadsListResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) +} + +// ThreadsGetResponse contains the result of the ThreadsGet request. +type ThreadsGetResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// ThreadsGet returns information about a single thread. +func ThreadsGet(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &ThreadsGetResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) } -// Thread TODO -func Thread(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// ThreadsUpdateResponse contains the result of the ThreadsUpdate request. +type ThreadsUpdateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` } -// UpdateThread TODO -func UpdateThread(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// ThreadsUpdate does *something* with a thread. +func ThreadsUpdate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &ThreadsUpdateResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) } diff --git a/routes/tokens.go b/routes/tokens.go index 1ff6392..62797c4 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -31,7 +31,7 @@ func TokensGet(w http.ResponseWriter, r *http.Request) { session := models.CurrentSession(r) // Respond with the token information - utils.JSONResponse(200, &TokensGetResponse{ + utils.JSONResponse(w, 200, &TokensGetResponse{ Success: true, Created: session.DateCreated, Expires: session.ExpirationDate, From 96f5dcb40125e56e107927e50561de8f22b4d73e Mon Sep 17 00:00:00 2001 From: "Piotr \"Orange\" Zduniak" Date: Fri, 7 Nov 2014 21:44:39 +0100 Subject: [PATCH 10/20] Rewrote the authentication middleware, made the code compilable --- .gitignore | 1 + auth.go | 35 ------------------------ main.go | 27 ++++++++++-------- routes/accounts.go | 14 ++++------ routes/contacts.go | 1 - routes/emails.go | 7 ++--- routes/keys.go | 3 +- routes/labels.go | 3 +- routes/middleware.go | 65 ++++++++++++++++++++++++++++++++++++++++++++ routes/tokens.go | 14 ++++------ utils/requests.go | 3 +- 11 files changed, 102 insertions(+), 71 deletions(-) delete mode 100644 auth.go create mode 100644 routes/middleware.go diff --git a/.gitignore b/.gitignore index b25c15b..627d99b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *~ +*.exe \ No newline at end of file diff --git a/auth.go b/auth.go deleted file mode 100644 index 4d6079d..0000000 --- a/auth.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/gorilla/context" - "github.com/lavab/api/db" - "github.com/lavab/api/dbutils" - "github.com/lavab/api/utils" -) - -// AuthWrapper is an auth middleware using the "Auth" header -// The session object gets saved in the gorilla/context map, use context.Get("session") to fetch it -func AuthWrapper(next handleFunc) handleFunc { - return func(w http.ResponseWriter, r *http.Request) { - authToken := r.Header.Get("Auth") - if authToken == "" { - utils.ErrorResponse(w, 401, "Missing auth token", "") - return - } - session, ok := dbutils.GetSession(authToken) - if !ok { - utils.ErrorResponse(w, 401, "Invalid auth token", "") - return - } - if session.HasExpired() { - utils.ErrorResponse(w, 419, "Authentication token has expired", "Session has expired on "+session.ExpDate) - db.Delete("sessions", session.ID) - return - } - - context.Set(r, "session", session) - next(w, r) - } -} diff --git a/main.go b/main.go index 43df403..d767746 100644 --- a/main.go +++ b/main.go @@ -1,22 +1,18 @@ package main import ( - "fmt" - "log" + "net" "net/http" - "os" - "strconv" - "time" "github.com/Sirupsen/logrus" "github.com/goji/glogrus" "github.com/namsral/flag" - "github.com/zenazn/goji" "github.com/zenazn/goji/graceful" "github.com/zenazn/goji/web" "github.com/zenazn/goji/web/middleware" "github.com/lavab/api/env" + "github.com/lavab/api/routes" ) // TODO: "Middleware that implements a few quick security wins" @@ -58,7 +54,7 @@ func main() { // Set up an auth'd mux auth := web.New() - mux.Use(models.AuthMiddleware) + mux.Use(routes.AuthMiddleware) // Index route mux.Get("/", routes.Hello) @@ -69,11 +65,11 @@ func main() { auth.Get("/accounts/:id", routes.AccountsGet) auth.Put("/accounts/:id", routes.AccountsUpdate) auth.Delete("/accounts/:id", routes.AccountsDelete) - auth.Post("/accounts/:id/wipe-data", routes.AccountsWipeUserData) + auth.Post("/accounts/:id/wipe-data", routes.AccountsWipeData) auth.Get("/accounts/:id/sessions", routes.AccountsSessionsList) // Tokens - auth.Get("/tokens", routes.TokenGet) + auth.Get("/tokens", routes.TokensGet) auth.Post("/tokens", routes.TokensCreate) auth.Delete("/tokens", routes.TokensDelete) @@ -115,7 +111,7 @@ func main() { mux.Compile() // Make the mux handle every request - http.Handle("/", DefaultMux) + http.Handle("/", mux) // Set up a new environment object env.G = &env.Environment{ @@ -146,8 +142,17 @@ func main() { log.Info("Stopped the application") }) + // Listen to the passed address + listener, err := net.Listen("tcp", *bindAddress) + if err != nil { + log.WithFields(logrus.Fields{ + "error": err, + "address": *bindAddress, + }).Fatal("Cannot set up a TCP listener") + } + // Start the listening - err := graceful.Serve(listener, http.DefaultServeMux) + err = graceful.Serve(listener, http.DefaultServeMux) if err != nil { // Don't use .Fatal! We need the code to shut down properly. log.Error(err) diff --git a/routes/accounts.go b/routes/accounts.go index 18543b3..29aa83d 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -1,9 +1,6 @@ package routes import ( - "encoding/json" - "fmt" - "log" "net/http" "github.com/Sirupsen/logrus" @@ -13,6 +10,7 @@ import ( "github.com/lavab/api/dbutils" "github.com/lavab/api/env" "github.com/lavab/api/models" + "github.com/lavab/api/models/base" "github.com/lavab/api/utils" ) @@ -51,7 +49,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { if err != nil { env.G.Log.WithFields(logrus.Fields{ "error": err, - }).Warning("Unable to decode a request") + }).Warn("Unable to decode a request") utils.JSONResponse(w, 409, &AccountsCreateResponse{ Success: false, @@ -61,7 +59,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { } // Ensure that the user with requested username doesn't exist - if _, ok := dbutils.FindUserByName(username); ok { + if _, ok := dbutils.FindUserByName(input.Username); ok { utils.JSONResponse(w, 409, &AccountsCreateResponse{ Success: false, Message: "Username already exists", @@ -70,7 +68,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { } // Try to hash the password - hash, err := utils.BcryptHash(password) + hash, err := utils.BcryptHash(input.Password) if err != nil { utils.JSONResponse(w, 500, &AccountsCreateResponse{ Success: false, @@ -87,7 +85,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // Create a new user object user := &models.User{ - Resource: base.MakeResource(utils.UUID(), username), + Resource: base.MakeResource(utils.UUID(), input.Username), Password: string(hash), } @@ -148,7 +146,7 @@ func AccountsGet(c *web.C, w http.ResponseWriter, r *http.Request) { // The session refers to a non-existing user env.G.Log.WithFields(logrus.Fields{ "id": session.ID, - }).Warning("Valid session referred to a removed account") + }).Warn("Valid session referred to a removed account") // Try to remove the orphaned session if err := db.Delete("sessions", session.ID); err != nil { diff --git a/routes/contacts.go b/routes/contacts.go index 21a24d9..d0effbf 100644 --- a/routes/contacts.go +++ b/routes/contacts.go @@ -1,7 +1,6 @@ package routes import ( - "fmt" "net/http" "github.com/lavab/api/utils" diff --git a/routes/emails.go b/routes/emails.go index fe7d458..44c3305 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -1,7 +1,6 @@ package routes import ( - "fmt" "net/http" "github.com/lavab/api/models" @@ -19,9 +18,9 @@ type EmailsListResponse struct { // EmailsList sends a list of the emails in the inbox. func EmailsList(w http.ResponseWriter, r *http.Request) { utils.JSONResponse(w, 200, &EmailsListResponse{ - Success: true, - ItemCount: 1, - Emails: []*models.Email{}, + Success: true, + ItemsCount: 1, + Emails: []*models.Email{}, }) } diff --git a/routes/keys.go b/routes/keys.go index 12d3f68..e22732a 100644 --- a/routes/keys.go +++ b/routes/keys.go @@ -1,8 +1,9 @@ package routes import ( - "fmt" "net/http" + + "github.com/lavab/api/utils" ) // KeysCreateResponse contains the result of the KeysCreate request. diff --git a/routes/labels.go b/routes/labels.go index 4e4dda5..45d0e97 100644 --- a/routes/labels.go +++ b/routes/labels.go @@ -1,8 +1,9 @@ package routes import ( - "fmt" "net/http" + + "github.com/lavab/api/utils" ) // LabelsListResponse contains the result of the LabelsList request. diff --git a/routes/middleware.go b/routes/middleware.go new file mode 100644 index 0000000..d886fb9 --- /dev/null +++ b/routes/middleware.go @@ -0,0 +1,65 @@ +package routes + +import ( + "net/http" + "strings" + + "github.com/zenazn/goji/web" + + "github.com/lavab/api/db" + "github.com/lavab/api/dbutils" + "github.com/lavab/api/utils" +) + +type AuthMiddlewareResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +func AuthMiddleware(c *web.C, h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Read the Authorization header + header := r.Header.Get("Authorization") + if header == "" { + utils.JSONResponse(w, 401, &AuthMiddlewareResponse{ + Success: false, + Message: "Missing auth token", + }) + 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", + }) + return + } + + // Get the session from the database + session, ok := dbutils.GetSession(headerParts[1]) + if !ok { + utils.JSONResponse(w, 401, &AuthMiddlewareResponse{ + Success: false, + Message: "Invalid authorization token", + }) + return + } + + // Check if it's expired + if session.HasExpired() { + utils.JSONResponse(w, 419, &AuthMiddlewareResponse{ + Success: false, + Message: "Authorization token has expired", + }) + db.Delete("sessions", session.ID) + return + } + + // Continue to the next middleware/route + c.Env["session"] = session + h.ServeHTTP(w, r) + }) +} diff --git a/routes/tokens.go b/routes/tokens.go index 62797c4..fe3b0e5 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -1,12 +1,9 @@ package routes import ( - "fmt" - "log" "net/http" - "time" - "github.com/gorilla/context" + "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" "github.com/lavab/api/db" @@ -59,7 +56,7 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { if err != nil { env.G.Log.WithFields(logrus.Fields{ "error": err, - }).Warning("Unable to decode a request") + }).Warn("Unable to decode a request") utils.JSONResponse(w, 409, &TokensCreateResponse{ Success: false, @@ -69,8 +66,8 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { } // Authenticate the user - user, ok := dbutils.FindUserByName(username) - if !ok || user == nil || !utils.BcryptVerify(user.Password, password) { + user, ok := dbutils.FindUserByName(input.Username) + if !ok || user == nil || !utils.BcryptVerify(user.Password, input.Password) { utils.JSONResponse(w, 403, &TokensCreateResponse{ Success: false, Message: "Wrong username or password", @@ -84,8 +81,7 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { // Create a new token token := &models.Session{ Expiring: base.Expiring{expDate}, - Resource: base.MakeResource(user.ID, ""), - Name: "Auth token expiring on " + expDate, + Resource: base.MakeResource(user.ID, "Auth token expiring on "+expDate), } // Insert int into the database diff --git a/utils/requests.go b/utils/requests.go index b1e5d2e..83fe3d9 100644 --- a/utils/requests.go +++ b/utils/requests.go @@ -7,6 +7,7 @@ import ( "net/http" "strings" + "github.com/Sirupsen/logrus" "github.com/gorilla/schema" "github.com/lavab/api/env" @@ -46,7 +47,7 @@ func JSONResponse(w http.ResponseWriter, status int, data interface{}) { func ParseRequest(r *http.Request, data interface{}) error { // Get the contentType for comparsions - contentType = r.Header.Get("Content-Type") + contentType := r.Header.Get("Content-Type") // Deterimine the passed ContentType if strings.Contains(contentType, "application/json") { From c6f6d9077311fd3d034e3fb2da0f6525d395c114 Mon Sep 17 00:00:00 2001 From: "Piotr \"Orange\" Zduniak" Date: Fri, 7 Nov 2014 22:39:04 +0100 Subject: [PATCH 11/20] Made code compilable after the merge --- .gitignore | 2 +- models/contact.go | 3 --- routes/accounts.go | 7 +++---- routes/middleware.go | 2 +- routes/tokens.go | 36 ++++++++++++++++++------------------ 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 627d99b..9bb4877 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ *~ -*.exe \ No newline at end of file +*.exe diff --git a/models/contact.go b/models/contact.go index 9582750..c0eaa7e 100644 --- a/models/contact.go +++ b/models/contact.go @@ -4,7 +4,4 @@ package models type Contact struct { Encrypted Resource - - // Picture is a profile picture - Picture Avatar `json:"picture" gorethink:"picture"` } diff --git a/routes/accounts.go b/routes/accounts.go index 29aa83d..aa1838f 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -10,7 +10,6 @@ import ( "github.com/lavab/api/dbutils" "github.com/lavab/api/env" "github.com/lavab/api/models" - "github.com/lavab/api/models/base" "github.com/lavab/api/utils" ) @@ -85,7 +84,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // Create a new user object user := &models.User{ - Resource: base.MakeResource(utils.UUID(), input.Username), + Resource: models.MakeResource(utils.UUID(), input.Username), Password: string(hash), } @@ -138,10 +137,10 @@ func AccountsGet(c *web.C, w http.ResponseWriter, r *http.Request) { } // Fetch the current session from the database - session := models.CurrentSession(r) + session := c.Env["session"].(*models.AuthToken) // Fetch the user object from the database - user, ok := dbutils.GetUser(session.UserID) + user, ok := dbutils.GetUser(session.AccountID) if !ok { // The session refers to a non-existing user env.G.Log.WithFields(logrus.Fields{ diff --git a/routes/middleware.go b/routes/middleware.go index d886fb9..af81d3b 100644 --- a/routes/middleware.go +++ b/routes/middleware.go @@ -49,7 +49,7 @@ func AuthMiddleware(c *web.C, h http.Handler) http.Handler { } // Check if it's expired - if session.HasExpired() { + if session.Expired() { utils.JSONResponse(w, 419, &AuthMiddlewareResponse{ Success: false, Message: "Authorization token has expired", diff --git a/routes/tokens.go b/routes/tokens.go index fe3b0e5..b31bb6b 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -2,6 +2,7 @@ package routes import ( "net/http" + "time" "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" @@ -10,28 +11,27 @@ import ( "github.com/lavab/api/dbutils" "github.com/lavab/api/env" "github.com/lavab/api/models" - "github.com/lavab/api/models/base" "github.com/lavab/api/utils" ) // TokensGetResponse contains the result of the TokensGet request. type TokensGetResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Created string `json:"created,omitempty"` - Expires string `json:"expires,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Created *time.Time `json:"created,omitempty"` + Expires *time.Time `json:"expires,omitempty"` } // TokensGet returns information about the current token. -func TokensGet(w http.ResponseWriter, r *http.Request) { +func TokensGet(c *web.C, w http.ResponseWriter, r *http.Request) { // Fetch the current session from the database - session := models.CurrentSession(r) + session := c.Env["session"].(*models.AuthToken) // Respond with the token information utils.JSONResponse(w, 200, &TokensGetResponse{ Success: true, - Created: session.DateCreated, - Expires: session.ExpirationDate, + Created: &session.DateCreated, + Expires: &session.ExpiryDate, }) } @@ -43,9 +43,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.Session `json:"token,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Token *models.AuthToken `json:"token,omitempty"` } // TokensCreate allows logging in to an account. @@ -75,13 +75,13 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { return } - // Calculate the expiration date - expDate := utils.HoursFromNowString(env.G.Config.SessionDuration) + // Calculate the expiry date + expDate := time.Now().Add(time.Hour * time.Duration(env.G.Config.SessionDuration)) // Create a new token - token := &models.Session{ - Expiring: base.Expiring{expDate}, - Resource: base.MakeResource(user.ID, "Auth token expiring on "+expDate), + token := &models.AuthToken{ + Expiring: models.Expiring{expDate}, + Resource: models.MakeResource(user.ID, "Auth token expiring on "+expDate.Format(time.RFC3339)), } // Insert int into the database @@ -104,7 +104,7 @@ type TokensDeleteResponse struct { // TokensDelete destroys the current session token. func TokensDelete(c *web.C, w http.ResponseWriter, r *http.Request) { // Get the session from the middleware - session := c.Env["session"].(*models.Session) + session := c.Env["session"].(*models.AuthToken) // Delete it from the database if err := db.Delete("sessions", session.ID); err != nil { From a2bdceb480e85630b21185eec66e976d3bed5dcb Mon Sep 17 00:00:00 2001 From: makkalot Date: Sat, 8 Nov 2014 20:09:52 +0200 Subject: [PATCH 12/20] Creating a CRUD interface that other tables can embed into for easier and more convinient access to database --- db/crud.go | 289 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 251 insertions(+), 38 deletions(-) diff --git a/db/crud.go b/db/crud.go index 0091ba2..66bac95 100644 --- a/db/crud.go +++ b/db/crud.go @@ -1,68 +1,281 @@ package db import ( - "errors" - "log" - r "github.com/dancannon/gorethink" ) -// TODO: throw custom errors +type RethinkTable interface { + GetTableName() string + GetDbName() string +} + +type RethinkCreater interface { + Insert(data interface{}) error +} + +type RethinkReader interface { + Find(id string) (*r.Cursor, error) + FindFetchOne(id string, value interface{}) error + + FindBy(key string, value interface{}) (*r.Cursor, error) + FindByAndFetch(key string, value interface{}, results interface{}) error + FindByAndFetchOne(key string, value interface{}, result interface{}) error + + Where(filter map[string]interface{}) (*r.Cursor, error) + WhereAndFetch(filter map[string]interface{}, results interface{}) error + WhereAndFetchOne(filter map[string]interface{}, result interface{}) error + + FindByIndex(index string, values ...interface{}) (*r.Cursor, error) + FindByIndexFetch(results interface{}, index string, values ...interface{}) error + FindByIndexFetchOne(result interface{}, index string, values ...interface{}) error +} + +type RethinkUpdater interface { + Update(data interface{}) error + UpdateId(id string, data interface{}) error +} + +type RethinkDeleter interface { + Delete(pred interface{}) error + DeleteId(id string) error +} + +//The interface that all tables should implement +type RethinkCrud interface { + RethinkCreater + RethinkReader + RethinkUpdater + RethinkDeleter + RethinkTable +} + +//The default impementation that should be embedded +type RethinkCrudImpl struct { + table string + db string +} + +func NewCrudTable(db, table string) *RethinkCrudImpl { + return &RethinkCrudImpl{ + db: db, + table: table, + } +} + +//The RethinkTable implementation +func (rc *RethinkCrudImpl) GetTableName() string { + return rc.table +} + +func (rc *RethinkCrudImpl) GetDbName() string { + return rc.db +} + +//Gets the current table as a Rethink Term +func (rc *RethinkCrudImpl) GetTable() r.Term { + return r.Table(rc.table) +} + +//inserts a document to the database +func (rc *RethinkCrudImpl) Insert(data interface{}) error { + _, err := rc.GetTable().Insert(data).RunWrite(config.Session) + if err != nil { + return NewDbErr(rc, err) + } -// Insert performs an insert operation for any map[string]interface{} or struct -func Insert(table string, data interface{}) error { - return insertHelper(table, data, "error") + return nil } -// Update performs an update operation for any map[string]interface{} or struct -func Update(table string, data interface{}) error { - return insertHelper(table, data, "update") +//Updates according to the specified options in data +func (rc *RethinkCrudImpl) Update(data interface{}) error { + _, err := rc.GetTable().Update(data).RunWrite(config.Session) + if err != nil { + return NewDbErr(rc, err) + } + + return nil } -// Delete deletes a database item based on id -func Delete(table string, id string) error { - _, err := r.Table(table).Get(id).Delete().RunWrite(config.Session) +//Updates a specific id in database, with options from data +func (rc *RethinkCrudImpl) UpdateId(id string, data interface{}) error { + _, err := rc.GetTable().Get(id).Update(data).RunWrite(config.Session) if err != nil { - log.Fatalf("Couldn't delete [%s] in table [%s]\n", id, table) + return NewDbErr(rc, err) } + return nil } -// Get fetches a database object with a specific id -func Get(table string, id string) (*r.Cursor, error) { - if response, err := r.Table(table).Get(id).Run(config.Session); err == nil { - return response, nil +//Deletes the documents that are in pred argument +func (rc *RethinkCrudImpl) Delete(pred interface{}) error { + _, err := rc.GetTable().Filter(pred).Delete().RunWrite(config.Session) + if err != nil { + return NewDbErr(rc, err) } - return nil, errors.New("Item not found") + + return nil } -// GetAll fetches all items in the table that satisfy item[index] == value -// TODO: find out how to match on nested keys -func GetAll(table string, index string, value interface{}) (*r.Cursor, error) { - if response, err := r.Table(table).GetAllByIndex(index, value).Run(config.Session); err == nil { - return response, nil +//Deletes a given id +func (rc *RethinkCrudImpl) DeleteId(id string) error { + _, err := rc.GetTable().Get(id).Delete().RunWrite(config.Session) + if err != nil { + return NewDbErr(rc, err) } - return nil, errors.New("Not found") + + return nil } -// GetByID is an alias for Get -var GetByID = Get +//Finds a given object from db, it does not perform any fetching +func (rc *RethinkCrudImpl) Find(id string) (*r.Cursor, error) { + cursor, err := rc.GetTable().Get(id).Run(config.Session) + if err != nil { + return nil, NewDbErr(rc, err) + } + + return cursor, nil +} -// GetByIndex is an alias for GetAll -var GetByIndex = GetAll +//Fetches the specified object from db and fills the value with it +func (rc *RethinkCrudImpl) FindFetchOne(id string, value interface{}) error { + cursor, err := rc.Find(id) + if err != nil { + return err + } -// Remove is an alias for Delete -var Remove = Delete + //now fetch the item from database + if err := cursor.One(value); err != nil { + return NewDbErr(rc, err) + } -// Rm is an alias for Delete -var Rm = Delete + //we have success here + return nil +} -// insertHelper adds an interface{} to the database. Helper func for db.Insert and db.Update -func insertHelper(table string, data interface{}, conflictResolution string) error { - // TODO check out the RunWrite result, conflict errors are reported there - _, err := r.Table(table).Insert(data, r.InsertOpts{Conflict: conflictResolution}).RunWrite(config.Session) +//FindBy is for looking up for key=value situations +func (rc *RethinkCrudImpl) FindBy(key string, value interface{}) (*r.Cursor, error) { + filterMap := map[string]interface{}{ + key: value, + } + cursor, err := rc.GetTable().Filter(filterMap).Run(config.Session) if err != nil { - log.Fatalln("Database insert operation failed. Data:\n", data) + return nil, NewDbErr(rc, err) } + + return cursor, nil +} + +//FindBy is for looking up for key=value situations with fetch all +func (rc *RethinkCrudImpl) FindByAndFetch(key string, value interface{}, results interface{}) error { + + cursor, err := rc.FindBy(key, value) + if err != nil { + return err + } + + //now fetch the item from database + if err := cursor.All(results); err != nil { + return NewDbErr(rc, err) + } + + //we have success here + return nil +} + +//Fetches the specified object from db and fills the value with it +func (rc *RethinkCrudImpl) FindByAndFetchOne(key string, value interface{}, result interface{}) error { + + cursor, err := rc.FindBy(key, value) + if err != nil { + return err + } + + //now fetch the item from database + if err := cursor.One(result); err != nil { + return NewDbErr(rc, err) + } + + //we have success here + return nil +} + +//Where is for asking for more than one field from database, useful for passing a few +//pairs for AND querying +func (rc *RethinkCrudImpl) Where(filter map[string]interface{}) (*r.Cursor, error) { + cursor, err := rc.GetTable().Filter(filter).Run(config.Session) + if err != nil { + return nil, NewDbErr(rc, err) + } + + return cursor, nil +} + +//Where with fetch all +func (rc *RethinkCrudImpl) WhereAndFetch(filter map[string]interface{}, results interface{}) error { + cursor, err := rc.Where(filter) + if err != nil { + return err + } + + //now fetch the item from database + if err := cursor.All(results); err != nil { + return NewDbErr(rc, err) + } + + //we have success here + return nil +} + +//Where with fetch all +func (rc *RethinkCrudImpl) WhereAndFetchOne(filter map[string]interface{}, result interface{}) error { + cursor, err := rc.Where(filter) + if err != nil { + return err + } + + //now fetch the item from database + if err := cursor.One(result); err != nil { + return NewDbErr(rc, err) + } + + //we have success here + return nil +} + +//GetAll fetches all items in the table that satisfy item[index] == value +func (rc *RethinkCrudImpl) FindByIndex(index string, values ...interface{}) (*r.Cursor, error) { + cursor, err := rc.GetTable().GetAllByIndex(index, values...).Run(config.Session) + if err != nil { + return nil, NewDbErr(rc, err) + } + + return cursor, nil +} + +func (rc *RethinkCrudImpl) FindByIndexFetch(results interface{}, index string, values ...interface{}) error { + cursor, err := rc.FindByIndex(index, values...) + if err != nil { + return err + } + + //now fetch the item from database + if err := cursor.All(results); err != nil { + return NewDbErr(rc, err) + } + + return nil +} + +func (rc *RethinkCrudImpl) FindByIndexFetchOne(result interface{}, index string, values ...interface{}) error { + cursor, err := rc.FindByIndex(index, values...) + if err != nil { + return err + } + + //now fetch the item from database + if err := cursor.One(result); err != nil { + return NewDbErr(rc, err) + } + return nil } From 6e121617d18b936c3e320fe546b057807f5202da Mon Sep 17 00:00:00 2001 From: makkalot Date: Sat, 8 Nov 2014 20:11:11 +0200 Subject: [PATCH 13/20] custom errors for database related problems --- db/errors.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/db/errors.go b/db/errors.go index 3ec6d80..1fb91ad 100644 --- a/db/errors.go +++ b/db/errors.go @@ -1,5 +1,39 @@ package db +import ( + "fmt" +) + +type DbError struct { + err error + msg string + table RethinkTable +} + +func (derr *DbError) Error() string { + return fmt.Sprintf( + "%s - Db: %s - Table : %s - %s ", + derr.msg, + derr.table.GetDbName(), + derr.table.GetTableName(), + derr.err) +} + +func NewDbErr(t RethinkTable, err error) *DbError { + return &DbError{ + err: err, + table: t, + } +} + +func NewDbErrWithMsg(t RethinkTable, err error, msg string) *DbError { + return &DbError{ + err: err, + table: t, + msg: msg, + } +} + type ConnectionError struct { error WrongAuthKey bool From a1a3819a3739bfea517999f49cdd001e413282a4 Mon Sep 17 00:00:00 2001 From: makkalot Date: Sat, 8 Nov 2014 20:12:00 +0200 Subject: [PATCH 14/20] refactoring table names into constants do can reuse them in other packages --- db/setup.go | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/db/setup.go b/db/setup.go index 2fcbfb0..31a3d9f 100644 --- a/db/setup.go +++ b/db/setup.go @@ -9,6 +9,17 @@ import ( r "github.com/dancannon/gorethink" ) +const ( + TABLE_SESSIONS = "sessions" + TABLE_USERS = "users" + TABLE_EMAILS = "emails" + TABLE_DRAFTS = "drafts" + TABLE_CONTACTS = "contacts" + TABLE_THREADS = "threads" + TABLE_LABELS = "labels" + TABLE_KEYS = "keys" +) + var config struct { Session *r.Session Url string @@ -16,6 +27,8 @@ var config struct { Db string } +var CurrentConfig = config + var dbs = []string{ "prod", "staging", @@ -23,14 +36,14 @@ var dbs = []string{ } var tablesAndIndexes = map[string][]string{ - "sessions": []string{"user", "user_id"}, - "users": []string{"name"}, - "emails": []string{"user_id"}, - "drafts": []string{"user_id"}, - "contacts": []string{}, - "threads": []string{"user_id"}, - "labels": []string{}, - "keys": []string{}, + TABLE_SESSIONS: []string{"user", "user_id"}, + TABLE_USERS: []string{"name"}, + TABLE_EMAILS: []string{"user_id"}, + TABLE_DRAFTS: []string{"user_id"}, + TABLE_CONTACTS: []string{}, + TABLE_THREADS: []string{"user_id"}, + TABLE_LABELS: []string{}, + TABLE_KEYS: []string{}, } func Init() { From 869e0c70890455226f94b5cf0b97639bae430525 Mon Sep 17 00:00:00 2001 From: makkalot Date: Sat, 8 Nov 2014 20:14:08 +0200 Subject: [PATCH 15/20] using the new crud api for the rest of the code that needs db access --- auth.go | 7 ++++--- dbutils/sessions.go | 21 +++++++++++---------- dbutils/setup.go | 22 ++++++++++++++++++++++ dbutils/users.go | 40 ++++++++++++++++++++-------------------- routes/me.go | 11 ++++------- routes/sessions.go | 16 +++++++++------- routes/setup.go | 8 ++++++++ 7 files changed, 78 insertions(+), 47 deletions(-) create mode 100644 dbutils/setup.go create mode 100644 routes/setup.go diff --git a/auth.go b/auth.go index 4d6079d..458350b 100644 --- a/auth.go +++ b/auth.go @@ -4,11 +4,12 @@ import ( "net/http" "github.com/gorilla/context" - "github.com/lavab/api/db" "github.com/lavab/api/dbutils" "github.com/lavab/api/utils" ) +var sessions = dbutils.Sessions + // AuthWrapper is an auth middleware using the "Auth" header // The session object gets saved in the gorilla/context map, use context.Get("session") to fetch it func AuthWrapper(next handleFunc) handleFunc { @@ -18,14 +19,14 @@ func AuthWrapper(next handleFunc) handleFunc { utils.ErrorResponse(w, 401, "Missing auth token", "") return } - session, ok := dbutils.GetSession(authToken) + session, ok := sessions.GetSession(authToken) if !ok { utils.ErrorResponse(w, 401, "Invalid auth token", "") return } if session.HasExpired() { utils.ErrorResponse(w, 419, "Authentication token has expired", "Session has expired on "+session.ExpDate) - db.Delete("sessions", session.ID) + sessions.DeleteId(session.ID) return } diff --git a/dbutils/sessions.go b/dbutils/sessions.go index fec1168..26c063a 100644 --- a/dbutils/sessions.go +++ b/dbutils/sessions.go @@ -7,16 +7,17 @@ import ( "github.com/lavab/api/models" ) -func GetSession(id string) (*models.Session, bool) { +type SessionTable struct { + db.RethinkCrud +} + +func (sessions *SessionTable) GetSession(id string) (*models.Session, bool) { var result models.Session - response, err := db.Get("sessions", id) - if err == nil && response != nil && !response.IsNil() { - err := response.One(&result) - if err != nil { - log.Fatalln("[utils.GetSession] Error when unfolding cursor") - return nil, false - } - return &result, true + + if err := sessions.FindFetchOne(id, &result); err != nil { + log.Println(err.Error()) + return nil, false } - return nil, false + + return &result, true } diff --git a/dbutils/setup.go b/dbutils/setup.go new file mode 100644 index 0000000..69353f8 --- /dev/null +++ b/dbutils/setup.go @@ -0,0 +1,22 @@ +package dbutils + +import ( + "github.com/lavab/api/db" +) + +//The crud interfaces for models +var Users UsersTable +var Sessions SessionTable + +//The init version for routes, mostly related to +//initing the table informatio +func init() { + //at this stage we should have the db config variable initialized + userCrud := db.NewCrudTable(db.CurrentConfig.Db, db.TABLE_USERS) + Users = UsersTable{RethinkCrud: userCrud} + + //init the sessions variable + sessionCrud := db.NewCrudTable(db.CurrentConfig.Db, db.TABLE_SESSIONS) + Sessions = SessionTable{RethinkCrud: sessionCrud} + +} diff --git a/dbutils/users.go b/dbutils/users.go index 438ab2e..3260269 100644 --- a/dbutils/users.go +++ b/dbutils/users.go @@ -7,30 +7,30 @@ import ( "github.com/lavab/api/models" ) -func GetUser(id string) (*models.User, bool) { +//implements the base crud interface +type UsersTable struct { + db.RethinkCrud +} + +func (users *UsersTable) GetUser(id string) (*models.User, bool) { var result models.User - response, err := db.Get("users", id) - if err == nil && !response.IsNil() { - err := response.One(&result) - if err != nil { - log.Fatalln("[utils.GetUser] Error when unfolding cursor") - return nil, false - } - return &result, true + + if err := users.FindFetchOne(id, &result); err != nil { + log.Println(err.Error()) + return nil, false } - return nil, false + + return &result, true + } -func FindUserByName(username string) (*models.User, bool) { +func (users *UsersTable) FindUserByName(username string) (*models.User, bool) { var result models.User - response, err := db.GetAll("users", "name", username) - if err == nil && response != nil && !response.IsNil() { - err := response.One(&result) - if err != nil { - log.Fatalln("[utils.FindUserByName] Error when unfolding cursor") - return nil, false - } - return &result, true + + if err := users.FindByIndexFetchOne(result, "name", username); err != nil { + log.Println(err.Error()) + return nil, false } - return nil, false + + return &result, true } diff --git a/routes/me.go b/routes/me.go index b25ea6c..3295848 100644 --- a/routes/me.go +++ b/routes/me.go @@ -3,22 +3,19 @@ package routes import ( "encoding/json" "fmt" - "log" - "net/http" - - "github.com/lavab/api/db" - "github.com/lavab/api/dbutils" "github.com/lavab/api/models" "github.com/lavab/api/utils" + "log" + "net/http" ) // Me returns information about the current user (more exactly, a JSONized models.User) func Me(w http.ResponseWriter, r *http.Request) { session := models.CurrentSession(r) - user, ok := dbutils.GetUser(session.UserID) + user, ok := users.GetUser(session.UserID) if !ok { debug := fmt.Sprintf("Session %s was deleted", session.ID) - if err := db.Delete("sessions", session.ID); err != nil { + if err := sessions.DeleteId(session.ID); err != nil { debug = "Error when trying to delete session associated with inactive account" log.Println("[routes.Me]", debug, err) } diff --git a/routes/sessions.go b/routes/sessions.go index 29b2566..e52419e 100644 --- a/routes/sessions.go +++ b/routes/sessions.go @@ -6,8 +6,6 @@ import ( "net/http" "github.com/gorilla/context" - "github.com/lavab/api/db" - "github.com/lavab/api/dbutils" "github.com/lavab/api/models" "github.com/lavab/api/models/base" "github.com/lavab/api/utils" @@ -18,7 +16,7 @@ const SessionDurationInHours = 72 // Login gets a username and password and returns a session token on success func Login(w http.ResponseWriter, r *http.Request) { username, password := r.FormValue("username"), r.FormValue("password") - user, ok := dbutils.FindUserByName(username) + user, ok := users.FindUserByName(username) if !ok || user == nil || !utils.BcryptVerify(user.Password, password) { utils.ErrorResponse(w, 403, "Wrong username or password", fmt.Sprintf("user: %+v", user)) @@ -31,7 +29,11 @@ func Login(w http.ResponseWriter, r *http.Request) { Resource: base.MakeResource(user.ID, ""), } session.Name = fmt.Sprintf("Auth session expiring on %s", session.ExpDate) - db.Insert("sessions", session) + if err := sessions.Insert(session); err != nil { + utils.ErrorResponse(w, 403, "Login Problem", + fmt.Sprintf("user: %+v", user)) + return + } utils.JSONResponse(w, 200, map[string]interface{}{ "message": "Authentication successful", @@ -45,7 +47,7 @@ func Signup(w http.ResponseWriter, r *http.Request) { username, password := r.FormValue("username"), r.FormValue("password") // regt := r.FormValue("reg_token") - if _, ok := dbutils.FindUserByName(username); ok { + if _, ok := users.FindUserByName(username); ok { utils.ErrorResponse(w, 409, "Username already exists", "") return } @@ -64,7 +66,7 @@ func Signup(w http.ResponseWriter, r *http.Request) { Password: string(hash), } - if err := db.Insert("users", user); err != nil { + if err := users.Insert(user); err != nil { utils.ErrorResponse(w, 500, "Internal server error", fmt.Sprintf("Couldn't insert %+v to database", user)) } @@ -79,7 +81,7 @@ func Signup(w http.ResponseWriter, r *http.Request) { // Logout destroys the current session token func Logout(w http.ResponseWriter, r *http.Request) { session := context.Get(r, "session").(*models.Session) - if err := db.Delete("sessions", session.ID); err != nil { + if err := sessions.DeleteId(session.ID); err != nil { utils.ErrorResponse(w, 500, "Internal server error", fmt.Sprint("Couldn't delete session %v. %v", session, err)) } diff --git a/routes/setup.go b/routes/setup.go new file mode 100644 index 0000000..8cda732 --- /dev/null +++ b/routes/setup.go @@ -0,0 +1,8 @@ +package routes + +import ( + "github.com/lavab/api/dbutils" +) + +var users = dbutils.Users +var sessions = dbutils.Sessions From ac397b6b6782accc61f434efd8e3b0d93b93458e Mon Sep 17 00:00:00 2001 From: makkalot Date: Mon, 10 Nov 2014 10:30:38 +0200 Subject: [PATCH 16/20] result should be a pointer --- dbutils/users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbutils/users.go b/dbutils/users.go index 3260269..47d81c7 100644 --- a/dbutils/users.go +++ b/dbutils/users.go @@ -27,7 +27,7 @@ func (users *UsersTable) GetUser(id string) (*models.User, bool) { func (users *UsersTable) FindUserByName(username string) (*models.User, bool) { var result models.User - if err := users.FindByIndexFetchOne(result, "name", username); err != nil { + if err := users.FindByIndexFetchOne(&result, "name", username); err != nil { log.Println(err.Error()) return nil, false } From 37395dfa270bd70428b2dd2f410b0b40aa39ef0b Mon Sep 17 00:00:00 2001 From: Andrei Simionescu Date: Mon, 10 Nov 2014 12:00:55 +0100 Subject: [PATCH 17/20] more changes to models --- dbutils/{users.go => accounts.go} | 16 +++++-------- dbutils/sessions.go | 4 ++-- models/{user.go => account.go} | 18 ++++++++++----- models/auth_token.go | 7 ------ models/base_expiring.go | 7 +++++- models/base_resource.go | 13 +++++------ models/contact.go | 4 ++-- models/file.go | 6 +---- models/invite.go | 18 --------------- models/label.go | 24 ++++++++++++++------ models/thread.go | 14 ++++++++++-- models/token.go | 37 +++++++++++++++++++++++++++++++ routes/me.go | 12 +++++----- routes/sessions.go | 25 ++++++++++----------- 14 files changed, 120 insertions(+), 85 deletions(-) rename dbutils/{users.go => accounts.go} (52%) rename models/{user.go => account.go} (57%) delete mode 100644 models/auth_token.go delete mode 100644 models/invite.go create mode 100644 models/token.go diff --git a/dbutils/users.go b/dbutils/accounts.go similarity index 52% rename from dbutils/users.go rename to dbutils/accounts.go index 438ab2e..0061597 100644 --- a/dbutils/users.go +++ b/dbutils/accounts.go @@ -1,19 +1,16 @@ package dbutils import ( - "log" - "github.com/lavab/api/db" "github.com/lavab/api/models" ) -func GetUser(id string) (*models.User, bool) { - var result models.User - response, err := db.Get("users", id) +func GetAccount(id string) (*models.Account, bool) { + var result models.Account + response, err := db.Get("accounts", id) if err == nil && !response.IsNil() { err := response.One(&result) if err != nil { - log.Fatalln("[utils.GetUser] Error when unfolding cursor") return nil, false } return &result, true @@ -21,13 +18,12 @@ func GetUser(id string) (*models.User, bool) { return nil, false } -func FindUserByName(username string) (*models.User, bool) { - var result models.User - response, err := db.GetAll("users", "name", username) +func FindAccountByUsername(username string) (*models.Account, bool) { + var result models.Account + response, err := db.GetAll("accounts", "name", username) if err == nil && response != nil && !response.IsNil() { err := response.One(&result) if err != nil { - log.Fatalln("[utils.FindUserByName] Error when unfolding cursor") return nil, false } return &result, true diff --git a/dbutils/sessions.go b/dbutils/sessions.go index 8fe4641..ebf7a7f 100644 --- a/dbutils/sessions.go +++ b/dbutils/sessions.go @@ -8,8 +8,8 @@ import ( ) // TODO change names to auth tokens instead of sessions -func GetSession(id string) (*models.AuthToken, bool) { - var result models.AuthToken +func GetSession(id string) (*models.Token, bool) { + var result models.Token response, err := db.Get("sessions", id) if err == nil && response != nil && !response.IsNil() { err := response.One(&result) diff --git a/models/user.go b/models/account.go similarity index 57% rename from models/user.go rename to models/account.go index 0f046fb..8d9fff0 100644 --- a/models/user.go +++ b/models/account.go @@ -1,13 +1,15 @@ package models -// User stores essential data for a Lavaboom user, and is thus not encrypted. -type User struct { +// Account stores essential data for a Lavaboom user, and is thus not encrypted. +type Account struct { Resource // Billing is a struct containing billing information. + // TODO Work in progress Billing BillingData `json:"billing" gorethink:"billing"` - // Password is the actual user password, hashed using bcrypt. + // Password is the password used to login to the account. + // It's hashed and salted using a cryptographically strong method (bcrypt|scrypt). Password string `json:"-" gorethink:"password"` // PgpExpDate is an RFC3339-encoded string containing the expiry date of the user's public key @@ -19,10 +21,16 @@ type User struct { // PgpPublicKey is a copy of the user's current public key. It can also be found in the 'keys' db. PgpPublicKey string `json:"pgp_public_key" gorethink:"pgp_public_key"` - // Settings is a struct containing app configuration data. + // Settings contains data needed to customize the user experience. + // TODO Work in progress Settings SettingsData `json:"settings" gorethink:"settings"` - // Type is the user type (free, beta, premium, etc) + // Type is the account type. + // Examples (work in progress): + // * beta: while in beta these are full accounts; after beta, these are normal accounts with special privileges + // * std: standard, free account + // * premium: premium account + // * superuser: Lavaboom staff Type string `json:"type" gorethink:"type"` } diff --git a/models/auth_token.go b/models/auth_token.go deleted file mode 100644 index 64acd90..0000000 --- a/models/auth_token.go +++ /dev/null @@ -1,7 +0,0 @@ -package models - -// AuthToken is a UUID used for user authentication, stored in the "auth_tokens" database -type AuthToken struct { - Expiring - Resource -} diff --git a/models/base_expiring.go b/models/base_expiring.go index 8221358..a2d82bd 100644 --- a/models/base_expiring.go +++ b/models/base_expiring.go @@ -16,7 +16,12 @@ func (e *Expiring) Expired() bool { return false } -// ExpireAfterNHours sets e.ExpiryDate to time.Now().UTC() + n hours +// ExpireAfterNHours sets the expiry date to time.Now().UTC() + n hours func (e *Expiring) ExpireAfterNHours(n int) { e.ExpiryDate = time.Now().UTC().Add(time.Duration(n) * time.Hour) } + +// ExpireSoon sets the expiry date to something in the near future. +func (e *Expiring) ExpireSoon() { + e.ExpiryDate = time.Now().UTC().Add(time.Duration(2) * time.Minute) +} diff --git a/models/base_resource.go b/models/base_resource.go index 5fa4809..d1df577 100644 --- a/models/base_resource.go +++ b/models/base_resource.go @@ -12,29 +12,28 @@ type Resource struct { // For some resources (invites, auth tokens) this is also the data itself. ID string `json:"id" gorethink:"id"` - // DateCreated is, shockingly, the date when the resource was created. + // DateCreated is, shockingly, the time when the resource was created. DateCreated time.Time `json:"date_created" gorethink:"date_created"` // DateModified records the time of the last change of the resource. DateModified time.Time `json:"date_modified" gorethink:"date_modified"` - // Name is a human-friendly description of the resource. - // Sometimes it can be essential to the resource, e.g. the `Account.Name` field. + // Name is the human-friendly name of the resource. It can either be essential (e.g. Account.Name) or optional. Name string `json:"name" gorethink:"name,omitempty"` - // AccountID is the ID of the user account that owns this resource. - AccountID string `json:"user_id" gorethink:"user_id"` + // Owner is the ID of the account that owns this resource. + Owner string `json:"owner" gorethink:"owner"` } // MakeResource creates a new Resource object with sane defaults. -func MakeResource(userID, name string) Resource { +func MakeResource(accountID, name string) Resource { t := time.Now() return Resource{ ID: utils.UUID(), DateModified: t, DateCreated: t, Name: name, - AccountID: userID, + Owner: accountID, } } diff --git a/models/contact.go b/models/contact.go index 9582750..090c060 100644 --- a/models/contact.go +++ b/models/contact.go @@ -5,6 +5,6 @@ type Contact struct { Encrypted Resource - // Picture is a profile picture - Picture Avatar `json:"picture" gorethink:"picture"` + // ProfilePicture is an encrypted picture associated with a contact. + ProfilePicture File `json:"profile_picture" gorethink:"profile_picture"` } diff --git a/models/file.go b/models/file.go index a5f85c8..9e1eb33 100644 --- a/models/file.go +++ b/models/file.go @@ -6,13 +6,9 @@ type File struct { Resource // Mime is the Internet media type of the file - // Check out: http://en.wikipedia.org/wiki/Internet_media_type + // Format: "type/subtype" – more info: en.wikipedia.org/wiki/Internet_media_type 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"` - - // Type is the generic type of the file - // Possible values: `file`, `audio`, `video`, `pdf`, `text`, `binary` - Type string `json:"type" gorethink:"type"` } diff --git a/models/invite.go b/models/invite.go deleted file mode 100644 index b4a1cc2..0000000 --- a/models/invite.go +++ /dev/null @@ -1,18 +0,0 @@ -package models - -// Invite is a token (Invite.ID) that allows a user -type Invite struct { - Expiring - Resource - - // AccountCreated is the ID of the account that was created using this invite. - AccountCreated string - - // Username is the desired username. It can be blank. - Username string -} - -// Used returns whether this invitation has been used -func (i *Invite) Used() bool { - return i.DateCreated != i.DateModified -} diff --git a/models/label.go b/models/label.go index a745c98..b36320a 100644 --- a/models/label.go +++ b/models/label.go @@ -4,13 +4,23 @@ package models // Label is what IMAP calls folders, some providers call tags, and what we (and Gmail) call labels. // It's both a simple way for users to organise their emails, but also a way to provide classic folder -// functionality (inbox, spam, drafts, etc). For example, to "archive" an email means to remove the "inbox" label. +// functionality (inbox, spam, drafts, etc). +// Examples: +// * star an email: add the "starred" label +// * archive an email: remove the "inbox" label +// * delete an email: apply the "deleted" label (and cue for deletion) type Label struct { Resource - EmailsUnread int `json:"emails_unread" gorethink:"emails_unread"` - EmailsTotal int `json:"emails_total" gorethink:"emails_total"` - Hidden bool `json:"hidden" gorethink:"hidden"` - Immutable bool `json:"immutable" gorethink:"immutable"` - ThreadsUnread int `json:"threads_unread"` - ThreadsTotal int `json:"threads_total"` + + // Builtin indicates whether a label is created/needed by the system. + // Examples: inbox, trash, spam, drafts, starred, etc. + Builtin bool `json:"builtin" gorethink:"builtin"` + + // EmailsUnread is the number of unread emails that have a particular label applied. + // Storing this for each label eliminates the need of db lookups for this commonly needed information. + EmailsUnread int `json:"emails_unread" gorethink:"emails_unread"` + + // EmailsTotal is the number of emails that have a particular label applied. + // Storing this for each label eliminates the need of db lookups for this commonly needed information. + EmailsTotal int `json:"emails_total" gorethink:"emails_total"` } diff --git a/models/thread.go b/models/thread.go index 0e64e66..f423a27 100644 --- a/models/thread.go +++ b/models/thread.go @@ -1,12 +1,22 @@ package models -// Thread is the data model for a conversation. +// Thread is the data model for a list of emails, usually making up a conversation. type Thread struct { Resource - // Emails is an array of email IDs belonging to this thread + // Emails is a list of email IDs belonging to this thread Emails []string `json:"emails" gorethink:"emails"` + // Labels is a list of label IDs assigned to this thread. + // Note that emails lack this functionality. This way you can't only archive part of a thread. + Labels []string `json:"labels" gorethink:"labels"` + // Members is a slice containing userIDs or email addresses for all members of the thread Members []string `json:"members" gorethink:"members"` + + // Snippet is a bit of text from the conversation, for context. It's only visible to the user. + Snippet Encrypted `json:"snippet" gorethink:"snippet"` + + // Subject is the subject of the thread. + Subject string `json:"subject" gorethink:"subject"` } diff --git a/models/token.go b/models/token.go new file mode 100644 index 0000000..2a824a4 --- /dev/null +++ b/models/token.go @@ -0,0 +1,37 @@ +package models + +// Token is a volatile, unique object. It can be used for user authentication, confirmations, invites, etc. +type Token struct { + Expiring + Resource + + // Type describes the token's purpose: auth, invite, upgrade, etc. + Type string `json:"type" gorethink:"type"` +} + +// MakeToken creates a generic token. +func MakeToken(accountID, _type string, nHours int) Token { + out := Token{ + Resource: MakeResource(accountID, ""), + Type: _type, + } + out.ExpireAfterNHours(nHours) + return out +} + +// Invalidate invalidates a token by adding a period (".") at the beginning of its type. +// It also shortens its expiration time. +func (t *Token) Invalidate() { + t.Type = "." + t.Type + t.ExpireSoon() +} + +// MakeAuthToken creates an authentication token, valid for a limited time. +func MakeAuthToken(accountID string) Token { + return MakeToken(accountID, "auth", 80) +} + +// MakeInviteToken creates an invitation to create an account. +func MakeInviteToken(accountID string) Token { + return MakeToken(accountID, "invite", 240) +} diff --git a/routes/me.go b/routes/me.go index 43fe1c1..3c656d1 100644 --- a/routes/me.go +++ b/routes/me.go @@ -13,10 +13,10 @@ import ( "github.com/lavab/api/utils" ) -// Me returns information about the current user (more exactly, a JSONized models.User) +// Me returns the current account data (models.Account). func Me(w http.ResponseWriter, r *http.Request) { - session, _ := context.Get(r, "session").(*models.AuthToken) - user, ok := dbutils.GetUser(session.AccountID) + session, _ := context.Get(r, "session").(*models.Token) + account, ok := dbutils.GetAccount(session.Owner) if !ok { debug := fmt.Sprintf("Session %s was deleted", session.ID) if err := db.Delete("sessions", session.ID); err != nil { @@ -26,9 +26,9 @@ func Me(w http.ResponseWriter, r *http.Request) { utils.ErrorResponse(w, 410, "Account deactivated", debug) return } - str, err := json.Marshal(user) + str, err := json.Marshal(account) if err != nil { - debug := fmt.Sprint("Failed to marshal models.User:", user) + debug := fmt.Sprint("Failed to marshal models.Account:", account) log.Println("[routes.Me]", debug) utils.ErrorResponse(w, 500, "Internal server error", debug) return @@ -41,7 +41,7 @@ func UpdateMe(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") } -// Sessions lists all active sessions for current user +// Sessions lists all active sessions for current account func Sessions(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") } diff --git a/routes/sessions.go b/routes/sessions.go index 2951f7d..cf60b1b 100644 --- a/routes/sessions.go +++ b/routes/sessions.go @@ -17,15 +17,15 @@ const SessionDurationInHours = 72 // Login gets a username and password and returns a session token on success func Login(w http.ResponseWriter, r *http.Request) { username, password := r.FormValue("username"), r.FormValue("password") - user, ok := dbutils.FindUserByName(username) - if !ok || user == nil || !utils.BcryptVerify(user.Password, password) { + account, ok := dbutils.FindAccountByUsername(username) + if !ok || account == nil || !utils.BcryptVerify(account.Password, password) { utils.ErrorResponse(w, 403, "Wrong username or password", - fmt.Sprintf("user: %+v", user)) + fmt.Sprintf("account: %+v", account)) return } - // TODO check number of sessions for the current user here - session := models.AuthToken{Resource: models.MakeResource(user.ID, "")} + // TODO check number of sessions for the current account here + session := models.Token{Resource: models.MakeResource(account.ID, "")} session.ExpireAfterNHours(SessionDurationInHours) db.Insert("sessions", session) @@ -39,9 +39,8 @@ func Login(w http.ResponseWriter, r *http.Request) { // Signup gets a username and password and creates a user account on success func Signup(w http.ResponseWriter, r *http.Request) { username, password := r.FormValue("username"), r.FormValue("password") - // regt := r.FormValue("reg_token") - if _, ok := dbutils.FindUserByName(username); ok { + if _, ok := dbutils.FindAccountByUsername(username); ok { utils.ErrorResponse(w, 409, "Username already exists", "") return } @@ -55,32 +54,32 @@ func Signup(w http.ResponseWriter, r *http.Request) { // TODO: sanitize user name (i.e. remove caps, periods) - user := models.User{ + account := models.Account{ Resource: models.MakeResource(utils.UUID(), username), Password: string(hash), } - if err := db.Insert("users", user); err != nil { + if err := db.Insert("account", account); err != nil { utils.ErrorResponse(w, 500, "Internal server error", - fmt.Sprintf("Couldn't insert %+v to database", user)) + fmt.Sprintf("Couldn't insert %+v to database", account)) } utils.JSONResponse(w, 201, map[string]interface{}{ "message": "Signup successful", "success": true, - "data": user, + "data": account, }) } // Logout destroys the current session token func Logout(w http.ResponseWriter, r *http.Request) { - session := context.Get(r, "session").(*models.AuthToken) + session := context.Get(r, "session").(*models.Token) if err := db.Delete("sessions", session.ID); err != nil { utils.ErrorResponse(w, 500, "Internal server error", fmt.Sprint("Couldn't delete session %v. %v", session, err)) } utils.JSONResponse(w, 410, map[string]interface{}{ - "message": fmt.Sprintf("Successfully logged out", session.AccountID), + "message": fmt.Sprintf("Successfully logged out", session.Owner), "success": true, "deleted": session.ID, }) From e2d7a885fc0a3ce4f7259180c234990973983035 Mon Sep 17 00:00:00 2001 From: Andrei Simionescu Date: Mon, 10 Nov 2014 13:47:56 +0100 Subject: [PATCH 18/20] merge fix --- models/base_resource.go | 4 ++-- routes/accounts.go | 20 ++++++++++---------- routes/tokens.go | 14 +++++++------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/models/base_resource.go b/models/base_resource.go index d1df577..c8d5629 100644 --- a/models/base_resource.go +++ b/models/base_resource.go @@ -26,14 +26,14 @@ type Resource struct { } // MakeResource creates a new Resource object with sane defaults. -func MakeResource(accountID, name string) Resource { +func MakeResource(ownerID, name string) Resource { t := time.Now() return Resource{ ID: utils.UUID(), DateModified: t, DateCreated: t, Name: name, - Owner: accountID, + Owner: ownerID, } } diff --git a/routes/accounts.go b/routes/accounts.go index aa1838f..423e52c 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -35,9 +35,9 @@ type AccountsCreateRequest struct { // AccountsCreateResponse contains the output of the AccountsCreate request. type AccountsCreateResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - User *models.User `json:"data,omitempty"` + Success bool `json:"success"` + Message string `json:"message"` + User *models.Account `json:"data,omitempty"` } // AccountsCreate creates a new account in the system. @@ -58,7 +58,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { } // Ensure that the user with requested username doesn't exist - if _, ok := dbutils.FindUserByName(input.Username); ok { + if _, ok := dbutils.FindAccountByUsername(input.Username); ok { utils.JSONResponse(w, 409, &AccountsCreateResponse{ Success: false, Message: "Username already exists", @@ -83,7 +83,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // TODO: sanitize user name (i.e. remove caps, periods) // Create a new user object - user := &models.User{ + user := &models.Account{ Resource: models.MakeResource(utils.UUID(), input.Username), Password: string(hash), } @@ -110,9 +110,9 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // AccountsGetResponse contains the result of the AccountsGet request. type AccountsGetResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - User *models.User `json:"user,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + User *models.Account `json:"user,omitempty"` } // AccountsGet returns the information about the specified account @@ -137,10 +137,10 @@ func AccountsGet(c *web.C, w http.ResponseWriter, r *http.Request) { } // Fetch the current session from the database - session := c.Env["session"].(*models.AuthToken) + session := c.Env["session"].(*models.Token) // Fetch the user object from the database - user, ok := dbutils.GetUser(session.AccountID) + user, ok := dbutils.GetAccount(session.Owner) if !ok { // The session refers to a non-existing user env.G.Log.WithFields(logrus.Fields{ diff --git a/routes/tokens.go b/routes/tokens.go index b31bb6b..20fc206 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -25,7 +25,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.AuthToken) + session := c.Env["session"].(*models.Token) // Respond with the token information utils.JSONResponse(w, 200, &TokensGetResponse{ @@ -43,9 +43,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.AuthToken `json:"token,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Token *models.Token `json:"token,omitempty"` } // TokensCreate allows logging in to an account. @@ -66,7 +66,7 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { } // Authenticate the user - user, ok := dbutils.FindUserByName(input.Username) + user, ok := dbutils.FindAccountByUsername(input.Username) if !ok || user == nil || !utils.BcryptVerify(user.Password, input.Password) { utils.JSONResponse(w, 403, &TokensCreateResponse{ Success: false, @@ -79,7 +79,7 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { expDate := time.Now().Add(time.Hour * time.Duration(env.G.Config.SessionDuration)) // Create a new token - token := &models.AuthToken{ + token := &models.Token{ Expiring: models.Expiring{expDate}, Resource: models.MakeResource(user.ID, "Auth token expiring on "+expDate.Format(time.RFC3339)), } @@ -104,7 +104,7 @@ type TokensDeleteResponse struct { // TokensDelete destroys the current session token. func TokensDelete(c *web.C, w http.ResponseWriter, r *http.Request) { // Get the session from the middleware - session := c.Env["session"].(*models.AuthToken) + session := c.Env["session"].(*models.Token) // Delete it from the database if err := db.Delete("sessions", session.ID); err != nil { From 1e88bc5c5b5651533f69da51d20810cee7a7a6e6 Mon Sep 17 00:00:00 2001 From: "Piotr \"Orange\" Zduniak" Date: Mon, 10 Nov 2014 18:02:20 +0100 Subject: [PATCH 19/20] Added a travis script --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a7db419 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: go + +go: + - 1.3.1 \ No newline at end of file From 5da3d13aa2cd02d5f16d10c0b5d3b87fc6519e12 Mon Sep 17 00:00:00 2001 From: "Piotr \"Orange\" Zduniak" Date: Mon, 10 Nov 2014 21:11:23 +0100 Subject: [PATCH 20/20] Merge fix --- db/crud.go | 281 ---------------------------------------- db/default_crud.go | 229 ++++++++++++++++++++++++++++++++ db/errors.go | 38 +++--- db/rethink_crud.go | 55 ++++++++ db/setup.go | 116 +++++------------ db/table_accounts.go | 32 +++++ db/table_sessions.go | 21 +++ dbutils/accounts.go | 34 ----- dbutils/sessions.go | 23 ---- dbutils/setup.go | 22 ---- env/env.go | 14 +- main.go | 64 +++++++++ models/base_expiring.go | 4 +- models/base_resource.go | 4 +- routes/accounts.go | 23 ++-- routes/emails.go | 2 +- routes/middleware.go | 20 +-- routes/setup.go | 8 -- routes/tokens.go | 10 +- utils/misc.go | 19 --- 20 files changed, 495 insertions(+), 524 deletions(-) delete mode 100644 db/crud.go create mode 100644 db/default_crud.go create mode 100644 db/rethink_crud.go create mode 100644 db/table_accounts.go create mode 100644 db/table_sessions.go delete mode 100644 dbutils/accounts.go delete mode 100644 dbutils/sessions.go delete mode 100644 dbutils/setup.go delete mode 100644 routes/setup.go diff --git a/db/crud.go b/db/crud.go deleted file mode 100644 index 66bac95..0000000 --- a/db/crud.go +++ /dev/null @@ -1,281 +0,0 @@ -package db - -import ( - r "github.com/dancannon/gorethink" -) - -type RethinkTable interface { - GetTableName() string - GetDbName() string -} - -type RethinkCreater interface { - Insert(data interface{}) error -} - -type RethinkReader interface { - Find(id string) (*r.Cursor, error) - FindFetchOne(id string, value interface{}) error - - FindBy(key string, value interface{}) (*r.Cursor, error) - FindByAndFetch(key string, value interface{}, results interface{}) error - FindByAndFetchOne(key string, value interface{}, result interface{}) error - - Where(filter map[string]interface{}) (*r.Cursor, error) - WhereAndFetch(filter map[string]interface{}, results interface{}) error - WhereAndFetchOne(filter map[string]interface{}, result interface{}) error - - FindByIndex(index string, values ...interface{}) (*r.Cursor, error) - FindByIndexFetch(results interface{}, index string, values ...interface{}) error - FindByIndexFetchOne(result interface{}, index string, values ...interface{}) error -} - -type RethinkUpdater interface { - Update(data interface{}) error - UpdateId(id string, data interface{}) error -} - -type RethinkDeleter interface { - Delete(pred interface{}) error - DeleteId(id string) error -} - -//The interface that all tables should implement -type RethinkCrud interface { - RethinkCreater - RethinkReader - RethinkUpdater - RethinkDeleter - RethinkTable -} - -//The default impementation that should be embedded -type RethinkCrudImpl struct { - table string - db string -} - -func NewCrudTable(db, table string) *RethinkCrudImpl { - return &RethinkCrudImpl{ - db: db, - table: table, - } -} - -//The RethinkTable implementation -func (rc *RethinkCrudImpl) GetTableName() string { - return rc.table -} - -func (rc *RethinkCrudImpl) GetDbName() string { - return rc.db -} - -//Gets the current table as a Rethink Term -func (rc *RethinkCrudImpl) GetTable() r.Term { - return r.Table(rc.table) -} - -//inserts a document to the database -func (rc *RethinkCrudImpl) Insert(data interface{}) error { - _, err := rc.GetTable().Insert(data).RunWrite(config.Session) - if err != nil { - return NewDbErr(rc, err) - } - - return nil -} - -//Updates according to the specified options in data -func (rc *RethinkCrudImpl) Update(data interface{}) error { - _, err := rc.GetTable().Update(data).RunWrite(config.Session) - if err != nil { - return NewDbErr(rc, err) - } - - return nil -} - -//Updates a specific id in database, with options from data -func (rc *RethinkCrudImpl) UpdateId(id string, data interface{}) error { - _, err := rc.GetTable().Get(id).Update(data).RunWrite(config.Session) - if err != nil { - return NewDbErr(rc, err) - } - - return nil -} - -//Deletes the documents that are in pred argument -func (rc *RethinkCrudImpl) Delete(pred interface{}) error { - _, err := rc.GetTable().Filter(pred).Delete().RunWrite(config.Session) - if err != nil { - return NewDbErr(rc, err) - } - - return nil -} - -//Deletes a given id -func (rc *RethinkCrudImpl) DeleteId(id string) error { - _, err := rc.GetTable().Get(id).Delete().RunWrite(config.Session) - if err != nil { - return NewDbErr(rc, err) - } - - return nil -} - -//Finds a given object from db, it does not perform any fetching -func (rc *RethinkCrudImpl) Find(id string) (*r.Cursor, error) { - cursor, err := rc.GetTable().Get(id).Run(config.Session) - if err != nil { - return nil, NewDbErr(rc, err) - } - - return cursor, nil -} - -//Fetches the specified object from db and fills the value with it -func (rc *RethinkCrudImpl) FindFetchOne(id string, value interface{}) error { - cursor, err := rc.Find(id) - if err != nil { - return err - } - - //now fetch the item from database - if err := cursor.One(value); err != nil { - return NewDbErr(rc, err) - } - - //we have success here - return nil -} - -//FindBy is for looking up for key=value situations -func (rc *RethinkCrudImpl) FindBy(key string, value interface{}) (*r.Cursor, error) { - filterMap := map[string]interface{}{ - key: value, - } - cursor, err := rc.GetTable().Filter(filterMap).Run(config.Session) - if err != nil { - return nil, NewDbErr(rc, err) - } - - return cursor, nil -} - -//FindBy is for looking up for key=value situations with fetch all -func (rc *RethinkCrudImpl) FindByAndFetch(key string, value interface{}, results interface{}) error { - - cursor, err := rc.FindBy(key, value) - if err != nil { - return err - } - - //now fetch the item from database - if err := cursor.All(results); err != nil { - return NewDbErr(rc, err) - } - - //we have success here - return nil -} - -//Fetches the specified object from db and fills the value with it -func (rc *RethinkCrudImpl) FindByAndFetchOne(key string, value interface{}, result interface{}) error { - - cursor, err := rc.FindBy(key, value) - if err != nil { - return err - } - - //now fetch the item from database - if err := cursor.One(result); err != nil { - return NewDbErr(rc, err) - } - - //we have success here - return nil -} - -//Where is for asking for more than one field from database, useful for passing a few -//pairs for AND querying -func (rc *RethinkCrudImpl) Where(filter map[string]interface{}) (*r.Cursor, error) { - cursor, err := rc.GetTable().Filter(filter).Run(config.Session) - if err != nil { - return nil, NewDbErr(rc, err) - } - - return cursor, nil -} - -//Where with fetch all -func (rc *RethinkCrudImpl) WhereAndFetch(filter map[string]interface{}, results interface{}) error { - cursor, err := rc.Where(filter) - if err != nil { - return err - } - - //now fetch the item from database - if err := cursor.All(results); err != nil { - return NewDbErr(rc, err) - } - - //we have success here - return nil -} - -//Where with fetch all -func (rc *RethinkCrudImpl) WhereAndFetchOne(filter map[string]interface{}, result interface{}) error { - cursor, err := rc.Where(filter) - if err != nil { - return err - } - - //now fetch the item from database - if err := cursor.One(result); err != nil { - return NewDbErr(rc, err) - } - - //we have success here - return nil -} - -//GetAll fetches all items in the table that satisfy item[index] == value -func (rc *RethinkCrudImpl) FindByIndex(index string, values ...interface{}) (*r.Cursor, error) { - cursor, err := rc.GetTable().GetAllByIndex(index, values...).Run(config.Session) - if err != nil { - return nil, NewDbErr(rc, err) - } - - return cursor, nil -} - -func (rc *RethinkCrudImpl) FindByIndexFetch(results interface{}, index string, values ...interface{}) error { - cursor, err := rc.FindByIndex(index, values...) - if err != nil { - return err - } - - //now fetch the item from database - if err := cursor.All(results); err != nil { - return NewDbErr(rc, err) - } - - return nil -} - -func (rc *RethinkCrudImpl) FindByIndexFetchOne(result interface{}, index string, values ...interface{}) error { - cursor, err := rc.FindByIndex(index, values...) - if err != nil { - return err - } - - //now fetch the item from database - if err := cursor.One(result); err != nil { - return NewDbErr(rc, err) - } - - return nil -} diff --git a/db/default_crud.go b/db/default_crud.go new file mode 100644 index 0000000..e9bf088 --- /dev/null +++ b/db/default_crud.go @@ -0,0 +1,229 @@ +package db + +import ( + "github.com/dancannon/gorethink" +) + +// Default contains the basic implementation of the gorethinkCRUD interface +type Default struct { + table string + db string + session *gorethink.Session +} + +// NewCRUDTable sets up a new Default struct +func NewCRUDTable(session *gorethink.Session, db, table string) *Default { + return &Default{ + db: db, + table: table, + session: session, + } +} + +// GetTableName returns table's name +func (d *Default) GetTableName() string { + return d.table +} + +// GetDBName returns database's name +func (d *Default) GetDBName() string { + return d.db +} + +// GetTable returns the table as a gorethink.Term +func (d *Default) GetTable() gorethink.Term { + return gorethink.Table(d.table) +} + +// Insert inserts a document into the database +func (d *Default) Insert(data interface{}) error { + _, err := d.GetTable().Insert(data).RunWrite(d.session) + if err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} + +// Update performs an update on an existing resource according to passed data +func (d *Default) Update(data interface{}) error { + _, err := d.GetTable().Update(data).RunWrite(d.session) + if err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} + +// UpdateID performs an update on an existing resource with ID that equals the id argument +func (d *Default) UpdateID(id string, data interface{}) error { + _, err := d.GetTable().Get(id).Update(data).RunWrite(d.session) + if err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} + +// Delete deletes resources that match the passed filter +func (d *Default) Delete(pred interface{}) error { + _, err := d.GetTable().Filter(pred).Delete().RunWrite(d.session) + if err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} + +// DeleteID deletes a resource with specified ID +func (d *Default) DeleteID(id string) error { + _, err := d.GetTable().Get(id).Delete().RunWrite(d.session) + if err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} + +// Find searches for a resource in the database and then returns a cursor +func (d *Default) Find(id string) (*gorethink.Cursor, error) { + cursor, err := d.GetTable().Get(id).Run(d.session) + if err != nil { + return nil, NewDatabaseError(d, err, "") + } + + return cursor, nil +} + +// FindFetchOne searches for a resource and then unmarshals the first row into value +func (d *Default) FindFetchOne(id string, value interface{}) error { + cursor, err := d.Find(id) + if err != nil { + return err + } + + if err := cursor.One(value); err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} + +// FindBy is an utility for fetching values if they are stored in a key-value manenr. +func (d *Default) FindBy(key string, value interface{}) (*gorethink.Cursor, error) { + filterMap := map[string]interface{}{ + key: value, + } + + cursor, err := d.GetTable().Filter(filterMap).Run(d.session) + if err != nil { + return nil, NewDatabaseError(d, err, "") + } + + return cursor, nil +} + +// FindByAndFetch retrieves a value by key and then fills results with the result. +func (d *Default) FindByAndFetch(key string, value interface{}, results interface{}) error { + cursor, err := d.FindBy(key, value) + if err != nil { + return err + } + + if err := cursor.All(results); err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} + +// FindByFetchOne retrieves a value by key and then fills result with the first row of the result +func (d *Default) FindByAndFetchOne(key string, value interface{}, result interface{}) error { + cursor, err := d.FindBy(key, value) + if err != nil { + return err + } + + if err := cursor.One(result); err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} + +// Where allows filtering with multiple fields +func (d *Default) Where(filter map[string]interface{}) (*gorethink.Cursor, error) { + cursor, err := d.GetTable().Filter(filter).Run(d.session) + if err != nil { + return nil, NewDatabaseError(d, err, "") + } + + return cursor, nil +} + +// WhereAndFetch filters with multiple fields and then fills results with all found resources +func (d *Default) WhereAndFetch(filter map[string]interface{}, results interface{}) error { + cursor, err := d.Where(filter) + if err != nil { + return err + } + + if err := cursor.All(results); err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} + +// WhereAndFetchOne filters with multiple fields and then fills result with the first found resource +func (d *Default) WhereAndFetchOne(filter map[string]interface{}, result interface{}) error { + cursor, err := d.Where(filter) + if err != nil { + return err + } + + if err := cursor.One(result); err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} + +// FindByIndex filters all resources whose index is matching +func (d *Default) FindByIndex(index string, values ...interface{}) (*gorethink.Cursor, error) { + cursor, err := d.GetTable().GetAllByIndex(index, values...).Run(d.session) + if err != nil { + return nil, NewDatabaseError(d, err, "") + } + + return cursor, nil +} + +// FindByIndexFetch filters all resources whose index is matching and fills results with all found resources +func (d *Default) FindByIndexFetch(results interface{}, index string, values ...interface{}) error { + cursor, err := d.FindByIndex(index, values...) + if err != nil { + return err + } + + //now fetch the item from database + if err := cursor.All(results); err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} + +// FindByIndexFetchOne filters all resources whose index is matching and fills result with the first one found +func (d *Default) FindByIndexFetchOne(result interface{}, index string, values ...interface{}) error { + cursor, err := d.FindByIndex(index, values...) + if err != nil { + return err + } + + if err := cursor.One(result); err != nil { + return NewDatabaseError(d, err, "") + } + + return nil +} diff --git a/db/errors.go b/db/errors.go index 1fb91ad..4b651a0 100644 --- a/db/errors.go +++ b/db/errors.go @@ -4,33 +4,27 @@ import ( "fmt" ) -type DbError struct { - err error - msg string - table RethinkTable +type DatabaseError struct { + err error + message string + table RethinkTable } -func (derr *DbError) Error() string { +func (d *DatabaseError) Error() string { return fmt.Sprintf( - "%s - Db: %s - Table : %s - %s ", - derr.msg, - derr.table.GetDbName(), - derr.table.GetTableName(), - derr.err) + "%s - DB: %s - Table : %s - %s", + d.message, + d.table.GetDBName(), + d.table.GetTableName(), + d.err, + ) } -func NewDbErr(t RethinkTable, err error) *DbError { - return &DbError{ - err: err, - table: t, - } -} - -func NewDbErrWithMsg(t RethinkTable, err error, msg string) *DbError { - return &DbError{ - err: err, - table: t, - msg: msg, +func NewDatabaseError(t RethinkTable, err error, message string) *DatabaseError { + return &DatabaseError{ + err: err, + table: t, + message: message, } } diff --git a/db/rethink_crud.go b/db/rethink_crud.go new file mode 100644 index 0000000..b04a381 --- /dev/null +++ b/db/rethink_crud.go @@ -0,0 +1,55 @@ +package db + +import ( + rethink "github.com/dancannon/gorethink" +) + +// RethinkTable contains the most basic table functions +type RethinkTable interface { + GetTableName() string + GetDBName() string +} + +// RethinkCreator contains a function to create new instances in the table +type RethinkCreator interface { + Insert(data interface{}) error +} + +// RethinkReader allows fetching resources from the database +type RethinkReader interface { + Find(id string) (*rethink.Cursor, error) + FindFetchOne(id string, value interface{}) error + + FindBy(key string, value interface{}) (*rethink.Cursor, error) + FindByAndFetch(key string, value interface{}, results interface{}) error + FindByAndFetchOne(key string, value interface{}, result interface{}) error + + Where(filter map[string]interface{}) (*rethink.Cursor, error) + WhereAndFetch(filter map[string]interface{}, results interface{}) error + WhereAndFetchOne(filter map[string]interface{}, result interface{}) error + + FindByIndex(index string, values ...interface{}) (*rethink.Cursor, error) + FindByIndexFetch(results interface{}, index string, values ...interface{}) error + FindByIndexFetchOne(result interface{}, index string, values ...interface{}) error +} + +// RethinkUpdater allows updating existing resources in the database +type RethinkUpdater interface { + Update(data interface{}) error + UpdateID(id string, data interface{}) error +} + +// RethinkDeleter allows deleting resources from the database +type RethinkDeleter interface { + Delete(pred interface{}) error + DeleteID(id string) error +} + +// RethinkCRUD is the interface that every table should implement +type RethinkCRUD interface { + RethinkCreator + RethinkReader + RethinkUpdater + RethinkDeleter + RethinkTable +} diff --git a/db/setup.go b/db/setup.go index fb53fdc..835a146 100644 --- a/db/setup.go +++ b/db/setup.go @@ -1,106 +1,56 @@ package db import ( - "fmt" - "log" - "os" - "time" - - r "github.com/dancannon/gorethink" + "github.com/dancannon/gorethink" ) -const ( - TABLE_SESSIONS = "sessions" - TABLE_USERS = "users" - TABLE_EMAILS = "emails" - TABLE_DRAFTS = "drafts" - TABLE_CONTACTS = "contacts" - TABLE_THREADS = "threads" - TABLE_LABELS = "labels" - TABLE_KEYS = "keys" +// Publicly exported table models +var ( + Accounts *AccountsTable + Sessions *TokensTable ) -var config struct { - Session *r.Session - Url string - AuthKey string - Db string +// Indexes of tables in the database +var tableIndexes = map[string][]string{ + "tokens": []string{"user", "user_id"}, + "accounts": []string{"name"}, + "emails": []string{"user_id"}, + "drafts": []string{"user_id"}, + "contacts": []string{}, + "threads": []string{"user_id"}, + "labels": []string{}, + "keys": []string{}, } -var CurrentConfig = config - -var dbs = []string{ +// List of names of databases +var databaseNames = []string{ "prod", "staging", "dev", } -var tablesAndIndexes = map[string][]string{ - TABLE_SESSIONS: []string{"user", "user_id"}, - TABLE_USERS: []string{"name"}, - TABLE_EMAILS: []string{"user_id"}, - TABLE_DRAFTS: []string{"user_id"}, - TABLE_CONTACTS: []string{}, - TABLE_THREADS: []string{"user_id"}, - TABLE_LABELS: []string{}, - TABLE_KEYS: []string{}, -} +// Setup configures the RethinkDB server +func Setup(opts gorethink.ConnectOpts) error { + // Initialize a new setup connection + setupSession, err := gorethink.Connect(opts) + if err != nil { + return err + } -func init() { - config.Url = "localhost:28015" - config.AuthKey = "" - config.Db = "dev" + // Create databases + for _, d := range databaseNames { + gorethink.DbCreate(d).Run(setupSession) - if tmp := os.Getenv("RETHINKDB_URL"); tmp != "" { - config.Url = tmp - } else if tmp := os.Getenv("RETHINKDB_PORT_28015_TCP_ADDR"); tmp != "" { - config.Url = fmt.Sprintf("%s:28015", tmp) - } else { - log.Printf("No database URL specified, using %s.\n", config.Url) - } - if tmp := os.Getenv("RETHINKDB_AUTHKEY"); tmp != "" { - config.AuthKey = tmp - } else { - log.Fatalln("Variable RETHINKDB_AUTHKEY not set.") - } - if tmp := os.Getenv("API_ENV"); tmp != "" { - // TODO add check that tmp is in dbs - config.Db = tmp - } else { - log.Printf("No database specified, using %s.\n", config.Db) - } + // Create tables + for t, indexes := range tableIndexes { + gorethink.Db(d).TableCreate(t).RunWrite(setupSession) - // Initialise databases, tables, and indexes. This might take a while if they don't exist - setupSession, err := r.Connect(r.ConnectOpts{ - Address: config.Url, - AuthKey: config.AuthKey, - MaxIdle: 10, - IdleTimeout: time.Second * 10, - }) - if err != nil { - log.Fatalf("Error connecting to DB: %s", err) - } - log.Println("Creating dbs \\ tables \\ indexes") - for _, d := range dbs { - log.Println(d) - r.DbCreate(d).Run(setupSession) - for t, indexes := range tablesAndIndexes { - log.Println("› ", t) - r.Db(d).TableCreate(t).RunWrite(setupSession) + // Create indexes for _, index := range indexes { - log.Println("› › ", index) - r.Db(d).Table(t).IndexCreate(index).Exec(setupSession) + gorethink.Db(d).Table(t).IndexCreate(index).Exec(setupSession) } } } - setupSession.Close() - // Setting up the main session - config.Session, err = r.Connect(r.ConnectOpts{ - Address: config.Url, - AuthKey: config.AuthKey, - Database: config.Db, - MaxIdle: 10, - IdleTimeout: time.Second * 10, - }) + return setupSession.Close() } diff --git a/db/table_accounts.go b/db/table_accounts.go new file mode 100644 index 0000000..d7a1ee9 --- /dev/null +++ b/db/table_accounts.go @@ -0,0 +1,32 @@ +package db + +import ( + "github.com/lavab/api/models" +) + +// AccountsTable implements the CRUD interface for accounts +type AccountsTable struct { + RethinkCRUD +} + +// GetAccount returns an account with specified ID +func (users *AccountsTable) GetAccount(id string) (*models.Account, error) { + var result models.Account + + if err := users.FindFetchOne(id, &result); err != nil { + return nil, err + } + + return &result, nil +} + +// FindAccountByName returns an account with specified name +func (users *AccountsTable) FindAccountByName(name string) (*models.Account, error) { + var result models.Account + + if err := users.FindByIndexFetchOne(&result, "name", name); err != nil { + return nil, err + } + + return &result, nil +} diff --git a/db/table_sessions.go b/db/table_sessions.go new file mode 100644 index 0000000..a19ccb0 --- /dev/null +++ b/db/table_sessions.go @@ -0,0 +1,21 @@ +package db + +import ( + "github.com/lavab/api/models" +) + +// TokensTable implements the CRUD interface for tokens +type TokensTable struct { + RethinkCRUD +} + +// GetToken returns a token with specified name +func (s *TokensTable) GetToken(id string) (*models.Token, error) { + var result models.Token + + if err := s.FindFetchOne(id, &result); err != nil { + return nil, err + } + + return &result, nil +} diff --git a/dbutils/accounts.go b/dbutils/accounts.go deleted file mode 100644 index 1479684..0000000 --- a/dbutils/accounts.go +++ /dev/null @@ -1,34 +0,0 @@ -package dbutils - -import ( - "github.com/lavab/api/db" - "github.com/lavab/api/models" -) - -//implements the base crud interface -type UsersTable struct { - db.RethinkCrud -} - -func (users *UsersTable) GetUser(id string) (*models.User, bool) { - var result models.User - - if err := users.FindFetchOne(id, &result); err != nil { - log.Println(err.Error()) - return nil, false - } - - return &result, true - -} - -func (users *UsersTable) FindUserByName(username string) (*models.User, bool) { - var result models.User - - if err := users.FindByIndexFetchOne(&result, "name", username); err != nil { - log.Println(err.Error()) - return nil, false - } - - return &result, true -} diff --git a/dbutils/sessions.go b/dbutils/sessions.go deleted file mode 100644 index 26c063a..0000000 --- a/dbutils/sessions.go +++ /dev/null @@ -1,23 +0,0 @@ -package dbutils - -import ( - "log" - - "github.com/lavab/api/db" - "github.com/lavab/api/models" -) - -type SessionTable struct { - db.RethinkCrud -} - -func (sessions *SessionTable) GetSession(id string) (*models.Session, bool) { - var result models.Session - - if err := sessions.FindFetchOne(id, &result); err != nil { - log.Println(err.Error()) - return nil, false - } - - return &result, true -} diff --git a/dbutils/setup.go b/dbutils/setup.go deleted file mode 100644 index 69353f8..0000000 --- a/dbutils/setup.go +++ /dev/null @@ -1,22 +0,0 @@ -package dbutils - -import ( - "github.com/lavab/api/db" -) - -//The crud interfaces for models -var Users UsersTable -var Sessions SessionTable - -//The init version for routes, mostly related to -//initing the table informatio -func init() { - //at this stage we should have the db config variable initialized - userCrud := db.NewCrudTable(db.CurrentConfig.Db, db.TABLE_USERS) - Users = UsersTable{RethinkCrud: userCrud} - - //init the sessions variable - sessionCrud := db.NewCrudTable(db.CurrentConfig.Db, db.TABLE_SESSIONS) - Sessions = SessionTable{RethinkCrud: sessionCrud} - -} diff --git a/env/env.go b/env/env.go index 29bfce7..9fd89bb 100644 --- a/env/env.go +++ b/env/env.go @@ -2,11 +2,21 @@ package env import ( "github.com/Sirupsen/logrus" + "github.com/dancannon/gorethink" + + "github.com/lavab/api/db" ) type Environment struct { - Log *logrus.Logger - Config *Config + Log *logrus.Logger + Config *Config + Rethink *gorethink.Session + R *R +} + +type R struct { + Accounts *db.AccountsTable + Tokens *db.TokensTable } var G *Environment diff --git a/main.go b/main.go index d767746..d9e9e08 100644 --- a/main.go +++ b/main.go @@ -3,14 +3,18 @@ package main import ( "net" "net/http" + "os" + "time" "github.com/Sirupsen/logrus" + "github.com/dancannon/gorethink" "github.com/goji/glogrus" "github.com/namsral/flag" "github.com/zenazn/goji/graceful" "github.com/zenazn/goji/web" "github.com/zenazn/goji/web/middleware" + "github.com/lavab/api/db" "github.com/lavab/api/env" "github.com/lavab/api/routes" ) @@ -19,10 +23,27 @@ import ( // https://github.com/unrolled/secure var ( + // General flags bindAddress = flag.String("bind", ":5000", "Network address used to bind") apiVersion = flag.String("version", "v0", "Shown API version") logFormatterType = flag.String("log", "text", "Log formatter type. Either \"json\" or \"text\"") sessionDuration = flag.Int("session_duration", 72, "Session duration expressed in hours") + // Database-related flags + rethinkdbURL = flag.String("rethinkdb_url", func() string { + address := os.Getenv("RETHINKDB_PORT_28015_TCP_ADDR") + if address == "" { + address = "localhost" + } + return address + ":28015" + }(), "Address of the RethinkDB database") + rethinkdbKey = flag.String("rethinkdb_key", os.Getenv("RETHINKDB_AUTHKEY"), "Authentication key of the RethinkDB database") + rethinkdbDatabase = flag.String("rethinkdb_db", func() string { + database := os.Getenv("RETHINKDB_NAME") + if database == "" { + database = "dev" + } + return database + }(), "Database name on the RethinkDB server") ) func main() { @@ -39,6 +60,47 @@ func main() { log.Formatter = &logrus.JSONFormatter{} } + // Set up the database + rethinkOpts := gorethink.ConnectOpts{ + Address: *rethinkdbURL, + AuthKey: *rethinkdbKey, + MaxIdle: 10, + IdleTimeout: time.Second * 10, + } + err := db.Setup(rethinkOpts) + if err != nil { + log.WithFields(logrus.Fields{ + "error": err, + }).Fatal("Unable to set up the database") + } + + // Initialize the actual connection + rethinkOpts.Database = *rethinkdbDatabase + rethinkSession, err := gorethink.Connect(rethinkOpts) + if err != nil { + log.WithFields(logrus.Fields{ + "error": err, + }).Fatal("Unable to connect to the database") + } + + // Initialize the tables + tables := &env.R{ + Accounts: &db.AccountsTable{ + RethinkCRUD: db.NewCRUDTable( + rethinkSession, + rethinkOpts.Database, + "accounts", + ), + }, + Tokens: &db.TokensTable{ + RethinkCRUD: db.NewCRUDTable( + rethinkSession, + rethinkOpts.Database, + "tokens", + ), + }, + } + // Create a new goji mux mux := web.New() @@ -122,6 +184,8 @@ func main() { LogFormatterType: *logFormatterType, SessionDuration: *sessionDuration, }, + Rethink: rethinkSession, + R: tables, } // Log that we're starting the server diff --git a/models/base_expiring.go b/models/base_expiring.go index a2d82bd..fe02c26 100644 --- a/models/base_expiring.go +++ b/models/base_expiring.go @@ -1,6 +1,8 @@ package models -import "time" +import ( + "time" +) // Expiring is a base struct for resources that expires e.g. sessions. type Expiring struct { diff --git a/models/base_resource.go b/models/base_resource.go index c8d5629..ef97bd9 100644 --- a/models/base_resource.go +++ b/models/base_resource.go @@ -3,7 +3,7 @@ package models import ( "time" - "github.com/lavab/api/utils" + "github.com/dchest/uniuri" ) // Resource is the base type for API resources. @@ -29,7 +29,7 @@ type Resource struct { func MakeResource(ownerID, name string) Resource { t := time.Now() return Resource{ - ID: utils.UUID(), + ID: uniuri.NewLen(uniuri.UUIDLen), DateModified: t, DateCreated: t, Name: name, diff --git a/routes/accounts.go b/routes/accounts.go index 423e52c..2103fa3 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -6,8 +6,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" - "github.com/lavab/api/db" - "github.com/lavab/api/dbutils" "github.com/lavab/api/env" "github.com/lavab/api/models" "github.com/lavab/api/utils" @@ -37,7 +35,7 @@ type AccountsCreateRequest struct { type AccountsCreateResponse struct { Success bool `json:"success"` Message string `json:"message"` - User *models.Account `json:"data,omitempty"` + Account *models.Account `json:"account,omitempty"` } // AccountsCreate creates a new account in the system. @@ -58,7 +56,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { } // Ensure that the user with requested username doesn't exist - if _, ok := dbutils.FindAccountByUsername(input.Username); ok { + if _, err := env.G.R.Accounts.FindAccountByName(input.Username); err != nil { utils.JSONResponse(w, 409, &AccountsCreateResponse{ Success: false, Message: "Username already exists", @@ -83,13 +81,13 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // TODO: sanitize user name (i.e. remove caps, periods) // Create a new user object - user := &models.Account{ - Resource: models.MakeResource(utils.UUID(), input.Username), + account := &models.Account{ + Resource: models.MakeResource("", input.Username), Password: string(hash), } // Try to save it in the database - if err := db.Insert("users", user); err != nil { + if err := env.G.R.Accounts.Insert(account); err != nil { utils.JSONResponse(w, 500, &AccountsCreateResponse{ Success: false, Message: "Internal server error - AC/CR/02", @@ -104,7 +102,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { utils.JSONResponse(w, 201, &AccountsCreateResponse{ Success: true, Message: "A new account was successfully created", - User: user, + Account: account, }) } @@ -140,15 +138,16 @@ func AccountsGet(c *web.C, w http.ResponseWriter, r *http.Request) { session := c.Env["session"].(*models.Token) // Fetch the user object from the database - user, ok := dbutils.GetAccount(session.Owner) - if !ok { + user, err := env.G.R.Accounts.GetAccount(session.Owner) + if err != nil { // The session refers to a non-existing user env.G.Log.WithFields(logrus.Fields{ - "id": session.ID, + "id": session.ID, + "error": err, }).Warn("Valid session referred to a removed account") // Try to remove the orphaned session - if err := db.Delete("sessions", session.ID); err != nil { + if err := env.G.R.Tokens.DeleteID(session.ID); err != nil { env.G.Log.WithFields(logrus.Fields{ "id": session.ID, "error": err, diff --git a/routes/emails.go b/routes/emails.go index 44c3305..deb3dd2 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -35,7 +35,7 @@ type EmailsCreateResponse struct { func EmailsCreate(w http.ResponseWriter, r *http.Request) { utils.JSONResponse(w, 200, &EmailsCreateResponse{ Success: true, - Created: []string{utils.UUID()}, + Created: []string{"123"}, }) } diff --git a/routes/middleware.go b/routes/middleware.go index af81d3b..ca06437 100644 --- a/routes/middleware.go +++ b/routes/middleware.go @@ -4,10 +4,10 @@ import ( "net/http" "strings" + "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" - "github.com/lavab/api/db" - "github.com/lavab/api/dbutils" + "github.com/lavab/api/env" "github.com/lavab/api/utils" ) @@ -38,9 +38,13 @@ func AuthMiddleware(c *web.C, h http.Handler) http.Handler { return } - // Get the session from the database - session, ok := dbutils.GetSession(headerParts[1]) - if !ok { + // Get the token from the database + token, err := env.G.R.Tokens.GetToken(headerParts[1]) + if err != nil { + env.G.Log.WithFields(logrus.Fields{ + "error": err, + }).Error("Cannot retrieve session from the database") + utils.JSONResponse(w, 401, &AuthMiddlewareResponse{ Success: false, Message: "Invalid authorization token", @@ -49,17 +53,17 @@ func AuthMiddleware(c *web.C, h http.Handler) http.Handler { } // Check if it's expired - if session.Expired() { + if token.Expired() { utils.JSONResponse(w, 419, &AuthMiddlewareResponse{ Success: false, Message: "Authorization token has expired", }) - db.Delete("sessions", session.ID) + env.G.R.Tokens.DeleteID(token.ID) return } // Continue to the next middleware/route - c.Env["session"] = session + c.Env["session"] = token h.ServeHTTP(w, r) }) } diff --git a/routes/setup.go b/routes/setup.go deleted file mode 100644 index 8cda732..0000000 --- a/routes/setup.go +++ /dev/null @@ -1,8 +0,0 @@ -package routes - -import ( - "github.com/lavab/api/dbutils" -) - -var users = dbutils.Users -var sessions = dbutils.Sessions diff --git a/routes/tokens.go b/routes/tokens.go index 20fc206..902eee9 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -7,8 +7,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" - "github.com/lavab/api/db" - "github.com/lavab/api/dbutils" "github.com/lavab/api/env" "github.com/lavab/api/models" "github.com/lavab/api/utils" @@ -66,8 +64,8 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { } // Authenticate the user - user, ok := dbutils.FindAccountByUsername(input.Username) - if !ok || user == nil || !utils.BcryptVerify(user.Password, input.Password) { + user, err := env.G.R.Accounts.FindAccountByName(input.Username) + if err != nil || !utils.BcryptVerify(user.Password, input.Password) { utils.JSONResponse(w, 403, &TokensCreateResponse{ Success: false, Message: "Wrong username or password", @@ -85,7 +83,7 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { } // Insert int into the database - db.Insert("sessions", token) + env.G.R.Tokens.Insert(token) // Respond with the freshly created token utils.JSONResponse(w, 201, &TokensCreateResponse{ @@ -107,7 +105,7 @@ func TokensDelete(c *web.C, w http.ResponseWriter, r *http.Request) { session := c.Env["session"].(*models.Token) // Delete it from the database - if err := db.Delete("sessions", session.ID); err != nil { + if err := env.G.R.Tokens.DeleteID(session.ID); err != nil { env.G.Log.WithFields(logrus.Fields{ "error": err, }).Error("Unable to delete a session") diff --git a/utils/misc.go b/utils/misc.go index 1d0cace..704c5f3 100644 --- a/utils/misc.go +++ b/utils/misc.go @@ -1,23 +1,9 @@ package utils import ( - "crypto/rand" - "log" "os" - - "github.com/dchest/uniuri" ) -// RandomString returns a secure random string of a certain length -func RandomString(length int) string { - tmp := make([]byte, length) - _, err := rand.Read(tmp) - if err != nil { - log.Fatalln("Secure random string generation has failed.", err) - } - return string(tmp) -} - // FileExists is a stupid little wrapper of os.Stat that checks whether a file exists func FileExists(name string) bool { if _, err := os.Stat(name); os.IsNotExist(err) { @@ -25,8 +11,3 @@ func FileExists(name string) bool { } return true } - -// UUID returns a new Universally Unique IDentifier (UUID) -func UUID() string { - return uniuri.NewLen(uniuri.UUIDLen) -}