diff --git a/CHANGELOG.md b/CHANGELOG.md index d971b68..45c5da5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.0.29 + +- Improved `Json.toJson`. +- Added field `APIAuthentication.data`. +- `APISecurity`: added `getAuthenticationData`. + ## 1.0.28 - Added `API-INFO` path: describes the API routes. diff --git a/lib/src/bones_api_authentication.dart b/lib/src/bones_api_authentication.dart index f380166..8b807a5 100644 --- a/lib/src/bones_api_authentication.dart +++ b/lib/src/bones_api_authentication.dart @@ -5,6 +5,8 @@ import 'package:bones_api/src/bones_api_security.dart'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart' as crypto; +import 'bones_api_utils.dart'; + /// Represents a authentication credential. class APICredential { /// The username/email of this credential. @@ -161,8 +163,10 @@ class APIAuthentication { final bool resumed; + final dynamic data; + APIAuthentication(this.token, - {List? permissions, this.resumed = false}) + {List? permissions, this.resumed = false, this.data}) : permissions = List.unmodifiable(permissions ?? []); @@ -205,9 +209,12 @@ class APIAuthentication { enabledPermissionsOfType(type).firstOrNull; Map toJson() => { - 'token': token, - 'permissions': permissions, + 'token': token.toJson(), + if (permissions.isNotEmpty) + 'permissions': permissions.map((e) => e.toJson()).toList(), if (resumed) 'resumed': resumed, + if (data != null) + 'data': Json.toJson(data, maskField: Json.standardJsonMaskField), }; } diff --git a/lib/src/bones_api_base.dart b/lib/src/bones_api_base.dart index dc6d10a..dd783bf 100644 --- a/lib/src/bones_api_base.dart +++ b/lib/src/bones_api_base.dart @@ -16,7 +16,7 @@ import 'bones_api_utils.dart'; /// Root class of an API. abstract class APIRoot { // ignore: constant_identifier_names - static const String VERSION = '1.0.28'; + static const String VERSION = '1.0.29'; static final Map _instances = {}; diff --git a/lib/src/bones_api_config.dart b/lib/src/bones_api_config.dart index b709cdc..78d1f4b 100644 --- a/lib/src/bones_api_config.dart +++ b/lib/src/bones_api_config.dart @@ -407,12 +407,6 @@ class APIConfig { return 'APIConfig$src$json'; } - bool _maskField(f) { - var k = f.toLowerCase(); - return k.contains('pass') || - k.contains('passphrase') || - k.contains('pin') || - k.contains('secret') || - k.contains('token'); - } + bool _maskField(String key) => + Json.standardJsonMaskField(key, extraKeys: const {'token'}); } diff --git a/lib/src/bones_api_security.dart b/lib/src/bones_api_security.dart index 537a5d6..19218d2 100644 --- a/lib/src/bones_api_security.dart +++ b/lib/src/bones_api_security.dart @@ -50,15 +50,24 @@ abstract class APISecurity { return checkCredential(credential).then((ok) { if (!ok) return null; - - return getCredentialPermissions(credential) - .then((permissions) => createAuthentication(credential, permissions)); + return _resolveAuthentication(credential, false); }); } + FutureOr _resolveAuthentication( + APICredential credential, bool resumed) { + var permissionRet = getCredentialPermissions(credential); + var dataRet = getAuthenticationData(credential); + + return permissionRet.resolveOther( + dataRet, + (permissions, data) => createAuthentication(credential, permissions, + data: data, resumed: resumed)); + } + APIAuthentication createAuthentication( APICredential credential, List permissions, - [bool resumed = false]) { + {Object? data, bool resumed = false}) { APIToken? token; if (credential.token != null) { token = @@ -67,7 +76,8 @@ abstract class APISecurity { token ??= getValidToken(credential.username)!; - return APIAuthentication(token, permissions: permissions, resumed: resumed); + return APIAuthentication(token, + permissions: permissions, data: data, resumed: resumed); } FutureOr resumeAuthentication(APIToken? token) { @@ -78,8 +88,7 @@ abstract class APISecurity { return checkCredential(credential).then((ok) { if (!ok) return null; - return getCredentialPermissions(credential).then( - (permissions) => createAuthentication(credential, permissions, true)); + return _resolveAuthentication(credential, true); }); } @@ -186,6 +195,8 @@ abstract class APISecurity { FutureOr> getCredentialPermissions( APICredential credential); + FutureOr getAuthenticationData(APICredential credential) => null; + FutureOr authenticateByRequest(APIRequest request) { var credential = resolveRequestCredential(request); credential ??= resolveSessionCredential(request); diff --git a/lib/src/bones_api_utils.dart b/lib/src/bones_api_utils.dart index f212cfa..9b34503 100644 --- a/lib/src/bones_api_utils.dart +++ b/lib/src/bones_api_utils.dart @@ -5,51 +5,103 @@ import 'package:bones_api/bones_api.dart'; import 'package:collection/collection.dart'; import 'package:reflection_factory/reflection_factory.dart'; +typedef ToEncodableJson = Object? Function(Object? object); + +typedef JsonFieldMatcher = bool Function(String key); + /// JSON utility class. class Json { + /// A standard implementation of mask filed. + /// + /// - [extraKeys] is the extra keys to mask. + static bool standardJsonMaskField(String key, {Iterable? extraKeys}) { + key = key.trim().toLowerCase(); + return key == 'password' || + key == 'pass' || + key == 'passwordhash' || + key == 'passhash' || + key == 'passphrase' || + key == 'ping' || + key == 'secret' || + key == 'privatekey' || + key == 'pkey' || + (extraKeys != null && extraKeys.contains(key)); + } + /// Converts [o] to a JSON collection/data. /// - [maskField] when preset indicates if a field value should be masked with [maskText]. static T? toJson(Object? o, - {bool Function(String key)? maskField, + {JsonFieldMatcher? maskField, String maskText = '***', - Object? Function(dynamic object)? toEncodable}) { + JsonFieldMatcher? removeField, + ToEncodableJson? toEncodable}) { + return _valueToJson(o, maskField, maskText, removeField, toEncodable) as T; + } + + static Object? _valueToJson(o, JsonFieldMatcher? maskField, String maskText, + JsonFieldMatcher? removeField, ToEncodableJson? toEncodable) { if (o == null) { return null; + } else if (o is String || o is num || o is bool) { + return o; + } else if (o is DateTime) { + return _dateTimeToJson(o); + } else if (o is Map) { + return _mapToJson(o, maskField, maskText, removeField, toEncodable); + } else if (o is Set) { + return _iterableToJson(o, maskField, maskText, removeField, toEncodable) + .toSet(); + } else if (o is Iterable) { + return _iterableToJson(o, maskField, maskText, removeField, toEncodable) + .toList(); + } else { + return _entityToJson(o, toEncodable); } + } - if (o is String || o is num || o is bool) { - return o as T; - } + static Iterable _iterableToJson( + Iterable o, + JsonFieldMatcher? maskField, + String maskText, + JsonFieldMatcher? removeField, + ToEncodableJson? toEncodable) { + return o.map( + (e) => _valueToJson(e, maskField, maskText, removeField, toEncodable)); + } - if (o is DateTime) { - return _dateTimeToJson(o) as T; - } + static Map _mapToJson( + Map o, + JsonFieldMatcher? maskField, + String maskText, + JsonFieldMatcher? removeField, + ToEncodableJson? toEncodable) { + var oEntries = o.entries; - if (maskField != null) { - if (o is Map) { - o = _mapToJson(o, maskField, maskText); - } else if (o is Iterable) { - o = o.map((e) { - return e is Map ? _mapToJson(o as Map, maskField, maskText) : e; - }).toList(); - } + if (removeField != null) { + oEntries = oEntries.where((e) => !removeField(e.key)); } - return o as T; + var entries = oEntries.map((e) { + var key = e.key; + var value = _mapKeyValueToJson( + key, e.value, maskField, maskText, removeField, toEncodable); + return MapEntry(key, value); + }); + + return Map.fromEntries(entries); } static String _dateTimeToJson(DateTime o) { return o.toUtc().toString(); } - static Map _mapToJson( - Map o, bool Function(String key)? maskField, String maskText) { - return o.map((key, value) => - MapEntry(key, _mapKeyToJson(key, value, maskField, maskText))); - } - - static dynamic _mapKeyToJson(String k, dynamic o, - bool Function(String key)? maskField, String maskText) { + static Object? _mapKeyValueToJson( + String k, + dynamic o, + JsonFieldMatcher? maskField, + String maskText, + JsonFieldMatcher? removeField, + ToEncodableJson? toEncodable) { if (o == null) { return null; } @@ -61,27 +113,41 @@ class Json { } } - if (o is String || o is num || o is bool) { - return o; - } + return _valueToJson(o, maskField, maskText, removeField, toEncodable); + } - if (o is DateTime) { - return _dateTimeToJson(o); + static Object? _entityToJson(dynamic o, ToEncodableJson? toEncodable) { + if (toEncodable != null) { + try { + return toEncodable(o); + } catch (_) { + return _entityToJsonImpl(o); + } + } else { + return _entityToJsonImpl(o); } + } - if (o is Map) { - return o.map((key, value) => - MapEntry(key, _mapKeyToJson(key, value, maskField, maskText))); - } else if (o is Set) { - return o.map((e) => _mapKeyToJson(k, e, maskField, maskText)).toSet(); - } else if (o is Iterable) { - return o.map((e) => _mapKeyToJson(k, e, maskField, maskText)).toList(); - } else { + static Object? _entityToJsonImpl(dynamic o) { + var classReflection = + ReflectionFactory().getRegisterClassReflection(o.runtimeType); + + if (classReflection != null) { try { - return o.toJson(); + return classReflection.toJson(o); } catch (_) { - return '$o'; + return _entityToJsonDefault(o); } + } else { + return _entityToJsonDefault(o); + } + } + + static _entityToJsonDefault(dynamic o) { + try { + return o.toJson(); + } catch (_) { + return '$o'; } } @@ -91,15 +157,15 @@ class Json { /// - [toEncodable] converts a not encodable [Object] to a encodable JSON collection/data. See [dart_convert.JsonEncoder]. static String encode(Object? o, {bool pretty = false, - bool Function(String key)? maskField, + JsonFieldMatcher? maskField, String maskText = '***', Object? Function(dynamic object)? toEncodable}) { + var json = toJson(o, + maskField: maskField, maskText: maskText, toEncodable: toEncodable); if (pretty) { - return dart_convert.JsonEncoder.withIndent(' ').convert(toJson(o, - maskField: maskField, maskText: maskText, toEncodable: toEncodable)); + return dart_convert.JsonEncoder.withIndent(' ').convert(json); } else { - return dart_convert.json.encode(toJson(o, - maskField: maskField, maskText: maskText, toEncodable: toEncodable)); + return dart_convert.json.encode(json); } } diff --git a/pubspec.yaml b/pubspec.yaml index f65f0f6..445d6f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bones_api description: Bones_API - A Powerful API backend framework for Dart. Comes with a built-in HTTP Server, routes handler, entity handler, SQL translator, and DB adapters. -version: 1.0.28 +version: 1.0.29 homepage: https://github.com/Colossus-Services/bones_api environment: diff --git a/test/bones_api_utils_test.dart b/test/bones_api_utils_test.dart index 9a40ab1..a766736 100644 --- a/test/bones_api_utils_test.dart +++ b/test/bones_api_utils_test.dart @@ -5,12 +5,48 @@ import 'package:test/test.dart'; final _log = logging.Logger('bones_api_test'); +class Foo { + int id; + + String name; + + Foo(this.id, this.name); + + @override + String toString() { + return '#$id[$name]'; + } +} + void main() { _log.handler.logToConsole(); group('Utils', () { setUp(() {}); + test('Json.toJson', () async { + expect(Json.toJson(123), equals(123)); + expect(Json.toJson(DateTime.utc(2021, 1, 2, 3, 4, 5)), + equals('2021-01-02 03:04:05.000Z')); + + expect( + Json.toJson({'a': 1, 'b': 2, 'p': 123}, removeField: (k) => k == 'p'), + equals({'a': 1, 'b': 2})); + + expect( + Json.toJson({'a': 1, 'b': 2, 'p': 123}, maskField: (k) => k == 'p'), + equals({'a': 1, 'b': 2, 'p': '***'})); + + expect(Json.toJson({'a': 1, 'b': 2, 'foo': Foo(51, 'x')}), + equals({'a': 1, 'b': 2, 'foo': '#51[x]'})); + + expect( + Json.toJson({'a': 1, 'b': 2, 'foo': Foo(51, 'x')}, toEncodable: (o) { + return o is Foo ? '${o.id}:${o.name}' : o; + }), + equals({'a': 1, 'b': 2, 'foo': '51:x'})); + }); + test('Json.encode', () async { expect(Json.encode({'a': 1, 'b': 2}), equals('{"a":1,"b":2}'));