@kaikybrofc/omnizap-system 2.3.4 → 2.3.5

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.
Files changed (25) hide show
  1. package/README.md +14 -14
  2. package/app/modules/stickerModule/stickerCommand.js +1 -6
  3. package/app/modules/stickerModule/stickerTextCommand.js +1 -6
  4. package/package.json +1 -1
  5. package/public/index.html +8 -8
  6. package/public/js/apps/homeApp.js +22 -18
  7. package/public/js/apps/loginApp.js +3 -1
  8. package/public/js/apps/userProfileApp.js +0 -9
  9. package/public/user/index.html +224 -120
  10. package/server/controllers/admin/adminBanService.js +138 -0
  11. package/server/controllers/admin/adminPanelHandlers.js +1965 -0
  12. package/server/controllers/{systemAdminController.js → admin/systemAdminController.js} +2 -2
  13. package/server/controllers/{stickerCatalogController.js → sticker/stickerCatalogController.js} +129 -2116
  14. package/server/controllers/userController.js +1 -1
  15. package/server/routes/admin/systemAdminRouter.js +1 -1
  16. package/server/routes/indexRouter.js +3 -3
  17. package/server/routes/{stickerCatalog → sticker}/stickerApiRouter.js +1 -1
  18. package/server/routes/{stickerCatalog → sticker}/stickerDataRouter.js +1 -1
  19. package/server/routes/{stickerCatalog → sticker}/stickerSiteRouter.js +1 -1
  20. /package/server/controllers/{stickerCatalog → sticker}/nonCatalogHandlers.js +0 -0
  21. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogAdminHttp.js +0 -0
  22. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogAuthHttp.js +0 -0
  23. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogPublicHttp.js +0 -0
  24. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogUploadHttp.js +0 -0
  25. /package/server/routes/{stickerCatalog → sticker}/catalogRouter.js +0 -0
@@ -16,14 +16,21 @@
16
16
  <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Sora:wght@500;600;700&display=swap" rel="stylesheet" />
17
17
  <style>
18
18
  :root {
19
- --bg: #0f172a;
20
- --bg-2: #111827;
21
- --card: #1e293bcf;
22
- --line: rgba(255, 255, 255, 0.05);
23
- --text: #f8fafc;
24
- --muted: #94a3b8;
25
- --primary: #22c55e;
26
- --radius: 18px;
19
+ --bg: #060d1a;
20
+ --bg-2: #0c1a31;
21
+ --bg-3: #102542;
22
+ --surface: #132340d6;
23
+ --surface-2: #152948f0;
24
+ --line: #8ab4ff2b;
25
+ --line-strong: #9fc4ff61;
26
+ --text: #f5f9ff;
27
+ --muted: #9eb2ce;
28
+ --primary: #1fd08b;
29
+ --accent: #5a8fff;
30
+ --danger: #ffb3b3;
31
+ --radius-xl: 26px;
32
+ --radius-lg: 18px;
33
+ --radius-md: 14px;
27
34
  }
28
35
 
