@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,141 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { URL } from 'node:url';
4
+
5
+ import logger from '../../app/utils/logger/loggerModule.js';
6
+
7
+ const parseEnvBool = (value, fallback) => {
8
+ if (value === undefined || value === null || value === '') return fallback;
9
+ const normalized = String(value).trim().toLowerCase();
10
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
11
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
12
+ return fallback;
13
+ };
14
+
15
+ const normalizeBasePath = (value, fallback) => {
16
+ const raw = String(value || '').trim() || fallback;
17
+ const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`;
18
+ const withoutTrailingSlash = withLeadingSlash.length > 1 && withLeadingSlash.endsWith('/') ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
19
+ return withoutTrailingSlash || fallback;
20
+ };
21
+
22
+ const STICKER_API_BASE_PATH = normalizeBasePath(process.env.STICKER_API_BASE_PATH, '/api/sticker-packs');
23
+ const STICKER_WEB_PATH = normalizeBasePath(process.env.STICKER_WEB_PATH, '/stickers');
24
+ const STICKER_ADMIN_WEB_PATH = `${STICKER_WEB_PATH}/admin`;
25
+ const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PATH, '/user');
26
+ const USER_SYSTEMADM_WEB_PATH = `${USER_PROFILE_WEB_PATH}/systemadm`;
27
+ const STICKER_ADMIN_REDIRECT_TO_USER = parseEnvBool(process.env.STICKER_ADMIN_REDIRECT_TO_USER, true);
28
+ const SITE_ORIGIN = String(process.env.SITE_ORIGIN || 'https://omnizap.shop')
29
+ .trim()
30
+ .replace(/\/+$/, '');
31
+
32
+ const USER_SYSTEMADM_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'user', 'systemadm', 'index.html');
33
+ const LEGACY_STICKER_ADMIN_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'stickers', 'admin', 'index.html');
34
+
35
+ let stickerCatalogControllerPromise = null;
36
+ const loadStickerCatalogController = async () => {
37
+ if (!stickerCatalogControllerPromise) {
38
+ stickerCatalogControllerPromise = import('./stickerCatalogController.js');
39
+ }
40
+ return stickerCatalogControllerPromise;
41
+ };
42
+
43
+ const sendHtml = (req, res, html) => {
44
+ res.statusCode = 200;
45
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
46
+ res.setHeader('Cache-Control', 'no-store');
47
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
48
+ if (req.method === 'HEAD') {
49
+ res.end();
50
+ return;
51
+ }
52
+ res.end(html);
53
+ };
54
+
55
+ const sendJson = (req, res, statusCode, payload) => {
56
+ const body = JSON.stringify(payload);
57
+ res.statusCode = statusCode;
58
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
59
+ res.setHeader('Cache-Control', 'no-store');
60
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
61
+ if (req.method === 'HEAD') {
62
+ res.end();
63
+ return;
64
+ }
65
+ res.end(body);
66
+ };
67
+
68
+ const sendRedirect = (res, location) => {
69
+ res.statusCode = 302;
70
+ res.setHeader('Location', location);
71
+ res.setHeader('Cache-Control', 'no-store');
72
+ res.end();
73
+ };
74
+
75
+ export const getSystemAdminRouteConfig = () => ({
76
+ webPath: USER_SYSTEMADM_WEB_PATH,
77
+ legacyWebPath: STICKER_ADMIN_WEB_PATH,
78
+ apiAdminBasePath: `${STICKER_API_BASE_PATH}/admin`,
79
+ apiAdminSessionPath: `${STICKER_API_BASE_PATH}/admin/session`,
80
+ });
81
+
82
+ export const maybeHandleSystemAdminRequest = async (req, res, { pathname, url }) => {
83
+ if (!['GET', 'HEAD', 'POST', 'PATCH', 'DELETE'].includes(req.method || '')) return false;
84
+
85
+ if (pathname === USER_SYSTEMADM_WEB_PATH || pathname === `${USER_SYSTEMADM_WEB_PATH}/`) {
86
+ if (!['GET', 'HEAD'].includes(req.method || '')) return false;
87
+ try {
88
+ const html = await fs.readFile(USER_SYSTEMADM_TEMPLATE_PATH, 'utf8');
89
+ sendHtml(req, res, html);
90
+ } catch (error) {
91
+ if (error?.code === 'ENOENT') {
92
+ sendJson(req, res, 404, { error: 'Template da pagina system admin nao encontrado.' });
93
+ return true;
94
+ }
95
+ logger.error('Falha ao renderizar pagina system admin.', {
96
+ action: 'user_system_admin_page_render_failed',
97
+ path: pathname,
98
+ error: error?.message,
99
+ });
100
+ sendJson(req, res, 500, { error: 'Falha interna ao renderizar pagina system admin.' });
101
+ }
102
+ return true;
103
+ }
104
+
105
+ if (pathname === STICKER_ADMIN_WEB_PATH || pathname === `${STICKER_ADMIN_WEB_PATH}/`) {
106
+ if (!['GET', 'HEAD'].includes(req.method || '')) return false;
107
+ if (STICKER_ADMIN_REDIRECT_TO_USER) {
108
+ const requestUrl = new URL(req.url || `${STICKER_ADMIN_WEB_PATH}/`, SITE_ORIGIN);
109
+ const userUrl = new URL(`${USER_SYSTEMADM_WEB_PATH}/`, SITE_ORIGIN);
110
+ for (const [key, value] of requestUrl.searchParams.entries()) {
111
+ userUrl.searchParams.append(key, value);
112
+ }
113
+ sendRedirect(res, `${userUrl.pathname}${userUrl.search}`);
114
+ return true;
115
+ }
116
+ try {
117
+ const html = await fs.readFile(LEGACY_STICKER_ADMIN_TEMPLATE_PATH, 'utf8');
118
+ sendHtml(req, res, html);
119
+ } catch (error) {
120
+ if (error?.code === 'ENOENT') {
121
+ sendJson(req, res, 404, { error: 'Template do painel admin nao encontrado.' });
122
+ return true;
123
+ }
124
+ logger.error('Falha ao renderizar pagina admin legado.', {
125
+ action: 'legacy_sticker_admin_page_render_failed',
126
+ path: pathname,
127
+ error: error?.message,
128
+ });
129
+ sendJson(req, res, 500, { error: 'Falha interna ao renderizar painel admin.' });
130
+ }
131
+ return true;
132
+ }
133
+
134
+ if (pathname === `${STICKER_API_BASE_PATH}/admin/session` || pathname.startsWith(`${STICKER_API_BASE_PATH}/admin/`)) {
135
+ const controller = await loadStickerCatalogController();
136
+ if (typeof controller?.maybeHandleStickerCatalogRequest !== 'function') return false;
137
+ return controller.maybeHandleStickerCatalogRequest(req, res, { pathname, url });
138
+ }
139
+
140
+ return false;
141
+ };
@@ -0,0 +1,87 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import logger from '../../app/utils/logger/loggerModule.js';
5
+
6
+ const normalizeBasePath = (value, fallback) => {
7
+ const raw = String(value || '').trim() || fallback;
8
+ const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`;
9
+ const withoutTrailingSlash = withLeadingSlash.length > 1 && withLeadingSlash.endsWith('/') ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
10
+ return withoutTrailingSlash || fallback;
11
+ };
12
+
13
+ const STICKER_API_BASE_PATH = normalizeBasePath(process.env.STICKER_API_BASE_PATH, '/api/sticker-packs');
14
+ const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PATH, '/user');
15
+ const USER_DASHBOARD_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'user', 'index.html');
16
+
17
+ const USER_API_PATHS = new Set([`${STICKER_API_BASE_PATH}/auth/google/session`, `${STICKER_API_BASE_PATH}/me`, `${STICKER_API_BASE_PATH}/bot-contact`]);
18
+
19
+ let stickerCatalogControllerPromise = null;
20
+ const loadStickerCatalogController = async () => {
21
+ if (!stickerCatalogControllerPromise) {
22
+ stickerCatalogControllerPromise = import('./stickerCatalogController.js');
23
+ }
24
+ return stickerCatalogControllerPromise;
25
+ };
26
+
27
+ const sendHtml = (req, res, html) => {
28
+ res.statusCode = 200;
29
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
30
+ res.setHeader('Cache-Control', 'no-store');
31
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
32
+ if (req.method === 'HEAD') {
33
+ res.end();
34
+ return;
35
+ }
36
+ res.end(html);
37
+ };
38
+
39
+ const sendJson = (req, res, statusCode, payload) => {
40
+ const body = JSON.stringify(payload);
41
+ res.statusCode = statusCode;
42
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
43
+ res.setHeader('Cache-Control', 'no-store');
44
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
45
+ if (req.method === 'HEAD') {
46
+ res.end();
47
+ return;
48
+ }
49
+ res.end(body);
50
+ };
51
+
52
+ export const getUserRouteConfig = () => ({
53
+ webPath: USER_PROFILE_WEB_PATH,
54
+ apiBasePath: STICKER_API_BASE_PATH,
55
+ });
56
+
57
+ export const maybeHandleUserRequest = async (req, res, { pathname, url }) => {
58
+ if (!['GET', 'HEAD', 'POST', 'PATCH', 'DELETE'].includes(req.method || '')) return false;
59
+
60
+ if (pathname === USER_PROFILE_WEB_PATH || pathname === `${USER_PROFILE_WEB_PATH}/`) {
61
+ if (!['GET', 'HEAD'].includes(req.method || '')) return false;
62
+ try {
63
+ const html = await fs.readFile(USER_DASHBOARD_TEMPLATE_PATH, 'utf8');
64
+ sendHtml(req, res, html);
65
+ } catch (error) {
66
+ if (error?.code === 'ENOENT') {
67
+ sendJson(req, res, 404, { error: 'Template da pagina de usuario nao encontrado.' });
68
+ return true;
69
+ }
70
+ logger.error('Falha ao renderizar pagina de usuario.', {
71
+ action: 'user_page_render_failed',
72
+ path: pathname,
73
+ error: error?.message,
74
+ });
75
+ sendJson(req, res, 500, { error: 'Falha interna ao renderizar pagina de usuario.' });
76
+ }
77
+ return true;
78
+ }
79
+
80
+ if (USER_API_PATHS.has(pathname)) {
81
+ const controller = await loadStickerCatalogController();
82
+ if (typeof controller?.maybeHandleStickerCatalogRequest !== 'function') return false;
83
+ return controller.maybeHandleStickerCatalogRequest(req, res, { pathname, url });
84
+ }
85
+
86
+ return false;
87
+ };
@@ -1,9 +1,11 @@
1
1
  import http from 'node:http';
