changeUsername method

Changes a username while preserving uniqueness atomically.

Implementation

@override
Future<AuthUsernameMutationResult> changeUsername(
  AuthUsernameChangeCommand command,
) => _serializeUsernameMutation(command.userId, () async {
  final user = _users._usersById[command.userId];
  final credential = _credentials._credentialsById[command.credentialId];
  if (user == null ||
      credential == null ||
      credential.userId != command.userId ||
      credential.identifier.contains('@')) {
    return const AuthUsernameMutationResult(
      status: AuthUsernameMutationStatus.notFound,
    );
  }
  if (authUserIsDisabled(user)) {
    return const AuthUsernameMutationResult(
      status: AuthUsernameMutationStatus.userUnavailable,
    );
  }
  final state = await _accountStates.find(user.id);
  if ((state?.disabled ?? false) || (state?.isLocked() ?? false)) {
    return const AuthUsernameMutationResult(
      status: AuthUsernameMutationStatus.userUnavailable,
    );
  }
  if (credential.identifier == command.username &&
      user.attributes['username'] == command.username) {
    return AuthUsernameMutationResult(
      status: AuthUsernameMutationStatus.unchanged,
      user: user,
      credential: credential,
    );
  }
  if (credential.identifier != command.expectedUsername ||
      user.attributes['username'] != command.expectedUsername) {
    return const AuthUsernameMutationResult(
      status: AuthUsernameMutationStatus.conflict,
    );
  }
  final conflictingId =
      _credentials._credentialIdsByIdentifier[command.username];
  if (conflictingId != null && conflictingId != credential.id) {
    return const AuthUsernameMutationResult(
      status: AuthUsernameMutationStatus.conflict,
    );
  }
  if (!_credentials._inFlightIdentifiers.add(command.username)) {
    return const AuthUsernameMutationResult(
      status: AuthUsernameMutationStatus.conflict,
    );
  }
  final updatedCredential = AuthPasswordCredential(
    id: credential.id,
    userId: credential.userId,
    identifier: command.username,
    passwordHash: credential.passwordHash,
    createdAt: credential.createdAt,
    updatedAt: command.updatedAt.toUtc(),
    enabled: credential.enabled,
  );
  final updatedUser = _usernameProjection(
    user,
    from: command.expectedUsername,
    to: command.username,
  );
  try {
    _credentials._credentialIdsByIdentifier.remove(credential.identifier);
    _credentials._credentialIdsByIdentifier[command.username] = credential.id;
    _credentials._credentialsById[credential.id] = updatedCredential;
    await _injectUsernameFault(
      AuthUsernameFaultPoint.changeAfterCredentialWrite,
    );
    _users._usersById[user.id] = updatedUser;
    if (user.email != null) _users._usersByEmail[user.email!] = updatedUser;
    await _injectUsernameFault(AuthUsernameFaultPoint.changeAfterUserWrite);
    return AuthUsernameMutationResult(
      status: AuthUsernameMutationStatus.changed,
      user: updatedUser,
      credential: updatedCredential,
    );
  } catch (_) {
    _credentials._credentialIdsByIdentifier.remove(command.username);
    _credentials._credentialIdsByIdentifier[credential.identifier] =
        credential.id;
    _credentials._credentialsById[credential.id] = credential;
    _users._usersById[user.id] = user;
    if (user.email != null) _users._usersByEmail[user.email!] = user;
    rethrow;
  } finally {
    _credentials._inFlightIdentifiers.remove(command.username);
  }
});