@kaikybrofc/omnizap-system 2.3.0 → 2.3.1
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.
- package/README.md +18 -20
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +27 -5
- package/database/index.js +1 -0
- package/database/migrations/20260228_0027_web_visit_event.sql +15 -0
- package/package.json +1 -1
- package/public/index.html +42 -52
- package/public/js/apps/homeApp.js +184 -31
- package/public/js/apps/stickersApp.js +0 -28
- package/public/js/apps/userApp.js +217 -1
- package/public/licenca/index.html +98 -2
- package/public/termos-de-uso/index.html +245 -25
- package/public/user/index.html +181 -1
- package/server/auth/googleWebAuth/googleWebAuthService.js +614 -0
- package/server/controllers/stickerCatalogController.js +231 -628
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
|
|
4
|
+
const GOOGLE_TOKENINFO_URL = 'https://oauth2.googleapis.com/tokeninfo';
|
|
5
|
+
const GOOGLE_WEB_SESSION_COOKIE_NAME = 'omnizap_google_session';
|
|
6
|
+
|
|
7
|
+
const normalizeCookiePath = (value, fallback = '/') => {
|
|
8
|
+
const raw = String(value || '').trim();
|
|
9
|
+
const base = raw || fallback;
|
|
10
|
+
const withSlash = base.startsWith('/') ? base : `/${base}`;
|
|
11
|
+
if (withSlash.length > 1 && withSlash.endsWith('/')) return withSlash.slice(0, -1);
|
|
12
|
+
return withSlash || '/';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const createGoogleWebAuthService = ({ executeQuery, runSqlTransaction, tables, logger, sendJson, readJsonBody, parseCookies, getCookieValuesFromRequest, appendSetCookie, buildCookieString, normalizeGoogleSubject, normalizeEmail, normalizeJid, sanitizeText, toIsoOrNull, toWhatsAppPhoneDigits, resolveWhatsAppOwnerJidFromLoginPayload, buildGoogleOwnerJid, assertGoogleIdentityNotBanned, googleClientId, sessionTtlMs, sessionDbTouchIntervalMs, sessionDbPruneIntervalMs, notAllowedErrorCode, sessionCookiePath = '/', legacyCookiePaths = [] }) => {
|
|
16
|
+
const webGoogleSessionMap = new Map();
|
|
17
|
+
let googleWebSessionDbPruneAt = 0;
|
|
18
|
+
const normalizedSessionCookiePath = normalizeCookiePath(sessionCookiePath, '/');
|
|
19
|
+
const normalizedLegacyCookiePaths = Array.from(new Set(['/', normalizedSessionCookiePath, ...legacyCookiePaths].map((pathValue) => normalizeCookiePath(pathValue, '/')).filter(Boolean)));
|
|
20
|
+
|
|
21
|
+
const pruneExpiredGoogleSessions = () => {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
for (const [token, session] of webGoogleSessionMap.entries()) {
|
|
24
|
+
if (!session || Number(session.expiresAt || 0) <= now) {
|
|
25
|
+
webGoogleSessionMap.delete(token);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const verifyGoogleIdToken = async (idToken) => {
|
|
31
|
+
const token = String(idToken || '').trim();
|
|
32
|
+
if (!token) {
|
|
33
|
+
const error = new Error('Token Google ausente.');
|
|
34
|
+
error.statusCode = 401;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let response;
|
|
39
|
+
try {
|
|
40
|
+
response = await axios.get(GOOGLE_TOKENINFO_URL, {
|
|
41
|
+
params: { id_token: token },
|
|
42
|
+
timeout: 5000,
|
|
43
|
+
validateStatus: () => true,
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const wrapped = new Error('Falha ao validar login Google.');
|
|
47
|
+
wrapped.statusCode = 502;
|
|
48
|
+
wrapped.cause = error;
|
|
49
|
+
throw wrapped;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (response.status < 200 || response.status >= 300) {
|
|
53
|
+
const reason = String(response?.data?.error_description || response?.data?.error || '').trim();
|
|
54
|
+
const error = new Error(reason || 'Token Google inválido.');
|
|
55
|
+
error.statusCode = 401;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const claims = response?.data && typeof response.data === 'object' ? response.data : {};
|
|
60
|
+
const aud = String(claims.aud || '').trim();
|
|
61
|
+
const iss = String(claims.iss || '').trim();
|
|
62
|
+
const sub = normalizeGoogleSubject(claims.sub);
|
|
63
|
+
const email = String(claims.email || '')
|
|
64
|
+
.trim()
|
|
65
|
+
.toLowerCase();
|
|
66
|
+
const emailVerified = String(claims.email_verified || '')
|
|
67
|
+
.trim()
|
|
68
|
+
.toLowerCase();
|
|
69
|
+
|
|
70
|
+
if (googleClientId && aud !== googleClientId) {
|
|
71
|
+
const error = new Error('Login Google não pertence a este aplicativo.');
|
|
72
|
+
error.statusCode = 403;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
if (iss && !['accounts.google.com', 'https://accounts.google.com'].includes(iss)) {
|
|
76
|
+
const error = new Error('Emissor do token Google inválido.');
|
|
77
|
+
error.statusCode = 401;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
if (!sub) {
|
|
81
|
+
const error = new Error('Token Google sem identificador de usuário.');
|
|
82
|
+
error.statusCode = 401;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
if (email && emailVerified && !['true', '1'].includes(emailVerified)) {
|
|
86
|
+
const error = new Error('Conta Google sem e-mail verificado.');
|
|
87
|
+
error.statusCode = 403;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
sub,
|
|
93
|
+
email: email || null,
|
|
94
|
+
name: sanitizeText(claims.name || claims.given_name || '', 120, { allowEmpty: true }) || null,
|
|
95
|
+
picture: String(claims.picture || '').trim() || null,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getGoogleWebSessionTokensFromRequest = (req) => {
|
|
100
|
+
const direct = getCookieValuesFromRequest(req, GOOGLE_WEB_SESSION_COOKIE_NAME);
|
|
101
|
+
if (direct.length > 0) return direct;
|
|
102
|
+
const cookies = parseCookies(req);
|
|
103
|
+
const fallback = String(cookies[GOOGLE_WEB_SESSION_COOKIE_NAME] || '').trim();
|
|
104
|
+
return fallback ? [fallback] : [];
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const normalizeGoogleWebSessionRow = (row) => {
|
|
108
|
+
if (!row || typeof row !== 'object') return null;
|
|
109
|
+
const token = String(row.session_token || '').trim();
|
|
110
|
+
const sub = normalizeGoogleSubject(row.google_sub);
|
|
111
|
+
const ownerJid = normalizeJid(row.owner_jid) || '';
|
|
112
|
+
const ownerPhone = toWhatsAppPhoneDigits(row.owner_phone || ownerJid) || '';
|
|
113
|
+
const expiresAt = Number(new Date(row.expires_at || 0));
|
|
114
|
+
if (!token || !sub || !ownerJid || !Number.isFinite(expiresAt)) return null;
|
|
115
|
+
const createdAtRaw = Number(new Date(row.created_at || 0));
|
|
116
|
+
const lastSeenAtRaw = Number(new Date(row.last_seen_at || 0));
|
|
117
|
+
return {
|
|
118
|
+
token,
|
|
119
|
+
sub,
|
|
120
|
+
email:
|
|
121
|
+
String(row.email || '')
|
|
122
|
+
.trim()
|
|
123
|
+
.toLowerCase() || null,
|
|
124
|
+
name: sanitizeText(row.name || '', 120, { allowEmpty: true }) || null,
|
|
125
|
+
picture: String(row.picture_url || '').trim() || null,
|
|
126
|
+
ownerJid,
|
|
127
|
+
ownerPhone,
|
|
128
|
+
createdAt: Number.isFinite(createdAtRaw) ? createdAtRaw : Date.now(),
|
|
129
|
+
expiresAt,
|
|
130
|
+
lastSeenAt: Number.isFinite(lastSeenAtRaw) ? lastSeenAtRaw : 0,
|
|
131
|
+
lastDbTouchAt: Date.now(),
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const maybePruneExpiredGoogleSessionsFromDb = async () => {
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
if (now - googleWebSessionDbPruneAt < sessionDbPruneIntervalMs) return;
|
|
138
|
+
googleWebSessionDbPruneAt = now;
|
|
139
|
+
try {
|
|
140
|
+
await executeQuery(
|
|
141
|
+
`DELETE FROM ${tables.STICKER_WEB_GOOGLE_SESSION}
|
|
142
|
+
WHERE revoked_at IS NOT NULL OR expires_at <= UTC_TIMESTAMP()`,
|
|
143
|
+
);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.warn('Falha ao limpar sessões Google web expiradas do banco.', {
|
|
146
|
+
action: 'sticker_pack_google_web_session_db_prune_failed',
|
|
147
|
+
error: error?.message,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const upsertGoogleWebUserRecord = async (user, connection = null) => {
|
|
153
|
+
const sub = normalizeGoogleSubject(user?.sub);
|
|
154
|
+
const ownerJid = normalizeJid(user?.ownerJid) || '';
|
|
155
|
+
if (!sub || !ownerJid) return;
|
|
156
|
+
const ownerPhone = toWhatsAppPhoneDigits(ownerJid) || null;
|
|
157
|
+
const email =
|
|
158
|
+
String(user?.email || '')
|
|
159
|
+
.trim()
|
|
160
|
+
.toLowerCase() || null;
|
|
161
|
+
const name = sanitizeText(user?.name || '', 120, { allowEmpty: true }) || null;
|
|
162
|
+
const pictureUrl =
|
|
163
|
+
String(user?.picture || '')
|
|
164
|
+
.trim()
|
|
165
|
+
.slice(0, 1024) || null;
|
|
166
|
+
|
|
167
|
+
await executeQuery(
|
|
168
|
+
`DELETE FROM ${tables.STICKER_WEB_GOOGLE_USER}
|
|
169
|
+
WHERE owner_jid = ?
|
|
170
|
+
AND google_sub <> ?`,
|
|
171
|
+
[ownerJid, sub],
|
|
172
|
+
connection,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
await executeQuery(
|
|
176
|
+
`INSERT INTO ${tables.STICKER_WEB_GOOGLE_USER}
|
|
177
|
+
(google_sub, owner_jid, email, name, picture_url, last_login_at, last_seen_at)
|
|
178
|
+
VALUES (?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())
|
|
179
|
+
ON DUPLICATE KEY UPDATE
|
|
180
|
+
owner_jid = VALUES(owner_jid),
|
|
181
|
+
email = VALUES(email),
|
|
182
|
+
name = VALUES(name),
|
|
183
|
+
picture_url = VALUES(picture_url),
|
|
184
|
+
last_login_at = UTC_TIMESTAMP(),
|
|
185
|
+
last_seen_at = UTC_TIMESTAMP()`,
|
|
186
|
+
[sub, ownerJid, email, name, pictureUrl],
|
|
187
|
+
connection,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await executeQuery(
|
|
191
|
+
`UPDATE ${tables.STICKER_WEB_GOOGLE_USER}
|
|
192
|
+
SET owner_phone = COALESCE(?, owner_phone)
|
|
193
|
+
WHERE google_sub = ?`,
|
|
194
|
+
[ownerPhone, sub],
|
|
195
|
+
connection,
|
|
196
|
+
).catch(() => {});
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const upsertGoogleWebSessionRecord = async (session, connection = null) => {
|
|
200
|
+
const token = String(session?.token || '').trim();
|
|
201
|
+
const sub = normalizeGoogleSubject(session?.sub);
|
|
202
|
+
const ownerJid = normalizeJid(session?.ownerJid) || '';
|
|
203
|
+
const ownerPhone = toWhatsAppPhoneDigits(session?.ownerPhone || ownerJid) || null;
|
|
204
|
+
const expiresAt = Number(session?.expiresAt || 0);
|
|
205
|
+
if (!token || !sub || !ownerJid || !Number.isFinite(expiresAt) || expiresAt <= 0) return;
|
|
206
|
+
const email =
|
|
207
|
+
String(session?.email || '')
|
|
208
|
+
.trim()
|
|
209
|
+
.toLowerCase() || null;
|
|
210
|
+
const name = sanitizeText(session?.name || '', 120, { allowEmpty: true }) || null;
|
|
211
|
+
const pictureUrl =
|
|
212
|
+
String(session?.picture || '')
|
|
213
|
+
.trim()
|
|
214
|
+
.slice(0, 1024) || null;
|
|
215
|
+
|
|
216
|
+
await executeQuery(
|
|
217
|
+
`INSERT INTO ${tables.STICKER_WEB_GOOGLE_SESSION}
|
|
218
|
+
(session_token, google_sub, owner_jid, email, name, picture_url, expires_at, revoked_at, last_seen_at)
|
|
219
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, UTC_TIMESTAMP())
|
|
220
|
+
ON DUPLICATE KEY UPDATE
|
|
221
|
+
google_sub = VALUES(google_sub),
|
|
222
|
+
owner_jid = VALUES(owner_jid),
|
|
223
|
+
email = VALUES(email),
|
|
224
|
+
name = VALUES(name),
|
|
225
|
+
picture_url = VALUES(picture_url),
|
|
226
|
+
expires_at = VALUES(expires_at),
|
|
227
|
+
revoked_at = NULL,
|
|
228
|
+
last_seen_at = UTC_TIMESTAMP()`,
|
|
229
|
+
[token, sub, ownerJid, email, name, pictureUrl, new Date(expiresAt)],
|
|
230
|
+
connection,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
await executeQuery(
|
|
234
|
+
`UPDATE ${tables.STICKER_WEB_GOOGLE_SESSION}
|
|
235
|
+
SET owner_phone = COALESCE(?, owner_phone)
|
|
236
|
+
WHERE session_token = ?`,
|
|
237
|
+
[ownerPhone, token],
|
|
238
|
+
connection,
|
|
239
|
+
).catch(() => {});
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const persistGoogleWebSessionToDb = async (session) => {
|
|
243
|
+
if (!session?.token || !session?.sub || !session?.ownerJid) return;
|
|
244
|
+
await maybePruneExpiredGoogleSessionsFromDb();
|
|
245
|
+
await runSqlTransaction(async (connection) => {
|
|
246
|
+
await upsertGoogleWebUserRecord(
|
|
247
|
+
{
|
|
248
|
+
sub: session.sub,
|
|
249
|
+
ownerJid: session.ownerJid,
|
|
250
|
+
email: session.email,
|
|
251
|
+
name: session.name,
|
|
252
|
+
picture: session.picture,
|
|
253
|
+
},
|
|
254
|
+
connection,
|
|
255
|
+
);
|
|
256
|
+
await upsertGoogleWebSessionRecord(session, connection);
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const findGoogleWebSessionInDbByToken = async (sessionToken) => {
|
|
261
|
+
const token = String(sessionToken || '').trim();
|
|
262
|
+
if (!token) return null;
|
|
263
|
+
await maybePruneExpiredGoogleSessionsFromDb();
|
|
264
|
+
const rows = await executeQuery(
|
|
265
|
+
`SELECT session_token, google_sub, owner_jid, owner_phone, email, name, picture_url, created_at, expires_at, last_seen_at
|
|
266
|
+
FROM ${tables.STICKER_WEB_GOOGLE_SESSION}
|
|
267
|
+
WHERE session_token = ?
|
|
268
|
+
AND revoked_at IS NULL
|
|
269
|
+
AND expires_at > UTC_TIMESTAMP()
|
|
270
|
+
LIMIT 1`,
|
|
271
|
+
[token],
|
|
272
|
+
);
|
|
273
|
+
return normalizeGoogleWebSessionRow(Array.isArray(rows) ? rows[0] : null);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const touchGoogleWebSessionSeenInDb = async (sessionToken) => {
|
|
277
|
+
const token = String(sessionToken || '').trim();
|
|
278
|
+
if (!token) return;
|
|
279
|
+
await executeQuery(
|
|
280
|
+
`UPDATE ${tables.STICKER_WEB_GOOGLE_SESSION}
|
|
281
|
+
SET last_seen_at = UTC_TIMESTAMP()
|
|
282
|
+
WHERE session_token = ?
|
|
283
|
+
AND revoked_at IS NULL`,
|
|
284
|
+
[token],
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const touchGoogleWebUserSeenInDb = async (googleSub) => {
|
|
289
|
+
const sub = normalizeGoogleSubject(googleSub);
|
|
290
|
+
if (!sub) return;
|
|
291
|
+
await executeQuery(
|
|
292
|
+
`UPDATE ${tables.STICKER_WEB_GOOGLE_USER}
|
|
293
|
+
SET last_seen_at = UTC_TIMESTAMP()
|
|
294
|
+
WHERE google_sub = ?`,
|
|
295
|
+
[sub],
|
|
296
|
+
);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const deleteGoogleWebSessionFromDb = async (sessionToken) => {
|
|
300
|
+
const token = String(sessionToken || '').trim();
|
|
301
|
+
if (!token) return 0;
|
|
302
|
+
const result = await executeQuery(`DELETE FROM ${tables.STICKER_WEB_GOOGLE_SESSION} WHERE session_token = ?`, [token]);
|
|
303
|
+
return Number(result?.affectedRows || 0);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const createGoogleWebSession = (claims, { ownerJid } = {}) => {
|
|
307
|
+
pruneExpiredGoogleSessions();
|
|
308
|
+
const token = randomUUID();
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
const resolvedOwnerJid = normalizeJid(ownerJid) || buildGoogleOwnerJid(claims.sub);
|
|
311
|
+
const resolvedOwnerPhone = toWhatsAppPhoneDigits(resolvedOwnerJid) || '';
|
|
312
|
+
return {
|
|
313
|
+
token,
|
|
314
|
+
sub: claims.sub,
|
|
315
|
+
email: claims.email || null,
|
|
316
|
+
name: claims.name || null,
|
|
317
|
+
picture: claims.picture || null,
|
|
318
|
+
ownerJid: resolvedOwnerJid,
|
|
319
|
+
ownerPhone: resolvedOwnerPhone,
|
|
320
|
+
createdAt: now,
|
|
321
|
+
expiresAt: now + sessionTtlMs,
|
|
322
|
+
lastSeenAt: now,
|
|
323
|
+
lastDbTouchAt: 0,
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const activateGoogleWebSession = (session) => {
|
|
328
|
+
if (!session?.token) return;
|
|
329
|
+
pruneExpiredGoogleSessions();
|
|
330
|
+
webGoogleSessionMap.set(session.token, session);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const resolveGoogleWebSessionFromRequest = async (req) => {
|
|
334
|
+
pruneExpiredGoogleSessions();
|
|
335
|
+
const sessionTokens = getGoogleWebSessionTokensFromRequest(req);
|
|
336
|
+
if (!sessionTokens.length) return null;
|
|
337
|
+
|
|
338
|
+
for (const sessionToken of sessionTokens) {
|
|
339
|
+
const session = webGoogleSessionMap.get(sessionToken);
|
|
340
|
+
if (!session) continue;
|
|
341
|
+
if (Number(session.expiresAt || 0) <= Date.now()) {
|
|
342
|
+
webGoogleSessionMap.delete(sessionToken);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const now = Date.now();
|
|
347
|
+
session.lastSeenAt = now;
|
|
348
|
+
if (now - Number(session.lastDbTouchAt || 0) >= sessionDbTouchIntervalMs) {
|
|
349
|
+
session.lastDbTouchAt = now;
|
|
350
|
+
void touchGoogleWebSessionSeenInDb(sessionToken).catch((error) => {
|
|
351
|
+
logger.warn('Falha ao atualizar last_seen da sessão Google web.', {
|
|
352
|
+
action: 'sticker_pack_google_web_session_touch_failed',
|
|
353
|
+
error: error?.message,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
void touchGoogleWebUserSeenInDb(session.sub).catch(() => {});
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
await assertGoogleIdentityNotBanned({
|
|
360
|
+
sub: session.sub,
|
|
361
|
+
email: session.email,
|
|
362
|
+
ownerJid: session.ownerJid,
|
|
363
|
+
});
|
|
364
|
+
return session;
|
|
365
|
+
} catch {
|
|
366
|
+
webGoogleSessionMap.delete(sessionToken);
|
|
367
|
+
void deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const sessionToken of sessionTokens) {
|
|
372
|
+
try {
|
|
373
|
+
const persistedSession = await findGoogleWebSessionInDbByToken(sessionToken);
|
|
374
|
+
if (!persistedSession) continue;
|
|
375
|
+
try {
|
|
376
|
+
await assertGoogleIdentityNotBanned({
|
|
377
|
+
sub: persistedSession.sub,
|
|
378
|
+
email: persistedSession.email,
|
|
379
|
+
ownerJid: persistedSession.ownerJid,
|
|
380
|
+
});
|
|
381
|
+
} catch {
|
|
382
|
+
await deleteGoogleWebSessionFromDb(sessionToken).catch(() => {});
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
webGoogleSessionMap.set(sessionToken, persistedSession);
|
|
386
|
+
return persistedSession;
|
|
387
|
+
} catch (error) {
|
|
388
|
+
logger.warn('Falha ao resolver sessão Google web no banco.', {
|
|
389
|
+
action: 'sticker_pack_google_web_session_db_resolve_failed',
|
|
390
|
+
error: error?.message,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return null;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const clearGoogleWebSessionCookie = (req, res) => {
|
|
399
|
+
for (const pathValue of normalizedLegacyCookiePaths) {
|
|
400
|
+
appendSetCookie(
|
|
401
|
+
res,
|
|
402
|
+
buildCookieString(GOOGLE_WEB_SESSION_COOKIE_NAME, '', req, {
|
|
403
|
+
path: pathValue,
|
|
404
|
+
maxAgeSeconds: 0,
|
|
405
|
+
}),
|
|
406
|
+
);
|
|
407
|
+
appendSetCookie(
|
|
408
|
+
res,
|
|
409
|
+
buildCookieString(GOOGLE_WEB_SESSION_COOKIE_NAME, '', req, {
|
|
410
|
+
path: pathValue,
|
|
411
|
+
maxAgeSeconds: 0,
|
|
412
|
+
domain: false,
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const mapGoogleSessionResponseData = (session) =>
|
|
419
|
+
session
|
|
420
|
+
? {
|
|
421
|
+
authenticated: true,
|
|
422
|
+
provider: 'google',
|
|
423
|
+
user: {
|
|
424
|
+
sub: session.sub,
|
|
425
|
+
email: session.email,
|
|
426
|
+
name: session.name,
|
|
427
|
+
picture: session.picture,
|
|
428
|
+
},
|
|
429
|
+
owner_jid: session.ownerJid,
|
|
430
|
+
owner_phone: toWhatsAppPhoneDigits(session.ownerPhone || session.ownerJid) || null,
|
|
431
|
+
expires_at: toIsoOrNull(session.expiresAt),
|
|
432
|
+
}
|
|
433
|
+
: {
|
|
434
|
+
authenticated: false,
|
|
435
|
+
provider: 'google',
|
|
436
|
+
user: null,
|
|
437
|
+
owner_jid: null,
|
|
438
|
+
owner_phone: null,
|
|
439
|
+
expires_at: null,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const revokeGoogleWebSessionsByIdentity = async ({ googleSub = '', email = '', ownerJid = '' } = {}) => {
|
|
443
|
+
const normalizedSub = normalizeGoogleSubject(googleSub);
|
|
444
|
+
const normalizedEmail = normalizeEmail(email);
|
|
445
|
+
const normalizedOwnerJid = normalizeJid(ownerJid) || '';
|
|
446
|
+
|
|
447
|
+
const clauses = [];
|
|
448
|
+
const params = [];
|
|
449
|
+
if (normalizedSub) {
|
|
450
|
+
clauses.push('google_sub = ?');
|
|
451
|
+
params.push(normalizedSub);
|
|
452
|
+
}
|
|
453
|
+
if (normalizedEmail) {
|
|
454
|
+
clauses.push('email = ?');
|
|
455
|
+
params.push(normalizedEmail);
|
|
456
|
+
}
|
|
457
|
+
if (normalizedOwnerJid) {
|
|
458
|
+
clauses.push('owner_jid = ?');
|
|
459
|
+
params.push(normalizedOwnerJid);
|
|
460
|
+
}
|
|
461
|
+
if (!clauses.length) return 0;
|
|
462
|
+
|
|
463
|
+
await executeQuery(
|
|
464
|
+
`DELETE FROM ${tables.STICKER_WEB_GOOGLE_SESSION}
|
|
465
|
+
WHERE ${clauses.join(' OR ')}`,
|
|
466
|
+
params,
|
|
467
|
+
).catch(() => {});
|
|
468
|
+
|
|
469
|
+
let removed = 0;
|
|
470
|
+
for (const [token, session] of webGoogleSessionMap.entries()) {
|
|
471
|
+
if (!session) continue;
|
|
472
|
+
const sessionSub = normalizeGoogleSubject(session.sub);
|
|
473
|
+
const sessionEmail = normalizeEmail(session.email);
|
|
474
|
+
const sessionOwner = normalizeJid(session.ownerJid) || '';
|
|
475
|
+
if ((normalizedSub && sessionSub === normalizedSub) || (normalizedEmail && sessionEmail === normalizedEmail) || (normalizedOwnerJid && sessionOwner === normalizedOwnerJid)) {
|
|
476
|
+
webGoogleSessionMap.delete(token);
|
|
477
|
+
removed += 1;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return removed;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const handleGoogleAuthSessionRequest = async (req, res) => {
|
|
485
|
+
if (!googleClientId) {
|
|
486
|
+
sendJson(req, res, 404, { error: 'Login Google desabilitado.' });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
491
|
+
const session = await resolveGoogleWebSessionFromRequest(req);
|
|
492
|
+
sendJson(req, res, 200, {
|
|
493
|
+
data: mapGoogleSessionResponseData(session),
|
|
494
|
+
});
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (req.method === 'DELETE') {
|
|
499
|
+
const tokens = getGoogleWebSessionTokensFromRequest(req);
|
|
500
|
+
for (const token of tokens) {
|
|
501
|
+
webGoogleSessionMap.delete(token);
|
|
502
|
+
await deleteGoogleWebSessionFromDb(token).catch((error) => {
|
|
503
|
+
logger.warn('Falha ao remover sessão Google web do banco.', {
|
|
504
|
+
action: 'sticker_pack_google_web_session_db_delete_failed',
|
|
505
|
+
error: error?.message,
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
clearGoogleWebSessionCookie(req, res);
|
|
510
|
+
sendJson(req, res, 200, { data: { cleared: true } });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (req.method !== 'POST') {
|
|
515
|
+
sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let payload = {};
|
|
520
|
+
try {
|
|
521
|
+
payload = await readJsonBody(req);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
sendJson(req, res, Number(error?.statusCode || 400), { error: error?.message || 'Body inválido.' });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
const claims = await verifyGoogleIdToken(payload?.google_id_token || payload?.id_token);
|
|
529
|
+
const linkedOwner = resolveWhatsAppOwnerJidFromLoginPayload(payload);
|
|
530
|
+
if (!linkedOwner.ownerJid) {
|
|
531
|
+
if (!linkedOwner.hasPayload) {
|
|
532
|
+
sendJson(req, res, 400, {
|
|
533
|
+
error: 'Abra esta pagina pelo link enviado no WhatsApp. Envie "iniciar" no bot para gerar o link de login.',
|
|
534
|
+
code: 'WHATSAPP_LOGIN_LINK_REQUIRED',
|
|
535
|
+
reason: 'missing_link',
|
|
536
|
+
});
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const reason = String(linkedOwner.reason || '')
|
|
541
|
+
.trim()
|
|
542
|
+
.toLowerCase();
|
|
543
|
+
const isUnauthorizedAttempt = ['invalid_signature', 'missing_signature'].includes(reason);
|
|
544
|
+
const statusCode = isUnauthorizedAttempt ? 403 : 400;
|
|
545
|
+
const errorMessage = reason === 'expired' ? 'Link de login expirado. Envie "iniciar" novamente no WhatsApp.' : isUnauthorizedAttempt ? 'Tentativa de login sem permissao detectada. Gere um novo link enviando "iniciar" no privado do bot.' : 'Link de login invalido. Envie "iniciar" novamente no WhatsApp.';
|
|
546
|
+
|
|
547
|
+
logger.warn('Tentativa de login web bloqueada por validacao do link WhatsApp.', {
|
|
548
|
+
action: 'sticker_pack_google_web_login_link_blocked',
|
|
549
|
+
reason: reason || 'unknown',
|
|
550
|
+
remote_ip: req.socket?.remoteAddress || null,
|
|
551
|
+
user_agent: req.headers?.['user-agent'] || null,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
sendJson(req, res, statusCode, {
|
|
555
|
+
error: errorMessage,
|
|
556
|
+
code: 'WHATSAPP_LOGIN_LINK_INVALID',
|
|
557
|
+
reason: reason || 'invalid_link',
|
|
558
|
+
});
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const ownerJid = linkedOwner.ownerJid;
|
|
562
|
+
|
|
563
|
+
await assertGoogleIdentityNotBanned({
|
|
564
|
+
sub: claims.sub,
|
|
565
|
+
email: claims.email,
|
|
566
|
+
ownerJid,
|
|
567
|
+
});
|
|
568
|
+
const session = createGoogleWebSession(claims, { ownerJid });
|
|
569
|
+
if (!session.ownerJid) {
|
|
570
|
+
sendJson(req, res, 400, { error: 'Nao foi possivel vincular a conta Google.' });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
await persistGoogleWebSessionToDb(session);
|
|
575
|
+
activateGoogleWebSession(session);
|
|
576
|
+
} catch (persistError) {
|
|
577
|
+
logger.error('Falha ao persistir sessão Google web no banco.', {
|
|
578
|
+
action: 'sticker_pack_google_web_session_db_persist_failed',
|
|
579
|
+
error: persistError?.message,
|
|
580
|
+
});
|
|
581
|
+
sendJson(req, res, 500, { error: 'Falha ao salvar sessão Google. Tente novamente.' });
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
appendSetCookie(
|
|
586
|
+
res,
|
|
587
|
+
buildCookieString(GOOGLE_WEB_SESSION_COOKIE_NAME, session.token, req, {
|
|
588
|
+
path: normalizedSessionCookiePath,
|
|
589
|
+
maxAgeSeconds: Math.floor(sessionTtlMs / 1000),
|
|
590
|
+
}),
|
|
591
|
+
);
|
|
592
|
+
sendJson(req, res, 200, {
|
|
593
|
+
data: mapGoogleSessionResponseData(session),
|
|
594
|
+
});
|
|
595
|
+
} catch (error) {
|
|
596
|
+
sendJson(req, res, Number(error?.statusCode || 401), {
|
|
597
|
+
error: error?.message || 'Login Google inválido.',
|
|
598
|
+
code: notAllowedErrorCode,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
cookieName: GOOGLE_WEB_SESSION_COOKIE_NAME,
|
|
605
|
+
getGoogleWebSessionTokensFromRequest,
|
|
606
|
+
upsertGoogleWebUserRecord,
|
|
607
|
+
resolveGoogleWebSessionFromRequest,
|
|
608
|
+
clearGoogleWebSessionCookie,
|
|
609
|
+
deleteGoogleWebSessionFromDb,
|
|
610
|
+
mapGoogleSessionResponseData,
|
|
611
|
+
handleGoogleAuthSessionRequest,
|
|
612
|
+
revokeGoogleWebSessionsByIdentity,
|
|
613
|
+
};
|
|
614
|
+
};
|