@kaikybrofc/omnizap-system 2.3.3 → 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 (46) hide show
  1. package/README.md +20 -22
  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/api-docs/index.html +3 -2
  6. package/public/apple-touch-icon.png +0 -0
  7. package/public/bot-whatsapp-para-grupo/index.html +3 -1
  8. package/public/bot-whatsapp-sem-programar/index.html +3 -1
  9. package/public/comandos/index.html +3 -2
  10. package/public/como-automatizar-avisos-no-whatsapp/index.html +3 -1
  11. package/public/como-criar-comandos-whatsapp/index.html +3 -1
  12. package/public/como-evitar-spam-no-whatsapp/index.html +3 -1
  13. package/public/como-moderar-grupo-whatsapp/index.html +3 -1
  14. package/public/como-organizar-comunidade-whatsapp/index.html +3 -1
  15. package/public/favicon-16x16.png +0 -0
  16. package/public/favicon-32x32.png +0 -0
  17. package/public/favicon.ico +0 -0
  18. package/public/index.html +11 -10
  19. package/public/js/apps/homeApp.js +22 -18
  20. package/public/js/apps/loginApp.js +3 -1
  21. package/public/js/apps/userProfileApp.js +0 -9
  22. package/public/licenca/index.html +3 -2
  23. package/public/login/index.html +3 -1
  24. package/public/melhor-bot-whatsapp-para-grupos/index.html +3 -1
  25. package/public/site.webmanifest +24 -0
  26. package/public/stickers/create/index.html +3 -1
  27. package/public/stickers/index.html +3 -1
  28. package/public/termos-de-uso/index.html +3 -2
  29. package/public/user/index.html +227 -121
  30. package/public/user/systemadm/index.html +3 -1
  31. package/server/controllers/admin/adminBanService.js +138 -0
  32. package/server/controllers/admin/adminPanelHandlers.js +1965 -0
  33. package/server/controllers/{systemAdminController.js → admin/systemAdminController.js} +2 -2
  34. package/server/controllers/{stickerCatalogController.js → sticker/stickerCatalogController.js} +129 -2116
  35. package/server/controllers/userController.js +1 -1
  36. package/server/routes/admin/systemAdminRouter.js +1 -1
  37. package/server/routes/indexRouter.js +3 -3
  38. package/server/routes/{stickerCatalog → sticker}/stickerApiRouter.js +1 -1
  39. package/server/routes/{stickerCatalog → sticker}/stickerDataRouter.js +1 -1
  40. package/server/routes/{stickerCatalog → sticker}/stickerSiteRouter.js +1 -1
  41. /package/server/controllers/{stickerCatalog → sticker}/nonCatalogHandlers.js +0 -0
  42. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogAdminHttp.js +0 -0
  43. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogAuthHttp.js +0 -0
  44. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogPublicHttp.js +0 -0
  45. /package/server/routes/{stickerCatalog → sticker}/catalogHandlers/catalogUploadHttp.js +0 -0
  46. /package/server/routes/{stickerCatalog → sticker}/catalogRouter.js +0 -0
@@ -8,20 +8,29 @@
8
8
  <meta name="robots" content="noindex, nofollow" />
9
9
  <meta name="theme-color" content="#0f172a" />
10
10
  <link rel="canonical" href="https://omnizap.shop/user/" />
11
- <link rel="icon" type="image/jpeg" href="https://iili.io/FC3FABe.jpg" />
11
+ <link rel="icon" href="/favicon.ico" sizes="any" />
12
+ <link rel="manifest" href="/site.webmanifest" />
13
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
12
14
  <link rel="preconnect" href="https://fonts.googleapis.com" />
13
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
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" />
15
17
  <style>
16
18
  :root {
17
- --bg: #0f172a;
18
- --bg-2: #111827;
19
- --card: #1e293bcf;
20
- --line: rgba(255, 255, 255, 0.05);
21
- --text: #f8fafc;
22
- --muted: #94a3b8;
23
- --primary: #22c55e;
24
- --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;
25
34
  }
26
35
 
