Updated auth

This commit is contained in:
thomashii 2021-09-29 15:40:27 +08:00
parent 7652c02183
commit 7cbbc686ba
31 changed files with 182 additions and 146 deletions

View file

@ -1,5 +1,11 @@
# Change Log # Change Log
## 4.1.1
* Changed `userId` field of `AuthToken` to String type
* Changed `serializer` return value to String type
* Changed `deserializer` input parameter to String type
## 4.1.0 ## 4.1.0
* Updated linter to `package:lints` * Updated linter to `package:lints`

View file

@ -18,9 +18,10 @@ Ensure you have read the [User Guide](https://angel3-docs.dukefirehawk.com/guide
```dart ```dart
configureServer(Angel app) async { configureServer(Angel app) async {
var auth = AngelAuth<User>(); var auth = AngelAuth<User>(
auth.serializer = ...; serializer: (user) => user.id ?? '',
auth.deserializer = ...; deserializer: (id) => fetchAUserByIdSomehow(id)
);
auth.strategies['local'] = LocalAuthStrategy(...); auth.strategies['local'] = LocalAuthStrategy(...);
// POST route to handle username+password // POST route to handle username+password

View file

@ -6,7 +6,7 @@ import 'package:angel3_framework/http.dart';
void main() async { void main() async {
var app = Angel(); var app = Angel();
var auth = AngelAuth<User>( var auth = AngelAuth<User>(
serializer: (user) => user.id, serializer: (user) => user.id ?? '',
deserializer: (id) => fetchAUserByIdSomehow(id)); deserializer: (id) => fetchAUserByIdSomehow(id));
// Middleware to decode JWT's and inject a user object... // Middleware to decode JWT's and inject a user object...
@ -31,7 +31,7 @@ class User {
String? id, username, password; String? id, username, password;
} }
Future<User> fetchAUserByIdSomehow(id) async { Future<User> fetchAUserByIdSomehow(String id) async {
// Fetch a user somehow... // Fetch a user somehow...
throw UnimplementedError(); throw UnimplementedError();
} }

View file

@ -32,41 +32,35 @@ class AuthToken {
SplayTreeMap.from({'alg': 'HS256', 'typ': 'JWT'}); SplayTreeMap.from({'alg': 'HS256', 'typ': 'JWT'});
String? ipAddress; String? ipAddress;
late DateTime issuedAt;
num lifeSpan; num lifeSpan;
dynamic userId; String userId;
late DateTime issuedAt;
Map<String, dynamic> payload = {}; Map<String, dynamic> payload = {};
AuthToken( AuthToken(
{this.ipAddress, {this.ipAddress,
this.lifeSpan = -1, this.lifeSpan = -1,
this.userId, required this.userId,
DateTime? issuedAt, DateTime? issuedAt,
Map payload = const {}}) { Map<String, dynamic>? payload}) {
this.issuedAt = issuedAt ?? DateTime.now(); this.issuedAt = issuedAt ?? DateTime.now();
if (payload != null) {
this.payload.addAll(payload.keys this.payload.addAll(payload.keys
.fold({}, ((out, k) => out?..[k.toString()] = payload[k])) ?? .fold({}, ((out, k) => out?..[k.toString()] = payload[k])) ??
{}); {});
/* }
this.payload.addAll(payload.keys.fold(
{},
((out, k) => out..[k.toString()] = payload[k])
as Map<String, dynamic>? Function(
Map<String, dynamic>?, dynamic)) ??
{});
*/
} }
factory AuthToken.fromJson(String jsons) => factory AuthToken.fromJson(String jsons) =>
AuthToken.fromMap(json.decode(jsons) as Map); AuthToken.fromMap(json.decode(jsons) as Map<String, dynamic>);
factory AuthToken.fromMap(Map data) { factory AuthToken.fromMap(Map<String, dynamic> data) {
return AuthToken( return AuthToken(
ipAddress: data['aud'].toString(), ipAddress: data['aud'].toString(),
lifeSpan: data['exp'] as num, lifeSpan: data['exp'] as num,
issuedAt: DateTime.parse(data['iat'].toString()), issuedAt: DateTime.parse(data['iat'].toString()),
userId: data['sub'], userId: data['sub'],
payload: data['pld'] as Map); payload: data['pld']);
} }
factory AuthToken.parse(String jwt) { factory AuthToken.parse(String jwt) {
@ -78,7 +72,8 @@ class AuthToken {
} }
var payloadString = decodeBase64(split[1]); var payloadString = decodeBase64(split[1]);
return AuthToken.fromMap(json.decode(payloadString) as Map); return AuthToken.fromMap(
json.decode(payloadString) as Map<String, dynamic>);
} }
factory AuthToken.validate(String jwt, Hmac hmac) { factory AuthToken.validate(String jwt, Hmac hmac) {
@ -100,7 +95,8 @@ class AuthToken {
message: 'JWT payload does not match hashed version.'); message: 'JWT payload does not match hashed version.');
} }
return AuthToken.fromMap(json.decode(payloadString) as Map); return AuthToken.fromMap(
json.decode(payloadString) as Map<String, dynamic>);
} }
String serialize(Hmac hmac) { String serialize(Hmac hmac) {
@ -111,7 +107,7 @@ class AuthToken {
return data + '.' + base64Url.encode(signature); return data + '.' + base64Url.encode(signature);
} }
Map toJson() { Map<String, dynamic> toJson() {
return _splayify({ return _splayify({
'iss': 'angel_auth', 'iss': 'angel_auth',
'aud': ipAddress, 'aud': ipAddress,
@ -123,7 +119,7 @@ class AuthToken {
} }
} }
SplayTreeMap _splayify(Map map) { Map<String, dynamic> _splayify(Map<String, dynamic> map) {
var data = {}; var data = {};
map.forEach((k, v) { map.forEach((k, v) {
data[k] = _splay(v); data[k] = _splay(v);
@ -131,11 +127,11 @@ SplayTreeMap _splayify(Map map) {
return SplayTreeMap.from(data); return SplayTreeMap.from(data);
} }
dynamic _splay(value) { dynamic _splay(dynamic value) {
if (value is Iterable) { if (value is Iterable) {
return value.map(_splay).toList(); return value.map(_splay).toList();
} else if (value is Map) { } else if (value is Map) {
return _splayify(value); return _splayify(value as Map<String, dynamic>);
} else { } else {
return value; return value;
} }

View file

@ -46,7 +46,7 @@ class ExternalAuthOptions {
/// * `client_id` /// * `client_id`
/// * `client_secret` /// * `client_secret`
/// * `redirect_uri` /// * `redirect_uri`
factory ExternalAuthOptions.fromMap(Map map) { factory ExternalAuthOptions.fromMap(Map<String, dynamic> map) {
var clientId = map['client_id']; var clientId = map['client_id'];
var clientSecret = map['client_secret']; var clientSecret = map['client_secret'];
if (clientId == null || clientSecret == null) { if (clientId == null || clientSecret == null) {
@ -55,8 +55,8 @@ class ExternalAuthOptions {
} }
return ExternalAuthOptions( return ExternalAuthOptions(
clientId: clientId as String, clientId: clientId,
clientSecret: clientSecret as String, clientSecret: clientSecret,
redirectUri: map['redirect_uri'], redirectUri: map['redirect_uri'],
scopes: map['scopes'] is Iterable scopes: map['scopes'] is Iterable
? ((map['scopes'] as Iterable).map((x) => x.toString())) ? ((map['scopes'] as Iterable).map((x) => x.toString()))

View file

@ -52,10 +52,10 @@ class AngelAuth<User> {
Map<String, AuthStrategy<User>> strategies = {}; Map<String, AuthStrategy<User>> strategies = {};
/// Serializes a user into a unique identifier associated only with one identity. /// Serializes a user into a unique identifier associated only with one identity.
FutureOr Function(User) serializer; FutureOr<String> Function(User) serializer;
/// Deserializes a unique identifier into its associated identity. In most cases, this is a user object or model instance. /// Deserializes a unique identifier into its associated identity. In most cases, this is a user object or model instance.
FutureOr<User> Function(Object) deserializer; FutureOr<User> Function(String) deserializer;
/// Fires the result of [deserializer] whenever a user signs in to the application. /// Fires the result of [deserializer] whenever a user signs in to the application.
Stream<User> get onLogin => _onLogin.stream; Stream<User> get onLogin => _onLogin.stream;
@ -200,6 +200,7 @@ class AngelAuth<User> {
/// String getUsername(User user) => user.name /// String getUsername(User user) => user.name
/// } /// }
/// ``` /// ```
/*
@deprecated @deprecated
Future decodeJwt(RequestContext req, ResponseContext res) async { Future decodeJwt(RequestContext req, ResponseContext res) async {
if (req.method == 'POST' && req.path == reviveTokenEndpoint) { if (req.method == 'POST' && req.path == reviveTokenEndpoint) {
@ -209,6 +210,7 @@ class AngelAuth<User> {
return true; return true;
} }
} }
*/
Future<_AuthResult<User>?> _decodeJwt( Future<_AuthResult<User>?> _decodeJwt(
RequestContext req, ResponseContext res) async { RequestContext req, ResponseContext res) async {
@ -431,7 +433,7 @@ class AngelAuth<User> {
req.accepts('application/json')) { req.accepts('application/json')) {
var user = hasExisting var user = hasExisting
? result ? result
: await deserializer((await serializer(result)) as Object); : await deserializer(await serializer(result));
_onLogin.add(user); _onLogin.add(user);
return {'data': user, 'token': jwt}; return {'data': user, 'token': jwt};
} }
@ -469,8 +471,8 @@ class AngelAuth<User> {
/// Log a user in on-demand. /// Log a user in on-demand.
Future loginById( Future loginById(
dynamic userId, RequestContext req, ResponseContext res) async { String userId, RequestContext req, ResponseContext res) async {
var user = await deserializer(userId as Object); var user = await deserializer(userId);
var token = var token =
AuthToken(userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip); AuthToken(userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
_apply(req, res, token, user); _apply(req, res, token, user);

View file

@ -38,8 +38,8 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
@override @override
Future<User?> authenticate(RequestContext req, ResponseContext res, Future<User?> authenticate(RequestContext req, ResponseContext res,
[AngelAuthOptions? options_]) async { [AngelAuthOptions? options]) async {
var options = options_ ?? AngelAuthOptions(); var _options = options ?? AngelAuthOptions();
User? verificationResult; User? verificationResult;
if (allowBasic) { if (allowBasic) {
@ -88,9 +88,9 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
if (verificationResult == null || if (verificationResult == null ||
(verificationResult is Map && verificationResult.isEmpty)) { (verificationResult is Map && verificationResult.isEmpty)) {
if (options.failureRedirect != null && if (_options.failureRedirect != null &&
options.failureRedirect!.isNotEmpty) { _options.failureRedirect!.isNotEmpty) {
await res.redirect(options.failureRedirect, code: 401); await res.redirect(_options.failureRedirect, code: 401);
return null; return null;
} }

View file

@ -1,6 +1,6 @@
name: angel3_auth name: angel3_auth
description: A complete authentication plugin for Angel3. Includes support for stateless JWT tokens, Basic Auth, and more. description: A complete authentication plugin for Angel3. Includes support for stateless JWT tokens, Basic Auth, and more.
version: 4.1.0 version: 4.1.1
homepage: https://angel3-framework.web.app/ homepage: https://angel3-framework.web.app/
repository: https://github.com/dukefirehawk/angel/tree/master/packages/auth repository: https://github.com/dukefirehawk/angel/tree/master/packages/auth
environment: environment:
@ -21,5 +21,5 @@ dev_dependencies:
test: ^1.17.4 test: ^1.17.4
lints: ^1.0.0 lints: ^1.0.0
#dependency_overrides: #dependency_overrides:
# angel3_container: # angel3_framework:
# path: ../container/angel_container # path: ../framework

View file

@ -73,7 +73,7 @@ void main() {
?.create({'username': 'jdoe1', 'password': 'password'}); ?.create({'username': 'jdoe1', 'password': 'password'});
auth = AngelAuth<User>( auth = AngelAuth<User>(
serializer: (u) => u.id, serializer: (u) => u.id ?? '',
deserializer: (id) async => deserializer: (id) async =>
await app.findService('users')?.read(id) as User); await app.findService('users')?.read(id) as User);
//auth.serializer = (u) => u.id; //auth.serializer = (u) => u.id;

View file

@ -8,7 +8,7 @@ import 'package:logging/logging.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
final AngelAuth<Map<String, String>> auth = AngelAuth<Map<String, String>>( final AngelAuth<Map<String, String>> auth = AngelAuth<Map<String, String>>(
serializer: (user) async => 1337, deserializer: (id) async => sampleUser); serializer: (user) async => '1337', deserializer: (id) async => sampleUser);
var headers = <String, String>{'accept': 'application/json'}; var headers = <String, String>{'accept': 'application/json'};
var localOpts = AngelAuthOptions<Map<String, String>>( var localOpts = AngelAuthOptions<Map<String, String>>(
failureRedirect: '/failure', successRedirect: '/success'); failureRedirect: '/failure', successRedirect: '/success');

View file

@ -11,7 +11,7 @@ void main() {
secureCookies: true, secureCookies: true,
cookieDomain: 'SECURE', cookieDomain: 'SECURE',
jwtLifeSpan: threeDays.inMilliseconds, jwtLifeSpan: threeDays.inMilliseconds,
serializer: (u) => u, serializer: (u) => u as String,
deserializer: (u) => u); deserializer: (u) => u);
setUp(() => defaultCookie = Cookie('a', 'b')); setUp(() => defaultCookie = Cookie('a', 'b'));

View file

@ -1,5 +1,9 @@
# Change Log # Change Log
## 4.1.0
* Updated linter to `package:lints`
## 4.0.2 ## 4.0.2
* Updated example * Updated example

View file

@ -1,21 +1,29 @@
MIT License (MIT) BSD 3-Clause License
Copyright (c) 2021 dukefirehawk.com Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy Redistribution and use in source and binary forms, with or without
of this software and associated documentation files (the "Software"), to deal modification, are permitted provided that the following conditions are met:
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 1. Redistributions of source code must retain the above copyright notice, this
copies or substantial portions of the Software. list of conditions and the following disclaimer.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 2. Redistributions in binary form must reproduce the above copyright notice,
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, this list of conditions and the following disclaimer in the documentation
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE and/or other materials provided with the distribution.
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 3. Neither the name of the copyright holder nor the names of its
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE contributors may be used to endorse or promote products derived from
SOFTWARE. this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -1,10 +1,9 @@
# Angel3 OAuth2 Handler # Angel3 OAuth2 Handler
[![version](https://img.shields.io/badge/pub-v4.0.2-brightgreen)](https://pub.dartlang.org/packages/angel3_auth_oauth2) ![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_auth_oauth2?include_prereleases)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety) ![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](<https://dart.dev/null-safety>)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion) [![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/master/packages/auth_oauth2/LICENSE)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/auth_oauth2/LICENSE)
Angel3 library for authenticating users with remote identity providers via OAuth2, i.e. Facebook, Google, Azure AD, etc. Angel3 library for authenticating users with remote identity providers via OAuth2, i.e. Facebook, Google, Azure AD, etc.

View file

@ -1,4 +1 @@
include: package:pedantic/analysis_options.yaml include: package:lints/recommended.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

@ -48,8 +48,8 @@ void main() async {
// Set up the authenticator plugin. // Set up the authenticator plugin.
var auth = AngelAuth<User>( var auth = AngelAuth<User>(
serializer: (user) async => user.id, serializer: (user) async => user.id ?? '',
deserializer: (id) => mappedUserService.read(id.toString()), deserializer: (id) => mappedUserService.read(id),
jwtKey: 'oauth2 example secret', jwtKey: 'oauth2 example secret',
allowCookie: false); allowCookie: false);
await app.configure(auth.configureServer); await app.configure(auth.configureServer);
@ -120,7 +120,7 @@ class User extends Model {
User({this.id, this.githubId}); User({this.id, this.githubId});
static User parse(Map map) => static User parse(Map<String, dynamic> map) =>
User(id: map['id'] as String?, githubId: map['github_id'] as int?); User(id: map['id'] as String?, githubId: map['github_id'] as int?);
static Map<String, dynamic> serialize(User user) => user.toJson(); static Map<String, dynamic> serialize(User user) => user.toJson();

View file

@ -1,15 +1,19 @@
name: angel3_auth_oauth2 name: angel3_auth_oauth2
version: 4.0.2 version: 4.1.0
description: Angel3 library for authenticating users with external identity providers via OAuth2. description: Angel3 library for authenticating users with external identity providers via OAuth2.
homepage: https://angel3-framework.web.app/ homepage: https://angel3-framework.web.app/
repository: https://github.com/dukefirehawk/angel/tree/angel3/packages/auth_oauth2 repository: https://github.com/dukefirehawk/angel/tree/master/packages/auth_oauth2
environment: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
angel3_auth: ^4.0.0 angel3_auth: ^4.1.0
angel3_framework: ^4.0.0 angel3_framework: ^4.2.0
http_parser: ^4.0.0 http_parser: ^4.0.0
oauth2: ^2.0.0 oauth2: ^2.0.0
dev_dependencies: dev_dependencies:
logging: ^1.0.1 logging: ^1.0.1
pedantic: ^1.11.0 lints: ^1.0.0
#dependency_overrides:
# angel3_auth:
# path: ../auth

View file

@ -5,7 +5,7 @@ import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:logging/logging.dart'; //import 'package:logging/logging.dart';
export 'package:angel3_http_exception/angel3_http_exception.dart'; export 'package:angel3_http_exception/angel3_http_exception.dart';
/// A function that configures an [Angel] client in some way. /// A function that configures an [Angel] client in some way.

View file

@ -25,6 +25,6 @@ dev_dependencies:
test: ^1.17.5 test: ^1.17.5
lints: ^1.0.0 lints: ^1.0.0
#dependency_overrides: #dependency_overrides:
# angel3_container: # angel3_auth:
# path: ../container/angel_container # path: ../auth

View file

@ -6,8 +6,8 @@ repository: https://github.com/dukefirehawk/angel/tree/angel3/packages/hot
environment: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
angel3_framework: ^4.1.0 angel3_framework: ^4.2.0
angel3_websocket: ^4.0.0 angel3_websocket: ^4.1.0
belatuk_html_builder: ^3.0.0 belatuk_html_builder: ^3.0.0
charcode: ^1.2.0 charcode: ^1.2.0
glob: ^2.0.1 glob: ^2.0.1
@ -19,3 +19,7 @@ dev_dependencies:
http: ^0.13.2 http: ^0.13.2
logging: ^1.0.1 logging: ^1.0.1
lints: ^1.0.0 lints: ^1.0.0
#dependency_overrides:
# angel3_websocket:
# path: ../websocket

View file

@ -17,3 +17,6 @@ dev_dependencies:
belatuk_pretty_logging: ^4.0.0 belatuk_pretty_logging: ^4.0.0
test: ^1.17.5 test: ^1.17.5
lints: ^1.0.0 lints: ^1.0.0
#dependency_overrides:
# angel3_auth:
# path: ../auth

View file

@ -6,11 +6,14 @@ repository: https://github.com/dukefirehawk/angel/tree/angel3/packages/sync
environment: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
angel3_framework: ^4.1.0 angel3_framework: ^4.2.0
angel3_websocket: ^4.0.0 angel3_websocket: ^4.1.0
belatuk_pub_sub: ^4.0.0 belatuk_pub_sub: ^4.0.0
stream_channel: ^2.1.0 stream_channel: ^2.1.0
dev_dependencies: dev_dependencies:
angel3_test: ^4.0.0 angel3_test: ^4.1.0
test: ^1.17.8 test: ^1.17.5
lints: ^1.0.0 lints: ^1.0.0
#dependency_overrides:
# angel3_websocket:
# path: ../websocket

View file

@ -1,5 +1,9 @@
# Change Log # Change Log
## 4.1.1
* Fixed NNBD issues
## 4.1.0 ## 4.1.0
* Updated linter to `package:lints` * Updated linter to `package:lints`

View file

@ -6,7 +6,7 @@ import 'package:angel3_websocket/server.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
Angel? app; Angel app;
late TestClient client; late TestClient client;
setUp(() async { setUp(() async {
@ -46,17 +46,16 @@ void main() {
create: (dynamic data, [params]) async => {'foo': 'bar'})); create: (dynamic data, [params]) async => {'foo': 'bar'}));
var ws = AngelWebSocket(app); var ws = AngelWebSocket(app);
await app!.configure(ws.configureServer); await app.configure(ws.configureServer);
app!.all('/ws', ws.handleRequest); app.all('/ws', ws.handleRequest);
app!.errorHandler = (e, req, res) => e.toJson(); app.errorHandler = (e, req, res) => e.toJson();
client = await connectTo(app!); client = await connectTo(app);
}); });
tearDown(() async { tearDown(() async {
await client.close(); await client.close();
app = null;
}); });
group('matchers', () { group('matchers', () {

View file

@ -1,5 +1,5 @@
name: angel3_test name: angel3_test
version: 4.1.0 version: 4.1.1
description: Testing utility library for the Angel3 framework. Use with package:test. description: Testing utility library for the Angel3 framework. Use with package:test.
homepage: https://angel3-framework.web.app/ homepage: https://angel3-framework.web.app/
repository: https://github.com/dukefirehawk/angel/tree/master/packages/test repository: https://github.com/dukefirehawk/angel/tree/master/packages/test
@ -19,7 +19,7 @@ dependencies:
dev_dependencies: dev_dependencies:
test: ^1.17.5 test: ^1.17.5
lints: ^1.0.0 lints: ^1.0.0
#dependency_overrides: dependency_overrides:
# angel3_container: angel3_websocket:
# path: ../container/angel_container path: ../websocket

View file

@ -6,7 +6,7 @@ import 'package:angel3_websocket/server.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
Angel? app; Angel app;
late TestClient client; late TestClient client;
setUp(() async { setUp(() async {
@ -48,17 +48,16 @@ void main() {
<String, dynamic>{'foo': 'bar'})); <String, dynamic>{'foo': 'bar'}));
var ws = AngelWebSocket(app); var ws = AngelWebSocket(app);
await app!.configure(ws.configureServer); await app.configure(ws.configureServer);
app!.all('/ws', ws.handleRequest); app.all('/ws', ws.handleRequest);
app!.errorHandler = (e, req, res) => e.toJson(); app.errorHandler = (e, req, res) => e.toJson();
client = await connectTo(app!, useZone: false); client = await connectTo(app, useZone: false);
}); });
tearDown(() async { tearDown(() async {
await client.close(); await client.close();
app = null;
}); });
group('matchers', () { group('matchers', () {

View file

@ -1,5 +1,10 @@
# Change Log # Change Log
## 4.1.1
* Fixed issue with type casting
* Changed `app` parameter of `AngelWebSocket` to non-nullable
## 4.1.0 ## 4.1.0
* Updated to use `package:belatuk_merge_map` * Updated to use `package:belatuk_merge_map`

View file

@ -40,17 +40,17 @@ class AngelWebSocket {
final StreamController<WebSocketContext> _onDisconnect = final StreamController<WebSocketContext> _onDisconnect =
StreamController<WebSocketContext>.broadcast(); StreamController<WebSocketContext>.broadcast();
final Angel? app; final Angel app;
/// If this is not `true`, then all client-side service parameters will be /// If this is not `true`, then all client-side service parameters will be
/// discarded, other than `params['query']`. /// discarded, other than `params['query']`.
final bool allowClientParams; final bool allowClientParams;
/// An optional whitelist of allowed client origins, or [:null:]. /// An optional whitelist of allowed client origins, or [:null:].
final List<String>? allowedOrigins; final List<String> allowedOrigins;
/// An optional whitelist of allowed client protocols, or [:null:]. /// An optional whitelist of allowed client protocols, or [:null:].
final List<String>? allowedProtocols; final List<String> allowedProtocols;
/// If `true`, then clients can authenticate their WebSockets by sending a valid JWT. /// If `true`, then clients can authenticate their WebSockets by sending a valid JWT.
final bool allowAuth; final bool allowAuth;
@ -93,8 +93,8 @@ class AngelWebSocket {
this.synchronizationChannel, this.synchronizationChannel,
this.serializer, this.serializer,
this.deserializer, this.deserializer,
this.allowedOrigins, this.allowedOrigins = const [],
this.allowedProtocols}) { this.allowedProtocols = const []}) {
serializer ??= json.encode; serializer ??= json.encode;
deserializer ??= (params) => params; deserializer ??= (params) => params;
} }
@ -161,7 +161,7 @@ class AngelWebSocket {
if (e.service.configuration.containsKey('ws:filter')) { if (e.service.configuration.containsKey('ws:filter')) {
return e.service.configuration['ws:filter'](e, socket); return e.service.configuration['ws:filter'](e, socket);
} else if (e.params != null && e.params!.containsKey('ws:filter')) { } else if (e.params != null && e.params!.containsKey('ws:filter')) {
return e.params!['ws:filter'](e, socket); return e.params?['ws:filter'](e, socket);
} else { } else {
return true; return true;
} }
@ -175,13 +175,15 @@ class AngelWebSocket {
Future<void> batchEvent(WebSocketEvent event, Future<void> batchEvent(WebSocketEvent event,
{Function(WebSocketContext socket)? filter, bool notify = true}) async { {Function(WebSocketContext socket)? filter, bool notify = true}) async {
// Default implementation will just immediately fire events // Default implementation will just immediately fire events
_clients.forEach((client) async { for (var client in _clients) {
dynamic result = true; dynamic result = true;
if (filter != null) result = await filter(client); if (filter != null) {
result = await filter(client);
}
if (result == true) { if (result == true) {
client.channel.sink.add((serializer ?? json.encode)(event.toJson())); client.channel.sink.add((serializer ?? json.encode)(event.toJson()));
} }
}); }
if (synchronizationChannel != null && notify != false) { if (synchronizationChannel != null && notify != false) {
synchronizationChannel!.sink.add(event); synchronizationChannel!.sink.add(event);
@ -200,11 +202,11 @@ class AngelWebSocket {
return null; return null;
} }
var service = app!.findService(split[0]); var service = app.findService(split[0]);
if (service == null) { if (service == null) {
socket.sendError(AngelHttpException.notFound( socket.sendError(AngelHttpException.notFound(
message: 'No service \"${split[0]}\" exists.')); message: 'No service "${split[0]}" exists.'));
return null; return null;
} }
@ -222,7 +224,7 @@ class AngelWebSocket {
var params = mergeMap<String, dynamic>([ var params = mergeMap<String, dynamic>([
(((deserializer ?? (params) => params)(action.params)) (((deserializer ?? (params) => params)(action.params))
as Map<String, dynamic>?)!, as Map<String, dynamic>),
{ {
'provider': Providers.websocket, 'provider': Providers.websocket,
'__requestctx': socket.request, '__requestctx': socket.request,
@ -257,7 +259,7 @@ class AngelWebSocket {
data: await service.remove(action.id, params)); data: await service.remove(action.id, params));
} else { } else {
socket.sendError(AngelHttpException.methodNotAllowed( socket.sendError(AngelHttpException.methodNotAllowed(
message: 'Method Not Allowed: \"$actionName\"')); message: 'Method Not Allowed: $actionName'));
return null; return null;
} }
} catch (e, st) { } catch (e, st) {
@ -278,7 +280,7 @@ class AngelWebSocket {
AuthToken token; AuthToken token;
token = AuthToken.validate(jwt, auth.hmac); token = AuthToken.validate(jwt, auth.hmac);
var user = await auth.deserializer(token.userId as Object); var user = await auth.deserializer(token.userId);
socket.request socket.request
..container!.registerSingleton<AuthToken>(token) ..container!.registerSingleton<AuthToken>(token)
..container!.registerSingleton(user, as: user.runtimeType); ..container!.registerSingleton(user, as: user.runtimeType);
@ -359,16 +361,16 @@ class AngelWebSocket {
// Send an error // Send an error
if (e is AngelHttpException) { if (e is AngelHttpException) {
socket.sendError(e); socket.sendError(e);
app?.logger?.severe(e.message, e.error ?? e, e.stackTrace); app.logger?.severe(e.message, e.error ?? e, e.stackTrace);
} else if (sendErrors) { } else if (sendErrors) {
var err = AngelHttpException(e, var err = AngelHttpException(e,
message: e.toString(), stackTrace: st, errors: [st.toString()]); message: e.toString(), stackTrace: st, errors: [st.toString()]);
socket.sendError(err); socket.sendError(err);
app?.logger?.severe(err.message, e, st); app.logger?.severe(err.message, e, st);
} else { } else {
var err = AngelHttpException(e); var err = AngelHttpException(e);
socket.sendError(err); socket.sendError(err);
app?.logger?.severe(e.toString(), e, st); app.logger?.severe(e.toString(), e, st);
} }
} }
@ -413,7 +415,7 @@ class AngelWebSocket {
/// Handles an incoming [WebSocketContext]. /// Handles an incoming [WebSocketContext].
Future<void> handleClient(WebSocketContext socket) async { Future<void> handleClient(WebSocketContext socket) async {
var origin = socket.request.headers?.value('origin'); var origin = socket.request.headers?.value('origin');
if (allowedOrigins != null && !allowedOrigins!.contains(origin)) { if (allowedOrigins.isNotEmpty && !allowedOrigins.contains(origin)) {
throw AngelHttpException.forbidden( throw AngelHttpException.forbidden(
message: message:
'WebSocket connections are not allowed from the origin "$origin".'); 'WebSocket connections are not allowed from the origin "$origin".');
@ -479,8 +481,8 @@ class AngelWebSocket {
throw AngelHttpException.badRequest( throw AngelHttpException.badRequest(
message: 'Missing `sec-websocket-key` header.'); message: 'Missing `sec-websocket-key` header.');
} else if (protocol != null && } else if (protocol != null &&
allowedProtocols != null && allowedProtocols.isNotEmpty &&
!allowedProtocols!.contains(protocol)) { !allowedProtocols.contains(protocol)) {
throw AngelHttpException.badRequest( throw AngelHttpException.badRequest(
message: 'Disallowed `sec-websocket-protocol` header "$protocol".'); message: 'Disallowed `sec-websocket-protocol` header "$protocol".');
} else { } else {

View file

@ -1,13 +1,13 @@
name: angel3_websocket name: angel3_websocket
version: 4.1.0 version: 4.1.1
description: This library provides WebSockets support for Angel3 framework. description: This library provides WebSockets support for Angel3 framework.
homepage: https://angel3-framework.web.app/ homepage: https://angel3-framework.web.app/
repository: https://github.com/dukefirehawk/angel/tree/master/packages/websocket repository: https://github.com/dukefirehawk/angel/tree/master/packages/websocket
environment: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
angel3_auth: ^4.0.0 angel3_auth: ^4.1.0
angel3_client: ^4.0.0 angel3_client: ^4.1.0
angel3_framework: ^4.2.0 angel3_framework: ^4.2.0
angel3_http_exception: ^3.0.0 angel3_http_exception: ^3.0.0
belatuk_merge_map: ^3.0.0 belatuk_merge_map: ^3.0.0
@ -22,7 +22,7 @@ dev_dependencies:
angel3_model: ^3.1.0 angel3_model: ^3.1.0
test: ^1.17.5 test: ^1.17.5
lints: ^1.0.0 lints: ^1.0.0
#dependency_overrides: dependency_overrides:
# angel3_container: angel3_auth:
# path: ../container/angel_container path: ../auth

View file

@ -9,7 +9,7 @@ import 'package:test/test.dart';
import 'common.dart'; import 'common.dart';
void main() { void main() {
srv.Angel? app; srv.Angel app;
late srv.AngelHttp http; late srv.AngelHttp http;
ws.WebSockets? client; ws.WebSockets? client;
srv.AngelWebSocket websockets; srv.AngelWebSocket websockets;
@ -18,17 +18,17 @@ void main() {
setUp(() async { setUp(() async {
app = srv.Angel(reflector: const MirrorsReflector()); app = srv.Angel(reflector: const MirrorsReflector());
http = srv.AngelHttp(app!, useZone: false); http = srv.AngelHttp(app, useZone: false);
websockets = srv.AngelWebSocket(app) websockets = srv.AngelWebSocket(app)
..onData.listen((data) { ..onData.listen((data) {
print('Received by server: $data'); print('Received by server: $data');
}); });
await app!.configure(websockets.configureServer); await app.configure(websockets.configureServer);
app!.all('/ws', websockets.handleRequest); app.all('/ws', websockets.handleRequest);
await app!.configure(GameController(websockets).configureServer); await app.configure(GameController(websockets).configureServer);
app!.logger = Logger('angel_auth')..onRecord.listen(print); app.logger = Logger('angel_auth')..onRecord.listen(print);
server = await http.startServer(); server = await http.startServer();
url = 'ws://${server!.address.address}:${server!.port}/ws'; url = 'ws://${server!.address.address}:${server!.port}/ws';
@ -53,7 +53,7 @@ void main() {
tearDown(() async { tearDown(() async {
await client!.close(); await client!.close();
await http.close(); await http.close();
app = null; //app = null;
client = null; client = null;
server = null; server = null;
url = null; url = null;

View file

@ -9,7 +9,7 @@ import 'package:test/test.dart';
import 'common.dart'; import 'common.dart';
void main() { void main() {
srv.Angel? app; srv.Angel app;
late srv.AngelHttp http; late srv.AngelHttp http;
ws.WebSockets? client; ws.WebSockets? client;
srv.AngelWebSocket websockets; srv.AngelWebSocket websockets;
@ -19,16 +19,16 @@ void main() {
setUp(() async { setUp(() async {
app = srv.Angel(reflector: MirrorsReflector()) app = srv.Angel(reflector: MirrorsReflector())
..use('/api/todos', TodoService()); ..use('/api/todos', TodoService());
http = srv.AngelHttp(app!, useZone: false); http = srv.AngelHttp(app, useZone: false);
websockets = srv.AngelWebSocket(app) websockets = srv.AngelWebSocket(app)
..onData.listen((data) { ..onData.listen((data) {
print('Received by server: $data'); print('Received by server: $data');
}); });
await app!.configure(websockets.configureServer); await app.configure(websockets.configureServer);
app!.all('/ws', websockets.handleRequest); app.all('/ws', websockets.handleRequest);
app!.logger = Logger('angel_auth')..onRecord.listen(print); app.logger = Logger('angel_auth')..onRecord.listen(print);
server = await http.startServer(); server = await http.startServer();
url = 'ws://${server!.address.address}:${server!.port}/ws'; url = 'ws://${server!.address.address}:${server!.port}/ws';
@ -51,7 +51,7 @@ void main() {
await client!.close(); await client!.close();
await http.server!.close(force: true); await http.server!.close(force: true);
app = null; //app = null;
client = null; client = null;
server = null; server = null;
url = null; url = null;