import 'package:charcode/ascii.dart'; import 'package:collection/collection.dart'; import 'package:quiver/core.dart'; import 'package:logging/logging.dart'; /// A common class containing parsing and validation logic for third-party authentication configuration. class ExternalAuthOptions { static final _log = Logger('VirtualDirectory'); /// The user's identifier, otherwise known as an "application id". final String clientId; /// The user's secret, other known as an "application secret". final String clientSecret; /// The user's redirect URI. final Uri redirectUri; /// The scopes to be passed to the external server. final Set scopes; ExternalAuthOptions._( this.clientId, this.clientSecret, this.redirectUri, this.scopes); factory ExternalAuthOptions( {required String clientId, required String clientSecret, required redirectUri, Iterable scopes = const []}) { if (redirectUri is String) { return ExternalAuthOptions._( clientId, clientSecret, Uri.parse(redirectUri), scopes.toSet()); } else if (redirectUri is Uri) { return ExternalAuthOptions._( clientId, clientSecret, redirectUri, scopes.toSet()); } else { _log.severe('RedirectUri is not valid'); throw ArgumentError.value( redirectUri, 'redirectUri', 'must be a String or Uri'); } } /// Returns a JSON-friendly representation of this object. /// /// Parses the following fields: /// * `client_id` /// * `client_secret` /// * `redirect_uri` factory ExternalAuthOptions.fromMap(Map map) { var clientId = map['client_id']; var clientSecret = map['client_secret']; if (clientId == null || clientSecret == null) { _log.severe('clientId or clientSecret is null'); throw ArgumentError('Invalid clientId and/or clientSecret'); } return ExternalAuthOptions( clientId: clientId, clientSecret: clientSecret, redirectUri: map['redirect_uri'], scopes: map['scopes'] is Iterable ? ((map['scopes'] as Iterable).map((x) => x.toString())) : [], ); } @override int get hashCode => hash4(clientId, clientSecret, redirectUri, scopes); @override bool operator ==(other) => other is ExternalAuthOptions && other.clientId == clientId && other.clientSecret == other.clientSecret && other.redirectUri == other.redirectUri && const SetEquality().equals(other.scopes, scopes); /// Creates a copy of this object, with the specified changes. ExternalAuthOptions copyWith( {String? clientId, String? clientSecret, redirectUri, Iterable scopes = const []}) { return ExternalAuthOptions( clientId: clientId ?? this.clientId, clientSecret: clientSecret ?? this.clientSecret, redirectUri: redirectUri ?? this.redirectUri, scopes: (scopes).followedBy(this.scopes), ); } /// Returns a JSON-friendly representation of this object. /// /// Contains the following fields: /// * `client_id` /// * `client_secret` /// * `redirect_uri` /// /// If [obscureSecret] is `true` (default), then the [clientSecret] will /// be replaced by the string ``. Map toJson({bool obscureSecret = true}) { return { 'client_id': clientId, 'client_secret': obscureSecret ? '' : clientSecret, 'redirect_uri': redirectUri.toString(), 'scopes': scopes.toList(), }; } /// Returns a [String] representation of this object. /// /// If [obscureText] is `true` (default), then the [clientSecret] will be /// replaced by asterisks in the output. /// /// If no [asteriskCount] is given, then the number of asterisks will equal the length of /// the actual [clientSecret]. @override String toString({bool obscureSecret = true, int? asteriskCount}) { String? secret; if (!obscureSecret) { secret = clientSecret; } else { var codeUnits = List.filled(asteriskCount ?? clientSecret.length, $asterisk); secret = String.fromCharCodes(codeUnits); } var b = StringBuffer('ExternalAuthOptions('); b.write('clientId=$clientId'); b.write(', clientSecret=$secret'); b.write(', redirectUri=$redirectUri'); b.write(', scopes=${scopes.toList()}'); b.write(')'); return b.toString(); } }