Files
arcane_implementations/interfaces/auth/amplify_cognito_interface.dart
T
2024-12-16 17:43:50 +01:00

319 lines
9.1 KiB
Dart

import "package:amplify_auth_cognito/amplify_auth_cognito.dart";
import "package:amplify_flutter/amplify_flutter.dart";
import "package:arcane_framework/arcane_framework.dart";
import "package:arcane_helper_utils/arcane_helper_utils.dart";
import "package:flutter/widgets.dart";
typedef Credentials = ({String email, String password});
class AmplifyInterface
with ArcaneAuthAccountRegistration, ArcaneAuthPasswordManagement
implements ArcaneAuthInterface {
AmplifyInterface._internal();
static bool _mocked = false;
static final ArcaneAuthInterface _instance = AmplifyInterface._internal();
static ArcaneAuthInterface get I => _instance;
AmplifyAuthCognito get _cognito => Amplify.Auth.getPlugin(
AmplifyAuthCognito.pluginKey,
);
Future<CognitoAuthSession?> get _session async {
try {
return await _cognito.fetchAuthSession();
} on AuthException {
return null;
}
}
@override
Future<bool> get isSignedIn =>
_session.then((value) => value?.isSignedIn == true);
@override
Future<String?> get accessToken => isSignedIn.then(
(loggedIn) => loggedIn
? _session.then(
(value) => value?.userPoolTokensResult.value.accessToken.raw,
)
: null,
);
@override
Future<String?> get refreshToken => isSignedIn.then(
(loggedIn) => loggedIn
? _session.then(
(value) => value?.userPoolTokensResult.value.refreshToken,
)
: null,
);
@override
Future<Result<void, String>> logout() async {
final result = await _cognito.signOut();
if (result is CognitoFailedSignOut) {
return Result.error(result.exception.message);
}
return Result.ok(null);
}
@override
Future<Result<void, String>> login<LoginInput>({
LoginInput? input,
Future<void> Function()? onLoggedIn,
}) async {
final bool alreadyLoggedIn = await isSignedIn;
if (alreadyLoggedIn) return Result.ok(null);
if (input == null) return Result.error("No input provided");
final credentials = input as ({String email, String password});
final String email = credentials.email;
final String password = credentials.password;
try {
final CognitoSignInResult result = await _cognito.signIn(
username: email,
password: password,
);
return await _handleSignInResult(result, email);
} on AuthException catch (e) {
return Result.error("Error signing in: ${e.message}");
} catch (e) {
return Result.error("Error signing in: $e");
}
}
Future<Result<void, String>> _handleSignInResult(
SignInResult result,
String email,
) async {
switch (result.nextStep.signInStep) {
case AuthSignInStep.confirmSignInWithSmsMfaCode:
final codeDeliveryDetails = result.nextStep.codeDeliveryDetails!;
return Result.error(_handleCodeDelivery(codeDeliveryDetails));
case AuthSignInStep.confirmSignInWithNewPassword:
return Result.error("Enter a new password to continue signing in");
case AuthSignInStep.confirmSignInWithCustomChallenge:
final parameters = result.nextStep.additionalInfo;
final prompt = parameters["prompt"]!;
return Result.error(prompt);
case AuthSignInStep.resetPassword:
final resetResult = await _cognito.resetPassword(
username: email,
);
return Result.error("Reset password result: $resetResult");
case AuthSignInStep.confirmSignUp:
// Resend the sign up code to the registered device.
final resendResult = await _cognito.resendSignUpCode(
username: email,
);
return Result.error(
_handleCodeDelivery(resendResult.codeDeliveryDetails),
);
case AuthSignInStep.done:
return Result.ok(null);
default:
Arcane.log(
"Sign-in failed",
level: Level.warning,
metadata: {
"result": result.toString(),
},
);
return Result.error(
"Unexpected sign-in result: ${result.nextStep.signInStep}",
);
}
}
String _handleCodeDelivery(AuthCodeDeliveryDetails codeDeliveryDetails) {
// TODO(any): localize this
return "A confirmation code has been sent to ${codeDeliveryDetails.destination}. "
"Please check your ${codeDeliveryDetails.deliveryMedium.name} for the code.";
}
@override
Future<Result<String, String>> resendVerificationCode<T>({T? input}) async {
try {
final String? email = input as String?;
if (email == null) return Result.error("No email address provided.");
final result = await _cognito.resendSignUpCode(
username: email.toLowerCase(),
);
final codeDeliveryDetails = result.codeDeliveryDetails;
final String returnValue = _handleCodeDelivery(codeDeliveryDetails);
return Result.ok(returnValue);
} on AuthException catch (e) {
return Result.error("Error resending verification code: ${e.message}");
}
}
@override
Future<Result<SignUpStep, String>> register<Credentials>({
Credentials? input,
}) async {
try {
if (input == null) {
return Result.error("Unable to create an account with no credentials");
}
final credentials = input as ({String email, String password});
final String email = credentials.email;
final String password = credentials.password;
final String accountEmail = email.toLowerCase();
final userAttributes = {
AuthUserAttributeKey.email: accountEmail,
};
final SignUpResult result = await _cognito.signUp(
username: accountEmail,
password: password,
options: SignUpOptions(
userAttributes: userAttributes,
),
);
if (result.nextStep.signUpStep == AuthSignUpStep.confirmSignUp) {
return Result.ok(SignUpStep.confirmSignUp);
}
return Result.ok(SignUpStep.done);
} on AuthException catch (e) {
return Result.error("Error signing up user: ${e.message}");
}
}
@override
Future<Result<bool, String>> confirmSignup({
String? username,
String? confirmationCode,
}) async {
if (username.isNullOrEmpty) {
return Result.error(
"Unable to confirm account due to an empty username.",
);
}
if (confirmationCode.isNullOrEmpty) {
return Result.error(
"Unable to confirm account due to an empty confirmation code.",
);
}
try {
final CognitoSignUpResult result = await _cognito.confirmSignUp(
username: username!.toLowerCase(),
confirmationCode: confirmationCode!,
);
return Result.ok(result.isSignUpComplete);
} on AuthException catch (e) {
return Result.error("Error confirming user: ${e.message}");
}
}
@override
Future<Result<bool, String>> resetPassword({
String? email,
String? newPassword,
String? code,
}) async {
try {
if (email.isNullOrEmpty) {
return Result.error("Email address is empty.");
}
late ResetPasswordResult result;
if (newPassword != null && code != null) {
result = await _cognito.confirmResetPassword(
username: email!,
newPassword: newPassword,
confirmationCode: code,
);
}
if (newPassword == null && code == null) {
result = await _cognito.resetPassword(
username: email!,
);
}
return Result.ok(result.isPasswordReset);
} on AuthException catch (e) {
return Result.error("Error resetting the password: ${e.message}");
}
}
@override
Future<void> init() async {
if (_mocked) return;
if (Amplify.isConfigured) return;
final plugin = AmplifyAuthCognito();
await Amplify.addPlugin(plugin);
await Amplify.configure(_amplifyconfig);
}
@visibleForTesting
static void setMocked() {
_mocked = true;
}
static final String _amplifyconfig = '''
{
"UserAgent": "aws-amplify-cli/2.0",
"Version": "1.0",
"auth": {
"plugins": {
"awsCognitoAuthPlugin": {
"IdentityManager": {
"Default": {}
},
"CredentialsProvider": {
"CognitoIdentity": {
"Default": {
"PoolId": "${EnvVar.cognitoPoolId.value}",
"Region": "${EnvVar.cognitoRegion.value}"
}
}
},
"CognitoUserPool": {
"Default": {
"PoolId": "${EnvVar.cognitoPoolId.value}",
"AppClientId": "${EnvVar.cognitoClientId.value}",
"Region": "${EnvVar.cognitoRegion.value}"
}
},
"Auth": {
"Default": {
"authenticationFlowType": "USER_SRP_AUTH",
"OAuth": {
"WebDomain": "${EnvVar.authUrl.value}",
"AppClientId": "${EnvVar.cognitoClientId.value}",
"SignInRedirectURI": "${EnvVar.redirectUri.value}",
"SignOutRedirectURI": "${EnvVar.redirectUri.value}",
"Scopes": [
"phone",
"email",
"openid",
"profile",
"aws.cognito.signin.user.admin"
]
}
}
}
}
}
}
}''';
}