package services import ( "context" "errors" "fmt" "log/slog" "strings" "sync" "code.tczkiot.com/wlw/ai-agent/identity" "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums" "code.tczkiot.com/wlw/ai-agent/internal/pkg/openidentity" ) var SubjectService = &subjectService{} type subjectService struct { mu sync.RWMutex query identity.QuerySubjectsFunc authorize identity.AuthorizeFunc } func SetQuerySubjects(query identity.QuerySubjectsFunc) { SubjectService.mu.Lock() defer SubjectService.mu.Unlock() SubjectService.query = query } func SetAuthorize(authorize identity.AuthorizeFunc) { SubjectService.mu.Lock() defer SubjectService.mu.Unlock() SubjectService.authorize = authorize } func (s *subjectService) Authorize(ctx context.Context, operation string) error { s.mu.RLock() fn := s.authorize s.mu.RUnlock() if fn == nil { return errors.New("ai-agent: Authorize is not initialized") } return fn(ctx, operation) } func (s *subjectService) Query(ctx context.Context, query identity.Query) ([]identity.Subject, error) { s.mu.RLock() fn := s.query s.mu.RUnlock() if fn == nil { return nil, errors.New("ai-agent: QuerySubjects is not initialized") } return fn(ctx, query) } func (s *subjectService) Current(ctx context.Context) (*identity.Subject, error) { items, err := s.Query(ctx, identity.Query{Current: true, EnabledOnly: true}) if err != nil { return nil, err } if len(items) == 0 { return nil, errors.New("ai-agent: current subject not found") } return &items[0], nil } func (s *subjectService) CurrentExternal(ctx context.Context) (*openidentity.ExternalUser, error) { subject, err := s.Current(ctx) if err != nil { return nil, err } if subject.Category != identity.CategoryUser || !subject.Enabled { return nil, errors.New("ai-agent: current subject is not a customer identity") } return &openidentity.ExternalUser{ ExternalSource: enums.ExternalSourceUser, ExternalID: fmt.Sprintf("%s:%d", subject.Type, subject.ID), ExternalName: subject.Name, SubjectType: subject.Type, SubjectID: subject.ID, }, nil } // ResolveExternal returns a host-authenticated customer when available and // falls back to the opaque browser identifier used by anonymous Web visitors. // The identifier is not a login token and is never accepted for dashboard APIs. func (s *subjectService) ResolveExternal(ctx context.Context, guestID, guestName string) (*openidentity.ExternalUser, error) { external, err := s.CurrentExternal(ctx) if err == nil { return external, nil } guestID = strings.TrimSpace(guestID) if guestID == "" || len(guestID) > 128 { return nil, err } guestName = strings.TrimSpace(guestName) if len(guestName) > 255 { guestName = guestName[:255] } return &openidentity.ExternalUser{ ExternalSource: enums.ExternalSourceGuest, ExternalID: guestID, ExternalName: guestName, }, nil } func (s *subjectService) Get(id int64) *identity.Subject { items, err := s.Query(context.Background(), identity.Query{ Types: []identity.SubjectType{identity.SubjectAdmin}, IDs: []int64{id}, EnabledOnly: true, }) if err != nil { slog.Warn("query external subject failed", "id", id, "error", err) return nil } if len(items) == 0 { return nil } return &items[0] } func (s *subjectService) FindByIDs(ids []int64) []identity.Subject { if len(ids) == 0 { return nil } items, err := s.Query(context.Background(), identity.Query{ Types: []identity.SubjectType{identity.SubjectAdmin}, IDs: ids, EnabledOnly: true, }) if err != nil { slog.Warn("query external subjects failed", "error", err) return nil } return items } func (s *subjectService) IsUserReference(subjectType identity.SubjectType, id int64) bool { if id <= 0 { return false } switch subjectType { case identity.SubjectCard, identity.SubjectDevice, identity.SubjectMallUser: default: return false } items, err := s.Query(context.Background(), identity.Query{ Types: []identity.SubjectType{subjectType}, IDs: []int64{id}, EnabledOnly: true, }) if err != nil { slog.Warn("query external customer subject failed", "type", subjectType, "id", id, "error", err) return false } return len(items) > 0 }