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