Packages app and gfit: Unify HTTP retries with retry.Transport.
[kraftakt.git] / app / user.go
1 package app
2
3 import (
4         "context"
5         "crypto/hmac"
6         "crypto/sha1"
7         "encoding/hex"
8         "fmt"
9         "net/http"
10         "sync"
11
12         "github.com/google/uuid"
13         "github.com/octo/retry"
14         legacy_context "golang.org/x/net/context"
15         "golang.org/x/oauth2"
16         "google.golang.org/appengine/datastore"
17         "google.golang.org/appengine/log"
18 )
19
20 type User struct {
21         key *datastore.Key
22
23         ID    string
24         Email string
25 }
26
27 type dbUser struct {
28         ID string
29 }
30
31 func NewUser(ctx context.Context, email string) (*User, error) {
32         var id string
33         err := datastore.RunInTransaction(ctx, func(ctx legacy_context.Context) error {
34                 key := datastore.NewKey(ctx, "User", email, 0, nil)
35
36                 var u dbUser
37                 err := datastore.Get(ctx, key, &u)
38                 if err != nil && err != datastore.ErrNoSuchEntity {
39                         return err
40                 }
41                 if err == nil {
42                         id = u.ID
43                         return nil
44                 }
45
46                 id = uuid.New().String()
47                 _, err = datastore.Put(ctx, key, &dbUser{
48                         ID: id,
49                 })
50                 return err
51         }, nil)
52         if err != nil {
53                 return nil, err
54         }
55
56         return &User{
57                 key:   datastore.NewKey(ctx, "User", email, 0, nil),
58                 ID:    id,
59                 Email: email,
60         }, nil
61 }
62
63 func UserByID(ctx context.Context, id string) (*User, error) {
64         q := datastore.NewQuery("User").Filter("ID=", id).KeysOnly()
65         keys, err := q.GetAll(ctx, nil)
66         if err != nil {
67                 return nil, fmt.Errorf("datastore.Query.GetAll(): %v", err)
68         }
69         if len(keys) != 1 {
70                 return nil, fmt.Errorf("len(keys) = %d, want 1", len(keys))
71         }
72
73         return &User{
74                 key:   keys[0],
75                 ID:    id,
76                 Email: keys[0].StringID(),
77         }, nil
78 }
79
80 func (u *User) Token(ctx context.Context, svc string) (*oauth2.Token, error) {
81         key := datastore.NewKey(ctx, "Token", svc, 0, u.key)
82
83         var tok oauth2.Token
84         if err := datastore.Get(ctx, key, &tok); err != nil {
85                 return nil, err
86         }
87
88         return &tok, nil
89 }
90
91 func (u *User) SetToken(ctx context.Context, svc string, tok *oauth2.Token) error {
92         key := datastore.NewKey(ctx, "Token", svc, 0, u.key)
93         _, err := datastore.Put(ctx, key, tok)
94         return err
95 }
96
97 func (u *User) DeleteToken(ctx context.Context, svc string) error {
98         key := datastore.NewKey(ctx, "Token", svc, 0, u.key)
99         return datastore.Delete(ctx, key)
100 }
101
102 func (u *User) OAuthClient(ctx context.Context, svc string, cfg *oauth2.Config) (*http.Client, error) {
103         key := datastore.NewKey(ctx, "Token", svc, 0, u.key)
104
105         var tok oauth2.Token
106         if err := datastore.Get(ctx, key, &tok); err != nil {
107                 return nil, fmt.Errorf("datastore.Get(%v) = %v", key, err)
108         }
109
110         src := cfg.TokenSource(ctx, &tok)
111         c := oauth2.NewClient(ctx, &persistingTokenSource{
112                 ctx: ctx,
113                 t:   &tok,
114                 src: src,
115                 key: key,
116         })
117         c.Transport = retry.Transport{
118                 RoundTripper: c.Transport,
119         }
120
121         return c, nil
122 }
123
124 func (u *User) String() string {
125         return u.Email
126 }
127
128 func (u *User) Sign(payload string) string {
129         mac := hmac.New(sha1.New, []byte(u.ID))
130         mac.Write([]byte(payload))
131
132         return hex.EncodeToString(mac.Sum(nil))
133 }
134
135 type persistingTokenSource struct {
136         ctx context.Context
137         t   *oauth2.Token
138         src oauth2.TokenSource
139         key *datastore.Key
140
141         sync.Mutex
142 }
143
144 func (s *persistingTokenSource) Token() (*oauth2.Token, error) {
145         s.Lock()
146         defer s.Unlock()
147
148         tok, err := s.src.Token()
149         if err != nil {
150                 return nil, err
151         }
152
153         if s.t.AccessToken != tok.AccessToken ||
154                 s.t.TokenType != tok.TokenType ||
155                 s.t.RefreshToken != tok.RefreshToken ||
156                 !s.t.Expiry.Equal(tok.Expiry) {
157                 if _, err := datastore.Put(s.ctx, s.key, tok); err != nil {
158                         log.Errorf(s.ctx, "persisting OAuth token in datastore failed: %v", err)
159                 }
160         }
161
162         s.t = tok
163         return tok, nil
164 }