Skip to content

Instantly share code, notes, and snippets.

@iansmith
Created January 21, 2013 03:26
Show Gist options
  • Save iansmith/4583407 to your computer and use it in GitHub Desktop.
Save iansmith/4583407 to your computer and use it in GitHub Desktop.
package main
import (
"fmt"
"net/http"
"strings"
"seven5/auth"
"seven5/user"
"seven5"
"code.google.com/p/goauth2/oauth"
)
const (
NAME = "myauth"
SCOPE = "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email "
LOGIN_TYPE = "auto" //can be "force" or "auto"
)
//MyUserWire defines what passes back and forth with the client side. This must be ok for Seven5 to
//serialize to json and must include an Id field of type seven5.Id. Use only seven5 types to make
//sure it flattens correctly.
type MyUserWire struct {
Id seven5.Id
ExternalId seven5.String255
Name seven5.String255
Email seven5.String255
Pic seven5.String255
}
//MyUser is both a seven5.Session and a user.Basic. Under the covers it uses a google user retreived
//at login to know the user's name, email, etc. Deleted objects are never really removed, just
//marked.
type MyUser struct {
*seven5.SimpleSession
*auth.GoogleUser
Id seven5.Id
sup *MySupport
deleted bool
}
//ToWire converts the user internal structure to the wire structure for sending to the other side. This
//cannot be done automatically because seven5 doesn't know what is in MyUser that you want marshalled.
//This function is required by the interface user.Basic
func (self *MyUser) ToWire() interface{} {
return &MyUserWire{
Id: self.Id,
ExternalId: seven5.String255(self.GoogleId),
Name: seven5.String255(self.Name),
Email: seven5.String255(self.Email()),
Pic: seven5.String255(self.Picture),
}
}
//Id is required by the contract of user.Basic. This is so it can do simple comparisons
//(x.WireId()==y.WireId()) without knowing the true type.
func (self *MyUser) WireId() seven5.Id {
return self.Id
}
//MySupport is a struct that maintains a list of all known users in memory. It also uses the environment
//to know the "magic" google ids that are admins. THIS IMPLEMENTATION IS NOT CONCURRENT SAFE! It's
//just a sample to show the API.
type MySupport struct {
env *auth.EnvironmentVars
conn auth.ServiceConnector
all []*MyUser
next seven5.Id
}
//IsStaff returns true if the user is a staff member. Staff members are intended to be lower than
//admins but we just map staff to admins.
func (self *MySupport) IsStaff(b user.Basic) bool {
return self.IsAdmin(b)
}
//IsAdmin returns true if the environment contains an environment variable of the form
//"MYAUTH_ADMIN" that has the email of this user contained somewhere in it.
func (self *MySupport) IsAdmin(b user.Basic) bool {
v:=self.env.GetAppValue("ADMIN")
return strings.Index(v,b.Email())!=-1
}
//FindByEmail computes the seven5 Id from the basic user object. It does this by walking the entire
//list of known users, looking for the email address. It panics if it can't find the user sought.
func (self *MySupport) FindByEmail(cand user.Basic) seven5.Id {
for _,v:=range self.KnownUsers() {
if v.Email()==cand.Email() {
return v.WireId()
}
}
panic("couldn't find user to convert to id!")
}
//FindById searches for user with a known Id.
func (self *MySupport) FindById(id seven5.Id) user.Basic {
for _,v:=range self.KnownUsers() {
if v.WireId()==id {
return v
}
}
panic("couldn't find user from an id id!")
}
//Update is called by the resource implementation to allow the proposed (wire) object to
//have some or all of it's fields copied into existing. Obviously, the fields need to be
//sanity checked.
func (self *MySupport) UpdateFields(p interface{}, e user.Basic) {
proposed:=p.(*MyUserWire)
existing:=e.(*MyUser)
email:=strings.TrimSpace(string(proposed.Email))
name:=strings.TrimSpace(string(proposed.Name))
pic:=strings.TrimSpace(string(proposed.Pic))
//we store these values inside the google object
if email!=existing.Email() && email!="" {
existing.SetEmail(email)
}
if name!=existing.Name && name!="" {
existing.GoogleUser.Name=name
}
if pic!=existing.Picture && pic!="" {
existing.GoogleUser.Picture=pic
}
//not allowed to change any other fields!
}
//KnownUsers returns a copy of all known users that are not deleted.
func (self *MySupport) KnownUsers() []user.Basic {
result:=[]user.Basic{}
for _, u:=range self.all {
if u.deleted {
continue
}
result = append(result, u)
}
return result
}
func (self *MySupport) Delete(id seven5.Id) user.Basic {
b:=self.FindById(id)
u:=b.(*MyUser)
u.deleted=true
return u
}
//NewMySupport creates an instance of MySupport. Should only be one in the program. You have to
//supply these parameters because we need them when we generate a new session.
func NewMySupport(env *auth.EnvironmentVars, conn auth.ServiceConnector) *MySupport{
return &MySupport{env, conn, nil, 0}
}
//Generate is called by the basic infrastructure to return a new "session" which is also a user.
//This implementation does a call to the fetcher, other values are ignored. This is where the
//"magic" happens to take a google user and create one of our MyUser object from it.
func (self *MySupport) Generate(t *oauth.Transport) (seven5.Session, error) {
i, err:=self.conn.Fetch(t)
if err!=nil {
return nil, err
}
google:=i.(*auth.GoogleUser)
//create the _internal_ object
u:=&MyUser {
Id: self.next,
SimpleSession:seven5.NewSimpleSession(),
GoogleUser:google,
sup:self,
deleted: false,
}
//update the counter
self.next++
//keep track of all known users
self.all = append(self.all, u)
return u,nil
}
func main() {
//we use the environment variables and the heroku name for our app deployment URL.
//Environment var PORT controls the port number we run on, both for production and test.
heroku := auth.NewHerokuDeploy(NAME)
//google connector needs the environment for client id and secret and heroku for URL calculating
goog := auth.NewGoogleAuthConnector(SCOPE, LOGIN_TYPE, heroku.Environment(), heroku)
//hook our application type to the Basic User support
mgr:=user.NewBasicManager(NewMySupport(heroku.Environment(), goog))
//a dispatcher takes in raw requests and picks the appropriate API to dispatch them on
//base dispatcher works with rest resources
bd := seven5.NewBaseDispatcher(NAME, mgr)
//seven5.ServeMux is a layer on top of the standard http.ServeMux and is downward compatible
mux := seven5.NewServeMux(nil)
//maps a user resource at "user" with wire type from above and implementation provided by the
//basic user support. users are accessed at /rest/user
bd.Resource("user", &MyUserWire{}, mgr.UserResource())
//maps the "meta" user resource into the url space at /rest/usermeta
//bd.Resource("usermeta", userManager.MetaWire(), userManager.MetaImplementation())
//we create an Auth Dispatcher, and use the defaults to map it to /auth/PROVIDERNAME/... in the
//URL space
auth := seven5.AuthDispatcherFromBase(bd, mux)
//we use google for login and user data
auth.AddProvider(goog)
//everything is set, just run the normal listen and route loop
http.ListenAndServe(fmt.Sprintf(":%d",heroku.Port()), mux)
//doesn't return
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment