Documentation
¶
Overview ¶
Package userprefs provides a flexible, concurrent-safe user preferences management system.
It supports multiple storage backends (PostgreSQL, SQLite), optional caching (Redis, in-memory), and a type-safe preference system. While originally designed for Discord bots, it can be used in any application requiring user-specific preference storage.
Package userprefs provides an adapter for the encryption package.
Package userprefs defines error variables used throughout the user preferences system.
Package userprefs defines interfaces for storage, caching, and logging used in user preferences management.
Package userprefs provides default logging implementations.
Package userprefs provides the Manager responsible for handling user preferences.
Package userprefs defines the core types used in the user preferences management system.
Package userprefs provides validation functions for user preferences.
Index ¶
- Constants
- Variables
- type Cache
- type Config
- type EncryptionAdapter
- type EncryptionManager
- type LogLevel
- type Logger
- type Manager
- func (m *Manager) DefinePreference(def PreferenceDefinition) error
- func (m *Manager) Delete(ctx context.Context, userID, key string) error
- func (m *Manager) Get(ctx context.Context, userID, key string) (*Preference, error)
- func (m *Manager) GetAll(ctx context.Context, userID string) (map[string]*Preference, error)
- func (m *Manager) GetAllDefinitions(_ context.Context) ([]*PreferenceDefinition, error)
- func (m *Manager) GetByCategory(ctx context.Context, userID, category string) (map[string]*Preference, error)
- func (m *Manager) GetDefinition(key string) (PreferenceDefinition, bool)
- func (m *Manager) Set(ctx context.Context, userID, key string, value interface{}) error
- type Option
- type Preference
- type PreferenceDefinition
- type PreferenceType
- type Storage
Constants ¶
const ( // StringType represents a preference value that is a string. StringType string = "string" // BoolType represents a preference value that is a boolean. BoolType string = "bool" // IntType represents a preference value that is an integer. IntType string = "int" // FloatType represents a preference value that is a floating-point number. FloatType string = "float" // JSONType represents a preference value that is a JSON object or array. // The actual Go type would typically be map[string]interface{} or []interface{}. JSONType string = "json" )
Constants for standard preference types. These should be used when creating PreferenceDefinition instances to ensure consistency.
Variables ¶
var ErrAlreadyExists = errors.New("resource already exists")
ErrAlreadyExists indicates that an attempt was made to create a resource that already exists.
var ErrCacheClosed = errors.New("cache is closed")
ErrCacheClosed indicates that an operation was attempted on a cache that has been closed.
ErrCacheUnavailable indicates that the cache backend is unavailable.
var ErrEncryptionFailed = errors.New("encryption operation failed")
ErrEncryptionFailed indicates that an encryption or decryption operation failed.
var ErrEncryptionRequired = errors.New("encryption manager required for encrypted preferences")
ErrEncryptionRequired indicates that an encryption manager is required but not configured.
var ErrInternal = errors.New("internal server error")
ErrInternal indicates an unexpected internal server error.
var ErrInvalidInput = errors.New("invalid input parameters")
ErrInvalidInput indicates that the input parameters provided to a function are invalid.
var ErrInvalidKey = errors.New("invalid preference key")
ErrInvalidKey indicates that the provided preference key is invalid.
var ErrInvalidType = errors.New("invalid preference type")
ErrInvalidType indicates that the provided preference type is invalid.
var ErrInvalidValue = errors.New("invalid preference value")
ErrInvalidValue indicates that the provided preference value is invalid.
var ErrNotFound = errors.New("preference not found")
ErrNotFound indicates that the requested preference was not found.
var ErrPreferenceNotDefined = errors.New("preference not defined")
ErrPreferenceNotDefined indicates that the preference has not been defined in the system.
var ErrSerialization = errors.New("data serialization error")
ErrSerialization indicates an error during data serialization or deserialization (e.g., JSON).
ErrStorageUnavailable indicates that the storage backend is unavailable.
var ErrValidation = errors.New("preference validation failed")
ErrValidation indicates that a preference value failed a validation check.
Functions ¶
This section is empty.
Types ¶
type Cache ¶
type Cache interface {
// Get retrieves an item from the cache by its key.
// The key is typically a string identifying the cached Preference (e.g., a composite of userID and preference key).
// On a cache hit, it returns the cached item as a byte slice (`[]byte`) and a nil error.
// The caller (e.g., the Manager) is responsible for unmarshalling this byte slice into the appropriate data structure.
// On a cache miss (item not found or expired), it must return nil for the byte slice and an error
// that can be checked using `errors.Is(err, userprefs.ErrNotFound)`.
// Other errors may indicate issues with the cache backend itself.
Get(ctx context.Context, key string) ([]byte, error)
// Set adds an item to the cache with a specific key and time-to-live (TTL).
// The 'value' must be a byte slice (`[]byte`), typically the marshalled form of a Preference.
// The caller (e.g., the Manager) is responsible for marshalling the data into a byte slice before calling Set.
// The 'ttl' (time-to-live) specifies how long the item should remain in the cache.
// A TTL of 0 might be interpreted by some implementations as "cache forever" or "use default TTL",
// so specific behavior should be documented by the implementation.
// Returns a nil error on success, or an error if the operation fails.
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
// Delete removes an item from the cache by its key.
// This method must be idempotent: it should return a nil error even if the
// key does not exist in the cache or has already been deleted.
// An error should only be returned for underlying cache system issues.
Delete(ctx context.Context, key string) error
// Close releases any resources held by the cache backend, such as network connections or background goroutines.
// It should be called when the Cache instance is no longer needed, especially for caches that manage persistent connections.
// Implementations should ensure this method is safe to call multiple times.
Close() error
}
Cache defines the contract for a caching layer. It is used by the Manager to temporarily store marshalled user preferences for faster retrieval and to reduce load on the primary Storage backend. All methods that accept a context.Context should honor its cancellation and timeout signals. Implementations must be thread-safe for concurrent access. The cache key is typically a composite string generated by the Manager (e.g., "user_id:preference_key").
type Config ¶
type Config struct {
// contains filtered or unexported fields
}
Config holds the internal configuration for a Manager instance. It is populated by applying functional Options (e.g., WithStorage, WithCache) when a new Manager is created with New(). This struct is not intended to be instantiated or modified directly by users of the package.
type EncryptionAdapter ¶ added in v1.1.0
type EncryptionAdapter struct {
// contains filtered or unexported fields
}
EncryptionAdapter adapts the encryption.Manager to implement the EncryptionManager interface. This allows the encryption package to be used with the userprefs Manager.
func NewEncryptionAdapter ¶ added in v1.1.0
func NewEncryptionAdapter() (*EncryptionAdapter, error)
NewEncryptionAdapter creates a new EncryptionAdapter with encryption key from environment. It validates the encryption key during initialization for fast-fail scenarios. Returns an error if the key is missing or doesn't meet security requirements.
func NewEncryptionAdapterWithKey ¶ added in v1.1.0
func NewEncryptionAdapterWithKey(key []byte) (*EncryptionAdapter, error)
NewEncryptionAdapterWithKey creates a new EncryptionAdapter with a provided key. This is primarily used for testing. In production, use NewEncryptionAdapter() with environment variables.
type EncryptionManager ¶ added in v1.1.0
type EncryptionManager interface {
// Encrypt encrypts plaintext and returns the encrypted value as a string.
// The implementation should use secure encryption (AES-256-GCM) and handle
// base64 encoding of the result for storage compatibility.
// Returns an error if encryption fails.
Encrypt(plaintext string) (string, error)
// Decrypt decrypts an encrypted value and returns the original plaintext.
// The encrypted value is expected to be in the format produced by Encrypt.
// Returns an error if decryption fails or the encrypted value is invalid.
Decrypt(encrypted string) (string, error)
}
EncryptionManager defines the contract for encrypting and decrypting preference values. It provides AES-256 encryption capabilities for sensitive user preferences marked as encrypted. Implementations must be thread-safe for concurrent access from multiple goroutines.
type LogLevel ¶ added in v1.0.0
type LogLevel int
LogLevel defines the various log levels. These correspond to slog's levels. Using a custom type allows for clearer API contracts within the userprefs package.
const ( LogLevelDebug LogLevel = LogLevel(slog.LevelDebug) // Debug messages LogLevelInfo LogLevel = LogLevel(slog.LevelInfo) // Informational messages LogLevelWarn LogLevel = LogLevel(slog.LevelWarn) // Warning messages LogLevelError LogLevel = LogLevel(slog.LevelError) // Error messages )
Log level constants, mirroring slog levels for internal mapping.
type Logger ¶
type Logger interface {
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
SetLevel(level LogLevel) // New method to set the log level dynamically
}
Logger defines the interface for logging operations. This allows for different logging implementations to be used.
func NewDefaultLogger ¶ added in v1.0.0
func NewDefaultLogger() Logger
NewDefaultLogger initializes a new defaultSlogLogger instance. It defaults to a JSON handler writing to os.Stderr with slog.LevelInfo. The log level can be changed dynamically via the SetLevel method. This function is now exported.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager is the central component for managing user preferences. It handles the definition of preference types, and orchestrates their retrieval, storage, and caching.
A Manager requires a userprefs.Storage implementation for persistence and can optionally be configured with a userprefs.Cache implementation to improve performance for frequently accessed preferences. All public methods of the Manager are thread-safe and can be called concurrently from multiple goroutines.
Instances of Manager are typically created using the New() function, configured via Options.
func New ¶
New creates and initializes a new Manager instance using functional options. This is the primary constructor for a Manager.
Essential options, particularly WithStorage, must be provided for the Manager to function correctly. Other options like WithCache, WithLogger, and WithDefinition allow for further customization. Example usage:
storage := NewMemoryStorage()
cache := NewMemoryCache(100)
logger := log.New(os.Stdout, "userprefs: ", log.LstdFlags)
manager := userprefs.New(
userprefs.WithStorage(storage),
userprefs.WithCache(cache),
userprefs.WithLogger(logger),
)
The returned Manager is ready for use.
func (*Manager) DefinePreference ¶
func (m *Manager) DefinePreference(def PreferenceDefinition) error
DefinePreference registers a new preference definition with the Manager. Each preference key must be unique within a Manager instance. Re-defining an existing key will overwrite the previous definition.
A PreferenceDefinition includes:
- Key: A unique string identifier for the preference.
- Type: The expected data type (e.g., String, Bool, Int, Float, JSON).
- DefaultValue: The value to use if the user hasn't set this preference.
- Category: An optional string for grouping preferences.
- Encrypted: Whether the preference value should be encrypted at rest.
- ValidateFunc: An optional function for custom value validation during Set operations.
Returns:
- ErrInvalidKey: if def.Key is empty.
- ErrInvalidType: if def.Type is not a supported PreferenceType (see IsValidType).
- ErrEncryptionRequired: if def.Encrypted is true but no encryption manager is configured.
- nil: on successful registration of the preference definition.
This method is thread-safe.
func (*Manager) Delete ¶
Delete removes a user's preference for a given key. The provided context.Context can be used for cancellation or timeouts, propagated to storage/cache.
Operational Flow:
- Input Validation: Checks userID and key. Returns ErrInvalidInput if empty.
- Definition Check: Verifies key is defined. Returns ErrPreferenceNotDefined if not. (Note: This check ensures operations are only on known preference types, though the preference might not exist for this specific user in storage).
- Storage Operation: Deletes the preference from the storage backend. - If storage returns ErrNotFound, this is considered a successful deletion (idempotency), as the desired state (preference not present) is achieved. Returns nil error.
- Cache Invalidation (if cache is configured): Deletes the corresponding entry from the cache, regardless of whether the item was found in storage.
Returns:
- nil: On successful deletion or if the preference was not found in storage (idempotent).
- ErrInvalidInput: If userID or key is empty.
- ErrPreferenceNotDefined: If the preference key has not been defined.
- A wrapped storage error: If the storage deletion fails for reasons other than ErrNotFound.
This method is thread-safe.
func (*Manager) Get ¶
Get retrieves a user's preference for a given key. The provided context.Context can be used for cancellation or timeouts, which will be propagated to cache and storage operations.
Operational Flow:
- Input Validation: Checks if userID and key are non-empty. If not, returns ErrInvalidInput.
- Definition Check: Verifies that the preference key has been defined. If not, returns ErrPreferenceNotDefined.
- Cache Lookup (if cache is configured): a. Attempts to retrieve the preference from the cache. b. On cache hit (no error), the cached preference is returned after decryption if needed. c. On cache error (excluding specific 'not found' errors, if distinguishable by the cache implementation): Logs the error. Returns a Preference populated with the *defined default value* and the original cache error. This allows the application to continue with a default during transient cache issues. d. On cache miss (or 'not found' error), proceeds to storage lookup.
- Storage Lookup (if no cache, or cache miss): a. Fetches the preference from the storage backend. b. If found in storage: The retrieved preference value is decrypted if needed and returned. If a cache is configured, the preference is asynchronously stored in the cache for future requests. c. If storage returns ErrNotFound: A Preference struct populated with the *defined default value* is returned with a nil error (indicating successful application of default). d. If storage returns any other error: That error is wrapped and returned.
Returns:
- (*Preference, nil): On successful retrieval (from cache or storage) or when a defined default value is applied.
- (nil, ErrInvalidInput): If userID or key is empty.
- (nil, ErrPreferenceNotDefined): If the preference key has not been defined.
- (nil, ErrEncryptionFailed): If decryption is required but fails.
- (*Preference with default, wrapped cache error): If cache fails and a default is applied.
- (nil, wrapped storage error): If storage fails and a default cannot be applied or is not applicable.
This method is thread-safe.
func (*Manager) GetAll ¶
GetAll retrieves all preferences for a given userID. The provided context.Context can be used for cancellation or timeouts, propagated to storage.
Behavior:
- Validates userID. Returns ErrInvalidInput if empty.
- Fetches all preference definitions known to the manager.
- Fetches all preferences for the user directly from the storage backend using storage.GetAll.
- For each defined preference: a. If a corresponding preference is found in the storage results, that preference is used. Its DefaultValue, Type, and Category are updated from the definition to ensure consistency. The value is decrypted if the preference is marked as encrypted. b. If not found in storage, a new Preference struct is created using the DefaultValue from its definition. The Value field is set to this DefaultValue. c. The processed preference is added to the result map.
- If a cache is configured, all retrieved/defaulted preferences are asynchronously added to the cache.
Returns:
- (map[string]*Preference, nil): A map of preference keys to Preference structs on success. The map will be empty if the user has no (defined) preferences or if no preferences are defined.
- (nil, ErrInvalidInput): If userID is empty.
- (nil, ErrEncryptionFailed): If decryption is required but fails.
- (nil, wrapped storage error): If the storage.GetAll operation fails.
This method is thread-safe.
func (*Manager) GetAllDefinitions ¶ added in v1.0.0
func (m *Manager) GetAllDefinitions(_ context.Context) ([]*PreferenceDefinition, error)
GetAllDefinitions retrieves all preference definitions.
func (*Manager) GetByCategory ¶
func (m *Manager) GetByCategory(ctx context.Context, userID, category string) (map[string]*Preference, error)
GetByCategory retrieves all preferences for a given userID that belong to the specified category. The provided context.Context can be used for cancellation or timeouts, propagated to storage.
Behavior:
- Validates userID. Returns ErrInvalidInput if empty.
- Fetches preferences directly from the storage backend. This method *does not* currently utilize or interact with the cache.
- For each preference retrieved from storage, it ensures the DefaultValue from its definition is populated in the returned Preference struct and decrypts values if needed.
Returns:
- (map[string]*Preference, nil): A map of preference keys to Preference structs on success. The map will be empty if no preferences match the category or if the user has no preferences.
- (nil, ErrInvalidInput): If userID is empty.
- (nil, ErrEncryptionFailed): If decryption is required but fails.
- (nil, wrapped storage error): If the storage operation fails.
This method is thread-safe.
func (*Manager) GetDefinition ¶ added in v1.0.0
func (m *Manager) GetDefinition(key string) (PreferenceDefinition, bool)
GetDefinition retrieves the preference definition for a given key. It is the exported version of the internal getDefinition logic.
func (*Manager) Set ¶
Set creates or updates a user's preference for a given key with the provided value. The provided context.Context can be used for cancellation or timeouts, propagated to storage/cache.
Operational Flow:
- Input Validation: Checks userID and key. Returns ErrInvalidInput if empty.
- Definition Check: Verifies key is defined. Returns ErrPreferenceNotDefined if not.
- Type Validation: Ensures the provided `value` matches the `Type` in the PreferenceDefinition. Returns ErrInvalidValue if type mismatch (e.g., providing a string for an Int preference).
- Custom Validation: If `ValidateFunc` is set in PreferenceDefinition, it's called. Returns ErrInvalidValue if this custom validation fails.
- Encryption: If the preference is marked as encrypted, the value is encrypted before storage.
- Storage Operation: Saves the preference (UserID, Key, Value, Type, Category, DefaultValue from definition, and current UpdatedAt) to the storage backend.
- Cache Invalidation (if cache is configured): Deletes the corresponding entry from the cache to maintain consistency. Subsequent Get calls will fetch from storage and repopulate cache.
Returns:
- nil: On successful creation or update.
- ErrInvalidInput: If userID or key is empty.
- ErrPreferenceNotDefined: If the preference key has not been defined.
- ErrInvalidValue: If the provided value fails type validation or custom validation.
- ErrEncryptionFailed: If encryption is required but fails.
- A wrapped storage error: If the storage operation fails.
This method is thread-safe.
type Option ¶
type Option func(*Config)
Option defines the signature for a functional option that configures a Manager instance. Functions of this type are passed to New() to customize the Manager's behavior, such as setting its storage backend, cache, or logger. Each Option function takes a pointer to a Config struct and modifies it.
func WithCache ¶
WithCache is a functional option that sets the Cache implementation for the Manager. If a Cache is provided, the Manager will use it to cache frequently accessed preferences, potentially improving performance by reducing load on the Storage backend. This option is optional.
func WithEncryption ¶ added in v1.1.0
func WithEncryption(em EncryptionManager) Option
WithEncryption is a functional option that sets the encryption implementation for the Manager. If an EncryptionManager is provided, the Manager will use it to encrypt and decrypt preference values marked as encrypted in their PreferenceDefinition. This option is optional but required if any preferences are marked as encrypted.
func WithLogger ¶
WithLogger is a functional option that sets the Logger implementation for the Manager. The Manager will use the provided Logger for logging informational messages, warnings, and errors. If not set, a default logger (writing to os.Stderr) may be used. This option is optional.
func WithStorage ¶
WithStorage is a functional option that sets the Storage implementation for the Manager. The provided Storage (s) will be used for persisting and retrieving user preferences. This is a mandatory option for a functional Manager.
type Preference ¶
type Preference struct {
// UserID is the unique identifier for the user to whom this preference belongs.
UserID string `json:"user_id"`
// Key is the unique string identifier for this preference, matching a PreferenceDefinition.Key.
Key string `json:"key"`
// Value is the actual value set by the user for this preference.
// Its type should conform to the Type specified in the corresponding PreferenceDefinition.
Value interface{} `json:"value"`
// DefaultValue holds the default value for this preference, as defined in its PreferenceDefinition.
// This field is populated by the Manager when a preference is retrieved or set,
// ensuring consistency. It's typically used by the application if the user hasn't set a specific Value.
DefaultValue interface{} `json:"default_value,omitempty"`
// Type is the string representation of the preference's data type (e.g., "string", "bool", "int").
// It corresponds to the Type in the PreferenceDefinition.
Type string `json:"type"`
// Category is an optional grouping string for the preference, from its PreferenceDefinition.
Category string `json:"category,omitempty"`
// UpdatedAt records the time when this preference was last set or modified in storage.
UpdatedAt time.Time `json:"updated_at"`
}
Preference represents a single user preference setting as stored and retrieved by the system. It encapsulates the user's specific value for a defined preference, along with metadata. JSON tags are included for serialization, typically used by storage implementations.
type PreferenceDefinition ¶
type PreferenceDefinition struct {
// Key is the unique string identifier for the preference type.
// This key is used to get, set, and define the preference.
Key string `json:"key"`
// Type is the expected data type of the preference's value (e.g., "string", "bool", "int", "float", "json").
// The Manager uses this for basic type validation when a preference is set.
Type string `json:"type"`
// DefaultValue is the value to be used if a user has not explicitly set this preference.
// The type of DefaultValue must match the specified Type.
DefaultValue interface{} `json:"default_value,omitempty"`
// Category is an optional string used to group related preferences. This can be useful for UI organization
// or for retrieving related sets of preferences (e.g., via Manager.GetByCategory).
Category string `json:"category,omitempty"`
// AllowedValues, if provided, restricts the preference's value to one of the items in this slice.
// The Manager checks against these values during Set operations if Type validation passes.
// The types of elements in AllowedValues must match the specified Type.
AllowedValues []interface{} `json:"allowed_values,omitempty"`
// Encrypted indicates whether this preference's value should be encrypted at rest.
// When true, the Manager will encrypt the value before storing it and decrypt it when retrieving.
// Defaults to false. Requires USERPREFS_ENCRYPTION_KEY environment variable to be set.
Encrypted bool `json:"encrypted,omitempty"`
// ValidateFunc is an optional custom function that can be provided to perform complex validation
// on a preference's value when it is being set. It is called after basic type checking and
// AllowedValues checks (if any). The function receives the value to be validated and should
// return nil if validation passes, or an error if it fails.
// The `json:"-"` tag indicates this field is not serialized to JSON.
ValidateFunc func(value interface{}) error `json:"-"`
}
PreferenceDefinition defines the schema, constraints, and default behavior for a particular preference key. Instances of PreferenceDefinition are registered with the Manager to make preferences known to the system. JSON tags are included for potential serialization, though definitions are typically configured in code.
type PreferenceType ¶ added in v1.0.0
type PreferenceType string
PreferenceType is a string alias for preference data types. Using a defined type allows for better type safety and discoverability compared to raw strings throughout the codebase. Deprecated: Use explicit string literals like "string", "bool", etc. The constants below are preferred for defining definition types.
type Storage ¶
type Storage interface {
// Get retrieves a specific Preference for a given userID and key.
// It must return a non-nil Preference and a nil error on success.
// If the preference is not found, it must return nil for the Preference and userprefs.ErrNotFound error.
// Other errors may be returned for issues like database connectivity problems.
Get(ctx context.Context, userID, key string) (*Preference, error)
// Set creates a new preference or updates an existing one (upsert operation).
// The provided Preference struct contains all necessary information (UserID, Key, Value, etc.).
// Implementations should ensure that the UpdatedAt field of the stored preference is set to the current time.
// It returns a nil error on success, or an error if the operation fails (e.g., due to database issues or serialization problems).
Set(ctx context.Context, pref *Preference) error
// Delete removes a specific preference for a given userID and key.
// This method must be idempotent: it should return a nil error even if the
// preference does not exist or has already been deleted.
// An error should only be returned for underlying storage issues.
Delete(ctx context.Context, userID, key string) error
// GetAll retrieves all preferences associated with a specific userID.
// It returns a map where keys are preference keys and values are pointers to Preference structs.
// If the user has no preferences, it must return a non-nil, empty map and a nil error.
// An error is returned for underlying storage issues.
GetAll(ctx context.Context, userID string) (map[string]*Preference, error)
// GetByCategory retrieves all preferences for a specific userID that belong to the specified category.
// It returns a map where keys are preference keys and values are pointers to Preference structs.
// If the user has no preferences in the given category, or the category does not exist,
// it must return a non-nil, empty map and a nil error.
// An error is returned for underlying storage issues.
GetByCategory(ctx context.Context, userID, category string) (map[string]*Preference, error)
// Close releases any resources held by the storage backend, such as database connections.
// It should be called when the Storage instance is no longer needed to prevent resource leaks.
// Implementations should ensure this method is safe to call multiple times, though typically called once.
Close() error
}
Storage defines the contract for persistent storage and retrieval of user preferences. Implementations are responsible for interacting with the underlying data store (e.g., SQL database, NoSQL database, file system). All methods that accept a context.Context should honor its cancellation and timeout signals. Implementations must be thread-safe, allowing for concurrent access from multiple goroutines.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package api provides HTTP handlers, middleware, and routing for the user preferences service.
|
Package api provides HTTP handlers, middleware, and routing for the user preferences service. |
|
Package cache provides in-memory caching implementations.
|
Package cache provides in-memory caching implementations. |
|
cmd
|
|
|
userprefs-server
command
Package main is the entry point for the userprefs-server application.
|
Package main is the entry point for the userprefs-server application. |
|
Package encryption provides AES-256 encryption/decryption capabilities for user preferences.
|
Package encryption provides AES-256 encryption/decryption capabilities for user preferences. |
|
Package storage provides a PostgreSQL-based implementation of the Storage interface.
|
Package storage provides a PostgreSQL-based implementation of the Storage interface. |