From c9de0428aa27fc0bad76c8627282655d56c47dc0 Mon Sep 17 00:00:00 2001 From: Pedro Nasser Date: Tue, 30 Aug 2016 16:47:34 -0300 Subject: [PATCH] added blog example --- examples/blog/.gitignore | 0 examples/blog/Dockerfile | 8 ++ examples/blog/README.md | 103 ++++++++++++++++++++ examples/blog/VERSION | 1 + examples/blog/database/database.go | 20 ++++ examples/blog/database/post.go | 73 ++++++++++++++ examples/blog/database/user.go | 65 +++++++++++++ examples/blog/main.go | 82 ++++++++++++++++ examples/blog/models/post.go | 9 ++ examples/blog/models/user.go | 17 ++++ examples/blog/routes/post_create.go | 30 ++++++ examples/blog/routes/post_list.go | 18 ++++ examples/blog/routes/post_read.go | 26 +++++ examples/blog/routes/server.go | 145 ++++++++++++++++++++++++++++ 14 files changed, 597 insertions(+) create mode 100644 examples/blog/.gitignore create mode 100644 examples/blog/Dockerfile create mode 100644 examples/blog/README.md create mode 100644 examples/blog/VERSION create mode 100644 examples/blog/database/database.go create mode 100644 examples/blog/database/post.go create mode 100644 examples/blog/database/user.go create mode 100644 examples/blog/main.go create mode 100644 examples/blog/models/post.go create mode 100644 examples/blog/models/user.go create mode 100644 examples/blog/routes/post_create.go create mode 100644 examples/blog/routes/post_list.go create mode 100644 examples/blog/routes/post_read.go create mode 100644 examples/blog/routes/server.go diff --git a/examples/blog/.gitignore b/examples/blog/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/examples/blog/Dockerfile b/examples/blog/Dockerfile new file mode 100644 index 000000000..43b5ae40d --- /dev/null +++ b/examples/blog/Dockerfile @@ -0,0 +1,8 @@ +FROM iron/go:dev + +ADD . $GOPATH/src/github.com/iron-io/functions/examples/blog +WORKDIR $GOPATH/src/github.com/iron-io/functions/examples/blog + +RUN go get . + +ENTRYPOINT ["go", "run", "main.go"] \ No newline at end of file diff --git a/examples/blog/README.md b/examples/blog/README.md new file mode 100644 index 000000000..fca724341 --- /dev/null +++ b/examples/blog/README.md @@ -0,0 +1,103 @@ +# Blog API Example + +## Requirements + +- Remote MongoDB instance (for example heroku) + +## Development + +### Building image locally + +``` +# SET BELOW TO YOUR DOCKER HUB USERNAME +USERNAME=YOUR_DOCKER_HUB_USERNAME + +# build it +docker build -t $USERNAME/functions-blog . +``` + +### Publishing it + +``` +# tagging +docker run --rm -v "$PWD":/app treeder/bump patch +docker tag $USERNAME/functions-blog:latest $USERNAME/functions-blog:`cat VERSION` + +# pushing to docker hub +docker push $USERNAME/functions-blog +``` + +## Running it on IronFunctions + +### First, let's define this environment variables + +``` +# Set your Function server address +# Eg. 127.0.0.1:8080 +FUNCAPI=YOUR_FUNCTIONS_ADDRESS + +# Set your mongoDB server address +# Eg. 127.0.0.1:27017 +MONGODB=YOUR_MONGODB_ADDRESS +``` + +### Creating our blog application in your IronFunctions + +With this command we are going to create an application with name `blog` and also defining the app configuration `DB`. + +``` +curl -X POST --data '{ + "app": { + "name": "blog", + "config": { "DB": "'$MONGODB'" } + } +}' http://$FUNCAPI/v1/apps +``` + +Now, we can create our blog routes: `/posts` and `/posts/:id` + +``` +curl -X POST --data '{ + "route": { + "image": "'$USERNAME'/functions-blog", + "path": "/posts" + } +}' http://$FUNCAPI/v1/apps/blog/routes +``` + +``` +curl -X POST --data '{ + "route": { + "image": "'$USERNAME'/functions-blog", + "path": "/posts/:id" + } +}' http://$FUNCAPI/v1/apps/blog/routes +``` + +### Testing our Blog via API + +Now that we created our IronFunction route, lets test our routes + +``` +curl -X GET http://$FUNCAPI/r/blog/posts +curl -X GET http://$FUNCAPI/r/blog/posts/123456 +``` + +These commands should return `{"error":"Invalid authentication"}` because we aren't sending any token. + +## Authentication + +### Creating a blog user + +First let's create our blog user. +``` + +``` + +### + +To get authorized to access our Blog API endpoints we must request a new token with a valid user. + +``` + +``` \ No newline at end of file diff --git a/examples/blog/VERSION b/examples/blog/VERSION new file mode 100644 index 000000000..8a9ecc2ea --- /dev/null +++ b/examples/blog/VERSION @@ -0,0 +1 @@ +0.0.1 \ No newline at end of file diff --git a/examples/blog/database/database.go b/examples/blog/database/database.go new file mode 100644 index 000000000..55c05f007 --- /dev/null +++ b/examples/blog/database/database.go @@ -0,0 +1,20 @@ +package database + +import "gopkg.in/mgo.v2" + +type Database struct { + Session *mgo.Session +} + +func New(uri string) *Database { + session, err := mgo.Dial(uri) + if err != nil { + panic(err) + } + + session.SetMode(mgo.Monotonic, true) + + return &Database{ + Session: session, + } +} diff --git a/examples/blog/database/post.go b/examples/blog/database/post.go new file mode 100644 index 000000000..d6d63902f --- /dev/null +++ b/examples/blog/database/post.go @@ -0,0 +1,73 @@ +package database + +import ( + "errors" + + "github.com/iron-io/functions/examples/blog/models" + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +var ( + ErrNotObjectIdHex = errors.New("Invalid ID") +) + +func (db *Database) SavePost(post *models.Post) (*models.Post, error) { + s := db.Session.Copy() + defer s.Close() + + c := s.DB("").C("post") + + if post.ID.Hex() == "" { + post.ID = bson.NewObjectId() + } + id := post.ID + + change := mgo.Change{ + Update: bson.M{"$set": post}, + ReturnNew: true, + Upsert: true, + } + + _, err := c.Find(bson.M{"_id": id}).Apply(change, &post) + + if err != nil { + return nil, err + } + + return post, nil +} + +func (db *Database) GetPost(id string) (*models.Post, error) { + s := db.Session.Copy() + defer s.Close() + + c := s.DB("").C("post") + + var post models.Post + if !bson.IsObjectIdHex(id) { + return nil, ErrNotObjectIdHex + } + + err := c.Find(bson.M{"_id": bson.ObjectIdHex(id)}).One(&post) + if err != nil { + return nil, err + } + + return &post, nil +} + +func (db *Database) GetPosts(query []bson.M) ([]*models.Post, error) { + s := db.Session.Copy() + defer s.Close() + + c := s.DB("").C("post") + + var posts []*models.Post + + err := c.Pipe(query).All(&posts) + if err != nil { + return nil, err + } + return posts, nil +} diff --git a/examples/blog/database/user.go b/examples/blog/database/user.go new file mode 100644 index 000000000..c6bff9f0a --- /dev/null +++ b/examples/blog/database/user.go @@ -0,0 +1,65 @@ +package database + +import ( + "github.com/iron-io/functions/examples/blog/models" + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +func (db *Database) SaveUser(user *models.User) (*models.User, error) { + s := db.Session.Copy() + defer s.Close() + + c := s.DB("").C("user") + id := user.Username + user.Username = "" + + if len(user.Password) > 0 { + user.Password = models.UserPasswordEncrypt(user.Password) + } + + change := mgo.Change{ + Update: bson.M{"$set": user}, + ReturnNew: true, + Upsert: true, + } + + _, err := c.Find(bson.M{"_id": id}).Apply(change, &user) + + if err != nil { + return nil, err + } + + return user, nil +} + +func (db *Database) GetUser(id string) (*models.User, error) { + s := db.Session.Copy() + defer s.Close() + + c := s.DB("").C("user") + + var user models.User + + err := c.Find(bson.M{"_id": id}).One(&user) + if err != nil { + return nil, err + } + + return &user, nil +} + +func (db *Database) GetUsers(query []bson.M) ([]*models.User, error) { + s := db.Session.Copy() + defer s.Close() + + c := s.DB("").C("user") + + var users []*models.User + + err := c.Pipe(query).All(&users) + if err != nil { + return nil, err + } + return users, nil +} diff --git a/examples/blog/main.go b/examples/blog/main.go new file mode 100644 index 000000000..f7e9da77c --- /dev/null +++ b/examples/blog/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/iron-io/functions/examples/blog/database" + "github.com/iron-io/functions/examples/blog/models" + "github.com/iron-io/functions/examples/blog/routes" +) + +func main() { + request := fmt.Sprintf("%s %s", os.Getenv("METHOD"), os.Getenv("ROUTE")) + + dbURI := os.Getenv("CONFIG_DB") + if dbURI == "" { + dbURI = "127.0.0.1/blog" + } + db := database.New(dbURI) + + if created := createUser(db); created { + return + } + + if os.Getenv("ROUTE") == "/token" { + route.HandleToken(db) + return + } + + auth, valid := route.Authentication() + if !valid { + route.SendError("Invalid authentication") + return + } + + switch request { + case "GET /posts": + route.HandlePostList(db, auth) + break + case "POST /posts": + route.HandlePostCreate(db, auth) + break + case "GET /posts/:id": + route.HandlePostRead(db, auth) + break + default: + route.SendError("Not found") + } +} + +func createUser(db *database.Database) bool { + env := os.Getenv("NEWUSER") + + if env == "" { + return false + } + + var user *models.User + err := json.Unmarshal([]byte(env), &user) + if err != nil { + fmt.Println(err) + return true + } + + if user.Username == "" || user.NewPassword == "" { + fmt.Println("missing username or password") + return true + } + + user.Password = []byte(user.NewPassword) + user.NewPassword = "" + + user, err = db.SaveUser(user) + if err != nil { + fmt.Println("couldn't create user") + } else { + fmt.Println("user created") + } + + return true +} diff --git a/examples/blog/models/post.go b/examples/blog/models/post.go new file mode 100644 index 000000000..80e72694b --- /dev/null +++ b/examples/blog/models/post.go @@ -0,0 +1,9 @@ +package models + +import "gopkg.in/mgo.v2/bson" + +type Post struct { + ID bson.ObjectId `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + Body string `json:"body" bson:"body"` +} diff --git a/examples/blog/models/user.go b/examples/blog/models/user.go new file mode 100644 index 000000000..a4809e0a5 --- /dev/null +++ b/examples/blog/models/user.go @@ -0,0 +1,17 @@ +package models + +import "golang.org/x/crypto/bcrypt" + +type User struct { + Username string `json:"username" bson:"_id,omitempty"` + Password []byte `json:"-" bson:"password"` + NewPassword string `json:"password" bson:"-"` +} + +func UserPasswordEncrypt(pass []byte) []byte { + hashedPassword, err := bcrypt.GenerateFromPassword(pass, bcrypt.DefaultCost) + if err != nil { + panic(err) + } + return hashedPassword +} diff --git a/examples/blog/routes/post_create.go b/examples/blog/routes/post_create.go new file mode 100644 index 000000000..5845713fa --- /dev/null +++ b/examples/blog/routes/post_create.go @@ -0,0 +1,30 @@ +package route + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/iron-io/functions/examples/blog/database" + "github.com/iron-io/functions/examples/blog/models" +) + +func HandlePostCreate(db *database.Database, auth map[string]interface{}) { + var post *models.Post + + err := json.Unmarshal([]byte(os.Getenv("PAYLOAD")), &post) + if err != nil { + fmt.Println("Invalid post") + return + } + + post, err = db.SavePost(post) + if err != nil { + fmt.Println("Couldn't save that post") + return + } + + SendResponse(Response{ + "post": post, + }) +} diff --git a/examples/blog/routes/post_list.go b/examples/blog/routes/post_list.go new file mode 100644 index 000000000..1131b1360 --- /dev/null +++ b/examples/blog/routes/post_list.go @@ -0,0 +1,18 @@ +package route + +import ( + "github.com/iron-io/functions/examples/blog/database" + "gopkg.in/mgo.v2/bson" +) + +func HandlePostList(db *database.Database, auth map[string]interface{}) { + posts, err := db.GetPosts([]bson.M{}) + if err != nil { + SendError("Couldn't retrieve posts") + return + } + + SendResponse(Response{ + "posts": posts, + }) +} diff --git a/examples/blog/routes/post_read.go b/examples/blog/routes/post_read.go new file mode 100644 index 000000000..70d9674f1 --- /dev/null +++ b/examples/blog/routes/post_read.go @@ -0,0 +1,26 @@ +package route + +import ( + "os" + + "github.com/iron-io/functions/examples/blog/database" +) + +func HandlePostRead(db *database.Database, auth map[string]interface{}) { + id := os.Getenv("PARAM_ID") + + if id == "" { + SendError("Missing post ID") + return + } + + post, err := db.GetPost(id) + if err != nil { + SendError("Couldn't retrieve that post") + return + } + + SendResponse(Response{ + "post": post, + }) +} diff --git a/examples/blog/routes/server.go b/examples/blog/routes/server.go new file mode 100644 index 000000000..ea9524331 --- /dev/null +++ b/examples/blog/routes/server.go @@ -0,0 +1,145 @@ +package route + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/iron-io/functions/examples/blog/database" + "github.com/iron-io/functions/examples/blog/models" + "golang.org/x/crypto/bcrypt" +) + +// import "github.com/iron-io/functions/examples/blog/database" + +var jwtSignKey = []byte("mysecretblog") + +type Response map[string]interface{} + +func SendResponse(resp Response) { + data, _ := json.Marshal(resp) + fmt.Println(string(data)) +} + +func SendError(err interface{}) { + SendResponse(Response{ + "error": err, + }) +} + +func HandleToken(db *database.Database) { + var login *models.User + err := json.Unmarshal([]byte(os.Getenv("PAYLOAD")), &login) + if err != nil { + fmt.Println("Missing username and password") + return + } + + user, err := db.GetUser(login.Username) + if err != nil { + SendError("Couldn't create a token") + return + } + + if err := bcrypt.CompareHashAndPassword(user.Password, []byte(login.NewPassword)); err != nil { + SendError("Couldn't create a token") + return + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user": login.Username, + "exp": time.Now().Add(1 * time.Hour), + }) + + // Sign and get the complete encoded token as a string using the secret + tokenString, err := token.SignedString(jwtSignKey) + if err != nil { + SendError("Couldn't create a token") + return + } + + SendResponse(Response{"token": tokenString}) +} + +func Authentication() (map[string]interface{}, bool) { + authorization := os.Getenv("HEADER_AUTHORIZATION") + + p := strings.Split(authorization, " ") + if len(p) <= 1 { + return nil, false + } + + token, err := jwt.Parse(p[1], func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + return jwtSignKey, nil + }) + + if err != nil { + return nil, false + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + return claims, true + } + + return nil, false +} + +// func New(db *database.Database) *gin.Engine { +// DB = db + +// r := gin.New() +// r.POST("/auth", func(c *gin.Context) { +// username := c.PostForm("username") +// password := c.PostForm("password") + +// user, err := db.GetUser(username) +// if err != nil { +// c.JSON(500, gin.H{"message": "Could not generate token"}) +// return +// } + +// err = bcrypt.CompareHashAndPassword(user.Password, []byte(password)) +// if err != nil { +// c.JSON(500, gin.H{"message": "Could not generate token"}) +// return +// } + +// token := jwt_lib.New(jwt_lib.GetSigningMethod("HS256")) +// claims := token.Claims.(jwt_lib.MapClaims) +// claims["ID"] = username +// claims["exp"] = time.Now().Add(time.Hour * 1).Unix() + +// tokenString, err := token.SignedString([]byte(jwtSignKey)) +// if err != nil { +// c.JSON(500, gin.H{"message": "Could not generate token"}) +// return +// } +// c.JSON(200, gin.H{"token": tokenString}) +// }) + +// r.POST("/testuser", func(c *gin.Context) { +// _, err := db.SaveUser(&models.User{ +// Username: "test", +// Password: []byte("test"), +// }) +// if err != nil { +// c.JSON(500, gin.H{"message": "Could create test user"}) +// return +// } +// c.JSON(200, gin.H{"message": "test user created"}) +// }) + +// blog := r.Group("/blog") +// blog.Use(jwtAuth(jwtSignKey)) +// blog.GET("/posts", handlePostList) +// blog.POST("/posts", handlePostCreate) +// blog.GET("/posts/:id", handlePostRead) + +// return r +// }