2
+
2
3
  import logger from '../../app/utils/logger/loggerModule.js';
3
4
  import { getMetricsServerConfig, isMetricsEnabled, recordHttpRequest, resolveRouteGroup } from '../../app/observability/metrics.js';
5
+ import { applyCachePolicy } from '../middleware/cachePolicy.js';
6
+ import { applySecurityHeaders } from '../middleware/securityHeaders.js';
7
+ import { getIndexRouteConfigs, routeRequest } from '../routes/indexRouter.js';
4
8
  import { parseRequestUrl, normalizeRequestId } from './requestContext.js';
5
- import { maybeHandleMetricsRoute } from '../routes/metricsRoute.js';
6
- import { getStickerCatalogRouteConfig, maybeHandleStickerCatalogRoute } from '../routes/stickerCatalogRoute.js';
7
9
 
8
10
  let server = null;
9
11
  let serverStarted = false;
@@ -12,6 +14,7 @@ export const startHttpServer = () => {
12
14
  if (!isMetricsEnabled() || serverStarted) return;
13
15
 
14
16
  const { host, port, path: metricsPath } = getMetricsServerConfig();
17
+
15
18
  server = http.createServer(async (req, res) => {
16
19
  const requestStartedAt = Date.now();
17
20
  const requestId = normalizeRequestId(req.headers['x-request-id']);
@@ -19,10 +22,22 @@ export const startHttpServer = () => {
19
22
 
20
23
  const parsedUrl = parseRequestUrl(req, host, port);
21
24
  const pathname = parsedUrl.pathname;
25
+
26
+ let routeConfigs = null;
27
+ try {
28
+ routeConfigs = await getIndexRouteConfigs();
29
+ } catch (error) {
30
+ logger.error('Erro ao carregar configuracao de rotas.', {
31
+ error: error?.message,
32
+ });
33
+ }
34
+
22
35
  let routeGroup = resolveRouteGroup({
23
36
  pathname,
24
37
  metricsPath,
25
- catalogConfig: null,
38
+ catalogConfig: routeConfigs?.stickerConfig || null,
39
+ userConfig: routeConfigs?.userConfig || null,
40
+ systemAdminConfig: routeConfigs?.systemAdminConfig || null,
26
41
  });
27
42
 
28
43
  res.once('finish', () => {
@@ -35,31 +50,36 @@ export const startHttpServer = () => {
35
50
  });
36
51
 
37
52
  try {
38
- const catalogConfig = await getStickerCatalogRouteConfig();
39
- routeGroup = resolveRouteGroup({
53
+ applySecurityHeaders(req, res);
54
+ applyCachePolicy(req, res, { pathname });
55
+
56
+ await routeRequest(req, res, {
40
57
  pathname,
58
+ url: parsedUrl,
41
59
  metricsPath,
42
- catalogConfig,
60
+ configs: routeConfigs,
43
61
  });
44
- const handledByCatalog = await maybeHandleStickerCatalogRoute(req, res, {
62
+
63
+ routeGroup = resolveRouteGroup({
45
64
  pathname,
46
- url: parsedUrl,
65
+ metricsPath,
66
+ catalogConfig: routeConfigs?.stickerConfig || null,
67
+ userConfig: routeConfigs?.userConfig || null,
68
+ systemAdminConfig: routeConfigs?.systemAdminConfig || null,
47
69
  });
48
- if (handledByCatalog) return;
49
70
  } catch (error) {
50
- logger.error('Erro ao inicializar rotas web de sticker packs.', {
71
+ logger.error('Falha ao processar request HTTP.', {
72
+ path: pathname,
73
+ method: req.method,
51
74
  error: error?.message,
52
75
  });
53
- }
54
76
 
55
- const handledMetrics = await maybeHandleMetricsRoute(req, res, {
56
- pathname,
57
- metricsPath,
58
- });
59
- if (handledMetrics) return;
60
-
61
- res.statusCode = 404;
62
- res.end('Not Found');
77
+ if (!res.writableEnded) {
78
+ res.statusCode = 500;
79
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
80
+ res.end(JSON.stringify({ error: 'Internal Server Error' }));
81
+ }
82
+ }
63
83
  });
64
84
 
65
85
  server.listen(port, host, () => {
@@ -70,21 +90,41 @@ export const startHttpServer = () => {
70
90
  metrics_path: metricsPath,
71
91
  });
72
92
 
73
- getStickerCatalogRouteConfig()
74
- .then((config) => {
75
- if (!config?.enabled) return;
76
- logger.info('Catalogo web de sticker packs habilitado', {
77
- web_path: config.webPath,
78
- api_base_path: config.apiBasePath,
79
- orphan_api_path: config.orphanApiPath,
80
- data_public_path: config.dataPublicPath,
81
- data_public_dir: config.dataPublicDir,
82
- host,
83
- port,
84
- });
93
+ getIndexRouteConfigs()
94
+ .then(({ userConfig, systemAdminConfig, stickerConfig }) => {
95
+ if (userConfig?.webPath) {
96
+ logger.info('Rotas web de usuario habilitadas', {
97
+ web_path: userConfig.webPath,
98
+ api_base_path: userConfig.apiBasePath,
99
+ host,
100
+ port,
101
+ });
102
+ }
103
+
104
+ if (systemAdminConfig?.webPath) {
105
+ logger.info('Rotas system admin habilitadas', {
106
+ web_path: systemAdminConfig.webPath,
107
+ legacy_web_path: systemAdminConfig.legacyWebPath,
108
+ api_admin_base_path: systemAdminConfig.apiAdminBasePath,
109
+ host,
110
+ port,
111
+ });
112
+ }
113
+
114
+ if (stickerConfig?.enabled && stickerConfig?.webPath) {
115
+ logger.info('Catalogo web de sticker packs habilitado', {
116
+ web_path: stickerConfig.webPath,
117
+ api_base_path: stickerConfig.apiBasePath,
118
+ orphan_api_path: stickerConfig.orphanApiPath,
119
+ data_public_path: stickerConfig.dataPublicPath,
120
+ data_public_dir: stickerConfig.dataPublicDir,
121
+ host,
122
+ port,
123
+ });
124
+ }
85
125
  })
86
126
  .catch((error) => {
87
- logger.warn('Nao foi possivel carregar configuracao do catalogo de sticker packs.', {
127
+ logger.warn('Nao foi possivel carregar configuracao das rotas na inicializacao.', {
88
128
  error: error?.message,
89
129
  });
90
130
  });
@@ -0,0 +1,24 @@
1
+ import { URL } from 'node:url';
2
+
3
+ import { isAssetPath } from './cachePolicyHelpers.js';
4
+
5
+ export const applyCachePolicy = (req, res, { pathname } = {}) => {
6
+ const resolvedPathname = pathname || new URL(req.url || '/', 'http://localhost').pathname;
7
+
8
+ if (isAssetPath(resolvedPathname)) {
9
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
10
+ return;
11
+ }
12
+
13
+ if (resolvedPathname === '/sitemap.xml' || resolvedPathname.startsWith('/stickers')) {
14
+ res.setHeader('Cache-Control', 'public, max-age=60');
15
+ return;
16
+ }
17
+
18
+ if (resolvedPathname.startsWith('/api/')) {
19
+ res.setHeader('Cache-Control', 'no-store');
20
+ return;
21
+ }
22
+
23
+ res.setHeader('Cache-Control', 'no-store');
24
+ };
@@ -0,0 +1 @@
1
+ export const isAssetPath = (pathname = '') => pathname === '/stickers/assets/styles.css' || pathname === '/stickers/assets/catalog.js' || pathname.startsWith('/stickers/assets/');
@@ -0,0 +1,89 @@
1
+ const rateLimitBuckets = new Map();
2
+ let pruneAt = 0;
3
+
4
+ const parseNumber = (value, fallback, min, max) => {
5
+ const parsed = Number(value);
6
+ if (!Number.isFinite(parsed)) return fallback;
7
+ return Math.max(min, Math.min(max, Math.floor(parsed)));
8
+ };
9
+
10
+ const RATE_LIMIT_TRUST_PROXY = ['1', 'true', 'yes', 'on'].includes(
11
+ String(process.env.RATE_LIMIT_TRUST_PROXY || '')
12
+ .trim()
13
+ .toLowerCase(),
14
+ );
15
+
16
+ const getClientIp = (req) => {
17
+ if (RATE_LIMIT_TRUST_PROXY) {
18
+ const forwarded = req.headers['x-forwarded-for'];
19
+ if (typeof forwarded === 'string' && forwarded.trim()) {
20
+ const [first] = forwarded
21
+ .split(',')
22
+ .map((part) => part.trim())
23
+ .filter(Boolean);
24
+ if (first) return first;
25
+ }
26
+ }
27
+
28
+ return req.socket?.remoteAddress || 'unknown';
29
+ };
30
+
31
+ const pruneBuckets = (windowMs, nowMs) => {
32
+ if (nowMs - pruneAt < windowMs) return;
33
+ pruneAt = nowMs;
34
+
35
+ for (const [key, bucket] of rateLimitBuckets.entries()) {
36
+ if (nowMs - bucket.start > windowMs) {
37
+ rateLimitBuckets.delete(key);
38
+ }
39
+ }
40
+ };
41
+
42
+ const sendTooManyRequests = (req, res, retryAfterSeconds) => {
43
+ if (res.writableEnded) return;
44
+ res.statusCode = 429;
45
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
46
+ res.setHeader('Retry-After', String(Math.max(1, retryAfterSeconds)));
47
+ if (req.method === 'HEAD') {
48
+ res.end();
49
+ return;
50
+ }
51
+ res.end(JSON.stringify({ error: 'Too Many Requests' }));
52
+ };
53
+
54
+ export const createRateLimit = ({ windowMs = 60_000, max = 60, keyPrefix = 'global' } = {}) => {
55
+ const safeWindowMs = parseNumber(windowMs, 60_000, 1_000, 60 * 60 * 1000);
56
+ const safeMax = parseNumber(max, 60, 1, 100_000);
57
+ const safeKeyPrefix = String(keyPrefix || 'global').trim() || 'global';
58
+
59
+ return (req, res) => {
60
+ const nowMs = Date.now();
61
+ pruneBuckets(safeWindowMs, nowMs);
62
+
63
+ const ip = getClientIp(req);
64
+ const key = `${safeKeyPrefix}:${ip}`;
65
+ const existing = rateLimitBuckets.get(key);
66
+
67
+ if (!existing || nowMs - existing.start > safeWindowMs) {
68
+ rateLimitBuckets.set(key, { start: nowMs, count: 1 });
69
+ return true;
70
+ }
71
+
72
+ existing.count += 1;
73
+ if (existing.count <= safeMax) return true;
74
+
75
+ const retryAfterSeconds = Math.ceil((safeWindowMs - (nowMs - existing.start)) / 1000);
76
+ sendTooManyRequests(req, res, retryAfterSeconds);
77
+ return false;
78
+ };
79
+ };
80
+
81
+ export const createAdminApiRateLimit = () => {
82
+ const windowMs = parseNumber(process.env.ADMIN_RATE_LIMIT_WINDOW_MS, 60_000, 1_000, 60 * 60 * 1000);
83
+ const max = parseNumber(process.env.ADMIN_RATE_LIMIT_MAX, 30, 1, 100_000);
84
+ return createRateLimit({
85
+ windowMs,
86
+ max,
87
+ keyPrefix: 'admin_api',
88
+ });
89
+ };
@@ -0,0 +1,16 @@
1
+ import logger from '../../app/utils/logger/loggerModule.js';
2
+
3
+ export const attachRequestLogger = (req, res, { pathname = '', requestId = '', startedAt = Date.now() } = {}) => {
4
+ if (req.__requestLoggerAttached) return;
5
+ req.__requestLoggerAttached = true;
6
+
7
+ res.once('finish', () => {
8
+ logger.info('HTTP request', {
9
+ request_id: requestId || null,
10
+ method: req.method || 'UNKNOWN',
11
+ path: pathname || null,
12
+ status_code: res.statusCode,
13
+ duration_ms: Math.max(0, Date.now() - Number(startedAt || Date.now())),
14
+ });
15
+ });
16
+ };
@@ -0,0 +1,42 @@
1
+ import logger from '../../app/utils/logger/loggerModule.js';
2
+
3
+ const ADMIN_TOKEN = String(process.env.ADMIN_TOKEN || process.env.ADMIN_API_TOKEN || '').trim();
4
+
5
+ const extractAdminTokenFromRequest = (req) => {
6
+ const headerToken = String(req.headers['x-admin-token'] || '').trim();
7
+ if (headerToken) return headerToken;
8
+
9
+ const authorizationHeader = String(req.headers.authorization || '').trim();
10
+ if (!authorizationHeader) return '';
11
+
12
+ const match = authorizationHeader.match(/^Bearer\s+(.+)$/i);
13
+ return String(match?.[1] || '').trim();
14
+ };
15
+
16
+ const sendUnauthorized = (res) => {
17
+ if (res.writableEnded) return;
18
+ res.statusCode = 401;
19
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
20
+ res.setHeader('Cache-Control', 'no-store');
21
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
22
+ };
23
+
24
+ /**
25
+ * Camada opcional de auth de admin.
26
+ * Quando `ADMIN_TOKEN` nao estiver configurado, delega para a auth interna do controller.
27
+ */
28
+ export const requireAdminAuth = (req, res) => {
29
+ if (!ADMIN_TOKEN) return true;
30
+
31
+ const requestToken = extractAdminTokenFromRequest(req);
32
+ if (requestToken && requestToken === ADMIN_TOKEN) return true;
33
+
34
+ logger.warn('Tentativa de acesso admin sem token valido.', {
35
+ action: 'admin_auth_token_invalid',
36
+ method: req.method || 'UNKNOWN',
37
+ path: req.url || '',
38
+ remote_address: req.socket?.remoteAddress || null,
39
+ });
40
+ sendUnauthorized(res);
41
+ return false;
42
+ };
@@ -0,0 +1,6 @@
1
+ export const applySecurityHeaders = (_req, res) => {
2
+ res.setHeader('X-Content-Type-Options', 'nosniff');
3
+ res.setHeader('X-Frame-Options', 'DENY');
4
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
5
+ res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
6
+ };
@@ -0,0 +1,56 @@
1
+ let systemAdminControllerPromise = null;
2
+
3
+ const loadSystemAdminController = async () => {
4
+ if (!systemAdminControllerPromise) {
5
+ systemAdminControllerPromise = import('../../controllers/systemAdminController.js');
6
+ }
7
+ return systemAdminControllerPromise;
8
+ };
9
+
10
+ const normalizeBasePath = (value, fallback) => {
11
+ const raw = String(value || '').trim() || fallback;
12
+ const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`;
13
+ const withoutTrailingSlash = withLeadingSlash.length > 1 && withLeadingSlash.endsWith('/') ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
14
+ return withoutTrailingSlash || fallback;
15
+ };
16
+
17
+ const startsWithPath = (pathname, prefix) => {
18
+ if (!pathname || !prefix) return false;
19
+ if (pathname === prefix) return true;
20
+ return pathname.startsWith(`${prefix}/`);
21
+ };
22
+
23
+ const DEFAULT_USER_SYSTEM_ADMIN_WEB_PATH = '/user/systemadm';
24
+ const DEFAULT_LEGACY_STICKER_ADMIN_WEB_PATH = '/stickers/admin';
25
+ const DEFAULT_STICKER_ADMIN_API_BASE_PATH = '/api/sticker-packs/admin';
26
+ const DEFAULT_STICKER_ADMIN_API_SESSION_PATH = '/api/sticker-packs/admin/session';
27
+
28
+ export const getSystemAdminRouterConfig = async () => {
29
+ const controller = await loadSystemAdminController();
30
+ const legacyConfig = (typeof controller?.getSystemAdminRouteConfig === 'function' ? controller.getSystemAdminRouteConfig() : null) || {};
31
+ return {
32
+ webPath: normalizeBasePath(legacyConfig.webPath, DEFAULT_USER_SYSTEM_ADMIN_WEB_PATH),
33
+ legacyWebPath: normalizeBasePath(legacyConfig.legacyWebPath, DEFAULT_LEGACY_STICKER_ADMIN_WEB_PATH),
34
+ apiAdminBasePath: normalizeBasePath(legacyConfig.apiAdminBasePath, DEFAULT_STICKER_ADMIN_API_BASE_PATH),
35
+ apiAdminSessionPath: normalizeBasePath(legacyConfig.apiAdminSessionPath, DEFAULT_STICKER_ADMIN_API_SESSION_PATH),
36
+ };
37
+ };
38
+
39
+ export const shouldHandleSystemAdminPath = (pathname, systemAdminConfig = null) => {
40
+ const resolvedConfig = systemAdminConfig || {
41
+ webPath: DEFAULT_USER_SYSTEM_ADMIN_WEB_PATH,
42
+ legacyWebPath: DEFAULT_LEGACY_STICKER_ADMIN_WEB_PATH,
43
+ apiAdminBasePath: DEFAULT_STICKER_ADMIN_API_BASE_PATH,
44
+ apiAdminSessionPath: DEFAULT_STICKER_ADMIN_API_SESSION_PATH,
45
+ };
46
+
47
+ if (startsWithPath(pathname, resolvedConfig.webPath)) return true;
48
+ if (startsWithPath(pathname, resolvedConfig.legacyWebPath)) return true;
49
+ return false;
50
+ };
51
+
52
+ export const maybeHandleSystemAdminRequest = async (req, res, { pathname, url }) => {
53
+ const controller = await loadSystemAdminController();
54
+ if (typeof controller?.maybeHandleSystemAdminRequest !== 'function') return false;
55
+ return controller.maybeHandleSystemAdminRequest(req, res, { pathname, url });
56
+ };
@@ -0,0 +1,41 @@
1
+ const sendJson = (req, res, statusCode, payload) => {
2
+ if (res.writableEnded) return true;
3
+ res.statusCode = statusCode;
4
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
5
+ if (req.method === 'HEAD') {
6
+ res.end();
7
+ return true;
8
+ }
9
+ res.end(JSON.stringify(payload));
10
+ return true;
11
+ };
12
+
13
+ const isAllowedMethod = (method) => method === 'GET' || method === 'HEAD';
14
+
15
+ export const shouldHandleHealthPath = (pathname) => pathname === '/healthz' || pathname === '/readyz';
16
+
17
+ export const maybeHandleHealthRequest = async (req, res, { pathname }) => {
18
+ if (!shouldHandleHealthPath(pathname)) return false;
19
+
20
+ if (!isAllowedMethod(req.method || '')) {
21
+ return sendJson(req, res, 405, { error: 'Method Not Allowed' });
22
+ }
23
+
24
+ if (pathname === '/healthz') {
25
+ return sendJson(req, res, 200, {
26
+ ok: true,
27
+ service: 'omnizap',
28
+ type: 'health',
29
+ });
30
+ }
31
+
32
+ if (pathname === '/readyz') {
33
+ return sendJson(req, res, 200, {
34
+ ok: true,
35
+ service: 'omnizap',
36
+ type: 'ready',
37
+ });
38
+ }
39
+
40
+ return false;
41
+ };