internal/store/registration.go
208 lines · 6826 bytes
11 symbols in this file
1package store
2
3import (
4 "errors"
5 "time"
6)
7
8// CreateInvite stores an invite code hash bound to an email address.
9func (s *Store) CreateInvite(codeHash, email string) error {
10 _, err := s.DB.Exec("INSERT INTO invites (code_hash, email) VALUES (?, ?)", codeHash, email)
11 return err
12}
13
14// ConsumeInvite redeems an invite exactly once, returning the address it was
15// issued for. Used and unknown codes fail identically.
16func (s *Store) ConsumeInvite(codeHash string) (string, error) {
17 res, err := s.DB.Exec(
18 "UPDATE invites SET used_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE code_hash = ? AND used_at IS NULL",
19 codeHash)
20 if err != nil {
21 return "", err
22 }
23 if n, _ := res.RowsAffected(); n == 0 {
24 return "", ErrNotFound
25 }
26 var email string
27 err = s.DB.QueryRow("SELECT email FROM invites WHERE code_hash = ?", codeHash).Scan(&email)
28 return email, err
29}
30
31// CreateEmailToken stores a verification code hash for one address.
32func (s *Store) CreateEmailToken(userID int64, address, tokenHash string, ttl time.Duration) error {
33 _, err := s.DB.Exec(
34 "INSERT INTO email_tokens (token_hash, user_id, address, expires_at) VALUES (?, ?, ?, ?)",
35 tokenHash, userID, address, fmtTime(time.Now().Add(ttl)))
36 return err
37}
38
39// CountEmailTokensSince is how many verification codes an account has
40// asked for since a moment, used or not.
41func (s *Store) CountEmailTokensSince(userID int64, since time.Time) (int, error) {
42 var n int
43 err := s.DB.QueryRow("SELECT count(*) FROM email_tokens WHERE user_id = ? AND created_at > ?",
44 userID, fmtTime(since)).Scan(&n)
45 return n, err
46}
47
48// ConsumeEmailToken redeems a verification code for the given user.
49func (s *Store) ConsumeEmailToken(userID int64, tokenHash string) (string, error) {
50 res, err := s.DB.Exec(`
51 UPDATE email_tokens SET used_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
52 WHERE token_hash = ? AND user_id = ? AND used_at IS NULL AND expires_at > ?`,
53 tokenHash, userID, fmtTime(time.Now()))
54 if err != nil {
55 return "", err
56 }
57 if n, _ := res.RowsAffected(); n == 0 {
58 return "", ErrNotFound
59 }
60 var address string
61 err = s.DB.QueryRow("SELECT address FROM email_tokens WHERE token_hash = ?", tokenHash).Scan(&address)
62 return address, err
63}
64
65// EmailTokenBelongsToAnotherUser reports whether a live code exists but
66// is owned by someone else. It answers only yes or no: naming the owner
67// would turn a guessed code into an account oracle.
68func (s *Store) EmailTokenBelongsToAnotherUser(userID int64, tokenHash string) (bool, error) {
69 var n int
70 err := s.DB.QueryRow(`
71 SELECT COUNT(*) FROM email_tokens
72 WHERE token_hash = ? AND user_id != ? AND used_at IS NULL AND expires_at > ?`,
73 tokenHash, userID, fmtTime(time.Now())).Scan(&n)
74 return n > 0, err
75}
76
77// CreateRegisteredUser makes a self-registered account, pending until its
78// email is verified.
79func (s *Store) CreateRegisteredUser(username string, pending bool) (int64, error) {
80 if taken, err := ownerNameTaken(s.DB, username); err != nil {
81 return 0, err
82 } else if taken {
83 return 0, errors.New("that username is taken")
84 }
85 res, err := s.DB.Exec("INSERT INTO users (username, pending) VALUES (?, ?)", username, boolInt(pending))
86 if err != nil {
87 if isUniqueErr(err) {
88 return 0, errors.New("that username is taken")
89 }
90 return 0, err
91 }
92 return res.LastInsertId()
93}
94
95// ClearPending activates a pending account.
96func (s *Store) ClearPending(userID int64) error {
97 _, err := s.DB.Exec("UPDATE users SET pending = 0 WHERE id = ?", userID)
98 return err
99}
100
101// EmailInUse reports whether an address is attached to any account.
102func (s *Store) EmailInUse(address string) (bool, error) {
103 var n int
104 err := s.DB.QueryRow("SELECT COUNT(*) FROM emails WHERE address = ?", address).Scan(&n)
105 return n > 0, err
106}
107
108// RedeemInvite performs the whole invite registration in one transaction:
109// consume the code, create the user, attach the invite's email as verified,
110// register the key. Any failure rolls everything back — the invite stays
111// redeemable and no partial account exists.
112func (s *Store) RedeemInvite(codeHash, username, keyFP, keyAlgo string, keyBlob []byte) (string, error) {
113 tx, err := s.DB.Begin()
114 if err != nil {
115 return "", err
116 }
117 defer tx.Rollback()
118
119 res, err := tx.Exec(
120 "UPDATE invites SET used_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE code_hash = ? AND used_at IS NULL",
121 codeHash)
122 if err != nil {
123 return "", err
124 }
125 if n, _ := res.RowsAffected(); n == 0 {
126 return "", ErrNotFound
127 }
128 var email string
129 if err := tx.QueryRow("SELECT email FROM invites WHERE code_hash = ?", codeHash).Scan(&email); err != nil {
130 return "", err
131 }
132
133 if taken, err := ownerNameTaken(tx, username); err != nil {
134 return "", err
135 } else if taken {
136 return "", errors.New("that username is taken")
137 }
138 ures, err := tx.Exec("INSERT INTO users (username) VALUES (?)", username)
139 if err != nil {
140 return "", err
141 }
142 uid, err := ures.LastInsertId()
143 if err != nil {
144 return "", err
145 }
146 if _, err := tx.Exec(
147 `INSERT INTO emails (user_id, address, verified_at, verified_by, is_primary)
148 VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), 'smtp', 1)`, uid, email); err != nil {
149 if isUniqueErr(err) {
150 return "", errors.New("the invited address already belongs to an account")
151 }
152 return "", err
153 }
154 if _, err := tx.Exec(
155 "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, ?, ?, ?, 'full')",
156 uid, keyFP, keyAlgo, keyBlob); err != nil {
157 if isUniqueErr(err) {
158 return "", ErrDuplicateKey
159 }
160 return "", err
161 }
162 if err := bumpKeyEpoch(tx); err != nil {
163 return "", err
164 }
165 return email, tx.Commit()
166}
167
168// RegisterOpen performs open registration in one transaction: pending user,
169// unverified email, key. Failure leaves nothing behind.
170func (s *Store) RegisterOpen(username, email, keyFP, keyAlgo string, keyBlob []byte) (int64, error) {
171 tx, err := s.DB.Begin()
172 if err != nil {
173 return 0, err
174 }
175 defer tx.Rollback()
176 if taken, err := ownerNameTaken(tx, username); err != nil {
177 return 0, err
178 } else if taken {
179 return 0, errors.New("that username is taken")
180 }
181 ures, err := tx.Exec("INSERT INTO users (username, pending) VALUES (?, 1)", username)
182 if err != nil {
183 return 0, err
184 }
185 uid, err := ures.LastInsertId()
186 if err != nil {
187 return 0, err
188 }
189 if _, err := tx.Exec(
190 "INSERT INTO emails (user_id, address, is_primary) VALUES (?, ?, 1)", uid, email); err != nil {
191 if isUniqueErr(err) {
192 return 0, errors.New("that address already belongs to an account")
193 }
194 return 0, err
195 }
196 if _, err := tx.Exec(
197 "INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope) VALUES (?, ?, ?, ?, 'full')",
198 uid, keyFP, keyAlgo, keyBlob); err != nil {
199 if isUniqueErr(err) {
200 return 0, ErrDuplicateKey
201 }
202 return 0, err
203 }
204 if err := bumpKeyEpoch(tx); err != nil {
205 return 0, err
206 }
207 return uid, tx.Commit()
208}