Skip to content

Integrate to your system

nvcnvn edited this page Dec 8, 2014 · 4 revisions

In the Getting started article you have setted up an simple REST API to do simple auth stuff (we recomend to read that article first).
Now you want to build some more, how about an "support ticket" site where:

  • Only logged user can post a ticket.
  • Only the owner or admin can view/delete the ticket.

kiddstuff/auth package provide auth.HANDLER_REGISTER function (in fact a function variale overided by manager) which have the signature:

func(fn auth.HandleFunc, owner bool, pri []string) http.Handler

The HANDLER_REGISTER wrapper function use the OR logic, thats mean:

auth.HANDLER_REGISTER(FooHandler, true, []{"manage_content", "do_foo"})

will trigger the FooHandler if the current user are the owner or he can "manage_content" or "do_foo".

To use it for our support ticket site:

// the CreateTicket only run for a "logged" user
r.Handle("/users/{user_id}/tickets",
	auth.HANDLER_REGISTER(CreateTicket, true, nil)).Methods("POST")

r.Handle("/users/{user_id}/tickets/{ticket_id}",
	auth.HANDLER_REGISTER(GetTicket, true, []string{"manage_content"})).Methods("GET")

r.Handle("/users/{user_id}/tickets/{ticket_id}",
	auth.HANDLER_REGISTER(DeleteTicket, true, []string{"manage_content"})).Methods("DELETE")

These routes not really pretty, you may ask why we include "/users/{user_id}" for these paths?
It because the HANDLER_REGISTER will look for the user_id param in request path, compare it with the current user's ID to determine if they are the owner or not (and of course an owner is a logged user).

Note: By running the setup.go we already have an admin an accoutn with "manage_user", "manage_setting", "manage_content" privileges.

Next we need to implemenet the handler functions, creat a tickets.go file in the same folder of main.go.
tickets.go

import (
	"github.com/kidstuff/auth"
	"labix.org/v2/mgo/bson"
	"net/http"
)

type Ticket struct {
	Id      bson.ObjectId `bson:"_id"`
	Content string
}

func CreateTicket(ctx *auth.AuthContext, rw http.ResponseWriter, req *http.Request) (int, error) {
	return http.StatusOK, nil
}

func GetTicket(ctx *auth.AuthContext, rw http.ResponseWriter, req *http.Request) (int, error) {
	return http.StatusOK, nil
}

func DeleteTicket(ctx *auth.AuthContext, rw http.ResponseWriter, req *http.Request) (int, error) {
	return http.StatusOK, nil
}

CreateTicket, GetTicket and DeleteTicket have the auth.HandleFunc signature:
With the auth.AuthContext allow us to access some resource like Loggin and Notification system (remember what you do in main.go?) and many more.
We will implement CreateTicket user step-by-step, begin with parsing user input with the json package:

func CreateTicket(ctx *auth.AuthContext, rw http.ResponseWriter, req *http.Request) (int, error) {
	// developer can call:
	// user, err := ctx.ValidCurrentUser(false, nil)
	// to get the current logged user's infomation
	t := Ticket{}
	err := json.NewDecoder(req.Body).Decode(&t)
	req.Body.Close()
	if err != nil {
		return http.StatusBadRequest, err
	}

	t.Id = bson.NewObjectId()

	// save stuff to database
	// ...
	return http.StatusOK, nil
}

in this handler we don't need to do any extra auth stuff, thank for the help of HANDLER_REGISTER. Now the next step is save the new ticket in database (in our example is MongoDB).
How to access the database (in general different kind of resources)? By a gloabl variable?
We have an hack-around for that which require to import gorilla/context package, take a look at the old main.go (yep, one more time).
Add this block to the end of file

// AuthServer or what kind of name you like
type AuthServer struct {
	r  *mux.Router
	db *mgo.Database
}

func (s *AuthServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	// I asked the mgo author and he said that the session should be clone or copy to avoid
	// something bad like accident close the session in other goroutine.
	cloneDB := s.db.Session.Clone().DB(s.db.Name)
	defer cloneDB.Session.Close()

	context.Set(req, DBKey, cloneDB)

	s.r.ServeHTTP(rw, req)
}
type ctxKey int
const (
	DBKey ctxKey = iota
)

Then replace:

http.ListenAndServe(SERVER_URL, r)

with:

http.ListenAndServe(SERVER_URL, &AuthServer{r, db})

After that, in your handler fucntion, you can access the databse by:

