@kaikybrofc/omnizap-system 2.3.1 → 2.3.3

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 (49) hide show
  1. package/README.md +82 -483
  2. package/app/controllers/messageController.js +473 -255
  3. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
  4. package/app/modules/stickerModule/stickerCommand.js +7 -2
  5. package/app/modules/stickerModule/stickerTextCommand.js +7 -2
  6. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +1 -3
  7. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +224 -53
  8. package/app/observability/metrics.js +6 -3
  9. package/app/services/googleWebLinkService.js +77 -0
  10. package/app/services/lidMapService.js +83 -4
  11. package/database/index.js +2 -0
  12. package/database/migrations/20260301_0028_message_analysis_event.sql +32 -0
  13. package/database/migrations/20260301_0029_admin_action_audit.sql +16 -0
  14. package/package.json +1 -1
  15. package/public/index.html +12 -8
  16. package/public/js/apps/createPackApp.js +4 -4
  17. package/public/js/apps/homeApp.js +78 -34
  18. package/public/js/apps/loginApp.js +245 -35
  19. package/public/js/apps/stickersAdminApp.js +4 -10
  20. package/public/js/apps/stickersApp.js +1 -1
  21. package/public/js/apps/userApp.js +956 -55
  22. package/public/js/apps/userProfileApp.js +244 -0
  23. package/public/login/index.html +437 -101
  24. package/public/termos-de-uso/index.html +1 -1
  25. package/public/user/index.html +2 -181
  26. package/public/user/systemadm/index.html +774 -0
  27. package/server/controllers/stickerCatalog/nonCatalogHandlers.js +183 -0
  28. package/server/controllers/stickerCatalogController.js +1289 -368
  29. package/server/controllers/systemAdminController.js +141 -0
  30. package/server/controllers/userController.js +87 -0
  31. package/server/http/httpServer.js +72 -32
  32. package/server/middleware/cachePolicy.js +24 -0
  33. package/server/middleware/cachePolicyHelpers.js +1 -0
  34. package/server/middleware/rateLimit.js +89 -0
  35. package/server/middleware/requestLogger.js +16 -0
  36. package/server/middleware/requireAdminAuth.js +42 -0
  37. package/server/middleware/securityHeaders.js +6 -0
  38. package/server/routes/admin/systemAdminRouter.js +56 -0
  39. package/server/routes/health/healthRouter.js +41 -0
  40. package/server/routes/indexRouter.js +197 -0
  41. package/server/routes/metrics/metricsRouter.js +13 -0
  42. package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +44 -0
  43. package/server/routes/stickerCatalog/stickerApiRouter.js +84 -0
  44. package/server/routes/stickerCatalog/stickerDataRouter.js +140 -0
  45. package/server/routes/stickerCatalog/stickerSiteRouter.js +43 -0
  46. package/server/routes/user/userRouter.js +56 -0
  47. package/server/utils/safePath.js +26 -0
  48. package/server/routes/metricsRoute.js +0 -7
  49. package/server/routes/stickerCatalogRoute.js +0 -20
