package services import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "github.com/router-for-me/CLIProxyAPI/v6/internal/domain/errors" "github.com/router-for-me/CLIProxyAPI/v6/internal/domain/ports" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" ) // AuthService implements the ports.AuthFileService interface type AuthService struct { repo ports.AuthRepository logger Logger } // NewAuthService creates a new AuthService func NewAuthService(repo ports.AuthRepository, logger Logger) *AuthService { return &AuthService{ repo: repo, logger: logger, } } // ListAuthFiles retrieves all authentication files func (s *AuthService) ListAuthFiles(ctx context.Context) ([]*ports.AuthFile, error) { if s.logger != nil { s.logger.Debug(ctx, "listing auth files") } files, err := s.repo.List(ctx) if err != nil { if s.logger != nil { s.logger.Error(ctx, "failed to list auth files", err) } return nil, err } return files, nil } // GetAuthFile retrieves a single authentication file by ID func (s *AuthService) GetAuthFile(ctx context.Context, id string) (*ports.AuthFile, error) { if id == "" { return nil, errors.New(errors.InvalidInput, "auth file ID is empty") } if s.logger != nil { s.logger.Debugf(ctx, "getting auth file: %s", id) } file, err := s.repo.GetByID(ctx, id) if err != nil { if s.logger != nil { s.logger.Error(ctx, fmt.Sprintf("failed to get auth file: %s", id), err) } return nil, err } return file, nil } // GetAuthFileModels retrieves models supported by an auth file func (s *AuthService) GetAuthFileModels(ctx context.Context, id string) ([]*ports.AuthFileModel, error) { if id == "" { return nil, errors.New(errors.InvalidInput, "auth file ID is empty") } if s.logger != nil { s.logger.Debugf(ctx, "getting models for auth file: %s", id) } // Get auth file to verify it exists file, err := s.GetAuthFile(ctx, id) if err != nil { return nil, err } // Get models from registry reg := registry.GetGlobalRegistry() models := reg.GetModelsForClient(file.ID) result := make([]*ports.AuthFileModel, 0, len(models)) for _, m := range models { model := &ports.AuthFileModel{ ID: m.ID, } if m.DisplayName != "" { model.DisplayName = m.DisplayName } if m.Type != "" { model.Type = m.Type } if m.OwnedBy != "" { model.OwnedBy = m.OwnedBy } result = append(result, model) } return result, nil } // UploadAuthFile uploads a new authentication file func (s *AuthService) UploadAuthFile(ctx context.Context, filename string, data []byte) (*ports.AuthFile, error) { if filename == "" { return nil, errors.New(errors.InvalidInput, "filename is empty") } if len(data) == 0 { return nil, errors.New(errors.InvalidInput, "file data is empty") } if !strings.HasSuffix(strings.ToLower(filename), ".json") { return nil, errors.NewValidationError("file must be .json", "filename", "expected .json extension") } // Validate JSON var metadata map[string]interface{} if err := json.Unmarshal(data, &metadata); err != nil { return nil, errors.Wrap(errors.ValidationFailed, "invalid JSON", err) } if s.logger != nil { s.logger.Debugf(ctx, "uploading auth file: %s", filename) } // Extract provider and email from metadata provider, _ := metadata["type"].(string) if provider == "" { provider = "unknown" } email, _ := metadata["email"].(string) label := provider if email != "" { label = email } file := &ports.AuthFile{ ID: filename, FileName: filename, Provider: provider, Label: label, Email: email, Metadata: metadata, Status: "active", } if err := s.repo.Save(ctx, file); err != nil { if s.logger != nil { s.logger.Error(ctx, fmt.Sprintf("failed to save auth file: %s", filename), err) } return nil, err } if s.logger != nil { s.logger.Infof(ctx, "auth file uploaded: %s", filename) } return file, nil } // DownloadAuthFile retrieves the raw content of an auth file func (s *AuthService) DownloadAuthFile(ctx context.Context, id string) ([]byte, error) { if id == "" { return nil, errors.New(errors.InvalidInput, "auth file ID is empty") } // Validate ID to prevent path traversal if strings.Contains(id, string(os.PathSeparator)) || strings.Contains(id, "/") { return nil, errors.New(errors.InvalidInput, "invalid auth file ID") } if !strings.HasSuffix(strings.ToLower(id), ".json") { return nil, errors.NewValidationError("filename must end with .json", "id", "expected .json extension") } if s.logger != nil { s.logger.Debugf(ctx, "downloading auth file: %s", id) } authDir := s.repo.GetAuthDir() fullPath := filepath.Join(authDir, id) data, err := os.ReadFile(fullPath) if err != nil { if os.IsNotExist(err) { return nil, errors.NewNotFoundError("auth file", id) } return nil, errors.Wrap(errors.InternalError, "failed to read auth file", err) } return data, nil } // DeleteAuthFile deletes an authentication file func (s *AuthService) DeleteAuthFile(ctx context.Context, id string) error { if id == "" { return errors.New(errors.InvalidInput, "auth file ID is empty") } if s.logger != nil { s.logger.Debugf(ctx, "deleting auth file: %s", id) } if err := s.repo.Delete(ctx, id); err != nil { if s.logger != nil { s.logger.Error(ctx, fmt.Sprintf("failed to delete auth file: %s", id), err) } return err } if s.logger != nil { s.logger.Infof(ctx, "auth file deleted: %s", id) } return nil } // DeleteAllAuthFiles deletes all authentication files func (s *AuthService) DeleteAllAuthFiles(ctx context.Context) (int, error) { if s.logger != nil { s.logger.Debug(ctx, "deleting all auth files") } deleted, err := s.repo.DeleteAll(ctx) if err != nil { if s.logger != nil { s.logger.Error(ctx, "failed to delete all auth files", err) } return 0, err } if s.logger != nil { s.logger.Infof(ctx, "deleted %d auth files", deleted) } return deleted, nil } // DisableAuthFile disables an authentication file func (s *AuthService) DisableAuthFile(ctx context.Context, id string) error { if id == "" { return errors.New(errors.InvalidInput, "auth file ID is empty") } if s.logger != nil { s.logger.Debugf(ctx, "disabling auth file: %s", id) } if err := s.repo.Disable(ctx, id, "disabled via management API"); err != nil { if s.logger != nil { s.logger.Error(ctx, fmt.Sprintf("failed to disable auth file: %s", id), err) } return err } if s.logger != nil { s.logger.Infof(ctx, "auth file disabled: %s", id) } return nil } // EnableAuthFile enables an authentication file func (s *AuthService) EnableAuthFile(ctx context.Context, id string) error { if id == "" { return errors.New(errors.InvalidInput, "auth file ID is empty") } if s.logger != nil { s.logger.Debugf(ctx, "enabling auth file: %s", id) } if err := s.repo.Enable(ctx, id); err != nil { if s.logger != nil { s.logger.Error(ctx, fmt.Sprintf("failed to enable auth file: %s", id), err) } return err } if s.logger != nil { s.logger.Infof(ctx, "auth file enabled: %s", id) } return nil } // RefreshAuthToken refreshes the token for an auth file func (s *AuthService) RefreshAuthToken(ctx context.Context, id string) error { if id == "" { return errors.New(errors.InvalidInput, "auth file ID is empty") } if s.logger != nil { s.logger.Debugf(ctx, "refreshing auth token: %s", id) } // Get the auth file file, err := s.GetAuthFile(ctx, id) if err != nil { return err } // Token refresh logic would be implemented here // This is a placeholder for the actual implementation _ = file if s.logger != nil { s.logger.Infof(ctx, "auth token refreshed: %s", id) } return nil } // Ensure AuthService implements the interface var _ ports.AuthFileService = (*AuthService)(nil)