| package persistence |
|
|
| import ( |
| "context" |
| "encoding/json" |
| "os" |
| "path/filepath" |
| "sort" |
| "strings" |
| "sync" |
| "time" |
|
|
| "github.com/router-for-me/CLIProxyAPI/v6/internal/domain/errors" |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/domain/ports" |
| coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" |
| ) |
|
|
| |
| type AuthRepository struct { |
| authDir string |
| authManager *coreauth.Manager |
| mu sync.RWMutex |
| } |
|
|
| |
| func NewAuthRepository(authDir string, authManager *coreauth.Manager) *AuthRepository { |
| return &AuthRepository{ |
| authDir: authDir, |
| authManager: authManager, |
| } |
| } |
|
|
| |
| func (r *AuthRepository) List(ctx context.Context) ([]*ports.AuthFile, error) { |
| r.mu.RLock() |
| defer r.mu.RUnlock() |
|
|
| |
| if r.authManager != nil { |
| return r.listFromManager(ctx) |
| } |
|
|
| |
| return r.listFromDisk(ctx) |
| } |
|
|
| |
| func (r *AuthRepository) listFromManager(ctx context.Context) ([]*ports.AuthFile, error) { |
| auths := r.authManager.List() |
| files := make([]*ports.AuthFile, 0, len(auths)) |
|
|
| for _, auth := range auths { |
| if file := r.mapAuthToFile(auth); file != nil { |
| files = append(files, file) |
| } |
| } |
|
|
| |
| sort.Slice(files, func(i, j int) bool { |
| return strings.ToLower(files[i].FileName) < strings.ToLower(files[j].FileName) |
| }) |
|
|
| return files, nil |
| } |
|
|
| |
| func (r *AuthRepository) listFromDisk(ctx context.Context) ([]*ports.AuthFile, error) { |
| entries, err := os.ReadDir(r.authDir) |
| if err != nil { |
| if os.IsNotExist(err) { |
| return []*ports.AuthFile{}, nil |
| } |
| return nil, errors.Wrap(errors.InternalError, "failed to read auth directory", err) |
| } |
|
|
| files := make([]*ports.AuthFile, 0) |
| for _, e := range entries { |
| if e.IsDir() { |
| continue |
| } |
|
|
| name := e.Name() |
| if !strings.HasSuffix(strings.ToLower(name), ".json") { |
| continue |
| } |
|
|
| info, err := e.Info() |
| if err != nil { |
| continue |
| } |
|
|
| file := &ports.AuthFile{ |
| ID: name, |
| FileName: name, |
| Size: info.Size(), |
| UpdatedAt: info.ModTime(), |
| } |
|
|
| |
| fullPath := filepath.Join(r.authDir, name) |
| if data, err := os.ReadFile(fullPath); err == nil { |
| var metadata map[string]interface{} |
| if err := json.Unmarshal(data, &metadata); err == nil { |
| if t, ok := metadata["type"].(string); ok { |
| file.Provider = t |
| } |
| if email, ok := metadata["email"].(string); ok { |
| file.Email = email |
| } |
| } |
| } |
|
|
| files = append(files, file) |
| } |
|
|
| return files, nil |
| } |
|
|
| |
| func (r *AuthRepository) GetByID(ctx context.Context, id string) (*ports.AuthFile, error) { |
| r.mu.RLock() |
| defer r.mu.RUnlock() |
|
|
| if r.authManager != nil { |
| if auth, ok := r.authManager.GetByID(id); ok { |
| return r.mapAuthToFile(auth), nil |
| } |
| } |
|
|
| |
| files, err := r.listFromDisk(ctx) |
| if err != nil { |
| return nil, err |
| } |
|
|
| for _, file := range files { |
| if file.ID == id || file.FileName == id { |
| return file, nil |
| } |
| } |
|
|
| return nil, errors.NewNotFoundError("auth file", id) |
| } |
|
|
| |
| func (r *AuthRepository) GetByName(ctx context.Context, name string) (*ports.AuthFile, error) { |
| return r.GetByID(ctx, name) |
| } |
|
|
| |
| func (r *AuthRepository) Save(ctx context.Context, file *ports.AuthFile) error { |
| r.mu.Lock() |
| defer r.mu.Unlock() |
|
|
| if file == nil { |
| return errors.New(errors.InvalidInput, "auth file is nil") |
| } |
|
|
| if file.FileName == "" { |
| return errors.New(errors.InvalidInput, "auth file name is empty") |
| } |
|
|
| |
| if err := os.MkdirAll(r.authDir, 0755); err != nil { |
| return errors.Wrap(errors.InternalError, "failed to create auth directory", err) |
| } |
|
|
| fullPath := filepath.Join(r.authDir, file.FileName) |
|
|
| |
| metadata := map[string]interface{}{ |
| "type": file.Provider, |
| } |
| if file.Email != "" { |
| metadata["email"] = file.Email |
| } |
| for k, v := range file.Metadata { |
| metadata[k] = v |
| } |
|
|
| |
| data, err := json.MarshalIndent(metadata, "", " ") |
| if err != nil { |
| return errors.Wrap(errors.InternalError, "failed to marshal auth file", err) |
| } |
|
|
| if err := os.WriteFile(fullPath, data, 0600); err != nil { |
| return errors.Wrap(errors.InternalError, "failed to write auth file", err) |
| } |
|
|
| |
| if r.authManager != nil { |
| auth := r.mapFileToAuth(file) |
| if _, err := r.authManager.Register(ctx, auth); err != nil { |
| |
| |
| _ = err |
| } |
| } |
|
|
| return nil |
| } |
|
|
| |
| func (r *AuthRepository) Delete(ctx context.Context, id string) error { |
| r.mu.Lock() |
| defer r.mu.Unlock() |
|
|
| fullPath := filepath.Join(r.authDir, id) |
|
|
| if !strings.HasSuffix(fullPath, ".json") { |
| fullPath += ".json" |
| } |
|
|
| if err := os.Remove(fullPath); err != nil { |
| if os.IsNotExist(err) { |
| return errors.NewNotFoundError("auth file", id) |
| } |
| return errors.Wrap(errors.InternalError, "failed to remove auth file", err) |
| } |
|
|
| |
| if r.authManager != nil { |
| if auth, ok := r.authManager.GetByID(id); ok { |
| auth.Disabled = true |
| auth.Status = coreauth.StatusDisabled |
| auth.StatusMessage = "removed via management API" |
| auth.UpdatedAt = time.Now() |
| _, _ = r.authManager.Update(ctx, auth) |
| } |
| } |
|
|
| return nil |
| } |
|
|
| |
| func (r *AuthRepository) DeleteAll(ctx context.Context) (int, error) { |
| r.mu.Lock() |
| defer r.mu.Unlock() |
|
|
| entries, err := os.ReadDir(r.authDir) |
| if err != nil { |
| if os.IsNotExist(err) { |
| return 0, nil |
| } |
| return 0, errors.Wrap(errors.InternalError, "failed to read auth directory", err) |
| } |
|
|
| deleted := 0 |
| for _, e := range entries { |
| if e.IsDir() { |
| continue |
| } |
|
|
| name := e.Name() |
| if !strings.HasSuffix(strings.ToLower(name), ".json") { |
| continue |
| } |
|
|
| fullPath := filepath.Join(r.authDir, name) |
| if err := os.Remove(fullPath); err == nil { |
| deleted++ |
|
|
| |
| if r.authManager != nil { |
| if auth, ok := r.authManager.GetByID(name); ok { |
| auth.Disabled = true |
| auth.Status = coreauth.StatusDisabled |
| auth.StatusMessage = "removed via management API" |
| auth.UpdatedAt = time.Now() |
| _, _ = r.authManager.Update(ctx, auth) |
| } |
| } |
| } |
| } |
|
|
| return deleted, nil |
| } |
|
|
| |
| func (r *AuthRepository) Disable(ctx context.Context, id string, reason string) error { |
| r.mu.Lock() |
| defer r.mu.Unlock() |
|
|
| if r.authManager == nil { |
| return errors.ErrAuthManagerUnavailable |
| } |
|
|
| auth, ok := r.authManager.GetByID(id) |
| if !ok { |
| return errors.NewNotFoundError("auth file", id) |
| } |
|
|
| auth.Disabled = true |
| auth.Status = coreauth.StatusDisabled |
| auth.StatusMessage = reason |
| auth.UpdatedAt = time.Now() |
|
|
| _, err := r.authManager.Update(ctx, auth) |
| if err != nil { |
| return errors.Wrap(errors.InternalError, "failed to disable auth file", err) |
| } |
|
|
| return nil |
| } |
|
|
| |
| func (r *AuthRepository) Enable(ctx context.Context, id string) error { |
| r.mu.Lock() |
| defer r.mu.Unlock() |
|
|
| if r.authManager == nil { |
| return errors.ErrAuthManagerUnavailable |
| } |
|
|
| auth, ok := r.authManager.GetByID(id) |
| if !ok { |
| return errors.NewNotFoundError("auth file", id) |
| } |
|
|
| auth.Disabled = false |
| auth.Status = coreauth.StatusActive |
| auth.StatusMessage = "" |
| auth.UpdatedAt = time.Now() |
|
|
| _, err := r.authManager.Update(ctx, auth) |
| if err != nil { |
| return errors.Wrap(errors.InternalError, "failed to enable auth file", err) |
| } |
|
|
| return nil |
| } |
|
|
| |
| func (r *AuthRepository) GetAuthDir() string { |
| r.mu.RLock() |
| defer r.mu.RUnlock() |
| return r.authDir |
| } |
|
|
| |
| func (r *AuthRepository) mapAuthToFile(auth *coreauth.Auth) *ports.AuthFile { |
| if auth == nil { |
| return nil |
| } |
|
|
| auth.EnsureIndex() |
|
|
| |
| runtimeOnly := r.isRuntimeOnlyAuth(auth) |
| if runtimeOnly && (auth.Disabled || auth.Status == coreauth.StatusDisabled) { |
| return nil |
| } |
|
|
| file := &ports.AuthFile{ |
| ID: auth.ID, |
| Provider: strings.TrimSpace(auth.Provider), |
| FileName: auth.FileName, |
| Label: auth.Label, |
| Status: string(auth.Status), |
| StatusMessage: auth.StatusMessage, |
| Disabled: auth.Disabled, |
| Unavailable: auth.Unavailable, |
| RuntimeOnly: runtimeOnly, |
| CreatedAt: auth.CreatedAt, |
| UpdatedAt: auth.UpdatedAt, |
| LastRefreshedAt: auth.LastRefreshedAt, |
| Metadata: make(map[string]interface{}), |
| Attributes: auth.Attributes, |
| } |
|
|
| |
| for k, v := range auth.Metadata { |
| file.Metadata[k] = v |
| } |
|
|
| |
| if auth.Metadata != nil { |
| if email, ok := auth.Metadata["email"].(string); ok { |
| file.Email = email |
| } |
| } |
| if file.Email == "" && auth.Attributes != nil { |
| if email := auth.Attributes["email"]; email != "" { |
| file.Email = email |
| } |
| } |
|
|
| |
| if path := r.authPath(auth); path != "" { |
| file.Path = path |
| if info, err := os.Stat(path); err == nil { |
| file.Size = info.Size() |
| if file.UpdatedAt.IsZero() { |
| file.UpdatedAt = info.ModTime() |
| } |
| } |
| } |
|
|
| return file |
| } |
|
|
| |
| func (r *AuthRepository) mapFileToAuth(file *ports.AuthFile) *coreauth.Auth { |
| if file == nil { |
| return nil |
| } |
|
|
| return &coreauth.Auth{ |
| ID: file.ID, |
| Provider: file.Provider, |
| FileName: file.FileName, |
| Label: file.Label, |
| Status: coreauth.Status(file.Status), |
| Disabled: file.Disabled, |
| Unavailable: file.Unavailable, |
| Metadata: file.Metadata, |
| Attributes: file.Attributes, |
| CreatedAt: file.CreatedAt, |
| UpdatedAt: file.UpdatedAt, |
| } |
| } |
|
|
| |
| func (r *AuthRepository) authPath(auth *coreauth.Auth) string { |
| if auth == nil || len(auth.Attributes) == 0 { |
| return "" |
| } |
| return strings.TrimSpace(auth.Attributes["path"]) |
| } |
|
|
| |
| func (r *AuthRepository) isRuntimeOnlyAuth(auth *coreauth.Auth) bool { |
| if auth == nil || len(auth.Attributes) == 0 { |
| return false |
| } |
| return strings.EqualFold(strings.TrimSpace(auth.Attributes["runtime_only"]), "true") |
| } |
|
|
| |
| func (r *AuthRepository) SetAuthManager(manager *coreauth.Manager) { |
| r.mu.Lock() |
| defer r.mu.Unlock() |
| r.authManager = manager |
| } |
|
|
| |
| var _ ports.AuthRepository = (*AuthRepository)(nil) |
|
|