import (
	"encoding/json"
	"errors"
	"github.com/kidstuff/auth"
	"labix.org/v2/mgo"
	"labix.org/v2/mgo/bson"
	"net/http"
)

// ...
// ...

func CreateTicket(ctx *auth.AuthContext, rw http.ResponseWriter, req *http.Request) (int, error) {
	// developer can call:
	// user, err := ctx.ValidCurrentUser(false, nil)
	// to get the current logged user's infomation
	t := Ticket{}
	err := json.NewDecoder(req.Body).Decode(&t)
	req.Body.Close()
	if err != nil {
		return http.StatusBadRequest, err
	}

	t.Id = bson.NewObjectId()

	db, ok := ctx.Value(DBKey).(*mgo.Database)
	if !ok {
		ctx.Logs.Errorf("Cannot access database")
		return http.StatusInternalServerError, errors.New("Cannot access database")
	}

	err = db.C("tickets").Insert(&t)
	if err != nil {
		return http.StatusInternalServerError, err
	}

	json.NewEncoder(rw).Encode(&t)
	return http.StatusOK, nil
}

In your real application, instead of attack the dabase pointer, you can make a "data access layer" or some kind of "model", but just keep this example simple by use the database directly.
Next we will implement the GetTicket (I will left the DeleteTicket for you).

import (
	"encoding/json"
	"errors"
	"github.com/gorilla/mux"
	"github.com/kidstuff/auth"
	"labix.org/v2/mgo"
	"labix.org/v2/mgo/bson"
	"net/http"
)

// ...
// ...

func GetTicket(ctx *auth.AuthContext, rw http.ResponseWriter, req *http.Request) (int, error) {
	sid := mux.Vars(req)["ticket_id"]
	if !bson.IsObjectIdHex(sid) {
		return http.StatusBadRequest, errors.New("Invalid id")
	}

	db, ok := ctx.Value(DBKey).(*mgo.Database)
	if !ok {
		ctx.Logs.Errorf("Cannot access database")
		return http.StatusInternalServerError, errors.New("Cannot access database")
	}

	t := Ticket{}
	err := db.C("tickets").FindId(bson.ObjectIdHex(sid)).One(&t)
	if err != nil {
		if err == mgo.ErrNotFound {
			return http.StatusNotFound, err
		}

		return http.StatusInternalServerError, err
	}

	json.NewEncoder(rw).Encode(&t)
	return http.StatusOK, nil
}

Lets build and test them...yayyyy!
Assuming you're running the application at "localhost:8080", we frist need to get the access token by send a request that include user email and password when you run setup.go:

POST /auth/tokens?grant_type=password&[email protected]&password=example HTTP/1.1
Host: localhost:8080

The kidstuff/auth server will return:

{
    "User": {
        "Id": "548274edfc6c3b15e3000001",
        "Email": "[email protected]",
        "LastActivity": "2014-12-08T10:12:32.952+07:00",
        "Privileges": [
            "manage_user",
            "manage_setting",
            "manage_content"
        ],
        "Approved": true,
        "Profile": {
            "JoinDay": "2014-12-06T10:15:57.523+07:00"
        }
    },
    "ExpiredOn": "2014-12-08T13:12:02.446718778+07:00",
    "AccessToken": "548274edfc6c3b15e3000001w1o_2XmojqRaL7CdOUgpfP6paLaoqmldikjvyk-wZYOaQjsGHEr2KsHwi4zEl9eqAkhC50P7v01W9Lz1ULX7jw=="
}

Keep the access token and your id for the next request.
Note: for more detail about what to send http://kidstuff.github.io/swagger/#!/default

Now we will creat a ticket by:

POST /users/548274edfc6c3b15e3000001/tickets HTTP/1.1
Host: localhost:8080
Authorization: Bearer 548274edfc6c3b15e3000001w1o_2XmojqRaL7CdOUgpfP6paLaoqmldikjvyk-wZYOaQjsGHEr2KsHwi4zEl9eqAkhC50P7v01W9Lz1ULX7jw==
Cache-Control: no-cache

{ "Content": "Please provide better document for kidstuff/auth package" }

You will get the ticket returned with a complete ID:

{
    "Id": "548534b7fc6c3b187a000001",
    "Content": "Please provide better document for kidstuff/auth package"
}

Repeat the action with invalid user id or invalid bearer access token you will get an error response.

I think that is pretty good for this article, but if you have any question/sugesstion, please tell us at https://groups.google.com/forum/#!category-topic/kidstuff-opensources/auth-main-package/TdqCQ6r4a6g

Clone this wiki locally