feat(auth): implement user and device authentication
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
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 = 12
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user