package iam import ( "context" "database/sql/driver" "fmt" "atlas9.dev/c/core" "golang.org/x/crypto/bcrypt" ) // PasswordStore is implemented by the app to persist password hashes. type PasswordStore interface { SetHash(ctx context.Context, userID core.ID, hash PasswordHash) error GetHash(ctx context.Context, userID core.ID) (PasswordHash, error) } func SetPassword(ctx context.Context, store PasswordStore, userID core.ID, password string) error { b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } hash := PasswordHash{hash: string(b)} return store.SetHash(ctx, userID, hash) } func CheckPassword(ctx context.Context, store PasswordStore, userID core.ID, password string) error { hash, err := store.GetHash(ctx, userID) if err != nil { return err } return bcrypt.CompareHashAndPassword([]byte(hash.hash), []byte(password)) } // PasswordHash is an opaque type representing a hashed password. // It implements driver.Valuer and sql.Scanner for database storage. type PasswordHash struct { hash string } func NewPasswordHash(hash string) PasswordHash { return PasswordHash{hash: hash} } func (p PasswordHash) Value() (driver.Value, error) { return p.hash, nil } func (p *PasswordHash) Scan(src any) error { switch v := src.(type) { case string: p.hash = v case []byte: p.hash = string(v) default: return fmt.Errorf("PasswordHash.Scan: unsupported type %T", src) } return nil } func (p PasswordHash) String() string { return p.hash } func (p PasswordHash) MarshalJSON() ([]byte, error) { return []byte(`""`), nil } func (p *PasswordHash) UnmarshalJSON([]byte) error { return nil } func (p PasswordHash) MarshalText() ([]byte, error) { return []byte(""), nil } func (p *PasswordHash) UnmarshalText([]byte) error { return nil }