package db import ( "context" "crypto/rand" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "codeberg.org/pronounscc/pronouns.cc/backend/common" "emperror.dev/errors" "github.com/georgysavva/scany/v2/pgxscan" ) type UserEmail struct { ID common.EmailID UserID common.UserID EmailAddress string } func (db *DB) UserEmails(ctx context.Context, userID common.UserID) (es []UserEmail, err error) { sql, args, err := sq.Select("*").From("user_emails").Where("user_id = ?", userID).OrderBy("id").ToSql() if err != nil { return nil, errors.Wrap(err, "building query") } err = pgxscan.Select(ctx, db, &es, sql, args...) if err != nil { return nil, errors.Wrap(err, "executing query") } return NotNull(es), nil } // UserByEmail gets a user by their email address. func (db *DB) UserByEmail(ctx context.Context, email string) (u User, err error) { sql, args, err := sq.Select("users.*"). From("user_emails"). Join("users ON user_emails.user_id = users.snowflake_id"). Where("email_address = ?", email). ToSql() if err != nil { return u, errors.Wrap(err, "building query") } err = pgxscan.Get(ctx, db, &u, sql, args...) if err != nil { if errors.Cause(err) == pgx.ErrNoRows { return u, ErrUserNotFound } return u, errors.Wrap(err, "executing query") } return u, nil } // EmailExists returns whether an email address already exists. It does not need to be comfirmed. func (db *DB) EmailExists(ctx context.Context, email string) (exists bool, err error) { err = db.QueryRow(ctx, "select exists(SELECT * FROM user_emails WHERE email = $1)", email).Scan(&exists) return exists, err } const ErrEmailInUse = errors.Sentinel("email already in use") // AddEmail adds a new email to the database, and generates a confirmation token for it. func (db *DB) AddEmail(ctx context.Context, tx pgx.Tx, userID common.UserID, email string) (e UserEmail, err error) { sql, args, err := sq.Insert("user_emails").SetMap(map[string]any{ "id": common.GenerateID(), "user_id": userID, "email_address": email, }).Suffix("RETURNING *").ToSql() if err != nil { return e, errors.Wrap(err, "building query") } err = pgxscan.Get(ctx, tx, &e, sql, args...) if err != nil { pge := &pgconn.PgError{} if errors.As(err, &pge) { // unique constraint violation if pge.Code == uniqueViolation { return e, ErrEmailInUse } } return e, errors.Wrap(err, "executing query") } return e, nil } func (db *DB) SetPassword(ctx context.Context, tx pgx.Tx, userID common.UserID, password string) (err error) { salt := make([]byte, 16) _, err = rand.Read(salt) if err != nil { return errors.Wrap(err, "creating salt") } hash := hashPassword([]byte(password), salt) sql, args, err := sq.Update("users").Set("password", hash).Set("salt", salt).Where("snowflake_id = ?", userID).ToSql() if err != nil { return errors.Wrap(err, "building sql") } _, err = tx.Exec(ctx, sql, args...) return errors.Wrap(err, "executing query") }