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/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/env/config.go b/env/config.go new file mode 100644 index 0000000..e0d4bdd --- /dev/null +++ b/env/config.go @@ -0,0 +1,8 @@ +package env + +type Config struct { + BindAddress string + APIVersion string + LogFormatterType string + SessionDuration int +} 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 e6aaf85..d767746 100644 --- a/main.go +++ b/main.go @@ -1,96 +1,165 @@ package main import ( - "fmt" - "log" + "net" "net/http" - "os" - "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/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" // 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\"") + sessionDuration = flag.Int("session_duration", 72, "Session duration expressed in hours") ) -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 automatically responds to OPTIONS requests + mux.Use(middleware.RequestID) + mux.Use(glogrus.NewGlogrus(log, "api")) + mux.Use(middleware.Recoverer) + mux.Use(middleware.AutomaticOptions) + + // Set up an auth'd mux + auth := web.New() + mux.Use(routes.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-data", routes.AccountsWipeData) + auth.Get("/accounts/:id/sessions", routes.AccountsSessionsList) + + // Tokens + auth.Get("/tokens", routes.TokensGet) + auth.Post("/tokens", routes.TokensCreate) + auth.Delete("/tokens", 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() + + // Make the mux handle every request + http.Handle("/", mux) + + // Set up a new environment object + env.G = &env.Environment{ + Log: log, + Config: &env.Config{ + BindAddress: *bindAddress, + APIVersion: *apiVersion, + LogFormatterType: *logFormatterType, + SessionDuration: *sessionDuration, + }, } - for _, rt := range authRoutes { - r.HandleFunc(rt.Path, AuthWrapper(rt.HandleFunc)).Methods(rt.Method) + // 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") + }) + + // 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") } - srv := &graceful.Server{ - Timeout: 10 * time.Second, - Server: &http.Server{ - Addr: config.PortString, - Handler: r, - }, + // 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) } - if config.TlsAvailable { - log.Fatal(srv.ListenAndServeTLS(cTlsFilePub, cTlsFilePriv)) - } else { - log.Fatal(srv.ListenAndServe()) - } + // If code reaches this place, it means that it was forcefully closed. + + // Wait until open connections close. + graceful.Wait() } 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.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/accounts.go b/routes/accounts.go new file mode 100644 index 0000000..29aa83d --- /dev/null +++ b/routes/accounts.go @@ -0,0 +1,231 @@ +package routes + +import ( + "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/models/base" + "github.com/lavab/api/utils" +) + +// AccountsListResponse contains the result of the AccountsList request. +type AccountsListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// AccountsList returns a list of accounts visible to an user +func AccountsList(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &AccountsListResponse{ + Success: false, + Message: "Method not implemented", + }) +} + +// 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 + err := utils.ParseRequest(r, input) + if err != nil { + env.G.Log.WithFields(logrus.Fields{ + "error": err, + }).Warn("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(input.Username); ok { + utils.JSONResponse(w, 409, &AccountsCreateResponse{ + Success: false, + Message: "Username already exists", + }) + return + } + + // Try to hash the password + hash, err := utils.BcryptHash(input.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(), input.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, + }) +} + +// 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"] + if !ok { + utils.JSONResponse(w, 409, &AccountsGetResponse{ + Success: false, + Message: "Invalid user ID", + }) + return + } + + // Right now we only support "me" as the ID + if id != "me" { + utils.JSONResponse(w, 501, &AccountsGetResponse{ + Success: false, + Message: `Only the "me" user is implemented`, + }) + 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 { + // The session refers to a non-existing user + env.G.Log.WithFields(logrus.Fields{ + "id": session.ID, + }).Warn("Valid session referred to a removed account") + + // Try to remove the orphaned session + if err := db.Delete("sessions", session.ID); err != nil { + 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.JSONResponse(w, 410, &AccountsGetResponse{ + Success: false, + Message: "Account disabled", + }) + return + } + + // Return the user struct + utils.JSONResponse(w, 200, &AccountsGetResponse{ + Success: true, + User: user, + }) +} + +// 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(w, 501, &AccountsUpdateResponse{ + Success: false, + Message: `Sorry, not implemented yet`, + }) +} + +// 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(w, 501, &AccountsDeleteResponse{ + Success: false, + Message: `Sorry, not implemented yet`, + }) +} + +// 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(w, 501, &AccountsWipeDataResponse{ + Success: false, + Message: `Sorry, not implemented yet`, + }) +} + +// 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(w, 501, &AccountsSessionsListResponse{ + Success: false, + Message: `Sorry, not implemented yet`, + }) +} 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/contacts.go b/routes/contacts.go index b9b8da2..d0effbf 100644 --- a/routes/contacts.go +++ b/routes/contacts.go @@ -1,38 +1,77 @@ package routes import ( - "fmt" "net/http" "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..44c3305 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -1,52 +1,83 @@ package routes import ( - "fmt" "net/http" "github.com/lavab/api/models" "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, + ItemsCount: 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 new file mode 100644 index 0000000..7594175 --- /dev/null +++ b/routes/hello.go @@ -0,0 +1,24 @@ +package routes + +import ( + "net/http" + + "github.com/lavab/api/env" + "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", + DocsURL: "http://lavaboom.readme.io/", + Version: env.G.Config.APIVersion, + }) +} diff --git a/routes/keys.go b/routes/keys.go index 5106401..e22732a 100644 --- a/routes/keys.go +++ b/routes/keys.go @@ -1,26 +1,49 @@ package routes import ( - "fmt" "net/http" + + "github.com/lavab/api/utils" ) -// 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"` +} + +// KeysCreate does *something* - TODO +func KeysCreate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &KeysCreateResponse{ + 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"` } -// SubmitKey TODO -func SubmitKey(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", + }) } -// Key TODO -func Key(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"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"` } -// VoteKey TODO -func VoteKey(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// 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..45d0e97 100644 --- a/routes/labels.go +++ b/routes/labels.go @@ -1,31 +1,77 @@ package routes import ( - "fmt" "net/http" + + "github.com/lavab/api/utils" ) -// 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"` +} + +// LabelsList does *something* - TODO +func LabelsList(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &LabelsListResponse{ + 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"` +} + +// LabelsCreate does *something* - TODO +func LabelsCreate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &LabelsCreateResponse{ + 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", + }) } -// CreateLabel TODO -func CreateLabel(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"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"` } -// Label TODO -func Label(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// LabelsUpdate does *something* - TODO +func LabelsUpdate(w http.ResponseWriter, r *http.Request) { + utils.JSONResponse(w, 501, &LabelsUpdateResponse{ + Success: false, + Message: "Sorry, not implemented yet", + }) } -// UpdateLabel TODO -func UpdateLabel(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"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"` } -// Label TODO -func DeleteLabel(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") +// 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/me.go b/routes/me.go deleted file mode 100644 index b25ea6c..0000000 --- a/routes/me.go +++ /dev/null @@ -1,46 +0,0 @@ -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" -) - -// 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) - if !ok { - debug := fmt.Sprintf("Session %s was deleted", session.ID) - 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) - } - 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) - return - } - fmt.Fprint(w, string(str)) -} - -// UpdateMe TODO -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 -func Sessions(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"success\":false,\"message\":\"Sorry, not implemented yet\"}") -} 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/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/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 new file mode 100644 index 0000000..fe3b0e5 --- /dev/null +++ b/routes/tokens.go @@ -0,0 +1,126 @@ +package routes + +import ( + "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/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"` +} + +// 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) + + // Respond with the token information + utils.JSONResponse(w, 200, &TokensGetResponse{ + Success: true, + Created: session.DateCreated, + Expires: session.ExpirationDate, + }) +} + +// 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 + err := utils.ParseRequest(r, input) + if err != nil { + env.G.Log.WithFields(logrus.Fields{ + "error": err, + }).Warn("Unable to decode a request") + + utils.JSONResponse(w, 409, &TokensCreateResponse{ + Success: false, + Message: "Invalid input format", + }) + return + } + + // Authenticate the user + 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", + }) + 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, "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, + }) +} + +// 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) + + // 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 52bdedd..0000000 --- a/utils/json.go +++ /dev/null @@ -1,28 +0,0 @@ -package utils - -import ( - "encoding/json" - "io" - "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) - out := map[string]interface{}{} - err := decoder.Decode(&out) - if err != nil { - return out, err - } - return out, nil -} diff --git a/utils/requests.go b/utils/requests.go new file mode 100644 index 0000000..83fe3d9 --- /dev/null +++ b/utils/requests.go @@ -0,0 +1,86 @@ +package utils + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "strings" + + "github.com/Sirupsen/logrus" + "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 31c1c15..0000000 --- a/utils/responses.go +++ /dev/null @@ -1,34 +0,0 @@ -package utils - -import ( - "fmt" - "net/http" -) - -// JSONResponse writes JSON to an http.ResponseWriter with the corresponding status code -func JSONResponse(w http.ResponseWriter, status int, data map[string]interface{}) { - if status < 100 || status > 599 { - status = 200 - } - w.WriteHeader(status) - fmt.Fprint(w, MakeJSON(data)) -} - -// 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") - } - fmt.Fprint(w, MakeJSON(out)) -} - -// FormatNotRecognizedResponse TODO -func FormatNotRecognizedResponse(w http.ResponseWriter, err error) { - ErrorResponse(w, 400, "Format not recognized", err.Error()) -}