// Package config provides a unified API for managing application configuration // Location: monorepo/native/desktop/maplefile/internal/config/methods.go package config import ( "context" "fmt" "net/url" "os" "strings" "time" ) // Implementation of ConfigService methods // getConfig is an internal method to get the current configuration func (s *configService) getConfig(ctx context.Context) (*Config, error) { s.mu.RLock() defer s.mu.RUnlock() return s.repo.LoadConfig(ctx) } // saveConfig is an internal method to save the configuration func (s *configService) saveConfig(ctx context.Context, config *Config) error { s.mu.Lock() defer s.mu.Unlock() return s.repo.SaveConfig(ctx, config) } // GetConfig returns the complete configuration func (s *configService) GetConfig(ctx context.Context) (*Config, error) { return s.getConfig(ctx) } // GetAppDataDirPath returns the proper application data directory path // The directory is mode-aware: "maplefile-dev" for dev mode, "maplefile" for production. func (s *configService) GetAppDataDirPath(ctx context.Context) (string, error) { return GetUserDataDir(GetAppName()) } // GetUserDataDirPath returns the data directory path for a specific user. // This path is: // 1. Isolated per user (different users get different directories) // 2. Isolated per environment (dev vs production) // 3. Privacy-preserving (email is hashed to create directory name) // // Structure: {appDataDir}/users/{emailHash}/ func (s *configService) GetUserDataDirPath(ctx context.Context, userEmail string) (string, error) { if userEmail == "" { return "", fmt.Errorf("user email is required") } return GetUserSpecificDataDir(GetAppName(), userEmail) } // GetUserFilesDirPath returns the directory where decrypted files are stored for a user. // Files are organized by collection: {userDir}/files/{collectionId}/{filename} func (s *configService) GetUserFilesDirPath(ctx context.Context, userEmail string) (string, error) { if userEmail == "" { return "", fmt.Errorf("user email is required") } return GetUserFilesDir(GetAppName(), userEmail) } // GetUserSearchIndexDir returns the search index directory path for a specific user. func (s *configService) GetUserSearchIndexDir(ctx context.Context, userEmail string) (string, error) { if userEmail == "" { return "", fmt.Errorf("user email is required") } return GetUserSearchIndexDir(GetAppName(), userEmail) } // GetLoggedInUserEmail returns the email of the currently logged-in user. // Returns an empty string if no user is logged in. func (s *configService) GetLoggedInUserEmail(ctx context.Context) (string, error) { config, err := s.getConfig(ctx) if err != nil { return "", err } if config.Credentials == nil { return "", nil } return config.Credentials.Email, nil } // GetCloudProviderAddress returns the cloud provider address func (s *configService) GetCloudProviderAddress(ctx context.Context) (string, error) { config, err := s.getConfig(ctx) if err != nil { return "", err } return config.CloudProviderAddress, nil } // SetCloudProviderAddress updates the cloud provider address with security validation. // In production mode, the address cannot be changed. // In dev mode, HTTP is allowed for localhost only. func (s *configService) SetCloudProviderAddress(ctx context.Context, address string) error { mode := os.Getenv("MAPLEFILE_MODE") if mode == "" { mode = "dev" } // Security: Block address changes in production mode if mode == "production" { return fmt.Errorf("cloud provider address cannot be changed in production mode") } // Validate URL format if err := validateCloudProviderURL(address, mode); err != nil { return fmt.Errorf("invalid cloud provider address: %w", err) } config, err := s.getConfig(ctx) if err != nil { return err } config.CloudProviderAddress = address return s.saveConfig(ctx, config) } // validateCloudProviderURL validates the cloud provider URL based on the current mode. // Returns an error if the URL is invalid or doesn't meet security requirements. func validateCloudProviderURL(rawURL string, mode string) error { if rawURL == "" { return fmt.Errorf("URL cannot be empty") } parsedURL, err := url.Parse(rawURL) if err != nil { return fmt.Errorf("malformed URL: %w", err) } // Validate scheme scheme := strings.ToLower(parsedURL.Scheme) if scheme != "http" && scheme != "https" { return fmt.Errorf("URL scheme must be http or https, got: %s", scheme) } // Validate host is present if parsedURL.Host == "" { return fmt.Errorf("URL must have a host") } // Security: In dev mode, allow HTTP only for localhost if mode == "dev" && scheme == "http" { host := strings.ToLower(parsedURL.Hostname()) if host != "localhost" && host != "127.0.0.1" && !strings.HasPrefix(host, "192.168.") && !strings.HasPrefix(host, "10.") { return fmt.Errorf("HTTP is only allowed for localhost/local network in dev mode; use HTTPS for remote servers") } } // Reject URLs with credentials embedded if parsedURL.User != nil { return fmt.Errorf("URL must not contain embedded credentials") } return nil } // SetLoggedInUserCredentials updates the authenticated user's credentials func (s *configService) SetLoggedInUserCredentials( ctx context.Context, email string, accessToken string, accessTokenExpiryTime *time.Time, refreshToken string, refreshTokenExpiryTime *time.Time, ) error { config, err := s.getConfig(ctx) if err != nil { return err } config.Credentials = &Credentials{ Email: email, AccessToken: accessToken, AccessTokenExpiryTime: accessTokenExpiryTime, RefreshToken: refreshToken, RefreshTokenExpiryTime: refreshTokenExpiryTime, } return s.saveConfig(ctx, config) } // GetLoggedInUserCredentials returns the authenticated user's credentials func (s *configService) GetLoggedInUserCredentials(ctx context.Context) (*Credentials, error) { config, err := s.getConfig(ctx) if err != nil { return nil, err } return config.Credentials, nil } // ClearLoggedInUserCredentials clears the authenticated user's credentials func (s *configService) ClearLoggedInUserCredentials(ctx context.Context) error { config, err := s.getConfig(ctx) if err != nil { return err } // Clear credentials by setting them to empty values config.Credentials = &Credentials{ Email: "", AccessToken: "", AccessTokenExpiryTime: nil, RefreshToken: "", RefreshTokenExpiryTime: nil, } return s.saveConfig(ctx, config) } // Desktop-specific methods // GetWindowSize returns the configured window size func (s *configService) GetWindowSize(ctx context.Context) (width int, height int, err error) { config, err := s.getConfig(ctx) if err != nil { return 0, 0, err } return config.WindowWidth, config.WindowHeight, nil } // SetWindowSize updates the window size configuration func (s *configService) SetWindowSize(ctx context.Context, width int, height int) error { config, err := s.getConfig(ctx) if err != nil { return err } config.WindowWidth = width config.WindowHeight = height return s.saveConfig(ctx, config) } // GetTheme returns the configured theme func (s *configService) GetTheme(ctx context.Context) (string, error) { config, err := s.getConfig(ctx) if err != nil { return "", err } return config.Theme, nil } // SetTheme updates the theme configuration func (s *configService) SetTheme(ctx context.Context, theme string) error { config, err := s.getConfig(ctx) if err != nil { return err } config.Theme = theme return s.saveConfig(ctx, config) } // GetLanguage returns the configured language func (s *configService) GetLanguage(ctx context.Context) (string, error) { config, err := s.getConfig(ctx) if err != nil { return "", err } return config.Language, nil } // SetLanguage updates the language configuration func (s *configService) SetLanguage(ctx context.Context, language string) error { config, err := s.getConfig(ctx) if err != nil { return err } config.Language = language return s.saveConfig(ctx, config) } // GetSyncMode returns the configured sync mode func (s *configService) GetSyncMode(ctx context.Context) (string, error) { config, err := s.getConfig(ctx) if err != nil { return "", err } return config.SyncMode, nil } // SetSyncMode updates the sync mode configuration func (s *configService) SetSyncMode(ctx context.Context, mode string) error { config, err := s.getConfig(ctx) if err != nil { return err } config.SyncMode = mode return s.saveConfig(ctx, config) } // GetAutoSync returns whether automatic sync is enabled func (s *configService) GetAutoSync(ctx context.Context) (bool, error) { config, err := s.getConfig(ctx) if err != nil { return false, err } return config.AutoSync, nil } // SetAutoSync updates the automatic sync setting func (s *configService) SetAutoSync(ctx context.Context, enabled bool) error { config, err := s.getConfig(ctx) if err != nil { return err } config.AutoSync = enabled return s.saveConfig(ctx, config) } // GetSyncInterval returns the sync interval in minutes func (s *configService) GetSyncInterval(ctx context.Context) (int, error) { config, err := s.getConfig(ctx) if err != nil { return 0, err } return config.SyncIntervalMinutes, nil } // SetSyncInterval updates the sync interval configuration func (s *configService) SetSyncInterval(ctx context.Context, minutes int) error { config, err := s.getConfig(ctx) if err != nil { return err } config.SyncIntervalMinutes = minutes return s.saveConfig(ctx, config) } // GetShowHiddenFiles returns whether hidden files should be shown func (s *configService) GetShowHiddenFiles(ctx context.Context) (bool, error) { config, err := s.getConfig(ctx) if err != nil { return false, err } return config.ShowHiddenFiles, nil } // SetShowHiddenFiles updates the show hidden files setting func (s *configService) SetShowHiddenFiles(ctx context.Context, show bool) error { config, err := s.getConfig(ctx) if err != nil { return err } config.ShowHiddenFiles = show return s.saveConfig(ctx, config) } // GetDefaultView returns the configured default view func (s *configService) GetDefaultView(ctx context.Context) (string, error) { config, err := s.getConfig(ctx) if err != nil { return "", err } return config.DefaultView, nil } // SetDefaultView updates the default view configuration func (s *configService) SetDefaultView(ctx context.Context, view string) error { config, err := s.getConfig(ctx) if err != nil { return err } config.DefaultView = view return s.saveConfig(ctx, config) } // GetSortPreferences returns the configured sort preferences func (s *configService) GetSortPreferences(ctx context.Context) (sortBy string, sortOrder string, err error) { config, err := s.getConfig(ctx) if err != nil { return "", "", err } return config.SortBy, config.SortOrder, nil } // SetSortPreferences updates the sort preferences func (s *configService) SetSortPreferences(ctx context.Context, sortBy string, sortOrder string) error { config, err := s.getConfig(ctx) if err != nil { return err } config.SortBy = sortBy config.SortOrder = sortOrder return s.saveConfig(ctx, config) } // Ensure our implementation satisfies the interface var _ ConfigService = (*configService)(nil)