mirror of
https://github.com/labstack/echo.git
synced 2024-11-24 08:22:21 +02:00
recipe: twitter like api
Signed-off-by: Vishal Rana <vr@labstack.com>
This commit is contained in:
parent
ff8a3bdc94
commit
f10daac5d6
11
glide.lock
generated
11
glide.lock
generated
@ -1,5 +1,5 @@
|
||||
hash: c3a41d26c94d8101a2aeaea5bc438cb75c5e9b7bd80850c7ec806c00c535d1c7
|
||||
updated: 2016-12-02T08:49:24.85691-08:00
|
||||
hash: 19ec155dd3ba25b967dd609c52b748b5302a30bede03936645c12fef7e9edd75
|
||||
updated: 2016-12-05T20:47:56.995548765-08:00
|
||||
imports:
|
||||
- name: github.com/daaku/go.zipexe
|
||||
version: a5fe2436ffcb3236e175e5149162b41cd28bd27d
|
||||
@ -57,6 +57,13 @@ imports:
|
||||
- internal/log
|
||||
- internal/modules
|
||||
- internal/remote_api
|
||||
- name: gopkg.in/mgo.v2
|
||||
version: 3f83fa5005286a7fe593b055f0d7771a7dce4655
|
||||
subpackages:
|
||||
- bson
|
||||
- internal/json
|
||||
- internal/sasl
|
||||
- internal/scram
|
||||
testImports:
|
||||
- name: github.com/davecgh/go-spew
|
||||
version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9
|
||||
|
@ -19,6 +19,7 @@ import:
|
||||
subpackages:
|
||||
- websocket
|
||||
- package: google.golang.org/appengine
|
||||
- package: gopkg.in/mgo.v2
|
||||
testImport:
|
||||
- package: github.com/stretchr/testify
|
||||
subpackages:
|
||||
|
14
recipe/twitter/handler/handler.go
Normal file
14
recipe/twitter/handler/handler.go
Normal file
@ -0,0 +1,14 @@
|
||||
package handler
|
||||
|
||||
import mgo "gopkg.in/mgo.v2"
|
||||
|
||||
type (
|
||||
Handler struct {
|
||||
DB *mgo.Session
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// Key (Should come from somewhere else).
|
||||
Key = "secret"
|
||||
)
|
73
recipe/twitter/handler/post.go
Normal file
73
recipe/twitter/handler/post.go
Normal file
@ -0,0 +1,73 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/recipe/twitter/model"
|
||||
mgo "gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
func (h *Handler) CreatePost(c echo.Context) (err error) {
|
||||
u := &model.User{
|
||||
ID: bson.ObjectIdHex(userIDFromToken(c)),
|
||||
}
|
||||
p := &model.Post{
|
||||
ID: bson.NewObjectId(),
|
||||
From: u.ID.Hex(),
|
||||
}
|
||||
if err = c.Bind(p); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Validation
|
||||
if p.To == "" || p.Message == "" {
|
||||
return &echo.HTTPError{Code: http.StatusBadRequest, Message: "invalid to or message fields"}
|
||||
}
|
||||
|
||||
// Find user from database
|
||||
db := h.DB.Clone()
|
||||
defer db.Close()
|
||||
if err = db.DB("twitter").C("users").FindId(u.ID).One(u); err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return echo.ErrNotFound
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Save post in database
|
||||
if err = db.DB("twitter").C("posts").Insert(p); err != nil {
|
||||
return
|
||||
}
|
||||
return c.JSON(http.StatusCreated, p)
|
||||
}
|
||||
|
||||
func (h *Handler) FetchPost(c echo.Context) (err error) {
|
||||
userID := userIDFromToken(c)
|
||||
page, _ := strconv.Atoi(c.QueryParam("page"))
|
||||
limit, _ := strconv.Atoi(c.QueryParam("limit"))
|
||||
|
||||
// Defaults
|
||||
if page == 0 {
|
||||
page = 1
|
||||
}
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
// Retrieve posts from database
|
||||
posts := []*model.Post{}
|
||||
db := h.DB.Clone()
|
||||
if err = db.DB("twitter").C("posts").
|
||||
Find(bson.M{"to": userID}).
|
||||
Skip((page - 1) * limit).
|
||||
Limit(limit).
|
||||
All(&posts); err != nil {
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
return c.JSON(http.StatusOK, posts)
|
||||
}
|
97
recipe/twitter/handler/user.go
Normal file
97
recipe/twitter/handler/user.go
Normal file
@ -0,0 +1,97 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/recipe/twitter/model"
|
||||
mgo "gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
func (h *Handler) Signup(c echo.Context) (err error) {
|
||||
// Bind
|
||||
u := &model.User{ID: bson.NewObjectId()}
|
||||
if err = c.Bind(u); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate
|
||||
if u.Email == "" || u.Password == "" {
|
||||
return &echo.HTTPError{Code: http.StatusBadRequest, Message: "invalid email or password"}
|
||||
}
|
||||
|
||||
// Save user
|
||||
db := h.DB.Clone()
|
||||
defer db.Close()
|
||||
if err = db.DB("twitter").C("users").Insert(u); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusCreated, u)
|
||||
}
|
||||
|
||||
func (h *Handler) Login(c echo.Context) (err error) {
|
||||
// Bind
|
||||
u := new(model.User)
|
||||
if err = c.Bind(u); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Find user
|
||||
db := h.DB.Clone()
|
||||
defer db.Close()
|
||||
if err = db.DB("twitter").C("users").
|
||||
Find(bson.M{"email": u.Email, "password": u.Password}).One(u); err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return &echo.HTTPError{Code: http.StatusUnauthorized, Message: "invalid email or password"}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//-----
|
||||
// JWT
|
||||
//-----
|
||||
|
||||
// Create token
|
||||
token := jwt.New(jwt.SigningMethodHS256)
|
||||
|
||||
// Set claims
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
claims["id"] = u.ID
|
||||
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
||||
|
||||
// Generate encoded token and send it as response
|
||||
u.Token, err = token.SignedString([]byte(Key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.Password = "" // Don't send password
|
||||
return c.JSON(http.StatusOK, u)
|
||||
}
|
||||
|
||||
func (h *Handler) Follow(c echo.Context) (err error) {
|
||||
userID := userIDFromToken(c)
|
||||
id := c.Param("id")
|
||||
|
||||
// Add a follower to user
|
||||
db := h.DB.Clone()
|
||||
defer db.Close()
|
||||
if err = db.DB("twitter").C("users").
|
||||
UpdateId(bson.ObjectIdHex(id), bson.M{"$addToSet": bson.M{"followers": userID}}); err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
return echo.ErrNotFound
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func userIDFromToken(c echo.Context) string {
|
||||
user := c.Get("user").(*jwt.Token)
|
||||
claims := user.Claims.(jwt.MapClaims)
|
||||
return claims["id"].(string)
|
||||
}
|
12
recipe/twitter/model/post.go
Normal file
12
recipe/twitter/model/post.go
Normal file
@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "gopkg.in/mgo.v2/bson"
|
||||
|
||||
type (
|
||||
Post struct {
|
||||
ID bson.ObjectId `json:"id" bson:"_id,omitempty"`
|
||||
To string `json:"to" bson:"to"`
|
||||
From string `json:"from" bson:"from"`
|
||||
Message string `json:"message" bson:"message"`
|
||||
}
|
||||
)
|
13
recipe/twitter/model/user.go
Normal file
13
recipe/twitter/model/user.go
Normal file
@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
import "gopkg.in/mgo.v2/bson"
|
||||
|
||||
type (
|
||||
User struct {
|
||||
ID bson.ObjectId `json:"id" bson:"_id,omitempty"`
|
||||
Email string `json:"email" bson:"email"`
|
||||
Password string `json:"password,omitempty" bson:"password"`
|
||||
Token string `json:"token,omitempty" bson:"-"`
|
||||
Followers []string `json:"followers,omitempty" bson:"followers,omitempty"`
|
||||
}
|
||||
)
|
52
recipe/twitter/server.go
Normal file
52
recipe/twitter/server.go
Normal file
@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/middleware"
|
||||
"github.com/labstack/echo/recipe/twitter/handler"
|
||||
"github.com/labstack/gommon/log"
|
||||
mgo "gopkg.in/mgo.v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
e := echo.New()
|
||||
e.Logger.SetLevel(log.ERROR)
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
|
||||
SigningKey: []byte(handler.Key),
|
||||
Skipper: func(c echo.Context) bool {
|
||||
// Skip authentication for and signup login requests
|
||||
if c.Path() == "/login" || c.Path() == "/signup" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}))
|
||||
|
||||
// Database connection
|
||||
db, err := mgo.Dial("localhost")
|
||||
if err != nil {
|
||||
e.Logger.Fatal(err)
|
||||
}
|
||||
|
||||
// Create indices
|
||||
if err = db.Copy().DB("twitter").C("users").EnsureIndex(mgo.Index{
|
||||
Key: []string{"email"},
|
||||
Unique: true,
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Initialize handler
|
||||
h := &handler.Handler{DB: db}
|
||||
|
||||
// Routes
|
||||
e.POST("/signup", h.Signup)
|
||||
e.POST("/login", h.Login)
|
||||
e.POST("/follow/:id", h.Follow)
|
||||
e.POST("/posts", h.CreatePost)
|
||||
e.GET("/feed", h.FetchPost)
|
||||
|
||||
// Start server
|
||||
e.Logger.Fatal(e.Start(":1323"))
|
||||
}
|
202
website/content/recipes/twitter.md
Normal file
202
website/content/recipes/twitter.md
Normal file
@ -0,0 +1,202 @@
|
||||
+++
|
||||
title = "Twitter API Example"
|
||||
description = "Twitter API example for Echo"
|
||||
[menu.main]
|
||||
name = "Twitter"
|
||||
parent = "recipes"
|
||||
weight = 2
|
||||
+++
|
||||
|
||||
This recipe shows how to create a Twitter like REST API using MongoDB (Database),
|
||||
JWT (API security) and JSON (Data exchange).
|
||||
|
||||
## Models
|
||||
|
||||
`user.go`
|
||||
|
||||
{{< embed "twitter/model/user.go" >}}
|
||||
|
||||
`post.go`
|
||||
|
||||
{{< embed "twitter/model/post.go" >}}
|
||||
|
||||
## Handlers
|
||||
|
||||
`handler.go`
|
||||
|
||||
{{< embed "twitter/handler/handler.go" >}}
|
||||
|
||||
`user.go`
|
||||
|
||||
{{< embed "twitter/handler/user.go" >}}
|
||||
|
||||
`post.go`
|
||||
|
||||
{{< embed "twitter/handler/post.go" >}}
|
||||
|
||||
## API
|
||||
|
||||
### Signup
|
||||
|
||||
User signup
|
||||
|
||||
- Retrieve user credentials from the body and validate against database.
|
||||
- For invalid email or password, send `400 - Bad Request` response.
|
||||
- For valid email and password, save user in database and send `201 - Created` response.
|
||||
-
|
||||
|
||||
#### Request
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X POST \
|
||||
http://localhost:1323/signup \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"jon@labstack.com","password":"shhh!"}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`201 - Created`
|
||||
|
||||
```js
|
||||
{
|
||||
"id": "58465b4ea6fe886d3215c6df",
|
||||
"email": "jon@labstack.com",
|
||||
"password": "shhh!"
|
||||
}
|
||||
```
|
||||
|
||||
### Login
|
||||
|
||||
User login
|
||||
|
||||
- Retrieve user credentials from the body and validate against database.
|
||||
- For invalid credentials, send `401 - Unauthorized` response.
|
||||
- For valid credentials, send `200 - OK` response:
|
||||
- Generate JWT for the user and send it as response.
|
||||
- Each subsequent request must include JWT in the `Authorization` header.
|
||||
|
||||
Method: `POST`<br>
|
||||
Path: `/login`
|
||||
|
||||
#### Request
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X POST \
|
||||
http://localhost:1323/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"jon@labstack.com","password":"shhh!"}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`200 - OK`
|
||||
|
||||
```js
|
||||
{
|
||||
"id": "58465b4ea6fe886d3215c6df",
|
||||
"email": "jon@labstack.com",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0"
|
||||
}
|
||||
```
|
||||
|
||||
Client should store the token, for browsers, you may use local storage.
|
||||
|
||||
### Follow
|
||||
|
||||
Follow a user
|
||||
|
||||
- For invalid token, send `400 - Bad Request` response.
|
||||
- For valid token:
|
||||
- If user is not found, send `404 - Not Found` response.
|
||||
- Add a follower to the specified user in the path parameter and send `200 - OK` response.
|
||||
|
||||
Method: `POST` <br>
|
||||
Path: `/follow/:id`
|
||||
|
||||
#### Request
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X POST \
|
||||
http://localhost:1323/follow/58465b4ea6fe886d3215c6df \
|
||||
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`200 - OK`
|
||||
|
||||
### Post
|
||||
|
||||
Post a message to specified user
|
||||
|
||||
- For invalid request payload, send `400 - Bad Request` response.
|
||||
- If user is not found, send `404 - Not Found` response.
|
||||
- Otherwise save post in the database and return it via `201 - Created` response.
|
||||
|
||||
Method: `POST` <br>
|
||||
Path: `/posts`
|
||||
|
||||
#### Request
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X POST \
|
||||
http://localhost:1323/posts \
|
||||
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"to":"58465b4ea6fe886d3215c6df","message":"hello"}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`201 - Created`
|
||||
|
||||
```sh
|
||||
{
|
||||
"id": "584661b9a6fe8871a3804cba",
|
||||
"to": "58465b4ea6fe886d3215c6df",
|
||||
"from": "58465b4ea6fe886d3215c6df",
|
||||
"message": "hello"
|
||||
}
|
||||
```
|
||||
|
||||
### Feed
|
||||
|
||||
List most recent messages based on optional `page` and `limit` query parameters
|
||||
|
||||
Method: `GET` <br>
|
||||
Path: `/feed?page=1&limit=5`
|
||||
|
||||
#### Request
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X GET \
|
||||
http://localhost:1323/feed \
|
||||
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`200 - OK`
|
||||
|
||||
```sh
|
||||
[
|
||||
{
|
||||
"id": "584661b9a6fe8871a3804cba",
|
||||
"to": "58465b4ea6fe886d3215c6df",
|
||||
"from": "58465b4ea6fe886d3215c6df",
|
||||
"message": "hello"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## [Source Code]({{< source "twitter" >}})
|
||||
|
||||
## Maintainers
|
||||
|
||||
- [vishr](https://github.com/vishr)
|
@ -1,9 +1,9 @@
|
||||
{{ partial "head.html" . }}
|
||||
<body>
|
||||
<body class="docs">
|
||||
{{ partial "topnav.html" . }}
|
||||
<div class="w3-container w3-content">
|
||||
{{ partial "sidenav.html" . }}
|
||||
<div class="w3-main">
|
||||
<div class="main">
|
||||
{{ partial "ad.html" }}
|
||||
<div class="w3-row-padding">
|
||||
<div class="w3-col s12 m10 l10">
|
||||
|
@ -1,12 +1,12 @@
|
||||
{{ partial "head.html" . }}
|
||||
<body>
|
||||
<body class="index">
|
||||
{{ partial "topnav.html" . }}
|
||||
<div class="w3-hide-large">
|
||||
{{ partial "sidenav.html" . }}
|
||||
</div>
|
||||
<div class="w3-container w3-content w3-padding-128">
|
||||
<div class="w3-container w3-content main">
|
||||
{{ partial "ad.html" }}
|
||||
<div class="w3-row-padding">
|
||||
<div class="w3-row">
|
||||
<div class="w3-col s12 m10 l10">
|
||||
<div class="hero">
|
||||
<h1>{{ .Site.Data.index.heading }}</h1>
|
||||
|
@ -1,8 +1,8 @@
|
||||
{{ partial "head.html" . }}
|
||||
<body>
|
||||
<body class="single">
|
||||
{{ partial "topnav.html" . }}
|
||||
<div class="w3-container w3-content w3-padding-64">
|
||||
<div class="w3-row-padding">
|
||||
<div class="w3-container w3-content main">
|
||||
<div class="w3-row">
|
||||
<div class="w3-col m10 l10">
|
||||
<h1>{{ .Title }}</h1>
|
||||
{{ .Content }}
|
||||
|
Loading…
Reference in New Issue
Block a user