150 lines
3.1 KiB
Go
150 lines
3.1 KiB
Go
package domain
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type UserRole string
|
|
|
|
const (
|
|
UserRoleAdmin UserRole = "ADMIN"
|
|
UserRoleBuyer UserRole = "BUYER"
|
|
|
|
MaxUsernameBytes = 128
|
|
MaxDeviceNameBytes = 128
|
|
MaxVersionBytes = 128
|
|
MinPasswordBytes = 5
|
|
MaxPasswordBytes = 72
|
|
)
|
|
|
|
type User struct {
|
|
ID string
|
|
Username string
|
|
PasswordHash string
|
|
Role UserRole
|
|
IsActive bool
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type Device struct {
|
|
ID string
|
|
Name string
|
|
TokenHash string
|
|
BoundUserID *string
|
|
AppVersion *string
|
|
AndroidVersion *string
|
|
PDDVersion *string
|
|
LastSeenAt *time.Time
|
|
ReadinessAt *time.Time
|
|
AccessibilityEnabled bool
|
|
PDDInstalled bool
|
|
IsEnabled bool
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type AdminSession struct {
|
|
ID string
|
|
TokenHash string
|
|
UserID string
|
|
ExpiresAt time.Time
|
|
RevokedAt *time.Time
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
type AccessToken struct {
|
|
ID string
|
|
TokenHash string
|
|
UserID string
|
|
DeviceID string
|
|
ExpiresAt time.Time
|
|
RevokedAt *time.Time
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
type AuthPrincipal struct {
|
|
UserID string
|
|
Username string
|
|
Role UserRole
|
|
SessionID string
|
|
DeviceID string
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
type AuthValidationError struct {
|
|
Fields map[string]string
|
|
}
|
|
|
|
func (e *AuthValidationError) Error() string {
|
|
return "authentication input validation failed"
|
|
}
|
|
|
|
func NormalizeUsername(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|
|
|
|
func ValidateUserInput(
|
|
username string,
|
|
password string,
|
|
role UserRole,
|
|
) error {
|
|
fields := make(map[string]string)
|
|
normalized := NormalizeUsername(username)
|
|
switch {
|
|
case normalized == "":
|
|
fields["username"] = "required"
|
|
case !utf8.ValidString(normalized):
|
|
fields["username"] = "must be valid UTF-8"
|
|
case len([]byte(normalized)) > MaxUsernameBytes:
|
|
fields["username"] = fmt.Sprintf(
|
|
"must not exceed %d UTF-8 bytes",
|
|
MaxUsernameBytes,
|
|
)
|
|
}
|
|
switch {
|
|
case password == "":
|
|
fields["password"] = "required"
|
|
case !utf8.ValidString(password):
|
|
fields["password"] = "must be valid UTF-8"
|
|
case len([]byte(password)) < MinPasswordBytes:
|
|
fields["password"] = fmt.Sprintf(
|
|
"must be at least %d UTF-8 bytes",
|
|
MinPasswordBytes,
|
|
)
|
|
case len([]byte(password)) > MaxPasswordBytes:
|
|
fields["password"] = fmt.Sprintf(
|
|
"must not exceed %d UTF-8 bytes",
|
|
MaxPasswordBytes,
|
|
)
|
|
}
|
|
if role != UserRoleAdmin && role != UserRoleBuyer {
|
|
fields["role"] = "must be ADMIN or BUYER"
|
|
}
|
|
if len(fields) > 0 {
|
|
return &AuthValidationError{Fields: fields}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateDeviceName(name string) error {
|
|
name = strings.TrimSpace(name)
|
|
switch {
|
|
case name == "":
|
|
return errors.New("device name is required")
|
|
case !utf8.ValidString(name):
|
|
return errors.New("device name must be valid UTF-8")
|
|
case len([]byte(name)) > MaxDeviceNameBytes:
|
|
return fmt.Errorf(
|
|
"device name must not exceed %d UTF-8 bytes",
|
|
MaxDeviceNameBytes,
|
|
)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|