package app import ( "context" "crypto/hmac" "crypto/sha1" "encoding/hex" "fmt" "net/http" "sync" "github.com/google/uuid" "github.com/octo/retry" legacy_context "golang.org/x/net/context" "golang.org/x/oauth2" "google.golang.org/appengine/datastore" "google.golang.org/appengine/log" ) type User struct { key *datastore.Key ID string Email string } type dbUser struct { ID string } func NewUser(ctx context.Context, email string) (*User, error) { var id string err := datastore.RunInTransaction(ctx, func(ctx legacy_context.Context) error { key := datastore.NewKey(ctx, "User", email, 0, nil) var u dbUser err := datastore.Get(ctx, key, &u) if err != nil && err != datastore.ErrNoSuchEntity { return err } if err == nil { id = u.ID return nil } id = uuid.New().String() _, err = datastore.Put(ctx, key, &dbUser{ ID: id, }) return err }, nil) if err != nil { return nil, err } return &User{ key: datastore.NewKey(ctx, "User", email, 0, nil), ID: id, Email: email, }, nil } func UserByID(ctx context.Context, id string) (*User, error) { q := datastore.NewQuery("User").Filter("ID=", id).KeysOnly() keys, err := q.GetAll(ctx, nil) if err != nil { return nil, fmt.Errorf("datastore.Query.GetAll(): %v", err) } if len(keys) != 1 { return nil, fmt.Errorf("len(keys) = %d, want 1", len(keys)) } return &User{ key: keys[0], ID: id, Email: keys[0].StringID(), }, nil } func (u *User) Token(ctx context.Context, svc string) (*oauth2.Token, error) { key := datastore.NewKey(ctx, "Token", svc, 0, u.key) var tok oauth2.Token if err := datastore.Get(ctx, key, &tok); err != nil { return nil, err } return &tok, nil } func (u *User) SetToken(ctx context.Context, svc string, tok *oauth2.Token) error { key := datastore.NewKey(ctx, "Token", svc, 0, u.key) _, err := datastore.Put(ctx, key, tok) return err } func (u *User) DeleteToken(ctx context.Context, svc string) error { key := datastore.NewKey(ctx, "Token", svc, 0, u.key) return datastore.Delete(ctx, key) } func (u *User) OAuthClient(ctx context.Context, svc string, cfg *oauth2.Config) (*http.Client, error) { key := datastore.NewKey(ctx, "Token", svc, 0, u.key) var tok oauth2.Token if err := datastore.Get(ctx, key, &tok); err != nil { return nil, fmt.Errorf("datastore.Get(%v) = %v", key, err) } src := cfg.TokenSource(ctx, &tok) c := oauth2.NewClient(ctx, &persistingTokenSource{ ctx: ctx, t: &tok, src: src, key: key, }) c.Transport = retry.Transport{ RoundTripper: c.Transport, } return c, nil } func (u *User) String() string { return u.Email } func (u *User) Sign(payload string) string { mac := hmac.New(sha1.New, []byte(u.ID)) mac.Write([]byte(payload)) return hex.EncodeToString(mac.Sum(nil)) } type persistingTokenSource struct { ctx context.Context t *oauth2.Token src oauth2.TokenSource key *datastore.Key sync.Mutex } func (s *persistingTokenSource) Token() (*oauth2.Token, error) { s.Lock() defer s.Unlock() tok, err := s.src.Token() if err != nil { return nil, err } if s.t.AccessToken != tok.AccessToken || s.t.TokenType != tok.TokenType || s.t.RefreshToken != tok.RefreshToken || !s.t.Expiry.Equal(tok.Expiry) { if _, err := datastore.Put(s.ctx, s.key, tok); err != nil { log.Errorf(s.ctx, "persisting OAuth token in datastore failed: %v", err) } } s.t = tok return tok, nil }