Extend usernameRE to [a-z0-9_.-], preserve dots in SanitizeForUsername, and add tests for validation, sanitization, and nip05 sync precedence. Co-authored-by: Cursor <cursoragent@cursor.com>
76 lines
1.9 KiB
Go
76 lines
1.9 KiB
Go
package user
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type SubscriptionType string
|
|
|
|
const (
|
|
SubYearly SubscriptionType = "yearly"
|
|
SubLifetime SubscriptionType = "lifetime"
|
|
)
|
|
|
|
func (s SubscriptionType) Valid() bool {
|
|
return s == SubYearly || s == SubLifetime
|
|
}
|
|
|
|
type User struct {
|
|
ID int64
|
|
Pubkey string
|
|
Username string
|
|
SubscriptionType SubscriptionType
|
|
ExpiresAt *time.Time
|
|
IsActive bool
|
|
ManualUsername bool
|
|
LastSyncedAt *time.Time
|
|
ExpiringReminderSentAt *time.Time
|
|
DeactivatedAt *time.Time
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
func (u *User) IsLifetime() bool { return u.SubscriptionType == SubLifetime }
|
|
|
|
func (u *User) InGrace() bool { return !u.IsActive && u.DeactivatedAt != nil }
|
|
|
|
var (
|
|
ErrInvalidUsername = errors.New("invalid username")
|
|
ErrUserNotFound = errors.New("user not found")
|
|
ErrUsernameTaken = errors.New("username taken")
|
|
)
|
|
|
|
// Username rules: 1-30 chars, [a-z0-9_.-], lowercase, must start with alnum.
|
|
// Dot is allowed per NIP-05 local-part spec.
|
|
var usernameRE = regexp.MustCompile(`^[a-z0-9][a-z0-9_.-]{0,29}$`)
|
|
|
|
func ValidateUsername(name string, reserved []string) error {
|
|
name = strings.ToLower(strings.TrimSpace(name))
|
|
if !usernameRE.MatchString(name) {
|
|
return ErrInvalidUsername
|
|
}
|
|
for _, r := range reserved {
|
|
if strings.EqualFold(name, strings.TrimSpace(r)) {
|
|
return ErrInvalidUsername
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NormalizeUsername(name string) string {
|
|
return strings.ToLower(strings.TrimSpace(name))
|
|
}
|
|
|
|
// ProvisionalUsername returns a deterministic placeholder handle for a pubkey
|
|
// when the caller did not supply one. The format `u_<first 16 hex>` keeps the
|
|
// result inside the 30-char username rule and matches usernameRE.
|
|
func ProvisionalUsername(pubkey string) string {
|
|
hex := strings.ToLower(strings.TrimSpace(pubkey))
|
|
if len(hex) > 16 {
|
|
hex = hex[:16]
|
|
}
|
|
return "u_" + hex
|
|
}
|