Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ curl --data "username=abc&password=def" localhost:5000/signup
curl --data "username=abc&password=def" localhost:5000/login
curl --header "Auth: <token>" localhost:5000/me
```

## Build status:

- `master` - [![Build Status](https://magnum.travis-ci.com/lavab/api.svg?token=kJbppXeTxzqpCVvt4t5X&branch=master)](https://magnum.travis-ci.com/lavab/api)
- `develop` - [![Build Status](https://magnum.travis-ci.com/lavab/api.svg?token=kJbppXeTxzqpCVvt4t5X&branch=develop)](https://magnum.travis-ci.com/lavab/api)
2 changes: 1 addition & 1 deletion env/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package env

type Config struct {
type Flags struct {
BindAddress string
APIVersion string
LogFormatterType string
Expand Down
16 changes: 5 additions & 11 deletions env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,10 @@ import (
"github.com/lavab/api/db"
)

type Environment struct {
Log *logrus.Logger
Config *Config
Rethink *gorethink.Session
R *R
}

type R struct {
var (
Config *Flags
Log *logrus.Logger
Rethink *gorethink.Session
Accounts *db.AccountsTable
Tokens *db.TokensTable
}

var G *Environment
)
66 changes: 34 additions & 32 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

"github.com/Sirupsen/logrus"
"github.com/dancannon/gorethink"
"github.com/goji/glogrus"
"github.com/lavab/glogrus"
"github.com/namsral/flag"
"github.com/zenazn/goji/graceful"
"github.com/zenazn/goji/web"
Expand All @@ -28,6 +28,7 @@ var (
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")
forceColors = flag.Bool("force_colors", false, "Force colored prompt?")
// Database-related flags
rethinkdbURL = flag.String("rethinkdb_url", func() string {
address := os.Getenv("RETHINKDB_PORT_28015_TCP_ADDR")
Expand All @@ -50,16 +51,29 @@ func main() {
// Parse the flags
flag.Parse()

// Put config into the environment package
env.Config = &env.Flags{
BindAddress: *bindAddress,
APIVersion: *apiVersion,
LogFormatterType: *logFormatterType,
SessionDuration: *sessionDuration,
}

// Set up a new logger
log := logrus.New()

// Set the formatter depending on the passed flag's value
if *logFormatterType == "text" {
log.Formatter = &logrus.TextFormatter{}
log.Formatter = &logrus.TextFormatter{
ForceColors: *forceColors,
}
} else if *logFormatterType == "json" {
log.Formatter = &logrus.JSONFormatter{}
}

// Pass it to the environment package
env.Log = log

// Set up the database
rethinkOpts := gorethink.ConnectOpts{
Address: *rethinkdbURL,
Expand All @@ -83,22 +97,23 @@ func main() {
}).Fatal("Unable to connect to the database")
}

// Put the RethinkDB session into the environment package
env.Rethink = rethinkSession

// 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",
),
},
env.Accounts = &db.AccountsTable{
RethinkCRUD: db.NewCRUDTable(
rethinkSession,
rethinkOpts.Database,
"accounts",
),
}
env.Tokens = &db.TokensTable{
RethinkCRUD: db.NewCRUDTable(
rethinkSession,
rethinkOpts.Database,
"tokens",
),
}

// Create a new goji mux
Expand All @@ -116,7 +131,7 @@ func main() {

// Set up an auth'd mux
auth := web.New()
mux.Use(routes.AuthMiddleware)
auth.Use(routes.AuthMiddleware)

// Index route
mux.Get("/", routes.Hello)
Expand Down Expand Up @@ -167,27 +182,14 @@ func main() {
auth.Post("/keys/:id/vote", routes.KeysVote)

// Merge the muxes
mux.Handle("/", auth)
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,
},
Rethink: rethinkSession,
R: tables,
}

// Log that we're starting the server
log.WithFields(logrus.Fields{
"address": *bindAddress,
Expand Down
46 changes: 46 additions & 0 deletions models/account.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package models

import (
"github.com/gyepisam/mcf"
_ "github.com/gyepisam/mcf/scrypt"
)

// Account stores essential data for a Lavaboom user, and is thus not encrypted.
type Account struct {
Resource
Expand Down Expand Up @@ -34,6 +39,47 @@ type Account struct {
Type string `json:"type" gorethink:"type"`
}

// SetPassword changes the account's password
func (a *Account) SetPassword(password string) error {
encrypted, err := mcf.Create(password)
if err != nil {
return err
}

a.Password = encrypted
return nil
}

// VerifyPassword checks if password is valid and upgrades it if its encrypting scheme was outdated
// Returns isValid, wasUpdated, error
func (a *Account) VerifyPassword(password string) (bool, bool, error) {
isValid, err := mcf.Verify(password, a.Password)
if err != nil {
return false, false, err
}

if !isValid {
return false, false, nil
}

isCurrent, err := mcf.IsCurrent(a.Password)
if err != nil {
return false, false, err
}

if !isCurrent {
err := a.SetPassword(password)
if err != nil {
return true, false, err
}

a.Touch()
return true, true, nil
}

return true, false, nil
}

// SettingsData TODO
type SettingsData struct {
}
Expand Down
42 changes: 20 additions & 22 deletions routes/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) {
var input AccountsCreateRequest
err := utils.ParseRequest(r, input)
if err != nil {
env.G.Log.WithFields(logrus.Fields{
env.Log.WithFields(logrus.Fields{
"error": err,
}).Warn("Unable to decode a request")

Expand All @@ -56,44 +56,42 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) {
}

// Ensure that the user with requested username doesn't exist
if _, err := env.G.R.Accounts.FindAccountByName(input.Username); err != nil {
if _, err := env.Accounts.FindAccountByName(input.Username); err != nil {
utils.JSONResponse(w, 409, &AccountsCreateResponse{
Success: false,
Message: "Username already exists",
})
return
}

// Try to hash the password
hash, err := utils.BcryptHash(input.Password)
// TODO: sanitize user name (i.e. remove caps, periods)

// Create a new user object
account := &models.Account{
Resource: models.MakeResource("", input.Username),
}

err = account.SetPassword(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{
env.Log.WithFields(logrus.Fields{
"error": err,
}).Error("Unable to hash a password")
}).Error("Unable to hash the password")
return
}

// TODO: sanitize user name (i.e. remove caps, periods)

// Create a new user object
account := &models.Account{
Resource: models.MakeResource("", input.Username),
Password: string(hash),
}

// Try to save it in the database
if err := env.G.R.Accounts.Insert(account); err != nil {
if err := env.Accounts.Insert(account); err != nil {
utils.JSONResponse(w, 500, &AccountsCreateResponse{
Success: false,
Message: "Internal server error - AC/CR/02",
})

env.G.Log.WithFields(logrus.Fields{
env.Log.WithFields(logrus.Fields{
"error": err,
}).Error("Could not insert an user to the database")
return
Expand All @@ -114,7 +112,7 @@ type AccountsGetResponse struct {
}

// AccountsGet returns the information about the specified account
func AccountsGet(c *web.C, w http.ResponseWriter, r *http.Request) {
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 {
Expand All @@ -138,22 +136,22 @@ 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, err := env.G.R.Accounts.GetAccount(session.Owner)
user, err := env.Accounts.GetAccount(session.Owner)
if err != nil {
// The session refers to a non-existing user
env.G.Log.WithFields(logrus.Fields{
env.Log.WithFields(logrus.Fields{
"id": session.ID,
"error": err,
}).Warn("Valid session referred to a removed account")

// Try to remove the orphaned session
if err := env.G.R.Tokens.DeleteID(session.ID); err != nil {
env.G.Log.WithFields(logrus.Fields{
if err := env.Tokens.DeleteID(session.ID); err != nil {
env.Log.WithFields(logrus.Fields{
"id": session.ID,
"error": err,
}).Error("Unable to remove an orphaned session")
} else {
env.G.Log.WithFields(logrus.Fields{
env.Log.WithFields(logrus.Fields{
"id": session.ID,
}).Info("Removed an orphaned session")
}
Expand Down
2 changes: 1 addition & 1 deletion routes/hello.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ 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,
Version: env.Config.APIVersion,
})
}
6 changes: 3 additions & 3 deletions routes/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ func AuthMiddleware(c *web.C, h http.Handler) http.Handler {
}

// Get the token from the database
token, err := env.G.R.Tokens.GetToken(headerParts[1])
token, err := env.Tokens.GetToken(headerParts[1])
if err != nil {
env.G.Log.WithFields(logrus.Fields{
env.Log.WithFields(logrus.Fields{
"error": err,
}).Error("Cannot retrieve session from the database")

Expand All @@ -58,7 +58,7 @@ func AuthMiddleware(c *web.C, h http.Handler) http.Handler {
Success: false,
Message: "Authorization token has expired",
})
env.G.R.Tokens.DeleteID(token.ID)
env.Tokens.DeleteID(token.ID)
return
}

Expand Down
Loading