Compare commits

...

4 commits

Author SHA1 Message Date
Patrick Stewart
245321849a add: adding support for creating examples and templates 2024-09-06 11:54:27 -07:00
Patrick Stewart
204b1b998e update: updating files with detailed comments 2024-09-06 11:53:40 -07:00
Patrick Stewart
2cb685578b update: updating files with detailed comments 2024-09-06 11:52:59 -07:00
Patrick Stewart
e7f8083b25 update: updating files with detailed comments 2024-09-06 11:52:15 -07:00
76 changed files with 4333 additions and 327 deletions

View file

View file

0
examples/app-template/.gitattributes vendored Normal file
View file

25
examples/app-template/.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Ignore autosave files
README.md~
.gitignore~
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map

View file

@ -0,0 +1,3 @@
## 1.0.0
- Initial version.

View file

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2024 Patrick Stewart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,42 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/protevus/branding/main/protevus-logo-bg.png"></a></p>
## About Protevus
> **Note:** This repository provides a base application setup inspired by Laravel. If you want to contribute to the core Protevus Platform, visit the main [Protevus Platform repository](https://github.com/protevus/platform).
Protevus is a reference implementation that demonstrates how to build applications using the Protevus Platform. It serves as a starting point for developers to create their own applications leveraging the power and versatility of the Protevus Platform.
## Features
- **Laravel-inspired Structure**: The starter application follows a structure and conventions inspired by the Laravel framework, making it familiar to Laravel developers.
- **Protevus Platform Integration**: The application is built on top of the Protevus Platform, providing a Laravel-compatible API and development experience.
- **Example Components**: The repository includes example components such as controllers, middleware, and services, showcasing best practices and usage patterns.
- **Extensibility**: Developers can easily extend and customize the starter application to meet their specific project requirements.
## Getting Started
To get started with the Protevus Starter Application, follow these steps:
1. **Install Dependencies**: Ensure you have the Dart SDK and the necessary dependencies installed on your system.
2. **Clone the Repository**: Clone the Protevus Starter Application repository to your local machine.
3. **Build and Run**: Follow the instructions in the repository's documentation to build and run the application.
## Documentation
Documentation for the Protevus Starter Application is available at [protevus.com/docs/protevus](https://protevus.com/docs/protevus). The documentation covers installation, configuration, customization, and deployment guides.
## Contributing
We welcome contributions from the community! If you'd like to contribute to the Protevus Starter Application, please follow the guidelines outlined in the [CONTRIBUTING.md](CONTRIBUTING.md) file.
## License
The Protevus Application is released under the [MIT License](LICENSE).
## Support and Community
If you have any questions, issues, or suggestions, please join our community:
- **GitHub Discussions**: [github.com/protevus/protevus/discussions](https://github.com/protevus/protevus/discussions)
- **Twitter**: [@Protevus](https://twitter.com/Protevus)

View file

@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options

View file

View file

View file

View file

View file

View file

View file

View file

View file

@ -0,0 +1,14 @@
{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.11.0/.schema/devbox.schema.json",
"packages": ["dart@latest"],
"shell": {
"init_hook": [
"echo 'Welcome to devbox!' > /dev/null"
],
"scripts": {
"test": [
"echo \"Error: no test specified\" && exit 1"
]
}
}
}

View file

@ -0,0 +1,53 @@
{
"lockfile_version": "1",
"packages": {
"dart@latest": {
"last_modified": "2024-06-03T07:19:07Z",
"resolved": "github:NixOS/nixpkgs/4a4ecb0ab415c9fccfb005567a215e6a9564cdf5#dart",
"source": "devbox-search",
"version": "3.4.2",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/9piqr817cdsgmz31m8q723lxhcpgqsa4-dart-3.4.2",
"default": true
}
],
"store_path": "/nix/store/9piqr817cdsgmz31m8q723lxhcpgqsa4-dart-3.4.2"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/1j3h5yqxvgzakv5gir1ssg7wggwxhmsd-dart-3.4.2",
"default": true
}
],
"store_path": "/nix/store/1j3h5yqxvgzakv5gir1ssg7wggwxhmsd-dart-3.4.2"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/k8a6gkss3s19p5dhbzgbdqqk5b8qzd7d-dart-3.4.2",
"default": true
}
],
"store_path": "/nix/store/k8a6gkss3s19p5dhbzgbdqqk5b8qzd7d-dart-3.4.2"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/wbj1csi5fk2w99aiglwgg1mv406pw4pn-dart-3.4.2",
"default": true
}
],
"store_path": "/nix/store/wbj1csi5fk2w99aiglwgg1mv406pw4pn-dart-3.4.2"
}
}
}
}
}

View file

View file

@ -0,0 +1,28 @@
name: app-template
description: An absolute bare-bones protevus app.
version: 1.0.0
# repository: https://github.com/my_org/my_repo
environment:
sdk: ^3.4.0
# Add regular dependencies here.
dependencies:
web: ^0.5.1
dev_dependencies:
build_runner: ^2.4.8
build_web_compilers: ^4.0.9
lints: ^3.0.0
# Override default source directory
source_directories:
- app
- bootstrap
- config
- database
- public
- resources
- routes
- storage
- tests

View file

View file

View file

View file

View file

