thread-master/apps/backend/internal/module/member/usecase/password.go

99 lines
2.4 KiB
Go
Raw Permalink Normal View History

2026-07-10 12:54:45 +00:00
package usecase
import (
"crypto/rand"
"math/big"
"unicode"
"apps/backend/internal/module/member/domain"
"golang.org/x/crypto/bcrypt"
)
// ValidatePasswordPolicy≥12、含大寫、小寫、數字、符號。
// 規則與 apps/web/src/lib/passwordPolicy.ts 一致;文案見 domain.PasswordPolicyEN/ZH。
func ValidatePasswordPolicy(password string) error {
if password == "" || len([]rune(password)) < domain.PasswordMinLen {
return domain.ErrWeakPassword
}
var upper, lower, digit, symbol bool
for _, r := range password {
switch {
case unicode.IsUpper(r):
upper = true
case unicode.IsLower(r):
lower = true
case unicode.IsDigit(r):
digit = true
case unicode.IsPunct(r) || unicode.IsSymbol(r):
symbol = true
default:
// 空白等非字母數字也算符號類
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
symbol = true
}
}
}
if !upper || !lower || !digit || !symbol {
return domain.ErrWeakPassword
}
return nil
}
// HashPassword validates policy then bcrypt.
func HashPassword(password string, cost int) (string, error) {
if err := ValidatePasswordPolicy(password); err != nil {
return "", err
}
if cost < bcrypt.MinCost {
cost = bcrypt.DefaultCost
}
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
return string(bytes), err
}
func CheckPasswordHash(password, hash string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}
func GetHashingCost(hashedPassword []byte) int {
cost, _ := bcrypt.Cost(hashedPassword)
return cost
}
// GenerateTempPassword 產生符合政策的臨時密碼16 字元)。
func GenerateTempPassword() string {
const (
lower = "abcdefghijkmnopqrstuvwxyz"
upper = "ABCDEFGHJKLMNPQRSTUVWXYZ"
digits = "23456789"
symbols = "!@#$%^&*+-="
all = lower + upper + digits + symbols
)
pick := func(set string) byte {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(set))))
if err != nil {
return set[0]
}
return set[n.Int64()]
}
// 保證四類各至少 1
out := []byte{
pick(lower), pick(upper), pick(digits), pick(symbols),
}
// 16 ≥ PasswordMinLen(12)
for len(out) < 16 {
out = append(out, pick(all))
}
// shuffle
for i := len(out) - 1; i > 0; i-- {
jBig, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
j := 0
if err == nil {
j = int(jBig.Int64())
}
out[i], out[j] = out[j], out[i]
}
return string(out)
}