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

Code before:
			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.AUTH_FAIL,
							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) {
			emitStatus('fail');

Review comment (bug):
I believe we should keep this as the general update action and link it to a revision to see the changed data. 

An alternative is we use something more generic (e.g. USER_SUSPENDED) as not every suspension is due to failed login attempts (e.g [users are suspended when their role is deleted](https://github.com/directus/directus/blob/89cd8775603ee068e87350928471f1f5d2af0695/api/src/services/roles.ts#L79)).

I would lean toward utilizing the update action so we are inline with the user suspension activity/revision created when the users role is deleted.

```suggestion
							action: Action.UPDATE,
```

Code after:
			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({
						service: 'authentication',
						reason: 'Rate limiter unreachable',