@ -2,6 +2,7 @@ name: protevus_platform
repository: https://github.com/protevus/platform
packages:
- packages/**
- examples/**
command:
version:

View file

@ -7,6 +7,14 @@
* file that was distributed with this source code.
*/
/// This library exports various components of the application framework.
///
/// It includes:
/// - Application and ApplicationServer classes for managing the application lifecycle
/// - Channel for handling request/response cycles
/// - IsolateApplicationServer and IsolateSupervisor for managing isolates
/// - Options for configuring the application
/// - Starter for initializing and running the application
library;
export 'src/application.dart';

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
@ -11,56 +20,138 @@ export 'application_server.dart';
export 'options.dart';
export 'starter.dart';
/// This object starts and stops instances of your [ApplicationChannel].
/// The Application class is responsible for starting and managing instances of an ApplicationChannel.
///
/// An application object opens HTTP listeners that forward requests to instances of your [ApplicationChannel].
/// It is unlikely that you need to use this class directly - the `conduit serve` command creates an application object
/// on your behalf.
class Application<T extends ApplicationChannel> {
/// A list of isolates that this application supervises.
///
/// This list contains [ApplicationIsolateSupervisor] instances, each representing
/// an isolate running a separate instance of the application. These supervisors
/// are responsible for managing the lifecycle and communication with their
/// respective isolates. The list is populated when the application starts and
/// is cleared when the application stops.
List<ApplicationIsolateSupervisor> supervisors = [];
/// The [ApplicationServer] listening for HTTP requests while under test.
///
/// This property is only valid when an application is started via [startOnCurrentIsolate].
/// It represents the server instance that handles incoming HTTP requests during testing.
/// The server is initialized when the application starts on the current isolate and
/// provides access to the underlying HTTP server and application channel.
///
/// Note: This property should not be accessed before calling [startOnCurrentIsolate],
/// as it will not be initialized until then.
late ApplicationServer server;
/// The [ApplicationChannel] handling requests while under test.
///
/// This property is only valid when an application is started via [startOnCurrentIsolate]. You use
/// this value to access elements of your application channel during testing.
/// This property provides access to the application channel instance when the application
/// is started using [startOnCurrentIsolate]. It allows direct interaction with the channel
/// during testing, enabling access to its properties and methods.
///
/// The returned value is cast to type [T], which should match the generic type parameter
/// of the [Application] class.
///
/// This property is only valid and accessible after calling [startOnCurrentIsolate].
/// Attempting to access it before starting the application or when using [start] instead
/// of [startOnCurrentIsolate] may result in unexpected behavior or errors.
///
/// Usage example:
/// ```dart
/// final app = Application<MyChannel>();
/// await app.startOnCurrentIsolate();
/// final myChannel = app.channel;
/// // Now you can interact with myChannel for testing purposes
/// ```
T get channel => server.channel as T;
/// The logger that this application will write messages to.
///
/// This logger's name will appear as 'conduit'.
Logger logger = Logger("conduit");
/// This logger is used throughout the application to record messages, errors,
/// and other important information. It is configured with the name 'protevus',
/// which will appear as the source of all log messages generated by this logger.
///
/// The Logger class is likely from a logging package, providing various methods
/// for different log levels (e.g., info, warning, error) and potentially
/// supporting different output destinations or formatting options.
///
/// Usage of this logger helps in debugging, monitoring, and maintaining the
/// application by providing a centralized way to capture and analyze runtime
/// information.
Logger logger = Logger("protevus");
/// The options used to configure this application.
///
/// Changing these values once the application has started will have no effect.
/// This property holds an instance of [ApplicationOptions] that contains various
/// configuration settings for the application. These options can include things
/// like port numbers, database configurations, or any other application-specific
/// settings.
///
/// The options are typically set before the application is started. It's important
/// to note that modifying these options after the application has been started
/// will not have any effect on the running application.
///
/// Example usage:
/// ```dart
/// final app = Application<MyChannel>();
/// app.options.port = 8080;
/// app.options.configurationFilePath = 'config.yaml';
/// await app.start();
/// ```
///
/// Default value is an instance of [ApplicationOptions] with default settings.
ApplicationOptions options = ApplicationOptions();
/// The duration to wait for each isolate during startup before failing.
///
/// A [TimeoutException] is thrown if an isolate fails to startup in this time period.
/// This property sets the maximum time allowed for each isolate to start up
/// during the application's initialization process. If an isolate fails to
/// complete its startup within this time frame, a [TimeoutException] is thrown.
///
/// Defaults to 30 seconds.
Duration isolateStartupTimeout = const Duration(seconds: 30);
/// Whether or not this application is running.
/// Indicates whether the application is currently running.
///
/// This will return true if [start]/[startOnCurrentIsolate] have been invoked and completed; i.e. this is the synchronous version of the [Future] returned by [start]/[startOnCurrentIsolate].
///
/// This value will return to false after [stop] has completed.
bool get isRunning => _hasFinishedLaunching;
/// Indicates whether the application has finished launching.
///
/// This boolean flag is set to true once the application has successfully
/// completed its startup process, including initializing all isolates and
/// opening HTTP listeners. It is used internally to track the application's
/// running state and is consulted by the [isRunning] getter.
///
/// The value is set to false initially and when the application is stopped,
/// and set to true at the end of successful [start] or [startOnCurrentIsolate]
/// method execution.
bool _hasFinishedLaunching = false;
/// Retrieves the [ChannelRuntime] for the current application channel type.
///
/// This getter accesses the [RuntimeContext.current] map using the generic type [T]
/// (which represents the application's channel type) as the key. It then casts
/// the retrieved value to [ChannelRuntime].
///
/// The [ChannelRuntime] object contains runtime information and utilities
/// specific to the channel type, which are used in various parts of the
/// application for setup, initialization, and execution.
///
/// This getter is used internally by the Application class to access
/// channel-specific runtime information without exposing it publicly.
ChannelRuntime get _runtime => RuntimeContext.current[T] as ChannelRuntime;
/// Starts this application, allowing it to handle HTTP requests.
///
/// This method spawns [numberOfInstances] isolates, instantiates your application channel
/// for each of these isolates, and opens an HTTP listener that sends requests to these instances.
/// This method initializes the application by spawning a specified number of isolates,
/// each running an instance of the application channel. It then sets up an HTTP listener
/// to distribute incoming requests across these isolates.
///
/// The [Future] returned from this method will complete once all isolates have successfully started
/// and are available to handle requests.
@ -110,7 +201,7 @@ class Application<T extends ApplicationChannel> {
_hasFinishedLaunching = true;
}
/// Starts the application on the current isolate, and does not spawn additional isolates.
/// Starts the application on the current isolate without spawning additional isolates.
///
/// An application started in this way will run on the same isolate this method is invoked on.
/// Performance is limited when running the application with this method; prefer to use [start].
@ -139,8 +230,20 @@ class Application<T extends ApplicationChannel> {
/// Stops the application from running.
///
/// Closes every isolate and their channel and stops listening for HTTP requests.
/// The [ServiceRegistry] will close any of its resources.
/// This method performs the following actions:
/// 1. Sets the '_hasFinishedLaunching' flag to false.
/// 2. Stops all supervisor isolates concurrently.
/// 3. Handles potential errors during supervisor shutdown, particularly checking for 'LateError'.
/// 4. Attempts to close the server forcefully.
/// 5. Logs any errors that occur during server closure.
/// 6. Resets the '_hasFinishedLaunching' flag and clears the supervisors list.
/// 7. Removes all listeners from the logger.
///
/// If a 'LateError' is encountered during supervisor shutdown, it throws a [StateError]
/// indicating that the channel type was not properly loaded.
///
/// This method ensures a clean shutdown of all application components and should be
/// called when the application needs to be terminated.
Future stop() async {
_hasFinishedLaunching = false;
await Future.wait(supervisors.map((s) => s.stop()))
@ -167,6 +270,26 @@ class Application<T extends ApplicationChannel> {
/// Creates an [APIDocument] from an [ApplicationChannel].
///
/// This static method generates API documentation for a given application channel type.
/// It is primarily used by the 'conduit document' CLI command to create OpenAPI (formerly Swagger) documentation.
///
/// The method performs the following steps:
/// 1. Retrieves the runtime context for the specified channel type.
/// 2. Runs global initialization with the provided configuration.
/// 3. Creates an ApplicationServer instance.
/// 4. Prepares the channel.
/// 5. Generates the API documentation.
/// 6. Closes the channel.
/// 7. Returns the generated APIDocument.
///
/// Parameters:
/// - [type]: The Type of the ApplicationChannel subclass.
/// - [config]: The ApplicationOptions containing configuration for the application.
/// - [projectSpec]: A Map containing additional project-specific information for the documentation.
///
/// Returns:
/// A Future that resolves to an [APIDocument] containing the generated API documentation.
///
/// This method is called by the `conduit document` CLI.
static Future<APIDocument> document(
Type type,
@ -188,6 +311,25 @@ class Application<T extends ApplicationChannel> {
return doc;
}
/// Spawns a new isolate to run an instance of the application.
///
/// This method creates a new isolate that runs an instance of the application channel.
/// It sets up the necessary communication channels and initializes the isolate with
/// the application's configuration and runtime information.
///
/// Parameters:
/// - [application]: The main Application instance.
/// - [config]: ApplicationOptions containing the configuration for this instance.
/// - [identifier]: A unique identifier for this isolate.
/// - [logger]: The Logger instance for logging.
/// - [startupTimeout]: The maximum duration allowed for the isolate to start up.
/// - [logToConsole]: Whether to enable console logging for this isolate (default: false).
///
/// Returns:
/// A Future that resolves to an [ApplicationIsolateSupervisor] managing the new isolate.
///
/// This method is crucial for scaling the application across multiple isolates,
/// allowing for better performance and resource utilization.
Future<ApplicationIsolateSupervisor> _spawn(
Application application,
ApplicationOptions config,
@ -224,7 +366,7 @@ class Application<T extends ApplicationChannel> {
}
}
/// Thrown when an application encounters an exception during startup.
/// Represents an exception that occurs during the startup process of an application.
///
/// Contains the original exception that halted startup.
class ApplicationStartupException implements Exception {

View file

@ -1,17 +1,38 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:io';
import 'package:logging/logging.dart';
import 'package:protevus_application/application.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_runtime/runtime.dart';
import 'package:logging/logging.dart';
/// Listens for HTTP requests and delivers them to its [ApplicationChannel] instance.
/// A class representing an application server in the Conduit framework.
///
/// A Conduit application creates instances of this type to pair an HTTP server and an
/// instance of an [ApplicationChannel] subclass. Instances are created by [Application]
/// and shouldn't be created otherwise.
/// The ApplicationServer class is responsible for managing the lifecycle of an HTTP server
/// and its associated [ApplicationChannel]. It handles server creation, starting, and stopping,
/// as well as routing incoming requests to the appropriate handlers.
///
/// Key features:
/// - Creates and manages an instance of [ApplicationChannel]
/// - Configures and starts an HTTP or HTTPS server
/// - Handles incoming requests and routes them to the appropriate controller
/// - Manages server lifecycle (start, stop, close)
/// - Provides logging capabilities
/// - Supports both IPv4 and IPv6
/// - Handles secure connections with SSL/TLS
///
/// This class is typically instantiated and managed by the Application class and should not
/// be created directly in most cases
class ApplicationServer {
/// Creates a new server.
/// Creates a new server instance.
///
/// You should not need to invoke this method directly.
ApplicationServer(this.channelType, this.options, this.identifier) {
@ -21,41 +42,145 @@ class ApplicationServer {
..options = options;
}
/// The configuration this instance used to start its [channel].
/// The configuration options used to start this server's [channel].
///
/// This property holds an instance of [ApplicationOptions] which contains
/// various settings used to configure the server, such as the address to bind to,
/// the port number, SSL/TLS settings, and other application-specific options.
/// These options are passed to the [ApplicationChannel] when it is initialized.
ApplicationOptions options;
/// The underlying [HttpServer].
/// The underlying [HttpServer] instance used by this [ApplicationServer].
///
/// This property represents the core HTTP server that handles incoming requests.
/// It is initialized when the server starts and is used throughout the lifecycle
/// of the [ApplicationServer] to manage incoming connections and route requests
/// to the appropriate handlers.
///
/// The server can be either a standard HTTP server or an HTTPS server, depending
/// on the configuration and security context provided during initialization.
late final HttpServer server;
/// The instance of [ApplicationChannel] serving requests.
///
/// This property represents the primary request handling pipeline for the application.
/// It is instantiated when the ApplicationServer is created and is responsible for
/// processing incoming HTTP requests, routing them to appropriate controllers,
/// and generating responses.
///
/// The [ApplicationChannel] is a custom class defined by the application developer
/// that sets up the request handling logic, including middleware, controllers,
/// and other application-specific components.
late ApplicationChannel channel;
/// The cached entrypoint of [channel].
///
/// This property stores the main [Controller] that serves as the entry point for request handling.
/// It is initialized when the server starts and is used to process incoming HTTP requests.
/// The entrypoint controller typically represents the root of the request handling pipeline
/// and may delegate to other controllers or middleware as needed.
late Controller entryPoint;
/// The type of [ApplicationChannel] this server will use.
///
/// This property stores the Type of the ApplicationChannel subclass that will be
/// instantiated and used by this ApplicationServer. The ApplicationChannel
/// defines the request handling logic and routing for the application.
final Type channelType;
/// Target for sending messages to other [ApplicationChannel.messageHub]s.
///
/// Events are added to this property by instances of [ApplicationMessageHub] and should not otherwise be used.
/// This property represents an [EventSink] that can be used to send messages
/// to other [ApplicationChannel.messageHub]s across different instances of
/// the application. It is primarily used for inter-server communication in
/// distributed setups.
///
/// The [hubSink] is typically set and managed by instances of [ApplicationMessageHub].
/// Application developers should not directly modify or use this property, as it is
/// intended for internal framework use.
///
/// The sink can be null if no message hub has been configured for this server.
EventSink<dynamic>? hubSink;
/// Whether or not this server requires an HTTPS listener.
/// Indicates whether this server requires an HTTPS listener.
///
/// This getter returns a boolean value that determines if the server
/// should use HTTPS instead of HTTP. It is typically set to true when
/// a security context is provided during server initialization.
///
/// Returns:
/// [bool]: true if the server requires HTTPS, false otherwise.
bool get requiresHTTPS => _requiresHTTPS;
/// Indicates whether this server instance is configured to use HTTPS.
///
/// This private variable is set to true when a security context is provided
/// during server initialization, indicating that the server should use HTTPS.
/// It is used internally to determine the server's connection type and is
/// accessed through the public getter [requiresHTTPS].
///
/// The value is false by default, assuming HTTP connection, and is only set to
/// true when HTTPS is explicitly configured.
bool _requiresHTTPS = false;
/// The unique identifier of this instance.
///
/// Each instance has its own identifier, a numeric value starting at 1, to identify it
/// among other instances.
///
/// This identifier is used to distinguish between different [ApplicationServer] instances
/// when multiple servers are running concurrently. It's particularly useful for logging
/// and debugging purposes, allowing developers to trace which server instance is handling
/// specific requests or operations.
///
/// The identifier is typically assigned automatically by the [Application] class when
/// creating new server instances, ensuring that each server has a unique number.
///
/// Example:
/// If three server instances are created, they might have identifiers 1, 2, and 3 respectively.
int identifier;
/// The logger of this instance
Logger get logger => Logger("conduit");
/// Returns the logger instance for this ApplicationServer.
///
/// This getter provides access to a [Logger] instance specifically configured
/// for the Conduit framework. The logger is named "conduit" and can be used
/// throughout the ApplicationServer and its associated classes for consistent
/// logging purposes.
///
/// The logger can be used to record various levels of information, warnings,
/// and errors during the server's operation, which is crucial for debugging
/// and monitoring the application's behavior.
///
/// Returns:
/// A [Logger] instance named "conduit".
Logger get logger => Logger("protevus");
/// Starts this instance, allowing it to receive HTTP requests.
///
/// Do not invoke this method directly.
/// This method initializes the server, preparing it to handle incoming HTTP requests.
/// It performs the following steps:
/// 1. Prepares the channel by calling [channel.prepare()].
/// 2. Sets up the entry point for request handling.
/// 3. Binds the HTTP server to the specified address and port.
/// 4. Configures HTTPS if a security context is provided.
///
/// The method supports both HTTP and HTTPS connections, determined by the presence
/// of a security context. It also handles IPv6 configuration and server sharing options.
///
/// Parameters:
/// [shareHttpServer] - A boolean indicating whether to share the HTTP server
/// across multiple instances. Defaults to false.
///
/// Returns:
/// A [Future] that completes when the server has successfully started and is
/// ready to receive requests.
///
/// Throws:
/// May throw exceptions related to network binding or security context configuration.
///
/// Note:
/// This method should not be invoked directly under normal circumstances.
/// It is typically called by the framework during the application startup process.
Future start({bool shareHttpServer = false}) async {
logger.fine("ApplicationServer($identifier).start entry");
@ -92,7 +217,21 @@ class ApplicationServer {
return didOpen();
}
/// Closes this HTTP server and channel.
/// Closes this HTTP server and associated channel.
///
/// This method performs the following steps:
/// 1. Closes the HTTP server, forcibly terminating any ongoing connections.
/// 2. Closes the associated [ApplicationChannel].
/// 3. Closes the [hubSink] if it exists.
///
/// The method logs the progress of each step for debugging purposes.
///
/// Returns:
/// A [Future] that completes when all closing operations are finished.
///
/// Note:
/// The [hubSink] is actually closed by channel.messageHub.close, but it's
/// explicitly closed here to satisfy the Dart analyzer.
Future close() async {
logger.fine("ApplicationServer($identifier).close Closing HTTP listener");
await server.close(force: true);
@ -104,7 +243,7 @@ class ApplicationServer {
logger.fine("ApplicationServer($identifier).close Closing complete");
}
/// Invoked when this server becomes ready receive requests.
/// Invoked when this server becomes ready to receive requests.
///
/// [ApplicationChannel.willStartReceivingRequests] is invoked after this opening has completed.
Future didOpen() async {
@ -117,6 +256,18 @@ class ApplicationServer {
logger.info("Server conduit/$identifier started.");
}
/// Sends an application event.
///
/// This method is designed to handle application-wide events. By default,
/// it does nothing and serves as a placeholder for potential event handling
/// implementations in derived classes.
///
/// Parameters:
/// [event]: A dynamic object representing the event to be sent.
/// It can be of any type, allowing flexibility in event structures.
///
/// Note:
/// Override this method in subclasses to implement specific event handling logic.
void sendApplicationEvent(dynamic event) {
// By default, do nothing
}

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:io';
@ -9,7 +18,7 @@ import 'package:protevus_runtime/runtime.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
/// An object that defines the behavior specific to your application.
/// An abstract class that defines the behavior specific to your application.
///
/// You create a subclass of [ApplicationChannel] to initialize your application's services and define how HTTP requests are handled by your application.
/// There *must* only be one subclass in an application and it must be visible to your application library file, e.g., 'package:my_app/my_app.dart'.
@ -21,7 +30,7 @@ import 'package:meta/meta.dart';
/// When your application is started, an instance of your application channel is created for each isolate (see [Application.start]). Each instance
/// is a replica of your application that runs in its own memory isolated thread.
abstract class ApplicationChannel implements APIComponentDocumenter {
/// You implement this method to provide global initialization for your application.
/// Provides global initialization for the application.
///
/// Most of your application initialization code is written in [prepare], which is invoked for each isolate. For initialization that
/// needs to occur once per application start, you must provide an implementation for this method. This method is invoked prior
@ -51,27 +60,43 @@ abstract class ApplicationChannel implements APIComponentDocumenter {
/// is for documentation purposes.
static Future initializeApplication(ApplicationOptions options) async {}
/// The logger that this object will write messages to.
/// Returns a Logger instance for this object.
///
/// This logger's name appears as 'conduit'.
Logger get logger => Logger("conduit");
Logger get logger => Logger("protevus");
/// The [ApplicationServer] that sends HTTP requests to this object.
/// Returns the [ApplicationServer] instance that sends HTTP requests to this object.
///
/// This getter provides access to the server associated with this ApplicationChannel.
/// The server is responsible for handling incoming HTTP requests and routing them
/// to the appropriate controllers within the channel.
ApplicationServer get server => _server;
/// Sets the ApplicationServer for this channel and establishes message hub connections.
///
/// This setter method performs two main tasks:
/// 1. It assigns the provided [server] to the private [_server] variable.
/// 2. It sets up the message hub connections:
/// - It adds a listener to the outbound stream of the messageHub, which sends
/// application events through the server.
/// - It sets the inbound sink of the messageHub as the hubSink of the server.
///
/// This setup allows for inter-isolate communication through the ApplicationMessageHub.
///
/// [server] The ApplicationServer instance to be set for this channel.
set server(ApplicationServer server) {
_server = server;
messageHub._outboundController.stream.listen(server.sendApplicationEvent);
server.hubSink = messageHub._inboundController.sink;
}
/// Use this object to send data to channels running on other isolates.
/// A messaging hub for inter-isolate communication within the application.
///
/// You use this object to synchronize state across the isolates of an application. Any data sent
/// through this object will be received by every other channel in your application (except the one that sent it).
final ApplicationMessageHub messageHub = ApplicationMessageHub();
/// The context used for setting up HTTPS in an application.
/// Returns a SecurityContext for HTTPS configuration if certificate and private key files are provided.
///
/// If this value is non-null, the [server] receiving HTTP requests will only accept requests over HTTPS.
///
@ -93,6 +118,17 @@ abstract class ApplicationChannel implements APIComponentDocumenter {
///
/// These options are set when starting the application. Changes to this object have no effect
/// on other isolates.
///
/// This property holds an instance of [ApplicationOptions] which contains various
/// configuration settings for the application. These options are typically set
/// during the application's startup process.
///
/// The options stored here are specific to this channel instance and do not
/// affect other isolates running in the application. This means that modifying
/// these options at runtime will only impact the current isolate.
///
/// The property is nullable, allowing for cases where options might not be set
/// or where default configurations are used in the absence of specific options.
ApplicationOptions? options;
/// You implement this accessor to define how HTTP requests are handled by your application.
@ -112,9 +148,17 @@ abstract class ApplicationChannel implements APIComponentDocumenter {
/// }
Controller get entryPoint;
/// The [ApplicationServer] instance associated with this channel.
///
/// This private variable stores the server that handles HTTP requests for this
/// ApplicationChannel. It is marked as 'late' because it will be initialized
/// after the channel is created, typically when the 'server' setter is called.
///
/// The server is responsible for managing incoming HTTP connections and
/// routing requests to the appropriate controllers within the channel.
late ApplicationServer _server;
/// You override this method to perform initialization tasks.
/// Performs initialization tasks for the application channel.
///
/// This method allows this instance to perform any initialization (other than setting up the [entryPoint]). This method
/// is often used to set up services that [Controller]s use to fulfill their duties. This method is invoked
@ -123,12 +167,12 @@ abstract class ApplicationChannel implements APIComponentDocumenter {
/// By default, this method does nothing.
Future prepare() async {}
/// You override this method to perform initialization tasks that occur after [entryPoint] has been established.
/// Overridable method called just before the application starts receiving requests.
///
/// Override this method to take action just before [entryPoint] starts receiving requests. By default, does nothing.
void willStartReceivingRequests() {}
/// You override this method to release any resources created in [prepare].
/// Releases resources and performs cleanup when the application channel is closing.
///
/// This method is invoked when the owning [Application] is stopped. It closes open ports
/// that this channel was using so that the application can be properly shut down.
@ -146,7 +190,8 @@ abstract class ApplicationChannel implements APIComponentDocumenter {
/// Creates an OpenAPI document for the components and paths in this channel.
///
/// This method invokes [entryPoint] and [prepare] before starting the documentation process.
/// This method generates a complete OpenAPI specification document for the application,
/// including all components, paths, and operations defined in the channel.
///
/// The documentation process first invokes [documentComponents] on this channel. Every controller in the channel will have its
/// [documentComponents] methods invoked. Any declared property
@ -181,6 +226,24 @@ abstract class ApplicationChannel implements APIComponentDocumenter {
return doc;
}
/// Documents the components of this ApplicationChannel and its controllers.
///
/// This method is responsible for generating API documentation for the components
/// of this ApplicationChannel and its associated controllers. It performs the following tasks:
///
/// 1. Calls `documentComponents` on the entry point controller, which typically
/// initiates the documentation process for all linked controllers.
///
/// 2. Retrieves all documentable channel components using the ChannelRuntime,
/// which are typically services or other objects that implement APIComponentDocumenter.
///
/// 3. Calls `documentComponents` on each of these channel components, allowing
/// them to add their own documentation to the API registry.
///
/// This method is marked with @mustCallSuper, indicating that subclasses
/// overriding this method must call the superclass implementation.
///
/// [registry] The APIDocumentContext used to store and organize the API documentation.
@mustCallSuper
@override
void documentComponents(APIDocumentContext registry) {
@ -194,7 +257,7 @@ abstract class ApplicationChannel implements APIComponentDocumenter {
}
}
/// An object that sends and receives messages between [ApplicationChannel]s.
/// An object that facilitates message passing between [ApplicationChannel]s in different isolates.
///
/// You use this object to share information between isolates. Each [ApplicationChannel] has a property of this type. A message sent through this object
/// is received by every other channel through its hub.
@ -218,14 +281,44 @@ abstract class ApplicationChannel implements APIComponentDocumenter {
/// }
/// });
class ApplicationMessageHub extends Stream<dynamic> implements Sink<dynamic> {
final Logger _logger = Logger("conduit");
/// A logger instance for the ApplicationMessageHub.
///
/// This logger is used to log messages and errors related to the ApplicationMessageHub.
/// It is named "protevus" to identify logs from this specific component.
final Logger _logger = Logger("protevus");
/// A StreamController for outbound messages.
///
/// This controller manages the stream of outbound messages sent from this
/// ApplicationMessageHub to other hubs. It is used internally to handle
/// the flow of messages being sent out to other isolates.
///
/// The stream is not broadcast, meaning it only allows a single subscriber.
/// This is typically used by the ApplicationServer to listen for outbound
/// messages and distribute them to other isolates.
final StreamController<dynamic> _outboundController =
StreamController<dynamic>();
/// A StreamController for inbound messages.
///
/// This controller manages the stream of inbound messages received by this
/// ApplicationMessageHub from other hubs. It is used internally to handle
/// the flow of messages coming in from other isolates.
///
/// The stream is broadcast, meaning it allows multiple subscribers. This allows
/// multiple parts of the application to listen for and react to incoming messages
/// independently.
final StreamController<dynamic> _inboundController =
StreamController<dynamic>.broadcast();
/// Adds a listener for messages from other hubs.
///
/// A class that facilitates message passing between [ApplicationChannel]s in different isolates.
///
/// This class implements both [Stream] and [Sink] interfaces, allowing it to send and receive messages
/// across isolates. It uses separate controllers for inbound and outbound messages to manage the flow
/// of data.
///
/// You use this method to add listeners for messages from other hubs.
/// When another hub [add]s a message, this hub will receive it on [onData].
///
@ -249,7 +342,8 @@ class ApplicationMessageHub extends Stream<dynamic> implements Sink<dynamic> {
/// Sends a message to all other hubs.
///
/// [event] will be delivered to all other isolates that have set up a callback for [listen].
/// This method allows sending a message [event] to all other isolates in the application.
/// The message will be delivered to all other isolates that have set up a callback using [listen].
///
/// [event] must be isolate-safe data - in general, this means it may not be or contain a closure. Consult the API reference `dart:isolate` for more details. If [event]
/// is not isolate-safe data, an error is delivered to [listen] on this isolate.
@ -258,6 +352,19 @@ class ApplicationMessageHub extends Stream<dynamic> implements Sink<dynamic> {
_outboundController.sink.add(event);
}
/// Closes the message hub and its associated stream controllers.
///
/// This method performs the following tasks:
/// 1. If the outbound controller has no listeners, it adds a dummy listener
/// to prevent potential issues with unhandled stream events.
/// 2. If the inbound controller has no listeners, it adds a dummy listener
/// for the same reason.
/// 3. Closes both the outbound and inbound controllers.
///
/// This method should be called when the application is shutting down or
/// when the message hub is no longer needed to ensure proper cleanup of resources.
///
/// Returns a Future that completes when both controllers have been closed.
@override
Future close() async {
if (!_outboundController.hasListener) {
@ -273,6 +380,10 @@ class ApplicationMessageHub extends Stream<dynamic> implements Sink<dynamic> {
}
}
/// An abstract class that defines the runtime behavior of an ApplicationChannel.
///
/// This class provides methods and properties for managing the lifecycle,
/// documentation, and instantiation of an ApplicationChannel.
abstract class ChannelRuntime {
Iterable<APIComponentDocumenter> getDocumentableChannelComponents(
ApplicationChannel channel,

View file

@ -1,10 +1,47 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:isolate';
import 'package:protevus_application/application.dart';
import 'package:logging/logging.dart';
import 'package:protevus_application/application.dart';
/// An isolated server implementation of the ApplicationServer class.
///
/// This class extends ApplicationServer to run in a separate isolate, allowing
/// for concurrent execution of multiple server instances. It manages communication
/// with a supervising application through message passing.
///
/// The server can be started, stopped, and can send and receive application events.
/// It also supports optional console logging for debugging purposes.
///
/// Constructor parameters:
/// - channelType: The type of channel to be used.
/// - configuration: ApplicationOptions for server configuration.
/// - identifier: A unique identifier for this server instance.
/// - supervisingApplicationPort: SendPort for communicating with the supervising application.
/// - logToConsole: Optional flag to enable console logging (default is false).
class ApplicationIsolateServer extends ApplicationServer {
/// Constructor for ApplicationIsolateServer.
///
/// Creates a new instance of ApplicationIsolateServer with the specified parameters.
///
/// Parameters:
/// - channelType: The type of channel to be used for communication.
/// - configuration: ApplicationOptions for configuring the server.
/// - identifier: A unique identifier for this server instance.
/// - supervisingApplicationPort: SendPort for communicating with the supervising application.
/// - logToConsole: Optional flag to enable console logging (default is false).
///
/// This constructor initializes the server, sets up logging if enabled, and establishes
/// communication with the supervising application. It also sets up a listener for
/// incoming messages from the supervisor.
ApplicationIsolateServer(
Type channelType,
ApplicationOptions configuration,
@ -26,9 +63,40 @@ class ApplicationIsolateServer extends ApplicationServer {
supervisingApplicationPort.send(supervisingReceivePort.sendPort);
}
/// A SendPort used for communication with the supervising application.
///
/// This SendPort allows the ApplicationIsolateServer to send messages and events
/// back to the supervising application, enabling bidirectional communication
/// between the isolated server and its parent process.
SendPort supervisingApplicationPort;
/// A ReceivePort for receiving messages from the supervising application.
///
/// This ReceivePort is used to listen for incoming messages from the supervising
/// application. It's initialized in the constructor and is used to set up a
/// listener for handling various commands and messages, such as stop requests
/// or application events.
///
/// The 'late' keyword indicates that this variable will be initialized after
/// the constructor body, but before it's used.
late ReceivePort supervisingReceivePort;
/// Starts the ApplicationIsolateServer.
///
/// This method overrides the base class's start method to add functionality
/// specific to the isolated server. It performs the following steps:
/// 1. Calls the superclass's start method with the provided shareHttpServer parameter.
/// 2. Logs a fine-level message indicating that the server has started.
/// 3. Sends a 'listening' message to the supervising application.
///
/// Parameters:
/// - shareHttpServer: A boolean indicating whether to share the HTTP server (default is false).
///
/// Returns:
/// A Future that completes with the result of the superclass's start method.
///
/// Throws:
/// Any exceptions that may be thrown by the superclass's start method.
@override
Future start({bool shareHttpServer = false}) async {
final result = await super.start(shareHttpServer: shareHttpServer);
@ -41,6 +109,21 @@ class ApplicationIsolateServer extends ApplicationServer {
return result;
}
/// Sends an application event to the supervising application.
///
/// This method overrides the base class's sendApplicationEvent method to
/// implement event sending in the context of an isolated server. It wraps
/// the event in a MessageHubMessage and sends it through the
/// supervisingApplicationPort.
///
/// Parameters:
/// - event: The application event to be sent. Can be of any type.
///
/// If an error occurs during the sending process, it is caught and added
/// to the hubSink as an error, along with the stack trace.
///
/// Note: This method does not throw exceptions directly; instead, it
/// reports errors through the hubSink.
@override
void sendApplicationEvent(dynamic event) {
try {
@ -50,6 +133,22 @@ class ApplicationIsolateServer extends ApplicationServer {
}
}
/// Listener method for handling incoming messages from the supervising application.
///
/// This method processes two types of messages:
/// 1. A stop message (ApplicationIsolateSupervisor.messageKeyStop):
/// When received, it calls the stop() method to shut down the server.
/// 2. A MessageHubMessage:
/// When received, it adds the payload of the message to the hubSink.
///
/// Parameters:
/// - message: The incoming message. Can be either a stop command or a MessageHubMessage.
///
/// This method doesn't return any value but performs actions based on the message type:
/// - For a stop message, it initiates the server shutdown process.
/// - For a MessageHubMessage, it propagates the payload to the hubSink if it exists.
///
/// Note: This method assumes that hubSink is properly initialized elsewhere in the class.
void listener(dynamic message) {
if (message == ApplicationIsolateSupervisor.messageKeyStop) {
stop();
@ -58,6 +157,21 @@ class ApplicationIsolateServer extends ApplicationServer {
}
}
/// Stops the ApplicationIsolateServer and performs cleanup operations.
///
/// This method performs the following steps:
/// 1. Closes the supervisingReceivePort to stop receiving messages.
/// 2. Logs a fine-level message indicating the server is closing.
/// 3. Calls the close() method to shut down the server.
/// 4. Logs a fine-level message confirming the server has closed.
/// 5. Clears all listeners from the logger.
/// 6. Logs a fine-level message indicating it's sending a stop acknowledgement.
/// 7. Sends a stop acknowledgement message to the supervising application.
///
/// Returns:
/// A Future that completes when all stop operations are finished.
///
/// Note: This method is asynchronous and should be awaited when called.
Future stop() async {
supervisingReceivePort.close();
logger.fine("ApplicationIsolateServer($identifier) closing server");
@ -72,10 +186,40 @@ class ApplicationIsolateServer extends ApplicationServer {
}
}
/// A typedef defining the signature for an isolate entry function.
///
/// This function type is used to define the entry point for an isolate in the context
/// of an ApplicationIsolateServer. It takes a single parameter of type
/// ApplicationInitialServerMessage, which contains all the necessary information
/// to initialize and run the server within the isolate.
///
/// Parameters:
/// - message: An ApplicationInitialServerMessage object containing configuration
/// details, identifiers, and communication ports needed to set up the server
/// in the isolate.
///
/// This typedef is typically used when spawning new isolates for server instances,
/// allowing for a standardized way of passing initial setup information to the isolate.
typedef IsolateEntryFunction = void Function(
ApplicationInitialServerMessage message,
);
/// Represents the initial message sent to an ApplicationIsolateServer when it's created.
///
/// This class encapsulates all the necessary information needed to initialize and
/// configure an ApplicationIsolateServer within an isolate. It includes details about
/// the stream type, configuration options, communication ports, and logging preferences.
///
/// Properties:
/// - streamTypeName: The name of the stream type to be used by the server.
/// - streamLibraryURI: The URI of the library containing the stream implementation.
/// - configuration: ApplicationOptions object containing server configuration details.
/// - parentMessagePort: SendPort for communicating with the parent (supervising) application.
/// - identifier: A unique identifier for the server instance.
/// - logToConsole: A boolean flag indicating whether to enable console logging (default is false).
///
/// This class is typically used when spawning a new isolate for an ApplicationIsolateServer,
/// providing all the necessary information in a single, structured message.
class ApplicationInitialServerMessage {
ApplicationInitialServerMessage(
this.streamTypeName,
@ -94,6 +238,16 @@ class ApplicationInitialServerMessage {
bool logToConsole = false;
}
/// Represents a message that can be sent through the message hub.
///
/// This class encapsulates a payload of any type, allowing for flexible
/// communication between different parts of the application.
///
/// The payload can be of any type (dynamic), making this class versatile
/// for various types of messages.
///
/// Parameters:
/// - payload: The content of the message, which can be of any type.
class MessageHubMessage {
MessageHubMessage(this.payload);

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:isolate';
@ -6,9 +15,26 @@ import 'package:logging/logging.dart';
/// Represents the supervision of a [ApplicationIsolateServer].
///
/// This class, ApplicationIsolateSupervisor, is responsible for supervising and managing
/// an [ApplicationIsolateServer]. It handles the lifecycle of the isolate, including
/// starting, stopping, and communicating with it. The supervisor also manages error
/// handling, message passing between isolates, and ensures proper startup and shutdown
/// of the supervised isolate.
///
/// You should not use this class directly.
class ApplicationIsolateSupervisor {
/// Create an instance of [ApplicationIsolateSupervisor].
/// Creates an instance of [ApplicationIsolateSupervisor].
///
/// This constructor initializes a new [ApplicationIsolateSupervisor] with the provided parameters.
///
/// Parameters:
/// - [supervisingApplication]: The [Application] instance that owns this supervisor.
/// - [isolate]: The [Isolate] being supervised.
/// - [receivePort]: The [ReceivePort] for receiving messages from the supervised isolate.
/// - [identifier]: A numeric identifier for the isolate relative to the [Application].
/// - [logger]: The [Logger] instance used for logging.
/// - [startupTimeout]: Optional. The maximum duration to wait for the isolate to start up.
/// Defaults to 30 seconds.
ApplicationIsolateSupervisor(
this.supervisingApplication,
this.isolate,
@ -18,34 +44,168 @@ class ApplicationIsolateSupervisor {
this.startupTimeout = const Duration(seconds: 30),
});
/// The [Isolate] being supervised.
/// The [Isolate] being supervised by this [ApplicationIsolateSupervisor].
///
/// This isolate represents a separate thread of execution where the
/// [ApplicationIsolateServer] runs. The supervisor manages the lifecycle
/// and communication with this isolate.
final Isolate isolate;
/// The [ReceivePort] for which messages coming from [isolate] will be received.
/// The [ReceivePort] for receiving messages from the supervised isolate.
///
/// This [ReceivePort] is used to establish a communication channel between
/// the supervisor and the supervised isolate. It allows the supervisor to
/// receive various types of messages, including startup notifications,
/// error reports, and custom messages from the isolate.
///
/// The [receivePort] is crucial for managing the lifecycle of the isolate
/// and handling inter-isolate communication. It is used in conjunction with
/// the [listener] method to process incoming messages from the isolate.
final ReceivePort receivePort;
/// A numeric identifier for the isolate relative to the [Application].
///
/// This identifier is unique within the context of the parent [Application].
/// It is used to distinguish between different isolates managed by the same
/// application, facilitating logging, debugging, and isolate management.
/// The identifier is typically assigned sequentially when creating new isolates.
final int identifier;
/// The maximum duration to wait for the isolate to start up.
///
/// This duration specifies the time limit for the supervised isolate to complete its
/// startup process. If the isolate fails to start within this timeout period, an
/// exception will be thrown. This helps prevent indefinite waiting in case of startup
/// issues.
///
/// The default value is typically set in the constructor, often to 30 seconds.
final Duration startupTimeout;
/// A reference to the owning [Application]
/// A reference to the owning [Application].
///
/// This property holds a reference to the [Application] instance that owns and manages
/// this [ApplicationIsolateSupervisor]. It allows the supervisor to interact with
/// the main application, access shared resources, and coordinate activities across
/// multiple isolates.
///
/// The supervising application is responsible for creating and managing the lifecycle
/// of this supervisor and its associated isolate. It also provides context for
/// operations such as logging, configuration, and inter-isolate communication.
Application supervisingApplication;
/// A reference to the [Logger] used by the [supervisingApplication].
/// The logger instance used for recording events and errors.
///
/// This [Logger] is typically shared with the [supervisingApplication] and is used
/// to log various events, warnings, and errors related to the isolate supervision
/// process. It helps in debugging and monitoring the behavior of the supervised
/// isolate and the supervisor itself.
///
/// The logger can be used to record information at different severity levels,
/// such as fine, info, warning, and severe, depending on the nature of the event
/// being logged.
Logger logger;
/// A list to store pending [MessageHubMessage] objects.
///
/// This queue is used to temporarily hold messages that are received when the
/// supervising application is not running. Once the application starts running,
/// these messages are processed and sent to other supervisors.
///
/// The queue helps ensure that no messages are lost during the startup phase
/// of the application, maintaining message integrity across isolates.
final List<MessageHubMessage> _pendingMessageQueue = [];
/// Indicates whether the isolate is currently in the process of launching.
///
/// This getter returns `true` if the isolate is still in the startup phase,
/// and `false` if the launch process has completed.
///
/// It checks the state of the [_launchCompleter] to determine if the
/// launch process is still ongoing. If the completer is not yet completed,
/// it means the isolate is still launching.
///
/// This property is useful for handling different behaviors or error states
/// depending on whether the isolate is in its launch phase or has already
/// started running normally.
bool get _isLaunching => !_launchCompleter.isCompleted;
/// The [SendPort] used to send messages to the supervised isolate.
///
/// This [SendPort] is initialized when the supervisor receives the corresponding
/// [SendPort] from the supervised isolate during the startup process. It enables
/// bi-directional communication between the supervisor and the supervised isolate.
///
/// The [_serverSendPort] is used to send various messages to the isolate, including
/// stop signals, custom application messages, and other control commands. It plays
/// a crucial role in managing the lifecycle and behavior of the supervised isolate.
late SendPort _serverSendPort;
/// A [Completer] used to manage the launch process of the supervised isolate.
///
/// This completer is initialized when the isolate is being launched and is completed
/// when the isolate has successfully started up. It's used in conjunction with
/// [resume] method to handle the asynchronous nature of isolate startup.
///
/// The completer allows other parts of the supervisor to wait for the isolate
/// to finish launching before proceeding with further operations. It's also used
/// to implement timeout functionality in case the isolate fails to start within
/// the specified [startupTimeout].
late Completer _launchCompleter;
/// A [Completer] used to manage the stop process of the supervised isolate.
///
/// This nullable [Completer] is initialized when the [stop] method is called
/// and is used to handle the asynchronous nature of stopping the isolate.
/// It allows the supervisor to wait for the isolate to acknowledge the stop
/// message before proceeding with the termination process.
///
/// The completer is set to null after the stop process is complete, indicating
/// that the isolate has been successfully stopped. This helps manage the state
/// of the stop operation and prevents multiple stop attempts from interfering
/// with each other.
Completer? _stopCompleter;
/// A constant string used as a message key to signal the supervised isolate to stop.
///
/// This constant is used in the communication protocol between the supervisor
/// and the supervised isolate. When sent to the isolate, it indicates that
/// the isolate should begin its shutdown process.
///
/// The underscore prefix in the value suggests that this is intended for
/// internal use within the isolate communication system.
static const String messageKeyStop = "_MessageStop";
/// A constant string used as a message key to indicate that the supervised isolate is listening.
///
/// This constant is part of the communication protocol between the supervisor
/// and the supervised isolate. When the isolate sends this message to the
/// supervisor, it signals that the isolate has completed its startup process
/// and is ready to receive and process messages.
///
/// The underscore prefix in the value suggests that this is intended for
/// internal use within the isolate communication system.
static const String messageKeyListening = "_MessageListening";
/// Resumes the [Isolate] being supervised.
///
/// This method initiates the process of resuming the supervised isolate and
/// sets up the necessary listeners and error handlers. It performs the following steps:
///
/// 1. Initializes a new [Completer] for managing the launch process.
/// 2. Sets up a listener for the [receivePort] to handle incoming messages.
/// 3. Configures the isolate to handle errors non-fatally.
/// 4. Adds an error listener to the isolate.
/// 5. Resumes the isolate from its paused state.
/// 6. Waits for the isolate to complete its startup process.
///
/// If the isolate fails to start within the specified [startupTimeout], a
/// [TimeoutException] is thrown with a detailed error message.
///
/// Returns a [Future] that completes when the isolate has successfully started,
/// or throws an exception if the startup process fails or times out.
///
/// Throws:
/// - [TimeoutException] if the isolate doesn't start within the [startupTimeout].
Future resume() {
_launchCompleter = Completer();
receivePort.listen(listener);
@ -71,6 +231,24 @@ class ApplicationIsolateSupervisor {
}
/// Stops the [Isolate] being supervised.
///
/// This method initiates the process of stopping the supervised isolate. It performs the following steps:
///
/// 1. Creates a new [Completer] to manage the stop process.
/// 2. Sends a stop message to the supervised isolate using [_serverSendPort].
/// 3. Waits for the isolate to acknowledge the stop message.
/// 4. If the isolate doesn't respond within 5 seconds, logs a severe message.
/// 5. Forcefully kills the isolate using [isolate.kill()].
/// 6. Closes the [receivePort] to clean up resources.
///
/// The method uses a timeout of 5 seconds to wait for the isolate's acknowledgment.
/// If the timeout occurs, it assumes the isolate is not responding and proceeds to terminate it.
///
/// This method ensures that the isolate is stopped one way or another, either gracefully
/// or by force, and properly cleans up associated resources.
///
/// Returns a [Future] that completes when the stop process is finished, regardless of
/// whether the isolate responded to the stop message or was forcefully terminated.
Future stop() async {
_stopCompleter = Completer();
logger.fine(
@ -91,6 +269,22 @@ class ApplicationIsolateSupervisor {
receivePort.close();
}
/// Handles incoming messages from the supervised isolate.
///
/// This method is the central message processing function for the supervisor.
/// It handles various types of messages:
///
/// - [SendPort]: Stores the send port for communicating with the isolate.
/// - [messageKeyListening]: Indicates the isolate has started and is listening.
/// - [messageKeyStop]: Acknowledges that the isolate has received a stop message.
/// - [List]: Represents an error from the isolate, which is then handled.
/// - [MessageHubMessage]: Inter-isolate communication message.
///
/// For [MessageHubMessage], if the supervising application is not running,
/// the message is queued. Otherwise, it's immediately sent to other supervisors.
///
/// This method is crucial for managing the lifecycle and communication of the
/// supervised isolate, handling startup, shutdown, errors, and inter-isolate messaging.
void listener(dynamic message) {
if (message is SendPort) {
_serverSendPort = message;
@ -122,12 +316,38 @@ class ApplicationIsolateSupervisor {
}
}
/// Sends all pending messages stored in the [_pendingMessageQueue] to other supervisors.
///
/// This method is typically called when the supervising application starts running
/// to process any messages that were received while the application was not active.
/// It performs the following steps:
///
/// 1. Creates a copy of the [_pendingMessageQueue] to safely iterate over it.
/// 2. Clears the original [_pendingMessageQueue].
/// 3. Sends each message in the copied list to other supervisors using [_sendMessageToOtherSupervisors].
///
/// This ensures that no messages are lost during the startup phase of the application
/// and maintains message integrity across isolates.
void sendPendingMessages() {
final list = List<MessageHubMessage>.from(_pendingMessageQueue);
_pendingMessageQueue.clear();
list.forEach(_sendMessageToOtherSupervisors);
}
/// Sends a [MessageHubMessage] to all other supervisors managed by the supervising application.
///
/// This method is responsible for propagating messages across different isolates
/// managed by the same application. It performs the following actions:
///
/// 1. Iterates through all supervisors in the supervising application.
/// 2. Excludes the current supervisor from the recipients.
/// 3. Sends the provided [message] to each of the other supervisors' isolates.
///
/// This method is crucial for maintaining communication and synchronization
/// between different isolates within the application.
///
/// Parameters:
/// - [message]: The [MessageHubMessage] to be sent to other supervisors.
void _sendMessageToOtherSupervisors(MessageHubMessage message) {
supervisingApplication.supervisors
.where((sup) => sup != this)
@ -136,6 +356,25 @@ class ApplicationIsolateSupervisor {
});
}
/// Handles exceptions thrown by the supervised isolate.
///
/// This method is responsible for processing and responding to exceptions
/// that occur within the supervised isolate. It behaves differently depending
/// on whether the isolate is still in the process of launching or not:
///
/// - If the isolate is launching ([_isLaunching] is true):
/// It wraps the error in an [ApplicationStartupException] and completes
/// the [_launchCompleter] with this error, effectively failing the launch process.
///
/// - If the isolate has already launched:
/// It logs the error as a severe uncaught exception using the supervisor's logger.
///
/// Parameters:
/// - [error]: The error or exception object thrown by the isolate.
/// - [stacktrace]: The [StackTrace] associated with the error.
///
/// This method is crucial for maintaining the stability and error handling
/// of the isolate, especially during its startup phase.
void _handleIsolateException(dynamic error, StackTrace stacktrace) {
if (_isLaunching) {
final appException = ApplicationStartupException(error);

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:io';
import 'package:args/args.dart';
@ -5,45 +14,73 @@ import 'package:protevus_application/application.dart';
/// An object that contains configuration values for an [Application].
///
/// You use this object in an [ApplicationChannel] to manage external configuration data for your application.
/// This class provides a set of options that can be used to configure an application,
/// including network settings, SSL configuration, and custom context values.
/// It also includes a static [ArgParser] for parsing command-line arguments.
///
/// Key features:
/// - Configurable address and port for HTTP requests
/// - IPv6 support
/// - SSL/HTTPS configuration options
/// - Client-side certificate usage flag
/// - Custom context for application-specific configuration
/// - Command-line argument parsing for easy configuration
///
/// Usage:
/// This class is typically used in conjunction with [ApplicationChannel] to set up
/// and configure an application based on external inputs or command-line arguments.
class ApplicationOptions {
/// The absolute path of the configuration file for this application.
///
/// This path is provided when an application is started by the `--config-path` option to `conduit serve`.
/// You may load the file at this path in [ApplicationChannel] to use configuration values.
/// This property stores the file path of the configuration file used by the application.
/// The path is typically set when the application is started using the `--config-path` option
/// with the `conduit serve` command.
///
/// The configuration file can contain application-specific settings and can be loaded
/// in the [ApplicationChannel] to access these configuration values.
///
/// This property may be null if no configuration file path was specified when starting the application.
///
/// Usage:
/// - Access this property to get the path of the configuration file.
/// - Use the path to load and parse the configuration file in your application logic.
/// - Ensure to handle cases where this property might be null.
String? configurationFilePath;
/// The address to listen for HTTP requests on.
///
/// By default, this address will default to 'any' address (0.0.0.0). If [isIpv6Only] is true,
/// 'any' will be any IPv6 address, otherwise, it will be any IPv4 or IPv6 address.
/// This property specifies the network address on which the application will listen for incoming HTTP requests.
///
/// This value may be an [InternetAddress] or a [String].
dynamic address;
/// The port to listen for HTTP requests on.
/// The port number on which the application will listen for HTTP requests.
///
/// Defaults to 8888.
int port = 8888;
/// Whether or not the application should only receive connections over IPv6.
///
/// This flag determines if the application should exclusively use IPv6 for incoming connections.
/// When set to true, the application will only accept IPv6 connections and reject IPv4 connections.
/// This setting can be useful in environments that require IPv6-only communication.
///
/// Defaults to false. This flag impacts the default value of the [address] property.
bool isIpv6Only = false;
/// Whether or not the application's request controllers should use client-side HTTPS certificates.
/// Indicates whether the application's request controllers should use client-side HTTPS certificates.
///
/// Defaults to false.
bool isUsingClientCertificate = false;
/// The path to a SSL certificate.
/// The path to a SSL certificate file.
///
/// If specified - along with [privateKeyFilePath] - an [Application] will only allow secure connections over HTTPS.
/// This value is often set through the `--ssl-certificate-path` command line option of `conduit serve`. For finer control
/// over how HTTPS is configured for an application, see [ApplicationChannel.securityContext].
String? certificateFilePath;
/// The path to a private key.
/// The path to a private key file for SSL/TLS encryption.
///
/// If specified - along with [certificateFilePath] - an [Application] will only allow secure connections over HTTPS.
/// This value is often set through the `--ssl-key-path` command line option of `conduit serve`. For finer control
@ -54,8 +91,40 @@ class ApplicationOptions {
///
/// This is a user-specific set of configuration options provided by [ApplicationChannel.initializeApplication].
/// Each instance of [ApplicationChannel] has access to these values if set.
///
/// This map allows for storing and retrieving custom configuration values that can be used
/// throughout the application. It provides a flexible way to pass application-specific
/// settings to different parts of the system.
///
/// The context can be populated during the application's initialization phase and
/// can contain any type of data that adheres to the dynamic type.
///
/// Usage:
/// - Add configuration values: `context['databaseUrl'] = 'postgres://...';`
/// - Retrieve values: `final dbUrl = options.context['databaseUrl'];`
///
/// Note: It's important to ensure type safety when retrieving values from this map,
/// as it uses dynamic typing.
final Map<String, dynamic> context = {};
/// A static [ArgParser] for parsing command-line arguments for the application.
///
/// This parser defines several options and flags that can be used to configure
/// the application when it is launched from the command line. The available
/// options include:
///
/// - address: The address to listen on for HTTP requests.
/// - config-path: The path to a configuration file.
/// - isolates: Number of isolates for handling requests.
/// - port: The port number to listen for HTTP requests on.
/// - ipv6-only: Flag to limit listening to IPv6 connections only.
/// - ssl-certificate-path: The path to an SSL certificate file.
/// - ssl-key-path: The path to an SSL private key file.
/// - timeout: Number of seconds to wait to ensure startup succeeded.
/// - help: Flag to display help information.
///
/// Each option is configured with a description, and some include default values
/// or abbreviations for easier command-line usage.
static final parser = ArgParser()
..addOption(
"address",

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:isolate';
@ -7,6 +16,21 @@ import 'package:protevus_application/application.dart';
Warning: do not remove. This method is invoked by a generated script.
*/
/// Starts the application either on the current isolate or across multiple isolates.
///
/// This function initializes and starts the application, setting up communication
/// between isolates using ports. It responds to stop commands and reports the
/// application's status back to the parent isolate.
///
/// Parameters:
/// - app: The Application instance to be started.
/// - isolateCount: The number of isolates to start the application on. If 0, starts on the current isolate.
/// - parentPort: The SendPort of the parent isolate for communication.
///
/// The function sets up a ReceivePort to listen for commands, particularly the "stop" command.
/// It then starts the application either on the current isolate or across multiple isolates
/// based on the isolateCount parameter. Finally, it sends a status message back to the parent isolate.
Future startApplication<T extends ApplicationChannel>(
Application<T> app,
int isolateCount,

View file

@ -7,9 +7,10 @@
* file that was distributed with this source code.
*/
library;
import 'package:protevus_auth/auth.dart';
import 'package:protevus_hashing/hashing.dart';
import 'package:crypto/crypto.dart';
export 'src/auth.dart';
export 'src/auth_code_controller.dart';
export 'src/auth_controller.dart';
export 'src/auth_redirect_controller.dart';
@ -20,3 +21,60 @@ export 'src/exceptions.dart';
export 'src/objects.dart';
export 'src/protocols.dart';
export 'src/validator.dart';
/// A utility method to generate a password hash using the PBKDF2 scheme.
///
/// This function takes a password and salt as input and generates a secure hash
/// using the PBKDF2 (Password-Based Key Derivation Function 2) algorithm.
String generatePasswordHash(
String password,
String salt, {
int hashRounds = 1000,
int hashLength = 32,
Hash? hashFunction,
}) {
final generator = PBKDF2(hashAlgorithm: hashFunction ?? sha256);
return generator.generateBase64Key(password, salt, hashRounds, hashLength);
}
/// A utility method to generate a random base64 salt.
///
/// This function generates a random salt encoded as a base64 string.
/// The salt is useful for adding randomness to password hashing processes,
/// making them more resistant to attacks.
String generateRandomSalt({int hashLength = 32}) {
return generateAsBase64String(hashLength);
}
/// A utility method to generate a ClientID and Client Secret Pair.
///
/// This function creates an [AuthClient] instance, which can be either public or confidential,
/// depending on whether a secret is provided.
///
/// Any client that allows the authorization code flow must include [redirectURI].
///
/// Note that [secret] is hashed with a randomly generated salt, and therefore cannot be retrieved
/// later. The plain-text secret must be stored securely elsewhere.
AuthClient generateAPICredentialPair(
String clientID,
String? secret, {
String? redirectURI,
int hashLength = 32,
int hashRounds = 1000,
Hash? hashFunction,
}) {
if (secret == null) {
return AuthClient.public(clientID, redirectURI: redirectURI);
}
final salt = generateRandomSalt(hashLength: hashLength);
final hashed = generatePasswordHash(
secret,
salt,
hashRounds: hashRounds,
hashLength: hashLength,
hashFunction: hashFunction,
);
return AuthClient.withRedirectURI(clientID, hashed, salt, redirectURI);
}

View file

@ -1,70 +0,0 @@
import 'package:protevus_auth/auth.dart';
import 'package:protevus_hashing/hashing.dart';
import 'package:crypto/crypto.dart';
export 'auth_code_controller.dart';
export 'auth_controller.dart';
export 'auth_redirect_controller.dart';
export 'authorization_parser.dart';
export 'authorization_server.dart';
export 'authorizer.dart';
export 'exceptions.dart';
export 'objects.dart';
export 'protocols.dart';
export 'validator.dart';
/// A utility method to generate a password hash using the PBKDF2 scheme.
///
///
String generatePasswordHash(
String password,
String salt, {
int hashRounds = 1000,
int hashLength = 32,
Hash? hashFunction,
}) {
final generator = PBKDF2(hashAlgorithm: hashFunction ?? sha256);
return generator.generateBase64Key(password, salt, hashRounds, hashLength);
}
/// A utility method to generate a random base64 salt.
///
///
String generateRandomSalt({int hashLength = 32}) {
return generateAsBase64String(hashLength);
}
/// A utility method to generate a ClientID and Client Secret Pair.
///
/// [secret] may be null. If secret is null, the return value is a 'public' client. Otherwise, the
/// client is 'confidential'. Public clients must not include a client secret when sent to the
/// authorization server. Confidential clients must include the secret in all requests. Use public clients when
/// the source code of the client application is visible, i.e. a JavaScript browser application.
///
/// Any client that allows the authorization code flow must include [redirectURI].
///
/// Note that [secret] is hashed with a randomly generated salt, and therefore cannot be retrieved
/// later. The plain-text secret must be stored securely elsewhere.
AuthClient generateAPICredentialPair(
String clientID,
String? secret, {
String? redirectURI,
int hashLength = 32,
int hashRounds = 1000,
Hash? hashFunction,
}) {
if (secret == null) {
return AuthClient.public(clientID, redirectURI: redirectURI);
}
final salt = generateRandomSalt(hashLength: hashLength);
final hashed = generatePasswordHash(
secret,
salt,
hashRounds: hashRounds,
hashLength: hashLength,
hashFunction: hashFunction,
);
return AuthClient.withRedirectURI(clientID, hashed, salt, redirectURI);
}

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:io';
@ -7,13 +16,20 @@ import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
/// Provides [AuthCodeController] with application-specific behavior.
///
/// This abstract class defines the interface for a delegate that can be used
/// with [AuthCodeController] to customize the rendering of the login form.
/// It is deprecated along with [AuthCodeController], and developers are
/// advised to see the documentation for alternative approaches.
///
/// The main responsibility of this delegate is to generate an HTML
/// representation of a login form when requested by the [AuthCodeController].
@Deprecated('AuthCodeController is deprecated. See docs.')
abstract class AuthCodeControllerDelegate {
/// Returns an HTML representation of a login form.
///
/// Invoked when [AuthCodeController.getAuthorizationPage] is called in response to a GET request.
/// Must provide HTML that will be returned to the browser for rendering. This form submission of this page
/// should be a POST to [requestUri].
/// This method is responsible for generating the HTML content of a login form
/// that will be displayed to the user when they attempt to authenticate.
///
/// The form submission should include the values of [responseType], [clientID], [state], [scope]
/// as well as user-entered username and password in `x-www-form-urlencoded` data, e.g.
@ -37,24 +53,29 @@ abstract class AuthCodeControllerDelegate {
/// Controller for issuing OAuth 2.0 authorization codes.
///
/// Deprecated, use [AuthRedirectController] instead.
/// This controller handles the authorization code grant flow of OAuth 2.0. It provides
/// endpoints for both initiating the flow (GET request) and completing it (POST request).
///
/// This controller provides an endpoint for the creating an OAuth 2.0 authorization code. This authorization code
/// can be exchanged for an access token with an [AuthController]. This is known as the OAuth 2.0 'Authorization Code Grant' flow.
///
/// See operation methods [getAuthorizationPage] and [authorize] for more details.
///
/// Usage:
///
/// router
/// .route("/auth/code")
/// .link(() => new AuthCodeController(authServer));
///
@Deprecated('Use AuthRedirectController instead.')
class AuthCodeController extends ResourceController {
/// Creates a new instance of an [AuthCodeController].
///
/// [authServer] is the required authorization server. If [delegate] is provided, this controller will return a login page for all GET requests.
/// This constructor initializes an [AuthCodeController] with the provided [authServer].
/// It is marked as deprecated, and users are advised to use [AuthRedirectController] instead.
///
/// Parameters:
/// - [authServer]: The required authorization server used for handling authentication.
/// - [delegate]: An optional [AuthCodeControllerDelegate] that, if provided, allows this controller
/// to return a login page for all GET requests.
///
/// The constructor also sets the [acceptedContentTypes] to only accept
/// "application/x-www-form-urlencoded" content type.
///
/// This controller is part of the OAuth 2.0 authorization code flow and is used
/// for issuing authorization codes. However, due to its deprecated status,
/// it's recommended to transition to newer alternatives as specified in the documentation.
@Deprecated('Use AuthRedirectController instead.')
AuthCodeController(this.authServer, {this.delegate}) {
acceptedContentTypes = [
@ -63,6 +84,11 @@ class AuthCodeController extends ResourceController {
}
/// A reference to the [AuthServer] used to grant authorization codes.
///
/// This [AuthServer] instance is responsible for handling the authentication
/// and authorization processes, including the generation and validation of
/// authorization codes. It is a crucial component of the OAuth 2.0 flow
/// implemented by this controller.
final AuthServer authServer;
/// A randomly generated value the client can use to verify the origin of the redirect.
@ -70,23 +96,53 @@ class AuthCodeController extends ResourceController {
/// Clients must include this query parameter and verify that any redirects from this
/// server have the same value for 'state' as passed in. This value is usually a randomly generated
/// session identifier.
///
/// This property is bound to the 'state' query parameter in the request URL.
/// It plays a crucial role in preventing cross-site request forgery (CSRF) attacks
/// by ensuring that the authorization request and response originate from the same client session.
///
/// The 'state' parameter should be:
/// - Unique for each authorization request
/// - Securely generated to be unguessable
/// - Stored by the client for later comparison
///
/// When the authorization server redirects the user back to the client,
/// it includes this state value, allowing the client to verify that the redirect
/// is in response to its own authorization request.
@Bind.query("state")
String? state;
/// The type of response expected from the authorization server.
///
/// This parameter is bound to the 'response_type' query parameter in the request URL.
/// For the authorization code flow, this value must be 'code'.
///
/// The response type indicates to the authorization server which grant type
/// is being utilized. In this case, 'code' signifies that the client expects
/// to receive an authorization code that can be exchanged for an access token
/// in a subsequent request.
///
/// Must be 'code'.
@Bind.query("response_type")
String? responseType;
/// The client ID of the authenticating client.
///
/// This must be a valid client ID according to [authServer].\
/// This property is bound to the 'client_id' query parameter in the request URL.
/// It represents the unique identifier of the client application requesting authorization.
///
/// The client ID must be registered and valid according to the [authServer].
/// It is used to identify the client during the OAuth 2.0 authorization process.
///
/// This field is nullable, but typically required for most OAuth 2.0 flows.
/// If not provided or invalid, the authorization request may be rejected.
@Bind.query("client_id")
String? clientID;
/// Renders an HTML login form.
final AuthCodeControllerDelegate? delegate;
/// Returns an HTML login form.
/// Returns an HTML login form for OAuth 2.0 authorization.
///
/// A client that wishes to authenticate with this server should direct the user
/// to this page. The user will enter their username and password that is sent as a POST
@ -115,20 +171,61 @@ class AuthCodeController extends ResourceController {
/// Creates a one-time use authorization code.
///
/// This method will respond with a redirect that contains an authorization code ('code')
/// and the passed in 'state'. If this request fails, the redirect URL
/// will contain an 'error' key instead of the authorization code.
/// This method handles the POST request for the OAuth 2.0 authorization code grant flow.
/// It authenticates the user with the provided credentials and, if successful, generates
/// a one-time use authorization code.
///
/// This method is typically invoked by the login form returned from the GET to this controller.
@Operation.post()
Future<Response> authorize({
/// The username of the authenticating user.
///
/// This parameter is bound to the 'username' query parameter in the request URL.
/// It represents the username of the user attempting to authenticate.
///
/// The username is used in conjunction with the password to verify the user's identity
/// during the OAuth 2.0 authorization code grant flow. It is a crucial part of the
/// user authentication process.
///
/// This field is nullable, but typically required for successful authentication.
/// If not provided or invalid, the authorization request may be rejected.
@Bind.query("username") String? username,
/// The password of the authenticating user.
///
/// This parameter is bound to the 'password' query parameter in the request URL.
/// It represents the password of the user attempting to authenticate.
///
/// The password is used in conjunction with the username to verify the user's identity
/// during the OAuth 2.0 authorization code grant flow. It is a crucial part of the
/// user authentication process.
///
/// This field is nullable, but typically required for successful authentication.
/// If not provided or invalid, the authorization request may be rejected.
///
/// Note: Transmitting passwords as query parameters is not recommended for production
/// environments due to security concerns. This approach should only be used in
/// controlled, secure environments or for testing purposes.
@Bind.query("password") String? password,
/// A space-delimited list of access scopes being requested.
///
/// This parameter is bound to the 'scope' query parameter in the request URL.
/// It represents the permissions that the client is requesting access to.
///
/// The scope is typically a string containing one or more space-separated
/// scope values. Each scope value represents a specific permission or
/// set of permissions that the client is requesting.
///
/// For example, a scope might look like: "read_profile edit_profile"
///
/// The authorization server can use this information to present
/// the user with a consent screen, allowing them to approve or deny
/// specific permissions requested by the client.
///
/// This field is optional. If not provided, the authorization server
/// may assign a default set of scopes or handle the request according
/// to its own policies.
@Bind.query("scope") String? scope,
}) async {
final client = await authServer.getClient(clientID!);
@ -174,6 +271,25 @@ class AuthCodeController extends ResourceController {
}
}
/// Overrides the default documentation for the request body of this controller's operations.
///
/// This method is called during the OpenAPI documentation generation process.
/// It modifies the request body schema for POST operations to:
/// 1. Set the format of the 'password' field to "password".
/// 2. Mark certain fields as required in the request body.
///
/// The method specifically targets the "application/x-www-form-urlencoded" content type
/// in POST requests. It updates the schema to indicate that the 'password' field should
/// be treated as a password input, and sets the following fields as required:
/// - client_id
/// - state
/// - response_type
/// - username
/// - password
///
/// Returns:
/// An [APIRequestBody] object representing the documented request body,
/// or null if there is no request body for the operation.
@override
APIRequestBody? documentOperationRequestBody(
APIDocumentContext context,
@ -194,6 +310,20 @@ class AuthCodeController extends ResourceController {
return body;
}
/// Overrides the default documentation for operation parameters.
///
/// This method is called during the OpenAPI documentation generation process.
/// It modifies the parameter documentation for the controller's operations by:
/// 1. Retrieving the default parameters using the superclass method.
/// 2. Setting all parameters except 'scope' as required.
///
/// Parameters:
/// context: The [APIDocumentContext] for the current documentation generation.
/// operation: The [Operation] being documented.
///
/// Returns:
/// A List of [APIParameter] objects representing the documented parameters,
/// with updated 'isRequired' properties.
@override
List<APIParameter> documentOperationParameters(
APIDocumentContext context,
@ -206,6 +336,28 @@ class AuthCodeController extends ResourceController {
return params;
}
/// Generates documentation for the operation responses of this controller.
///
/// This method overrides the default behavior to provide custom documentation
/// for the GET and POST operations of the AuthCodeController.
///
/// For GET requests:
/// - Defines a 200 OK response that serves a login form in HTML format.
///
/// For POST requests:
/// - Defines a 302 Found (Moved Temporarily) response for successful requests,
/// indicating that the 'code' query parameter in the redirect URI contains
/// the authorization code, or an 'error' parameter is present for errors.
/// - Defines a 400 Bad Request response for cases where the 'client_id' is
/// invalid and the redirect URI cannot be verified.
///
/// Parameters:
/// context: The API documentation context.
/// operation: The operation being documented.
///
/// Returns:
/// A Map of status codes to APIResponse objects describing the possible
/// responses for the operation.
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
@ -240,6 +392,24 @@ class AuthCodeController extends ResourceController {
throw StateError("AuthCodeController documentation failed.");
}
/// Customizes the documentation for the operations of this controller.
///
/// This method overrides the default implementation to add additional
/// information specific to the OAuth 2.0 authorization code flow.
///
/// It performs the following tasks:
/// 1. Calls the superclass method to get the default operation documentation.
/// 2. Updates the authorization URL in the documented authorization code flow
/// of the auth server to match the current route.
///
/// Parameters:
/// context: The [APIDocumentContext] for the current documentation generation.
/// route: The route string for this controller.
/// path: The [APIPath] object representing the path in the API documentation.
///
/// Returns:
/// A Map of operation names to [APIOperation] objects representing the
/// documented operations for this controller.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,
@ -252,6 +422,27 @@ class AuthCodeController extends ResourceController {
return ops;
}
/// Generates a redirect response for the OAuth 2.0 authorization code flow.
///
/// This method constructs a redirect URI based on the provided parameters and
/// returns a Response object with appropriate headers for redirection.
///
/// Parameters:
/// - [inputUri]: The base URI to redirect to. If null, falls back to the client's redirectURI.
/// - [clientStateOrNull]: The state parameter provided by the client. If not null, it's included in the redirect URI.
/// - [code]: The authorization code to be included in the redirect URI. Optional.
/// - [error]: An AuthServerException containing error details. Optional.
///
/// Returns:
/// - A Response object with status 302 (Found) and appropriate headers for redirection.
/// - If no valid redirect URI can be constructed, returns a 400 (Bad Request) response.
///
/// The method constructs the redirect URI by:
/// 1. Determining the base URI (from input or client's redirect URI)
/// 2. Adding query parameters for code, state, and error as applicable
/// 3. Constructing a new URI with these parameters
///
/// The response includes headers for location, cache control, and pragma.
static Response _redirectResponse(
String? inputUri,
String? clientStateOrNull, {

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:io';
@ -49,6 +58,30 @@ class AuthController extends ResourceController {
final AuthorizationBasicParser _parser = const AuthorizationBasicParser();
/// This class, AuthController, is responsible for handling OAuth 2.0 token operations.
/// It provides functionality for issuing and refreshing access tokens using various grant types.
///
/// Key features:
/// - Supports 'password', 'refresh_token', and 'authorization_code' grant types
/// - Handles client authentication via Basic Authorization header
/// - Processes token requests and returns RFC6749 compliant responses
/// - Includes error handling for various authentication scenarios
/// - Provides OpenAPI documentation support
///
/// The main method, 'grant', processes token requests based on the provided grant type.
/// It interacts with an AuthServer to perform the actual authentication and token generation.
///
/// This controller also includes methods for generating API documentation,
/// including operation parameters, request body, and responses.
///
/// Usage:
/// router
/// .route("/auth/token")
/// .link(() => new AuthController(authServer));
///
/// Note: This controller expects client credentials to be provided in the Authorization header
/// using the Basic authentication scheme.
///
/// Creates or refreshes an authentication token.
///
/// When grant_type is 'password', there must be username and password values.
@ -59,11 +92,88 @@ class AuthController extends ResourceController {
/// include a valid Client ID and Secret in the Basic authorization scheme format.
@Operation.post()
Future<Response> grant({
/// The username of the user attempting to authenticate.
///
/// This parameter is typically used with the 'password' grant type.
/// It should be provided as a query parameter in the request.
@Bind.query("username") String? username,
/// The password of the user attempting to authenticate.
///
/// This parameter is typically used with the 'password' grant type.
/// It should be provided as a query parameter in the request.
/// Note: Sending passwords as query parameters is not recommended for production environments due to security concerns.
@Bind.query("password") String? password,
/// The refresh token used to obtain a new access token.
///
/// This parameter is typically used with the 'refresh_token' grant type.
/// It should be provided as a query parameter in the request.
/// The refresh token is used to request a new access token when the current one has expired.
///
/// Example:
/// curl -X POST -d "grant_type=refresh_token&refresh_token=<refresh_token>" https://example.com/auth/token
///
/// Note: The refresh token should be securely stored and managed by the client application.
/// It is important to handle refresh tokens with care to prevent unauthorized access to user resources.
///
/// See also:
/// - [RFC 6749, Section 6](https://tools.ietf.org/html/rfc6749#section-6) for more details on the refresh token grant type.
/// - [OAuth 2.0 Refresh Token Grant](https://oauth.net/2/grant-types/refresh-token/) for a detailed explanation of the refresh token grant type.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.1) for security considerations when implementing OAuth 2.0.
@Bind.query("refresh_token") String? refreshToken,
/// The authorization code obtained from the authorization server.
///
/// This parameter is typically used with the 'authorization_code' grant type.
/// It should be provided as a query parameter in the request.
/// The authorization code is used to request an access token after the user has granted permission to the client application.
///
/// Example:
/// curl -X POST -d "grant_type=authorization_code&code=<authorization_code>&redirect_uri=<redirect_uri>" https://example.com/auth/token
///
/// Note: The authorization code should be securely transmitted and used only once to prevent replay attacks.
/// It is important to handle authorization codes with care to protect user data and ensure the security of the OAuth 2.0 flow.
///
/// See also:
/// - [RFC 6749, Section 4.1.3](https://tools.ietf.org/html/rfc6749#section-4.1.3) for more details on the authorization code grant type.
/// - [OAuth 2.0 Authorization Code Grant](https://oauth.net/2/grant-types/authorization-code/) for a detailed explanation of the authorization code grant type.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.1) for security considerations when implementing OAuth 2.0.
@Bind.query("code") String? authCode,
/// The URI to which the authorization server will redirect the user-agent after obtaining authorization.
///
/// This parameter is typically used with the 'authorization_code' grant type.
/// It should be provided as a query parameter in the request.
/// The redirect URI is used to ensure that the authorization code is sent to the correct client application.
///
/// Example:
/// curl -X POST -d "grant_type=authorization_code&code=<authorization_code>&redirect_uri=https://example.com/callback" https://example.com/auth/token
///
/// Note: The redirect URI should be registered with the authorization server and should match the URI used during the authorization request.
/// It is important to handle redirect URIs with care to prevent unauthorized access to user resources.
///
/// See also:
/// - [RFC 6749, Section 4.1.3](https://tools.ietf.org/html/rfc6749#section-4.1.3) for more details on the authorization code grant type.
/// - [OAuth 2.0 Authorization Code Grant](https://oauth.net/2/grant-types/authorization-code/) for a detailed explanation of the authorization code grant type.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.1) for security considerations when implementing OAuth 2.0.
@Bind.query("grant_type") String? grantType,
/// The scope of the access request, which defines the resources and permissions that the client application is requesting.
///
/// This parameter is optional and should be provided as a query parameter in the request.
/// The scope value is a space-delimited list of scope identifiers, which indicate the specific resources and permissions that the client application needs to access.
///
/// Example:
/// curl -X POST -d "grant_type=authorization_code&code=<authorization_code>&redirect_uri=<redirect_uri>&scope=read write" https://example.com/auth/token
///
/// Note: The scope parameter should be used to limit the access granted to the client application to only the necessary resources and permissions.
/// It is important to handle scope values with care to ensure that the client application does not have unintended access to user resources.
///
/// See also:
/// - [RFC 6749, Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3) for more details on the scope parameter.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.2) for security considerations when implementing OAuth 2.0 scope.
/// - [OAuth 2.0 Scope](https://oauth.net/2/scope/) for a detailed explanation of the scope parameter and its usage.
@Bind.query("scope") String? scope,
}) async {
AuthBasicCredentials basicRecord;
@ -118,6 +228,28 @@ class AuthController extends ResourceController {
/// Transforms a [AuthToken] into a [Response] object with an RFC6749 compliant JSON token
/// as the HTTP response body.
///
/// This static method takes an [AuthToken] object and converts it into a [Response] object
/// that adheres to the OAuth 2.0 specification (RFC6749). The response includes:
/// - A status code of 200 (OK)
/// - Headers to prevent caching of the token
/// - A body containing the token information in JSON format
///
/// Parameters:
/// - token: An [AuthToken] object containing the authentication token details
///
/// Returns:
/// A [Response] object with the token information, ready to be sent to the client
///
/// Example usage:
/// ```dart
/// AuthToken myToken = // ... obtain token
/// Response response = AuthController.tokenResponse(myToken);
/// ```
///
/// See also:
/// - [RFC6749](https://tools.ietf.org/html/rfc6749) for the OAuth 2.0 specification
/// - [AuthToken] for the structure of the token object
static Response tokenResponse(AuthToken token) {
return Response(
HttpStatus.ok,
@ -126,6 +258,24 @@ class AuthController extends ResourceController {
);
}
/// Processes the response before it is sent, specifically handling duplicate parameter errors.
///
/// This method is called just before a response is sent. It checks for responses with a 400 status code
/// and modifies the error message in case of duplicate parameters in the request, which violates the OAuth 2.0 specification.
///
/// The method performs the following actions:
/// 1. Checks if the response status code is 400 (Bad Request).
/// 2. If the response body contains an "error" key with a string value, it examines the error message.
/// 3. If the error message indicates multiple values (likely due to duplicate parameters), it replaces the error message
/// with a standard "invalid_request" error as defined in the OAuth 2.0 specification.
///
/// This post-processing helps to maintain compliance with the OAuth 2.0 specification by providing a standard error
/// response for invalid requests, even in the case of duplicate parameters which are not explicitly handled elsewhere.
///
/// Parameters:
/// response: The Response object that will be sent to the client.
///
/// Note: This method directly modifies the response object if conditions are met.
@override
void willSendResponse(Response response) {
if (response.statusCode == 400) {
@ -145,6 +295,20 @@ class AuthController extends ResourceController {
}
}
/// Modifies the list of API parameters documented for this operation.
///
/// This method overrides the default behavior to remove the 'Authorization' header
/// from the list of documented parameters. This is typically done because the
/// Authorization header is handled separately in OAuth 2.0 flows and doesn't need
/// to be explicitly documented as an operation parameter.
///
/// Parameters:
/// - context: The current API documentation context.
/// - operation: The operation being documented (can be null).
///
/// Returns:
/// A list of [APIParameter] objects representing the documented parameters
/// for this operation, with the Authorization header removed.
@override
List<APIParameter> documentOperationParameters(
APIDocumentContext context,
@ -155,6 +319,21 @@ class AuthController extends ResourceController {
return parameters;
}
/// Customizes the documentation for the request body of this operation.
///
/// This method overrides the default behavior to add specific requirements
/// and formatting for the OAuth 2.0 token endpoint:
///
/// 1. It marks the 'grant_type' parameter as required in the request body.
/// 2. It sets the format of the 'password' field to "password", indicating
/// that it should be treated as a sensitive input in API documentation tools.
///
/// Parameters:
/// - context: The current API documentation context.
/// - operation: The operation being documented (can be null).
///
/// Returns:
/// An [APIRequestBody] object with the customized schema for the request body.
@override
APIRequestBody documentOperationRequestBody(
APIDocumentContext context,
@ -169,6 +348,20 @@ class AuthController extends ResourceController {
return body;
}
/// Customizes the API documentation for the operations handled by this controller.
///
/// This method overrides the default behavior to:
/// 1. Add OAuth 2.0 client authentication security requirement to all operations.
/// 2. Set the token and refresh URLs for the documented authorization code flow.
/// 3. Set the token and refresh URLs for the documented password flow.
///
/// Parameters:
/// - context: The current API documentation context.
/// - route: The route string for the current operations.
/// - path: The APIPath object representing the current path.
///
/// Returns:
/// A map of operation names to APIOperation objects with the customized documentation.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,
@ -193,6 +386,25 @@ class AuthController extends ResourceController {
return operations;
}
/// Defines the API responses for the token operation in OpenAPI documentation.
///
/// This method overrides the default behavior to provide custom documentation
/// for the responses of the token endpoint. It describes two possible responses:
///
/// 1. A successful response (200 OK) when credentials are successfully exchanged for a token.
/// This response includes details about the issued token such as access_token, token_type,
/// expiration time, refresh_token, and scope.
///
/// 2. An error response (400 Bad Request) for cases of invalid credentials or missing parameters.
/// This response includes an error message.
///
/// Parameters:
/// - context: The current API documentation context.
/// - operation: The operation being documented (can be null).
///
/// Returns:
/// A map of status codes to APIResponse objects describing the possible
/// responses for this operation.
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
@ -218,6 +430,27 @@ class AuthController extends ResourceController {
};
}
/// Creates a Response object for an authentication error.
///
/// This method generates a standardized HTTP response for various authentication
/// errors that may occur during the OAuth 2.0 flow. It uses the [AuthRequestError]
/// enum to determine the specific error and creates a response with:
/// - A status code of 400 (Bad Request)
/// - A JSON body containing an "error" key with a description of the error
///
/// Parameters:
/// - error: An [AuthRequestError] enum representing the specific authentication error.
///
/// Returns:
/// A [Response] object with status code 400 and a JSON body describing the error.
///
/// Example usage:
/// ```dart
/// Response errorResponse = _responseForError(AuthRequestError.invalidRequest);
/// ```
///
/// The error string in the response body is generated using [AuthServerException.errorString],
/// ensuring consistency with OAuth 2.0 error reporting standards.
Response _responseForError(AuthRequestError error) {
return Response.badRequest(
body: {"error": AuthServerException.errorString(error)},

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:io';
@ -6,13 +15,31 @@ import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
/// Provides [AuthRedirectController] with application-specific behavior.
/// Abstract class defining the interface for providing application-specific behavior to [AuthRedirectController].
///
/// This delegate is responsible for rendering the HTML login form when [AuthRedirectController.getAuthorizationPage]
/// is called in response to a GET request. Implementations of this class should customize the login form
/// according to the application's needs while ensuring that the form submission adheres to the required format.
///
/// The rendered form should:
/// - Be submitted as a POST request to the [requestUri].
/// - Include all provided parameters (responseType, clientID, state, scope) in the form submission.
/// - Collect and include user-entered username and password.
/// - Use 'application/x-www-form-urlencoded' as the Content-Type for form submission.
///
/// Example of expected form submission:
///
/// POST https://example.com/auth/code
/// Content-Type: application/x-www-form-urlencoded
///
/// response_type=code&client_id=com.conduit.app&state=o9u3jla&username=bob&password=password
///
/// Implementations should take care to handle all provided parameters and ensure secure transmission of credentials.
abstract class AuthRedirectControllerDelegate {
/// Returns an HTML representation of a login form.
///
/// Invoked when [AuthRedirectController.getAuthorizationPage] is called in response to a GET request.
/// Must provide HTML that will be returned to the browser for rendering. This form submission of this page
/// should be a POST to [requestUri].
/// This method is responsible for generating and returning the HTML content for a login form
/// when [AuthRedirectController.getAuthorizationPage] is called in response to a GET request.
///
/// The form submission should include the values of [responseType], [clientID], [state], [scope]
/// as well as user-entered username and password in `x-www-form-urlencoded` data, e.g.
@ -51,7 +78,23 @@ abstract class AuthRedirectControllerDelegate {
class AuthRedirectController extends ResourceController {
/// Creates a new instance of an [AuthRedirectController].
///
/// [authServer] is the required authorization server. If [delegate] is provided, this controller will return a login page for all GET requests.
/// This constructor initializes an [AuthRedirectController] with the provided [authServer].
///
/// Parameters:
/// - [authServer]: The required authorization server.
/// - [delegate]: Optional. If provided, this controller will return a login page for all GET requests.
/// - [allowsImplicit]: Optional. Defaults to true. Determines if the controller allows the Implicit Grant Flow.
///
/// The constructor also sets the [acceptedContentTypes] to ["application/x-www-form-urlencoded"].
///
/// Usage:
/// ```dart
/// final authRedirectController = AuthRedirectController(
/// myAuthServer,
/// delegate: myDelegate,
/// allowsImplicit: false,
/// );
/// ```
AuthRedirectController(
this.authServer, {
this.delegate,
@ -62,38 +105,108 @@ class AuthRedirectController extends ResourceController {
];
}
/// A pre-defined Response object for unsupported response types.
///
/// This static final variable creates a Response object with:
/// - HTTP status code 400 (Bad Request)
/// - HTML content indicating an "unsupported_response_type" error
/// - Content-Type set to text/html
///
/// This response is used when the 'response_type' parameter in the request
/// is neither 'code' nor 'token', or when 'token' is requested but implicit
/// grant flow is not allowed.
static final Response _unsupportedResponseTypeResponse = Response.badRequest(
body: "<h1>Error</h1><p>unsupported_response_type</p>",
)..contentType = ContentType.html;
/// A reference to the [AuthServer] used to grant authorization codes and access tokens.
///
/// This property holds an instance of [AuthServer] which is responsible for
/// handling the authentication and authorization processes. It is used by
/// this controller to issue authorization codes and access tokens as part of
/// the OAuth 2.0 flow.
///
/// The [AuthServer] instance should be properly configured and initialized
/// before being assigned to this property.
late final AuthServer authServer;
/// When true, the controller allows for the Implicit Grant Flow
/// Determines whether the controller allows the OAuth 2.0 Implicit Grant Flow.
///
/// When set to true, the controller will process requests for access tokens
/// directly (response_type=token). When false, such requests will be rejected.
///
/// This property is typically set in the constructor and should not be
/// modified after initialization.
final bool allowsImplicit;
/// A randomly generated value the client can use to verify the origin of the redirect.
///
/// Clients must include this query parameter and verify that any redirects from this
/// server have the same value for 'state' as passed in. This value is usually a randomly generated
/// session identifier.
/// This property is bound to the 'state' query parameter of the incoming request.
/// It serves as a security measure to prevent cross-site request forgery (CSRF) attacks.
///
/// Clients must include this query parameter when initiating an authorization request.
/// Upon receiving a redirect from this server, clients should verify that the 'state'
/// value in the redirect matches the one they initially sent. This ensures that the
/// response is for the request they initiated and not for a malicious request.
///
/// The value of 'state' is typically a randomly generated string or session identifier.
/// It should be unique for each authorization request to maintain security.
///
/// Example usage:
/// ```
/// GET /authorize?response_type=code&client_id=CLIENT_ID&state=RANDOM_STATE
/// ```
@Bind.query("state")
String? state;
/// Must be 'code' or 'token'.
/// The type of response requested from the authorization endpoint.
///
/// This property is bound to the 'response_type' query parameter of the incoming request.
/// It must be either 'code' or 'token':
/// - 'code': Indicates that the client is initiating the authorization code flow.
/// - 'token': Indicates that the client is initiating the implicit flow.
///
/// The value of this property determines the type of credential (authorization code or access token)
/// that will be issued upon successful authentication.
///
/// Note: The availability of the 'token' response type depends on the [allowsImplicit] setting
/// of the controller.
@Bind.query("response_type")
String? responseType;
/// The client ID of the authenticating client.
///
/// This must be a valid client ID according to [authServer].\
/// This property is bound to the 'client_id' query parameter of the incoming request.
/// It represents the unique identifier of the client application requesting authorization.
///
/// The client ID must be registered and valid according to the [authServer].
/// It is used to identify the client during the authorization process and to ensure
/// that only authorized clients can request access tokens or authorization codes.
///
/// This field is nullable, but typically required for most OAuth 2.0 flows.
/// If not provided in the request, it may lead to authorization failures.
///
/// Example usage in a request URL:
/// ```
/// GET /authorize?client_id=my_client_id&...
/// ```
@Bind.query("client_id")
String? clientID;
/// Renders an HTML login form.
/// Delegate responsible for rendering the HTML login form.
///
/// If provided, this delegate will be used to generate a custom login page
/// when [getAuthorizationPage] is called. The delegate's [render] method
/// is responsible for creating the HTML content of the login form.
///
/// When this property is null, the controller will not serve a login page
/// and will respond with a 405 Method Not Allowed status for GET requests.
///
/// This delegate allows for customization of the login experience while
/// maintaining the required OAuth 2.0 authorization flow.
final AuthRedirectControllerDelegate? delegate;
/// Returns an HTML login form.
/// Returns an HTML login form for OAuth 2.0 authorization.
///
/// A client that wishes to authenticate with this server should direct the user
/// to this page. The user will enter their username and password that is sent as a POST
@ -129,9 +242,10 @@ class AuthRedirectController extends ResourceController {
/// Creates a one-time use authorization code or an access token.
///
/// This method will respond with a redirect that either contains an authorization code ('code')
/// or an access token ('token') along with the passed in 'state'. If this request fails,
/// the redirect URL will contain an 'error' instead of the authorization code or access token.
/// This method handles the OAuth 2.0 authorization process, responding with a redirect
/// that contains either an authorization code ('code') or an access token ('token')
/// along with the passed in 'state'. If the request fails, the redirect URL will
/// contain an 'error' instead of the authorization code or access token.
///
/// This method is typically invoked by the login form returned from the GET to this controller.
@Operation.post()
@ -229,6 +343,19 @@ class AuthRedirectController extends ResourceController {
}
}
/// Customizes the API documentation for the request body of this controller's operations.
///
/// This method overrides the default implementation to add specific details to the
/// POST operation's request body schema:
/// - Sets the format of the 'password' field to "password".
/// - Marks 'client_id', 'state', 'response_type', 'username', and 'password' as required fields.
///
/// Parameters:
/// - context: The API documentation context.
/// - operation: The operation being documented.
///
/// Returns:
/// The modified [APIRequestBody] object, or null if no modifications were made.
@override
APIRequestBody? documentOperationRequestBody(
APIDocumentContext context,
@ -249,6 +376,20 @@ class AuthRedirectController extends ResourceController {
return body;
}
/// Customizes the API documentation for the operation parameters of this controller.
///
/// This method overrides the default implementation to mark all parameters
/// as required, except for the 'scope' parameter. It does this by:
/// 1. Calling the superclass method to get the initial list of parameters.
/// 2. Iterating through all parameters except 'scope'.
/// 3. Setting the 'isRequired' property of each parameter to true.
///
/// Parameters:
/// - context: The API documentation context.
/// - operation: The operation being documented.
///
/// Returns:
/// A list of [APIParameter] objects with updated 'isRequired' properties.
@override
List<APIParameter> documentOperationParameters(
APIDocumentContext context,
@ -261,6 +402,29 @@ class AuthRedirectController extends ResourceController {
return params;
}
/// Generates API documentation for the responses of this controller's operations.
///
/// This method overrides the default implementation to provide custom documentation
/// for the GET and POST operations of the AuthRedirectController.
///
/// For GET requests:
/// - Documents a 200 response that serves a login form in HTML format.
///
/// For POST requests:
/// - Documents a 302 (Moved Temporarily) response for successful authorizations,
/// explaining the structure of the redirect URI for both 'code' and 'token' response types.
/// - Documents a 400 (Bad Request) response for cases where the client ID is invalid
/// and the redirect URI cannot be verified.
///
/// Parameters:
/// - context: The API documentation context.
/// - operation: The operation being documented.
///
/// Returns:
/// A Map of status codes to APIResponse objects describing the possible responses.
///
/// Throws:
/// StateError if documentation fails (i.e., for unexpected HTTP methods).
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
@ -298,6 +462,24 @@ class AuthRedirectController extends ResourceController {
throw StateError("AuthRedirectController documentation failed.");
}
/// Customizes the API documentation for the operations of this controller.
///
/// This method overrides the default implementation to update the authorization URLs
/// for both the Authorization Code Flow and Implicit Flow in the API documentation.
///
/// It performs the following steps:
/// 1. Calls the superclass method to get the initial operations documentation.
/// 2. Constructs a URI from the given route.
/// 3. Sets this URI as the authorization URL for both the Authorization Code Flow
/// and the Implicit Flow in the auth server's documentation.
///
/// Parameters:
/// - context: The API documentation context.
/// - route: The route string for this controller.
/// - path: The APIPath object representing this controller's path.
///
/// Returns:
/// A Map of operation IDs to APIOperation objects describing the operations of this controller.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,
@ -311,6 +493,27 @@ class AuthRedirectController extends ResourceController {
return ops;
}
/// Generates a redirect response for OAuth 2.0 authorization flow.
///
/// This method constructs a redirect URI based on the given parameters and the type of response
/// (code or token) requested. It handles both successful authorizations and error cases.
///
/// Parameters:
/// - [inputUri]: The base URI to redirect to. If null, falls back to the client's registered redirect URI.
/// - [clientStateOrNull]: The state parameter provided by the client for CSRF protection.
/// - [code]: The authorization code (for code flow).
/// - [token]: The access token (for token/implicit flow).
/// - [error]: Any error that occurred during the authorization process.
///
/// Returns:
/// - A [Response] object with a 302 status code and appropriate headers for redirection.
/// - If the redirect URI is invalid or cannot be constructed, returns a 400 Bad Request response.
///
/// The method constructs the redirect URI as follows:
/// - For 'code' response type: Adds code, state, and error (if any) as query parameters.
/// - For 'token' response type: Adds token details, state, and error (if any) as URI fragment.
///
/// The response includes headers to prevent caching of the redirect.
Response _redirectResponse(
String? inputUri,
String? clientStateOrNull, {

View file

@ -1,5 +1,23 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:convert';
/// An abstract class for parsing authorization headers.
///
/// This class defines a common interface for parsing different types of
/// authorization headers. Implementations of this class should provide
/// specific parsing logic for different authorization schemes (e.g., Bearer, Basic).
///
/// The type parameter [T] represents the return type of the [parse] method,
/// allowing for flexibility in the parsed result (e.g., String for Bearer tokens,
/// custom credential objects for other schemes).
abstract class AuthorizationParser<T> {
const AuthorizationParser();
@ -7,11 +25,24 @@ abstract class AuthorizationParser<T> {
}
/// Parses a Bearer token from an Authorization header.
///
/// This class extends [AuthorizationParser] and specializes in parsing Bearer tokens
/// from Authorization headers. It implements the [parse] method to extract the token
/// from a given header string.
///
/// Usage:
/// ```dart
/// final parser = AuthorizationBearerParser();
/// final token = parser.parse("Bearer myToken123");
/// print(token); // Outputs: myToken123
/// ```
///
/// If the header is invalid or missing, it throws an [AuthorizationParserException]
/// with an appropriate [AuthorizationParserExceptionReason].
class AuthorizationBearerParser extends AuthorizationParser<String?> {
const AuthorizationBearerParser();
/// Parses a Bearer token from [authorizationHeader]. If the header is malformed or doesn't exist,
/// throws an [AuthorizationParserException]. Otherwise, returns the [String] representation of the bearer token.
/// Parses a Bearer token from an Authorization header.
///
/// For example, if the input to this method is "Bearer token" it would return 'token'.
///
@ -37,6 +68,26 @@ class AuthorizationBearerParser extends AuthorizationParser<String?> {
/// A structure to hold Basic authorization credentials.
///
/// This class represents the credentials used in Basic HTTP Authentication.
/// It contains two properties: [username] and [password].
///
/// The [username] and [password] are marked as `late final`, indicating that
/// they must be initialized before use, but can only be set once.
///
/// This class is typically used in conjunction with [AuthorizationBasicParser]
/// to parse and store credentials from a Basic Authorization header.
///
/// The [toString] method is overridden to provide a string representation
/// of the credentials in the format "username:password".
///
/// Example usage:
/// ```dart
/// final credentials = AuthBasicCredentials()
/// ..username = 'john_doe'
/// ..password = 'secret123';
/// print(credentials); // Outputs: john_doe:secret123
/// ```
///
/// See [AuthorizationBasicParser] for getting instances of this type.
class AuthBasicCredentials {
/// The username of a Basic Authorization header.
@ -50,14 +101,38 @@ class AuthBasicCredentials {
}
/// Parses a Basic Authorization header.
///
/// This class extends [AuthorizationParser] and specializes in parsing Basic Authentication
/// credentials from Authorization headers. It implements the [parse] method to extract
/// the username and password from a given header string.
///
/// The parser expects the header to be in the format "Basic <base64-encoded-credentials>",
/// where the credentials are a string of "username:password" encoded in Base64.
///
/// Usage:
/// ```dart
/// final parser = AuthorizationBasicParser();
/// final credentials = parser.parse("Basic dXNlcm5hbWU6cGFzc3dvcmQ=");
/// print(credentials.username); // Outputs: username
/// print(credentials.password); // Outputs: password
/// ```
///
/// If the header is invalid, missing, or cannot be properly decoded, it throws an
/// [AuthorizationParserException] with an appropriate [AuthorizationParserExceptionReason].
class AuthorizationBasicParser
extends AuthorizationParser<AuthBasicCredentials> {
/// Creates a constant instance of [AuthorizationBasicParser].
///
/// This constructor allows for the creation of immutable instances of the parser,
/// which can be safely shared and reused across multiple parts of an application.
///
/// Example usage:
/// ```dart
/// final parser = const AuthorizationBasicParser();
/// ```
const AuthorizationBasicParser();
/// Returns a [AuthBasicCredentials] containing the username and password
/// base64 encoded in [authorizationHeader]. For example, if the input to this method
/// was 'Basic base64String' it would decode the base64String
/// and return the username and password by splitting that decoded string around the character ':'.
/// Parses a Basic Authorization header and returns [AuthBasicCredentials].
///
/// If [authorizationHeader] is malformed or null, throws an [AuthorizationParserException].
@override
@ -100,10 +175,49 @@ class AuthorizationBasicParser
}
}
/// The reason either [AuthorizationBearerParser] or [AuthorizationBasicParser] failed.
/// Enumerates the possible reasons for authorization parsing failures.
///
/// This enum is used in conjunction with [AuthorizationParserException] to
/// provide more specific information about why the parsing of an authorization
/// header failed.
///
/// The enum contains two values:
/// - [missing]: Indicates that the required authorization header was not present.
/// - [malformed]: Indicates that the authorization header was present but its
/// format was incorrect or could not be properly parsed.
///
/// This enum is typically used by [AuthorizationBearerParser] and
/// [AuthorizationBasicParser] to specify the nature of parsing failures.
enum AuthorizationParserExceptionReason { missing, malformed }
/// An exception indicating why Authorization parsing failed.
/// An exception class for errors encountered during authorization parsing.
///
/// This exception is thrown when there's an issue parsing an authorization header.
/// It contains a [reason] field of type [AuthorizationParserExceptionReason]
/// which provides more specific information about why the parsing failed.
///
/// The [reason] can be either [AuthorizationParserExceptionReason.missing]
/// (indicating the absence of a required authorization header) or
/// [AuthorizationParserExceptionReason.malformed] (indicating an incorrectly
/// formatted authorization header).
///
/// This exception is typically thrown by implementations of [AuthorizationParser],
/// such as [AuthorizationBearerParser] and [AuthorizationBasicParser].
///
/// Example usage:
/// ```dart
/// try {
/// parser.parse(header);
/// } catch (e) {
/// if (e is AuthorizationParserException) {
/// if (e.reason == AuthorizationParserExceptionReason.missing) {
/// print('Authorization header is missing');
/// } else if (e.reason == AuthorizationParserExceptionReason.malformed) {
/// print('Authorization header is malformed');
/// }
/// }
/// }
/// ```
class AuthorizationParserException implements Exception {
AuthorizationParserException(this.reason);

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:math';
@ -8,9 +17,10 @@ import 'package:crypto/crypto.dart';
/// A OAuth 2.0 authorization server.
///
/// An [AuthServer] is an implementation of an OAuth 2.0 authorization server. An authorization server
/// issues, refreshes and revokes access tokens. It also verifies previously issued tokens, as
/// well as client and resource owner credentials.
/// This class implements the core functionality of an OAuth 2.0 authorization server,
/// including client management, token issuance, token refresh, and token verification.
/// It supports various OAuth 2.0 flows such as password, client credentials, authorization code,
/// and refresh token.
///
/// [AuthServer]s are typically used in conjunction with [AuthController] and [AuthRedirectController].
/// These controllers provide HTTP interfaces to the [AuthServer] for issuing and refreshing tokens.
@ -57,9 +67,29 @@ import 'package:crypto/crypto.dart';
/// }
///
class AuthServer implements AuthValidator, APIComponentDocumenter {
/// Creates a new instance of an [AuthServer] with a [delegate].
/// This constructor initializes an [AuthServer] with the provided [delegate],
/// which is responsible for managing authentication-related data storage and retrieval.
///
/// [hashFunction] defaults to [sha256].
/// Parameters:
/// - [delegate]: An instance of [AuthServerDelegate] that handles data persistence.
/// - [hashRounds]: The number of iterations for password hashing. Defaults to 1000.
/// - [hashLength]: The length of the generated hash in bytes. Defaults to 32.
/// - [hashFunction]: The hash function to use. Defaults to [sha256].
///
/// The [hashRounds], [hashLength], and [hashFunction] parameters configure the
/// password hashing mechanism used by this [AuthServer] instance. These values
/// affect the security and performance of password hashing operations.
///
/// Example:
/// ```dart
/// final delegate = MyAuthServerDelegate();
/// final authServer = AuthServer(
/// delegate,
/// hashRounds: 1000,
/// hashLength: 32,
/// hashFunction: sha256,
/// );
/// ```
AuthServer(
this.delegate, {
this.hashRounds = 1000,
@ -74,32 +104,117 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
///
/// It is preferable to use the implementation of [AuthServerDelegate] from 'package:conduit_core/managed_auth.dart'. See
/// [AuthServer] for more details.
///
/// This delegate plays a crucial role in the OAuth 2.0 flow by managing the persistence
/// of authentication-related objects. It abstracts away the storage implementation,
/// allowing for flexibility in how these objects are stored (e.g., in-memory, database).
///
/// The delegate is responsible for the following main tasks:
/// 1. Storing and retrieving AuthClient information
/// 2. Managing AuthToken lifecycle (creation, retrieval, and revocation)
/// 3. Handling AuthCode operations for the authorization code flow
/// 4. Fetching ResourceOwner information for authentication purposes
///
/// Implementations of this delegate should ensure thread-safety and efficient
/// data access to maintain the performance and security of the authentication server.
final AuthServerDelegate delegate;
/// The number of hashing rounds performed by this instance when validating a password.
///
/// This value determines the number of iterations the password hashing algorithm
/// will perform. A higher number of rounds increases the computational cost and
/// time required to hash a password, making it more resistant to brute-force attacks.
/// However, it also increases the time needed for legitimate password verification.
///
/// The optimal value balances security and performance based on the specific
/// requirements of the application. Common values range from 1000 to 50000,
/// but may need adjustment based on hardware capabilities and security needs.
final int hashRounds;
/// The resulting key length of a password hash when generated by this instance.
///
/// This value determines the length (in bytes) of the generated password hash.
/// A longer hash length generally provides more security against certain types of attacks,
/// but also requires more storage space. Common values range from 16 to 64 bytes.
///
/// This parameter is used in conjunction with [hashRounds] and [hashFunction]
/// to configure the password hashing algorithm (typically PBKDF2).
final int hashLength;
/// The [Hash] function used by the PBKDF2 algorithm to generate password hashes by this instance.
///
/// This function is used in the password hashing process to create secure, one-way
/// hashes of passwords. The PBKDF2 (Password-Based Key Derivation Function 2)
/// algorithm uses this hash function repeatedly to increase the computational cost
/// of cracking the resulting hash.
///
/// By default, this is set to [sha256], but it can be customized to use other
/// cryptographic hash functions if needed. The choice of hash function affects
/// the security and performance characteristics of the password hashing process.
///
/// This property works in conjunction with [hashRounds] and [hashLength] to
/// configure the overall password hashing strategy of the AuthServer.
final Hash hashFunction;
/// Used during OpenAPI documentation.
/// Represents the OAuth 2.0 Authorization Code flow for OpenAPI documentation purposes.
///
/// This property is used to document the Authorization Code flow in the OpenAPI
/// specification generated for this AuthServer. It is initialized as an empty
/// OAuth2 flow with an empty scopes map, which can be populated later with
/// the specific scopes supported by the server.
///
/// The Authorization Code flow is a secure way of obtaining access tokens
/// that involves a client application directing the resource owner to an
/// authorization server to grant permission, then using the resulting
/// authorization code to obtain an access token.
///
/// This property is typically used in conjunction with the `documentComponents`
/// method to properly document the OAuth2 security scheme in the API specification.
final APISecuritySchemeOAuth2Flow documentedAuthorizationCodeFlow =
APISecuritySchemeOAuth2Flow.empty()..scopes = {};
/// Used during OpenAPI documentation.
/// Represents the OAuth 2.0 Password flow for OpenAPI documentation purposes.
///
/// This property is used to document the Password flow in the OpenAPI
/// specification generated for this AuthServer. It is initialized as an empty
/// OAuth2 flow with an empty scopes map, which can be populated later with
/// the specific scopes supported by the server for the Password flow.
///
/// The Password flow allows users to exchange their username and password
/// directly for an access token. This flow should only be used by trusted
/// applications due to its sensitivity in handling user credentials.
///
/// This property is typically used in conjunction with the `documentComponents`
/// method to properly document the OAuth2 security scheme in the API specification.
final APISecuritySchemeOAuth2Flow documentedPasswordFlow =
APISecuritySchemeOAuth2Flow.empty()..scopes = {};
/// Used during OpenAPI documentation.
/// Represents the OAuth 2.0 Implicit flow for OpenAPI documentation purposes.
///
/// This property is used to document the Implicit flow in the OpenAPI
/// specification generated for this AuthServer. It is initialized as an empty
/// OAuth2 flow with an empty scopes map, which can be populated later with
/// the specific scopes supported by the server for the Implicit flow.
///
/// The Implicit flow is designed for client-side applications (e.g., single-page web apps)
/// where the access token is returned immediately without an extra authorization code
/// exchange step. This flow has some security trade-offs and is generally not recommended
/// for new implementations.
///
/// This property is typically used in conjunction with the `documentComponents`
/// method to properly document the OAuth2 security scheme in the API specification.
final APISecuritySchemeOAuth2Flow documentedImplicitFlow =
APISecuritySchemeOAuth2Flow.empty()..scopes = {};
/// Constant representing the token type "bearer" for OAuth 2.0 access tokens.
///
/// This value is used to specify the type of token issued by the authorization server.
/// The "bearer" token type is defined in RFC 6750 and is the most common type used in OAuth 2.0.
/// Bearer tokens can be used by any party in possession of the token to access protected resources
/// without demonstrating possession of a cryptographic key.
static const String tokenTypeBearer = "bearer";
/// Hashes a [password] with [salt] using PBKDF2 algorithm.
/// Hashes a password using the PBKDF2 algorithm.
///
/// See [hashRounds], [hashLength] and [hashFunction] for more details. This method
/// invoke [auth.generatePasswordHash] with the above inputs.
@ -113,7 +228,7 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
);
}
/// Adds an OAuth2 client.
/// Adds a new OAuth2 client to the authentication server.
///
/// [delegate] will store this client for future use.
Future addClient(AuthClient client) async {
@ -132,14 +247,14 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return delegate.addClient(this, client);
}
/// Returns a [AuthClient] record for its [clientID].
/// Retrieves an [AuthClient] record based on the provided [clientID].
///
/// Returns null if none exists.
Future<AuthClient?> getClient(String clientID) async {
return delegate.getClient(this, clientID);
}
/// Revokes a [AuthClient] record.
/// Revokes and removes an [AuthClient] record associated with the given [clientID].
///
/// Removes cached occurrences of [AuthClient] for [clientID].
/// Asks [delegate] to remove an [AuthClient] by its ID via [AuthServerDelegate.removeClient].
@ -151,7 +266,7 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return delegate.removeClient(this, clientID);
}
/// Revokes access for an [ResourceOwner].
/// Revokes all access grants for a specific resource owner.
///
/// All authorization codes and tokens for the [ResourceOwner] identified by [identifier]
/// will be revoked.
@ -163,7 +278,7 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
await delegate.removeTokens(this, identifier);
}
/// Authenticates a username and password of an [ResourceOwner] and returns an [AuthToken] upon success.
/// Authenticates a username and password of a [ResourceOwner] and returns an [AuthToken] upon success.
///
/// This method works with this instance's [delegate] to generate and store a new token if all credentials are correct.
/// If credentials are not correct, it will throw the appropriate [AuthRequestError].
@ -230,7 +345,7 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return token;
}
/// Returns a [Authorization] for [accessToken].
/// Verifies the validity of an access token and returns an [Authorization] object.
///
/// This method obtains an [AuthToken] for [accessToken] from [delegate] and then verifies that the token is valid.
/// If the token is valid, an [Authorization] object is returned. Otherwise, an [AuthServerException] is thrown.
@ -269,9 +384,34 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
/// Refreshes a valid [AuthToken] instance.
///
/// This method will refresh a [AuthToken] given the [AuthToken]'s [refreshToken] for a given client ID.
/// This method coordinates with this instance's [delegate] to update the old token with a new access token and issue/expiration dates if successful.
/// If not successful, it will throw an [AuthRequestError].
/// This method refreshes an existing [AuthToken] using its [refreshToken] for a given client ID.
/// It coordinates with the instance's [delegate] to update the old token with a new access token
/// and issue/expiration dates if successful. If unsuccessful, it throws an [AuthRequestError].
///
/// The method performs several validation steps:
/// 1. Verifies the client ID and retrieves the corresponding [AuthClient].
/// 2. Checks for the presence of a refresh token.
/// 3. Retrieves the existing token using the refresh token.
/// 4. Validates the client secret.
/// 5. Handles scope validation and updates:
/// - If new scopes are requested, it ensures they are subsets of existing scopes and allowed by the client.
/// - If no new scopes are requested, it verifies that existing scopes are still valid for the client.
///
/// Parameters:
/// - [refreshToken]: The refresh token of the [AuthToken] to be refreshed.
/// - [clientID]: The ID of the client requesting the token refresh.
/// - [clientSecret]: The secret of the client requesting the token refresh.
/// - [requestedScopes]: Optional list of scopes to be applied to the refreshed token.
///
/// Returns:
/// A [Future] that resolves to a new [AuthToken] with updated access token, issue date, and expiration date.
///
/// Throws:
/// - [AuthServerException] with [AuthRequestError.invalidClient] if the client ID is invalid or empty.
/// - [AuthServerException] with [AuthRequestError.invalidRequest] if the refresh token is missing.
/// - [AuthServerException] with [AuthRequestError.invalidGrant] if the token is not found or doesn't match the client ID.
/// - [AuthServerException] with [AuthRequestError.invalidClient] if the client secret is invalid.
/// - [AuthServerException] with [AuthRequestError.invalidScope] if the requested scopes are invalid or not allowed.
Future<AuthToken> refresh(
String? refreshToken,
String clientID,
@ -356,9 +496,34 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
/// Creates a one-time use authorization code for a given client ID and user credentials.
///
/// This methods works with this instance's [delegate] to generate and store the authorization code
/// if the credentials are correct. If they are not correct, it will throw the
/// appropriate [AuthRequestError].
/// This method is part of the OAuth 2.0 Authorization Code flow. It authenticates a user
/// with their username and password for a specific client, and if successful, generates
/// a short-lived authorization code.
///
/// The method performs several steps:
/// 1. Validates the client ID and retrieves the client information.
/// 2. Authenticates the user with the provided username and password.
/// 3. Validates the requested scopes against the client's allowed scopes and the user's permissions.
/// 4. Generates a new authorization code.
/// 5. Stores the authorization code using the delegate.
///
/// Parameters:
/// - [username]: The username of the resource owner (user).
/// - [password]: The password of the resource owner.
/// - [clientID]: The ID of the client requesting the authorization code.
/// - [expirationInSeconds]: The lifetime of the authorization code in seconds (default is 600 seconds or 10 minutes).
/// - [requestedScopes]: Optional list of scopes the client is requesting access to.
///
/// Returns:
/// A [Future] that resolves to an [AuthCode] object representing the generated authorization code.
///
/// Throws:
/// - [AuthServerException] with [AuthRequestError.invalidClient] if the client ID is invalid or empty.
/// - [AuthServerException] with [AuthRequestError.invalidRequest] if the username or password is missing.
/// - [AuthServerException] with [AuthRequestError.unauthorizedClient] if the client doesn't have a redirect URI.
/// - [AuthServerException] with [AuthRequestError.accessDenied] if the user credentials are invalid.
///
/// The generated authorization code can later be exchanged for an access token using the `exchange` method.
Future<AuthCode> authenticateForCode(
String? username,
String? password,
@ -408,9 +573,35 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
/// Exchanges a valid authorization code for an [AuthToken].
///
/// If the authorization code has not expired, has not been used, matches the client ID,
/// and the client secret is correct, it will return a valid [AuthToken]. Otherwise,
/// it will throw an appropriate [AuthRequestError].
/// This method is part of the OAuth 2.0 Authorization Code flow. It allows a client
/// to exchange a previously obtained authorization code for an access token.
///
/// The method performs several validation steps:
/// 1. Verifies the client ID and retrieves the corresponding [AuthClient].
/// 2. Checks for the presence of the authorization code.
/// 3. Validates the client secret.
/// 4. Retrieves and validates the stored authorization code.
/// 5. Checks if the authorization code is still valid and hasn't been used.
/// 6. Ensures the client ID matches the one associated with the authorization code.
///
/// If all validations pass, it generates a new access token and stores it using the delegate.
///
/// Parameters:
/// - [authCodeString]: The authorization code to be exchanged.
/// - [clientID]: The ID of the client requesting the token exchange.
/// - [clientSecret]: The secret of the client requesting the token exchange.
/// - [expirationInSeconds]: The lifetime of the generated access token in seconds (default is 3600 seconds or 1 hour).
///
/// Returns:
/// A [Future] that resolves to an [AuthToken] representing the newly created access token.
///
/// Throws:
/// - [AuthServerException] with [AuthRequestError.invalidClient] if the client ID is invalid or empty, or if the client secret is incorrect.
/// - [AuthServerException] with [AuthRequestError.invalidRequest] if the authorization code is missing.
/// - [AuthServerException] with [AuthRequestError.invalidGrant] if the authorization code is invalid, expired, or has been used before.
///
/// This method is crucial for completing the Authorization Code flow, allowing clients
/// to securely obtain access tokens after receiving user authorization.
Future<AuthToken> exchange(
String? authCodeString,
String clientID,
@ -474,6 +665,22 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
//////
// APIDocumentable overrides
//////
/// Generates and registers security schemes for API documentation.
///
/// This method is responsible for documenting the security components of the API,
/// specifically the OAuth2 client authentication and standard OAuth2 flows.
///
/// It performs the following tasks:
/// 1. Registers a basic HTTP authentication scheme for OAuth2 client authentication.
/// 2. Creates and registers an OAuth2 security scheme with authorization code and password flows.
/// 3. Defers cleanup of unused flows based on the presence of required URLs.
///
/// The method uses the [APIDocumentContext] to register these security schemes,
/// making them available for use in the API documentation.
///
/// Parameters:
/// - [context]: The [APIDocumentContext] used to register security schemes and defer cleanup operations.
@override
void documentComponents(APIDocumentContext context) {
final basic = APISecurityScheme.http("basic")
@ -508,6 +715,27 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
/////
// AuthValidator overrides
/////
/// Documents the security requirements for an [Authorizer] in the API specification.
///
/// This method generates the appropriate [APISecurityRequirement] objects
/// based on the type of authorization parser used by the [Authorizer].
///
/// For basic authentication (AuthorizationBasicParser), it specifies the
/// requirement for OAuth2 client authentication.
///
/// For bearer token authentication (AuthorizationBearerParser), it specifies
/// the requirement for OAuth2 with optional scopes.
///
/// Parameters:
/// - [context]: The API documentation context.
/// - [authorizer]: The Authorizer instance for which to generate requirements.
/// - [scopes]: Optional list of scopes to be included in the OAuth2 requirement.
///
/// Returns:
/// A list of [APISecurityRequirement] objects representing the security
/// requirements for the given authorizer. Returns an empty list if the
/// parser type is not recognized.
@override
List<APISecurityRequirement> documentRequirementsForAuthorizer(
APIDocumentContext context,
@ -529,6 +757,25 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return [];
}
/// Validates an authorization request using the specified parser and authorization data.
///
/// This method is responsible for validating different types of authorization,
/// including client credentials (Basic) and bearer tokens.
///
/// Parameters:
/// - [parser]: An instance of [AuthorizationParser] used to parse the authorization data.
/// - [authorizationData]: The authorization data to be validated, type depends on the parser.
/// - [requiredScope]: Optional list of [AuthScope]s required for the authorization.
///
/// Returns:
/// A [FutureOr<Authorization>] representing the validated authorization.
///
/// Throws:
/// - [ArgumentError] if an invalid parser is provided.
///
/// The method behaves differently based on the type of parser:
/// - For [AuthorizationBasicParser], it validates client credentials.
/// - For [AuthorizationBearerParser], it verifies the bearer token.
@override
FutureOr<Authorization> validate<T>(
AuthorizationParser<T> parser,
@ -547,6 +794,31 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
);
}
/// Validates client credentials for OAuth 2.0 client authentication.
///
/// This method is used to authenticate a client using its client ID and secret
/// as part of the OAuth 2.0 client authentication process.
///
/// The method performs the following steps:
/// 1. Retrieves the client using the provided client ID (username).
/// 2. Validates the client's existence and secret.
/// 3. For public clients (no secret), it allows authentication with an empty password.
/// 4. For confidential clients, it verifies the provided password against the stored hashed secret.
///
/// Parameters:
/// - [credentials]: An [AuthBasicCredentials] object containing the client ID (username) and secret (password).
///
/// Returns:
/// A [Future<Authorization>] representing the authenticated client.
///
/// Throws:
/// - [AuthServerException] with [AuthRequestError.invalidClient] if:
/// - The client is not found.
/// - A public client provides a non-empty password.
/// - A confidential client provides an incorrect secret.
///
/// This method is typically used in the context of the client credentials grant type
/// or when a client needs to authenticate itself for other OAuth 2.0 flows.
Future<Authorization> _validateClientCredentials(
AuthBasicCredentials credentials,
) async {
@ -574,6 +846,29 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return Authorization(client.id, null, this, credentials: credentials);
}
/// Validates and filters the requested scopes for a client and resource owner.
///
/// This method checks the requested scopes against the client's allowed scopes
/// and the resource owner's permitted scopes. It ensures that only valid and
/// authorized scopes are granted.
///
/// Parameters:
/// - [client]: The [AuthClient] requesting the scopes.
/// - [authenticatable]: The [ResourceOwner] being authenticated.
/// - [requestedScopes]: The list of [AuthScope]s requested by the client.
///
/// Returns:
/// A list of validated [AuthScope]s that are allowed for both the client and
/// the resource owner. Returns null if the client doesn't support scopes.
///
/// Throws:
/// - [AuthServerException] with [AuthRequestError.invalidScope] if:
/// - The client supports scopes but no scopes are requested.
/// - None of the requested scopes are allowed for the client.
/// - The filtered scopes are not allowed for the resource owner.
///
/// This method is crucial for maintaining the principle of least privilege
/// in OAuth 2.0 flows by ensuring that tokens are issued with appropriate scopes.
List<AuthScope>? _validatedScopes(
AuthClient client,
ResourceOwner authenticatable,
@ -611,6 +906,24 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return validScopes;
}
/// Generates a new [AuthToken] with the specified parameters.
///
/// This method creates and initializes a new [AuthToken] object with the given
/// owner ID, client ID, and expiration time. It also sets other properties such
/// as the access token, issue date, token type, and optional refresh token.
///
/// Parameters:
/// - [ownerID]: The identifier of the resource owner (user).
/// - [clientID]: The identifier of the client application.
/// - [expirationInSeconds]: The number of seconds until the token expires.
/// - [allowRefresh]: Whether to generate a refresh token (default is true).
/// - [scopes]: Optional list of scopes associated with the token.
///
/// Returns:
/// A new [AuthToken] instance with all properties set according to the input parameters.
///
/// The access token and refresh token (if allowed) are generated as random strings.
/// The token type is set to "bearer" as defined by [tokenTypeBearer].
AuthToken _generateToken(
int? ownerID,
String clientID,
@ -635,6 +948,24 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return token;
}
/// Generates a new [AuthCode] with the specified parameters.
///
/// This method creates and initializes a new [AuthCode] object with the given
/// owner ID, client, and expiration time. It also sets other properties such
/// as the authorization code, issue date, and optional scopes.
///
/// Parameters:
/// - [ownerID]: The identifier of the resource owner (user).
/// - [client]: The [AuthClient] for which the auth code is being generated.
/// - [expirationInSeconds]: The number of seconds until the auth code expires.
/// - [scopes]: Optional list of scopes associated with the auth code.
///
/// Returns:
/// A new [AuthCode] instance with all properties set according to the input parameters.
///
/// The authorization code is generated as a random string of 32 characters.
/// The issue date is set to the current UTC time, and the expiration date is
/// calculated based on the [expirationInSeconds] parameter.
AuthCode _generateAuthCode(
int? ownerID,
AuthClient client,
@ -652,6 +983,19 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
}
}
/// Generates a random string of specified length.
///
/// This function creates a random string using a combination of uppercase letters,
/// lowercase letters, and digits. It uses a cryptographically secure random number
/// generator to ensure unpredictability.
///
/// The function works by repeatedly selecting random characters from a predefined
/// set of possible characters and appending them to a string buffer. The selection
/// process uses the modulo operation to ensure an even distribution across the
/// character set.
///
/// Returns:
/// A string of the specified [length] containing random characters.
String randomStringOfLength(int length) {
const possibleCharacters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:io';
@ -8,8 +17,9 @@ import 'package:protevus_openapi/v3.dart';
/// A [Controller] that validates the Authorization header of a request.
///
/// An instance of this type will validate that the authorization information in an Authorization header is sufficient to access
/// the next controller in the channel.
/// This class, Authorizer, is responsible for authenticating and authorizing incoming HTTP requests.
/// It validates the Authorization header, processes it according to the specified parser (e.g., Bearer or Basic),
/// and then uses the provided validator to check the credentials.
///
/// For each request, this controller parses the authorization header, validates it with an [AuthValidator] and then create an [Authorization] object
/// if successful. The [Request] keeps a reference to this [Authorization] and is then sent to the next controller in the channel.
@ -28,7 +38,7 @@ import 'package:protevus_openapi/v3.dart';
class Authorizer extends Controller {
/// Creates an instance of [Authorizer].
///
/// Use this constructor to provide custom [AuthorizationParser]s.
/// This constructor allows for creating an [Authorizer] with custom configurations.
///
/// By default, this instance will parse bearer tokens from the authorization header, e.g.:
///
@ -43,7 +53,11 @@ class Authorizer extends Controller {
/// Creates an instance of [Authorizer] with Basic Authentication parsing.
///
/// Parses a username and password from the request's Basic Authentication data in the Authorization header, e.g.:
/// This constructor initializes an [Authorizer] that uses Basic Authentication.
/// It sets up the [Authorizer] to parse the Authorization header of incoming requests
/// using the [AuthorizationBasicParser].
///
/// The Authorization header for Basic Authentication should be in the format:
///
/// Authorization: Basic base64(username:password)
Authorizer.basic(AuthValidator? validator)
@ -51,7 +65,9 @@ class Authorizer extends Controller {
/// Creates an instance of [Authorizer] with Bearer token parsing.
///
/// Parses a bearer token from the request's Authorization header, e.g.
/// This constructor initializes an [Authorizer] that uses Bearer token authentication.
/// It sets up the [Authorizer] to parse the Authorization header of incoming requests
/// using the [AuthorizationBearerParser].
///
/// Authorization: Bearer ap9ijlarlkz8jIOa9laweo
///
@ -65,12 +81,19 @@ class Authorizer extends Controller {
/// The validating authorization object.
///
/// This object will check credentials parsed from the Authorization header and produce an
/// [Authorization] object representing the authorization the credentials have. It may also
/// reject a request. This is typically an instance of [AuthServer].
/// This property holds an instance of [AuthValidator] responsible for validating
/// the credentials parsed from the Authorization header. It processes these
/// credentials and produces an [Authorization] object that represents the
/// authorization level of the provided credentials.
///
/// The validator can also reject a request if the credentials are invalid or
/// insufficient. This property is typically set to an instance of [AuthServer].
///
/// The validator is crucial for determining whether a request should be allowed
/// to proceed based on the provided authorization information.
final AuthValidator? validator;
/// The list of required scopes.
/// The list of required scopes for authorization.
///
/// If [validator] grants scope-limited authorizations (e.g., OAuth2 bearer tokens), the authorization
/// provided by the request's header must have access to all [scopes] in order to move on to the next controller.
@ -79,7 +102,7 @@ class Authorizer extends Controller {
/// an [AuthScope] and added to this list.
final List<AuthScope>? scopes;
/// Parses the Authorization header.
/// Parses the Authorization header of incoming requests.
///
/// The parser determines how to interpret the data in the Authorization header. Concrete subclasses
/// are [AuthorizationBasicParser] and [AuthorizationBearerParser].
@ -87,6 +110,20 @@ class Authorizer extends Controller {
/// Once parsed, the parsed value is validated by [validator].
final AuthorizationParser parser;
/// Handles the incoming request by validating its authorization.
///
/// This method performs the following steps:
/// 1. Extracts the Authorization header from the request.
/// 2. If the header is missing, returns an unauthorized response.
/// 3. Attempts to parse the authorization data using the configured parser.
/// 4. Validates the parsed data using the configured validator.
/// 5. If validation succeeds, adds the authorization to the request and proceeds.
/// 6. If validation fails due to insufficient scope, returns a forbidden response.
/// 7. For other validation failures, returns an unauthorized response.
/// 8. Handles parsing exceptions by returning appropriate error responses.
///
/// @param request The incoming HTTP request to be authorized.
/// @return A [Future] that resolves to either the authorized [Request] or an error [Response].
@override
FutureOr<RequestOrResponse> handle(Request request) async {
final authData = request.raw.headers.value(HttpHeaders.authorizationHeader);
@ -121,6 +158,19 @@ class Authorizer extends Controller {
return request;
}
/// Generates an appropriate HTTP response based on the type of AuthorizationParserException.
///
/// This method takes an [AuthorizationParserException] as input and returns
/// a [Response] object based on the exception's reason:
///
/// - For [AuthorizationParserExceptionReason.malformed], it returns a 400 Bad Request
/// response with a body indicating an invalid authorization header.
/// - For [AuthorizationParserExceptionReason.missing], it returns a 401 Unauthorized
/// response.
/// - For any other reason, it returns a 500 Server Error response.
///
/// @param e The AuthorizationParserException that occurred during parsing.
/// @return A Response object appropriate to the exception reason.
Response _responseFromParseException(AuthorizationParserException e) {
switch (e.reason) {
case AuthorizationParserExceptionReason.malformed:
@ -134,6 +184,21 @@ class Authorizer extends Controller {
}
}
/// Adds a response modifier to the request to handle scope requirements.
///
/// This method is called after successful authorization and adds a response
/// modifier to the request. The modifier's purpose is to enhance 403 (Forbidden)
/// responses that are due to insufficient scope.
///
/// If this [Authorizer] has required scopes and the response is a 403 with a body
/// containing a "scope" key, this modifier will add any of this [Authorizer]'s
/// required scopes that aren't already present in the response body's scope list.
///
/// This ensures that if a downstream controller returns a 403 due to insufficient
/// scope, the response includes all the scopes required by both this [Authorizer]
/// and the downstream controller.
///
/// @param request The [Request] object to which the modifier will be added.
void _addScopeRequirementModifier(Request request) {
// If a controller returns a 403 because of invalid scope,
// this Authorizer adds its required scope as well.
@ -154,10 +219,49 @@ class Authorizer extends Controller {
}
}
/// Documents the components for the API documentation.
///
/// This method is responsible for registering custom API responses that are specific
/// to authorization-related errors. It adds three responses to the API documentation:
///
/// 1. "InsufficientScope": Used when the provided credentials or bearer token have
/// insufficient permissions to access a route.
///
/// 2. "InsufficientAccess": Used when the provided credentials or bearer token are
/// not authorized for a specific request.
///
/// 3. "MalformedAuthorizationHeader": Used when the provided Authorization header
/// is malformed.
///
/// Each response is registered with a description and a schema defining the
/// structure of the JSON response body.
///
/// @param context The APIDocumentContext used to register the responses.
@override
void documentComponents(APIDocumentContext context) {
/// Calls the superclass's documentComponents method.
///
/// This method invokes the documentComponents method of the superclass,
/// ensuring that any component documentation defined in the parent class
/// is properly registered in the API documentation context.
///
/// @param context The APIDocumentContext used for registering API components.
super.documentComponents(context);
/// Registers an "InsufficientScope" response in the API documentation.
///
/// This response is used when the provided credentials or bearer token
/// have insufficient permissions to access a specific route. It includes
/// details about the error and the required scope for the operation.
///
/// The response has the following structure:
/// - A description explaining the insufficient scope error.
/// - Content of type "application/json" with a schema containing:
/// - An "error" field of type string.
/// - A "scope" field of type string, describing the required scope.
///
/// This response can be referenced in API operations to standardize
/// the documentation of insufficient scope errors.
context.responses.register(
"InsufficientScope",
APIResponse(
@ -174,6 +278,19 @@ class Authorizer extends Controller {
),
);
/// Registers an "InsufficientAccess" response in the API documentation.
///
/// This response is used when the provided credentials or bearer token
/// are not authorized for a specific request. It includes details about
/// the error in a JSON format.
///
/// The response has the following structure:
/// - A description explaining the insufficient access error.
/// - Content of type "application/json" with a schema containing:
/// - An "error" field of type string.
///
/// This response can be referenced in API operations to standardize
/// the documentation of insufficient access errors.
context.responses.register(
"InsufficientAccess",
APIResponse(
@ -188,6 +305,18 @@ class Authorizer extends Controller {
),
);
/// Registers a "MalformedAuthorizationHeader" response in the API documentation.
///
/// This response is used when the provided Authorization header is malformed.
/// It includes details about the error in a JSON format.
///
/// The response has the following structure:
/// - A description explaining the malformed authorization header error.
/// - Content of type "application/json" with a schema containing:
/// - An "error" field of type string.
///
/// This response can be referenced in API operations to standardize
/// the documentation of malformed authorization header errors.
context.responses.register(
"MalformedAuthorizationHeader",
APIResponse(
@ -203,6 +332,23 @@ class Authorizer extends Controller {
);
}
/// Documents the operations for the API documentation.
///
/// This method is responsible for adding security-related responses and requirements
/// to each operation in the API documentation. It performs the following tasks:
///
/// 1. Calls the superclass's documentOperations method to get the base operations.
/// 2. For each operation:
/// - Adds a 400 response for malformed authorization headers.
/// - Adds a 401 response for insufficient access.
/// - Adds a 403 response for insufficient scope.
/// - Retrieves security requirements from the validator.
/// - Adds these security requirements to the operation.
///
/// @param context The APIDocumentContext used for documenting the API.
/// @param route The route string for which operations are being documented.
/// @param path The APIPath object representing the path of the operations.
/// @return A map of operation names to APIOperation objects with added security documentation.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,

View file

@ -1,11 +1,58 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_auth/auth.dart';
/// An exception thrown by [AuthServer].
/// An exception class for handling authentication server errors.
///
/// This class implements the [Exception] interface and is used to represent
/// various errors that can occur during the authentication process.
///
/// The [AuthServerException] contains:
/// - [reason]: An [AuthRequestError] enum value representing the specific error.
/// - [client]: An optional [AuthClient] associated with the error.
///
/// It also provides utility methods:
/// - [errorString]: A static method that converts [AuthRequestError] enum values to standardized error strings.
/// - [reasonString]: A getter that returns the error string for the current [reason].
///
/// The [toString] method is overridden to provide a custom string representation of the exception.
class AuthServerException implements Exception {
/// Creates an [AuthServerException] with the specified [reason] and optional [client].
///
/// The [reason] parameter is an [AuthRequestError] enum value representing the specific error.
/// The [client] parameter is an optional [AuthClient] associated with the error.
///
/// Example:
/// ```dart
/// var exception = AuthServerException(AuthRequestError.invalidRequest, null);
/// ```
AuthServerException(this.reason, this.client);
/// Returns a string suitable to be included in a query string or JSON response body
/// to indicate the error during processing an OAuth 2.0 request.
/// Converts an [AuthRequestError] enum value to its corresponding string representation.
///
/// This static method takes an [AuthRequestError] as input and returns a standardized
/// string that represents the error. These strings are suitable for inclusion in
/// query strings or JSON response bodies when indicating errors during the processing
/// of OAuth 2.0 requests.
///
/// The returned strings conform to the error codes defined in the OAuth 2.0 specification,
/// with the exception of 'invalid_token', which is a custom addition.
///
/// Example:
/// ```dart
/// var errorString = AuthServerException.errorString(AuthRequestError.invalidRequest);
/// print(errorString); // Outputs: "invalid_request"
/// ```
///
/// @param error The [AuthRequestError] enum value to convert.
/// @return A string representation of the error.
static String errorString(AuthRequestError error) {
switch (error) {
case AuthRequestError.invalidRequest:
@ -36,25 +83,54 @@ class AuthServerException implements Exception {
}
}
/// The specific reason for the authentication error.
///
/// This property holds an [AuthRequestError] enum value that represents
/// the specific error that occurred during the authentication process.
/// It provides detailed information about why the authentication request failed.
AuthRequestError reason;
/// The optional [AuthClient] associated with this exception.
///
/// This property may contain an [AuthClient] instance that is related to the
/// authentication error. It can be null if no specific client is associated
/// with the error or if the error occurred before client authentication.
///
/// This information can be useful for debugging or logging purposes, providing
/// context about which client encountered the authentication error.
AuthClient? client;
/// Returns a string representation of the [reason] for this exception.
///
/// This getter utilizes the static [errorString] method to convert the
/// [AuthRequestError] enum value stored in [reason] to its corresponding
/// string representation.
///
/// @return A standardized string representation of the error reason.
String get reasonString {
return errorString(reason);
}
/// Returns a string representation of the [AuthServerException].
///
/// This method overrides the default [Object.toString] method to provide
/// a custom string representation of the exception. The returned string
/// includes the exception class name, the [reason] for the exception,
/// and the associated [client] (if any).
///
/// @return A string in the format "AuthServerException: [reason] [client]".
@override
String toString() {
return "AuthServerException: $reason $client";
}
}
/// The possible errors as defined by the OAuth 2.0 specification.
/// Enum representing possible errors as defined by the OAuth 2.0 specification.
///
/// Auth endpoints will use this list of values to determine the response sent back
/// to a client upon a failed request.
enum AuthRequestError {
/// The request was invalid...
/// Represents an invalid request error.
///
/// The request is missing a required parameter, includes an
/// unsupported parameter value (other than grant type),
@ -63,7 +139,7 @@ enum AuthRequestError {
/// client, or is otherwise malformed.
invalidRequest,
/// The client was invalid...
/// Represents an invalid client error.
///
/// Client authentication failed (e.g., unknown client, no
/// client authentication included, or unsupported
@ -77,7 +153,7 @@ enum AuthRequestError {
/// matching the authentication scheme used by the client.
invalidClient,
/// The grant was invalid...
/// Represents an invalid grant error.
///
/// The provided authorization grant (e.g., authorization
/// code, resource owner credentials) or refresh token is
@ -86,36 +162,81 @@ enum AuthRequestError {
/// another client.
invalidGrant,
/// The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner.
/// Represents an invalid scope error.
///
/// This error occurs when the requested scope is invalid, unknown, malformed,
/// or exceeds the scope granted by the resource owner. It typically indicates
/// that the client has requested access to resources or permissions that are
/// either not recognized by the authorization server or not authorized for
/// the particular client or user.
///
/// In the OAuth 2.0 flow, this error might be returned if a client requests
/// access to a scope that doesn't exist or that the user hasn't granted
/// permission for.
invalidScope,
/// The authorization grant type is not supported by the authorization server.
/// Represents an unsupported grant type error.
///
/// This error occurs when the authorization server does not support the
/// grant type requested by the client. It typically indicates that the
/// client has specified a grant type that is either not recognized or
/// not implemented by the authorization server.
///
/// In the OAuth 2.0 flow, this error might be returned if, for example,
/// a client requests a grant type like "password" when the server only
/// supports "authorization_code" and "refresh_token" grant types.
unsupportedGrantType,
/// The authorization server does not support obtaining an authorization code using this method.
/// Represents an unsupported response type error.
///
/// This error occurs when the authorization server does not support obtaining
/// an authorization code using the specified response type. It typically
/// indicates that the client has requested a response type that is not
/// recognized or not implemented by the authorization server.
unsupportedResponseType,
/// The authenticated client is not authorized to use this authorization grant type.
/// Represents an unauthorized client error.
///
/// This error occurs when the client is not authorized to request an
/// authorization code using this method. It typically indicates that
/// the client does not have the necessary permissions or credentials
/// to perform the requested action, even though it may be properly
/// authenticated.
unauthorizedClient,
/// The resource owner or authorization server denied the request.
/// Represents an access denied error.
///
/// This error occurs when the resource owner or authorization server denies the request.
/// It is typically used when the authenticated user does not have sufficient permissions
/// to perform the requested action, or when the user explicitly denies authorization
/// during the OAuth flow.
accessDenied,
/// The server encountered an error during processing the request.
/// Represents a server error.
///
/// This error occurs when the authorization server encounters an unexpected
/// condition that prevented it from fulfilling the request. This is typically
/// used for internal server errors or other unexpected issues that prevent
/// the server from properly processing the authentication request.
serverError,
/// The server is temporarily unable to fulfill the request.
/// Represents a temporarily unavailable error.
///
/// This error occurs when the authorization server is temporarily unable to handle
/// the request due to a temporary overloading or maintenance of the server.
/// The client may repeat the request at a later time. The server SHOULD include
/// a Retry-After HTTP header field in the response indicating how long the client
/// should wait before retrying the request.
temporarilyUnavailable,
/// Indicates that the token is invalid.
/// Represents an invalid token error.
///
/// This particular error reason is not part of the OAuth 2.0 spec.
/// This error occurs when the provided token is invalid, expired, or otherwise
/// not acceptable for the requested operation. It is typically used when a client
/// presents an access token that cannot be validated or is no longer valid.
///
/// Note: This particular error reason is not part of the standard OAuth 2.0
/// specification. It is a custom addition to handle scenarios specific to
/// token validation that are not covered by other standard error types.
invalidToken
}

View file

@ -1,20 +1,32 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart';
/// Represents an OAuth 2.0 client ID and secret pair.
///
/// See the conduit/managed_auth library for a concrete implementation of this type.
/// This class encapsulates the information necessary for OAuth 2.0 client authentication.
/// It can represent both public and confidential clients, with support for the authorization code grant flow.
///
/// Use the command line tool `conduit auth` to create instances of this type and store them to a database.
class AuthClient {
/// Creates an instance of [AuthClient].
///
/// [id] must not be null. [hashedSecret] and [salt] must either both be null or both be valid values. If [hashedSecret] and [salt]
/// are valid values, this client is a confidential client. Otherwise, the client is public. The terms 'confidential' and 'public'
/// are described by the OAuth 2.0 specification.
/// This constructor creates an [AuthClient] with the given parameters.
///
/// If this client supports scopes, [allowedScopes] must contain a list of scopes that tokens may request when authorized
/// by this client.
///
/// NOTE: [id] must not be null. [hashedSecret] and [salt] must either both be null or both be valid values. If [hashedSecret] and [salt]
/// are valid values, this client is a confidential client. Otherwise, the client is public. The terms 'confidential' and 'public'
/// are described by the OAuth 2.0 specification.
AuthClient(
String id,
String? hashedSecret,
@ -29,6 +41,16 @@ class AuthClient {
);
/// Creates an instance of a public [AuthClient].
///
/// This constructor creates a public [AuthClient] with the given [id].
/// Public clients do not have a client secret.
///
/// - [id]: The unique identifier for the client.
/// - [allowedScopes]: Optional list of scopes that this client is allowed to request.
/// - [redirectURI]: Optional URI to redirect to after authorization.
///
/// This is equivalent to calling [AuthClient.withRedirectURI] with null values for
/// hashedSecret and salt.
AuthClient.public(String id,
{List<AuthScope>? allowedScopes, String? redirectURI})
: this.withRedirectURI(
@ -41,7 +63,17 @@ class AuthClient {
/// Creates an instance of [AuthClient] that uses the authorization code grant flow.
///
/// All values must be non-null. This is confidential client.
/// This constructor creates a confidential [AuthClient] with the given parameters.
///
/// - [id]: The unique identifier for the client.
/// - [hashedSecret]: The hashed secret of the client.
/// - [salt]: The salt used to hash the client secret.
/// - [redirectURI]: The URI to redirect to after authorization.
/// - [allowedScopes]: Optional list of scopes that this client is allowed to request.
///
/// This constructor is specifically for clients that use the authorization code grant flow,
/// which requires a redirect URI. All parameters except [allowedScopes] must be non-null.
/// The presence of [hashedSecret] and [salt] indicates that this is a confidential client.
AuthClient.withRedirectURI(
this.id,
this.hashedSecret,
@ -52,28 +84,57 @@ class AuthClient {
this.allowedScopes = allowedScopes;
}
/// The list of allowed scopes for this client.
///
/// This private variable stores the allowed scopes for the AuthClient.
/// It is used internally to manage and validate the scopes that this client
/// is authorized to request during the authentication process.
List<AuthScope>? _allowedScopes;
/// The ID of the client.
/// The unique identifier for this OAuth 2.0 client.
///
/// This is a required field for all OAuth 2.0 clients and is used to identify
/// the client during the authentication and authorization process. It should
/// be a string value that is unique among all clients registered with the
/// authorization server.
final String id;
/// The hashed secret of the client.
///
/// This value may be null if the client is public. See [isPublic].
/// This property stores the hashed version of the client's secret, which is used for authentication
/// in confidential clients. The secret is hashed for security reasons, to avoid storing the raw secret.
///
/// This value may be null if the client is public. A null value indicates that this is a public client,
/// which doesn't use a client secret for authentication. See [isPublic] for more information on
/// determining if a client is public or confidential.
///
/// The hashed secret is typically used in conjunction with the [salt] property to verify
/// the client's credentials during the authentication process.
String? hashedSecret;
/// The salt [hashedSecret] was hashed with.
/// The salt used to hash the client secret.
///
/// This value may be null if the client is public. See [isPublic].
String? salt;
/// The redirection URI for authorization codes and/or tokens.
///
/// This property stores the URI where the authorization server should redirect
/// the user after they grant or deny permission to the client. It is used in
/// the authorization code grant flow of OAuth 2.0.
///
/// In the context of OAuth 2.0:
/// - For authorization code grant, this URI is where the authorization code is sent.
/// - For implicit grant, this URI is where the access token is sent.
///
/// This value may be null if the client doesn't support the authorization code flow.
String? redirectURI;
/// The list of scopes available when authorizing with this client.
///
/// This getter returns the list of allowed scopes for the client. The setter
/// filters the provided list to remove any redundant scopes.
///
/// Scoping is determined by this instance; i.e. the authorizing client determines which scopes a token
/// has. This list contains all valid scopes for this client. If null, client does not support scopes
/// and all access tokens have same authorization.
@ -87,32 +148,54 @@ class AuthClient {
}).toList();
}
/// Whether or not this instance allows scoping or not.
/// Determines if this instance supports authorization scopes.
///
/// In application's that do not use authorization scopes, this will return false.
/// Otherwise, will return true.
bool get supportsScopes => allowedScopes != null;
/// Whether or not this client can issue tokens for the provided [scope].
/// Determines if this client can issue tokens for the provided [scope].
///
/// This method checks if the given [scope] is allowed for this client by comparing it
/// against the client's [allowedScopes]. It returns true if the provided [scope] is
/// a subset of or equal to any of the scopes in [allowedScopes].
///
/// If [allowedScopes] is null or empty, this method returns false, indicating that
/// no scopes are allowed for this client.
///
/// [scope]: The AuthScope to check against this client's allowed scopes.
///
/// Returns true if the scope is allowed, false otherwise.
bool allowsScope(AuthScope scope) {
return allowedScopes
?.any((clientScope) => scope.isSubsetOrEqualTo(clientScope)) ??
false;
}
/// Whether or not this is a public or confidential client.
/// Whether or not this is a public client.
///
/// Public clients do not have a client secret and are used for clients that can't store
/// their secret confidentially, i.e. JavaScript browser applications.
bool get isPublic => hashedSecret == null;
/// Whether or not this is a public or confidential client.
/// Determines whether this client is confidential or public.
///
/// Confidential clients have a client secret that must be used when authenticating with
/// a client-authenticated request. Confidential clients are used when you can
/// be sure that the client secret cannot be viewed by anyone outside of the developer.
bool get isConfidential => hashedSecret != null;
/// Returns a string representation of the AuthClient instance.
///
/// This method provides a human-readable description of the AuthClient, including:
/// - Whether the client is public or confidential
/// - The client's ID
/// - The client's redirect URI (if set)
///
/// The format of the returned string is:
/// "AuthClient (public/confidential): [client_id] [redirect_uri]"
///
/// @return A string representation of the AuthClient.
@override
String toString() {
return "AuthClient (${isPublic ? "public" : "confidental"}): $id $redirectURI";
@ -121,24 +204,67 @@ class AuthClient {
/// Represents an OAuth 2.0 token.
///
/// [AuthServerDelegate] and [AuthServer] will exchange OAuth 2.0
/// tokens through instances of this type.
/// This class encapsulates the properties and functionality of an OAuth 2.0 token,
/// including access token, refresh token, expiration details, and associated scopes.
/// It is used by [AuthServerDelegate] and [AuthServer] to exchange OAuth 2.0 tokens.
///
/// See the `package:conduit_core/managed_auth` library for a concrete implementation of this type.
class AuthToken {
/// The value to be passed as a Bearer Authorization header.
/// The access token string for OAuth 2.0 authentication.
///
/// This token is used in the Authorization header of HTTP requests to authenticate
/// the client. It should be included in the header as "Bearer <accessToken>".
///
/// The access token is typically a short-lived credential that grants access to
/// protected resources on behalf of the resource owner (user).
///
/// This value may be null if the token has not been issued or has been invalidated.
String? accessToken;
/// The value to be passed for refreshing a token.
/// The refresh token associated with this OAuth 2.0 token.
///
/// A refresh token is a credential that can be used to obtain a new access token
/// when the current access token becomes invalid or expires. This allows the client
/// to obtain continued access to protected resources without requiring the resource
/// owner to re-authorize the application.
///
/// This value may be null if the authorization server does not issue refresh tokens
/// or if the token has not been issued with a refresh token.
String? refreshToken;
/// The time this token was issued on.
///
/// This property represents the date and time when the OAuth 2.0 token was originally issued.
/// It can be used to calculate the age of the token or to implement token refresh policies.
/// The value is stored as a [DateTime] object, which allows for easy manipulation and comparison.
///
/// This value may be null if the issue date is not tracked or has not been set.
DateTime? issueDate;
/// The time when this token expires.
/// The expiration date and time of this token.
///
/// This property represents the point in time when the OAuth 2.0 token will become invalid.
/// After this date and time, the token should no longer be accepted for authentication.
///
/// The value is stored as a [DateTime] object, which allows for easy comparison with the current time
/// to determine if the token has expired. This is typically used in conjunction with [issueDate]
/// to calculate the token's lifespan and manage token refresh cycles.
///
/// This value may be null if the token does not have an expiration date or if it has not been set.
DateTime? expirationDate;
/// The type of token, currently only 'bearer' is valid.
/// The type of token used for authentication.
///
/// This property specifies the type of token being used. In the OAuth 2.0 framework,
/// the most common token type is 'bearer'. The token type is typically used in the
/// HTTP Authorization header to indicate how the access token should be used.
///
/// Currently, only 'bearer' is considered valid for this implementation.
///
/// Example usage in an HTTP header:
/// Authorization: Bearer <access_token>
///
/// This value may be null if the token type has not been set or is unknown.
String? type;
/// The identifier of the resource owner.
@ -146,15 +272,60 @@ class AuthToken {
/// Tokens are owned by a resource owner, typically a User, Profile or Account
/// in an application. This value is the primary key or identifying value of those
/// instances.
///
/// This property represents the unique identifier of the resource owner associated
/// with the OAuth 2.0 token. It is typically used to link the token to a specific
/// user or account in the system.
///
/// The value is stored as an integer, which could be:
/// - A database primary key
/// - A unique user ID
/// - Any other numeric identifier that uniquely identifies the resource owner
///
/// This property may be null if the token is not associated with a specific
/// resource owner or if the association has not been established.
int? resourceOwnerIdentifier;
/// The client ID this token was issued from.
/// The client ID associated with this token.
///
/// This property represents the unique identifier of the OAuth 2.0 client
/// that was used to obtain this token. It is used to link the token back
/// to the client application that requested it.
///
/// The client ID is typically assigned by the authorization server when
/// the client application is registered, and it's used to identify the
/// client during the authentication and token issuance process.
late String clientID;
/// Scopes this token has access to.
/// The list of authorization scopes associated with this token.
///
/// This property represents the set of permissions or access rights granted to this token.
/// Each [AuthScope] in the list defines a specific area of access or functionality
/// that the token holder is allowed to use.
///
/// The scopes determine what actions or resources the token can access within the system.
/// If this list is null, it typically means the token has no specific scope restrictions
/// and may have full access (depending on the system's implementation).
///
/// This property is crucial for implementing fine-grained access control in OAuth 2.0
/// systems, allowing for precise definition of what each token is allowed to do.
List<AuthScope>? scopes;
/// Whether or not this token is expired by evaluated [expirationDate].
/// Determines whether this token has expired.
///
/// This getter compares the token's [expirationDate] with the current UTC time
/// to determine if the token has expired. It returns true if the token has
/// expired, and false if it is still valid.
///
/// The comparison is done by calculating the difference in seconds between
/// the expiration date and the current time. If this difference is less than
/// or equal to zero, the token is considered expired.
///
/// Returns:
/// [bool]: true if the token has expired, false otherwise.
///
/// Note: This getter assumes that [expirationDate] is not null. If it is null,
/// this will result in a null pointer exception.
bool get isExpired {
return expirationDate!.difference(DateTime.now().toUtc()).inSeconds <= 0;
}
@ -182,37 +353,118 @@ class AuthToken {
/// Represents an OAuth 2.0 authorization code.
///
/// [AuthServerDelegate] and [AuthServer] will exchange OAuth 2.0
/// authorization codes through instances of this type.
/// This class encapsulates the properties and functionality of an OAuth 2.0 authorization code,
/// which is used in the authorization code grant flow. It contains information such as the code itself,
/// associated client and resource owner details, issue and expiration dates, and requested scopes.
///
/// See the conduit/managed_auth library for a concrete implementation of this type.
class AuthCode {
/// The actual one-time code used to exchange for tokens.
///
/// This property represents the authorization code in the OAuth 2.0 authorization code flow.
/// It is a short-lived, single-use code that is issued by the authorization server and can be
/// exchanged for an access token and, optionally, a refresh token.
///
/// The code is typically valid for a short period (usually a few minutes) and can only be
/// used once. After it has been exchanged for tokens, it becomes invalid.
///
/// This value may be null if the code has not been generated yet or has been invalidated.
String? code;
/// The client ID the authorization code was issued under.
/// The client ID associated with this authorization code.
///
/// This property represents the unique identifier of the OAuth 2.0 client
/// that requested the authorization code. It is used to link the authorization
/// code back to the client application that initiated the OAuth flow.
///
/// The client ID is typically assigned by the authorization server when
/// the client application is registered, and it's used to identify the
/// client during the authorization code exchange process.
///
/// This property is marked as 'late', indicating that it must be initialized
/// before it's accessed, but not necessarily in the constructor.
late String clientID;
/// The identifier of the resource owner.
/// The identifier of the resource owner associated with this authorization code.
///
/// Authorization codes are owned by a resource owner, typically a User, Profile or Account
/// in an application. This value is the primary key or identifying value of those
/// instances.
int? resourceOwnerIdentifier;
/// The timestamp this authorization code was issued on.
/// The timestamp when this authorization code was issued.
///
/// This property represents the date and time when the OAuth 2.0 authorization code
/// was originally created and issued by the authorization server. It can be used to:
/// - Calculate the age of the authorization code
/// - Implement expiration policies
/// - Audit the authorization process
///
/// The value is stored as a [DateTime] object, which allows for easy manipulation
/// and comparison with other dates and times.
///
/// This value may be null if the issue date is not tracked or has not been set.
DateTime? issueDate;
/// When this authorization code expires, recommended for 10 minutes after issue date.
/// The expiration date and time of this authorization code.
///
/// This property represents the point in time when the OAuth 2.0 authorization code
/// will become invalid. After this date and time, the code should no longer be
/// accepted for token exchange.
///
/// It is recommended to set this value to 10 minutes after the [issueDate] to
/// limit the window of opportunity for potential attacks using intercepted
/// authorization codes.
///
/// The value is stored as a [DateTime] object, which allows for easy comparison
/// with the current time to determine if the code has expired. This is typically
/// used in conjunction with [issueDate] to enforce the short-lived nature of
/// authorization codes.
///
/// This value may be null if the authorization code does not have an expiration
/// date or if it has not been set.
DateTime? expirationDate;
/// Whether or not this authorization code has already been exchanged for a token.
/// Indicates whether this authorization code has already been exchanged for a token.
///
/// In the OAuth 2.0 authorization code flow, an authorization code should only be used once
/// to obtain an access token. This property helps track whether the code has been exchanged.
///
/// - If `true`, the code has already been used to obtain a token and should not be accepted again.
/// - If `false` or `null`, the code has not yet been exchanged and may still be valid for token issuance.
///
/// This property is crucial for preventing authorization code replay attacks, where an attacker
/// might attempt to use a single authorization code multiple times.
bool? hasBeenExchanged;
/// Scopes the exchanged token will have.
/// The list of scopes requested for the token to be exchanged.
///
/// This property represents the set of permissions or access rights that are being
/// requested for the OAuth 2.0 token during the authorization code exchange process.
/// Each [AuthScope] in the list defines a specific area of access or functionality
/// that the token is requesting to use.
///
/// If this list is null, it typically means no specific scopes are being requested,
/// and the token may receive default scopes or full access (depending on the system's
/// implementation and configuration).
///
/// The actual scopes granted to the token may be a subset of these requested scopes,
/// based on the authorization server's policies and the resource owner's consent.
List<AuthScope>? requestedScopes;
/// Whether or not this code has expired yet, according to its [expirationDate].
/// Determines whether this authorization code has expired.
///
/// This getter compares the [expirationDate] of the authorization code with the current UTC time
/// to determine if the code has expired. It returns true if the code has expired, and false if it is still valid.
///
/// The comparison is done by calculating the difference in seconds between the expiration date and the current time.
/// If this difference is less than or equal to zero, the code is considered expired.
///
/// Returns:
/// [bool]: true if the authorization code has expired, false otherwise.
///
/// Note: This getter assumes that [expirationDate] is not null. If it is null,
/// this will result in a null pointer exception.
bool get isExpired {
return expirationDate!.difference(DateTime.now().toUtc()).inSeconds <= 0;
}
@ -220,12 +472,34 @@ class AuthCode {
/// Authorization information for a [Request] after it has passed through an [Authorizer].
///
/// This class encapsulates various pieces of authorization information, including:
/// - The client ID under which the permission was granted
/// - The identifier for the resource owner (if applicable)
/// - The [AuthValidator] that granted the permission
/// - Basic authorization credentials (if provided)
/// - A list of scopes that this authorization has access to
///
/// It also provides a method to check if the authorization has access to a specific scope.
///
/// This class is typically used in conjunction with [Authorizer] and [AuthValidator]
/// to manage and verify authorization in a request-response cycle.
/// After a request has passed through an [Authorizer], an instance of this type
/// is created and attached to the request (see [Request.authorization]). Instances of this type contain the information
/// that the [Authorizer] obtained from an [AuthValidator] (typically an [AuthServer])
/// about the validity of the credentials in a request.
class Authorization {
/// Creates an instance of a [Authorization].
/// Creates an instance of [Authorization].
///
/// This constructor initializes an [Authorization] object with the provided parameters:
///
/// - [clientID]: The client ID under which the permission was granted.
/// - [ownerID]: The identifier for the owner of the resource, if provided. Can be null.
/// - [validator]: The [AuthValidator] that granted this permission.
/// - [credentials]: Optional. Basic authorization credentials, if provided.
/// - [scopes]: Optional. The list of scopes this authorization has access to.
///
/// This class is typically used to represent the authorization information for a [Request]
/// after it has passed through an [Authorizer].
Authorization(
this.clientID,
this.ownerID,
@ -234,34 +508,71 @@ class Authorization {
this.scopes,
});
/// The client ID the permission was granted under.
/// The client ID associated with this authorization.
///
/// This property represents the unique identifier of the OAuth 2.0 client
/// that was granted permission. It is used to link the authorization
/// back to the specific client application that requested it.
///
/// The client ID is typically assigned by the authorization server when
/// the client application is registered, and it's used to identify the
/// client throughout the OAuth 2.0 flow.
final String clientID;
/// The identifier for the owner of the resource, if provided.
///
/// If this instance refers to the authorization of a resource owner, this value will
/// be its identifying value. For example, in an application where a 'User' is stored in a database,
/// this value would be the primary key of that user.
/// This property represents the unique identifier of the resource owner associated
/// with this authorization. In OAuth 2.0 terminology, the resource owner is typically
/// the end-user who grants permission to an application to access their data.
///
/// If this authorization does not refer to a specific resource owner, this value will be null.
final int? ownerID;
/// The [AuthValidator] that granted this permission.
///
/// This property represents the [AuthValidator] instance that was responsible
/// for validating and granting the authorization. It can be used to trace
/// the origin of the authorization or to perform additional validation
/// if needed.
///
/// The validator might be null in cases where the authorization was not
/// granted through a standard validation process or if the information
/// about the validator is not relevant or available.
final AuthValidator? validator;
/// Basic authorization credentials, if provided.
///
/// If this instance represents the authorization header of a request with basic authorization credentials,
/// the parsed credentials will be available in this property. Otherwise, this value is null.
/// This property holds the parsed basic authorization credentials if they were
/// present in the authorization header of the request. If the request did not
/// use basic authorization, or if the credentials were not successfully parsed,
/// this property will be null.
///
/// The [AuthBasicCredentials] object typically contains a username and password
/// pair extracted from the 'Authorization' header of an HTTP request using the
/// Basic authentication scheme.
///
/// This can be useful for endpoints that support both OAuth 2.0 token-based
/// authentication and traditional username/password authentication via Basic Auth.
final AuthBasicCredentials? credentials;
/// The list of scopes this authorization has access to.
///
/// If the access token used to create this instance has scope,
/// those scopes will be available here. Otherwise, null.
/// This property represents the set of permissions or access rights granted to this authorization.
/// Each [AuthScope] in the list defines a specific area of access or functionality
/// that the authorization is allowed to use.
///
/// If the access token used to create this instance has scopes associated with it,
/// those scopes will be available in this list. If no scopes were associated with
/// the access token, or if scopes are not being used in the system, this property will be null.
///
/// Scopes are crucial for implementing fine-grained access control in OAuth 2.0 systems,
/// allowing for precise definition of what each authorization is allowed to do.
///
/// This list can be used in conjunction with the [isAuthorizedForScope] method to check
/// if the authorization has access to a specific scope.
List<AuthScope>? scopes;
/// Whether or not this instance has access to a specific scope.
/// Determines if this authorization has access to a specific scope.
///
/// This method checks each element in [scopes] for any that gives privileges
/// to access [scope].
@ -271,7 +582,7 @@ class Authorization {
}
}
/// Instances represent OAuth 2.0 scope.
/// Represents and manages OAuth 2.0 scopes.
///
/// An OAuth 2.0 token may optionally have authorization scopes. An authorization scope provides more granular
/// authorization to protected resources. Without authorization scopes, any valid token can pass through an
@ -282,7 +593,7 @@ class Authorization {
/// any of the scopes the client provides. Scopes are then granted to the access token. An [Authorizer] may specify
/// a one or more required scopes that a token must have to pass to the next controller.
class AuthScope {
/// Creates an instance of this type from [scopeString].
/// Creates an instance of [AuthScope] from a [scopeString].
///
/// A simple authorization scope string is a single keyword. Valid characters are
///
@ -320,6 +631,27 @@ class AuthScope {
return scope;
}
/// Parses and creates an [AuthScope] instance from a given scope string.
///
/// This factory method performs several validation checks on the input [scopeString]:
/// 1. Ensures the string is not empty.
/// 2. Validates that each character in the string is within the allowed set of characters.
/// 3. Parses the string into segments and extracts the modifier (if any).
///
/// The allowed characters are: A-Za-z0-9!#$%&'`()*+,./:;<=>?@[]^_{|}-
///
/// If any validation fails, a [FormatException] is thrown with a descriptive error message.
///
/// After successful validation and parsing, it creates and returns a new [AuthScope] instance.
///
/// Parameters:
/// [scopeString]: The string representation of the scope to parse.
///
/// Returns:
/// A new [AuthScope] instance representing the parsed scope.
///
/// Throws:
/// [FormatException] if the [scopeString] is empty or contains invalid characters.
factory AuthScope._parse(String scopeString) {
if (scopeString.isEmpty) {
throw FormatException(
@ -345,16 +677,28 @@ class AuthScope {
return AuthScope._(scopeString, segments, lastModifier);
}
/// Private constructor for creating an [AuthScope] instance.
///
/// This constructor is used internally by the class to create instances
/// after parsing and validating the scope string.
///
/// Parameters:
/// [_scopeString]: The original, unparsed scope string.
/// [_segments]: A list of parsed [_AuthScopeSegment] objects representing the scope's segments.
/// [_lastModifier]: The modifier of the last segment, if any.
///
/// This constructor is marked as `const` to allow for compile-time constant instances,
/// which can improve performance and memory usage in certain scenarios.
const AuthScope._(this._scopeString, this._segments, this._lastModifier);
/// Signifies 'any' scope in [AuthServerDelegate.getAllowedScopes].
/// Represents a special constant for indicating 'any' scope in [AuthServerDelegate.getAllowedScopes].
///
/// See [AuthServerDelegate.getAllowedScopes] for more details.
static const List<AuthScope> any = [
AuthScope._("_scope:_constant:_marker", [], null)
];
/// Returns true if that [providedScopes] fulfills [requiredScopes].
/// Verifies if the provided scopes fulfill the required scopes.
///
/// For all [requiredScopes], there must be a scope in [requiredScopes] that meets or exceeds
/// that scope for this method to return true. If [requiredScopes] is null, this method
@ -375,23 +719,97 @@ class AuthScope {
});
}
/// A cache to store previously created AuthScope instances.
///
/// This static map serves as a cache to store AuthScope instances that have been
/// previously created. The key is the string representation of the scope, and
/// the value is the corresponding AuthScope instance.
///
/// Caching AuthScope instances can improve performance by avoiding repeated
/// parsing and object creation for frequently used scopes. When an AuthScope
/// is requested with a scope string that already exists in this cache, the
/// cached instance is returned instead of creating a new one.
static final Map<String, AuthScope> _cache = {};
/// The original, unparsed scope string.
///
/// This private field stores the complete scope string as it was originally provided
/// when creating the AuthScope instance. It represents the full, unmodified scope
/// including all segments and modifiers.
///
/// This string is used for caching purposes and when converting the AuthScope
/// back to its string representation (e.g., in the toString() method).
final String _scopeString;
/// Individual segments, separated by `:` character, of this instance.
/// Returns an iterable of individual segments of this AuthScope instance.
///
/// Will always have a length of at least 1.
Iterable<String?> get segments => _segments.map((s) => s.name);
/// The modifier of this scope, if it exists.
/// Returns the modifier of this scope, if it exists.
///
/// If this instance does not have a modifier, returns null.
/// The modifier is an optional component of an AuthScope that provides additional
/// specification or restriction to the scope. It is typically the last part of a
/// scope string, following a dot (.) after the last segment.
///
/// For example, in the scope "user:profile.readonly", "readonly" is the modifier.
///
/// Returns:
/// A [String] representing the modifier if one exists, or null if this AuthScope
/// does not have a modifier.
///
/// This getter provides access to the private [_lastModifier] field, allowing
/// external code to check for the presence and value of a modifier without
/// directly accessing the internal state of the AuthScope.
String? get modifier => _lastModifier;
/// List of segments that make up this AuthScope.
///
/// This private field stores the parsed segments of the scope string as a list of
/// [_AuthScopeSegment] objects. Each segment represents a part of the scope,
/// separated by colons in the original scope string.
///
/// For example, for a scope string "user:profile:read", this list would contain
/// three _AuthScopeSegment objects representing "user", "profile", and "read"
/// respectively.
///
/// This list is used internally for scope comparisons and validations.
final List<_AuthScopeSegment> _segments;
/// The modifier of the last segment in this AuthScope.
///
/// This private field stores the modifier of the last segment in the AuthScope,
/// if one exists. A modifier provides additional specification or restriction
/// to a scope and is typically the part following a dot (.) in the last segment
/// of a scope string.
///
/// For example, in the scope "user:profile.readonly", "readonly" would be stored
/// in this field.
///
/// The value is null if the AuthScope does not have a modifier in its last segment.
final String? _lastModifier;
/// Parses the given scope string into a list of [_AuthScopeSegment] objects.
///
/// This method performs the following steps:
/// 1. Checks if the input string is empty and throws a [FormatException] if it is.
/// 2. Splits the string by ':' and creates [_AuthScopeSegment] objects for each segment.
/// 3. Validates each segment, ensuring:
/// - Only the last segment can have a modifier.
/// - There are no empty segments.
/// - There are no leading or trailing colons.
///
/// If any validation fails, a [FormatException] is thrown with a descriptive error message
/// and the position in the string where the error occurred.
///
/// Parameters:
/// [scopeString]: The string representation of the scope to parse.
///
/// Returns:
/// A list of [_AuthScopeSegment] objects representing the parsed segments of the scope.
///
/// Throws:
/// [FormatException] if the [scopeString] is empty or contains invalid segments.
static List<_AuthScopeSegment> _parseSegments(String scopeString) {
if (scopeString.isEmpty) {
throw FormatException(
@ -435,7 +853,7 @@ class AuthScope {
return elements;
}
/// Whether or not this instance is a subset or equal to [incomingScope].
/// Determines if this [AuthScope] is a subset of or equal to the [incomingScope].
///
/// The scope `users:posts` is a subset of `users`.
///
@ -477,16 +895,42 @@ class AuthScope {
}
/// Alias of [isSubsetOrEqualTo].
///
/// This method is deprecated and will be removed in a future version.
/// Use [isSubsetOrEqualTo] instead.
///
/// Determines if this [AuthScope] allows the [incomingScope].
/// It is equivalent to calling [isSubsetOrEqualTo] with the same argument.
///
/// [incomingScope]: The AuthScope to compare against this instance.
///
/// Returns true if this AuthScope is a subset of or equal to the [incomingScope],
/// false otherwise.
@Deprecated('Use AuthScope.isSubsetOrEqualTo() instead')
bool allowsScope(AuthScope incomingScope) => isSubsetOrEqualTo(incomingScope);
/// String variant of [isSubsetOrEqualTo].
/// Checks if this AuthScope is a subset of or equal to the given scope string.
///
/// Parses an instance of this type from [scopeString] and invokes
/// [isSubsetOrEqualTo].
bool allows(String scopeString) => isSubsetOrEqualTo(AuthScope(scopeString));
/// Whether or not two scopes are exactly the same.
/// Determines if this [AuthScope] is exactly the same as the given [scope].
///
/// This method compares each segment and modifier of both scopes to ensure they are identical.
///
/// Parameters:
/// [scope]: The [AuthScope] to compare against this instance.
///
/// Returns:
/// [bool]: true if both scopes are exactly the same, false otherwise.
///
/// The comparison is performed as follows:
/// 1. Iterates through each segment of both scopes simultaneously.
/// 2. If the given scope has fewer segments, returns false.
/// 3. Compares the name and modifier of each segment.
/// 4. If any segment's name or modifier doesn't match, returns false.
/// 5. If all segments match and both scopes have the same number of segments, returns true.
bool isExactlyScope(AuthScope scope) {
final incomingIterator = scope._segments.iterator;
for (final segment in _segments) {
@ -506,18 +950,44 @@ class AuthScope {
return true;
}
/// String variant of [isExactlyScope].
/// Checks if this AuthScope is exactly the same as the given scope string.
///
/// Parses an instance of this type from [scopeString] and invokes [isExactlyScope].
bool isExactly(String scopeString) {
return isExactlyScope(AuthScope(scopeString));
}
/// Returns a string representation of this AuthScope.
///
/// This method overrides the default [Object.toString] method to provide
/// a string representation of the AuthScope instance. It returns the
/// original, unparsed scope string that was used to create this AuthScope.
///
/// Returns:
/// A [String] representing the complete scope, including all segments
/// and modifiers, exactly as it was originally provided.
///
/// Example:
/// final scope = AuthScope('user:profile.readonly');
/// print(scope.toString()); // Outputs: 'user:profile.readonly'
@override
String toString() => _scopeString;
}
/// Represents a segment of an AuthScope.
///
/// An AuthScope can be composed of one or more segments, where each segment
/// may have a name and an optional modifier. This class parses and stores
/// the components of a single segment.
class _AuthScopeSegment {
/// Constructs an AuthScopeSegment from the given [segment] string.
///
/// The [segment] string is expected to be in the format "name.modifier" or "name".
/// If a modifier is present, it is stored in the [modifier] field. If not, [modifier]
/// remains null. The [name] field always contains the name of the segment.
///
/// Parameters:
/// [segment]: A [String] representing the segment, which may include a modifier.
_AuthScopeSegment(String segment) {
final split = segment.split(".");
if (split.length == 2) {
@ -528,9 +998,42 @@ class _AuthScopeSegment {
}
}
/// The name of the segment.
///
/// This property represents the main part of the segment before any modifier.
/// For example, in the segment "user.readonly", "user" would be the name.
///
/// This value can be null if the segment is empty or malformed.
String? name;
/// The modifier of the segment, if present.
///
/// This property represents the optional part of the segment after the name,
/// if a modifier is specified. For example, in the segment "user.readonly",
/// "readonly" would be the modifier.
///
/// If no modifier is present in the segment, this value is null.
String? modifier;
/// Returns a string representation of this AuthScopeSegment.
///
/// This method overrides the default [Object.toString] method to provide
/// a string representation of the AuthScopeSegment instance.
///
/// If the segment has a modifier, it returns the name and modifier
/// separated by a dot (e.g., "name.modifier").
/// If there's no modifier, it returns just the name.
///
/// Returns:
/// A [String] representing the complete segment, including the
/// modifier if present.
///
/// Example:
/// final segment = _AuthScopeSegment('user.readonly');
/// print(segment.toString()); // Outputs: 'user.readonly'
///
/// final segmentNoModifier = _AuthScopeSegment('user');
/// print(segmentNoModifier.toString()); // Outputs: 'user'
@override
String toString() {
if (modifier == null) {

View file

@ -1,7 +1,16 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'package:protevus_auth/auth.dart';
/// The properties of an OAuth 2.0 Resource Owner.
/// Defines the interface for a Resource Owner in OAuth 2.0 authentication.
///
/// Your application's 'user' type must implement the methods declared in this interface. [AuthServer] can
/// validate the credentials of a [ResourceOwner] to grant authorization codes and access tokens on behalf of that
@ -9,20 +18,59 @@ import 'package:protevus_auth/auth.dart';
abstract class ResourceOwner {
/// The username of the resource owner.
///
/// This value must be unique amongst all resource owners. It is often an email address. This value
/// is used by authenticating users to identify their account.
/// This property represents the unique identifier for a resource owner, typically used for authentication purposes.
/// It must be unique among all resource owners in the system. Often, this value is an email address.
///
/// The username is used by authenticating users to identify their account when logging in or performing
/// other authentication-related actions.
///
/// This property is nullable, which means it can be null in some cases, such as when creating a new
/// resource owner instance before setting the username.
String? username;
/// The hashed password of this instance.
///
/// This property stores the password of the resource owner in a hashed format.
/// Hashing is a one-way process that converts the plain text password into a
/// fixed-length string of characters, which is more secure to store than the
/// original password.
///
/// The hashed password is used for password verification during authentication
/// without storing the actual password. This enhances security by ensuring
/// that even if the database is compromised, the original passwords remain
/// unknown.
///
/// This property is nullable, allowing for cases where a password might not
/// be set or required for certain types of resource owners.
String? hashedPassword;
/// The salt the [hashedPassword] was hashed with.
/// The salt used in the hashing process for [hashedPassword].
///
/// A salt is a random string that is added to the password before hashing,
/// which adds an extra layer of security to the hashed password. It helps
/// protect against rainbow table attacks and ensures that even if two users
/// have the same password, their hashed passwords will be different.
///
/// This property is nullable to accommodate cases where a salt might not be
/// used or stored separately from the hashed password.
String? salt;
/// A unique identifier of this resource owner.
///
/// This unique identifier is used by [AuthServer] to associate authorization codes and access tokens with
/// this resource owner.
/// This property represents a unique identifier for the resource owner, typically
/// used in authentication and authorization processes. The [AuthServer] uses this
/// identifier to associate authorization codes and access tokens with the specific
/// resource owner.
///
/// The identifier is of type [int] and is nullable, allowing for cases where an ID
/// might not be assigned yet (e.g., when creating a new resource owner instance).
///
/// It's crucial to ensure that this ID remains unique across all resource owners
/// in the system to maintain the integrity of the authentication and authorization
/// processes.
///
/// This getter method should be implemented to return the unique identifier of
/// the resource owner.
int? get id;
}
@ -33,8 +81,23 @@ abstract class ResourceOwner {
///
/// Prefer to use `ManagedAuthDelegate` from 'package:conduit_core/managed_auth.dart' instead of implementing this interface;
/// there are important details to consider and test when implementing this interface.
///
/// This abstract class defines the contract for implementing an authentication and authorization system.
/// It provides methods for managing resource owners, clients, tokens, and authorization codes.
/// Implementations of this class are responsible for handling the storage, retrieval, and management
/// of these entities, as well as customizing certain behaviors of the authentication process.
///
/// Key responsibilities include:
/// - Managing resource owners (users)
/// - Handling client applications
/// - Storing and retrieving access and refresh tokens
/// - Managing authorization codes
/// - Customizing token formats and allowed scopes
///
/// Each method in this class corresponds to a specific operation in the OAuth 2.0 flow,
/// allowing for a flexible and extensible authentication system.
abstract class AuthServerDelegate {
/// Must return a [ResourceOwner] for a [username].
/// Retrieves a [ResourceOwner] based on the provided [username].
///
/// This method must return an instance of [ResourceOwner] if one exists for [username]. Otherwise, it must return null.
///
@ -43,19 +106,19 @@ abstract class AuthServerDelegate {
/// [server] is the [AuthServer] invoking this method.
FutureOr<ResourceOwner?> getResourceOwner(AuthServer server, String username);
/// Must store [client].
/// Stores a new [AuthClient] in the system.
///
/// [client] must be returned by [getClient] after this method has been invoked, and until (if ever)
/// [removeClient] is invoked.
FutureOr addClient(AuthServer server, AuthClient client);
/// Must return [AuthClient] for a client ID.
/// Retrieves an [AuthClient] based on the provided client ID.
///
/// This method must return an instance of [AuthClient] if one exists for [clientID]. Otherwise, it must return null.
/// [server] is the [AuthServer] requesting the [AuthClient].
FutureOr<AuthClient?> getClient(AuthServer server, String clientID);
/// Removes an [AuthClient] for a client ID.
/// Removes an [AuthClient] for a given client ID.
///
/// This method must delete the [AuthClient] for [clientID]. Subsequent requests to this
/// instance for [getClient] must return null after this method completes. If there is no
@ -64,7 +127,7 @@ abstract class AuthServerDelegate {
/// [server] is the [AuthServer] requesting the [AuthClient].
FutureOr removeClient(AuthServer server, String clientID);
/// Returns a [AuthToken] searching by its access token or refresh token.
/// Retrieves an [AuthToken] based on either its access token or refresh token.
///
/// Exactly one of [byAccessToken] and [byRefreshToken] may be non-null, if not, this method must throw an error.
///
@ -80,12 +143,12 @@ abstract class AuthServerDelegate {
String? byRefreshToken,
});
/// This method must delete all [AuthToken] and [AuthCode]s for a [ResourceOwner].
/// Deletes all [AuthToken]s and [AuthCode]s associated with a specific [ResourceOwner].
///
/// [server] is the requesting [AuthServer]. [resourceOwnerID] is the [ResourceOwner.id].
FutureOr removeTokens(AuthServer server, int resourceOwnerID);
/// Must delete a [AuthToken] granted by [grantedByCode].
/// Deletes an [AuthToken] that was granted by a specific [AuthCode].
///
/// If an [AuthToken] has been granted by exchanging [AuthCode], that token must be revoked
/// and can no longer be used to authorize access to a resource. [grantedByCode] should
@ -94,7 +157,7 @@ abstract class AuthServerDelegate {
/// This method is invoked when attempting to exchange an authorization code that has already granted a token.
FutureOr removeToken(AuthServer server, AuthCode grantedByCode);
/// Must store [token].
/// Stores an [AuthToken] in the system.
///
/// [token] must be stored such that it is accessible from [getToken], and until it is either
/// revoked via [removeToken] or [removeTokens], or until it has expired and can reasonably
@ -107,7 +170,7 @@ abstract class AuthServerDelegate {
/// is null.
FutureOr addToken(AuthServer server, AuthToken token, {AuthCode? issuedFrom});
/// Must update [AuthToken] with [newAccessToken, [newIssueDate, [newExpirationDate].
/// Updates an existing [AuthToken] with new values.
///
/// This method must must update an existing [AuthToken], found by [oldAccessToken],
/// with the values [newAccessToken], [newIssueDate] and [newExpirationDate].
@ -122,23 +185,23 @@ abstract class AuthServerDelegate {
DateTime? newExpirationDate,
);
/// Must store [code].
/// Stores an [AuthCode] in the system.
///
/// [code] must be accessible until its expiration date.
FutureOr addCode(AuthServer server, AuthCode code);
/// Must return [AuthCode] for its identifiying [code].
/// Retrieves an [AuthCode] based on its identifying code.
///
/// This must return an instance of [AuthCode] where [AuthCode.code] matches [code].
/// Return null if no matching code.
FutureOr<AuthCode?> getCode(AuthServer server, String code);
/// Must remove [AuthCode] identified by [code].
/// Removes an [AuthCode] from the system based on its identifying code.
///
/// The [AuthCode.code] matching [code] must be deleted and no longer accessible.
FutureOr removeCode(AuthServer server, String? code);
/// Returns list of allowed scopes for a given [ResourceOwner].
/// Returns a list of allowed scopes for a given [ResourceOwner].
///
/// Subclasses override this method to return a list of [AuthScope]s based on some attribute(s) of an [ResourceOwner].
/// That [ResourceOwner] is then restricted to only those scopes, even if the authenticating client would allow other scopes

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'package:protevus_openapi/documentable.dart';
@ -5,14 +14,14 @@ import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
/// Instances that implement this type can be used by an [Authorizer] to determine authorization of a request.
/// A mixin that defines the interface for validating authorization data.
///
/// When an [Authorizer] processes a [Request], it invokes [validate], passing in the parsed Authorization
/// header of the [Request].
///
/// [AuthServer] implements this interface.
mixin AuthValidator {
/// Returns an [Authorization] if [authorizationData] is valid.
/// Validates authorization data and returns an [Authorization] if valid.
///
/// This method is invoked by [Authorizer] to validate the Authorization header of a request. [authorizationData]
/// is the parsed contents of the Authorization header, while [parser] is the object that parsed the header.
@ -28,7 +37,7 @@ mixin AuthValidator {
List<AuthScope>? requiredScope,
});
/// Provide [APISecurityRequirement]s for [authorizer].
/// Provides [APISecurityRequirement]s for the given [authorizer].
///
/// An [Authorizer] that adds security requirements to operations will invoke this method to allow this validator to define those requirements.
/// The [Authorizer] must provide the [context] it was given to document the operations, itself and optionally a list of [scopes] required to pass it.

View file

@ -13,7 +13,28 @@ import 'dart:mirrors';
import 'package:protevus_config/config.dart';
import 'package:protevus_runtime/runtime.dart';
/// A compiler class for configurations in the Protevus Platform.
///
/// This class extends the [Compiler] class and provides functionality to
/// compile configuration-related data and modify package files.
///
/// The [compile] method creates a map of configuration names to their
/// corresponding [ConfigurationRuntimeImpl] instances by scanning for
/// subclasses of [Configuration] in the given [MirrorContext].
///
/// The [deflectPackage] method modifies the "conduit_config.dart" file in the
/// destination directory by removing a specific export statement.
class ConfigurationCompiler extends Compiler {
/// Compiles configuration data from the given [MirrorContext].
///
/// This method scans the [context] for all subclasses of [Configuration]
/// and creates a map where:
/// - The keys are the names of these subclasses (as strings)
/// - The values are instances of [ConfigurationRuntimeImpl] created from
/// the corresponding subclass
///
/// Returns a [Map<String, Object>] where each entry represents a
/// configuration class and its runtime implementation.
@override
Map<String, Object> compile(MirrorContext context) {
return Map.fromEntries(
@ -26,15 +47,26 @@ class ConfigurationCompiler extends Compiler {
);
}
/// Modifies the package file by removing a specific export statement.
///
/// This method performs the following steps:
/// 1. Locates the "config.dart" file in the "lib/" directory of the [destinationDirectory].
/// 2. Reads the contents of the file.
/// 3. Removes the line "export 'package:protevus_config/src/compiler.dart';" from the file contents.
/// 4. Writes the modified contents back to the file.
///
/// This operation is typically used to adjust the exported modules in the compiled package.
///
/// [destinationDirectory] is the directory where the package files are located.
@override
void deflectPackage(Directory destinationDirectory) {
final libFile = File.fromUri(
destinationDirectory.uri.resolve("lib/").resolve("conduit_config.dart"),
destinationDirectory.uri.resolve("lib/").resolve("config.dart"),
);
final contents = libFile.readAsStringSync();
libFile.writeAsStringSync(
contents.replaceFirst(
"export 'package:conduit_config/src/compiler.dart';", ""),
"export 'package:protevus_config/src/compiler.dart';", ""),
);
}
}

View file

@ -14,17 +14,90 @@ import 'package:protevus_runtime/runtime.dart';
import 'package:meta/meta.dart';
import 'package:yaml/yaml.dart';
/// Subclasses of [Configuration] read YAML strings and files, assigning values from the YAML document to properties
/// of an instance of this type.
/// A base class for configuration management in Dart applications.
///
/// [Configuration] provides a framework for reading and parsing YAML-based
/// configuration files or strings. It offers various constructors to create
/// configuration objects from different sources (maps, strings, or files),
/// and includes methods for decoding and validating configuration data.
///
/// Key features:
/// - Supports creating configurations from YAML strings, files, or maps
/// - Provides a runtime context for configuration-specific operations
/// - Includes a default decoding mechanism that can be overridden
/// - Offers a validation method to ensure all required fields are present
/// - Allows for environment variable substitution in configuration values
///
/// Subclasses of [Configuration] should implement specific configuration
/// structures by defining properties that correspond to expected YAML keys.
/// The [decode] and [validate] methods can be overridden to provide custom
/// behavior for complex configuration scenarios.
///
/// Example usage:
/// ```dart
/// class MyConfig extends Configuration {
/// late String apiKey;
/// int port = 8080;
///
/// @override
/// void validate() {
/// super.validate();
/// if (port < 1000 || port > 65535) {
/// throw ConfigurationException(this, "Invalid port number");
/// }
/// }
/// }
///
/// final config = MyConfig.fromFile(File('config.yaml'));
/// ```
abstract class Configuration {
/// Default constructor.
/// Default constructor for the Configuration class.
///
/// This constructor creates a new instance of the Configuration class
/// without any initial configuration data. It can be used as a starting
/// point for creating custom configurations, which can then be populated
/// using other methods or by setting properties directly.
Configuration();
/// Creates a [Configuration] instance from a given map.
///
/// This constructor takes a [Map] with dynamic keys and values, converts
/// all keys to strings, and then decodes the resulting map into the
/// configuration properties. This is useful when you have configuration
/// data already in a map format, possibly from a non-YAML source.
///
/// [map] The input map containing configuration data. Keys will be
/// converted to strings, while values remain as their original types.
///
/// Example:
/// ```dart
/// final configMap = {'key1': 'value1', 'key2': 42};
/// final config = MyConfiguration.fromMap(configMap);
/// ```
Configuration.fromMap(Map<dynamic, dynamic> map) {
decode(map.map<String, dynamic>((k, v) => MapEntry(k.toString(), v)));
}
/// [contents] must be YAML.
/// Creates a [Configuration] instance from a YAML string.
///
/// This constructor takes a [String] containing YAML content, parses it into
/// a map, and then decodes the resulting map into the configuration properties.
/// It's useful when you have configuration data as a YAML string, perhaps
/// loaded from a file or received from an API.
///
/// [contents] A string containing valid YAML data. This will be parsed and
/// used to populate the configuration properties.
///
/// Throws a [YamlException] if the YAML parsing fails.
///
/// Example:
/// ```dart
/// final yamlString = '''
/// api_key: abc123
/// port: 8080
/// ''';
/// final config = MyConfiguration.fromString(yamlString);
/// ```
Configuration.fromString(String contents) {
final yamlMap = loadYaml(contents) as Map<dynamic, dynamic>?;
final map =
@ -32,15 +105,29 @@ abstract class Configuration {
decode(map);
}
/// Opens a file and reads its string contents into this instance's properties.
/// Creates a [Configuration] instance from a YAML file.
///
/// [file] must contain valid YAML data.
Configuration.fromFile(File file) : this.fromString(file.readAsStringSync());
/// Returns the [ConfigurationRuntime] associated with the current instance's runtime type.
///
/// This getter retrieves the [ConfigurationRuntime] from the [RuntimeContext.current] map,
/// using the runtime type of the current instance as the key. The retrieved value
/// is then cast to [ConfigurationRuntime].
///
/// This is typically used internally to access runtime-specific configuration
/// operations and validations.
///
/// Returns:
/// The [ConfigurationRuntime] associated with this configuration's type.
///
/// Throws:
/// A runtime exception if the retrieved value cannot be cast to [ConfigurationRuntime].
ConfigurationRuntime get _runtime =>
RuntimeContext.current[runtimeType] as ConfigurationRuntime;
/// Ingests [value] into the properties of this type.
/// Decodes the given [value] and populates the properties of this configuration instance.
///
/// Override this method to provide decoding behavior other than the default behavior.
void decode(dynamic value) {
@ -58,7 +145,8 @@ abstract class Configuration {
/// Validates this configuration.
///
/// By default, ensures all required keys are non-null.
/// This method is called automatically after the configuration is decoded. It performs
/// validation checks on the configuration data to ensure its integrity and correctness.
///
/// Override this method to perform validations on input data. Throw [ConfigurationException]
/// for invalid data.
@ -67,6 +155,24 @@ abstract class Configuration {
_runtime.validate(this);
}
/// Retrieves an environment variable value or returns the original value.
///
/// This method checks if the given [value] is a string that starts with '$'.
/// If so, it interprets the rest of the string as an environment variable name
/// and attempts to retrieve its value from the system environment.
///
/// If the environment variable exists, its value is returned.
/// If the environment variable does not exist, null is returned.
/// If the [value] is not a string starting with '$', the original [value] is returned unchanged.
///
/// Parameters:
/// [value]: The value to check. Can be of any type.
///
/// Returns:
/// - The value of the environment variable if [value] is a string starting with '$'
/// and the corresponding environment variable exists.
/// - null if [value] is a string starting with '$' but the environment variable doesn't exist.
/// - The original [value] if it's not a string starting with '$'.
static dynamic getEnvironmentOrValue(dynamic value) {
if (value is String && value.startsWith(r"$")) {
final envKey = value.substring(1);
@ -80,10 +186,72 @@ abstract class Configuration {
}
}
/// An abstract class representing the runtime behavior for configuration objects.
///
/// This class provides methods for decoding and validating configuration objects,
/// as well as a utility method for handling exceptions during the decoding process.
///
/// Implementations of this class should provide concrete logic for decoding
/// configuration data from input maps and validating the resulting configuration objects.
///
/// The [tryDecode] method offers a standardized way to handle exceptions that may occur
/// during the decoding process, wrapping them in appropriate [ConfigurationException]s
/// with detailed key paths for easier debugging.
abstract class ConfigurationRuntime {
/// Decodes the input map and populates the given configuration object.
///
/// This method is responsible for parsing the input map and setting the
/// corresponding values in the configuration object. It should handle
/// type conversions, nested structures, and any specific logic required
/// for populating the configuration.
///
/// Parameters:
/// [configuration]: The Configuration object to be populated with decoded values.
/// [input]: A Map containing the raw configuration data to be decoded.
///
/// Implementations of this method should handle potential errors gracefully,
/// possibly by throwing ConfigurationException for invalid or missing data.
void decode(Configuration configuration, Map input);
/// Validates the given configuration object.
///
/// This method is responsible for performing validation checks on the
/// provided configuration object. It should ensure that all required
/// fields are present and that the values meet any specific criteria
/// or constraints defined for the configuration.
///
/// Parameters:
/// [configuration]: The Configuration object to be validated.
///
/// Implementations of this method should throw a [ConfigurationException]
/// if any validation errors are encountered, providing clear and specific
/// error messages to aid in debugging and resolution of configuration issues.
void validate(Configuration configuration);
/// Attempts to decode a configuration property and handles exceptions.
///
/// This method provides a standardized way to handle exceptions that may occur
/// during the decoding process of a configuration property. It wraps the decoding
/// logic in a try-catch block and transforms various exceptions into appropriate
/// [ConfigurationException]s with detailed key paths for easier debugging.
///
/// Parameters:
/// [configuration]: The Configuration object being decoded.
/// [name]: The name of the property being decoded.
/// [decode]: A function that performs the actual decoding logic.
///
/// Returns:
/// The result of the [decode] function if successful.
///
/// Throws:
/// [ConfigurationException]:
/// - If a [ConfigurationException] is caught, it's re-thrown with an updated key path.
/// - If an [IntermediateException] is caught, it's transformed into a [ConfigurationException]
/// with appropriate error details.
/// - For any other exception, a new [ConfigurationException] is thrown with the exception message.
///
/// This method is particularly useful for maintaining a consistent error handling
/// approach across different configuration properties and types.
dynamic tryDecode(
Configuration configuration,
String name,
@ -134,18 +302,40 @@ abstract class ConfigurationRuntime {
}
}
/// Possible options for a configuration item property's optionality.
/// Enumerates the possible options for a configuration item property's optionality.
///
/// This enum is used to specify whether a configuration property is required or optional
/// when parsing configuration data. It helps in determining how to handle missing keys
/// in the source YAML configuration.
enum ConfigurationItemAttributeType {
/// [Configuration] properties marked as [required] will throw an exception
/// if their source YAML doesn't contain a matching key.
/// Indicates that a configuration property is required.
///
/// When a configuration property is marked as [required], it means that
/// the corresponding key must be present in the source YAML configuration.
/// If the key is missing, an exception will be thrown during the parsing
/// or validation process.
///
/// This helps ensure that all necessary configuration values are provided
/// and reduces the risk of runtime errors due to missing configuration data.
required,
/// Indicates that a configuration property is optional.
///
/// When a configuration property is marked as [optional], it means that
/// the corresponding key can be omitted from the source YAML configuration
/// without causing an error. If the key is missing, the property will be
/// silently ignored during the parsing process.
///
/// This allows for more flexible configuration structures where some
/// properties are not mandatory and can be omitted without affecting
/// the overall functionality of the configuration.
///
/// [Configuration] properties marked as [optional] will be silently ignored
/// if their source YAML doesn't contain a matching key.
optional
}
/// [Configuration] properties may be attributed with these.
/// Represents an attribute for configuration item properties.
///
/// **NOTICE**: This will be removed in version 2.0.0.
/// To signify required or optional config you could do:
@ -204,14 +394,83 @@ const ConfigurationItemAttribute requiredConfiguration =
const ConfigurationItemAttribute optionalConfiguration =
ConfigurationItemAttribute._(ConfigurationItemAttributeType.optional);
/// Thrown when reading data into a [Configuration] fails.
/// Represents an exception thrown when reading data into a [Configuration] fails.
///
/// This exception provides detailed information about the configuration error,
/// including the configuration object where the error occurred, the error message,
/// and optionally, the key path to the problematic configuration item.
///
/// The class offers two constructors:
/// 1. A general constructor for creating exceptions with custom messages.
/// 2. A specialized constructor [ConfigurationException.missingKeys] for creating
/// exceptions specifically related to missing required keys.
///
/// The [toString] method provides a formatted error message that includes the
/// configuration type, the key path (if available), and the error message.
///
/// Usage:
/// ```dart
/// throw ConfigurationException(
/// myConfig,
/// "Invalid value",
/// keyPath: ['server', 'port'],
/// );
/// ```
///
/// Or for missing keys:
/// ```dart
/// throw ConfigurationException.missingKeys(
/// myConfig,
/// ['apiKey', 'secret'],
/// );
/// ```
class ConfigurationException {
/// Creates a new [ConfigurationException] instance.
///
/// This constructor is used to create an exception that provides information
/// about a configuration error.
///
/// Parameters:
/// - [configuration]: The [Configuration] object where the error occurred.
/// - [message]: A string describing the error.
/// - [keyPath]: An optional list of keys or indices that specify the path to
/// the problematic configuration item. Defaults to an empty list.
///
/// Example:
/// ```dart
/// throw ConfigurationException(
/// myConfig,
/// "Invalid port number",
/// keyPath: ['server', 'port'],
/// );
/// ```
ConfigurationException(
this.configuration,
this.message, {
this.keyPath = const [],
});
/// Creates a [ConfigurationException] for missing required keys.
///
/// This constructor is specifically used to create an exception when one or more
/// required keys are missing from the configuration.
///
/// Parameters:
/// - [configuration]: The [Configuration] object where the missing keys were detected.
/// - [missingKeys]: A list of strings representing the names of the missing required keys.
/// - [keyPath]: An optional list of keys or indices that specify the path to the
/// configuration item where the missing keys were expected. Defaults to an empty list.
///
/// The [message] is automatically generated to list all the missing keys.
///
/// Example:
/// ```dart
/// throw ConfigurationException.missingKeys(
/// myConfig,
/// ['apiKey', 'secret'],
/// keyPath: ['server', 'authentication'],
/// );
/// ```
ConfigurationException.missingKeys(
this.configuration,
List<String> missingKeys, {
@ -219,17 +478,52 @@ class ConfigurationException {
}) : message =
"missing required key(s): ${missingKeys.map((s) => "'$s'").join(", ")}";
/// The [Configuration] in which this exception occurred.
/// The [Configuration] instance in which this exception occurred.
///
/// This field stores a reference to the [Configuration] object that was being
/// processed when the exception was thrown. It provides context about which
/// specific configuration was involved in the error, allowing for more
/// detailed error reporting and easier debugging.
///
/// The stored configuration can be used to access additional information
/// about the configuration state at the time of the error, which can be
/// helpful in diagnosing and resolving configuration-related issues.
final Configuration configuration;
/// The reason for the exception.
///
/// This field contains a string describing the specific error or reason
/// why the [ConfigurationException] was thrown. It provides detailed
/// information about what went wrong during the configuration process.
///
/// The message can be used for logging, debugging, or displaying error
/// information to users or developers to help diagnose and fix
/// configuration-related issues.
final String message;
/// The key of the object being evaluated.
/// The key path of the object being evaluated.
///
/// Either a string (adds '.name') or an int (adds '\[value\]').
final List<dynamic> keyPath;
/// Provides a string representation of the [ConfigurationException].
///
/// This method generates a formatted error message that includes:
/// - The type of the configuration where the error occurred
/// - The key path to the problematic configuration item (if available)
/// - The specific error message
///
/// The key path is constructed by joining the elements in [keyPath]:
/// - String elements are joined with dots (e.g., 'server.port')
/// - Integer elements are enclosed in square brackets (e.g., '[0]')
///
/// If [keyPath] is empty, a general error message for the configuration is returned.
///
/// Returns:
/// A string containing the formatted error message.
///
/// Throws:
/// [StateError] if an element in [keyPath] is neither a String nor an int.
@override
String toString() {
if (keyPath.isEmpty) {
@ -254,16 +548,78 @@ class ConfigurationException {
}
}
/// Thrown when [Configuration] subclass is invalid and requires a change in code.
/// Represents an error that occurs when a [Configuration] subclass is invalid and requires a change in code.
///
/// This exception is thrown when there's a structural or logical issue with a [Configuration] subclass
/// that cannot be resolved at runtime and requires modifications to the code itself.
///
/// The [ConfigurationError] provides information about the specific [Configuration] type that caused the error
/// and a descriptive message explaining the nature of the invalidity.
///
/// Properties:
/// - [type]: The Type of the [Configuration] subclass where the error occurred.
/// - [message]: A String describing the specific error or invalidity.
///
/// Usage:
/// ```dart
/// throw ConfigurationError(MyConfig, "Missing required property 'apiKey'");
/// ```
///
/// The [toString] method provides a formatted error message combining the invalid type and the error description.
class ConfigurationError {
/// Creates a new [ConfigurationError] instance.
///
/// This constructor is used to create an error that indicates an invalid [Configuration] subclass
/// which requires changes to the code itself to resolve.
///
/// Parameters:
/// - [type]: The [Type] of the [Configuration] subclass where the error occurred.
/// - [message]: A string describing the specific error or invalidity.
///
/// This error is typically thrown when there's a structural or logical issue with a [Configuration]
/// subclass that cannot be resolved at runtime and requires modifications to the code.
///
/// Example:
/// ```dart
/// throw ConfigurationError(MyConfig, "Missing required property 'apiKey'");
/// ```
ConfigurationError(this.type, this.message);
/// The type of [Configuration] in which this error appears in.
/// The type of [Configuration] in which this error appears.
///
/// This property stores the [Type] of the [Configuration] subclass that is
/// considered invalid or problematic. It provides context about which specific
/// configuration class triggered the error, allowing for more precise error
/// reporting and easier debugging.
///
/// The stored type can be used to identify the exact [Configuration] subclass
/// that needs to be modified or corrected to resolve the error.
final Type type;
/// The reason for the error.
///
/// This field contains a string describing the specific error or reason
/// why the [ConfigurationError] was thrown. It provides detailed
/// information about what makes the [Configuration] subclass invalid
/// or problematic.
///
/// The message can be used for logging, debugging, or displaying error
/// information to developers to help diagnose and fix issues related
/// to the structure or implementation of the [Configuration] subclass.
String message;
/// Returns a string representation of the [ConfigurationError].
///
/// This method generates a formatted error message that includes:
/// - The type of the invalid [Configuration] subclass
/// - The specific error message describing the invalidity
///
/// The resulting string is useful for logging, debugging, or displaying
/// error information to developers to help identify and fix issues with
/// the [Configuration] subclass implementation.
///
/// Returns:
/// A string containing the formatted error message.
@override
String toString() {
return "Invalid configuration type '$type'. $message";

View file

@ -10,17 +10,73 @@
import 'package:protevus_config/config.dart';
/// A [Configuration] to represent a database connection configuration.
///
/// This class extends [Configuration] and provides properties and methods
/// for managing database connection settings. It includes properties for
/// host, port, database name, username, password, and a flag for temporary
/// databases. The class supports initialization from various sources
/// (file, string, map) and provides a custom decoder for parsing connection
/// strings.
///
/// Properties:
/// - [host]: The host of the database to connect to (required).
/// - [port]: The port of the database to connect to (required).
/// - [databaseName]: The name of the database to connect to (required).
/// - [username]: A username for authenticating to the database (optional).
/// - [password]: A password for authenticating to the database (optional).
/// - [isTemporary]: A flag to represent permanence, used for test suites (optional).
///
/// The [decode] method allows parsing of connection strings or maps to
/// populate the configuration properties.
class DatabaseConfiguration extends Configuration {
/// Default constructor.
/// Default constructor for DatabaseConfiguration.
///
/// Creates a new instance of DatabaseConfiguration without initializing any properties.
/// Properties can be set manually or through the decode method after instantiation.
DatabaseConfiguration();
/// Creates a [DatabaseConfiguration] instance from a file.
///
/// This named constructor initializes the configuration by reading from a file.
/// The file path is passed to the superclass constructor [Configuration.fromFile].
///
/// Parameters:
/// [file]: The path to the configuration file.
DatabaseConfiguration.fromFile(super.file) : super.fromFile();
/// Creates a [DatabaseConfiguration] instance from a YAML string.
///
/// This named constructor initializes the configuration by parsing a YAML string.
/// The YAML string is passed to the superclass constructor [Configuration.fromString].
///
/// Parameters:
/// [yaml]: A string containing YAML-formatted configuration data.
DatabaseConfiguration.fromString(super.yaml) : super.fromString();
/// Creates a [DatabaseConfiguration] instance from a Map.
///
/// This named constructor initializes the configuration using a Map of key-value pairs.
/// The Map is passed to the superclass constructor [Configuration.fromMap].
///
/// Parameters:
/// [yaml]: A Map containing configuration data.
DatabaseConfiguration.fromMap(super.yaml) : super.fromMap();
/// A named constructor that contains all of the properties of this instance.
/// Creates a [DatabaseConfiguration] instance with all connection information provided.
///
/// This named constructor allows for the direct initialization of all database connection
/// properties in a single call. It sets both required and optional properties.
///
/// Parameters:
/// [username]: The username for database authentication (optional).
/// [password]: The password for database authentication (optional).
/// [host]: The host address of the database server (required).
/// [port]: The port number on which the database server is listening (required).
/// [databaseName]: The name of the specific database to connect to (required).
/// [isTemporary]: A flag indicating if this is a temporary database connection (optional, defaults to false).
///
/// This constructor provides a convenient way to create a fully configured
/// [DatabaseConfiguration] object when all connection details are known in advance.
DatabaseConfiguration.withConnectionInfo(
this.username,
this.password,
@ -32,36 +88,102 @@ class DatabaseConfiguration extends Configuration {
/// The host of the database to connect to.
///
/// This property is required.
/// This property represents the hostname or IP address of the database server
/// that this configuration will connect to. It is a required field and must be
/// set before attempting to establish a database connection.
///
/// The value should be a valid hostname (e.g., 'localhost', 'db.example.com')
/// or an IP address (e.g., '192.168.1.100').
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
late String host;
/// The port of the database to connect to.
///
/// This property is required.
/// This property represents the network port number on which the database server
/// is listening for connections. It is a required field and must be set before
/// attempting to establish a database connection.
///
/// The value should be a valid port number, typically an integer between 0 and 65535.
/// Common database port numbers include 5432 for PostgreSQL, 3306 for MySQL,
/// and 1433 for SQL Server, but the actual port may vary depending on the specific
/// database configuration.
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
late int port;
/// The name of the database to connect to.
///
/// This property is required.
/// This property represents the specific database name within the database server
/// that this configuration will target. It is a required field and must be set
/// before attempting to establish a database connection.
///
/// The value should be a valid database name as defined in your database server.
/// For example, it could be 'myapp_database', 'users_db', or 'production_data'.
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
late String databaseName;
/// A username for authenticating to the database.
///
/// This property is optional.
/// This property represents the username used for authentication when connecting
/// to the database. It is an optional field, meaning it can be null if authentication
/// is not required or if other authentication methods are used.
///
/// The value should be a string containing the username as configured in the
/// database server for this particular connection. For example, it could be
/// 'db_user', 'admin', or 'app_service_account'.
///
/// If this property is set, it is typically used in conjunction with the [password]
/// property to form a complete set of credentials for database authentication.
String? username;
/// A password for authenticating to the database.
///
/// This property is optional.
/// This property represents the password used for authentication when connecting
/// to the database. It is an optional field, meaning it can be null if authentication
/// is not required or if other authentication methods are used.
///
/// The value should be a string containing the password that corresponds to the
/// [username] for this database connection. For security reasons, it's important
/// to handle this value carefully and avoid exposing it in logs or user interfaces.
///
/// If this property is set, it is typically used in conjunction with the [username]
/// property to form a complete set of credentials for database authentication.
///
/// Note: In production environments, it's recommended to use secure methods of
/// storing and retrieving passwords, such as environment variables or secure
/// secret management systems, rather than hardcoding them in the configuration.
String? password;
/// A flag to represent permanence.
/// A flag to represent permanence of the database.
///
/// This flag is used for test suites that use a temporary database to run tests against,
/// dropping it after the tests are complete.
/// This property is optional.
bool isTemporary = false;
/// Decodes and populates the configuration from a given value.
///
/// This method can handle two types of input:
/// 1. A Map: In this case, it delegates to the superclass's decode method.
/// 2. A String: It parses the string as a URI to extract database connection details.
///
/// For string input, it extracts:
/// - Host and port from the URI
/// - Database name from the path (if present)
/// - Username and password from the userInfo part of the URI (if present)
///
/// After parsing, it calls the validate method to ensure all required fields are set.
///
/// Parameters:
/// [value]: The input to decode. Can be a Map or a String.
///
/// Throws:
/// [ConfigurationException]: If the input is neither a Map nor a String.
@override
void decode(dynamic value) {
if (value is Map) {
@ -101,27 +223,80 @@ class DatabaseConfiguration extends Configuration {
}
/// A [Configuration] to represent an external HTTP API.
///
/// This class extends [Configuration] and provides properties for managing
/// external API connection settings. It includes properties for the base URL,
/// client ID, and client secret.
///
/// The class supports initialization from various sources (file, string, map)
/// through its constructors.
///
/// Properties:
/// - [baseURL]: The base URL of the described API (required).
/// - [clientID]: The client ID for API authentication (optional).
/// - [clientSecret]: The client secret for API authentication (optional).
///
/// Constructors:
/// - Default constructor: Creates an empty instance.
/// - [fromFile]: Initializes from a configuration file.
/// - [fromString]: Initializes from a YAML string.
/// - [fromMap]: Initializes from a Map.
class APIConfiguration extends Configuration {
/// Default constructor for APIConfiguration.
///
/// Creates a new instance of APIConfiguration without initializing any properties.
/// Properties can be set manually or through the decode method after instantiation.
APIConfiguration();
/// Creates an [APIConfiguration] instance from a file.
///
/// This named constructor initializes the configuration by reading from a file.
/// The file path is passed to the superclass constructor [Configuration.fromFile].
///
/// Parameters:
/// [file]: The path to the configuration file.
APIConfiguration.fromFile(super.file) : super.fromFile();
/// Creates an [APIConfiguration] instance from a YAML string.
///
/// This named constructor initializes the configuration by parsing a YAML string.
/// The YAML string is passed to the superclass constructor [Configuration.fromString].
///
/// Parameters:
/// [yaml]: A string containing YAML-formatted configuration data.
APIConfiguration.fromString(super.yaml) : super.fromString();
/// Creates an [APIConfiguration] instance from a Map.
///
/// This named constructor initializes the configuration using a Map of key-value pairs.
/// The Map is passed to the superclass constructor [Configuration.fromMap].
///
/// Parameters:
/// [yaml]: A Map containing configuration data.
APIConfiguration.fromMap(super.yaml) : super.fromMap();
/// The base URL of the described API.
///
/// This property represents the root URL for the external API that this configuration
/// is describing. It is a required field and must be set before using the API configuration.
///
/// The value should be a complete URL, including the protocol (http or https),
/// domain name, and optionally the port and base path. It serves as the foundation
/// for constructing full URLs to specific API endpoints.
///
/// This property is marked as 'late', which means it must be initialized
/// before it's first used, but not necessarily in the constructor.
///
/// This property is required.
/// Example: https://external.api.com:80/resources
late String baseURL;
/// The client ID.
/// The client ID for API authentication.
///
/// This property is optional.
String? clientID;
/// The client secret.
/// The client secret for API authentication.
///
/// This property is optional.
String? clientSecret;

View file

@ -7,10 +7,31 @@
* file that was distributed with this source code.
*/
/// An exception class used for intermediate error handling.
///
/// This class encapsulates an underlying exception and a key path,
/// allowing for more detailed error reporting in nested structures.
///
/// [underlying] is the original exception that was caught.
/// [keyPath] is a list representing the path to the error in a nested structure.
class IntermediateException implements Exception {
/// Creates an [IntermediateException] with the given [underlying] exception and [keyPath].
///
/// [underlying] is the original exception that was caught.
/// [keyPath] is a list representing the path to the error in a nested structure.
IntermediateException(this.underlying, this.keyPath);
/// The original exception that was caught.
///
/// This field stores the underlying exception that triggered the creation
/// of this [IntermediateException]. It can be of any type, hence the
/// [dynamic] type annotation.
final dynamic underlying;
/// A list representing the path to the error in a nested structure.
///
/// This field stores the key path as a list of dynamic elements. Each element
/// in the list represents a key or index in the nested structure, helping to
/// pinpoint the exact location of the error.
final List<dynamic> keyPath;
}

View file

@ -8,10 +8,47 @@
*/
import 'dart:mirrors';
import 'package:protevus_config/config.dart';
/// A codec for decoding and encoding values based on their type using reflection.
///
/// This class uses the dart:mirrors library to introspect types and provide
/// appropriate decoding and encoding logic for various data types including
/// int, bool, Configuration subclasses, List, and Map.
///
/// The class supports:
/// - Decoding values from various input formats to their corresponding Dart types.
/// - Generating source code strings for decoding operations.
/// - Validating Configuration subclasses to ensure they have a default constructor.
///
/// Usage:
/// ```dart
/// final codec = MirrorTypeCodec(reflectType(SomeType));
/// final decodedValue = codec._decodeValue(inputValue);
/// final sourceCode = codec.source;
/// ```
///
/// Note: This class relies heavily on reflection, which may have performance
/// implications and is not supported in all Dart runtime environments.
class MirrorTypeCodec {
/// Constructor for MirrorTypeCodec.
///
/// This constructor takes a [TypeMirror] as its parameter and initializes the codec.
/// It performs a validation check for Configuration subclasses to ensure they have
/// a default (unnamed) constructor with all optional parameters.
///
/// Parameters:
/// [type]: The TypeMirror representing the type for which this codec is being created.
///
/// Throws:
/// [StateError]: If the type is a subclass of Configuration and doesn't have
/// an unnamed constructor with all optional parameters.
///
/// The constructor specifically:
/// 1. Checks if the type is a subclass of Configuration.
/// 2. If so, it verifies the presence of a default constructor.
/// 3. Throws a StateError if the required constructor is missing, providing
/// a detailed error message to guide the developer.
MirrorTypeCodec(this.type) {
if (type.isSubtypeOf(reflectType(Configuration))) {
final klass = type as ClassMirror;
@ -32,8 +69,34 @@ class MirrorTypeCodec {
}
}
/// The [TypeMirror] representing the type for which this codec is created.
///
/// This field stores the reflection information about the type that this
/// [MirrorTypeCodec] instance is designed to handle. It is used throughout
/// the class to determine how to decode and encode values of this type.
final TypeMirror type;
/// Decodes a value based on its type using reflection.
///
/// This method takes a [dynamic] input value and decodes it according to the
/// type specified by this codec's [type] property. It supports decoding for:
/// - Integers
/// - Booleans
/// - Configuration subclasses
/// - Lists
/// - Maps
///
/// If the input type doesn't match any of these, the original value is returned.
///
/// Parameters:
/// [value]: The input value to be decoded.
///
/// Returns:
/// The decoded value, with its type corresponding to the codec's [type].
///
/// Throws:
/// May throw exceptions if decoding fails, particularly for nested structures
/// like Lists and Maps.
dynamic _decodeValue(dynamic value) {
if (type.isSubtypeOf(reflectType(int))) {
return _decodeInt(value);
@ -50,6 +113,21 @@ class MirrorTypeCodec {
return value;
}
/// Decodes a boolean value from various input types.
///
/// This method handles the conversion of input values to boolean:
/// - If the input is a String, it returns true if the string is "true" (case-sensitive),
/// and false otherwise.
/// - For non-String inputs, it attempts to cast the value directly to a bool.
///
/// Parameters:
/// [value]: The input value to be decoded into a boolean.
///
/// Returns:
/// A boolean representation of the input value.
///
/// Throws:
/// TypeError: If the input cannot be cast to a bool (for non-String inputs).
dynamic _decodeBool(dynamic value) {
if (value is String) {
return value == "true";
@ -58,6 +136,21 @@ class MirrorTypeCodec {
return value as bool;
}
/// Decodes an integer value from various input types.
///
/// This method handles the conversion of input values to integers:
/// - If the input is a String, it attempts to parse it as an integer.
/// - For non-String inputs, it attempts to cast the value directly to an int.
///
/// Parameters:
/// [value]: The input value to be decoded into an integer.
///
/// Returns:
/// An integer representation of the input value.
///
/// Throws:
/// FormatException: If the input String cannot be parsed as an integer.
/// TypeError: If the input cannot be cast to an int (for non-String inputs).
dynamic _decodeInt(dynamic value) {
if (value is String) {
return int.parse(value);
@ -66,6 +159,21 @@ class MirrorTypeCodec {
return value as int;
}
/// Decodes a Configuration object from the given input.
///
/// This method creates a new instance of the Configuration subclass
/// represented by this codec's type, and then decodes the input object
/// into it.
///
/// Parameters:
/// [object]: The input object to be decoded into a Configuration instance.
///
/// Returns:
/// A new instance of the Configuration subclass, populated with the decoded data.
///
/// Throws:
/// May throw exceptions if the instantiation fails or if the decode
/// method of the Configuration subclass throws an exception.
Configuration _decodeConfig(dynamic object) {
final item = (type as ClassMirror).newInstance(Symbol.empty, []).reflectee
as Configuration;
@ -75,6 +183,26 @@ class MirrorTypeCodec {
return item;
}
/// Decodes a List value based on the codec's type parameters.
///
/// This method creates a new List instance and populates it with decoded elements
/// from the input List. It uses an inner decoder to process each element according
/// to the type specified in the codec's type arguments.
///
/// Parameters:
/// [value]: The input List to be decoded.
///
/// Returns:
/// A new List containing the decoded elements.
///
/// Throws:
/// IntermediateException: If an error occurs during the decoding of any element.
/// The exception includes the index of the problematic element in its keyPath.
///
/// Note:
/// - The method creates a growable List.
/// - It uses reflection to create the new List instance.
/// - Each element is decoded using an inner decoder based on the first type argument.
List _decodeList(List value) {
final out = (type as ClassMirror).newInstance(const Symbol('empty'), [], {
const Symbol('growable'): true,
@ -94,6 +222,27 @@ class MirrorTypeCodec {
return out;
}
/// Decodes a Map value based on the codec's type parameters.
///
/// This method creates a new Map instance and populates it with decoded key-value pairs
/// from the input Map. It uses an inner decoder to process each value according
/// to the type specified in the codec's type arguments.
///
/// Parameters:
/// [value]: The input Map to be decoded.
///
/// Returns:
/// A new Map containing the decoded key-value pairs.
///
/// Throws:
/// StateError: If any key in the input Map is not a String.
/// IntermediateException: If an error occurs during the decoding of any value.
/// The exception includes the key of the problematic value in its keyPath.
///
/// Note:
/// - The method creates a new Map instance using reflection.
/// - It enforces that all keys must be Strings.
/// - Each value is decoded using an inner decoder based on the last type argument.
Map<dynamic, dynamic> _decodeMap(Map value) {
final map =
(type as ClassMirror).newInstance(Symbol.empty, []).reflectee as Map;
@ -117,10 +266,36 @@ class MirrorTypeCodec {
return map;
}
/// Returns a string representation of the expected type for this codec.
///
/// This getter uses the [reflectedType] property of the [type] field
/// to obtain a string representation of the type that this codec is
/// expecting to handle. This is useful for generating type-specific
/// decoding logic or for debugging purposes.
///
/// Returns:
/// A [String] representing the name of the expected type.
String get expectedType {
return type.reflectedType.toString();
}
/// Returns the source code for decoding a value based on its type.
///
/// This getter generates and returns a string containing Dart code that can be used
/// to decode a value of the type represented by this codec. The returned code varies
/// depending on the type:
///
/// - For [int], it returns code to parse integers from strings or cast to int.
/// - For [bool], it returns code to convert strings to booleans or cast to bool.
/// - For [Configuration] subclasses, it returns code to create and decode a new instance.
/// - For [List], it returns code to decode each element of the list.
/// - For [Map], it returns code to decode each value in the map.
/// - For any other type, it returns code that simply returns the input value unchanged.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding the value.
String get source {
if (type.isSubtypeOf(reflectType(int))) {
return _decodeIntSource;
@ -137,6 +312,20 @@ class MirrorTypeCodec {
return "return v;";
}
/// Generates source code for decoding a List value.
///
/// This getter creates a string containing Dart code that decodes a List
/// based on the codec's type parameters. The generated code:
/// - Creates a new List to store decoded elements.
/// - Defines an inner decoder function for processing each element.
/// - Iterates through the input List, decoding each element.
/// - Handles exceptions, wrapping them in IntermediateException with the index.
///
/// The decoder function uses the source code from the inner codec,
/// which is based on the first type argument of the List.
///
/// Returns:
/// A String containing the Dart code for List decoding.
String get _decodeListSource {
final typeParam = MirrorTypeCodec(type.typeArguments.first);
return """
@ -159,6 +348,21 @@ return out;
""";
}
/// Generates source code for decoding a Map value.
///
/// This getter creates a string containing Dart code that decodes a Map
/// based on the codec's type parameters. The generated code:
/// - Creates a new Map to store decoded key-value pairs.
/// - Defines an inner decoder function for processing each value.
/// - Iterates through the input Map, ensuring all keys are Strings.
/// - Decodes each value using the inner decoder function.
/// - Handles exceptions, wrapping them in IntermediateException with the key.
///
/// The decoder function uses the source code from the inner codec,
/// which is based on the last type argument of the Map.
///
/// Returns:
/// A String containing the Dart code for Map decoding.
String get _decodeMapSource {
final typeParam = MirrorTypeCodec(type.typeArguments.last);
return """
@ -185,6 +389,17 @@ return map;
""";
}
/// Generates source code for decoding a Configuration object.
///
/// This getter returns a string containing Dart code that:
/// 1. Creates a new instance of the Configuration subclass represented by [expectedType].
/// 2. Calls the `decode` method on this new instance, passing in the input value 'v'.
/// 3. Returns the decoded Configuration object.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding a Configuration object.
String get _decodeConfigSource {
return """
final item = $expectedType();
@ -195,6 +410,17 @@ return map;
""";
}
/// Generates source code for decoding an integer value.
///
/// This getter returns a string containing Dart code that:
/// 1. Checks if the input value 'v' is a String.
/// 2. If it is a String, parses it to an integer using `int.parse()`.
/// 3. If it's not a String, casts the value directly to an int.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding an integer value.
String get _decodeIntSource {
return """
if (v is String) {
@ -205,6 +431,17 @@ return map;
""";
}
/// Generates source code for decoding a boolean value.
///
/// This getter returns a string containing Dart code that:
/// 1. Checks if the input value 'v' is a String.
/// 2. If it is a String, returns true if it equals "true", false otherwise.
/// 3. If it's not a String, casts the value directly to a bool.
///
/// The generated code assumes the input value is named 'v'.
///
/// Returns:
/// A [String] containing Dart code for decoding a boolean value.
String get _decodeBoolSource {
return """
if (v is String) {
@ -216,6 +453,24 @@ return map;
}
}
/// Represents a property of a Configuration class, providing metadata and decoding capabilities.
///
/// This class encapsulates information about a single property within a Configuration
/// subclass, including its name, whether it's required, and how to decode its value.
///
/// It uses the [MirrorTypeCodec] to handle the decoding of values based on the property's type.
///
/// Key features:
/// - Extracts the property name from the [VariableMirror].
/// - Determines if the property is required based on its metadata.
/// - Provides access to the decoding logic through the [codec] field.
/// - Offers a method to decode input values, taking into account environment variables.
///
/// Usage:
/// ```dart
/// final property = MirrorConfigurationProperty(someVariableMirror);
/// final decodedValue = property.decode(inputValue);
/// ```
class MirrorConfigurationProperty {
MirrorConfigurationProperty(this.property)
: codec = MirrorTypeCodec(property.type);

View file

@ -8,21 +8,73 @@
*/
import 'dart:mirrors';
import 'package:protevus_config/config.dart';
import 'package:protevus_runtime/runtime.dart';
/// ConfigurationRuntimeImpl is a class that extends ConfigurationRuntime and implements SourceCompiler.
///
/// This class is responsible for handling the runtime configuration of the application. It uses
/// Dart's mirror system to introspect and manipulate configuration objects at runtime.
///
/// Key features:
/// - Decodes configuration input from a Map into a strongly-typed Configuration object
/// - Validates the configuration to ensure all required fields are present
/// - Generates implementation code for decoding and validating configurations
/// - Collects and manages configuration properties
///
/// The class provides methods for decoding input, validating configurations, and compiling
/// source code for runtime configuration handling. It also includes utility methods for
/// collecting properties and generating implementation strings for decode and validate operations.
class ConfigurationRuntimeImpl extends ConfigurationRuntime
implements SourceCompiler {
/// Constructs a ConfigurationRuntimeImpl instance for the given type.
///
/// The constructor initializes the type and properties of the configuration runtime.
/// It collects properties using the `_collectProperties` method.
///
/// Parameters:
/// - type: The ClassMirror representing the type of the configuration object.
ConfigurationRuntimeImpl(this.type) {
// Should be done in the constructor so a type check could be run.
properties = _collectProperties();
}
/// The ClassMirror representing the type of the configuration object.
///
/// This field stores the reflection information for the configuration class,
/// allowing for runtime introspection and manipulation of the configuration object.
final ClassMirror type;
/// A map of property names to MirrorConfigurationProperty objects.
///
/// This late-initialized field stores the configuration properties of the class.
/// Each key is a string representing the property name, and the corresponding value
/// is a MirrorConfigurationProperty object containing metadata about that property.
///
/// The properties are collected during the initialization of the ConfigurationRuntimeImpl
/// instance and are used for decoding, validating, and generating implementation code
/// for the configuration.
late final Map<String, MirrorConfigurationProperty> properties;
/// Decodes the input map into the given configuration object.
///
/// This method takes a [Configuration] object and a [Map] input, and populates
/// the configuration object with the decoded values from the input map.
///
/// The method performs the following steps:
/// 1. Creates a copy of the input map.
/// 2. Iterates through each property in the configuration.
/// 3. For each property, it attempts to decode the corresponding value from the input.
/// 4. If the decoded value is not null and of the correct type, it sets the value on the configuration object.
/// 5. After processing all properties, it checks if there are any unexpected keys left in the input map.
///
/// Throws a [ConfigurationException] if:
/// - A decoded value is of the wrong type.
/// - There are unexpected keys in the input map after processing all known properties.
///
/// Parameters:
/// - [configuration]: The Configuration object to be populated with decoded values.
/// - [input]: A Map containing the input values to be decoded.
@override
void decode(Configuration configuration, Map input) {
final values = Map.from(input);
@ -61,6 +113,24 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
}
}
/// Generates the implementation string for the decode method.
///
/// This getter creates a String representation of the decode method implementation.
/// The generated code does the following:
/// 1. Creates a copy of the input map.
/// 2. Iterates through each property in the configuration.
/// 3. For each property:
/// - Retrieves the value from the input, considering environment variables.
/// - If a value exists, it attempts to decode it.
/// - Checks if the decoded value is of the expected type.
/// - If valid, assigns the decoded value to the configuration object.
/// 4. After processing all properties, it checks for any unexpected keys in the input.
///
/// The generated code includes proper error handling, throwing ConfigurationExceptions
/// for type mismatches or unexpected input keys.
///
/// Returns:
/// A String containing the implementation code for the decode method.
String get decodeImpl {
final buf = StringBuffer();
@ -98,6 +168,22 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
return buf.toString();
}
/// Validates the given configuration object to ensure all required properties are present.
///
/// This method performs the following steps:
/// 1. Creates a mirror of the configuration object for reflection.
/// 2. Iterates through all properties of the configuration.
/// 3. For each property, it checks if:
/// - The property is required.
/// - The property value is null or cannot be accessed.
/// 4. Collects a list of all required properties that are missing or null.
/// 5. If any required properties are missing, it throws a ConfigurationException.
///
/// Parameters:
/// - configuration: The Configuration object to be validated.
///
/// Throws:
/// - ConfigurationException: If any required properties are missing or null.
@override
void validate(Configuration configuration) {
final configMirror = reflect(configuration);
@ -121,6 +207,23 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
}
}
/// Collects and returns a map of configuration properties for the current type.
///
/// This method traverses the class hierarchy, starting from the current type
/// up to (but not including) the Configuration class, collecting all non-static
/// and non-private variable declarations. It then creates a map where:
///
/// - Keys are the string names of the properties
/// - Values are MirrorConfigurationProperty objects created from the VariableMirrors
///
/// The method performs the following steps:
/// 1. Initializes an empty list to store VariableMirror objects.
/// 2. Traverses the class hierarchy, collecting relevant VariableMirrors.
/// 3. Creates a map from the collected VariableMirrors.
/// 4. Returns the resulting map of property names to MirrorConfigurationProperty objects.
///
/// Returns:
/// A Map<String, MirrorConfigurationProperty> representing the configuration properties.
Map<String, MirrorConfigurationProperty> _collectProperties() {
final declarations = <VariableMirror>[];
@ -142,6 +245,22 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
return m;
}
/// Generates the implementation string for the validate method.
///
/// This getter creates a String representation of the validate method implementation.
/// The generated code does the following:
/// 1. Initializes a list to store missing keys.
/// 2. Iterates through each property in the configuration.
/// 3. For each property:
/// - Attempts to retrieve the property value from the configuration object.
/// - Checks if the property is required and its value is null.
/// - If required and null, or if an error occurs during retrieval, adds the property name to the missing keys list.
/// 4. After checking all properties, throws a ConfigurationException if any keys are missing.
///
/// The generated code includes error handling to catch any issues during property access.
///
/// Returns:
/// A String containing the implementation code for the validate method.
String get validateImpl {
final buf = StringBuffer();
@ -170,6 +289,23 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
return buf.toString();
}
/// Compiles the configuration runtime implementation into a string representation.
///
/// This method generates the source code for a ConfigurationRuntimeImpl class
/// that extends ConfigurationRuntime. The generated class includes implementations
/// for the 'decode' and 'validate' methods.
///
/// The method performs the following steps:
/// 1. Retrieves import directives for the current type and adds them to the generated code.
/// 2. Adds an import for the intermediate_exception.dart file.
/// 3. Creates an instance of ConfigurationRuntimeImpl.
/// 4. Generates the class definition with implementations of decode and validate methods.
///
/// Parameters:
/// - ctx: A BuildContext object used to retrieve import directives.
///
/// Returns:
/// A Future<String> containing the generated source code for the ConfigurationRuntimeImpl class.
@override
Future<String> compile(BuildContext ctx) async {
final directives = await ctx.getImportDirectives(