Skip to content

Instantly share code, notes, and snippets.

@vastbinderj
Created April 9, 2014 15:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vastbinderj/10281831 to your computer and use it in GitHub Desktop.
Save vastbinderj/10281831 to your computer and use it in GitHub Desktop.
ottemo-create-user-func
package main
import (
"fmt"
"github.com/codegangsta/martini"
"github.com/martini-contrib/binding"
"github.com/martini-contrib/render"
"github.com/martini-contrib/secure"
"github.com/martini-contrib/sessions"
"github.com/ottemo/ottemo-go/auth"
"github.com/ottemo/ottemo-go/config"
"github.com/ottemo/ottemo-go/product"
"github.com/ottemo/ottemo-go/visitor"
"labix.org/v2/mgo"
"labix.org/v2/mgo/bson"
"log"
"net/http"
)
var conf *config.Config
// init - initialization and configuration
func init() {
// read in config file
filename, err := config.DiscoverConfig("config.json", "local.json")
if err != nil {
log.Fatal("No configuration file found")
}
conf, err = config.NewConfig(filename)
if err != nil {
log.Fatalf("Error parsing config.json: %s", err.Error())
}
}
func main() {
m := App()
listen := fmt.Sprintf("%s:%d", conf.Host, conf.Port)
fmt.Printf("Ottemo listening on %s\n", listen)
log.Fatal(http.ListenAndServe(listen, m))
}
// App returns a new martini app.
func App() *martini.ClassicMartini {
m := martini.Classic()
// TODO: Cookie secret needs to be random.
store := sessions.NewCookieStore([]byte("secret"))
m.Use(sessions.Sessions("ottemo", store))
// Set security values in production.
if martini.Env == martini.Prod {
m.Use(secure.Secure(secure.Options{
SSLRedirect: true,
}))
}
// Initialize Middleware.
m.Use(Db(conf.DbURL, conf.DbName))
m.Use(render.Renderer(render.Options{Extensions: []string{".html"}}))
m.Use(visitor.SessionVisitor)
// Application Routes.
m.Get("/", func(r render.Render) {
r.HTML(http.StatusOK, "index", nil)
})
m.Get("/login", func(r render.Render) {
r.HTML(http.StatusOK, "login", nil)
})
m.Post("/login", binding.Bind(auth.LocalLoginRequest{}), auth.LocalLoginHandler)
m.Get("/register", func(r render.Render) {
r.HTML(http.StatusOK, "register", nil)
})
m.Post("/register", binding.Bind(visitor.RegisterLocalRequest{}), visitor.RegisterLocalHandler)
// API Routes
// TODO better error handling when resources not found
m.Get("/api/v1/visitor", func(r render.Render) {
// r.JSON(200, visitor.GetVisitors)
})
m.Get("/api/v1/visitor/:id", func(r render.Render, db *mgo.Database, params martini.Params) {
v, err := visitor.GetVisitorByID(db, bson.ObjectIdHex(params["id"]))
if err != nil {
r.Error(500)
}
r.JSON(200, v)
})
// Create a visitor
m.Post("/api/v1/visitor", binding.Bind(visitor.RegisterLocalRequest{}), visitor.CreateLocalVisitor)
m.Get("/api/v1/visitor/email/:id", func(r render.Render, db *mgo.Database, params martini.Params) {
v, err := visitor.GetVisitorByEmail(db, params["id"])
if err != nil {
r.Error(500)
}
r.JSON(200, v)
})
// Admin Tool Routes
m.Get("/admintool", func(r render.Render) {
r.HTML(http.StatusOK, "admintool/index", nil)
})
m.Get("/admintool/product", func(r render.Render) {
r.HTML(http.StatusOK, "admintool/catalog/product/index", nil)
})
m.Get("/admintool/product/new", func(r render.Render) {
r.HTML(http.StatusOK, "admintool/catalog/product/index", nil)
})
m.Post("/admintool/product/new", binding.Bind(product.ProductRequest{}), product.NewProductHandler)
m.Get("/admintool/product/update", func(r render.Render) {
r.HTML(http.StatusOK, "admintool/catalog/product/index", nil)
})
// m.Post("/admintool/product/update", binding.Bind(product.ProductRequest{}), product.UpdateProductHandler)
return m
}
// Db maps a MongoDb instance to the current request context.
func Db(url, name string) martini.Handler {
s, err := mgo.Dial(url)
if err != nil {
log.Fatal(err)
}
return func(c martini.Context) {
clone := s.Clone()
c.Map(clone.DB(name))
defer clone.Close()
c.Next()
}
}
/*Package visitor contains data types and supporting functions
for visitory creation, login, etc.
*/
package visitor
import (
"code.google.com/p/go-uuid/uuid"
"code.google.com/p/go.crypto/bcrypt"
"github.com/codegangsta/martini"
"github.com/martini-contrib/render"
"github.com/martini-contrib/sessions"
"labix.org/v2/mgo"
"labix.org/v2/mgo/bson"
"net/http"
)
const (
// COLLECTION is the name of the MongoDB collection
COLLECTION = "visitors"
// VISITOR is the string representing a visitor role.
VISITOR = "visitor"
// MEMBER is the string representing a visitor role.
MEMBER = "member"
// ADMIN is the string representing a visitor role.
ADMIN = "admin"
// RETIRED is the string representing a visitor role.
RETIRED = "retired"
)
var roles = []string{VISITOR, MEMBER, ADMIN, RETIRED}
// Visitor is a user of the site.
type Visitor struct {
ID bson.ObjectId `bson:"_id" json:"id"`
Email string `bson:"email" json:"email"`
Hash string `bson:"hash" json:"hash"`
Fname string `bson:"fname" json:"fname"`
Lname string `bson:"lname" json:"lname"`
PhoneNumbers []PhoneNumber `bson:"phone_numbers" json:"phone_numbers"`
Roles []string `bson:"roles" json:"roles"`
Addresses []Address `bson:"addresses" json:"addresses"`
SessionToken string `bson:"session_token" json:"session_token"`
IsEnabled bool `bson:"is_enabled" json:"is_enabled"`
IsActivated bool `bson:"is_activated" json:"is_activated"`
ActivationCode string `bson:"activiation_code" json:"activation_code"`
}
// Address holds the visitors billing/shipping address. A visitory can have
// multiple addresses but should only have one primary billing and shipping.
type Address struct {
PrimaryBilling bool `bson:"primary_billing" json:"primary_billing"`
PrimaryShipping bool `bson:"primary_shipping" json:"primary_shipping"`
StreetLine1 string `bson:"street_line1" json:"street_line1"`
StreetLine2 string `bson:"street_line2" json:"street_line2"`
City string `bson:"city" json:"city"`
State string `bson:"state" json:"state"`
Country string `bson:"country" json:"country"`
}
// PhoneNumber holds contact information for the visitor. Primarily
// used for shipping and billing.
type PhoneNumber struct {
Role string `bson:"role" json:"role"`
CountryCode int `bson:"country_code" json:"country_code"`
AreaCode int `bson:"area_code" json:"area_code"`
Digits int `bson:"digits" json:"digits"`
}
// HasRole returns true if the user has the visitor role provided.
// Will return false if the role is invalid.
func (v *Visitor) HasRole(role string) bool {
if !IsValidRole(role) {
return false
}
for _, r := range roles {
if r == role {
return true
}
}
return false
}
// LocalLogin compares the provided password and returns true if once hashes it matches.
func (v *Visitor) LocalLogin(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(v.Hash), []byte(password))
return err == nil
}
// RegisterLocalRequest is used to bind form attributes when register a new user
// using local authentication.
type RegisterLocalRequest struct {
Email string `form:"email" binding:"required"`
Password string `form:"password" binding:"required"`
Fname string `form:"fname" binding:"required"`
Lname string `form:"lname" binding:"required"`
}
// RegisterLocalHandler handles a Post request to register a new member using local authentication.
func RegisterLocalHandler(db *mgo.Database, nv RegisterLocalRequest, r render.Render, s sessions.Session) {
v, err := NewVisitorFromEmail(nv.Email, nv.Password, nv.Fname, nv.Lname)
if err != nil {
r.HTML(http.StatusInternalServerError, "internal_error", nil)
return
}
// First, we must verify that this email is not registered.
_, err = GetVisitorByEmail(db, v.Email)
if err != nil {
switch err.Error() {
// If the existing email was not found, pass. If there was another error,
// consider it a 500 and stop.
case "not found":
default:
r.HTML(http.StatusInternalServerError, "internal_error", nil)
return
}
} else {
// TODO: If the user was found, send an email asking if the user
// forgot they had an existing account, but don't log them in.
r.HTML(http.StatusOK, "index", nil)
return
}
if err := InsertVisitor(db, v); err != nil {
r.HTML(http.StatusInternalServerError, "internal_error", nil)
return
}
s.Set("visitorID", v.ID.Hex())
s.Set("token", v.SessionToken)
// r.HTML(http.StatusOK, "index", nil)
r.JSON(200, v)
}
// CreateLocalVisitor handles a post request to create a new local visitor
func CreateLocalVisitor(db *mgo.Database, nv RegisterLocalRequest, r render.Render) {
v, err := NewVisitorFromEmail(nv.Email, nv.Password, nv.Fname, nv.Lname)
if err != nil {
r.HTML(http.StatusInternalServerError, "internal_error", nil)
return
}
// First, we must verify that this email is not registered.
_, err = GetVisitorByEmail(db, v.Email)
if err != nil {
switch err.Error() {
// If the existing email was not found, pass. If there was another error,
// consider it a 500 and stop.
case "not found":
default:
r.Error(500)
return
}
} else {
// TODO If the user was found, send an email asking if the user
// forgot they had an existing account, but don't log them in.
r.Error(500)
return
}
if err := InsertVisitor(db, v); err != nil {
r.HTML(http.StatusInternalServerError, "internal_error", nil)
return
}
r.JSON(200, v)
}
// SessionVisitor is a martini middleware that looks for a visitorID in a session and maps either
// an existing or null Visitor to the current context. No authentication or authorization is performed.
// TODO: This could also map the visitors cart.
// TODO: Render custom error pages rather than returning 500.
func SessionVisitor(db *mgo.Database, s sessions.Session, c martini.Context, r render.Render) {
v := &Visitor{}
vid := s.Get("visitorID")
token := s.Get("token")
if vid != nil && token != nil {
// vid should never not be a string. So this is ok.
switch vid.(type) {
case string:
default:
r.HTML(http.StatusInternalServerError, "internal_error", nil)
return
}
if !bson.IsObjectIdHex(vid.(string)) {
r.HTML(http.StatusInternalServerError, "internal_error", nil)
return
}
switch token.(type) {
case string:
default:
r.HTML(http.StatusInternalServerError, "internal_error", nil)
return
}
var err error
v, err = GetVisitorByID(db, bson.ObjectIdHex(vid.(string)))
if err != nil {
switch err.Error() {
// Visitor was not found. So it could've been deleted.
// Clear the session.
case "not found":
s.Clear()
v = &Visitor{}
default:
s.Clear()
r.HTML(http.StatusInternalServerError, "internal_error", nil)
return
}
} else {
if token.(string) != v.SessionToken {
s.Clear()
v = &Visitor{}
}
}
}
c.Map(v)
}
// GetVisitorByID returns a pointer to a visitor with the provided id
// from the provided database.
func GetVisitorByID(db *mgo.Database, id bson.ObjectId) (*Visitor, error) {
v := &Visitor{}
err := db.C(COLLECTION).FindId(id).One(&v)
return v, err
}
// GetVisitorByEmail returns a pointer to a visitor with the provided email
// from the provided database.
func GetVisitorByEmail(db *mgo.Database, email string) (*Visitor, error) {
v := &Visitor{}
err := db.C(COLLECTION).Find(bson.M{"email": email}).One(&v)
return v, err
}
// NewVisitorFromEmail initializes a pointer to a new Visitor with the role of
// visitor.
func NewVisitorFromEmail(email, password, fname, lname string) (*Visitor, error) {
v := &Visitor{Email: email, Fname: fname, Lname: lname}
hash, err := hashPassword(password)
if err != nil {
return v, err
}
v.Hash = hash
v.Roles = append(v.Roles, VISITOR)
v.SessionToken = bson.Now().String()
v.ID = bson.NewObjectId()
v.IsEnabled = true
v.ActivationCode = uuid.New()
return v, nil
}
// InsertVisitor inserts a Visitor into the Mongodb instance.
func InsertVisitor(db *mgo.Database, v *Visitor) error {
return db.C(COLLECTION).Insert(v)
}
func hashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
return "", err
}
return string(hash), nil
}
// IsValidRole returns true if the provided role is in roles.
func IsValidRole(role string) bool {
for _, r := range roles {
if role == r {
return true
}
}
return false
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment