API / internal /infrastructure /persistence /auth_repository.go
sshinmen's picture
Update HF Spaces deployment with latest changes
2e6b65c
Raw
History Blame Contribute Delete
10.9 kB
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"
)
// AuthRepository implements the ports.AuthRepository interface
type AuthRepository struct {
authDir string
authManager *coreauth.Manager
mu sync.RWMutex
}
// NewAuthRepository creates a new AuthRepository
func NewAuthRepository(authDir string, authManager *coreauth.Manager) *AuthRepository {
return &AuthRepository{
authDir: authDir,
authManager: authManager,
}
}
// List retrieves all authentication files
func (r *AuthRepository) List(ctx context.Context) ([]*ports.AuthFile, error) {
r.mu.RLock()
defer r.mu.RUnlock()
// If auth manager is available, use it
if r.authManager != nil {
return r.listFromManager(ctx)
}
// Fallback to listing from disk
return r.listFromDisk(ctx)
}
// listFromManager retrieves auth files from the auth manager
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 by name
sort.Slice(files, func(i, j int) bool {
return strings.ToLower(files[i].FileName) < strings.ToLower(files[j].FileName)
})
return files, nil
}
// listFromDisk retrieves auth files from disk
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(),
}
// Read file to get type and email
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
}
// GetByID retrieves an authentication file by its ID
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
}
}
// Try by filename
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)
}
// GetByName retrieves an authentication file by its filename
func (r *AuthRepository) GetByName(ctx context.Context, name string) (*ports.AuthFile, error) {
return r.GetByID(ctx, name)
}
// Save persists an authentication file
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")
}
// Ensure auth directory exists
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)
// Build metadata from file
metadata := map[string]interface{}{
"type": file.Provider,
}
if file.Email != "" {
metadata["email"] = file.Email
}
for k, v := range file.Metadata {
metadata[k] = v
}
// Write file
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)
}
// Register with auth manager if available
if r.authManager != nil {
auth := r.mapFileToAuth(file)
if _, err := r.authManager.Register(ctx, auth); err != nil {
// Log but don't fail - file is already saved
// This could be handled by a logger passed to the repository
_ = err
}
}
return nil
}
// Delete removes an authentication file
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)
}
// Disable in auth manager if available
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
}
// DeleteAll removes all authentication files
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++
// Disable in auth manager
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
}
// Disable marks an authentication file as disabled
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
}
// Enable marks an authentication file as enabled
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
}
// GetAuthDir returns the authentication directory path
func (r *AuthRepository) GetAuthDir() string {
r.mu.RLock()
defer r.mu.RUnlock()
return r.authDir
}
// mapAuthToFile maps a coreauth.Auth to an ports.AuthFile
func (r *AuthRepository) mapAuthToFile(auth *coreauth.Auth) *ports.AuthFile {
if auth == nil {
return nil
}
auth.EnsureIndex()
// Skip runtime-only disabled auths
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,
}
// Copy metadata
for k, v := range auth.Metadata {
file.Metadata[k] = v
}
// Extract email
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
}
}
// Get file info if path exists
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
}
// mapFileToAuth maps an ports.AuthFile to a coreauth.Auth
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,
}
}
// authPath extracts the path from auth attributes
func (r *AuthRepository) authPath(auth *coreauth.Auth) string {
if auth == nil || len(auth.Attributes) == 0 {
return ""
}
return strings.TrimSpace(auth.Attributes["path"])
}
// isRuntimeOnlyAuth checks if an auth is runtime-only
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")
}
// SetAuthManager updates the auth manager
func (r *AuthRepository) SetAuthManager(manager *coreauth.Manager) {
r.mu.Lock()
defer r.mu.Unlock()
r.authManager = manager
}
// Ensure AuthRepository implements the interface
var _ ports.AuthRepository = (*AuthRepository)(nil)