Compare commits
4 commits
7fa2bb0f7e
...
245321849a
Author | SHA1 | Date | |
---|---|---|---|
|
245321849a | ||
|
204b1b998e | ||
|
2cb685578b | ||
|
e7f8083b25 |
76 changed files with 4333 additions and 327 deletions
0
examples/app-template/.editorconfig
Normal file
0
examples/app-template/.editorconfig
Normal file
0
examples/app-template/.env.example.yaml
Normal file
0
examples/app-template/.env.example.yaml
Normal file
0
examples/app-template/.gitattributes
vendored
Normal file
0
examples/app-template/.gitattributes
vendored
Normal file
25
examples/app-template/.gitignore
vendored
Normal file
25
examples/app-template/.gitignore
vendored
Normal 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
|
3
examples/app-template/CHANGELOG.md
Normal file
3
examples/app-template/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial version.
|
22
examples/app-template/LICENSE
Normal file
22
examples/app-template/LICENSE
Normal 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.
|
42
examples/app-template/README.md
Normal file
42
examples/app-template/README.md
Normal 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)
|
||||||
|
|
30
examples/app-template/analysis_options.yaml
Normal file
30
examples/app-template/analysis_options.yaml
Normal 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
|
0
examples/app-template/app/models/user.dart
Normal file
0
examples/app-template/app/models/user.dart
Normal file
0
examples/app-template/bootstrap/app.dart
Normal file
0
examples/app-template/bootstrap/app.dart
Normal file
0
examples/app-template/bootstrap/cache/.gitkeep
vendored
Normal file
0
examples/app-template/bootstrap/cache/.gitkeep
vendored
Normal file
0
examples/app-template/bootstrap/providers.dart
Normal file
0
examples/app-template/bootstrap/providers.dart
Normal file
0
examples/app-template/caden.dart
Normal file
0
examples/app-template/caden.dart
Normal file
0
examples/app-template/config/app.dart
Normal file
0
examples/app-template/config/app.dart
Normal file
0
examples/app-template/config/auth.dart
Normal file
0
examples/app-template/config/auth.dart
Normal file
0
examples/app-template/config/cache.dart
Normal file
0
examples/app-template/config/cache.dart
Normal file
0
examples/app-template/config/database.dart
Normal file
0
examples/app-template/config/database.dart
Normal file
0
examples/app-template/config/filesystems.dart
Normal file
0
examples/app-template/config/filesystems.dart
Normal file
0
examples/app-template/config/logging.dart
Normal file
0
examples/app-template/config/logging.dart
Normal file
0
examples/app-template/config/mail.dart
Normal file
0
examples/app-template/config/mail.dart
Normal file
0
examples/app-template/config/queue.dart
Normal file
0
examples/app-template/config/queue.dart
Normal file
0
examples/app-template/config/services.dart
Normal file
0
examples/app-template/config/services.dart
Normal file
0
examples/app-template/config/session.dart
Normal file
0
examples/app-template/config/session.dart
Normal file
14
examples/app-template/devbox.json
Normal file
14
examples/app-template/devbox.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
examples/app-template/devbox.lock
Normal file
53
examples/app-template/devbox.lock
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
examples/app-template/public/index.dart
Normal file
0
examples/app-template/public/index.dart
Normal file
28
examples/app-template/pubspec.yaml
Normal file
28
examples/app-template/pubspec.yaml
Normal 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
|
0
examples/app-template/resource/css/.gitkeep
Normal file
0
examples/app-template/resource/css/.gitkeep
Normal file
0
examples/app-template/resource/js/.gitkeep
Normal file
0
examples/app-template/resource/js/.gitkeep
Normal file
0
examples/app-template/resource/views/welcome.blade.dart
Normal file
0
examples/app-template/resource/views/welcome.blade.dart
Normal file
0
examples/app-template/routes/console.dart
Normal file
0
examples/app-template/routes/console.dart
Normal file
0
examples/app-template/routes/web.dart
Normal file
0
examples/app-template/routes/web.dart
Normal file
0
examples/app-template/storage/.gitkeep
Normal file
0
examples/app-template/storage/.gitkeep
Normal file
0
examples/app-template/storage/app/.gitkeep
Normal file
0
examples/app-template/storage/app/.gitkeep
Normal file
0
examples/app-template/storage/app/public/.gitkeep
Normal file
0
examples/app-template/storage/app/public/.gitkeep
Normal file
0
examples/app-template/storage/framework/cache/.gitkeep
vendored
Normal file
0
examples/app-template/storage/framework/cache/.gitkeep
vendored
Normal file
0
examples/app-template/storage/framework/cache/data/.gitkeep
vendored
Normal file
0
examples/app-template/storage/framework/cache/data/.gitkeep
vendored
Normal file
0
examples/app-template/storage/framework/testing/.gitkeep
Normal file
0
examples/app-template/storage/framework/testing/.gitkeep
Normal file
0
examples/app-template/storage/framework/views/.gitkeep
Normal file
0
examples/app-template/storage/framework/views/.gitkeep
Normal file
0
examples/app-template/test/.gitkeep
Normal file
0
examples/app-template/test/.gitkeep
Normal file
|
@ -2,6 +2,7 @@ name: protevus_platform
|
||||||
repository: https://github.com/protevus/platform
|
repository: https://github.com/protevus/platform
|
||||||
packages:
|
packages:
|
||||||
- packages/**
|
- packages/**
|
||||||
|
- examples/**
|
||||||
|
|
||||||
command:
|
command:
|
||||||
version:
|
version:
|
||||||
|
|
|
@ -7,6 +7,14 @@
|
||||||
* file that was distributed with this source code.
|
* 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;
|
library;
|
||||||
|
|
||||||
export 'src/application.dart';
|
export 'src/application.dart';
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
@ -11,56 +20,138 @@ export 'application_server.dart';
|
||||||
export 'options.dart';
|
export 'options.dart';
|
||||||
export 'starter.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].
|
/// 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
|
/// It is unlikely that you need to use this class directly - the `conduit serve` command creates an application object
|
||||||
/// on your behalf.
|
/// on your behalf.
|
||||||
class Application<T extends ApplicationChannel> {
|
class Application<T extends ApplicationChannel> {
|
||||||
/// A list of isolates that this application supervises.
|
/// 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 = [];
|
List<ApplicationIsolateSupervisor> supervisors = [];
|
||||||
|
|
||||||
/// The [ApplicationServer] listening for HTTP requests while under test.
|
/// The [ApplicationServer] listening for HTTP requests while under test.
|
||||||
///
|
///
|
||||||
/// This property is only valid when an application is started via [startOnCurrentIsolate].
|
/// 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;
|
late ApplicationServer server;
|
||||||
|
|
||||||
/// The [ApplicationChannel] handling requests while under test.
|
/// The [ApplicationChannel] handling requests while under test.
|
||||||
///
|
///
|
||||||
/// This property is only valid when an application is started via [startOnCurrentIsolate]. You use
|
/// This property provides access to the application channel instance when the application
|
||||||
/// this value to access elements of your application channel during testing.
|
/// 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;
|
T get channel => server.channel as T;
|
||||||
|
|
||||||
/// The logger that this application will write messages to.
|
/// The logger that this application will write messages to.
|
||||||
///
|
///
|
||||||
/// This logger's name will appear as 'conduit'.
|
/// This logger is used throughout the application to record messages, errors,
|
||||||
Logger logger = Logger("conduit");
|
/// 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.
|
/// 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();
|
ApplicationOptions options = ApplicationOptions();
|
||||||
|
|
||||||
/// The duration to wait for each isolate during startup before failing.
|
/// 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.
|
/// Defaults to 30 seconds.
|
||||||
Duration isolateStartupTimeout = const Duration(seconds: 30);
|
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 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.
|
/// This value will return to false after [stop] has completed.
|
||||||
bool get isRunning => _hasFinishedLaunching;
|
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;
|
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;
|
ChannelRuntime get _runtime => RuntimeContext.current[T] as ChannelRuntime;
|
||||||
|
|
||||||
/// Starts this application, allowing it to handle HTTP requests.
|
/// Starts this application, allowing it to handle HTTP requests.
|
||||||
///
|
///
|
||||||
/// This method spawns [numberOfInstances] isolates, instantiates your application channel
|
/// This method initializes the application by spawning a specified number of isolates,
|
||||||
/// for each of these isolates, and opens an HTTP listener that sends requests to these instances.
|
/// 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
|
/// The [Future] returned from this method will complete once all isolates have successfully started
|
||||||
/// and are available to handle requests.
|
/// and are available to handle requests.
|
||||||
|
@ -110,7 +201,7 @@ class Application<T extends ApplicationChannel> {
|
||||||
_hasFinishedLaunching = true;
|
_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.
|
/// 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].
|
/// 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.
|
/// Stops the application from running.
|
||||||
///
|
///
|
||||||
/// Closes every isolate and their channel and stops listening for HTTP requests.
|
/// This method performs the following actions:
|
||||||
/// The [ServiceRegistry] will close any of its resources.
|
/// 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 {
|
Future stop() async {
|
||||||
_hasFinishedLaunching = false;
|
_hasFinishedLaunching = false;
|
||||||
await Future.wait(supervisors.map((s) => s.stop()))
|
await Future.wait(supervisors.map((s) => s.stop()))
|
||||||
|
@ -167,6 +270,26 @@ class Application<T extends ApplicationChannel> {
|
||||||
|
|
||||||
/// Creates an [APIDocument] from an [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.
|
/// This method is called by the `conduit document` CLI.
|
||||||
static Future<APIDocument> document(
|
static Future<APIDocument> document(
|
||||||
Type type,
|
Type type,
|
||||||
|
@ -188,6 +311,25 @@ class Application<T extends ApplicationChannel> {
|
||||||
return doc;
|
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(
|
Future<ApplicationIsolateSupervisor> _spawn(
|
||||||
Application application,
|
Application application,
|
||||||
ApplicationOptions config,
|
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.
|
/// Contains the original exception that halted startup.
|
||||||
class ApplicationStartupException implements Exception {
|
class ApplicationStartupException implements Exception {
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:protevus_application/application.dart';
|
import 'package:protevus_application/application.dart';
|
||||||
import 'package:protevus_http/http.dart';
|
import 'package:protevus_http/http.dart';
|
||||||
import 'package:protevus_runtime/runtime.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
|
/// The ApplicationServer class is responsible for managing the lifecycle of an HTTP server
|
||||||
/// instance of an [ApplicationChannel] subclass. Instances are created by [Application]
|
/// and its associated [ApplicationChannel]. It handles server creation, starting, and stopping,
|
||||||
/// and shouldn't be created otherwise.
|
/// 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 {
|
class ApplicationServer {
|
||||||
/// Creates a new server.
|
/// Creates a new server instance.
|
||||||
///
|
///
|
||||||
/// You should not need to invoke this method directly.
|
/// You should not need to invoke this method directly.
|
||||||
ApplicationServer(this.channelType, this.options, this.identifier) {
|
ApplicationServer(this.channelType, this.options, this.identifier) {
|
||||||
|
@ -21,41 +42,145 @@ class ApplicationServer {
|
||||||
..options = options;
|
..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;
|
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;
|
late final HttpServer server;
|
||||||
|
|
||||||
/// The instance of [ApplicationChannel] serving requests.
|
/// 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;
|
late ApplicationChannel channel;
|
||||||
|
|
||||||
/// The cached entrypoint of [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;
|
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;
|
final Type channelType;
|
||||||
|
|
||||||
/// Target for sending messages to other [ApplicationChannel.messageHub]s.
|
/// 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;
|
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;
|
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;
|
bool _requiresHTTPS = false;
|
||||||
|
|
||||||
/// The unique identifier of this instance.
|
/// The unique identifier of this instance.
|
||||||
///
|
///
|
||||||
/// Each instance has its own identifier, a numeric value starting at 1, to identify it
|
/// Each instance has its own identifier, a numeric value starting at 1, to identify it
|
||||||
/// among other instances.
|
/// 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;
|
int identifier;
|
||||||
|
|
||||||
/// The logger of this instance
|
/// Returns the logger instance for this ApplicationServer.
|
||||||
Logger get logger => Logger("conduit");
|
///
|
||||||
|
/// 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.
|
/// 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 {
|
Future start({bool shareHttpServer = false}) async {
|
||||||
logger.fine("ApplicationServer($identifier).start entry");
|
logger.fine("ApplicationServer($identifier).start entry");
|
||||||
|
|
||||||
|
@ -92,7 +217,21 @@ class ApplicationServer {
|
||||||
return didOpen();
|
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 {
|
Future close() async {
|
||||||
logger.fine("ApplicationServer($identifier).close Closing HTTP listener");
|
logger.fine("ApplicationServer($identifier).close Closing HTTP listener");
|
||||||
await server.close(force: true);
|
await server.close(force: true);
|
||||||
|
@ -104,7 +243,7 @@ class ApplicationServer {
|
||||||
logger.fine("ApplicationServer($identifier).close Closing complete");
|
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.
|
/// [ApplicationChannel.willStartReceivingRequests] is invoked after this opening has completed.
|
||||||
Future didOpen() async {
|
Future didOpen() async {
|
||||||
|
@ -117,6 +256,18 @@ class ApplicationServer {
|
||||||
logger.info("Server conduit/$identifier started.");
|
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) {
|
void sendApplicationEvent(dynamic event) {
|
||||||
// By default, do nothing
|
// By default, do nothing
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
@ -9,7 +18,7 @@ import 'package:protevus_runtime/runtime.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:meta/meta.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.
|
/// 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'.
|
/// 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
|
/// 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.
|
/// is a replica of your application that runs in its own memory isolated thread.
|
||||||
abstract class ApplicationChannel implements APIComponentDocumenter {
|
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
|
/// 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
|
/// 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.
|
/// is for documentation purposes.
|
||||||
static Future initializeApplication(ApplicationOptions options) async {}
|
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'.
|
/// 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;
|
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) {
|
set server(ApplicationServer server) {
|
||||||
_server = server;
|
_server = server;
|
||||||
messageHub._outboundController.stream.listen(server.sendApplicationEvent);
|
messageHub._outboundController.stream.listen(server.sendApplicationEvent);
|
||||||
server.hubSink = messageHub._inboundController.sink;
|
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
|
/// 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).
|
/// through this object will be received by every other channel in your application (except the one that sent it).
|
||||||
final ApplicationMessageHub messageHub = ApplicationMessageHub();
|
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.
|
/// 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
|
/// These options are set when starting the application. Changes to this object have no effect
|
||||||
/// on other isolates.
|
/// 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;
|
ApplicationOptions? options;
|
||||||
|
|
||||||
/// You implement this accessor to define how HTTP requests are handled by your application.
|
/// 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;
|
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;
|
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
|
/// 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
|
/// 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.
|
/// By default, this method does nothing.
|
||||||
Future prepare() async {}
|
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.
|
/// Override this method to take action just before [entryPoint] starts receiving requests. By default, does nothing.
|
||||||
void willStartReceivingRequests() {}
|
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
|
/// 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.
|
/// 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.
|
/// 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
|
/// The documentation process first invokes [documentComponents] on this channel. Every controller in the channel will have its
|
||||||
/// [documentComponents] methods invoked. Any declared property
|
/// [documentComponents] methods invoked. Any declared property
|
||||||
|
@ -181,6 +226,24 @@ abstract class ApplicationChannel implements APIComponentDocumenter {
|
||||||
return doc;
|
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
|
@mustCallSuper
|
||||||
@override
|
@override
|
||||||
void documentComponents(APIDocumentContext registry) {
|
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
|
/// 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.
|
/// 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> {
|
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 =
|
final StreamController<dynamic> _outboundController =
|
||||||
StreamController<dynamic>();
|
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 =
|
final StreamController<dynamic> _inboundController =
|
||||||
StreamController<dynamic>.broadcast();
|
StreamController<dynamic>.broadcast();
|
||||||
|
|
||||||
/// Adds a listener for messages from other hubs.
|
/// 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.
|
/// 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].
|
/// 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.
|
/// 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]
|
/// [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.
|
/// 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);
|
_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
|
@override
|
||||||
Future close() async {
|
Future close() async {
|
||||||
if (!_outboundController.hasListener) {
|
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 {
|
abstract class ChannelRuntime {
|
||||||
Iterable<APIComponentDocumenter> getDocumentableChannelComponents(
|
Iterable<APIComponentDocumenter> getDocumentableChannelComponents(
|
||||||
ApplicationChannel channel,
|
ApplicationChannel channel,
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:protevus_application/application.dart';
|
|
||||||
import 'package:logging/logging.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 {
|
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(
|
ApplicationIsolateServer(
|
||||||
Type channelType,
|
Type channelType,
|
||||||
ApplicationOptions configuration,
|
ApplicationOptions configuration,
|
||||||
|
@ -26,9 +63,40 @@ class ApplicationIsolateServer extends ApplicationServer {
|
||||||
supervisingApplicationPort.send(supervisingReceivePort.sendPort);
|
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;
|
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;
|
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
|
@override
|
||||||
Future start({bool shareHttpServer = false}) async {
|
Future start({bool shareHttpServer = false}) async {
|
||||||
final result = await super.start(shareHttpServer: shareHttpServer);
|
final result = await super.start(shareHttpServer: shareHttpServer);
|
||||||
|
@ -41,6 +109,21 @@ class ApplicationIsolateServer extends ApplicationServer {
|
||||||
return result;
|
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
|
@override
|
||||||
void sendApplicationEvent(dynamic event) {
|
void sendApplicationEvent(dynamic event) {
|
||||||
try {
|
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) {
|
void listener(dynamic message) {
|
||||||
if (message == ApplicationIsolateSupervisor.messageKeyStop) {
|
if (message == ApplicationIsolateSupervisor.messageKeyStop) {
|
||||||
stop();
|
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 {
|
Future stop() async {
|
||||||
supervisingReceivePort.close();
|
supervisingReceivePort.close();
|
||||||
logger.fine("ApplicationIsolateServer($identifier) closing server");
|
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(
|
typedef IsolateEntryFunction = void Function(
|
||||||
ApplicationInitialServerMessage message,
|
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 {
|
class ApplicationInitialServerMessage {
|
||||||
ApplicationInitialServerMessage(
|
ApplicationInitialServerMessage(
|
||||||
this.streamTypeName,
|
this.streamTypeName,
|
||||||
|
@ -94,6 +238,16 @@ class ApplicationInitialServerMessage {
|
||||||
bool logToConsole = false;
|
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 {
|
class MessageHubMessage {
|
||||||
MessageHubMessage(this.payload);
|
MessageHubMessage(this.payload);
|
||||||
|
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
|
@ -6,9 +15,26 @@ import 'package:logging/logging.dart';
|
||||||
|
|
||||||
/// Represents the supervision of a [ApplicationIsolateServer].
|
/// 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.
|
/// You should not use this class directly.
|
||||||
class ApplicationIsolateSupervisor {
|
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(
|
ApplicationIsolateSupervisor(
|
||||||
this.supervisingApplication,
|
this.supervisingApplication,
|
||||||
this.isolate,
|
this.isolate,
|
||||||
|
@ -18,34 +44,168 @@ class ApplicationIsolateSupervisor {
|
||||||
this.startupTimeout = const Duration(seconds: 30),
|
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;
|
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;
|
final ReceivePort receivePort;
|
||||||
|
|
||||||
/// A numeric identifier for the isolate relative to the [Application].
|
/// 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;
|
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;
|
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;
|
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;
|
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 = [];
|
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;
|
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;
|
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;
|
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;
|
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";
|
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";
|
static const String messageKeyListening = "_MessageListening";
|
||||||
|
|
||||||
/// Resumes the [Isolate] being supervised.
|
/// 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() {
|
Future resume() {
|
||||||
_launchCompleter = Completer();
|
_launchCompleter = Completer();
|
||||||
receivePort.listen(listener);
|
receivePort.listen(listener);
|
||||||
|
@ -71,6 +231,24 @@ class ApplicationIsolateSupervisor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops the [Isolate] being supervised.
|
/// 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 {
|
Future stop() async {
|
||||||
_stopCompleter = Completer();
|
_stopCompleter = Completer();
|
||||||
logger.fine(
|
logger.fine(
|
||||||
|
@ -91,6 +269,22 @@ class ApplicationIsolateSupervisor {
|
||||||
receivePort.close();
|
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) {
|
void listener(dynamic message) {
|
||||||
if (message is SendPort) {
|
if (message is SendPort) {
|
||||||
_serverSendPort = message;
|
_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() {
|
void sendPendingMessages() {
|
||||||
final list = List<MessageHubMessage>.from(_pendingMessageQueue);
|
final list = List<MessageHubMessage>.from(_pendingMessageQueue);
|
||||||
_pendingMessageQueue.clear();
|
_pendingMessageQueue.clear();
|
||||||
list.forEach(_sendMessageToOtherSupervisors);
|
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) {
|
void _sendMessageToOtherSupervisors(MessageHubMessage message) {
|
||||||
supervisingApplication.supervisors
|
supervisingApplication.supervisors
|
||||||
.where((sup) => sup != this)
|
.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) {
|
void _handleIsolateException(dynamic error, StackTrace stacktrace) {
|
||||||
if (_isLaunching) {
|
if (_isLaunching) {
|
||||||
final appException = ApplicationStartupException(error);
|
final appException = ApplicationStartupException(error);
|
||||||
|
|
|
@ -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 'dart:io';
|
||||||
|
|
||||||
import 'package:args/args.dart';
|
import 'package:args/args.dart';
|
||||||
|
@ -5,45 +14,73 @@ import 'package:protevus_application/application.dart';
|
||||||
|
|
||||||
/// An object that contains configuration values for an [Application].
|
/// 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 {
|
class ApplicationOptions {
|
||||||
/// The absolute path of the configuration file for this application.
|
/// 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`.
|
/// This property stores the file path of the configuration file used by the application.
|
||||||
/// You may load the file at this path in [ApplicationChannel] to use configuration values.
|
/// 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;
|
String? configurationFilePath;
|
||||||
|
|
||||||
/// The address to listen for HTTP requests on.
|
/// 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,
|
/// This property specifies the network address on which the application will listen for incoming HTTP requests.
|
||||||
/// 'any' will be any IPv6 address, otherwise, it will be any IPv4 or IPv6 address.
|
|
||||||
///
|
///
|
||||||
/// This value may be an [InternetAddress] or a [String].
|
/// This value may be an [InternetAddress] or a [String].
|
||||||
dynamic address;
|
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.
|
/// Defaults to 8888.
|
||||||
int port = 8888;
|
int port = 8888;
|
||||||
|
|
||||||
/// Whether or not the application should only receive connections over IPv6.
|
/// 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.
|
/// Defaults to false. This flag impacts the default value of the [address] property.
|
||||||
bool isIpv6Only = false;
|
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.
|
/// Defaults to false.
|
||||||
bool isUsingClientCertificate = 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.
|
/// 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
|
/// 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].
|
/// over how HTTPS is configured for an application, see [ApplicationChannel.securityContext].
|
||||||
String? certificateFilePath;
|
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.
|
/// 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
|
/// 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].
|
/// This is a user-specific set of configuration options provided by [ApplicationChannel.initializeApplication].
|
||||||
/// Each instance of [ApplicationChannel] has access to these values if set.
|
/// 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 = {};
|
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()
|
static final parser = ArgParser()
|
||||||
..addOption(
|
..addOption(
|
||||||
"address",
|
"address",
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:isolate';
|
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.
|
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>(
|
Future startApplication<T extends ApplicationChannel>(
|
||||||
Application<T> app,
|
Application<T> app,
|
||||||
int isolateCount,
|
int isolateCount,
|
||||||
|
|
|
@ -7,9 +7,10 @@
|
||||||
* file that was distributed with this source code.
|
* 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_code_controller.dart';
|
||||||
export 'src/auth_controller.dart';
|
export 'src/auth_controller.dart';
|
||||||
export 'src/auth_redirect_controller.dart';
|
export 'src/auth_redirect_controller.dart';
|
||||||
|
@ -20,3 +21,60 @@ export 'src/exceptions.dart';
|
||||||
export 'src/objects.dart';
|
export 'src/objects.dart';
|
||||||
export 'src/protocols.dart';
|
export 'src/protocols.dart';
|
||||||
export 'src/validator.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);
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
@ -7,13 +16,20 @@ import 'package:protevus_http/http.dart';
|
||||||
import 'package:protevus_openapi/v3.dart';
|
import 'package:protevus_openapi/v3.dart';
|
||||||
|
|
||||||
/// Provides [AuthCodeController] with application-specific behavior.
|
/// 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.')
|
@Deprecated('AuthCodeController is deprecated. See docs.')
|
||||||
abstract class AuthCodeControllerDelegate {
|
abstract class AuthCodeControllerDelegate {
|
||||||
/// Returns an HTML representation of a login form.
|
/// Returns an HTML representation of a login form.
|
||||||
///
|
///
|
||||||
/// Invoked when [AuthCodeController.getAuthorizationPage] is called in response to a GET request.
|
/// This method is responsible for generating the HTML content of a login form
|
||||||
/// Must provide HTML that will be returned to the browser for rendering. This form submission of this page
|
/// that will be displayed to the user when they attempt to authenticate.
|
||||||
/// should be a POST to [requestUri].
|
|
||||||
///
|
///
|
||||||
/// The form submission should include the values of [responseType], [clientID], [state], [scope]
|
/// 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.
|
/// 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.
|
/// 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")
|
/// .route("/auth/code")
|
||||||
/// .link(() => new AuthCodeController(authServer));
|
/// .link(() => new AuthCodeController(authServer));
|
||||||
///
|
|
||||||
@Deprecated('Use AuthRedirectController instead.')
|
@Deprecated('Use AuthRedirectController instead.')
|
||||||
class AuthCodeController extends ResourceController {
|
class AuthCodeController extends ResourceController {
|
||||||
/// Creates a new instance of an [AuthCodeController].
|
/// 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.')
|
@Deprecated('Use AuthRedirectController instead.')
|
||||||
AuthCodeController(this.authServer, {this.delegate}) {
|
AuthCodeController(this.authServer, {this.delegate}) {
|
||||||
acceptedContentTypes = [
|
acceptedContentTypes = [
|
||||||
|
@ -63,6 +84,11 @@ class AuthCodeController extends ResourceController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A reference to the [AuthServer] used to grant authorization codes.
|
/// 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;
|
final AuthServer authServer;
|
||||||
|
|
||||||
/// A randomly generated value the client can use to verify the origin of the redirect.
|
/// 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
|
/// 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
|
/// server have the same value for 'state' as passed in. This value is usually a randomly generated
|
||||||
/// session identifier.
|
/// 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")
|
@Bind.query("state")
|
||||||
String? 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'.
|
/// Must be 'code'.
|
||||||
@Bind.query("response_type")
|
@Bind.query("response_type")
|
||||||
String? responseType;
|
String? responseType;
|
||||||
|
|
||||||
/// The client ID of the authenticating client.
|
/// 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")
|
@Bind.query("client_id")
|
||||||
String? clientID;
|
String? clientID;
|
||||||
|
|
||||||
/// Renders an HTML login form.
|
/// Renders an HTML login form.
|
||||||
final AuthCodeControllerDelegate? delegate;
|
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
|
/// 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
|
/// 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.
|
/// Creates a one-time use authorization code.
|
||||||
///
|
///
|
||||||
/// This method will respond with a redirect that contains an authorization code ('code')
|
/// This method handles the POST request for the OAuth 2.0 authorization code grant flow.
|
||||||
/// and the passed in 'state'. If this request fails, the redirect URL
|
/// It authenticates the user with the provided credentials and, if successful, generates
|
||||||
/// will contain an 'error' key instead of the authorization code.
|
/// a one-time use authorization code.
|
||||||
///
|
///
|
||||||
/// This method is typically invoked by the login form returned from the GET to this controller.
|
/// This method is typically invoked by the login form returned from the GET to this controller.
|
||||||
@Operation.post()
|
@Operation.post()
|
||||||
Future<Response> authorize({
|
Future<Response> authorize({
|
||||||
/// The username of the authenticating user.
|
/// 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,
|
@Bind.query("username") String? username,
|
||||||
|
|
||||||
/// The password of the authenticating user.
|
/// 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,
|
@Bind.query("password") String? password,
|
||||||
|
|
||||||
/// A space-delimited list of access scopes being requested.
|
/// 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,
|
@Bind.query("scope") String? scope,
|
||||||
}) async {
|
}) async {
|
||||||
final client = await authServer.getClient(clientID!);
|
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
|
@override
|
||||||
APIRequestBody? documentOperationRequestBody(
|
APIRequestBody? documentOperationRequestBody(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -194,6 +310,20 @@ class AuthCodeController extends ResourceController {
|
||||||
return body;
|
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
|
@override
|
||||||
List<APIParameter> documentOperationParameters(
|
List<APIParameter> documentOperationParameters(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -206,6 +336,28 @@ class AuthCodeController extends ResourceController {
|
||||||
return params;
|
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
|
@override
|
||||||
Map<String, APIResponse> documentOperationResponses(
|
Map<String, APIResponse> documentOperationResponses(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -240,6 +392,24 @@ class AuthCodeController extends ResourceController {
|
||||||
throw StateError("AuthCodeController documentation failed.");
|
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
|
@override
|
||||||
Map<String, APIOperation> documentOperations(
|
Map<String, APIOperation> documentOperations(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -252,6 +422,27 @@ class AuthCodeController extends ResourceController {
|
||||||
return ops;
|
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(
|
static Response _redirectResponse(
|
||||||
String? inputUri,
|
String? inputUri,
|
||||||
String? clientStateOrNull, {
|
String? clientStateOrNull, {
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
@ -49,6 +58,30 @@ class AuthController extends ResourceController {
|
||||||
|
|
||||||
final AuthorizationBasicParser _parser = const AuthorizationBasicParser();
|
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.
|
/// Creates or refreshes an authentication token.
|
||||||
///
|
///
|
||||||
/// When grant_type is 'password', there must be username and password values.
|
/// 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.
|
/// include a valid Client ID and Secret in the Basic authorization scheme format.
|
||||||
@Operation.post()
|
@Operation.post()
|
||||||
Future<Response> grant({
|
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,
|
@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,
|
@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,
|
@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,
|
@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,
|
@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,
|
@Bind.query("scope") String? scope,
|
||||||
}) async {
|
}) async {
|
||||||
AuthBasicCredentials basicRecord;
|
AuthBasicCredentials basicRecord;
|
||||||
|
@ -118,6 +228,28 @@ class AuthController extends ResourceController {
|
||||||
|
|
||||||
/// Transforms a [AuthToken] into a [Response] object with an RFC6749 compliant JSON token
|
/// Transforms a [AuthToken] into a [Response] object with an RFC6749 compliant JSON token
|
||||||
/// as the HTTP response body.
|
/// 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) {
|
static Response tokenResponse(AuthToken token) {
|
||||||
return Response(
|
return Response(
|
||||||
HttpStatus.ok,
|
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
|
@override
|
||||||
void willSendResponse(Response response) {
|
void willSendResponse(Response response) {
|
||||||
if (response.statusCode == 400) {
|
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
|
@override
|
||||||
List<APIParameter> documentOperationParameters(
|
List<APIParameter> documentOperationParameters(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -155,6 +319,21 @@ class AuthController extends ResourceController {
|
||||||
return parameters;
|
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
|
@override
|
||||||
APIRequestBody documentOperationRequestBody(
|
APIRequestBody documentOperationRequestBody(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -169,6 +348,20 @@ class AuthController extends ResourceController {
|
||||||
return body;
|
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
|
@override
|
||||||
Map<String, APIOperation> documentOperations(
|
Map<String, APIOperation> documentOperations(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -193,6 +386,25 @@ class AuthController extends ResourceController {
|
||||||
return operations;
|
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
|
@override
|
||||||
Map<String, APIResponse> documentOperationResponses(
|
Map<String, APIResponse> documentOperationResponses(
|
||||||
APIDocumentContext context,
|
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) {
|
Response _responseForError(AuthRequestError error) {
|
||||||
return Response.badRequest(
|
return Response.badRequest(
|
||||||
body: {"error": AuthServerException.errorString(error)},
|
body: {"error": AuthServerException.errorString(error)},
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
@ -6,13 +15,31 @@ import 'package:protevus_auth/auth.dart';
|
||||||
import 'package:protevus_http/http.dart';
|
import 'package:protevus_http/http.dart';
|
||||||
import 'package:protevus_openapi/v3.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 {
|
abstract class AuthRedirectControllerDelegate {
|
||||||
/// Returns an HTML representation of a login form.
|
/// Returns an HTML representation of a login form.
|
||||||
///
|
///
|
||||||
/// Invoked when [AuthRedirectController.getAuthorizationPage] is called in response to a GET request.
|
/// This method is responsible for generating and returning the HTML content for a login form
|
||||||
/// Must provide HTML that will be returned to the browser for rendering. This form submission of this page
|
/// when [AuthRedirectController.getAuthorizationPage] is called in response to a GET request.
|
||||||
/// should be a POST to [requestUri].
|
|
||||||
///
|
///
|
||||||
/// The form submission should include the values of [responseType], [clientID], [state], [scope]
|
/// 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.
|
/// 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 {
|
class AuthRedirectController extends ResourceController {
|
||||||
/// Creates a new instance of an [AuthRedirectController].
|
/// 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(
|
AuthRedirectController(
|
||||||
this.authServer, {
|
this.authServer, {
|
||||||
this.delegate,
|
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(
|
static final Response _unsupportedResponseTypeResponse = Response.badRequest(
|
||||||
body: "<h1>Error</h1><p>unsupported_response_type</p>",
|
body: "<h1>Error</h1><p>unsupported_response_type</p>",
|
||||||
)..contentType = ContentType.html;
|
)..contentType = ContentType.html;
|
||||||
|
|
||||||
/// A reference to the [AuthServer] used to grant authorization codes and access tokens.
|
/// 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;
|
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;
|
final bool allowsImplicit;
|
||||||
|
|
||||||
/// A randomly generated value the client can use to verify the origin of the redirect.
|
/// 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
|
/// This property is bound to the 'state' query parameter of the incoming request.
|
||||||
/// server have the same value for 'state' as passed in. This value is usually a randomly generated
|
/// It serves as a security measure to prevent cross-site request forgery (CSRF) attacks.
|
||||||
/// session identifier.
|
///
|
||||||
|
/// 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")
|
@Bind.query("state")
|
||||||
String? 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")
|
@Bind.query("response_type")
|
||||||
String? responseType;
|
String? responseType;
|
||||||
|
|
||||||
/// The client ID of the authenticating client.
|
/// 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")
|
@Bind.query("client_id")
|
||||||
String? clientID;
|
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;
|
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
|
/// 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
|
/// 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.
|
/// 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')
|
/// This method handles the OAuth 2.0 authorization process, responding with a redirect
|
||||||
/// or an access token ('token') along with the passed in 'state'. If this request fails,
|
/// that contains either an authorization code ('code') or an access token ('token')
|
||||||
/// the redirect URL will contain an 'error' instead of the authorization code or access 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.
|
/// This method is typically invoked by the login form returned from the GET to this controller.
|
||||||
@Operation.post()
|
@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
|
@override
|
||||||
APIRequestBody? documentOperationRequestBody(
|
APIRequestBody? documentOperationRequestBody(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -249,6 +376,20 @@ class AuthRedirectController extends ResourceController {
|
||||||
return body;
|
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
|
@override
|
||||||
List<APIParameter> documentOperationParameters(
|
List<APIParameter> documentOperationParameters(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -261,6 +402,29 @@ class AuthRedirectController extends ResourceController {
|
||||||
return params;
|
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
|
@override
|
||||||
Map<String, APIResponse> documentOperationResponses(
|
Map<String, APIResponse> documentOperationResponses(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -298,6 +462,24 @@ class AuthRedirectController extends ResourceController {
|
||||||
throw StateError("AuthRedirectController documentation failed.");
|
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
|
@override
|
||||||
Map<String, APIOperation> documentOperations(
|
Map<String, APIOperation> documentOperations(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -311,6 +493,27 @@ class AuthRedirectController extends ResourceController {
|
||||||
return ops;
|
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(
|
Response _redirectResponse(
|
||||||
String? inputUri,
|
String? inputUri,
|
||||||
String? clientStateOrNull, {
|
String? clientStateOrNull, {
|
||||||
|
|
|
@ -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';
|
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> {
|
abstract class AuthorizationParser<T> {
|
||||||
const AuthorizationParser();
|
const AuthorizationParser();
|
||||||
|
|
||||||
|
@ -7,11 +25,24 @@ abstract class AuthorizationParser<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a Bearer token from an Authorization header.
|
/// 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?> {
|
class AuthorizationBearerParser extends AuthorizationParser<String?> {
|
||||||
const AuthorizationBearerParser();
|
const AuthorizationBearerParser();
|
||||||
|
|
||||||
/// Parses a Bearer token from [authorizationHeader]. If the header is malformed or doesn't exist,
|
/// Parses a Bearer token from an Authorization header.
|
||||||
/// throws an [AuthorizationParserException]. Otherwise, returns the [String] representation of the bearer token.
|
|
||||||
///
|
///
|
||||||
/// For example, if the input to this method is "Bearer token" it would return 'token'.
|
/// 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.
|
/// 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.
|
/// See [AuthorizationBasicParser] for getting instances of this type.
|
||||||
class AuthBasicCredentials {
|
class AuthBasicCredentials {
|
||||||
/// The username of a Basic Authorization header.
|
/// The username of a Basic Authorization header.
|
||||||
|
@ -50,14 +101,38 @@ class AuthBasicCredentials {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a Basic Authorization header.
|
/// 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
|
class AuthorizationBasicParser
|
||||||
extends AuthorizationParser<AuthBasicCredentials> {
|
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();
|
const AuthorizationBasicParser();
|
||||||
|
|
||||||
/// Returns a [AuthBasicCredentials] containing the username and password
|
/// Parses a Basic Authorization header and returns [AuthBasicCredentials].
|
||||||
/// 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 ':'.
|
|
||||||
///
|
///
|
||||||
/// If [authorizationHeader] is malformed or null, throws an [AuthorizationParserException].
|
/// If [authorizationHeader] is malformed or null, throws an [AuthorizationParserException].
|
||||||
@override
|
@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 }
|
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 {
|
class AuthorizationParserException implements Exception {
|
||||||
AuthorizationParserException(this.reason);
|
AuthorizationParserException(this.reason);
|
||||||
|
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
@ -8,9 +17,10 @@ import 'package:crypto/crypto.dart';
|
||||||
|
|
||||||
/// A OAuth 2.0 authorization server.
|
/// A OAuth 2.0 authorization server.
|
||||||
///
|
///
|
||||||
/// An [AuthServer] is an implementation of an OAuth 2.0 authorization server. An authorization server
|
/// This class implements the core functionality of an OAuth 2.0 authorization server,
|
||||||
/// issues, refreshes and revokes access tokens. It also verifies previously issued tokens, as
|
/// including client management, token issuance, token refresh, and token verification.
|
||||||
/// well as client and resource owner credentials.
|
/// 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].
|
/// [AuthServer]s are typically used in conjunction with [AuthController] and [AuthRedirectController].
|
||||||
/// These controllers provide HTTP interfaces to the [AuthServer] for issuing and refreshing tokens.
|
/// 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 {
|
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(
|
AuthServer(
|
||||||
this.delegate, {
|
this.delegate, {
|
||||||
this.hashRounds = 1000,
|
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
|
/// It is preferable to use the implementation of [AuthServerDelegate] from 'package:conduit_core/managed_auth.dart'. See
|
||||||
/// [AuthServer] for more details.
|
/// [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;
|
final AuthServerDelegate delegate;
|
||||||
|
|
||||||
/// The number of hashing rounds performed by this instance when validating a password.
|
/// 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;
|
final int hashRounds;
|
||||||
|
|
||||||
/// The resulting key length of a password hash when generated by this instance.
|
/// 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;
|
final int hashLength;
|
||||||
|
|
||||||
/// The [Hash] function used by the PBKDF2 algorithm to generate password hashes by this instance.
|
/// 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;
|
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 =
|
final APISecuritySchemeOAuth2Flow documentedAuthorizationCodeFlow =
|
||||||
APISecuritySchemeOAuth2Flow.empty()..scopes = {};
|
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 =
|
final APISecuritySchemeOAuth2Flow documentedPasswordFlow =
|
||||||
APISecuritySchemeOAuth2Flow.empty()..scopes = {};
|
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 =
|
final APISecuritySchemeOAuth2Flow documentedImplicitFlow =
|
||||||
APISecuritySchemeOAuth2Flow.empty()..scopes = {};
|
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";
|
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
|
/// See [hashRounds], [hashLength] and [hashFunction] for more details. This method
|
||||||
/// invoke [auth.generatePasswordHash] with the above inputs.
|
/// 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.
|
/// [delegate] will store this client for future use.
|
||||||
Future addClient(AuthClient client) async {
|
Future addClient(AuthClient client) async {
|
||||||
|
@ -132,14 +247,14 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
|
||||||
return delegate.addClient(this, client);
|
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.
|
/// Returns null if none exists.
|
||||||
Future<AuthClient?> getClient(String clientID) async {
|
Future<AuthClient?> getClient(String clientID) async {
|
||||||
return delegate.getClient(this, clientID);
|
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].
|
/// Removes cached occurrences of [AuthClient] for [clientID].
|
||||||
/// Asks [delegate] to remove an [AuthClient] by its ID via [AuthServerDelegate.removeClient].
|
/// 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);
|
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]
|
/// All authorization codes and tokens for the [ResourceOwner] identified by [identifier]
|
||||||
/// will be revoked.
|
/// will be revoked.
|
||||||
|
@ -163,7 +278,7 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
|
||||||
await delegate.removeTokens(this, identifier);
|
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.
|
/// 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].
|
/// If credentials are not correct, it will throw the appropriate [AuthRequestError].
|
||||||
|
@ -230,7 +345,7 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
|
||||||
return token;
|
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.
|
/// 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.
|
/// 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.
|
/// Refreshes a valid [AuthToken] instance.
|
||||||
///
|
///
|
||||||
/// This method will refresh a [AuthToken] given the [AuthToken]'s [refreshToken] for a given client ID.
|
/// This method refreshes an existing [AuthToken] using its [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.
|
/// It coordinates with the instance's [delegate] to update the old token with a new access token
|
||||||
/// If not successful, it will throw an [AuthRequestError].
|
/// 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(
|
Future<AuthToken> refresh(
|
||||||
String? refreshToken,
|
String? refreshToken,
|
||||||
String clientID,
|
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.
|
/// 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
|
/// This method is part of the OAuth 2.0 Authorization Code flow. It authenticates a user
|
||||||
/// if the credentials are correct. If they are not correct, it will throw the
|
/// with their username and password for a specific client, and if successful, generates
|
||||||
/// appropriate [AuthRequestError].
|
/// 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(
|
Future<AuthCode> authenticateForCode(
|
||||||
String? username,
|
String? username,
|
||||||
String? password,
|
String? password,
|
||||||
|
@ -408,9 +573,35 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
|
||||||
|
|
||||||
/// Exchanges a valid authorization code for an [AuthToken].
|
/// Exchanges a valid authorization code for an [AuthToken].
|
||||||
///
|
///
|
||||||
/// If the authorization code has not expired, has not been used, matches the client ID,
|
/// This method is part of the OAuth 2.0 Authorization Code flow. It allows a client
|
||||||
/// and the client secret is correct, it will return a valid [AuthToken]. Otherwise,
|
/// to exchange a previously obtained authorization code for an access token.
|
||||||
/// it will throw an appropriate [AuthRequestError].
|
///
|
||||||
|
/// 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(
|
Future<AuthToken> exchange(
|
||||||
String? authCodeString,
|
String? authCodeString,
|
||||||
String clientID,
|
String clientID,
|
||||||
|
@ -474,6 +665,22 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
|
||||||
//////
|
//////
|
||||||
// APIDocumentable overrides
|
// 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
|
@override
|
||||||
void documentComponents(APIDocumentContext context) {
|
void documentComponents(APIDocumentContext context) {
|
||||||
final basic = APISecurityScheme.http("basic")
|
final basic = APISecurityScheme.http("basic")
|
||||||
|
@ -508,6 +715,27 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
|
||||||
/////
|
/////
|
||||||
// AuthValidator overrides
|
// 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
|
@override
|
||||||
List<APISecurityRequirement> documentRequirementsForAuthorizer(
|
List<APISecurityRequirement> documentRequirementsForAuthorizer(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
@ -529,6 +757,25 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
|
||||||
return [];
|
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
|
@override
|
||||||
FutureOr<Authorization> validate<T>(
|
FutureOr<Authorization> validate<T>(
|
||||||
AuthorizationParser<T> parser,
|
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(
|
Future<Authorization> _validateClientCredentials(
|
||||||
AuthBasicCredentials credentials,
|
AuthBasicCredentials credentials,
|
||||||
) async {
|
) async {
|
||||||
|
@ -574,6 +846,29 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
|
||||||
return Authorization(client.id, null, this, credentials: credentials);
|
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(
|
List<AuthScope>? _validatedScopes(
|
||||||
AuthClient client,
|
AuthClient client,
|
||||||
ResourceOwner authenticatable,
|
ResourceOwner authenticatable,
|
||||||
|
@ -611,6 +906,24 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
|
||||||
return validScopes;
|
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(
|
AuthToken _generateToken(
|
||||||
int? ownerID,
|
int? ownerID,
|
||||||
String clientID,
|
String clientID,
|
||||||
|
@ -635,6 +948,24 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
|
||||||
return token;
|
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(
|
AuthCode _generateAuthCode(
|
||||||
int? ownerID,
|
int? ownerID,
|
||||||
AuthClient client,
|
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) {
|
String randomStringOfLength(int length) {
|
||||||
const possibleCharacters =
|
const possibleCharacters =
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
|
@ -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:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
@ -8,8 +17,9 @@ import 'package:protevus_openapi/v3.dart';
|
||||||
|
|
||||||
/// A [Controller] that validates the Authorization header of a request.
|
/// 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
|
/// This class, Authorizer, is responsible for authenticating and authorizing incoming HTTP requests.
|
||||||
/// the next controller in the channel.
|
/// 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
|
/// 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.
|
/// 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 {
|
class Authorizer extends Controller {
|
||||||
/// Creates an instance of [Authorizer].
|
/// 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.:
|
/// 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.
|
/// 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)
|
/// Authorization: Basic base64(username:password)
|
||||||
Authorizer.basic(AuthValidator? validator)
|
Authorizer.basic(AuthValidator? validator)
|
||||||
|
@ -51,7 +65,9 @@ class Authorizer extends Controller {
|
||||||
|
|
||||||
/// Creates an instance of [Authorizer] with Bearer token parsing.
|
/// 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
|
/// Authorization: Bearer ap9ijlarlkz8jIOa9laweo
|
||||||
///
|
///
|
||||||
|
@ -65,12 +81,19 @@ class Authorizer extends Controller {
|
||||||
|
|
||||||
/// The validating authorization object.
|
/// The validating authorization object.
|
||||||
///
|
///
|
||||||
/// This object will check credentials parsed from the Authorization header and produce an
|
/// This property holds an instance of [AuthValidator] responsible for validating
|
||||||
/// [Authorization] object representing the authorization the credentials have. It may also
|
/// the credentials parsed from the Authorization header. It processes these
|
||||||
/// reject a request. This is typically an instance of [AuthServer].
|
/// 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;
|
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
|
/// 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.
|
/// 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.
|
/// an [AuthScope] and added to this list.
|
||||||
final List<AuthScope>? scopes;
|
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
|
/// The parser determines how to interpret the data in the Authorization header. Concrete subclasses
|
||||||
/// are [AuthorizationBasicParser] and [AuthorizationBearerParser].
|
/// are [AuthorizationBasicParser] and [AuthorizationBearerParser].
|
||||||
|
@ -87,6 +110,20 @@ class Authorizer extends Controller {
|
||||||
/// Once parsed, the parsed value is validated by [validator].
|
/// Once parsed, the parsed value is validated by [validator].
|
||||||
final AuthorizationParser parser;
|
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
|
@override
|
||||||
FutureOr<RequestOrResponse> handle(Request request) async {
|
FutureOr<RequestOrResponse> handle(Request request) async {
|
||||||
final authData = request.raw.headers.value(HttpHeaders.authorizationHeader);
|
final authData = request.raw.headers.value(HttpHeaders.authorizationHeader);
|
||||||
|
@ -121,6 +158,19 @@ class Authorizer extends Controller {
|
||||||
return request;
|
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) {
|
Response _responseFromParseException(AuthorizationParserException e) {
|
||||||
switch (e.reason) {
|
switch (e.reason) {
|
||||||
case AuthorizationParserExceptionReason.malformed:
|
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) {
|
void _addScopeRequirementModifier(Request request) {
|
||||||
// If a controller returns a 403 because of invalid scope,
|
// If a controller returns a 403 because of invalid scope,
|
||||||
// this Authorizer adds its required scope as well.
|
// 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
|
@override
|
||||||
void documentComponents(APIDocumentContext context) {
|
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);
|
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(
|
context.responses.register(
|
||||||
"InsufficientScope",
|
"InsufficientScope",
|
||||||
APIResponse(
|
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(
|
context.responses.register(
|
||||||
"InsufficientAccess",
|
"InsufficientAccess",
|
||||||
APIResponse(
|
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(
|
context.responses.register(
|
||||||
"MalformedAuthorizationHeader",
|
"MalformedAuthorizationHeader",
|
||||||
APIResponse(
|
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
|
@override
|
||||||
Map<String, APIOperation> documentOperations(
|
Map<String, APIOperation> documentOperations(
|
||||||
APIDocumentContext context,
|
APIDocumentContext context,
|
||||||
|
|
|
@ -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';
|
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 {
|
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);
|
AuthServerException(this.reason, this.client);
|
||||||
|
|
||||||
/// Returns a string suitable to be included in a query string or JSON response body
|
/// Converts an [AuthRequestError] enum value to its corresponding string representation.
|
||||||
/// to indicate the error during processing an OAuth 2.0 request.
|
///
|
||||||
|
/// 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) {
|
static String errorString(AuthRequestError error) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case AuthRequestError.invalidRequest:
|
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;
|
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;
|
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 {
|
String get reasonString {
|
||||||
return errorString(reason);
|
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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return "AuthServerException: $reason $client";
|
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
|
/// Auth endpoints will use this list of values to determine the response sent back
|
||||||
/// to a client upon a failed request.
|
/// to a client upon a failed request.
|
||||||
enum AuthRequestError {
|
enum AuthRequestError {
|
||||||
/// The request was invalid...
|
/// Represents an invalid request error.
|
||||||
///
|
///
|
||||||
/// The request is missing a required parameter, includes an
|
/// The request is missing a required parameter, includes an
|
||||||
/// unsupported parameter value (other than grant type),
|
/// unsupported parameter value (other than grant type),
|
||||||
|
@ -63,7 +139,7 @@ enum AuthRequestError {
|
||||||
/// client, or is otherwise malformed.
|
/// client, or is otherwise malformed.
|
||||||
invalidRequest,
|
invalidRequest,
|
||||||
|
|
||||||
/// The client was invalid...
|
/// Represents an invalid client error.
|
||||||
///
|
///
|
||||||
/// Client authentication failed (e.g., unknown client, no
|
/// Client authentication failed (e.g., unknown client, no
|
||||||
/// client authentication included, or unsupported
|
/// client authentication included, or unsupported
|
||||||
|
@ -77,7 +153,7 @@ enum AuthRequestError {
|
||||||
/// matching the authentication scheme used by the client.
|
/// matching the authentication scheme used by the client.
|
||||||
invalidClient,
|
invalidClient,
|
||||||
|
|
||||||
/// The grant was invalid...
|
/// Represents an invalid grant error.
|
||||||
///
|
///
|
||||||
/// The provided authorization grant (e.g., authorization
|
/// The provided authorization grant (e.g., authorization
|
||||||
/// code, resource owner credentials) or refresh token is
|
/// code, resource owner credentials) or refresh token is
|
||||||
|
@ -86,36 +162,81 @@ enum AuthRequestError {
|
||||||
/// another client.
|
/// another client.
|
||||||
invalidGrant,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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
|
invalidToken
|
||||||
}
|
}
|
||||||
|
|
|
@ -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_auth/auth.dart';
|
||||||
import 'package:protevus_http/http.dart';
|
import 'package:protevus_http/http.dart';
|
||||||
|
|
||||||
/// Represents an OAuth 2.0 client ID and secret pair.
|
/// 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.
|
/// Use the command line tool `conduit auth` to create instances of this type and store them to a database.
|
||||||
class AuthClient {
|
class AuthClient {
|
||||||
/// Creates an instance of [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]
|
/// This constructor creates an [AuthClient] with the given parameters.
|
||||||
/// 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.
|
|
||||||
///
|
///
|
||||||
/// If this client supports scopes, [allowedScopes] must contain a list of scopes that tokens may request when authorized
|
/// If this client supports scopes, [allowedScopes] must contain a list of scopes that tokens may request when authorized
|
||||||
/// by this client.
|
/// 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(
|
AuthClient(
|
||||||
String id,
|
String id,
|
||||||
String? hashedSecret,
|
String? hashedSecret,
|
||||||
|
@ -29,6 +41,16 @@ class AuthClient {
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Creates an instance of a public [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,
|
AuthClient.public(String id,
|
||||||
{List<AuthScope>? allowedScopes, String? redirectURI})
|
{List<AuthScope>? allowedScopes, String? redirectURI})
|
||||||
: this.withRedirectURI(
|
: this.withRedirectURI(
|
||||||
|
@ -41,7 +63,17 @@ class AuthClient {
|
||||||
|
|
||||||
/// Creates an instance of [AuthClient] that uses the authorization code grant flow.
|
/// 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(
|
AuthClient.withRedirectURI(
|
||||||
this.id,
|
this.id,
|
||||||
this.hashedSecret,
|
this.hashedSecret,
|
||||||
|
@ -52,28 +84,57 @@ class AuthClient {
|
||||||
this.allowedScopes = allowedScopes;
|
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;
|
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;
|
final String id;
|
||||||
|
|
||||||
/// The hashed secret of the client.
|
/// 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;
|
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].
|
/// This value may be null if the client is public. See [isPublic].
|
||||||
String? salt;
|
String? salt;
|
||||||
|
|
||||||
/// The redirection URI for authorization codes and/or tokens.
|
/// 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.
|
/// This value may be null if the client doesn't support the authorization code flow.
|
||||||
String? redirectURI;
|
String? redirectURI;
|
||||||
|
|
||||||
/// The list of scopes available when authorizing with this client.
|
/// 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
|
/// 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
|
/// has. This list contains all valid scopes for this client. If null, client does not support scopes
|
||||||
/// and all access tokens have same authorization.
|
/// and all access tokens have same authorization.
|
||||||
|
@ -87,32 +148,54 @@ class AuthClient {
|
||||||
}).toList();
|
}).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.
|
/// In application's that do not use authorization scopes, this will return false.
|
||||||
/// Otherwise, will return true.
|
/// Otherwise, will return true.
|
||||||
bool get supportsScopes => allowedScopes != null;
|
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) {
|
bool allowsScope(AuthScope scope) {
|
||||||
return allowedScopes
|
return allowedScopes
|
||||||
?.any((clientScope) => scope.isSubsetOrEqualTo(clientScope)) ??
|
?.any((clientScope) => scope.isSubsetOrEqualTo(clientScope)) ??
|
||||||
false;
|
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
|
/// 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.
|
/// their secret confidentially, i.e. JavaScript browser applications.
|
||||||
bool get isPublic => hashedSecret == null;
|
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
|
/// Confidential clients have a client secret that must be used when authenticating with
|
||||||
/// a client-authenticated request. Confidential clients are used when you can
|
/// 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.
|
/// be sure that the client secret cannot be viewed by anyone outside of the developer.
|
||||||
bool get isConfidential => hashedSecret != null;
|
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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return "AuthClient (${isPublic ? "public" : "confidental"}): $id $redirectURI";
|
return "AuthClient (${isPublic ? "public" : "confidental"}): $id $redirectURI";
|
||||||
|
@ -121,24 +204,67 @@ class AuthClient {
|
||||||
|
|
||||||
/// Represents an OAuth 2.0 token.
|
/// Represents an OAuth 2.0 token.
|
||||||
///
|
///
|
||||||
/// [AuthServerDelegate] and [AuthServer] will exchange OAuth 2.0
|
/// This class encapsulates the properties and functionality of an OAuth 2.0 token,
|
||||||
/// tokens through instances of this type.
|
/// 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.
|
/// See the `package:conduit_core/managed_auth` library for a concrete implementation of this type.
|
||||||
class AuthToken {
|
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;
|
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;
|
String? refreshToken;
|
||||||
|
|
||||||
/// The time this token was issued on.
|
/// 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;
|
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;
|
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;
|
String? type;
|
||||||
|
|
||||||
/// The identifier of the resource owner.
|
/// 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
|
/// 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
|
/// in an application. This value is the primary key or identifying value of those
|
||||||
/// instances.
|
/// 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;
|
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;
|
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;
|
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 {
|
bool get isExpired {
|
||||||
return expirationDate!.difference(DateTime.now().toUtc()).inSeconds <= 0;
|
return expirationDate!.difference(DateTime.now().toUtc()).inSeconds <= 0;
|
||||||
}
|
}
|
||||||
|
@ -182,37 +353,118 @@ class AuthToken {
|
||||||
|
|
||||||
/// Represents an OAuth 2.0 authorization code.
|
/// Represents an OAuth 2.0 authorization code.
|
||||||
///
|
///
|
||||||
/// [AuthServerDelegate] and [AuthServer] will exchange OAuth 2.0
|
/// This class encapsulates the properties and functionality of an OAuth 2.0 authorization code,
|
||||||
/// authorization codes through instances of this type.
|
/// 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.
|
/// See the conduit/managed_auth library for a concrete implementation of this type.
|
||||||
class AuthCode {
|
class AuthCode {
|
||||||
/// The actual one-time code used to exchange for tokens.
|
/// 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;
|
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;
|
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
|
/// 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
|
/// in an application. This value is the primary key or identifying value of those
|
||||||
/// instances.
|
/// instances.
|
||||||
int? resourceOwnerIdentifier;
|
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;
|
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;
|
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;
|
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;
|
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 {
|
bool get isExpired {
|
||||||
return expirationDate!.difference(DateTime.now().toUtc()).inSeconds <= 0;
|
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].
|
/// 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
|
/// 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
|
/// 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])
|
/// that the [Authorizer] obtained from an [AuthValidator] (typically an [AuthServer])
|
||||||
/// about the validity of the credentials in a request.
|
/// about the validity of the credentials in a request.
|
||||||
class Authorization {
|
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(
|
Authorization(
|
||||||
this.clientID,
|
this.clientID,
|
||||||
this.ownerID,
|
this.ownerID,
|
||||||
|
@ -234,34 +508,71 @@ class Authorization {
|
||||||
this.scopes,
|
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;
|
final String clientID;
|
||||||
|
|
||||||
/// The identifier for the owner of the resource, if provided.
|
/// The identifier for the owner of the resource, if provided.
|
||||||
///
|
///
|
||||||
/// If this instance refers to the authorization of a resource owner, this value will
|
/// This property represents the unique identifier of the resource owner associated
|
||||||
/// be its identifying value. For example, in an application where a 'User' is stored in a database,
|
/// with this authorization. In OAuth 2.0 terminology, the resource owner is typically
|
||||||
/// this value would be the primary key of that user.
|
/// 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.
|
/// If this authorization does not refer to a specific resource owner, this value will be null.
|
||||||
final int? ownerID;
|
final int? ownerID;
|
||||||
|
|
||||||
/// The [AuthValidator] that granted this permission.
|
/// 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;
|
final AuthValidator? validator;
|
||||||
|
|
||||||
/// Basic authorization credentials, if provided.
|
/// Basic authorization credentials, if provided.
|
||||||
///
|
///
|
||||||
/// If this instance represents the authorization header of a request with basic authorization credentials,
|
/// This property holds the parsed basic authorization credentials if they were
|
||||||
/// the parsed credentials will be available in this property. Otherwise, this value is null.
|
/// 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;
|
final AuthBasicCredentials? credentials;
|
||||||
|
|
||||||
/// The list of scopes this authorization has access to.
|
/// The list of scopes this authorization has access to.
|
||||||
///
|
///
|
||||||
/// If the access token used to create this instance has scope,
|
/// This property represents the set of permissions or access rights granted to this authorization.
|
||||||
/// those scopes will be available here. Otherwise, null.
|
/// 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;
|
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
|
/// This method checks each element in [scopes] for any that gives privileges
|
||||||
/// to access [scope].
|
/// 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
|
/// 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
|
/// 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
|
/// 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.
|
/// a one or more required scopes that a token must have to pass to the next controller.
|
||||||
class AuthScope {
|
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
|
/// A simple authorization scope string is a single keyword. Valid characters are
|
||||||
///
|
///
|
||||||
|
@ -320,6 +631,27 @@ class AuthScope {
|
||||||
return scope;
|
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) {
|
factory AuthScope._parse(String scopeString) {
|
||||||
if (scopeString.isEmpty) {
|
if (scopeString.isEmpty) {
|
||||||
throw FormatException(
|
throw FormatException(
|
||||||
|
@ -345,16 +677,28 @@ class AuthScope {
|
||||||
return AuthScope._(scopeString, segments, lastModifier);
|
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);
|
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.
|
/// See [AuthServerDelegate.getAllowedScopes] for more details.
|
||||||
static const List<AuthScope> any = [
|
static const List<AuthScope> any = [
|
||||||
AuthScope._("_scope:_constant:_marker", [], null)
|
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
|
/// 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
|
/// 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 = {};
|
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;
|
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.
|
/// Will always have a length of at least 1.
|
||||||
Iterable<String?> get segments => _segments.map((s) => s.name);
|
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;
|
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;
|
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;
|
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) {
|
static List<_AuthScopeSegment> _parseSegments(String scopeString) {
|
||||||
if (scopeString.isEmpty) {
|
if (scopeString.isEmpty) {
|
||||||
throw FormatException(
|
throw FormatException(
|
||||||
|
@ -435,7 +853,7 @@ class AuthScope {
|
||||||
return elements;
|
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`.
|
/// The scope `users:posts` is a subset of `users`.
|
||||||
///
|
///
|
||||||
|
@ -477,16 +895,42 @@ class AuthScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alias of [isSubsetOrEqualTo].
|
/// 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')
|
@Deprecated('Use AuthScope.isSubsetOrEqualTo() instead')
|
||||||
bool allowsScope(AuthScope incomingScope) => isSubsetOrEqualTo(incomingScope);
|
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
|
/// Parses an instance of this type from [scopeString] and invokes
|
||||||
/// [isSubsetOrEqualTo].
|
/// [isSubsetOrEqualTo].
|
||||||
bool allows(String scopeString) => isSubsetOrEqualTo(AuthScope(scopeString));
|
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) {
|
bool isExactlyScope(AuthScope scope) {
|
||||||
final incomingIterator = scope._segments.iterator;
|
final incomingIterator = scope._segments.iterator;
|
||||||
for (final segment in _segments) {
|
for (final segment in _segments) {
|
||||||
|
@ -506,18 +950,44 @@ class AuthScope {
|
||||||
return true;
|
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].
|
/// Parses an instance of this type from [scopeString] and invokes [isExactlyScope].
|
||||||
bool isExactly(String scopeString) {
|
bool isExactly(String scopeString) {
|
||||||
return isExactlyScope(AuthScope(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
|
@override
|
||||||
String toString() => _scopeString;
|
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 {
|
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) {
|
_AuthScopeSegment(String segment) {
|
||||||
final split = segment.split(".");
|
final split = segment.split(".");
|
||||||
if (split.length == 2) {
|
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;
|
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;
|
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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
if (modifier == null) {
|
if (modifier == null) {
|
||||||
|
|
|
@ -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 'dart:async';
|
||||||
import 'package:protevus_auth/auth.dart';
|
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
|
/// 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
|
/// 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 {
|
abstract class ResourceOwner {
|
||||||
/// The username of the resource owner.
|
/// The username of the resource owner.
|
||||||
///
|
///
|
||||||
/// This value must be unique amongst all resource owners. It is often an email address. This value
|
/// This property represents the unique identifier for a resource owner, typically used for authentication purposes.
|
||||||
/// is used by authenticating users to identify their account.
|
/// 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;
|
String? username;
|
||||||
|
|
||||||
/// The hashed password of this instance.
|
/// 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;
|
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;
|
String? salt;
|
||||||
|
|
||||||
/// A unique identifier of this resource owner.
|
/// A unique identifier of this resource owner.
|
||||||
///
|
///
|
||||||
/// This unique identifier is used by [AuthServer] to associate authorization codes and access tokens with
|
/// This property represents a unique identifier for the resource owner, typically
|
||||||
/// this resource owner.
|
/// 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;
|
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;
|
/// 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.
|
/// 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 {
|
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.
|
/// 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.
|
/// [server] is the [AuthServer] invoking this method.
|
||||||
FutureOr<ResourceOwner?> getResourceOwner(AuthServer server, String username);
|
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)
|
/// [client] must be returned by [getClient] after this method has been invoked, and until (if ever)
|
||||||
/// [removeClient] is invoked.
|
/// [removeClient] is invoked.
|
||||||
FutureOr addClient(AuthServer server, AuthClient client);
|
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.
|
/// 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].
|
/// [server] is the [AuthServer] requesting the [AuthClient].
|
||||||
FutureOr<AuthClient?> getClient(AuthServer server, String clientID);
|
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
|
/// 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
|
/// 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].
|
/// [server] is the [AuthServer] requesting the [AuthClient].
|
||||||
FutureOr removeClient(AuthServer server, String clientID);
|
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.
|
/// 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,
|
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].
|
/// [server] is the requesting [AuthServer]. [resourceOwnerID] is the [ResourceOwner.id].
|
||||||
FutureOr removeTokens(AuthServer server, int resourceOwnerID);
|
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
|
/// 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
|
/// 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.
|
/// This method is invoked when attempting to exchange an authorization code that has already granted a token.
|
||||||
FutureOr removeToken(AuthServer server, AuthCode grantedByCode);
|
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
|
/// [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
|
/// revoked via [removeToken] or [removeTokens], or until it has expired and can reasonably
|
||||||
|
@ -107,7 +170,7 @@ abstract class AuthServerDelegate {
|
||||||
/// is null.
|
/// is null.
|
||||||
FutureOr addToken(AuthServer server, AuthToken token, {AuthCode? issuedFrom});
|
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],
|
/// This method must must update an existing [AuthToken], found by [oldAccessToken],
|
||||||
/// with the values [newAccessToken], [newIssueDate] and [newExpirationDate].
|
/// with the values [newAccessToken], [newIssueDate] and [newExpirationDate].
|
||||||
|
@ -122,23 +185,23 @@ abstract class AuthServerDelegate {
|
||||||
DateTime? newExpirationDate,
|
DateTime? newExpirationDate,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Must store [code].
|
/// Stores an [AuthCode] in the system.
|
||||||
///
|
///
|
||||||
/// [code] must be accessible until its expiration date.
|
/// [code] must be accessible until its expiration date.
|
||||||
FutureOr addCode(AuthServer server, AuthCode code);
|
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].
|
/// This must return an instance of [AuthCode] where [AuthCode.code] matches [code].
|
||||||
/// Return null if no matching code.
|
/// Return null if no matching code.
|
||||||
FutureOr<AuthCode?> getCode(AuthServer server, String 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.
|
/// The [AuthCode.code] matching [code] must be deleted and no longer accessible.
|
||||||
FutureOr removeCode(AuthServer server, String? code);
|
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].
|
/// 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
|
/// That [ResourceOwner] is then restricted to only those scopes, even if the authenticating client would allow other scopes
|
||||||
|
|
|
@ -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:async';
|
||||||
|
|
||||||
import 'package:protevus_openapi/documentable.dart';
|
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_http/http.dart';
|
||||||
import 'package:protevus_openapi/v3.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
|
/// When an [Authorizer] processes a [Request], it invokes [validate], passing in the parsed Authorization
|
||||||
/// header of the [Request].
|
/// header of the [Request].
|
||||||
///
|
///
|
||||||
/// [AuthServer] implements this interface.
|
/// [AuthServer] implements this interface.
|
||||||
mixin AuthValidator {
|
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]
|
/// 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.
|
/// 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,
|
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.
|
/// 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.
|
/// The [Authorizer] must provide the [context] it was given to document the operations, itself and optionally a list of [scopes] required to pass it.
|
||||||
|
|
|
@ -13,7 +13,28 @@ import 'dart:mirrors';
|
||||||
import 'package:protevus_config/config.dart';
|
import 'package:protevus_config/config.dart';
|
||||||
import 'package:protevus_runtime/runtime.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 {
|
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
|
@override
|
||||||
Map<String, Object> compile(MirrorContext context) {
|
Map<String, Object> compile(MirrorContext context) {
|
||||||
return Map.fromEntries(
|
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
|
@override
|
||||||
void deflectPackage(Directory destinationDirectory) {
|
void deflectPackage(Directory destinationDirectory) {
|
||||||
final libFile = File.fromUri(
|
final libFile = File.fromUri(
|
||||||
destinationDirectory.uri.resolve("lib/").resolve("conduit_config.dart"),
|
destinationDirectory.uri.resolve("lib/").resolve("config.dart"),
|
||||||
);
|
);
|
||||||
final contents = libFile.readAsStringSync();
|
final contents = libFile.readAsStringSync();
|
||||||
libFile.writeAsStringSync(
|
libFile.writeAsStringSync(
|
||||||
contents.replaceFirst(
|
contents.replaceFirst(
|
||||||
"export 'package:conduit_config/src/compiler.dart';", ""),
|
"export 'package:protevus_config/src/compiler.dart';", ""),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,17 +14,90 @@ import 'package:protevus_runtime/runtime.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:yaml/yaml.dart';
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
/// Subclasses of [Configuration] read YAML strings and files, assigning values from the YAML document to properties
|
/// A base class for configuration management in Dart applications.
|
||||||
/// of an instance of this type.
|
///
|
||||||
|
/// [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 {
|
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();
|
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) {
|
Configuration.fromMap(Map<dynamic, dynamic> map) {
|
||||||
decode(map.map<String, dynamic>((k, v) => MapEntry(k.toString(), v)));
|
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) {
|
Configuration.fromString(String contents) {
|
||||||
final yamlMap = loadYaml(contents) as Map<dynamic, dynamic>?;
|
final yamlMap = loadYaml(contents) as Map<dynamic, dynamic>?;
|
||||||
final map =
|
final map =
|
||||||
|
@ -32,15 +105,29 @@ abstract class Configuration {
|
||||||
decode(map);
|
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.
|
/// [file] must contain valid YAML data.
|
||||||
Configuration.fromFile(File file) : this.fromString(file.readAsStringSync());
|
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 =>
|
ConfigurationRuntime get _runtime =>
|
||||||
RuntimeContext.current[runtimeType] as ConfigurationRuntime;
|
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.
|
/// Override this method to provide decoding behavior other than the default behavior.
|
||||||
void decode(dynamic value) {
|
void decode(dynamic value) {
|
||||||
|
@ -58,7 +145,8 @@ abstract class Configuration {
|
||||||
|
|
||||||
/// Validates this 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]
|
/// Override this method to perform validations on input data. Throw [ConfigurationException]
|
||||||
/// for invalid data.
|
/// for invalid data.
|
||||||
|
@ -67,6 +155,24 @@ abstract class Configuration {
|
||||||
_runtime.validate(this);
|
_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) {
|
static dynamic getEnvironmentOrValue(dynamic value) {
|
||||||
if (value is String && value.startsWith(r"$")) {
|
if (value is String && value.startsWith(r"$")) {
|
||||||
final envKey = value.substring(1);
|
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 {
|
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);
|
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);
|
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(
|
dynamic tryDecode(
|
||||||
Configuration configuration,
|
Configuration configuration,
|
||||||
String name,
|
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 {
|
enum ConfigurationItemAttributeType {
|
||||||
/// [Configuration] properties marked as [required] will throw an exception
|
/// Indicates that a configuration property is required.
|
||||||
/// if their source YAML doesn't contain a matching key.
|
///
|
||||||
|
/// 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,
|
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
|
/// [Configuration] properties marked as [optional] will be silently ignored
|
||||||
/// if their source YAML doesn't contain a matching key.
|
/// if their source YAML doesn't contain a matching key.
|
||||||
optional
|
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.
|
/// **NOTICE**: This will be removed in version 2.0.0.
|
||||||
/// To signify required or optional config you could do:
|
/// To signify required or optional config you could do:
|
||||||
|
@ -204,14 +394,83 @@ const ConfigurationItemAttribute requiredConfiguration =
|
||||||
const ConfigurationItemAttribute optionalConfiguration =
|
const ConfigurationItemAttribute optionalConfiguration =
|
||||||
ConfigurationItemAttribute._(ConfigurationItemAttributeType.optional);
|
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 {
|
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(
|
ConfigurationException(
|
||||||
this.configuration,
|
this.configuration,
|
||||||
this.message, {
|
this.message, {
|
||||||
this.keyPath = const [],
|
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(
|
ConfigurationException.missingKeys(
|
||||||
this.configuration,
|
this.configuration,
|
||||||
List<String> missingKeys, {
|
List<String> missingKeys, {
|
||||||
|
@ -219,17 +478,52 @@ class ConfigurationException {
|
||||||
}) : message =
|
}) : message =
|
||||||
"missing required key(s): ${missingKeys.map((s) => "'$s'").join(", ")}";
|
"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;
|
final Configuration configuration;
|
||||||
|
|
||||||
/// The reason for the exception.
|
/// 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;
|
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\]').
|
/// Either a string (adds '.name') or an int (adds '\[value\]').
|
||||||
final List<dynamic> keyPath;
|
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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
if (keyPath.isEmpty) {
|
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 {
|
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);
|
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;
|
final Type type;
|
||||||
|
|
||||||
/// The reason for the error.
|
/// 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;
|
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
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return "Invalid configuration type '$type'. $message";
|
return "Invalid configuration type '$type'. $message";
|
||||||
|
|
|
@ -10,17 +10,73 @@
|
||||||
import 'package:protevus_config/config.dart';
|
import 'package:protevus_config/config.dart';
|
||||||
|
|
||||||
/// A [Configuration] to represent a database connection configuration.
|
/// 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 {
|
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();
|
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();
|
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();
|
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();
|
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(
|
DatabaseConfiguration.withConnectionInfo(
|
||||||
this.username,
|
this.username,
|
||||||
this.password,
|
this.password,
|
||||||
|
@ -32,36 +88,102 @@ class DatabaseConfiguration extends Configuration {
|
||||||
|
|
||||||
/// The host of the database to connect to.
|
/// 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;
|
late String host;
|
||||||
|
|
||||||
/// The port of the database to connect to.
|
/// 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;
|
late int port;
|
||||||
|
|
||||||
/// The name of the database to connect to.
|
/// 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;
|
late String databaseName;
|
||||||
|
|
||||||
/// A username for authenticating to the database.
|
/// 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;
|
String? username;
|
||||||
|
|
||||||
/// A password for authenticating to the database.
|
/// 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;
|
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,
|
/// This flag is used for test suites that use a temporary database to run tests against,
|
||||||
/// dropping it after the tests are complete.
|
/// dropping it after the tests are complete.
|
||||||
/// This property is optional.
|
/// This property is optional.
|
||||||
bool isTemporary = false;
|
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
|
@override
|
||||||
void decode(dynamic value) {
|
void decode(dynamic value) {
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
|
@ -101,27 +223,80 @@ class DatabaseConfiguration extends Configuration {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [Configuration] to represent an external HTTP API.
|
/// 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 {
|
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();
|
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();
|
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();
|
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();
|
APIConfiguration.fromMap(super.yaml) : super.fromMap();
|
||||||
|
|
||||||
/// The base URL of the described API.
|
/// 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.
|
/// This property is required.
|
||||||
/// Example: https://external.api.com:80/resources
|
/// Example: https://external.api.com:80/resources
|
||||||
late String baseURL;
|
late String baseURL;
|
||||||
|
|
||||||
/// The client ID.
|
/// The client ID for API authentication.
|
||||||
///
|
///
|
||||||
/// This property is optional.
|
/// This property is optional.
|
||||||
String? clientID;
|
String? clientID;
|
||||||
|
|
||||||
/// The client secret.
|
/// The client secret for API authentication.
|
||||||
///
|
///
|
||||||
/// This property is optional.
|
/// This property is optional.
|
||||||
String? clientSecret;
|
String? clientSecret;
|
||||||
|
|
|
@ -7,10 +7,31 @@
|
||||||
* file that was distributed with this source code.
|
* 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 {
|
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);
|
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;
|
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;
|
final List<dynamic> keyPath;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,47 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:mirrors';
|
import 'dart:mirrors';
|
||||||
|
|
||||||
import 'package:protevus_config/config.dart';
|
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 {
|
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) {
|
MirrorTypeCodec(this.type) {
|
||||||
if (type.isSubtypeOf(reflectType(Configuration))) {
|
if (type.isSubtypeOf(reflectType(Configuration))) {
|
||||||
final klass = type as ClassMirror;
|
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;
|
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) {
|
dynamic _decodeValue(dynamic value) {
|
||||||
if (type.isSubtypeOf(reflectType(int))) {
|
if (type.isSubtypeOf(reflectType(int))) {
|
||||||
return _decodeInt(value);
|
return _decodeInt(value);
|
||||||
|
@ -50,6 +113,21 @@ class MirrorTypeCodec {
|
||||||
return value;
|
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) {
|
dynamic _decodeBool(dynamic value) {
|
||||||
if (value is String) {
|
if (value is String) {
|
||||||
return value == "true";
|
return value == "true";
|
||||||
|
@ -58,6 +136,21 @@ class MirrorTypeCodec {
|
||||||
return value as bool;
|
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) {
|
dynamic _decodeInt(dynamic value) {
|
||||||
if (value is String) {
|
if (value is String) {
|
||||||
return int.parse(value);
|
return int.parse(value);
|
||||||
|
@ -66,6 +159,21 @@ class MirrorTypeCodec {
|
||||||
return value as int;
|
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) {
|
Configuration _decodeConfig(dynamic object) {
|
||||||
final item = (type as ClassMirror).newInstance(Symbol.empty, []).reflectee
|
final item = (type as ClassMirror).newInstance(Symbol.empty, []).reflectee
|
||||||
as Configuration;
|
as Configuration;
|
||||||
|
@ -75,6 +183,26 @@ class MirrorTypeCodec {
|
||||||
return item;
|
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) {
|
List _decodeList(List value) {
|
||||||
final out = (type as ClassMirror).newInstance(const Symbol('empty'), [], {
|
final out = (type as ClassMirror).newInstance(const Symbol('empty'), [], {
|
||||||
const Symbol('growable'): true,
|
const Symbol('growable'): true,
|
||||||
|
@ -94,6 +222,27 @@ class MirrorTypeCodec {
|
||||||
return out;
|
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) {
|
Map<dynamic, dynamic> _decodeMap(Map value) {
|
||||||
final map =
|
final map =
|
||||||
(type as ClassMirror).newInstance(Symbol.empty, []).reflectee as Map;
|
(type as ClassMirror).newInstance(Symbol.empty, []).reflectee as Map;
|
||||||
|
@ -117,10 +266,36 @@ class MirrorTypeCodec {
|
||||||
return map;
|
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 {
|
String get expectedType {
|
||||||
return type.reflectedType.toString();
|
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 {
|
String get source {
|
||||||
if (type.isSubtypeOf(reflectType(int))) {
|
if (type.isSubtypeOf(reflectType(int))) {
|
||||||
return _decodeIntSource;
|
return _decodeIntSource;
|
||||||
|
@ -137,6 +312,20 @@ class MirrorTypeCodec {
|
||||||
return "return v;";
|
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 {
|
String get _decodeListSource {
|
||||||
final typeParam = MirrorTypeCodec(type.typeArguments.first);
|
final typeParam = MirrorTypeCodec(type.typeArguments.first);
|
||||||
return """
|
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 {
|
String get _decodeMapSource {
|
||||||
final typeParam = MirrorTypeCodec(type.typeArguments.last);
|
final typeParam = MirrorTypeCodec(type.typeArguments.last);
|
||||||
return """
|
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 {
|
String get _decodeConfigSource {
|
||||||
return """
|
return """
|
||||||
final item = $expectedType();
|
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 {
|
String get _decodeIntSource {
|
||||||
return """
|
return """
|
||||||
if (v is String) {
|
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 {
|
String get _decodeBoolSource {
|
||||||
return """
|
return """
|
||||||
if (v is String) {
|
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 {
|
class MirrorConfigurationProperty {
|
||||||
MirrorConfigurationProperty(this.property)
|
MirrorConfigurationProperty(this.property)
|
||||||
: codec = MirrorTypeCodec(property.type);
|
: codec = MirrorTypeCodec(property.type);
|
||||||
|
|
|
@ -8,21 +8,73 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:mirrors';
|
import 'dart:mirrors';
|
||||||
|
|
||||||
import 'package:protevus_config/config.dart';
|
import 'package:protevus_config/config.dart';
|
||||||
import 'package:protevus_runtime/runtime.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
|
class ConfigurationRuntimeImpl extends ConfigurationRuntime
|
||||||
implements SourceCompiler {
|
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) {
|
ConfigurationRuntimeImpl(this.type) {
|
||||||
// Should be done in the constructor so a type check could be run.
|
// Should be done in the constructor so a type check could be run.
|
||||||
properties = _collectProperties();
|
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;
|
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;
|
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
|
@override
|
||||||
void decode(Configuration configuration, Map input) {
|
void decode(Configuration configuration, Map input) {
|
||||||
final values = Map.from(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 {
|
String get decodeImpl {
|
||||||
final buf = StringBuffer();
|
final buf = StringBuffer();
|
||||||
|
|
||||||
|
@ -98,6 +168,22 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
|
||||||
return buf.toString();
|
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
|
@override
|
||||||
void validate(Configuration configuration) {
|
void validate(Configuration configuration) {
|
||||||
final configMirror = reflect(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() {
|
Map<String, MirrorConfigurationProperty> _collectProperties() {
|
||||||
final declarations = <VariableMirror>[];
|
final declarations = <VariableMirror>[];
|
||||||
|
|
||||||
|
@ -142,6 +245,22 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
|
||||||
return m;
|
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 {
|
String get validateImpl {
|
||||||
final buf = StringBuffer();
|
final buf = StringBuffer();
|
||||||
|
|
||||||
|
@ -170,6 +289,23 @@ class ConfigurationRuntimeImpl extends ConfigurationRuntime
|
||||||
return buf.toString();
|
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
|
@override
|
||||||
Future<String> compile(BuildContext ctx) async {
|
Future<String> compile(BuildContext ctx) async {
|
||||||
final directives = await ctx.getImportDirectives(
|
final directives = await ctx.getImportDirectives(
|
||||||
|
|
Loading…
Reference in a new issue