29
36
  * {
@@ -40,33 +47,33 @@
40
47
  body {
41
48
  color: var(--text);
42
49
  font-family: 'Manrope', system-ui, sans-serif;
43
- background: radial-gradient(55rem 22rem at -10% -10%, #2563eb30, transparent 60%), radial-gradient(55rem 22rem at 110% -8%, #7c3aed22, transparent 56%), linear-gradient(160deg, var(--bg), var(--bg-2));
50
+ background:
51
+ radial-gradient(70rem 40rem at -12% -18%, #1d4ed855, transparent 62%),
52
+ radial-gradient(55rem 30rem at 108% -6%, #0ea5e941, transparent 66%),
53
+ linear-gradient(160deg, var(--bg), var(--bg-2) 52%, var(--bg-3));
44
54
  min-height: 100vh;
45
- display: flex;
46
- align-items: center;
47
- justify-content: center;
48
- padding: 24px 14px;
55
+ padding: clamp(12px, 2.2vw, 28px);
49
56
  }
50
57
 
51
58
  .page {
52
- width: min(840px, 100%);
53
- border: 1px solid rgba(255, 255, 255, 0.05);
54
- border-radius: 24px;
55
- background: linear-gradient(150deg, #111827e8, #1e293bee);
59
+ width: min(1080px, 100%);
60
+ margin-inline: auto;
61
+ border: 1px solid #8ab4ff1f;
62
+ border-radius: var(--radius-xl);
63
+ background: linear-gradient(145deg, #101d37ef 2%, #0f1b33f3 44%, #13294af2 100%);
56
64
  box-shadow:
57
- 0 18px 44px #0209166e,
58
- inset 0 1px 0 #95c1ff1a;
65
+ 0 25px 70px #01091585,
66
+ inset 0 1px 0 #d3e4ff2e;
59
67
  overflow: hidden;
60
68
  }
61
69
 
62
70
  .head {
63
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
64
- padding: 20px 22px;
65
- display: flex;
71
+ border-bottom: 1px solid var(--line);
72
+ padding: clamp(16px, 2.1vw, 24px);
73
+ display: grid;
74
+ grid-template-columns: 1fr auto;
75
+ gap: 12px 16px;
66
76
  align-items: center;
67
- justify-content: space-between;
68
- gap: 14px;
69
- flex-wrap: wrap;
70
77
  }
71
78
 
72
79
  .brand {
@@ -75,71 +82,93 @@
75
82
  gap: 10px;
76
83
  font-family: 'Sora', sans-serif;
77
84
  font-weight: 700;
78
- letter-spacing: 0.2px;
85
+ letter-spacing: 0.03em;
79
86
  text-decoration: none;
80
87
  color: var(--text);
81
88
  }
82
89
 
83
90
  .brand img {
84
- width: 30px;
85
- height: 30px;
86
- border-radius: 50%;
87
- border: 1px solid rgba(255, 255, 255, 0.05);
91
+ width: 34px;
92
+ height: 34px;
93
+ border-radius: 12px;
94
+ border: 1px solid var(--line);
88
95
  object-fit: cover;
96
+ box-shadow: 0 10px 22px #0000004b;
89
97
  }
90
98
 
91
99
  .head-actions {
92
- display: flex;
93
- flex-wrap: wrap;
100
+ display: grid;
101
+ grid-auto-flow: column;
102
+ grid-auto-columns: max-content;
94
103
  gap: 8px;
95
104
  }
96
105
 
97
106
  .btn {
98
107
  border: 1px solid var(--line);
99
108
  border-radius: 12px;
100
- background: #101a2f;
109
+ background: #0e1b33;
101
110
  color: var(--text);
102
111
  text-decoration: none;
103
- padding: 9px 12px;
112
+ padding: 10px 14px;
104
113
  font-size: 14px;
105
- font-weight: 600;
114
+ font-weight: 700;
115
+ line-height: 1.2;
116
+ text-align: center;
117
+ display: inline-flex;
118
+ justify-content: center;
119
+ align-items: center;
106
120
  transition:
107
121
  transform 0.2s ease,
108
122
  border-color 0.2s ease,
109
- box-shadow 0.2s ease;
123
+ box-shadow 0.2s ease,
124
+ background-color 0.2s ease;
110
125
  cursor: pointer;
111
126
  }
112
127
 
113
128
  .btn:hover {
114
129
  transform: translateY(-1px);
115
- border-color: #2563eb;
116
- box-shadow: 0 10px 20px #02091650;
130
+ border-color: var(--line-strong);
131
+ box-shadow: 0 12px 24px #01091557;
117
132
  }
118
133
 
119
134
  .btn.primary {
120
135
  border-color: transparent;
121
- background: #22c55e;
122
- color: #0f172a;
123
- box-shadow: 0 10px 22px #22c55e30;
136
+ background: linear-gradient(120deg, #1fd08b, #22c55e);
137
+ color: #072116;
138
+ box-shadow: 0 12px 24px #22c55e3d;
124
139
  }
125
140
 
126
141
  .btn.primary:hover {
127
- background: #16a34a;
142
+ background: linear-gradient(120deg, #2be09b, #33d46d);
128
143
  }
129
144
 
130
145
  .content {
131
- padding: 24px 22px;
146
+ padding: clamp(18px, 2.6vw, 30px);
147
+ display: grid;
148
+ gap: clamp(14px, 2vw, 22px);
149
+ }
150
+
151
+ .intro {
132
152
  display: grid;
133
- gap: 14px;
153
+ gap: 8px;
154
+ }
155
+
156
+ .eyebrow {
157
+ margin: 0;
158
+ font-family: 'Sora', sans-serif;
159
+ font-size: 12px;
160
+ letter-spacing: 0.16em;
161
+ text-transform: uppercase;
162
+ color: #95b8ff;
134
163
  }
135
164
 
136
165
  h1 {
137
166
  margin: 0;
138
167
  font-family: 'Sora', sans-serif;
139
- font-size: clamp(26px, 3.6vw, 36px);
140
- line-height: 1.08;
141
- letter-spacing: -0.02em;
142
- background: linear-gradient(90deg, #eef5ff 0%, #60a5fa 46%, #a78bfa 100%);
168
+ font-size: clamp(28px, 4vw, 46px);
169
+ line-height: 1.05;
170
+ letter-spacing: -0.03em;
171
+ background: linear-gradient(100deg, #f6fbff 0%, #9bc5ff 52%, #77b4ff 100%);
143
172
  -webkit-background-clip: text;
144
173
  background-clip: text;
145
174
  color: transparent;
@@ -147,141 +176,205 @@
147
176
 
148
177
  .lead {
149
178
  margin: 0;
150
- color: #bfd1ea;
179
+ color: #bfd3ef;
151
180
  line-height: 1.6;
152
181
  font-size: 16px;
182
+ max-width: 68ch;
153
183
  }
154
184
 
155
185
  .card {
156
- border: 1px solid #334f7bc7;
157
- border-radius: var(--radius);
158
- background: var(--card);
159
- padding: 16px;
186
+ border: 1px solid #5d8dd23b;
187
+ border-radius: var(--radius-lg);
188
+ background: linear-gradient(150deg, var(--surface) 0%, var(--surface-2) 100%);
189
+ padding: clamp(14px, 2.2vw, 20px);
160
190
  display: grid;
161
191
  gap: 12px;
162
- backdrop-filter: blur(8px);
192
+ backdrop-filter: blur(9px);
193
+ }
194
+
195
+ .dashboard-card {
196
+ position: relative;
197
+ overflow: hidden;
198
+ }
199
+
200
+ .dashboard-card::before {
201
+ content: '';
202
+ position: absolute;
203
+ inset: 0;
204
+ pointer-events: none;
205
+ background: linear-gradient(160deg, #8fb3ff10 0%, transparent 36%, #1fd08b0f 100%);
163
206
  }
164
207
 
165
208
  .status {
209
+ position: relative;
166
210
  margin: 0;
167
- color: #dce8fa;
211
+ color: #e7f0ff;
168
212
  font-size: 15px;
213
+ font-weight: 600;
169
214
  line-height: 1.5;
215
+ padding: 12px 14px;
216
+ border: 1px solid #6d9de440;
217
+ border-radius: var(--radius-md);
218
+ background: #112647d1;
170
219
  }
171
220
 
172
221
  .error {
222
+ position: relative;
173
223
  margin: 0;
174
- border: 1px solid #a74949;
175
- border-radius: 12px;
176
- background: #3b181899;
177
- color: #ffd7d7;
178
- padding: 9px 10px;
224
+ border: 1px solid #ff8f8f87;
225
+ border-radius: var(--radius-md);
226
+ background: #3d1e2fb0;
227
+ color: var(--danger);
228
+ padding: 10px 12px;
179
229
  font-size: 14px;
180
230
  }
181
231
 
182
232
  .profile {
233
+ position: relative;
183
234
  display: grid;
184
235
  grid-template-columns: auto 1fr;
185
- gap: 12px;
236
+ gap: 12px 14px;
186
237
  align-items: center;
187
- border: 1px solid #31517fb5;
188
- border-radius: 14px;
189
- background: #1e293bbf;
190
- padding: 12px;
238
+ border: 1px solid #7facf249;
239
+ border-radius: var(--radius-md);
240
+ background: #0f1f38bf;
241
+ padding: clamp(12px, 1.8vw, 16px);
191
242
  }
192
243
 
193
244
  .avatar {
194
- width: 66px;
195
- height: 66px;
245
+ width: clamp(64px, 8vw, 82px);
246
+ height: clamp(64px, 8vw, 82px);
196
247
  border-radius: 16px;
197
- border: 1px solid #3c5a89;
248
+ border: 1px solid #7ca8e178;
198
249
  object-fit: cover;
199
- background: #0b1323;
250
+ background: #0b1528;
251
+ box-shadow: 0 10px 24px #030b1961;
200
252
  }
201
253
 
202
254
  .profile-meta h2 {
203
- margin: 0 0 4px;
204
- font-size: 20px;
255
+ margin: 0 0 6px;
256
+ font-size: clamp(20px, 2.1vw, 26px);
205
257
  line-height: 1.2;
258
+ letter-spacing: -0.02em;
206
259
  }
207
260
 
208
261
  .profile-meta p {
209
262
  margin: 0;
210
263
  color: var(--muted);
211
264
  font-size: 14px;
212
- line-height: 1.45;
265
+ line-height: 1.5;
213
266
  word-break: break-word;
214
267
  }
215
268
 
216
269
  .grid {
270
+ position: relative;
217
271
  display: grid;
218
- grid-template-columns: repeat(4, minmax(0, 1fr));
219
- gap: 8px;
272
+ grid-template-columns: repeat(2, minmax(0, 1fr));
273
+ gap: 10px;
220
274
  }
221
275
 
222
276
  .metric {
223
- border: 1px solid #31517fb5;
224
- border-radius: 12px;
225
- background: #1e293bbf;
226
- padding: 10px;
277
+ border: 1px solid #7facf242;
278
+ border-radius: var(--radius-md);
279
+ background: #0f1f39b8;
280
+ padding: 12px;
281
+ min-height: 94px;
282
+ display: grid;
283
+ align-content: space-between;
227
284
  }
228
285
 
229
286
  .metric-label {
230
- margin: 0 0 4px;
231
- color: #94a3b8;
287
+ margin: 0;
288
+ color: #9bb2d2;
232
289
  font-size: 12px;
290
+ font-weight: 600;
291
+ letter-spacing: 0.05em;
292
+ text-transform: uppercase;
233
293
  }
234
294
 
235
295
  .metric-value {
236
- margin: 0;
237
- color: #e8f1ff;
238
- font-size: 19px;
296
+ margin: 6px 0 0;
297
+ color: #f1f7ff;
298
+ font-size: clamp(22px, 3vw, 28px);
239
299
  font-weight: 800;
240
- letter-spacing: -0.01em;
300
+ letter-spacing: -0.02em;
301
+ line-height: 1.05;
241
302
  }
242
303
 
243
- .summary {
244
- border: 1px solid #2b8b5d9c;
245
- border-radius: 12px;
246
- background: #11302294;
247
- padding: 10px;
304
+ .actions {
305
+ position: relative;
248
306
  display: grid;
249
- gap: 6px;
250
- color: #dfffe8;
251
- font-size: 14px;
252
- }
253
-
254
- .summary strong {
255
- color: #f4fff8;
307
+ grid-template-columns: repeat(2, minmax(0, 1fr));
308
+ gap: 10px;
256
309
  }
257
310
 
258
- .actions {
259
- display: flex;
260
- flex-wrap: wrap;
261
- gap: 8px;
311
+ #user-chat-link {
312
+ grid-column: 1 / -1;
262
313
  }
263
314
 
264
315
  .footer {
265
316
  margin: 0;
266
- color: #7f94b8;
317
+ color: #88a2c8;
267
318
  font-size: 12px;
268
- }
319
+ text-align: center;
320
+ }
321
+
322
+ @media (min-width: 920px) {
323
+ .dashboard-card {
324
+ grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
325
+ grid-template-areas:
326
+ 'status status'
327
+ 'error error'
328
+ 'profile grid'
329
+ 'actions actions';
330
+ gap: 14px;
331
+ }
269
332
 
270
- @media (max-width: 760px) {
271
- .grid {
272
- grid-template-columns: repeat(2, minmax(0, 1fr));
333
+ #user-status {
334
+ grid-area: status;
335
+ }
336
+
337
+ #user-error {
338
+ grid-area: error;
339
+ }
340
+
341
+ #user-profile {
342
+ grid-area: profile;
343
+ }
344
+
345
+ #user-grid {
346
+ grid-area: grid;
347
+ align-self: start;
348
+ }
349
+
350
+ #user-actions {
351
+ grid-area: actions;
352
+ grid-template-columns: repeat(3, minmax(0, 1fr));
353
+ }
354
+
355
+ #user-chat-link {
356
+ grid-column: span 2;
273
357
  }
274
358
  }
275
359
 
276
- @media (max-width: 620px) {
360
+ @media (max-width: 840px) {
277
361
  .head {
278
- padding: 16px;
362
+ grid-template-columns: 1fr;
279
363
  }
280
364
 
281
- .content {
282
- padding: 18px 16px;
365
+ .head-actions {
366
+ width: 100%;
367
+ grid-template-columns: repeat(2, minmax(0, 1fr));
368
+ grid-auto-flow: row;
369
+ grid-auto-columns: 1fr;
370
+ }
371
+
372
+ .head-actions .btn {
373
+ width: 100%;
283
374
  }
375
+ }
284
376
 
377
+ @media (max-width: 620px) {
285
378
  .profile {
286
379
  grid-template-columns: 1fr;
287
380
  text-align: center;
@@ -291,9 +384,23 @@
291
384
  margin-inline: auto;
292
385
  }
293
386
 
294
- .actions .btn {
387
+ .actions {
388
+ grid-template-columns: 1fr;
389
+ }
390
+
391
+ #user-chat-link {
392
+ grid-column: auto;
393
+ }
394
+
395
+ .actions .btn,
396
+ .actions button.btn {
295
397
  width: 100%;
296
- text-align: center;
398
+ }
399
+ }
400
+
401
+ @media (max-width: 430px) {
402
+ .grid {
403
+ grid-template-columns: 1fr;
297
404
  }
298
405
  }
299
406
  </style>
@@ -312,10 +419,13 @@
312
419
  </header>
313
420
 
314
421
  <section class="content">
315
- <h1>Minha Conta</h1>
316
- <p class="lead">Informações da sua conta vinculada ao OmniZap e atalhos rápidos de acesso.</p>
422
+ <div class="intro">
423
+ <p class="eyebrow">Painel do usuário</p>
424
+ <h1>Minha Conta</h1>
425
+ <p class="lead">Informações da sua conta vinculada ao OmniZap e atalhos rápidos para gerenciar seus stickers com agilidade no desktop e no celular.</p>
426
+ </div>
317
427
 
318
- <article class="card">
428
+ <article class="card dashboard-card">
319
429
  <p id="user-status" class="status">Carregando dados da conta...</p>
320
430
  <p id="user-error" class="error" hidden></p>
321
431
 
@@ -347,12 +457,6 @@
347
457
  </article>
348
458
  </div>
349
459
 
350
- <div id="user-summary" class="summary" hidden>
351
- <div><strong>Owner JID:</strong> <span id="user-owner-jid"></span></div>
352
- <div><strong>Google ID:</strong> <span id="user-google-sub"></span></div>
353
- <div><strong>Sessão expira:</strong> <span id="user-expires-at"></span></div>
354
- </div>
355
-
356
460
  <div id="user-actions" class="actions" hidden>
357
461
  <a id="user-chat-link" class="btn primary" href="https://api.whatsapp.com/send/?text=%2Fmenu&type=custom_url&app_absent=0" target="_blank" rel="noreferrer noopener"> Voltar ao chat do bot (/menu) </a>
358
462
  <a id="user-manage-main-link" class="btn" href="/stickers/perfil">Gerenciar meus stickers</a>
@@ -0,0 +1,138 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ export const createStickerCatalogAdminBanService = ({ executeQuery, tables, sanitizeText, normalizeGoogleSubject, normalizeEmail, normalizeJid, toIsoOrNull, revokeGoogleWebSessionsByIdentity = async () => 0 }) => {
4
+ const TABLES = tables;
5
+
6
+ const mapAdminBanRow = (row) => {
7
+ if (!row || typeof row !== 'object') return null;
8
+ return {
9
+ id: String(row.id || '').trim(),
10
+ google_sub: normalizeGoogleSubject(row.google_sub),
11
+ email: normalizeEmail(row.email),
12
+ owner_jid: normalizeJid(row.owner_jid) || null,
13
+ reason: sanitizeText(row.reason || '', 255, { allowEmpty: true }) || null,
14
+ created_by_google_sub: normalizeGoogleSubject(row.created_by_google_sub),
15
+ created_by_email: normalizeEmail(row.created_by_email),
16
+ created_at: toIsoOrNull(row.created_at),
17
+ updated_at: toIsoOrNull(row.updated_at),
18
+ revoked_at: toIsoOrNull(row.revoked_at),
19
+ };
20
+ };
21
+
22
+ const findActiveAdminBanForIdentity = async ({ googleSub = '', email = '', ownerJid = '' } = {}) => {
23
+ const normalizedSub = normalizeGoogleSubject(googleSub);
24
+ const normalizedEmail = normalizeEmail(email);
25
+ const normalizedOwnerJid = normalizeJid(ownerJid) || '';
26
+ const clauses = [];
27
+ const params = [];
28
+ if (normalizedSub) {
29
+ clauses.push('google_sub = ?');
30
+ params.push(normalizedSub);
31
+ }
32
+ if (normalizedEmail) {
33
+ clauses.push('email = ?');
34
+ params.push(normalizedEmail);
35
+ }
36
+ if (normalizedOwnerJid) {
37
+ clauses.push('owner_jid = ?');
38
+ params.push(normalizedOwnerJid);
39
+ }
40
+ if (!clauses.length) return null;
41
+
42
+ const rows = await executeQuery(
43
+ `SELECT *
44
+ FROM ${TABLES.STICKER_WEB_ADMIN_BAN}
45
+ WHERE revoked_at IS NULL
46
+ AND (${clauses.join(' OR ')})
47
+ ORDER BY created_at DESC
48
+ LIMIT 1`,
49
+ params,
50
+ );
51
+ return mapAdminBanRow(Array.isArray(rows) ? rows[0] : null);
52
+ };
53
+
54
+ const listAdminBans = async ({ activeOnly = false, limit = 100 } = {}) => {
55
+ const safeLimit = Math.max(1, Math.min(500, Number(limit || 100)));
56
+ const rows = await executeQuery(
57
+ `SELECT *
58
+ FROM ${TABLES.STICKER_WEB_ADMIN_BAN}
59
+ ${activeOnly ? 'WHERE revoked_at IS NULL' : ''}
60
+ ORDER BY created_at DESC
61
+ LIMIT ${safeLimit}`,
62
+ );
63
+ return (Array.isArray(rows) ? rows : []).map(mapAdminBanRow).filter(Boolean);
64
+ };
65
+
66
+ const createAdminBanRecord = async ({ googleSub = '', email = '', ownerJid = '', reason = '', adminSession = null }) => {
67
+ const normalizedSub = normalizeGoogleSubject(googleSub);
68
+ const normalizedEmail = normalizeEmail(email);
69
+ const normalizedOwnerJid = normalizeJid(ownerJid) || '';
70
+ if (!normalizedSub && !normalizedEmail && !normalizedOwnerJid) {
71
+ const error = new Error('Informe google_sub, email ou owner_jid para banir.');
72
+ error.statusCode = 400;
73
+ throw error;
74
+ }
75
+
76
+ const existing = await findActiveAdminBanForIdentity({
77
+ googleSub: normalizedSub,
78
+ email: normalizedEmail,
79
+ ownerJid: normalizedOwnerJid,
80
+ });
81
+ if (existing) return { created: false, ban: existing };
82
+
83
+ const banId = randomUUID();
84
+ await executeQuery(
85
+ `INSERT INTO ${TABLES.STICKER_WEB_ADMIN_BAN}
86
+ (id, google_sub, email, owner_jid, reason, created_by_google_sub, created_by_email)
87
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
88
+ [banId, normalizedSub || null, normalizedEmail || null, normalizedOwnerJid || null, sanitizeText(reason || '', 255, { allowEmpty: true }) || null, normalizeGoogleSubject(adminSession?.googleSub) || null, normalizeEmail(adminSession?.email) || null],
89
+ );
90
+
91
+ if (normalizedSub || normalizedEmail || normalizedOwnerJid) {
92
+ await revokeGoogleWebSessionsByIdentity({
93
+ googleSub: normalizedSub,
94
+ email: normalizedEmail,
95
+ ownerJid: normalizedOwnerJid,
96
+ }).catch(() => {});
97
+ }
98
+
99
+ const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_WEB_ADMIN_BAN} WHERE id = ? LIMIT 1`, [banId]);
100
+ return { created: true, ban: mapAdminBanRow(Array.isArray(rows) ? rows[0] : null) };
101
+ };
102
+
103
+ const revokeAdminBanRecord = async (banId) => {
104
+ const normalizedId = sanitizeText(banId, 36, { allowEmpty: false });
105
+ if (!normalizedId) {
106
+ const error = new Error('ban_id invalido.');
107
+ error.statusCode = 400;
108
+ throw error;
109
+ }
110
+ await executeQuery(
111
+ `UPDATE ${TABLES.STICKER_WEB_ADMIN_BAN}
112
+ SET revoked_at = COALESCE(revoked_at, UTC_TIMESTAMP())
113
+ WHERE id = ?`,
114
+ [normalizedId],
115
+ );
116
+ const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_WEB_ADMIN_BAN} WHERE id = ? LIMIT 1`, [normalizedId]);
117
+ return mapAdminBanRow(Array.isArray(rows) ? rows[0] : null);
118
+ };
119
+
120
+ const assertGoogleIdentityNotBanned = async ({ sub = '', email = '', ownerJid = '' } = {}) => {
121
+ const ban = await findActiveAdminBanForIdentity({ googleSub: sub, email, ownerJid });
122
+ if (!ban) return null;
123
+ const error = new Error('Conta bloqueada pela administracao.');
124
+ error.statusCode = 403;
125
+ error.code = 'ADMIN_BANNED';
126
+ error.ban = ban;
127
+ throw error;
128
+ };
129
+
130
+ return {
131
+ mapAdminBanRow,
132
+ findActiveAdminBanForIdentity,
133
+ listAdminBans,
134
+ createAdminBanRecord,
135
+ revokeAdminBanRecord,
136
+ assertGoogleIdentityNotBanned,
137
+ };
138
+ };