| 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" |
| ) |
|
|
| |
| type AuthService struct { |
| repo ports.AuthRepository |
| logger Logger |
| } |
|
|
| |
| func NewAuthService(repo ports.AuthRepository, logger Logger) *AuthService { |
| return &AuthService{ |
| repo: repo, |
| logger: logger, |
| } |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| file, err := s.GetAuthFile(ctx, id) |
| if err != nil { |
| return nil, err |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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") |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| func (s *AuthService) DownloadAuthFile(ctx context.Context, id string) ([]byte, error) { |
| if id == "" { |
| return nil, errors.New(errors.InvalidInput, "auth file ID is empty") |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| file, err := s.GetAuthFile(ctx, id) |
| if err != nil { |
| return err |
| } |
|
|
| |
| |
| _ = file |
|
|
| if s.logger != nil { |
| s.logger.Infof(ctx, "auth token refreshed: %s", id) |
| } |
|
|
| return nil |
| } |
|
|
| |
| var _ ports.AuthFileService = (*AuthService)(nil) |
|
|