You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

846 lines
21 KiB
Go

package user
import (
"fmt"
"strings"
"time"
"knowfoolery/backend/shared/types"
"knowfoolery/backend/shared/errors"
"knowfoolery/backend/services/user-service/internal/domain/valueobjects"
)
// User represents a user aggregate
type User struct {
// Identity
id types.UserID
email string
// Profile information
profile *valueobjects.UserProfile
playerName *valueobjects.PlayerName
preferences *valueobjects.UserPreferences
// Authentication data
authProvider string // "zitadel", "google", "facebook", etc.
externalID string // ID from external auth provider
emailVerified bool
// Account status
status types.UserStatus
isActive bool
isBanned bool
banReason *string
bannedUntil *types.Timestamp
// Statistics
statistics *valueobjects.UserStatistics
// Lifecycle
createdAt types.Timestamp
updatedAt types.Timestamp
lastLoginAt *types.Timestamp
// Security
roles []types.UserRole
permissions []string
// Privacy settings
privacySettings *valueobjects.PrivacySettings
// Version control
version int
}
// NewUser creates a new user
func NewUser(
email string,
authProvider string,
externalID string,
) (*User, error) {
// Validate inputs
if err := validateEmail(email); err != nil {
return nil, fmt.Errorf("invalid email: %w", err)
}
if authProvider == "" {
return nil, errors.ErrValidationFailed("auth_provider", "auth provider cannot be empty")
}
if externalID == "" {
return nil, errors.ErrValidationFailed("external_id", "external ID cannot be empty")
}
now := types.NewTimestamp()
// Create default profile
profile, err := valueobjects.NewUserProfile("", "", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to create default profile: %w", err)
}
// Create default preferences
preferences := valueobjects.NewDefaultUserPreferences()
// Create default privacy settings
privacySettings := valueobjects.NewDefaultPrivacySettings()
// Create default statistics
statistics := valueobjects.NewUserStatistics()
user := &User{
id: types.NewUserID(),
email: strings.ToLower(strings.TrimSpace(email)),
profile: profile,
playerName: nil, // Will be set later
preferences: preferences,
authProvider: authProvider,
externalID: externalID,
emailVerified: false,
status: types.UserStatusActive,
isActive: true,
isBanned: false,
banReason: nil,
bannedUntil: nil,
statistics: statistics,
createdAt: now,
updatedAt: now,
lastLoginAt: nil,
roles: []types.UserRole{types.UserRolePlayer}, // Default role
permissions: []string{},
privacySettings: privacySettings,
version: 1,
}
return user, nil
}
// NewUserWithID creates a user with a specific ID (for loading from persistence)
func NewUserWithID(
id types.UserID,
email string,
authProvider string,
externalID string,
createdAt types.Timestamp,
) (*User, error) {
if id.IsEmpty() {
return nil, errors.ErrValidationFailed("id", "user ID cannot be empty")
}
user, err := NewUser(email, authProvider, externalID)
if err != nil {
return nil, err
}
user.id = id
user.createdAt = createdAt
user.updatedAt = createdAt
return user, nil
}
// Getters
// ID returns the user's unique identifier
func (u *User) ID() types.UserID {
return u.id
}
// Email returns the user's email address
func (u *User) Email() string {
return u.email
}
// Profile returns the user's profile
func (u *User) Profile() *valueobjects.UserProfile {
return u.profile
}
// PlayerName returns the user's player name
func (u *User) PlayerName() *valueobjects.PlayerName {
return u.playerName
}
// Preferences returns the user's preferences
func (u *User) Preferences() *valueobjects.UserPreferences {
return u.preferences
}
// AuthProvider returns the authentication provider
func (u *User) AuthProvider() string {
return u.authProvider
}
// ExternalID returns the external ID from auth provider
func (u *User) ExternalID() string {
return u.externalID
}
// IsEmailVerified returns true if email is verified
func (u *User) IsEmailVerified() bool {
return u.emailVerified
}
// Status returns the user status
func (u *User) Status() types.UserStatus {
return u.status
}
// IsActive returns true if the user is active
func (u *User) IsActive() bool {
return u.isActive && u.status == types.UserStatusActive && !u.isBanned
}
// IsBanned returns true if the user is banned
func (u *User) IsBanned() bool {
return u.isBanned
}
// BanReason returns the ban reason if banned
func (u *User) BanReason() *string {
return u.banReason
}
// BannedUntil returns when the ban expires
func (u *User) BannedUntil() *types.Timestamp {
return u.bannedUntil
}
// Statistics returns the user's statistics
func (u *User) Statistics() *valueobjects.UserStatistics {
return u.statistics
}
// CreatedAt returns the creation timestamp
func (u *User) CreatedAt() types.Timestamp {
return u.createdAt
}
// UpdatedAt returns the last update timestamp
func (u *User) UpdatedAt() types.Timestamp {
return u.updatedAt
}
// LastLoginAt returns the last login timestamp
func (u *User) LastLoginAt() *types.Timestamp {
return u.lastLoginAt
}
// Roles returns the user's roles
func (u *User) Roles() []types.UserRole {
return append([]types.UserRole(nil), u.roles...) // Return copy
}
// Permissions returns the user's permissions
func (u *User) Permissions() []string {
return append([]string(nil), u.permissions...) // Return copy
}
// PrivacySettings returns the user's privacy settings
func (u *User) PrivacySettings() *valueobjects.PrivacySettings {
return u.privacySettings
}
// Version returns the current version
func (u *User) Version() int {
return u.version
}
// Business methods
// UpdateProfile updates the user's profile
func (u *User) UpdateProfile(
firstName string,
lastName string,
dateOfBirth *types.Date,
avatar *string,
) error {
newProfile, err := valueobjects.NewUserProfile(firstName, lastName, dateOfBirth, avatar)
if err != nil {
return fmt.Errorf("failed to update profile: %w", err)
}
u.profile = newProfile
u.markUpdated()
return nil
}
// SetPlayerName sets the user's player name
func (u *User) SetPlayerName(playerName string) error {
if playerName == "" {
u.playerName = nil
u.markUpdated()
return nil
}
newPlayerName, err := valueobjects.NewPlayerName(playerName)
if err != nil {
return fmt.Errorf("failed to set player name: %w", err)
}
u.playerName = newPlayerName
u.markUpdated()
return nil
}
// UpdateEmail updates the user's email address
func (u *User) UpdateEmail(newEmail string) error {
if err := validateEmail(newEmail); err != nil {
return fmt.Errorf("invalid email: %w", err)
}
normalizedEmail := strings.ToLower(strings.TrimSpace(newEmail))
if normalizedEmail == u.email {
return nil // No change
}
u.email = normalizedEmail
u.emailVerified = false // Reset verification when email changes
u.markUpdated()
return nil
}
// VerifyEmail marks the email as verified
func (u *User) VerifyEmail() {
if !u.emailVerified {
u.emailVerified = true
u.markUpdated()
}
}
// UpdatePreferences updates the user's preferences
func (u *User) UpdatePreferences(preferences *valueobjects.UserPreferences) error {
if preferences == nil {
return errors.ErrValidationFailed("preferences", "preferences cannot be nil")
}
if err := preferences.Validate(); err != nil {
return fmt.Errorf("invalid preferences: %w", err)
}
u.preferences = preferences
u.markUpdated()
return nil
}
// UpdatePrivacySettings updates the user's privacy settings
func (u *User) UpdatePrivacySettings(settings *valueobjects.PrivacySettings) error {
if settings == nil {
return errors.ErrValidationFailed("privacy_settings", "privacy settings cannot be nil")
}
if err := settings.Validate(); err != nil {
return fmt.Errorf("invalid privacy settings: %w", err)
}
u.privacySettings = settings
u.markUpdated()
return nil
}
// RecordLogin records a successful login
func (u *User) RecordLogin() error {
if !u.IsActive() {
return errors.ErrOperationNotAllowed("record login", "user is not active")
}
now := types.NewTimestamp()
u.lastLoginAt = &now
// Update statistics
u.statistics.RecordLogin()
u.markUpdated()
return nil
}
// Ban bans the user
func (u *User) Ban(reason string, bannedBy types.UserID, duration *types.Duration) error {
if u.isBanned {
return errors.ErrOperationNotAllowed("ban user", "user is already banned")
}
if reason == "" {
return errors.ErrValidationFailed("reason", "ban reason cannot be empty")
}
if bannedBy.IsEmpty() {
return errors.ErrValidationFailed("banned_by", "banned by user ID cannot be empty")
}
u.isBanned = true
u.banReason = &reason
// Set ban expiration if duration provided
if duration != nil {
bannedUntil := types.NewTimestamp().Add(*duration)
u.bannedUntil = &bannedUntil
}
u.status = types.UserStatusBanned
u.markUpdated()
return nil
}
// Unban removes the ban from the user
func (u *User) Unban() error {
if !u.isBanned {
return errors.ErrOperationNotAllowed("unban user", "user is not banned")
}
u.isBanned = false
u.banReason = nil
u.bannedUntil = nil
u.status = types.UserStatusActive
u.markUpdated()
return nil
}
// CheckBanExpiration checks if a temporary ban has expired
func (u *User) CheckBanExpiration() bool {
if !u.isBanned || u.bannedUntil == nil {
return false
}
now := types.NewTimestamp()
if now.After(*u.bannedUntil) {
// Ban has expired, unban the user
u.Unban()
return true
}
return false
}
// Deactivate deactivates the user account
func (u *User) Deactivate() error {
if !u.isActive {
return errors.ErrOperationNotAllowed("deactivate user", "user is already inactive")
}
u.isActive = false
u.status = types.UserStatusInactive
u.markUpdated()
return nil
}
// Reactivate reactivates the user account
func (u *User) Reactivate() error {
if u.isActive {
return errors.ErrOperationNotAllowed("reactivate user", "user is already active")
}
if u.isBanned {
return errors.ErrOperationNotAllowed("reactivate user", "cannot reactivate banned user")
}
u.isActive = true
u.status = types.UserStatusActive
u.markUpdated()
return nil
}
// AddRole adds a role to the user
func (u *User) AddRole(role types.UserRole) error {
// Check if role already exists
for _, existingRole := range u.roles {
if existingRole == role {
return nil // Already has role
}
}
u.roles = append(u.roles, role)
u.markUpdated()
return nil
}
// RemoveRole removes a role from the user
func (u *User) RemoveRole(role types.UserRole) error {
// Don't allow removal of Player role (everyone must have it)
if role == types.UserRolePlayer {
return errors.ErrOperationNotAllowed("remove role", "cannot remove Player role")
}
for i, existingRole := range u.roles {
if existingRole == role {
u.roles = append(u.roles[:i], u.roles[i+1:]...)
u.markUpdated()
return nil
}
}
return errors.ErrNotFound("role", string(role))
}
// AddPermission adds a permission to the user
func (u *User) AddPermission(permission string) error {
permission = strings.TrimSpace(permission)
if permission == "" {
return errors.ErrValidationFailed("permission", "permission cannot be empty")
}
// Check if permission already exists
for _, existingPermission := range u.permissions {
if existingPermission == permission {
return nil // Already has permission
}
}
u.permissions = append(u.permissions, permission)
u.markUpdated()
return nil
}
// RemovePermission removes a permission from the user
func (u *User) RemovePermission(permission string) error {
for i, existingPermission := range u.permissions {
if existingPermission == permission {
u.permissions = append(u.permissions[:i], u.permissions[i+1:]...)
u.markUpdated()
return nil
}
}
return errors.ErrNotFound("permission", permission)
}
// UpdateStatistics updates user statistics
func (u *User) UpdateStatistics(stats *valueobjects.UserStatistics) error {
if stats == nil {
return errors.ErrValidationFailed("statistics", "statistics cannot be nil")
}
if err := stats.Validate(); err != nil {
return fmt.Errorf("invalid statistics: %w", err)
}
u.statistics = stats
u.markUpdated()
return nil
}
// Query methods
// HasRole checks if the user has a specific role
func (u *User) HasRole(role types.UserRole) bool {
for _, userRole := range u.roles {
if userRole == role {
return true
}
}
return false
}
// HasPermission checks if the user has a specific permission
func (u *User) HasPermission(permission string) bool {
for _, userPermission := range u.permissions {
if userPermission == permission {
return true
}
}
return false
}
// IsAdmin returns true if the user is an admin
func (u *User) IsAdmin() bool {
return u.HasRole(types.UserRoleAdmin)
}
// IsModerator returns true if the user is a moderator
func (u *User) IsModerator() bool {
return u.HasRole(types.UserRoleModerator)
}
// CanPlay returns true if the user can play games
func (u *User) CanPlay() bool {
return u.IsActive() && !u.isBanned
}
// GetDisplayName returns the best display name for the user
func (u *User) GetDisplayName() string {
// Prefer player name
if u.playerName != nil && u.playerName.Value() != "" {
return u.playerName.Value()
}
// Fall back to profile name
if u.profile != nil {
fullName := strings.TrimSpace(u.profile.FirstName() + " " + u.profile.LastName())
if fullName != "" {
return fullName
}
if u.profile.FirstName() != "" {
return u.profile.FirstName()
}
}
// Fall back to email prefix
parts := strings.Split(u.email, "@")
if len(parts) > 0 && parts[0] != "" {
return parts[0]
}
// Last resort
return "User"
}
// GetAge returns the user's age if date of birth is available
func (u *User) GetAge() *int {
if u.profile == nil || u.profile.DateOfBirth() == nil {
return nil
}
now := time.Now()
birthDate := time.Time(*u.profile.DateOfBirth())
age := now.Year() - birthDate.Year()
if now.YearDay() < birthDate.YearDay() {
age--
}
return &age
}
// GetAccountAge returns how long the user has been registered
func (u *User) GetAccountAge() time.Duration {
now := types.NewTimestamp()
return now.Sub(u.createdAt)
}
// IsNewUser returns true if the user registered recently
func (u *User) IsNewUser() bool {
age := u.GetAccountAge()
return age < 7*24*time.Hour // Less than 7 days old
}
// Validation
// Validate performs comprehensive validation
func (u *User) Validate() *types.ValidationResult {
result := types.NewValidationResult()
// Validate ID
if u.id.IsEmpty() {
result.AddError("user ID cannot be empty")
}
// Validate email
if err := validateEmail(u.email); err != nil {
result.AddErrorf("invalid email: %v", err)
}
// Validate auth provider
if u.authProvider == "" {
result.AddError("auth provider cannot be empty")
}
// Validate external ID
if u.externalID == "" {
result.AddError("external ID cannot be empty")
}
// Validate profile
if u.profile != nil {
if profileResult := u.profile.Validate(); !profileResult.IsValid() {
for _, error := range profileResult.Errors() {
result.AddErrorf("profile validation failed: %s", error)
}
}
}
// Validate player name
if u.playerName != nil {
if playerNameResult := u.playerName.Validate(); !playerNameResult.IsValid() {
for _, error := range playerNameResult.Errors() {
result.AddErrorf("player name validation failed: %s", error)
}
}
}
// Validate preferences
if u.preferences != nil {
if prefResult := u.preferences.Validate(); !prefResult.IsValid() {
for _, error := range prefResult.Errors() {
result.AddErrorf("preferences validation failed: %s", error)
}
}
}
// Validate privacy settings
if u.privacySettings != nil {
if privacyResult := u.privacySettings.Validate(); !privacyResult.IsValid() {
for _, error := range privacyResult.Errors() {
result.AddErrorf("privacy settings validation failed: %s", error)
}
}
}
// Validate statistics
if u.statistics != nil {
if statsResult := u.statistics.Validate(); !statsResult.IsValid() {
for _, error := range statsResult.Errors() {
result.AddErrorf("statistics validation failed: %s", error)
}
}
}
// Validate roles
if len(u.roles) == 0 {
result.AddError("user must have at least one role")
}
// Check for Player role
hasPlayerRole := false
for _, role := range u.roles {
if role == types.UserRolePlayer {
hasPlayerRole = true
break
}
}
if !hasPlayerRole {
result.AddError("user must have Player role")
}
// Validate ban logic
if u.isBanned && u.banReason == nil {
result.AddError("banned user must have ban reason")
}
if u.banReason != nil && !u.isBanned {
result.AddError("user has ban reason but is not marked as banned")
}
return result
}
// Helper methods
// markUpdated updates the timestamp and version
func (u *User) markUpdated() {
u.updatedAt = types.NewTimestamp()
u.version++
}
// ToSnapshot creates a read-only snapshot of the user
func (u *User) ToSnapshot() *UserSnapshot {
var playerName *string
if u.playerName != nil {
name := u.playerName.Value()
playerName = &name
}
return &UserSnapshot{
ID: u.id,
Email: u.email,
Profile: u.profile.ToSnapshot(),
PlayerName: playerName,
Preferences: u.preferences.ToSnapshot(),
AuthProvider: u.authProvider,
ExternalID: u.externalID,
EmailVerified: u.emailVerified,
Status: u.status,
IsActive: u.isActive,
IsBanned: u.isBanned,
BanReason: u.banReason,
BannedUntil: u.bannedUntil,
Statistics: u.statistics.ToSnapshot(),
CreatedAt: u.createdAt,
UpdatedAt: u.updatedAt,
LastLoginAt: u.lastLoginAt,
Roles: append([]types.UserRole(nil), u.roles...),
Permissions: append([]string(nil), u.permissions...),
PrivacySettings: u.privacySettings.ToSnapshot(),
Version: u.version,
}
}
// UserSnapshot represents a read-only view of a user
type UserSnapshot struct {
ID types.UserID `json:"id"`
Email string `json:"email"`
Profile *valueobjects.UserProfileSnapshot `json:"profile"`
PlayerName *string `json:"player_name,omitempty"`
Preferences *valueobjects.UserPreferencesSnapshot `json:"preferences"`
AuthProvider string `json:"auth_provider"`
ExternalID string `json:"external_id"`
EmailVerified bool `json:"email_verified"`
Status types.UserStatus `json:"status"`
IsActive bool `json:"is_active"`
IsBanned bool `json:"is_banned"`
BanReason *string `json:"ban_reason,omitempty"`
BannedUntil *types.Timestamp `json:"banned_until,omitempty"`
Statistics *valueobjects.UserStatisticsSnapshot `json:"statistics"`
CreatedAt types.Timestamp `json:"created_at"`
UpdatedAt types.Timestamp `json:"updated_at"`
LastLoginAt *types.Timestamp `json:"last_login_at,omitempty"`
Roles []types.UserRole `json:"roles"`
Permissions []string `json:"permissions"`
PrivacySettings *valueobjects.PrivacySettingsSnapshot `json:"privacy_settings"`
Version int `json:"version"`
}
// GetDisplayName returns the display name for the snapshot
func (us *UserSnapshot) GetDisplayName() string {
if us.PlayerName != nil && *us.PlayerName != "" {
return *us.PlayerName
}
if us.Profile != nil {
fullName := strings.TrimSpace(us.Profile.FirstName + " " + us.Profile.LastName)
if fullName != "" {
return fullName
}
if us.Profile.FirstName != "" {
return us.Profile.FirstName
}
}
parts := strings.Split(us.Email, "@")
if len(parts) > 0 && parts[0] != "" {
return parts[0]
}
return "User"
}
// Validation helper functions
func validateEmail(email string) error {
email = strings.TrimSpace(email)
if email == "" {
return errors.ErrValidationFailed("email", "email cannot be empty")
}
if len(email) > types.MaxEmailLength {
return errors.ErrValidationFailed("email", fmt.Sprintf("email too long (max %d characters)", types.MaxEmailLength))
}
// Basic email format validation
if !strings.Contains(email, "@") {
return errors.ErrValidationFailed("email", "email must contain @ symbol")
}
parts := strings.Split(email, "@")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return errors.ErrValidationFailed("email", "invalid email format")
}
// More comprehensive email validation would go here
// For production, use a proper email validation library
return nil
}