From 4bc088fd821075aa48e0b532f9b5dd3ca4c4a002 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Tue, 25 Aug 2026 15:44:02 +0300 Subject: [PATCH] deliberi/db: enforce canonical unique emails and add token helpers --- deliberi/db/db.go | 36 ++++++++++++++++++++++++++++++++++++ deliberi/db/email.go | 39 ++++++++++++++++++++++++++++----------- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/deliberi/db/db.go b/deliberi/db/db.go index 12806332..621823ed 100644 --- a/deliberi/db/db.go +++ b/deliberi/db/db.go @@ -148,9 +148,45 @@ func runMigrations(conn *sql.Conn, logger *slog.Logger) error { }); err != nil { return err } + if err := orm.RunMigration(conn, logger, "email-canonical-unique", func(tx *sql.Tx) error { + // legacy appview data could hold case-variant or repeated addresses; keep + // one row per canonical address: verified wins, else primary, else earliest + if _, err := tx.Exec(` + delete from emails where id not in ( + select id from ( + select id, row_number() over ( + partition by lower(trim(email)) + order by verified desc, is_primary desc, id asc + ) as rn + from emails + ) where rn = 1 + ) + `); err != nil { + return err + } + // survivors keep legacy casing; normalize so handler lookups match + if _, err := tx.Exec(`update emails set email = lower(trim(email))`); err != nil { + return err + } + // legacy AddEmail could auto-promote an unverified first row; the + // invariant is verified-only primary + if _, err := tx.Exec(`update emails set is_primary = false where verified = false`); err != nil { + return err + } + _, err := tx.Exec(`create unique index if not exists emails_email_lower_unique on emails (lower(email))`) + return err + }); err != nil { + return err + } return nil } func isColumnExistsErr(err error) bool { return err != nil && strings.Contains(err.Error(), "duplicate column name") } + +// reports whether err is a sqlite unique-constraint violation (e.g. the +// lower(email) reservation index) +func IsUniqueConstraintErr(err error) bool { + return err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed") +} diff --git a/deliberi/db/email.go b/deliberi/db/email.go index c3d98311..eaf3ae88 100644 --- a/deliberi/db/email.go +++ b/deliberi/db/email.go @@ -71,9 +71,6 @@ func GetDidForEmail(e Execer, em string) (string, error) { return did, nil } -// GetEmailToDid maps committer emails to dids, optionally restricting to -// verified emails. did-prefixed inputs pass through as already-resolved. this -// is what resolves git committer emails to tangled accounts. func GetEmailToDid(e Execer, emails []string, isVerifiedFilter bool) (map[string]string, error) { if len(emails) == 0 { return make(map[string]string), nil @@ -170,18 +167,19 @@ func CheckEmailExistsAtAll(e Execer, email string) (bool, error) { return count > 0, nil } -func CheckValidVerificationCode(e Execer, did string, email string, code string) (bool, error) { +func GetEmailByToken(e Execer, token string) (models.Email, error) { query := ` - select count(*) + select did, email, verified, is_primary, verification_code from emails - where did = ? and email = ? and verification_code = ? + where verification_code = ? + limit 1 ` - var count int - err := e.QueryRow(query, did, email, code).Scan(&count) + var email models.Email + err := e.QueryRow(query, token).Scan(&email.Did, &email.Address, &email.Verified, &email.Primary, &email.VerificationCode) if err != nil { - return false, err + return models.Email{}, err } - return count > 0, nil + return email, nil } func AddEmail(e Execer, email models.Email) error { @@ -196,7 +194,6 @@ func AddEmail(e Execer, email models.Email) error { return err } - // first email for a did becomes primary if count == 0 { email.Primary = true } @@ -283,6 +280,26 @@ func GetAllEmails(e Execer, did string) ([]models.Email, error) { return emails, nil } +func InsertUnverifiedEmail(e Execer, did string, email string, code string) error { + query := ` + insert into emails (did, email, verified, is_primary, verification_code) + values (?, ?, 0, 0, ?) + ` + _, err := e.Exec(query, did, email, code) + return err +} + +func CountVerifiedEmails(e Execer, did string) (int, error) { + query := ` + select count(*) + from emails + where did = ? and verified = true + ` + var count int + err := e.QueryRow(query, did).Scan(&count) + return count, err +} + func UpdateVerificationCode(e Execer, did string, email string, code string) error { query := ` update emails -- 2.51.2