27
36
  * {
@@ -38,33 +47,33 @@
38
47
  body {
39
48
  color: var(--text);
40
49
  font-family: 'Manrope', system-ui, sans-serif;
41
- 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));
42
54
  min-height: 100vh;
43
- display: flex;
44
- align-items: center;
45
- justify-content: center;
46
- padding: 24px 14px;
55
+ padding: clamp(12px, 2.2vw, 28px);
47
56
  }
48
57
 
49
58
  .page {
50
- width: min(840px, 100%);
51
- border: 1px solid rgba(255, 255, 255, 0.05);
52
- border-radius: 24px;
53
- 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%);
54
64
  box-shadow:
55
- 0 18px 44px #0209166e,
56
- inset 0 1px 0 #95c1ff1a;
65
+ 0 25px 70px #01091585,
66
+ inset 0 1px 0 #d3e4ff2e;
57
67
  overflow: hidden;
58
68
  }
59
69
 
60
70
  .head {
61
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
62
- padding: 20px 22px;
63
- 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;
64
76
  align-items: center;
65
- justify-content: space-between;
66
- gap: 14px;
67
- flex-wrap: wrap;
68
77
  }
69
78
 
70
79
  .brand {
@@ -73,71 +82,93 @@
73
82
  gap: 10px;
74
83
  font-family: 'Sora', sans-serif;
75
84
  font-weight: 700;
76
- letter-spacing: 0.2px;
85
+ letter-spacing: 0.03em;
77
86
  text-decoration: none;
78
87
  color: var(--text);
79
88
  }
80
89
 
81
90
  .brand img {
82
- width: 30px;
83
- height: 30px;
84
- border-radius: 50%;
85
- 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);
86
95
  object-fit: cover;
96
+ box-shadow: 0 10px 22px #0000004b;
87
97
  }
88
98
 
89
99
  .head-actions {
90
- display: flex;
91
- flex-wrap: wrap;
100
+ display: grid;
101
+ grid-auto-flow: column;
102
+ grid-auto-columns: max-content;
92
103
  gap: 8px;
93
104
  }
94
105
 
95
106
  .btn {
96
107
  border: 1px solid var(--line);
97
108
  border-radius: 12px;
98
- background: #101a2f;
109
+ background: #0e1b33;
99
110
  color: var(--text);
100
111
  text-decoration: none;
101
- padding: 9px 12px;
112
+ padding: 10px 14px;
102
113
  font-size: 14px;
103
- 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;
104
120
  transition:
105
121
  transform 0.2s ease,
106
122
  border-color 0.2s ease,
107
- box-shadow 0.2s ease;
123
+ box-shadow 0.2s ease,
124
+ background-color 0.2s ease;
108
125
  cursor: pointer;
109
126
  }
110
127
 
111
128
  .btn:hover {
112
129
  transform: translateY(-1px);
113
- border-color: #2563eb;
114
- box-shadow: 0 10px 20px #02091650;
130
+ border-color: var(--line-strong);
131
+ box-shadow: 0 12px 24px #01091557;
115
132
  }
116
133
 
117
134
  .btn.primary {
118
135
  border-color: transparent;
119
- background: #22c55e;
120
- color: #0f172a;
121
- box-shadow: 0 10px 22px #22c55e30;
136
+ background: linear-gradient(120deg, #1fd08b, #22c55e);
137
+ color: #072116;
138
+ box-shadow: 0 12px 24px #22c55e3d;
122
139
  }
123
140
 
124
141
  .btn.primary:hover {
125
- background: #16a34a;
142
+ background: linear-gradient(120deg, #2be09b, #33d46d);
126
143
  }
127
144
 
128
145
  .content {
129
- padding: 24px 22px;
146
+ padding: clamp(18px, 2.6vw, 30px);
147
+ display: grid;
148
+ gap: clamp(14px, 2vw, 22px);
149
+ }
150
+
151
+ .intro {
130
152
  display: grid;
131
- 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;
132
163
  }
133
164
 
134
165
  h1 {
135
166
  margin: 0;
136
167
  font-family: 'Sora', sans-serif;
137
- font-size: clamp(26px, 3.6vw, 36px);
138
- line-height: 1.08;
139
- letter-spacing: -0.02em;
140
- 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%);
141
172
  -webkit-background-clip: text;
142
173
  background-clip: text;
143
174
  color: transparent;
@@ -145,141 +176,205 @@
145
176
 
146
177
  .lead {
147
178
  margin: 0;
148
- color: #bfd1ea;
179
+ color: #bfd3ef;
149
180
  line-height: 1.6;
150
181
  font-size: 16px;
182
+ max-width: 68ch;
151
183
  }
152
184
 
153
185
  .card {
154
- border: 1px solid #334f7bc7;
155
- border-radius: var(--radius);
156
- background: var(--card);
157
- 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);
158
190
  display: grid;
159
191
  gap: 12px;
160
- 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%);
161
206
  }
