package domain import ( "strings" "time" ) // Member is the person profile (stand-alone user_info),uid 為數字主鍵。 type Member struct { UID int64 `bson:"uid" json:"uid"` TenantID string `bson:"tenant_id" json:"tenant_id"` Email string `bson:"email" json:"email"` Phone string `bson:"phone,omitempty" json:"phone,omitempty"` DisplayName string `bson:"display_name" json:"display_name"` Nickname string `bson:"nickname,omitempty" json:"nickname,omitempty"` FullName string `bson:"full_name,omitempty" json:"full_name,omitempty"` Roles []string `bson:"roles" json:"roles"` Status string `bson:"status" json:"status"` EmailVerified bool `bson:"email_verified" json:"email_verified"` EmailVerifiedAt *int64 `bson:"email_verified_at,omitempty" json:"email_verified_at,omitempty"` PhoneVerified bool `bson:"phone_verified" json:"phone_verified"` PhoneVerifiedAt *int64 `bson:"phone_verified_at,omitempty" json:"phone_verified_at,omitempty"` InviteCode string `bson:"invite_code,omitempty" json:"invite_code,omitempty"` ParentUID *int64 `bson:"parent_uid,omitempty" json:"parent_uid,omitempty"` Bio string `bson:"bio,omitempty" json:"bio,omitempty"` Timezone string `bson:"timezone,omitempty" json:"timezone,omitempty"` AvatarURL string `bson:"avatar_url,omitempty" json:"avatar_url,omitempty"` NotifyEmail bool `bson:"notify_email,omitempty" json:"notify_email,omitempty"` GenderCode int8 `bson:"gender_code,omitempty" json:"gender_code,omitempty"` Birthdate int64 `bson:"birthdate,omitempty" json:"birthdate,omitempty"` // YYYYMMDD National string `bson:"national,omitempty" json:"national,omitempty"` Address string `bson:"address,omitempty" json:"address,omitempty"` PostCode string `bson:"post_code,omitempty" json:"post_code,omitempty"` PreferredLanguage string `bson:"preferred_language,omitempty" json:"preferred_language,omitempty"` Currency string `bson:"currency,omitempty" json:"currency,omitempty"` // Primary password for platform email login (also mirrored on Identity). // Must be JSON-tagged: monc Redis cache marshals with encoding/json; // json:"-" strips the hash and makes warm-cache login always fail. PasswordHash string `bson:"password_hash,omitempty" json:"password_hash,omitempty"` CreatedAt int64 `bson:"created_at" json:"created_at"` UpdatedAt int64 `bson:"updated_at" json:"updated_at"` } func (m Member) IsAdmin() bool { for _, r := range m.Roles { if r == RoleAdmin { return true } } return false } func (m Member) IsSuspended() bool { return m.Status == StatusSuspended } // Identity binds a login channel to a member (stand-alone account + account_uid). type Identity struct { ID string `bson:"_id,omitempty" json:"id,omitempty"` UID int64 `bson:"uid" json:"uid"` LoginID string `bson:"login_id" json:"login_id"` // email / phone / oauth subject Platform string `bson:"platform" json:"platform"` // platform|google|line|apple AccountType string `bson:"account_type" json:"account_type"` // json tag required for monc cache (see Member.PasswordHash). PasswordHash string `bson:"password_hash,omitempty" json:"password_hash,omitempty"` CreatedAt int64 `bson:"created_at" json:"created_at"` UpdatedAt int64 `bson:"updated_at" json:"updated_at"` } // UserSettings — AI/placement overrides type UserSettings struct { UID int64 `bson:"uid" json:"uid"` Provider string `bson:"provider" json:"provider"` Model string `bson:"model" json:"model"` APIKeyConfigured bool `bson:"api_key_configured" json:"api_key_configured"` ResearchAPIKeyConfigured bool `bson:"research_api_key_configured" json:"research_api_key_configured"` // APIKey — legacy single key (migrated into ProviderAPIKeys on save) APIKey string `bson:"api_key,omitempty" json:"-"` // ProviderAPIKeys — per-provider BYOK: "xai" | "opencode-go" ProviderAPIKeys map[string]string `bson:"provider_api_keys,omitempty" json:"-"` ExaAPIKey string `bson:"exa_api_key,omitempty" json:"-"` WebSearchProvider string `bson:"web_search_provider" json:"web_search_provider"` ExpandStrategy string `bson:"expand_strategy" json:"expand_strategy"` ExaAPIKeyConfigured bool `bson:"exa_api_key_configured" json:"exa_api_key_configured"` DevModeEnabled bool `bson:"dev_mode_enabled" json:"dev_mode_enabled"` UpdatedAt int64 `bson:"updated_at" json:"updated_at"` } func DefaultSettings(uid int64) *UserSettings { return &UserSettings{ UID: uid, Provider: "xai", Model: "grok-3", WebSearchProvider: "exa", ExpandStrategy: "hybrid", ProviderAPIKeys: map[string]string{}, } } // KeyForProvider returns BYOK for provider (legacy APIKey fallback for current provider). func (s *UserSettings) KeyForProvider(provider string) string { if s == nil { return "" } provider = strings.ToLower(strings.TrimSpace(provider)) if provider == "" { provider = s.Provider } if s.ProviderAPIKeys != nil { if k := strings.TrimSpace(s.ProviderAPIKeys[provider]); k != "" { return k } } // legacy single key only applies to currently selected provider if provider == s.Provider || s.Provider == "" { return strings.TrimSpace(s.APIKey) } return "" } // SetKeyForProvider stores BYOK under provider map. func (s *UserSettings) SetKeyForProvider(provider, key string) { if s == nil { return } provider = strings.ToLower(strings.TrimSpace(provider)) if s.ProviderAPIKeys == nil { s.ProviderAPIKeys = map[string]string{} } key = strings.TrimSpace(key) if key == "" { delete(s.ProviderAPIKeys, provider) } else { s.ProviderAPIKeys[provider] = key } // keep legacy field in sync for current provider if provider == s.Provider { s.APIKey = key s.APIKeyConfigured = key != "" s.ResearchAPIKeyConfigured = key != "" } } // HasKeyForProvider — UI configured flag. func (s *UserSettings) HasKeyForProvider(provider string) bool { return strings.TrimSpace(s.KeyForProvider(provider)) != "" } func NowNano() int64 { return time.Now().UTC().UnixNano() }