Repository: directus/directus
File: api/src/services/authentication.ts (TypeScript)

Code before:
			emitStatus('fail');
			await stall(STALL_TIME, timeStart);
			throw new InvalidCredentialsError();
		}

		const settingsService = new SettingsService({
			knex: this.knex,
			schema: this.schema,
		});

		const { auth_login_attempts: allowedAttempts } = await settingsService.readSingleton({
			fields: ['auth_login_attempts'],
		});

		if (allowedAttempts !== null) {
			loginAttemptsLimiter.points = allowedAttempts;

			try {
				await loginAttemptsLimiter.consume(user.id);
			} catch (error) {
				if (error instanceof RateLimiterRes && error.remainingPoints === 0) {
					await this.knex('directus_users').update({ status: 'suspended' }).where({ id: user.id });
					user.status = 'suspended';

					if (this.accountability) {
						await this.activityService.createOne({
							action: Action.UPDATE,
							user: user.id,
							ip: this.accountability.ip,
							user_agent: this.accountability.userAgent,
							origin: this.accountability.origin,
							collection: 'directus_users',
							item: user.id,
							comment: `User suspended after ${allowedAttempts} failed attempts`,
						});
					}

					// This means that new attempts after the user has been re-activated will be accepted
					await loginAttemptsLimiter.set(user.id, 0, 0);
				} else {
					throw new ServiceUnavailableError({
						service: 'authentication',
						reason: 'Rate limiter unreachable',
					});
				}
			}
		}

		try {
			await provider.login(clone(user), cloneDeep(updatedPayload));
		} catch (e) {

Review comment (suggestion):
As it is now a generic `UPDATE` action we should add an accompanying revisions record so auditor can see the data changed. [See sample usage](https://github.com/directus/directus/blob/5bdf3e6ed154d3e5d6f20db92a84b130cd529c40/api/src/services/items.ts#L346)

Code after:
		if (user?.status !== 'active' || user?.provider !== providerName) {
			emitStatus('fail');
			await stall(STALL_TIME, timeStart);
			throw new InvalidCredentialsError();
		}

		const settingsService = new SettingsService({
			knex: this.knex,
			schema: this.schema,
		});

		const { auth_login_attempts: allowedAttempts } = await settingsService.readSingleton({
			fields: ['auth_login_attempts'],
		});

		if (allowedAttempts !== null) {
			loginAttemptsLimiter.points = allowedAttempts;

			try {
				await loginAttemptsLimiter.consume(user.id);
			} catch (error) {
				if (error instanceof RateLimiterRes && error.remainingPoints === 0) {
					await this.knex('directus_users').update({ status: 'suspended' }).where({ id: user.id });
					user.status = 'suspended';

					if (this.accountability) {
						const activity = await this.activityService.createOne({
							action: Action.UPDATE,
							user: user.id,
							ip: this.accountability.ip,
							user_agent: this.accountability.userAgent,
							origin: this.accountability.origin,
							collection: 'directus_users',
							item: user.id,
						});

						const revisionsService = new RevisionsService({ knex: this.knex, schema: this.schema });

						await revisionsService.createOne({
							activity: activity,
							collection: 'directus_users',
							item: user.id,
							data: user,
							delta: { status: 'suspended' },
						});
					}

					// This means that new attempts after the user has been re-activated will be accepted
					await loginAttemptsLimiter.set(user.id, 0, 0);
				} else {
					throw new ServiceUnavailableError({