Another service we are going to make use of is the Google App Engine Users API, which provides the authentication of Google accounts (and Google Apps accounts).
Create a new file called users.go
and add the following code:
type User struct { Key *datastore.Key `json:"id" datastore:"-"` UserID string `json:"-"` DisplayName string `json:"display_name"` AvatarURL string `json:"avatar_url"` Score int `json:"score"` }
Similar to the Question
struct, we have Key
and a few fields that make up the User
entity. This struct represents an object that belongs to our application that describes a user; we will have one for every authenticated user in our system, but this isn't the same user object that we'll get from the Users API.
Importing the https://godoc.org/google.golang.org/appengine/user package and calling the user.Current(context.Context)
function will return either nil (if no user is authenticated) or a user.User
object. This object belongs to the Users API and isn't suitable for our data store, so we need to write a helper function that will translate the App Engine user into our User
.
Add the following code to users.go
:
func UserFromAEUser(ctx context.Context) (*User, error) { aeuser := user.Current(ctx) if aeuser == nil { return nil, errors.New("not logged in") } var appUser User appUser.Key = datastore.NewKey(ctx, "User", aeuser.ID, 0, nil) err := datastore.Get(ctx, appUser.Key, &appUser) if err != nil && err != datastore.ErrNoSuchEntity { return nil, err } if err == nil { return &appUser, nil } appUser.UserID = aeuser.ID appUser.DisplayName = aeuser.String() appUser.AvatarURL = gravatarURL(aeuser.Email) log.Infof(ctx, "saving new user: %s", aeuser.String()) appUser.Key, err = datastore.Put(ctx, appUser.Key, &appUser) if err != nil { return nil, err } return &appUser, nil }
We get the currently authenticated user by calling user.Current
, and if it is nil
, we return with an error. This means that the user is not logged in and the operation cannot complete. Our web package will be checking and ensuring that users are logged in for us, so by the time they hit an API endpoint, we'll expect them to be authenticated.
We then create a new appUser
variable (which is of our User
type) and set datastore.Key
. This time, we aren't making an incomplete key; instead, we are using datastore.NewKey
and specifying a string ID, matching the User API ID. This key predictability means that not only will there only be one User
entity per authenticated user in our application, but it also allows us to load a User
entity without having to use a query.
We then call datastore.Get
to attempt to load the User
entity. If this is the first time the user has logged in, there will be no entity and the returned error will be the special datastore.ErrNoSuchEntity
variable. If that's the case, we set the appropriate fields and use datastore.Put
to save it. Otherwise, we just return the loaded User
.
Note that we are checking for early returns in this function. This is to ensure that it is easy to read the execution flow of our code without having to follow it in and out of indented blocks. I call this the line of sight of code and have written about it on my blog at https://medium.com/@matryer.
For now, we'll use Gravatar again for avatar pictures, so add the following helper function to the bottom of users.go
:
func gravatarURL(email string) string { m := md5.New() io.WriteString(m, strings.ToLower(email)) return fmt.Sprintf("//www.gravatar.com/avatar/%x", m.Sum(nil)) }
If you recall, our Question type doesn't take the author as User
; rather, the type was UserCard
. When we embed denormalized data into other entities, sometimes we will want them to look slightly different from the master entity. In our case, since we do not store the key in the User
entity (remember the Key
fields have datastore:"-"
), we need to have a new type that stores the key.
At the bottom of users.go
, add the UserCard
struct and the associated helper method for User
:
type UserCard struct { Key *datastore.Key `json:"id"` DisplayName string `json:"display_name"` AvatarURL string `json:"avatar_url"` } func (u User) Card() UserCard { return UserCard{ Key: u.Key, DisplayName: u.DisplayName, AvatarURL: u.AvatarURL, } }
Note that UserCard
doesn't specify a datastore
tag, so the Key
field will indeed be persisted in the data store. Our Card()
helper function just builds and returns UserCard
by copying the values of each field. This seems wasteful but offers great control, especially if you want embedded data to look very different from its original entity.
3.144.47.208