internal/store/registration.go

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

208 lines · 6826 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// 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}