startAccountCreation static method

Future<({UuidValue? accountRequestId, EmailAccountRequestResult result})> startAccountCreation(
  1. Session session, {
  2. required String email,
  3. required String password,
  4. Transaction? transaction,
})

Returns the result of the operation and a process ID for the account request.

An account request is only created if the result is EmailAccountRequestResult.accountRequestCreated. In all other cases accountRequestId will be null.

The caller should ensure that the actual result does not leak to the outside client. Instead clients generally should always see a message like "If this email was not registered already, a new account has been created and a verification email has been sent". This prevents the endpoint from being misused to scan for registered/valid email addresses.

The caller might decide to initiate a password reset (via email, not in the client response), to help users which try to register but already have an account.

In the success case of EmailAccountRequestResult.accountRequestCreated, the caller may store additional information attached to the accountRequestId, which will be returned from verifyAccountCreation later on.

Implementation

static Future<
    ({
      EmailAccountRequestResult result,
      UuidValue? accountRequestId,
    })> startAccountCreation(
  final Session session, {
  required String email,
  required final String password,
  final Transaction? transaction,
}) async {
  if (!EmailAccounts.config.passwordValidationFunction(password)) {
    throw EmailAccountPasswordPolicyViolationException();
  }

  return DatabaseUtil.runInTransactionOrSavepoint(
    session.db,
    transaction,
    (final transaction) async {
      email = email.trim().toLowerCase();

      final existingAccountCount = await EmailAccount.db.count(
        session,
        where: (final t) => t.email.equals(email),
        transaction: transaction,
      );
      if (existingAccountCount > 0) {
        return (
          result: EmailAccountRequestResult.emailAlreadyRegistered,
          accountRequestId: null,
        );
      }

      final verificationCode =
          EmailAccounts.config.registrationVerificationCodeGenerator();

      final pendingAccountRequest = await EmailAccountRequest.db.findFirstRow(
        session,
        where: (final t) => t.email.equals(email),
        transaction: transaction,
      );
      if (pendingAccountRequest != null) {
        if (pendingAccountRequest.createdAt.isBefore(clock.now().subtract(
              EmailAccounts.config.registrationVerificationCodeLifetime,
            ))) {
          await EmailAccountRequest.db.deleteRow(
            session,
            pendingAccountRequest,
            transaction: transaction,
          );
        } else {
          return (
            result: EmailAccountRequestResult.emailAlreadyRequested,
            accountRequestId: null,
          );
        }
      }

      final passwordHash = await EmailAccountSecretHash.createHash(
        value: password,
      );
      final verificationCodeHash = await EmailAccountSecretHash.createHash(
        value: verificationCode,
      );

      final emailAccountRequest = await EmailAccountRequest.db.insertRow(
        session,
        EmailAccountRequest(
          email: email,
          passwordHash: passwordHash.hash.asByteData,
          passwordSalt: passwordHash.salt.asByteData,
          verificationCodeHash: verificationCodeHash.hash.asByteData,
          verificationCodeSalt: verificationCodeHash.salt.asByteData,
        ),
        transaction: transaction,
      );

      EmailAccounts.config.sendRegistrationVerificationCode?.call(
        session,
        email: email,
        accountRequestId: emailAccountRequest.id!,
        verificationCode: verificationCode,
        transaction: transaction,
      );

      return (
        result: EmailAccountRequestResult.accountRequestCreated,
        accountRequestId: emailAccountRequest.id!,
      );
    },
  );
}