162
207
 
163
208
  .status {
209
+ position: relative;
164
210
  margin: 0;
165
- color: #dce8fa;
211
+ color: #e7f0ff;
166
212
  font-size: 15px;
213
+ font-weight: 600;
167
214
  line-height: 1.5;
215
+ padding: 12px 14px;
216
+ border: 1px solid #6d9de440;
217
+ border-radius: var(--radius-md);
218
+ background: #112647d1;
168
219
  }
169
220
 
170
221
  .error {
222
+ position: relative;
171
223
  margin: 0;
172
- border: 1px solid #a74949;
173
- border-radius: 12px;
174
- background: #3b181899;
175
- color: #ffd7d7;
176
- 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;
177
229
  font-size: 14px;
178
230
  }
179
231
 
180
232
  .profile {
233
+ position: relative;
181
234
  display: grid;
182
235
  grid-template-columns: auto 1fr;
183
- gap: 12px;
236
+ gap: 12px 14px;
184
237
  align-items: center;
185
- border: 1px solid #31517fb5;
186
- border-radius: 14px;
187
- background: #1e293bbf;
188
- padding: 12px;
238
+ border: 1px solid #7facf249;
239
+ border-radius: var(--radius-md);
240
+ background: #0f1f38bf;
241
+ padding: clamp(12px, 1.8vw, 16px);
189
242
  }
190
243
 
191
244
  .avatar {
192
- width: 66px;
193
- height: 66px;
245
+ width: clamp(64px, 8vw, 82px);
246
+ height: clamp(64px, 8vw, 82px);
194
247
  border-radius: 16px;
195
- border: 1px solid #3c5a89;
248
+ border: 1px solid #7ca8e178;
196
249
  object-fit: cover;
197
- background: #0b1323;
250
+ background: #0b1528;
251
+ box-shadow: 0 10px 24px #030b1961;
198
252
  }
199
253
 
200
254
  .profile-meta h2 {
201
- margin: 0 0 4px;
202
- font-size: 20px;
255
+ margin: 0 0 6px;
256
+ font-size: clamp(20px, 2.1vw, 26px);
203
257
  line-height: 1.2;
258
+ letter-spacing: -0.02em;
204
259
  }
205
260
 
206
261
  .profile-meta p {
207
262
  margin: 0;
208
263
  color: var(--muted);
209
264
  font-size: 14px;
210
- line-height: 1.45;
265
+ line-height: 1.5;
211
266
  word-break: break-word;
212
267
  }
213
268
 
214
269
  .grid {
270
+ position: relative;
215
271
  display: grid;
216
- grid-template-columns: repeat(4, minmax(0, 1fr));
217
- gap: 8px;
272
+ grid-template-columns: repeat(2, minmax(0, 1fr));
273
+ gap: 10px;
218
274
  }
219
275
 
220
276
  .metric {
221
- border: 1px solid #31517fb5;
222
- border-radius: 12px;
223
- background: #1e293bbf;
224
- 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;
225
284
  }
226
285
 
227
286
  .metric-label {
228
- margin: 0 0 4px;
229
- color: #94a3b8;
287
+ margin: 0;
288
+ color: #9bb2d2;
230
289
  font-size: 12px;
290
+ font-weight: 600;
291
+ letter-spacing: 0.05em;
292
+ text-transform: uppercase;
231
293
  }
