package provision import ( "context" "errors" "fmt" "strings" "time" "github.com/google/uuid" "github.com/mrhid6/vantage/shared/models" "go.mongodb.org/mongo-driver/v2/mongo" "golang.org/x/crypto/bcrypt" ) // BcryptCost is the work factor for every password hash Vantage writes. // Changing it changes nothing about existing hashes, which carry their own cost. const BcryptCost = 12 // ErrEmailTaken is returned when the unique index on users.email rejects an insert. var ErrEmailTaken = errors.New("email already registered") // CreateUser hashes password and inserts the user. An empty password leaves the // hash empty, which is how OIDC users are stored. func CreateUser(ctx context.Context, db *mongo.Database, instanceID, email, password, role, authSource string) (*models.User, error) { var hash string if password != "" { b, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost) if err != nil { return nil, err } hash = string(b) } return CreateUserWithHash(ctx, db, instanceID, email, hash, role, authSource) } // CreateUserWithHash inserts a user whose password was already hashed // elsewhere. sitesvc hashes at signup and only holds the hash by the time the // verification link is opened. func CreateUserWithHash(ctx context.Context, db *mongo.Database, instanceID, email, passwordHash, role, authSource string) (*models.User, error) { email = strings.ToLower(strings.TrimSpace(email)) if email == "" { return nil, fmt.Errorf("email required") } if !models.ValidRole(role) { return nil, fmt.Errorf("invalid role %q", role) } u := &models.User{ UserID: uuid.NewString(), InstanceID: instanceID, Email: email, PasswordHash: passwordHash, Role: role, AuthSource: authSource, CreatedAt: time.Now().UTC(), } if _, err := db.Collection("users").InsertOne(ctx, u); err != nil { if mongo.IsDuplicateKeyError(err) { return nil, ErrEmailTaken } return nil, err } return u, nil }