-
Notifications
You must be signed in to change notification settings - Fork 1
/
model.go
169 lines (152 loc) · 4.21 KB
/
model.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package model
import (
"context"
"net/http"
"github.com/flimzy/kivik"
kerrors "github.com/flimzy/kivik/errors"
"github.com/go-kivik/couchdb/chttp"
"github.com/pkg/errors"
fb "github.com/FlashbackSRS/flashback-model"
"github.com/FlashbackSRS/flashback/oauth2/auth"
)
// stateDB is a local database, which is never synced, for storing of persistent
// state.
const stateDB = "flashback"
// currentUserDoc is the doc ID for storing the current user state.
const currentUserDoc = "_local/currentUser"
// Repo represents an instance of the Couch/Pouch model.
type Repo struct {
appURL string
chttp *chttp.Client
remote kivikClient
local kivikClient
state kivikDB
// user is the username, without the "user-" prefix
user string
}
// New returns a new Repo instance, pointing to the specified remote server.
func New(ctx context.Context, remoteURL, appURL string) (*Repo, error) {
remoteClient, err := remoteConnection(remoteURL)
if err != nil {
return nil, err
}
httpClient, err := chttp.New(ctx, remoteURL)
if err != nil {
return nil, err
}
localClient, err := localConnection()
if err != nil {
return nil, err
}
if e := localClient.CreateDB(ctx, stateDB); e != nil && kivik.StatusCode(e) != kivik.StatusConflict {
return nil, e
}
stateDB, err := localClient.DB(ctx, stateDB)
if err != nil {
return nil, err
}
setTransport(httpClient)
return &Repo{
chttp: httpClient,
remote: remoteClient,
local: localClient,
state: stateDB,
appURL: appURL,
}, nil
}
// Auth attempts to authenticate with the provided OAuth2 provider/token pair.
func (r *Repo) Auth(ctx context.Context, provider, token string) error {
auth := auth.NewOAuth2(provider, token)
if err := r.chttp.Auth(ctx, auth); err != nil {
return errors.Wrap(err, "OAuth2 auth failed")
}
var response struct {
Ctx struct {
Name string `json:"name"`
} `json:"userCtx"`
}
if _, err := r.chttp.DoJSON(ctx, http.MethodGet, "/_session", nil, &response); err != nil {
return errors.Wrap(err, "failed to validate session")
}
if response.Ctx.Name == "" {
return errors.New("no user set in session")
}
return r.setUser(ctx, response.Ctx.Name)
}
type user struct {
ID string `json:"_id"`
Rev string `json:"_rev,omitempty"`
Username string `json:"username"`
}
func (r *Repo) fetchUser(ctx context.Context) (user, error) {
var u user
row, err := r.state.Get(ctx, currentUserDoc)
if err != nil {
return user{}, err
}
if e := row.ScanDoc(&u); e != nil {
return user{}, e
}
return u, nil
}
func (r *Repo) setUser(ctx context.Context, username string) error {
u, err := r.fetchUser(ctx)
if err != nil && kivik.StatusCode(err) != kivik.StatusNotFound {
return err
}
u.ID = currentUserDoc
u.Username = username
if _, e := r.state.Put(ctx, currentUserDoc, u); e != nil {
return errors.Wrap(e, "failed to store local state")
}
r.user = username
return nil
}
// Logout clears the auth session.
func (r *Repo) Logout(ctx context.Context) error {
if err := r.chttp.Logout(ctx); err != nil {
return err
}
r.user = ""
if _, e := r.state.Delete(ctx, currentUserDoc, ""); e != nil {
return e
}
return nil
}
// ErrNotLoggedIn is returned by CurrentUser if no user is logged in.
var ErrNotLoggedIn = kerrors.Status(kivik.StatusUnauthorized, "not logged in")
// CurrentUser returns the currently registered user.
func (r *Repo) CurrentUser() (string, error) {
if r.user == "" {
return "", ErrNotLoggedIn
}
return r.user, nil
}
func (r *Repo) newDB(ctx context.Context, dbName string) (kivikDB, error) {
if _, err := r.CurrentUser(); err != nil {
return nil, err
}
return r.local.DB(ctx, dbName)
}
func (r *Repo) userDB(ctx context.Context) (kivikDB, error) {
user, err := r.CurrentUser()
if err != nil {
return nil, err
}
return r.newDB(ctx, "user-"+user)
}
func (r *Repo) bundleDB(ctx context.Context, bundle *fb.Bundle) (kivikDB, error) {
if _, err := r.CurrentUser(); err != nil {
return nil, err
}
if bundle == nil {
return nil, errors.New("nil bundle")
}
if err := bundle.Validate(); err != nil {
return nil, errors.Wrap(err, "invalid bundle")
}
if err := r.local.CreateDB(ctx, bundle.ID); err != nil && kivik.StatusCode(err) != kivik.StatusPreconditionFailed {
return nil, err
}
return r.newDB(ctx, bundle.ID)
}