232
294
 
233
295
  .metric-value {
234
- margin: 0;
235
- color: #e8f1ff;
236
- font-size: 19px;
296
+ margin: 6px 0 0;
297
+ color: #f1f7ff;
298
+ font-size: clamp(22px, 3vw, 28px);
237
299
  font-weight: 800;
238
- letter-spacing: -0.01em;
300
+ letter-spacing: -0.02em;
301
+ line-height: 1.05;
239
302
  }
240
303
 
241
- .summary {
242
- border: 1px solid #2b8b5d9c;
243
- border-radius: 12px;
244
- background: #11302294;
245
- padding: 10px;
304
+ .actions {
305
+ position: relative;
246
306
  display: grid;
247
- gap: 6px;
248
- color: #dfffe8;
249
- font-size: 14px;
250
- }
251
-
252
- .summary strong {
253
- color: #f4fff8;
307
+ grid-template-columns: repeat(2, minmax(0, 1fr));
308
+ gap: 10px;
254
309
  }
255
310
 
256
- .actions {
257
- display: flex;
258
- flex-wrap: wrap;
259
- gap: 8px;
311
+ #user-chat-link {
312
+ grid-column: 1 / -1;
260
313
  }
261
314
 
262
315
  .footer {
263
316
  margin: 0;
264
- color: #7f94b8;
317
+ color: #88a2c8;
265
318
  font-size: 12px;
266
- }
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
+ }
267
332
 
268
- @media (max-width: 760px) {
269
- .grid {
270
- 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;
271
357
  }
272
358
  }
273
359
 
274
- @media (max-width: 620px) {
360
+ @media (max-width: 840px) {
275
361
  .head {
276
- padding: 16px;
362
+ grid-template-columns: 1fr;
277
363
  }
278
364
 
279
- .content {
280
- 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%;
281
374
  }
375
+ }
282
376
 
377
+ @media (max-width: 620px) {
283
378
  .profile {
284
379
  grid-template-columns: 1fr;
285
380
  text-align: center;
@@ -289,9 +384,23 @@
289
384
  margin-inline: auto;
290
385
  }
291
386
 
292
- .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 {
293
397
  width: 100%;
294
- text-align: center;
398
+ }
399
+ }
400
+
401
+ @media (max-width: 430px) {
402
+ .grid {
403
+ grid-template-columns: 1fr;
295
404
  }
296
405
  }
297
406
  </style>
@@ -310,10 +419,13 @@
310
419
  </header>
311
420
 
312
421
  <section class="content">
313
- <h1>Minha Conta</h1>
314
- <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>
315
427
 
316
- <article class="card">
428
+ <article class="card dashboard-card">
317
429
  <p id="user-status" class="status">Carregando dados da conta...</p>
318
430
  <p id="user-error" class="error" hidden></p>
319
431
 
@@ -345,12 +457,6 @@
345
457
  </article>
346
458
  </div>
347
459
 
348
- <div id="user-summary" class="summary" hidden>
349
- <div><strong>Owner JID:</strong> <span id="user-owner-jid"></span></div>
350
- <div><strong>Google ID:</strong> <span id="user-google-sub"></span></div>
351
- <div><strong>Sessão expira:</strong> <span id="user-expires-at"></span></div>
352
- </div>
353
-
354
460
  <div id="user-actions" class="actions" hidden>
355
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>
356
462
  <a id="user-manage-main-link" class="btn" href="/stickers/perfil">Gerenciar meus stickers</a>
@@ -8,7 +8,9 @@
8
8
  <meta name="robots" content="noindex, nofollow" />
9
9
  <meta name="theme-color" content="#0f172a" />
10
10
  <link rel="canonical" href="https://omnizap.shop/user/systemadm/" />
11
- <link rel="icon" type="image/jpeg" href="https://iili.io/FC3FABe.jpg" />
11
+ <link rel="icon" href="/favicon.ico" sizes="any" />
12
+ <link rel="manifest" href="/site.webmanifest" />
13
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
12
14
  <link rel="preconnect" href="https://fonts.googleapis.com" />
13
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
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" />
@@ -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
+ };