diff --git a/README.md b/README.md index b6edbe4..2cffa35 100644 --- a/README.md +++ b/README.md @@ -3,31 +3,39 @@ Basic MVC Web Application in Golang This project demonstrates how to structure and build a website using the Go language without a framework. -## Quick Start with MySQL - To download, run the following command: ~~~ go get github.com/josephspurrier/gowebapp ~~~ -Start MySQL and import config/mysql.sql to create the database and tables. +## Quick Start with Bolt + +The gowebapp.db file will be created once you start the application. + +Build and run from the root directory. Open your web browser to: http://localhost. You should see the welcome page. + +Navigate to the login page, and then to the register page. Create a new user and you should be able to login. That's it. -Open config/config.json and edit the Database section so the connection information matches your MySQL instance. +## Quick Start with MongoDB + +Start MongoDB. + +Open config/config.json and edit the Database section so the connection information matches your MongoDB instance. Also, change Type from Bolt to MongoDB. Build and run from the root directory. Open your web browser to: http://localhost. You should see the welcome page. Navigate to the login page, and then to the register page. Create a new user and you should be able to login. That's it. -## Switching to MongoDB +## Quick Start with MySQL + +Start MySQL and import config/mysql.sql to create the database and tables. -A few people have asked for MongoDB so I made a few code modifications here: -https://gist.github.com/josephspurrier/7742f8e863ee46dd12ba +Open config/config.json and edit the Database section so the connection information matches your MySQL instance. Also, change Type from Bolt to MySQL. -And a few things to keep in mind: +Build and run from the root directory. Open your web browser to: http://localhost. You should see the welcome page. -* in login.go, use this to check for no results found: if err == model.ErrNoResult -* And to get the ID, use the User.Id() func +Navigate to the login page, and then to the register page. Create a new user and you should be able to login. That's it. ## Overview @@ -274,7 +282,9 @@ if !recaptcha.Verified(r) { It's a good idea to abstract the database layer out so if you need to make changes, you don't have to look through business logic to find the queries. All -the queries are stored in the models folder: +the queries are stored in the models folder. + +The user.go file at the root of the model directory is a compliation of all the queries for each database type. Connect to the database (only once needed in your application): diff --git a/config/config.json b/config/config.json index 884d250..3272d4e 100644 --- a/config/config.json +++ b/config/config.json @@ -1,6 +1,13 @@ { "Database": { - "Type": "MySQL", + "Type": "Bolt", + "Bolt": { + "Path": "gowebapp.db" + }, + "MongoDB": { + "URL": "127.0.0.1", + "Database": "gowebapp" + }, "MySQL": { "Username": "root", "Password": "", diff --git a/controller/login.go b/controller/login.go index afb2e6b..cb0a739 100644 --- a/controller/login.go +++ b/controller/login.go @@ -1,7 +1,6 @@ package controller import ( - "database/sql" "fmt" "log" "net/http" @@ -72,7 +71,7 @@ func LoginPOST(w http.ResponseWriter, r *http.Request) { result, err := model.UserByEmail(email) // Determine if user exists - if err == sql.ErrNoRows { + if err == model.ErrNoResult { loginAttempt(sess) sess.AddFlash(view.Flash{"Password is incorrect - Attempt: " + fmt.Sprintf("%v", sess.Values[sessLoginAttempt]), view.FlashWarning}) sess.Save(r, w) @@ -90,7 +89,7 @@ func LoginPOST(w http.ResponseWriter, r *http.Request) { // Login successfully session.Empty(sess) sess.AddFlash(view.Flash{"Login successful!", view.FlashSuccess}) - sess.Values["id"] = result.Id + sess.Values["id"] = result.ID() sess.Values["email"] = email sess.Values["first_name"] = result.First_name sess.Save(r, w) diff --git a/controller/register.go b/controller/register.go index b9304a4..482cf08 100644 --- a/controller/register.go +++ b/controller/register.go @@ -1,7 +1,6 @@ package controller import ( - "database/sql" "log" "net/http" @@ -70,9 +69,9 @@ func RegisterPOST(w http.ResponseWriter, r *http.Request) { } // Get database result - _, err := model.UserIdByEmail(email) + _, err := model.UserByEmail(email) - if err == sql.ErrNoRows { // If success (no user exists with that email) + if err == model.ErrNoResult { // If success (no user exists with that email) ex := model.UserCreate(first_name, last_name, email, password) // Will only error if there is a problem with the query if ex != nil { diff --git a/gowebapp.go b/gowebapp.go index 7526e44..9a173c7 100644 --- a/gowebapp.go +++ b/gowebapp.go @@ -63,7 +63,7 @@ var config = &configuration{} // configuration contains the application settings type configuration struct { - Database database.Databases `json:"Database"` + Database database.DatabaseInfo `json:"Database"` Email email.SMTPInfo `json:"Email"` Recaptcha recaptcha.RecaptchaInfo `json:"Recaptcha"` Server server.Server `json:"Server"` diff --git a/model/bolt/user.go b/model/bolt/user.go new file mode 100644 index 0000000..579c8da --- /dev/null +++ b/model/bolt/user.go @@ -0,0 +1,80 @@ +package model + +import ( + "errors" + "time" + + "github.com/josephspurrier/gowebapp/shared/database" + + "gopkg.in/mgo.v2/bson" +) + +// ***************************************************************************** +// User +// ***************************************************************************** + +// User table contains the information for each user +type User struct { + ObjectId bson.ObjectId `bson:"_id"` + First_name string `db:"first_name" bson:"first_name"` + Last_name string `db:"last_name" bson:"last_name"` + Email string `db:"email" bson:"email"` + Password string `db:"password" bson:"password"` + Status_id uint8 `db:"status_id" bson:"status_id"` + Created_at time.Time `db:"created_at" bson:"created_at"` + Updated_at time.Time `db:"updated_at" bson:"updated_at"` + Deleted uint8 `db:"deleted" bson:"deleted"` +} + +var ( + ErrCode = errors.New("Case statement in code is not correct.") + ErrNoResult = errors.New("Result not found.") + ErrUnavailable = errors.New("Database is unavailable.") +) + +// Id returns the user id +func (u *User) ID() string { + return u.ObjectId.Hex() +} + +// standardizeErrors returns the same error regardless of the database used +func standardizeError(err error) error { + return err +} + +// UserByEmail gets user information from email +func UserByEmail(email string) (User, error) { + var err error + + result := User{} + + err = database.View("user", email, &result) + if err != nil { + err = ErrNoResult + } + + return result, standardizeError(err) +} + +// UserCreate creates user +func UserCreate(first_name, last_name, email, password string) error { + var err error + + now := time.Now() + + user := &User{ + ObjectId: bson.NewObjectId(), + First_name: first_name, + Last_name: last_name, + Email: email, + Password: password, + Status_id: 1, + Created_at: now, + Updated_at: now, + Deleted: 0, + } + + err = database.Update("user", user.Email, &user) + + return standardizeError(err) +} diff --git a/model/mongo/user.go b/model/mongo/user.go new file mode 100644 index 0000000..0afb56f --- /dev/null +++ b/model/mongo/user.go @@ -0,0 +1,96 @@ +package model + +import ( + "errors" + "time" + + "github.com/josephspurrier/gowebapp/shared/database" + + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// ***************************************************************************** +// User +// ***************************************************************************** + +// User table contains the information for each user +type User struct { + ObjectId bson.ObjectId `bson:"_id"` + First_name string `db:"first_name" bson:"first_name"` + Last_name string `db:"last_name" bson:"last_name"` + Email string `db:"email" bson:"email"` + Password string `db:"password" bson:"password"` + Status_id uint8 `db:"status_id" bson:"status_id"` + Created_at time.Time `db:"created_at" bson:"created_at"` + Updated_at time.Time `db:"updated_at" bson:"updated_at"` + Deleted uint8 `db:"deleted" bson:"deleted"` +} + +var ( + ErrCode = errors.New("Case statement in code is not correct.") + ErrNoResult = errors.New("Result not found.") + ErrUnavailable = errors.New("Database is unavailable.") +) + +// Id returns the user id +func (u *User) ID() string { + return u.ObjectId.Hex() +} + +// standardizeErrors returns the same error regardless of the database used +func standardizeError(err error) error { + if err == mgo.ErrNotFound { + return ErrNoResult + } + + return err +} + +// UserByEmail gets user information from email +func UserByEmail(email string) (User, error) { + var err error + + result := User{} + + if database.CheckConnection() { + session := database.Mongo.Copy() + defer session.Close() + c := session.DB(database.ReadConfig().MongoDB.Database).C("user") + err = c.Find(bson.M{"email": email}).One(&result) + } else { + err = ErrUnavailable + } + + return result, standardizeError(err) +} + +// UserCreate creates user +func UserCreate(first_name, last_name, email, password string) error { + var err error + + now := time.Now() + + if database.CheckConnection() { + session := database.Mongo.Copy() + defer session.Close() + c := session.DB(database.ReadConfig().MongoDB.Database).C("user") + + user := &User{ + ObjectId: bson.NewObjectId(), + First_name: first_name, + Last_name: last_name, + Email: email, + Password: password, + Status_id: 1, + Created_at: now, + Updated_at: now, + Deleted: 0, + } + err = c.Insert(user) + } else { + err = ErrUnavailable + } + + return standardizeError(err) +} diff --git a/model/sql/user.go b/model/sql/user.go new file mode 100644 index 0000000..366ae8b --- /dev/null +++ b/model/sql/user.go @@ -0,0 +1,76 @@ +package model + +import ( + "database/sql" + "errors" + "fmt" + "time" + + "github.com/josephspurrier/gowebapp/shared/database" +) + +// ***************************************************************************** +// User +// ***************************************************************************** + +// User table contains the information for each user +type User struct { + Id uint32 `db:"id"` + First_name string `db:"first_name"` + Last_name string `db:"last_name"` + Email string `db:"email"` + Password string `db:"password"` + Status_id uint8 `db:"status_id"` + Created_at time.Time `db:"created_at"` + Updated_at time.Time `db:"updated_at"` + Deleted uint8 `db:"deleted"` +} + +// User_status table contains every possible user status (active/inactive) +type User_status struct { + Id uint8 `db:"id"` + Status string `db:"status"` + Created_at time.Time `db:"created_at"` + Updated_at time.Time `db:"updated_at"` + Deleted uint8 `db:"deleted"` +} + +var ( + ErrCode = errors.New("Case statement in code is not correct.") + ErrNoResult = errors.New("Result not found.") + ErrUnavailable = errors.New("Database is unavailable.") +) + +// Id returns the user id +func (u *User) ID() string { + return fmt.Sprintf("%v", u.Id) +} + +// standardizeErrors returns the same error regardless of the database used +func standardizeError(err error) error { + if err == sql.ErrNoRows { + return ErrNoResult + } + + return err +} + +// UserByEmail gets user information from email +func UserByEmail(email string) (User, error) { + result := User{} + err := database.Sql.Get(&result, "SELECT id, password, status_id, first_name FROM user WHERE email = ? LIMIT 1", email) + return result, err +} + +// UserIdByEmail gets user id from email +func UserIdByEmail(email string) (User, error) { + result := User{} + err := database.Sql.Get(&result, "SELECT id FROM user WHERE email = ? LIMIT 1", email) + return result, err +} + +// UserCreate creates user +func UserCreate(first_name, last_name, email, password string) error { + _, err := database.Sql.Exec("INSERT INTO user (first_name, last_name, email, password) VALUES (?,?,?,?)", first_name, last_name, email, password) + return err +} diff --git a/model/user.go b/model/user.go index fab14e0..d5e4f52 100644 --- a/model/user.go +++ b/model/user.go @@ -1,9 +1,15 @@ package model import ( + "database/sql" + "errors" + "fmt" "time" "github.com/josephspurrier/gowebapp/shared/database" + + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" ) // ***************************************************************************** @@ -12,42 +18,136 @@ import ( // User table contains the information for each user type User struct { - Id uint32 `db:"id"` - First_name string `db:"first_name"` - Last_name string `db:"last_name"` - Email string `db:"email"` - Password string `db:"password"` - Status_id uint8 `db:"status_id"` - Created_at time.Time `db:"created_at"` - Updated_at time.Time `db:"updated_at"` - Deleted uint8 `db:"deleted"` + ObjectId bson.ObjectId `bson:"_id"` + Id uint32 `db:"id" bson:"id,omitempty"` // Don't use Id, use ID() instead for consistency with MongoDB + First_name string `db:"first_name" bson:"first_name"` + Last_name string `db:"last_name" bson:"last_name"` + Email string `db:"email" bson:"email"` + Password string `db:"password" bson:"password"` + Status_id uint8 `db:"status_id" bson:"status_id"` + Created_at time.Time `db:"created_at" bson:"created_at"` + Updated_at time.Time `db:"updated_at" bson:"updated_at"` + Deleted uint8 `db:"deleted" bson:"deleted"` } // User_status table contains every possible user status (active/inactive) type User_status struct { - Id uint8 `db:"id"` - Status string `db:"status"` - Created_at time.Time `db:"created_at"` - Updated_at time.Time `db:"updated_at"` - Deleted uint8 `db:"deleted"` + Id uint8 `db:"id" bson:"id"` + Status string `db:"status" bson:"status"` + Created_at time.Time `db:"created_at" bson:"created_at"` + Updated_at time.Time `db:"updated_at" bson:"updated_at"` + Deleted uint8 `db:"deleted" bson:"deleted"` +} + +var ( + ErrCode = errors.New("Case statement in code is not correct.") + ErrNoResult = errors.New("Result not found.") + ErrUnavailable = errors.New("Database is unavailable.") +) + +// Id returns the user id +func (u *User) ID() string { + r := "" + + switch database.ReadConfig().Type { + case database.TypeMySQL: + r = fmt.Sprintf("%v", u.Id) + case database.TypeMongoDB: + r = u.ObjectId.Hex() + case database.TypeBolt: + r = u.ObjectId.Hex() + } + + return r +} + +// standardizeErrors returns the same error regardless of the database used +func standardizeError(err error) error { + if err == sql.ErrNoRows || err == mgo.ErrNotFound { + return ErrNoResult + } + + return err } // UserByEmail gets user information from email func UserByEmail(email string) (User, error) { - result := User{} - err := database.DB.Get(&result, "SELECT id, password, status_id, first_name FROM user WHERE email = ? LIMIT 1", email) - return result, err -} + var err error -// UserIdByEmail gets user id from email -func UserIdByEmail(email string) (User, error) { result := User{} - err := database.DB.Get(&result, "SELECT id FROM user WHERE email = ? LIMIT 1", email) - return result, err + + switch database.ReadConfig().Type { + case database.TypeMySQL: + err = database.Sql.Get(&result, "SELECT id, password, status_id, first_name FROM user WHERE email = ? LIMIT 1", email) + case database.TypeMongoDB: + if database.CheckConnection() { + session := database.Mongo.Copy() + defer session.Close() + c := session.DB(database.ReadConfig().MongoDB.Database).C("user") + err = c.Find(bson.M{"email": email}).One(&result) + } else { + err = ErrUnavailable + } + case database.TypeBolt: + err = database.View("user", email, &result) + if err != nil { + err = ErrNoResult + } + default: + err = ErrCode + } + + return result, standardizeError(err) } // UserCreate creates user func UserCreate(first_name, last_name, email, password string) error { - _, err := database.DB.Exec("INSERT INTO user (first_name, last_name, email, password) VALUES (?,?,?,?)", first_name, last_name, email, password) - return err + var err error + + now := time.Now() + + switch database.ReadConfig().Type { + case database.TypeMySQL: + _, err = database.Sql.Exec("INSERT INTO user (first_name, last_name, email, password) VALUES (?,?,?,?)", first_name, + last_name, email, password) + case database.TypeMongoDB: + if database.CheckConnection() { + session := database.Mongo.Copy() + defer session.Close() + c := session.DB(database.ReadConfig().MongoDB.Database).C("user") + + user := &User{ + ObjectId: bson.NewObjectId(), + First_name: first_name, + Last_name: last_name, + Email: email, + Password: password, + Status_id: 1, + Created_at: now, + Updated_at: now, + Deleted: 0, + } + err = c.Insert(user) + } else { + err = ErrUnavailable + } + case database.TypeBolt: + user := &User{ + ObjectId: bson.NewObjectId(), + First_name: first_name, + Last_name: last_name, + Email: email, + Password: password, + Status_id: 1, + Created_at: now, + Updated_at: now, + Deleted: 0, + } + + err = database.Update("user", user.Email, &user) + default: + err = ErrCode + } + + return standardizeError(err) } diff --git a/shared/database/database.go b/shared/database/database.go index 31f65f0..91792a0 100644 --- a/shared/database/database.go +++ b/shared/database/database.go @@ -1,21 +1,37 @@ package database import ( + "encoding/json" "fmt" "log" + "time" + "github.com/boltdb/bolt" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" + "gopkg.in/mgo.v2" ) var ( - // Database wrapper - DB *sqlx.DB + BoltDB *bolt.DB // Bolt wrapper + Mongo *mgo.Session // Mongo wrapper + Sql *sqlx.DB // SQL wrapper + databases DatabaseInfo // Database info ) -type Databases struct { - Type string - MySQL MySQLInfo +type DatabaseType string + +const ( + TypeBolt DatabaseType = "Bolt" + TypeMongoDB DatabaseType = "MongoDB" + TypeMySQL DatabaseType = "MySQL" +) + +type DatabaseInfo struct { + Type DatabaseType + MySQL MySQLInfo + Bolt BoltInfo + MongoDB MongoDBInfo } // MySQLInfo is the details for the database connection @@ -28,6 +44,17 @@ type MySQLInfo struct { Parameter string } +// BoltInfo is the details for the database connection +type BoltInfo struct { + Path string +} + +// MongoDBInfo is the details for the database connection +type MongoDBInfo struct { + URL string + Database string +} + // DSN returns the Data Source Name func DSN(ci MySQLInfo) string { // Example: root:@tcp(localhost:3306)/test @@ -43,21 +70,112 @@ func DSN(ci MySQLInfo) string { } // Connect to the database -func Connect(d Databases) { +func Connect(d DatabaseInfo) { var err error + // Store the config + databases = d + switch d.Type { - case "MySQL": + case TypeMySQL: // Connect to MySQL - if DB, err = sqlx.Connect("mysql", DSN(d.MySQL)); err != nil { + if Sql, err = sqlx.Connect("mysql", DSN(d.MySQL)); err != nil { log.Println("SQL Driver Error", err) } // Check if is alive - if err = DB.Ping(); err != nil { + if err = Sql.Ping(); err != nil { + log.Println("Database Error", err) + } + case TypeBolt: + // Connect to Bolt + if BoltDB, err = bolt.Open(d.Bolt.Path, 0600, nil); err != nil { + log.Println("Bolt Driver Error", err) + } + case TypeMongoDB: + // Connect to MongoDB + if Mongo, err = mgo.DialWithTimeout(d.MongoDB.URL, 5); err != nil { + log.Println("MongoDB Driver Error", err) + return + } + + // Prevents these errors: read tcp 127.0.0.1:27017: i/o timeout + Mongo.SetSocketTimeout(1 * time.Second) + + // Check if is alive + if err = Mongo.Ping(); err != nil { log.Println("Database Error", err) } default: log.Println("No registered database in config") } } + +// Update makes a modification to Bolt +func Update(bucket_name string, key string, dataStruct interface{}) error { + err := BoltDB.Update(func(tx *bolt.Tx) error { + // Create the bucket + bucket, e := tx.CreateBucketIfNotExists([]byte(bucket_name)) + if e != nil { + return e + } + + // Encode the record + encoded_record, e := json.Marshal(dataStruct) + if e != nil { + return e + } + + // Store the record + if e = bucket.Put([]byte(key), encoded_record); e != nil { + return e + } + return nil + }) + return err +} + +// View retrieves a record in Bolt +func View(bucket_name string, key string, dataStruct interface{}) error { + err := BoltDB.View(func(tx *bolt.Tx) error { + // Get the bucket + b := tx.Bucket([]byte(bucket_name)) + if b == nil { + return bolt.ErrBucketNotFound + } + + // Retrieve the record + v := b.Get([]byte(key)) + if len(v) < 1 { + return bolt.ErrInvalid + } + + // Decode the record + e := json.Unmarshal(v, &dataStruct) + if e != nil { + return e + } + + return nil + }) + + return err +} + +// CheckConnection returns true if MongoDB is available +func CheckConnection() bool { + if Mongo == nil { + Connect(databases) + } + + if Mongo != nil { + return true + } + + return false +} + +// ReadConfig returns the database information +func ReadConfig() DatabaseInfo { + return databases +}