@@ -0,0 +1,244 @@
1
+ /* global document, window, fetch, URL, URLSearchParams */
2
+
3
+ const DEFAULT_API_BASE_PATH = '/api/sticker-packs';
4
+ const DEFAULT_STICKERS_PATH = '/stickers';
5
+ const DEFAULT_LOGIN_PATH = '/login';
6
+ const FALLBACK_AVATAR = 'https://iili.io/FC3FABe.jpg';
7
+
8
+ const root = document.getElementById('user-app-root');
9
+
10
+ if (root) {
11
+ const ui = {
12
+ status: document.getElementById('user-status'),
13
+ error: document.getElementById('user-error'),
14
+ profile: document.getElementById('user-profile'),
15
+ avatar: document.getElementById('user-avatar'),
16
+ name: document.getElementById('user-name'),
17
+ email: document.getElementById('user-email'),
18
+ whatsapp: document.getElementById('user-whatsapp'),
19
+ grid: document.getElementById('user-grid'),
20
+ metricPacks: document.getElementById('metric-packs'),
21
+ metricStickers: document.getElementById('metric-stickers'),
22
+ metricDownloads: document.getElementById('metric-downloads'),
23
+ metricLikes: document.getElementById('metric-likes'),
24
+ summary: document.getElementById('user-summary'),
25
+ ownerJid: document.getElementById('user-owner-jid'),
26
+ googleSub: document.getElementById('user-google-sub'),
27
+ expiresAt: document.getElementById('user-expires-at'),
28
+ actions: document.getElementById('user-actions'),
29
+ chatLink: document.getElementById('user-chat-link'),
30
+ logoutBtn: document.getElementById('user-logout-btn'),
31
+ manageHeadLink: document.getElementById('user-manage-head-link'),
32
+ manageMainLink: document.getElementById('user-manage-main-link'),
33
+ currentYear: document.getElementById('user-current-year'),
34
+ };
35
+
36
+ const state = {
37
+ apiBasePath: String(root.dataset.apiBasePath || DEFAULT_API_BASE_PATH).trim() || DEFAULT_API_BASE_PATH,
38
+ stickersPath: String(root.dataset.stickersPath || DEFAULT_STICKERS_PATH).trim() || DEFAULT_STICKERS_PATH,
39
+ loginPath: String(root.dataset.loginPath || DEFAULT_LOGIN_PATH).trim() || DEFAULT_LOGIN_PATH,
40
+ botPhone: '',
41
+ };
42
+
43
+ const sessionApiPath = `${state.apiBasePath}/auth/google/session`;
44
+ const myProfileApiPath = `${state.apiBasePath}/me`;
45
+ const botContactApiPath = `${state.apiBasePath}/bot-contact`;
46
+
47
+ const setText = (el, value) => {
48
+ if (!el) return;
49
+ el.textContent = String(value || '');
50
+ };
51
+
52
+ const showError = (message) => {
53
+ if (!ui.error) return;
54
+ const safeMessage = String(message || '').trim();
55
+ ui.error.hidden = !safeMessage;
56
+ if (safeMessage) ui.error.textContent = safeMessage;
57
+ };
58
+
59
+ const normalizeDigits = (value) => String(value || '').replace(/\D+/g, '');
60
+
61
+ const formatPhone = (digits) => {
62
+ const value = normalizeDigits(digits);
63
+ if (!value) return '';
64
+ if (value.length <= 4) return value;
65
+ return `${value.slice(0, 2)} ${value.slice(2, -4)}-${value.slice(-4)}`.trim();
66
+ };
67
+
68
+ const formatNumber = (value) =>
69
+ new Intl.NumberFormat('pt-BR', {
70
+ maximumFractionDigits: 0,
71
+ }).format(Math.max(0, Number(value || 0)));
72
+
73
+ const formatDateTime = (value) => {
74
+ const ms = Date.parse(String(value || ''));
75
+ if (!Number.isFinite(ms)) return 'n/d';
76
+ return new Intl.DateTimeFormat('pt-BR', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(ms));
77
+ };
78
+
79
+ const buildLoginRedirectUrl = () => {
80
+ const loginUrl = new URL(state.loginPath, window.location.origin);
81
+ loginUrl.searchParams.set('next', '/user/');
82
+ return `${loginUrl.pathname}${loginUrl.search}`;
83
+ };
84
+
85
+ const buildWhatsAppMenuUrl = (phoneDigits) => {
86
+ const params = new URLSearchParams({
87
+ text: '/menu',
88
+ type: 'custom_url',
89
+ app_absent: '0',
90
+ });
91
+ const digits = normalizeDigits(phoneDigits);
92
+ if (digits) params.set('phone', digits);
93
+ return `https://api.whatsapp.com/send/?${params.toString()}`;
94
+ };
95
+
96
+ const fetchJson = async (url, init = {}) => {
97
+ const response = await fetch(url, {
98
+ credentials: 'include',
99
+ ...init,
100
+ });
101
+ let payload = null;
102
+ try {
103
+ payload = await response.json();
104
+ } catch {
105
+ payload = null;
106
+ }
107
+
108
+ if (!response.ok) {
109
+ const err = new Error(payload?.error || `Falha HTTP ${response.status}`);
110
+ err.statusCode = response.status;
111
+ throw err;
112
+ }
113
+ return payload || {};
114
+ };
115
+
116
+ const redirectToLogin = () => {
117
+ window.location.assign(buildLoginRedirectUrl());
118
+ };
119
+
120
+ const loadBotPhone = async () => {
121
+ try {
122
+ const payload = await fetchJson(botContactApiPath, { method: 'GET' });
123
+ state.botPhone = normalizeDigits(payload?.data?.phone || '');
124
+ } catch {
125
+ state.botPhone = '';
126
+ }
127
+ if (ui.chatLink) ui.chatLink.href = buildWhatsAppMenuUrl(state.botPhone);
128
+ };
129
+
130
+ const renderSession = (sessionData) => {
131
+ const user = sessionData?.user || {};
132
+ const ownerPhone = String(sessionData?.owner_phone || '').trim();
133
+ const ownerJid = String(sessionData?.owner_jid || '').trim();
134
+
135
+ setText(ui.name, user?.name || 'Conta Google');
136
+ setText(ui.email, user?.email || 'Email não disponível');
137
+ if (ownerPhone) {
138
+ setText(ui.whatsapp, `WhatsApp vinculado: +${formatPhone(ownerPhone)}`);
139
+ } else if (ownerJid) {
140
+ setText(ui.whatsapp, `WhatsApp vinculado via owner: ${ownerJid}`);
141
+ } else {
142
+ setText(ui.whatsapp, 'WhatsApp não vinculado.');
143
+ }
144
+
145
+ if (ui.avatar) {
146
+ const picture = String(user?.picture || '').trim() || FALLBACK_AVATAR;
147
+ ui.avatar.src = picture;
148
+ ui.avatar.onerror = () => {
149
+ ui.avatar.src = FALLBACK_AVATAR;
150
+ };
151
+ }
152
+
153
+ setText(ui.ownerJid, ownerJid || 'n/d');
154
+ setText(ui.googleSub, String(user?.sub || '').trim() || 'n/d');
155
+ setText(ui.expiresAt, formatDateTime(sessionData?.expires_at));
156
+
157
+ if (ui.profile) ui.profile.hidden = false;
158
+ if (ui.summary) ui.summary.hidden = false;
159
+ if (ui.actions) ui.actions.hidden = false;
160
+ };
161
+
162
+ const renderPackMetrics = (payload) => {
163
+ const data = payload?.data || {};
164
+ const packs = Array.isArray(data?.packs) ? data.packs : [];
165
+ const stats = data?.stats && typeof data.stats === 'object' ? data.stats : {};
166
+
167
+ let stickers = 0;
168
+ let downloads = 0;
169
+ let likes = 0;
170
+
171
+ for (const pack of packs) {
172
+ stickers += Number(pack?.sticker_count || 0);
173
+ downloads += Number(pack?.engagement?.open_count || 0);
174
+ likes += Number(pack?.engagement?.like_count || 0);
175
+ }
176
+
177
+ setText(ui.metricPacks, formatNumber(stats.total || packs.length));
178
+ setText(ui.metricStickers, formatNumber(stickers));
179
+ setText(ui.metricDownloads, formatNumber(downloads));
180
+ setText(ui.metricLikes, formatNumber(likes));
181
+ if (ui.grid) ui.grid.hidden = false;
182
+ };
183
+
184
+ const handleLogout = async () => {
185
+ if (!ui.logoutBtn) return;
186
+ ui.logoutBtn.disabled = true;
187
+ ui.logoutBtn.textContent = 'Encerrando...';
188
+ try {
189
+ await fetchJson(sessionApiPath, { method: 'DELETE' });
190
+ } catch {
191
+ // no-op
192
+ }
193
+ window.location.assign(`${state.loginPath}/`);
194
+ };
195
+
196
+ const init = async () => {
197
+ const manageHref = `${state.stickersPath.replace(/\/+$/, '') || DEFAULT_STICKERS_PATH}/perfil`;
198
+ if (ui.manageHeadLink) ui.manageHeadLink.href = manageHref;
199
+ if (ui.manageMainLink) ui.manageMainLink.href = manageHref;
200
+ if (ui.currentYear) ui.currentYear.textContent = String(new Date().getFullYear());
201
+
202
+ setText(ui.status, 'Validando sua sessão...');
203
+ showError('');
204
+
205
+ let sessionData = null;
206
+ try {
207
+ const sessionPayload = await fetchJson(sessionApiPath, { method: 'GET' });
208
+ sessionData = sessionPayload?.data || {};
209
+ if (!sessionData?.authenticated || !sessionData?.user?.sub) {
210
+ redirectToLogin();
211
+ return;
212
+ }
213
+ } catch (error) {
214
+ showError(error?.message || 'Falha ao validar sessão.');
215
+ setText(ui.status, 'Não foi possível validar sua sessão agora.');
216
+ return;
217
+ }
218
+
219
+ renderSession(sessionData);
220
+ await loadBotPhone();
221
+
222
+ try {
223
+ const myProfilePayload = await fetchJson(myProfileApiPath, { method: 'GET' });
224
+ const sessionOk = Boolean(myProfilePayload?.data?.session?.authenticated);
225
+ if (!sessionOk) {
226
+ redirectToLogin();
227
+ return;
228
+ }
229
+ renderPackMetrics(myProfilePayload);
230
+ setText(ui.status, 'Sessão ativa. Dados da sua conta carregados com sucesso.');
231
+ } catch (error) {
232
+ showError(error?.message || 'Falha ao carregar dados da conta.');
233
+ setText(ui.status, 'Sessão ativa, mas não foi possível carregar todos os dados.');
234
+ }
235
+ };
236
+
237
+ if (ui.logoutBtn) {
238
+ ui.logoutBtn.addEventListener('click', () => {
239
+ void handleLogout();
240
+ });
241
+ }
242
+
243
+ void init();
244
+ }