Files
galaxy-game/user/internal/adapters/local/id_generator.go
T
2026-04-25 23:20:55 +02:00

143 lines
4.1 KiB
Go

package local
import (
"crypto/rand"
"encoding/base32"
"fmt"
"strings"
"galaxy/user/internal/domain/common"
"galaxy/user/internal/domain/entitlement"
"galaxy/user/internal/domain/policy"
)
var base32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
// userNameSuffixAlphabet is the Crockford lowercase Base32 alphabet with
// `i`, `l`, `o`, and `u` excluded to avoid visual confusables. The chosen
// 32 characters also keep each byte pair aligned with a 5-bit group so the
// 5-byte random source encodes into exactly eight suffix characters.
const userNameSuffixAlphabet = "0123456789abcdefghjkmnpqrstvwxyz"
const userNameSuffixLength = 8
// IDGenerator creates opaque stable user identifiers and generated initial
// user names.
type IDGenerator struct{}
// NewUserID returns one newly generated opaque user identifier.
func (IDGenerator) NewUserID() (common.UserID, error) {
token, err := randomToken(10)
if err != nil {
return "", fmt.Errorf("generate user id: %w", err)
}
userID := common.UserID("user-" + token)
if err := userID.Validate(); err != nil {
return "", fmt.Errorf("generate user id: %w", err)
}
return userID, nil
}
// NewUserName returns one generated user name in the `player-<suffix>` form.
// The suffix is eight characters drawn from the Crockford lowercase Base32
// alphabet (confusable-free: `i`, `l`, `o`, `u` are excluded).
func (IDGenerator) NewUserName() (common.UserName, error) {
suffix, err := randomSuffix(userNameSuffixLength)
if err != nil {
return "", fmt.Errorf("generate user name: %w", err)
}
userName := common.UserName("player-" + suffix)
if err := userName.Validate(); err != nil {
return "", fmt.Errorf("generate user name: %w", err)
}
return userName, nil
}
// NewEntitlementRecordID returns one generated entitlement history record
// identifier.
func (IDGenerator) NewEntitlementRecordID() (entitlement.EntitlementRecordID, error) {
token, err := randomToken(10)
if err != nil {
return "", fmt.Errorf("generate entitlement record id: %w", err)
}
recordID := entitlement.EntitlementRecordID("entitlement-" + token)
if err := recordID.Validate(); err != nil {
return "", fmt.Errorf("generate entitlement record id: %w", err)
}
return recordID, nil
}
// NewSanctionRecordID returns one generated sanction history record
// identifier.
func (IDGenerator) NewSanctionRecordID() (policy.SanctionRecordID, error) {
token, err := randomToken(10)
if err != nil {
return "", fmt.Errorf("generate sanction record id: %w", err)
}
recordID := policy.SanctionRecordID("sanction-" + token)
if err := recordID.Validate(); err != nil {
return "", fmt.Errorf("generate sanction record id: %w", err)
}
return recordID, nil
}
// NewLimitRecordID returns one generated limit history record identifier.
func (IDGenerator) NewLimitRecordID() (policy.LimitRecordID, error) {
token, err := randomToken(10)
if err != nil {
return "", fmt.Errorf("generate limit record id: %w", err)
}
recordID := policy.LimitRecordID("limit-" + token)
if err := recordID.Validate(); err != nil {
return "", fmt.Errorf("generate limit record id: %w", err)
}
return recordID, nil
}
func randomToken(size int) (string, error) {
buffer := make([]byte, size)
if _, err := rand.Read(buffer); err != nil {
return "", err
}
return strings.ToLower(base32NoPadding.EncodeToString(buffer)), nil
}
// randomSuffix returns a length-character suffix encoded from crypto-random
// bytes through the userNameSuffixAlphabet. Each character consumes five
// random bits, so the caller receives `ceil(length * 5 / 8)` bytes of
// entropy in the underlying buffer.
func randomSuffix(length int) (string, error) {
byteCount := (length*5 + 7) / 8
buffer := make([]byte, byteCount)
if _, err := rand.Read(buffer); err != nil {
return "", err
}
encoded := make([]byte, length)
for index := range encoded {
bitOffset := index * 5
byteIndex := bitOffset / 8
shift := bitOffset % 8
value := uint16(buffer[byteIndex]) << 8
if byteIndex+1 < len(buffer) {
value |= uint16(buffer[byteIndex+1])
}
encoded[index] = userNameSuffixAlphabet[(value>>(16-5-shift))&0x1F]
}
return string(encoded), nil
}