internal/store/registration.go

aa6d5bfab86d3163d485be9d89b313c2b0546009
gitbay/internal/store/registration.go history · blame · raw

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}