2026-07-10 12:54:45 +00:00
|
|
|
|
package domain
|
|
|
|
|
|
|
2026-07-13 01:15:30 +00:00
|
|
|
|
import (
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
)
|
2026-07-10 12:54:45 +00:00
|
|
|
|
|
|
|
|
|
|
// 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"`
|
2026-07-13 01:15:30 +00:00
|
|
|
|
// 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"`
|
2026-07-10 12:54:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func DefaultSettings(uid int64) *UserSettings {
|
|
|
|
|
|
return &UserSettings{
|
|
|
|
|
|
UID: uid, Provider: "xai", Model: "grok-3",
|
|
|
|
|
|
WebSearchProvider: "exa", ExpandStrategy: "hybrid",
|
2026-07-13 01:15:30 +00:00
|
|
|
|
ProviderAPIKeys: map[string]string{},
|
2026-07-10 12:54:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 01:15:30 +00:00
|
|
|
|
// 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)) != ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 12:54:45 +00:00
|
|
|
|
func NowNano() int64 { return time.Now().UTC().UnixNano() }
|