@hed-hog/core 0.0.87 → 0.0.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,17 +6,53 @@ import { Cron, CronExpression } from '@nestjs/schedule';
6
6
  export class TasksService {
7
7
  private readonly logger = new Logger(TasksService.name);
8
8
 
9
- constructor(private readonly prismaService: PrismaService) {
10
- this.clear();
11
- }
9
+ constructor(private readonly prismaService: PrismaService) {}
12
10
 
11
+ /**
12
+ * Executa todas as tarefas de limpeza do banco de dados diariamente à meia-noite.
13
+ *
14
+ * As tarefas são executadas sequencialmente para evitar contenção excessiva no banco.
15
+ * Cada tarefa possui logging detalhado para monitoramento e troubleshooting.
16
+ */
13
17
  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
14
18
  async clear() {
15
19
  this.logger.verbose('++++++++++++++++++++++++++++++++++');
16
20
  this.logger.verbose('Running scheduled cleanup tasks');
17
21
  this.logger.verbose('++++++++++++++++++++++++++++++++++');
22
+
23
+ await this.clearExpiredMfaChallenges();
24
+ this.logger.verbose('++++++++++++++++++++++++++++++++++');
25
+
26
+ await this.clearExpiredSessions();
27
+ this.logger.verbose('++++++++++++++++++++++++++++++++++');
28
+
29
+ await this.clearExpiredUserIdentifierChallenges();
30
+ this.logger.verbose('++++++++++++++++++++++++++++++++++');
31
+
32
+ await this.clearUnverifiedMfaWithExpiredChallenges();
33
+ this.logger.verbose('++++++++++++++++++++++++++++++++++');
34
+
35
+ this.logger.verbose('Finished scheduled cleanup tasks');
36
+ this.logger.verbose('++++++++++++++++++++++++++++++++++');
37
+ }
38
+
39
+ /**
40
+ * Limpa challenges de autenticação multifator (MFA) que já expiraram ou foram verificados.
41
+ *
42
+ * Remove registros da tabela user_mfa_challenge que:
43
+ * - Expiraram (expires_at < now())
44
+ * - Já foram verificados (verified_at IS NOT NULL)
45
+ *
46
+ * Por quê: Challenges expirados ou já utilizados não têm mais utilidade e ocupam
47
+ * espaço desnecessário no banco de dados. A remoção periódica mantém a performance
48
+ * das consultas e reduz o tamanho do banco.
49
+ *
50
+ * Limite: 20.000 registros por execução para evitar lock prolongado da tabela.
51
+ */
52
+ private async clearExpiredMfaChallenges(): Promise<void> {
18
53
  this.logger.verbose('Starting cleanup of expired/verified MFA challenges');
19
- let startAt = Date.now();
54
+ const startAt = Date.now();
55
+
20
56
  await this.prismaService.$queryRaw`
21
57
  WITH del AS (
22
58
  SELECT user_mfa_id
@@ -30,27 +66,71 @@ export class TasksService {
30
66
  USING del
31
67
  WHERE c.user_mfa_id = del.user_mfa_id;
32
68
  `;
33
- this.logger.verbose(`Expired/verified MFA challenges cleaned up in ${Date.now() - startAt}ms`);
34
- this.logger.verbose('++++++++++++++++++++++++++++++++++');
69
+
70
+ this.logger.verbose(
71
+ `Expired/verified MFA challenges cleaned up in ${Date.now() - startAt}ms`,
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Limpa sessões de usuário expiradas e antigas.
77
+ *
78
+ * Remove registros da tabela user_session que:
79
+ * - Expiraram (expires_at < now())
80
+ * - Foram criadas há mais de 90 dias (created_at < now() - 90 days)
81
+ *
82
+ * Por quê: Sessões expiradas não podem mais ser utilizadas para autenticação.
83
+ * O critério adicional de 90 dias garante retenção de dados para auditoria/análise
84
+ * antes da exclusão definitiva. Isso permite investigar padrões de uso e
85
+ * possíveis problemas de segurança dentro de uma janela razoável.
86
+ *
87
+ * Limite: 20.000 registros por execução para evitar lock prolongado da tabela.
88
+ */
89
+ private async clearExpiredSessions(): Promise<void> {
35
90
  this.logger.verbose('Starting cleanup of expired sessions');
36
- startAt = Date.now();
91
+ const startAt = Date.now();
92
+
37
93
  await this.prismaService.$queryRaw`
38
94
  WITH del AS (
39
- SELECT id
40
- FROM user_session
41
- WHERE expires_at < now()
42
- AND created_at < (now() - interval '90 days')
43
- ORDER BY id
44
- LIMIT 20000
95
+ SELECT id
96
+ FROM user_session
97
+ WHERE expires_at < now()
98
+ AND created_at < (now() - interval '90 days')
99
+ ORDER BY id
100
+ LIMIT 20000
45
101
  )
46
102
  DELETE FROM user_session s
47
103
  USING del
48
104
  WHERE s.id = del.id;
49
105
  `;
50
- this.logger.verbose(`Expired sessions cleaned up in ${Date.now() - startAt}ms`);
51
- this.logger.verbose('++++++++++++++++++++++++++++++++++');
106
+
107
+ this.logger.verbose(
108
+ `Expired sessions cleaned up in ${Date.now() - startAt}ms`,
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Limpa challenges de verificação de identificadores de usuário (email, telefone, etc.)
114
+ * que expiraram e não foram verificados, removendo também os identificadores órfãos.
115
+ *
116
+ * Remove em cascata:
117
+ * 1. user_identifier_challenge: challenges expirados não verificados
118
+ * 2. user_identifier: identificadores que nunca foram verificados e perderam seus challenges
119
+ *
120
+ * Por quê: Quando um usuário tenta adicionar um novo email/telefone, é gerado um challenge
121
+ * para verificação. Se o usuário não verifica dentro do prazo, tanto o challenge quanto
122
+ * o identificador não verificado devem ser removidos para evitar:
123
+ * - Acúmulo de dados órfãos (identificadores que nunca serão usados)
124
+ * - Bloqueio indevido de emails/telefones (um email não verificado não deve impedir
125
+ * outro usuário de cadastrá-lo)
126
+ * - Confusão na interface (mostrar identificadores pendentes indefinidamente)
127
+ *
128
+ * Limite: 20.000 registros por execução para evitar lock prolongado das tabelas.
129
+ */
130
+ private async clearExpiredUserIdentifierChallenges(): Promise<void> {
52
131
  this.logger.verbose('Starting cleanup of expired user identifier challenges');
53
- startAt = Date.now();
132
+ const startAt = Date.now();
133
+
54
134
  await this.prismaService.$queryRaw`
55
135
  WITH expired_challenges AS (
56
136
  SELECT user_identifier_id
@@ -62,7 +142,8 @@ export class TasksService {
62
142
  deleted_challenges AS (
63
143
  DELETE FROM user_identifier_challenge c
64
144
  USING expired_challenges
65
- WHERE c.user_identifier_id = expired_challenges.user_identifier_id AND c.verified_at IS NULL
145
+ WHERE c.user_identifier_id = expired_challenges.user_identifier_id
146
+ AND c.verified_at IS NULL
66
147
  RETURNING c.user_identifier_id
67
148
  )
68
149
  DELETE FROM user_identifier ui
@@ -70,10 +151,67 @@ export class TasksService {
70
151
  WHERE ui.id = deleted_challenges.user_identifier_id
71
152
  AND ui.verified_at IS NULL;
72
153
  `;
73
- this.logger.verbose(`Expired user identifier challenges and unverified identifiers cleaned up in ${Date.now() - startAt}ms`);
74
- this.logger.verbose('++++++++++++++++++++++++++++++++++');
75
154
 
76
- this.logger.verbose('Finished scheduled cleanup tasks');
77
- this.logger.verbose('++++++++++++++++++++++++++++++++++');
155
+ this.logger.verbose(
156
+ `Expired user identifier challenges and unverified identifiers cleaned up in ${Date.now() - startAt}ms`,
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Limpa configurações de autenticação multifator (MFA) não verificadas cujos
162
+ * challenges de verificação expiraram, removendo também os challenges associados.
163
+ *
164
+ * Remove em cascata:
165
+ * 1. user_mfa_challenge: todos os challenges associados ao MFA não verificado
166
+ * 2. user_mfa: configuração de MFA que nunca foi verificada e não possui challenges válidos
167
+ *
168
+ * Por quê: Quando um usuário configura MFA (TOTP, SMS, etc.), é gerado um challenge
169
+ * para verificar que o método funciona. Se o usuário:
170
+ * - Abandonou o processo de configuração (não verificou)
171
+ * - Todos os challenges expiraram (não há tentativas válidas pendentes)
172
+ *
173
+ * Então a configuração incompleta deve ser removida porque:
174
+ * - Não pode ser utilizada para autenticação (nunca foi verificada)
175
+ * - Ocupa espaço desnecessário no banco
176
+ * - Pode causar confusão na interface (métodos MFA "pendentes" indefinidamente)
177
+ * - Permite ao usuário reconfigurar o método sem conflitos
178
+ *
179
+ * Limite: 20.000 registros por execução para evitar lock prolongado das tabelas.
180
+ */
181
+ private async clearUnverifiedMfaWithExpiredChallenges(): Promise<void> {
182
+ this.logger.verbose(
183
+ 'Starting cleanup of unverified MFA with expired challenges',
184
+ );
185
+ const startAt = Date.now();
186
+
187
+ await this.prismaService.$queryRaw`
188
+ WITH unverified_mfa AS (
189
+ SELECT um.id
190
+ FROM user_mfa um
191
+ WHERE um.verified_at IS NULL
192
+ AND NOT EXISTS (
193
+ SELECT 1
194
+ FROM user_mfa_challenge umc
195
+ WHERE umc.user_mfa_id = um.id
196
+ AND umc.expires_at >= now()
197
+ )
198
+ LIMIT 20000
199
+ ),
200
+ deleted_challenges AS (
201
+ DELETE FROM user_mfa_challenge umc
202
+ USING unverified_mfa
203
+ WHERE umc.user_mfa_id = unverified_mfa.id
204
+ RETURNING umc.user_mfa_id
205
+ )
206
+ DELETE FROM user_mfa um
207
+ USING unverified_mfa
208
+ WHERE um.id = unverified_mfa.id;
209
+ `;
210
+
211
+ this.logger.verbose(
212
+ `Unverified MFA with expired challenges cleaned up in ${Date.now() - startAt}ms`,
213
+ );
78
214
  }
215
+
216
+
79
217
  }