@kaikybrofc/omnizap-system 2.3.1 → 2.3.2
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 +20 -18
- package/app/controllers/messageController.js +473 -255
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +1 -3
- package/app/observability/metrics.js +6 -3
- package/app/services/googleWebLinkService.js +77 -0
- package/database/index.js +2 -0
- package/database/migrations/20260301_0028_message_analysis_event.sql +32 -0
- package/database/migrations/20260301_0029_admin_action_audit.sql +16 -0
- package/package.json +1 -1
- package/public/index.html +12 -8
- package/public/js/apps/homeApp.js +75 -30
- package/public/js/apps/loginApp.js +184 -29
- package/public/js/apps/stickersAdminApp.js +3 -9
- package/public/js/apps/userApp.js +985 -55
- package/public/js/apps/userProfileApp.js +244 -0
- package/public/login/index.html +430 -100
- package/public/termos-de-uso/index.html +1 -1
- package/public/user/index.html +2 -180
- package/public/user/systemadm/index.html +774 -0
- package/server/controllers/stickerCatalog/nonCatalogHandlers.js +208 -0
- package/server/controllers/stickerCatalogController.js +1186 -363
- package/server/controllers/systemAdminController.js +141 -0
- package/server/controllers/userController.js +87 -0
- package/server/http/httpServer.js +72 -32
- package/server/middleware/cachePolicy.js +24 -0
- package/server/middleware/cachePolicyHelpers.js +2 -0
- package/server/middleware/rateLimit.js +82 -0
- package/server/middleware/requestLogger.js +16 -0
- package/server/middleware/requireAdminAuth.js +42 -0
- package/server/middleware/securityHeaders.js +6 -0
- package/server/routes/admin/systemAdminRouter.js +56 -0
- package/server/routes/health/healthRouter.js +41 -0
- package/server/routes/indexRouter.js +203 -0
- package/server/routes/metrics/metricsRouter.js +13 -0
- package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +44 -0
- package/server/routes/stickerCatalog/stickerApiRouter.js +84 -0
- package/server/routes/stickerCatalog/stickerDataRouter.js +140 -0
- package/server/routes/stickerCatalog/stickerSiteRouter.js +43 -0
- package/server/routes/user/userRouter.js +56 -0
- package/server/utils/safePath.js +26 -0
- package/server/routes/metricsRoute.js +0 -7
- package/server/routes/stickerCatalogRoute.js +0 -20
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { maybeHandleMetricsRequest } from './metrics/metricsRouter.js';
|
|
4
|
+
import { maybeHandleHealthRequest, shouldHandleHealthPath } from './health/healthRouter.js';
|
|
5
|
+
import { buildUserApiPaths, getUserRouterConfig, maybeHandleUserRequest, shouldHandleUserPath } from './user/userRouter.js';
|
|
6
|
+
import { getSystemAdminRouterConfig, maybeHandleSystemAdminRequest, shouldHandleSystemAdminPath } from './admin/systemAdminRouter.js';
|
|
7
|
+
import { getStickerSiteRouterConfig, maybeHandleStickerSiteRequest, shouldHandleStickerSitePath } from './stickerCatalog/stickerSiteRouter.js';
|
|
8
|
+
import { getStickerDataRouterConfig, maybeHandleStickerDataRequest, shouldHandleStickerDataPath } from './stickerCatalog/stickerDataRouter.js';
|
|
9
|
+
import { getStickerApiRouterConfig, maybeHandleStickerApiRequest, shouldHandleStickerApiPath } from './stickerCatalog/stickerApiRouter.js';
|
|
10
|
+
|
|
11
|
+
const startsWithPath = (pathname, prefix) => {
|
|
12
|
+
if (!pathname || !prefix) return false;
|
|
13
|
+
if (pathname === prefix) return true;
|
|
14
|
+
return pathname.startsWith(`${prefix}/`);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const normalizeBasePath = (value, fallback) => {
|
|
18
|
+
const raw = String(value || '').trim() || fallback;
|
|
19
|
+
const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`;
|
|
20
|
+
const withoutTrailingSlash = withLeadingSlash.length > 1 && withLeadingSlash.endsWith('/') ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
21
|
+
return withoutTrailingSlash || fallback;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const sendNotFound = (req, res) => {
|
|
25
|
+
if (res.writableEnded) return true;
|
|
26
|
+
res.statusCode = 404;
|
|
27
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
28
|
+
if (req.method === 'HEAD') {
|
|
29
|
+
res.end();
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let indexRouteConfigsPromise = null;
|
|
37
|
+
|
|
38
|
+
const loadUserConfigSafe = async () => {
|
|
39
|
+
try {
|
|
40
|
+
return await getUserRouterConfig();
|
|
41
|
+
} catch {
|
|
42
|
+
return {
|
|
43
|
+
webPath: '/user',
|
|
44
|
+
apiBasePath: '/api/sticker-packs',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const loadSystemAdminConfigSafe = async () => {
|
|
50
|
+
try {
|
|
51
|
+
return await getSystemAdminRouterConfig();
|
|
52
|
+
} catch {
|
|
53
|
+
return {
|
|
54
|
+
webPath: '/user/systemadm',
|
|
55
|
+
legacyWebPath: '/stickers/admin',
|
|
56
|
+
apiAdminBasePath: '/api/sticker-packs/admin',
|
|
57
|
+
apiAdminSessionPath: '/api/sticker-packs/admin/session',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const loadStickerSiteConfigSafe = async () => {
|
|
63
|
+
try {
|
|
64
|
+
return await getStickerSiteRouterConfig();
|
|
65
|
+
} catch {
|
|
66
|
+
return {
|
|
67
|
+
enabled: true,
|
|
68
|
+
webPath: '/stickers',
|
|
69
|
+
apiBasePath: '/api/sticker-packs',
|
|
70
|
+
orphanApiPath: '/api/sticker-packs/orphan-stickers',
|
|
71
|
+
dataPublicPath: '/data',
|
|
72
|
+
dataPublicDir: path.resolve(process.cwd(), 'data'),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const loadStickerDataConfigSafe = async () => {
|
|
78
|
+
try {
|
|
79
|
+
return await getStickerDataRouterConfig();
|
|
80
|
+
} catch {
|
|
81
|
+
return {
|
|
82
|
+
dataPublicPath: '/data',
|
|
83
|
+
dataPublicDir: path.resolve(process.cwd(), 'data'),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const loadStickerApiConfigSafe = async () => {
|
|
89
|
+
try {
|
|
90
|
+
return await getStickerApiRouterConfig();
|
|
91
|
+
} catch {
|
|
92
|
+
return {
|
|
93
|
+
apiBasePath: '/api/sticker-packs',
|
|
94
|
+
marketplaceStatsPath: '/api/marketplace/stats',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const getIndexRouteConfigs = async () => {
|
|
100
|
+
if (!indexRouteConfigsPromise) {
|
|
101
|
+
indexRouteConfigsPromise = Promise.all([
|
|
102
|
+
loadUserConfigSafe(),
|
|
103
|
+
loadSystemAdminConfigSafe(),
|
|
104
|
+
loadStickerSiteConfigSafe(),
|
|
105
|
+
loadStickerDataConfigSafe(),
|
|
106
|
+
loadStickerApiConfigSafe(),
|
|
107
|
+
]).then(([userConfig, systemAdminConfig, stickerSiteConfig, stickerDataConfig, stickerApiConfig]) => ({
|
|
108
|
+
userConfig,
|
|
109
|
+
systemAdminConfig,
|
|
110
|
+
stickerConfig: {
|
|
111
|
+
...stickerSiteConfig,
|
|
112
|
+
...stickerDataConfig,
|
|
113
|
+
...stickerApiConfig,
|
|
114
|
+
},
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return indexRouteConfigsPromise;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const shouldHandleSystemAdminStep = (pathname, systemAdminConfig) => shouldHandleSystemAdminPath(pathname, systemAdminConfig);
|
|
122
|
+
|
|
123
|
+
const shouldHandleUserStep = (pathname, userConfig) => shouldHandleUserPath(pathname, userConfig);
|
|
124
|
+
|
|
125
|
+
const shouldHandleMetricsStep = (pathname, metricsPath) => startsWithPath(pathname, normalizeBasePath(metricsPath, '/metrics'));
|
|
126
|
+
|
|
127
|
+
export const routeRequest = async (req, res, { pathname, url, metricsPath = '/metrics', configs = null } = {}) => {
|
|
128
|
+
const resolvedConfigs = configs || (await getIndexRouteConfigs());
|
|
129
|
+
const userConfig = resolvedConfigs?.userConfig || null;
|
|
130
|
+
const systemAdminConfig = resolvedConfigs?.systemAdminConfig || null;
|
|
131
|
+
const stickerConfig = resolvedConfigs?.stickerConfig || null;
|
|
132
|
+
|
|
133
|
+
// 1) Metrics
|
|
134
|
+
if (shouldHandleMetricsStep(pathname, metricsPath)) {
|
|
135
|
+
const handled = await maybeHandleMetricsRequest(req, res, { pathname, metricsPath });
|
|
136
|
+
if (handled) return true;
|
|
137
|
+
return sendNotFound(req, res);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 2) Health checks
|
|
141
|
+
if (shouldHandleHealthPath(pathname)) {
|
|
142
|
+
const handled = await maybeHandleHealthRequest(req, res, { pathname });
|
|
143
|
+
if (handled) return true;
|
|
144
|
+
return sendNotFound(req, res);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 3) User
|
|
148
|
+
const systemAdminCandidate = shouldHandleSystemAdminStep(pathname, systemAdminConfig);
|
|
149
|
+
if (shouldHandleUserStep(pathname, userConfig)) {
|
|
150
|
+
const handled = await maybeHandleUserRequest(req, res, { pathname, url });
|
|
151
|
+
if (handled) return true;
|
|
152
|
+
|
|
153
|
+
// Permite /user/systemadm continuar para o router de admin.
|
|
154
|
+
if (!systemAdminCandidate) return sendNotFound(req, res);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 4) System admin + legacy /stickers/admin
|
|
158
|
+
if (systemAdminCandidate) {
|
|
159
|
+
const handled = await maybeHandleSystemAdminRequest(req, res, { pathname, url });
|
|
160
|
+
if (handled) return true;
|
|
161
|
+
return sendNotFound(req, res);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 5) Sticker catalog apenas nos prefixes permitidos
|
|
165
|
+
if (shouldHandleStickerSitePath(pathname, stickerConfig)) {
|
|
166
|
+
const handled = await maybeHandleStickerSiteRequest(req, res, { pathname, url });
|
|
167
|
+
if (handled) return true;
|
|
168
|
+
return sendNotFound(req, res);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (shouldHandleStickerDataPath(pathname, stickerConfig)) {
|
|
172
|
+
const handled = await maybeHandleStickerDataRequest(req, res, {
|
|
173
|
+
pathname,
|
|
174
|
+
config: {
|
|
175
|
+
dataPublicPath: stickerConfig?.dataPublicPath,
|
|
176
|
+
dataPublicDir: stickerConfig?.dataPublicDir,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
if (handled) return true;
|
|
180
|
+
return sendNotFound(req, res);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (shouldHandleStickerApiPath(pathname, stickerConfig)) {
|
|
184
|
+
const handled = await maybeHandleStickerApiRequest(req, res, {
|
|
185
|
+
pathname,
|
|
186
|
+
url,
|
|
187
|
+
config: {
|
|
188
|
+
apiBasePath: stickerConfig?.apiBasePath,
|
|
189
|
+
marketplaceStatsPath: stickerConfig?.marketplaceStatsPath,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
if (handled) return true;
|
|
193
|
+
return sendNotFound(req, res);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 6) 404 global
|
|
197
|
+
return sendNotFound(req, res);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const getUserApiPathsFromConfig = (userConfig = null) => {
|
|
201
|
+
const apiBasePath = userConfig?.apiBasePath || '/api/sticker-packs';
|
|
202
|
+
return buildUserApiPaths(apiBasePath);
|
|
203
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { sendMetricsResponse } from '../../controllers/metricsController.js';
|
|
2
|
+
|
|
3
|
+
const startsWithPath = (pathname, prefix) => {
|
|
4
|
+
if (!pathname || !prefix) return false;
|
|
5
|
+
if (pathname === prefix) return true;
|
|
6
|
+
return pathname.startsWith(`${prefix}/`);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const maybeHandleMetricsRequest = async (_req, res, { pathname, metricsPath }) => {
|
|
10
|
+
if (!startsWithPath(pathname, metricsPath)) return false;
|
|
11
|
+
await sendMetricsResponse(res);
|
|
12
|
+
return true;
|
|
13
|
+
};
|
|
@@ -11,6 +11,31 @@ export const handleCatalogAdminRoutes = async ({ req, res, url, segments, handle
|
|
|
11
11
|
return true;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
if (segments.length === 3 && segments[1] === 'users' && segments[2] === 'force-logout') {
|
|
15
|
+
await handlers.handleAdminForceLogoutRequest(req, res);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (segments.length === 2 && segments[1] === 'feature-flags') {
|
|
20
|
+
await handlers.handleAdminFeatureFlagsRequest(req, res);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (segments.length === 2 && segments[1] === 'ops') {
|
|
25
|
+
await handlers.handleAdminOpsActionRequest(req, res);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (segments.length === 2 && segments[1] === 'search') {
|
|
30
|
+
await handlers.handleAdminSearchRequest(req, res, url);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (segments.length === 2 && segments[1] === 'export') {
|
|
35
|
+
await handlers.handleAdminExportRequest(req, res, url);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
if (segments.length === 2 && segments[1] === 'moderators') {
|
|
15
40
|
await handlers.handleAdminModeratorsRequest(req, res);
|
|
16
41
|
return true;
|
|
@@ -27,6 +52,10 @@ export const handleCatalogAdminRoutes = async ({ req, res, url, segments, handle
|
|
|
27
52
|
}
|
|
28
53
|
|
|
29
54
|
if (segments.length === 3 && segments[1] === 'packs') {
|
|
55
|
+
if (req.method === 'DELETE') {
|
|
56
|
+
await handlers.handleAdminPackDeleteRequest(req, res, segments[2]);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
30
59
|
await handlers.handleAdminPackDetailsRequest(req, res, segments[2]);
|
|
31
60
|
return true;
|
|
32
61
|
}
|
|
@@ -36,11 +65,21 @@ export const handleCatalogAdminRoutes = async ({ req, res, url, segments, handle
|
|
|
36
65
|
return true;
|
|
37
66
|
}
|
|
38
67
|
|
|
68
|
+
if (segments.length === 5 && segments[1] === 'packs' && segments[3] === 'stickers') {
|
|
69
|
+
await handlers.handleAdminPackStickerDeleteRequest(req, res, segments[2], segments[4]);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
39
73
|
if (segments.length === 6 && segments[1] === 'packs' && segments[3] === 'stickers' && segments[5] === 'delete') {
|
|
40
74
|
await handlers.handleAdminPackStickerDeleteRequest(req, res, segments[2], segments[4]);
|
|
41
75
|
return true;
|
|
42
76
|
}
|
|
43
77
|
|
|
78
|
+
if (segments.length === 3 && segments[1] === 'stickers') {
|
|
79
|
+
await handlers.handleAdminGlobalStickerDeleteRequest(req, res, segments[2]);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
44
83
|
if (segments.length === 4 && segments[1] === 'stickers' && segments[3] === 'delete') {
|
|
45
84
|
await handlers.handleAdminGlobalStickerDeleteRequest(req, res, segments[2]);
|
|
46
85
|
return true;
|
|
@@ -56,6 +95,11 @@ export const handleCatalogAdminRoutes = async ({ req, res, url, segments, handle
|
|
|
56
95
|
return true;
|
|
57
96
|
}
|
|
58
97
|
|
|
98
|
+
if (segments.length === 3 && segments[1] === 'bans') {
|
|
99
|
+
await handlers.handleAdminBanRevokeRequest(req, res, segments[2]);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
59
103
|
sendJson(req, res, 404, { error: 'Rota admin nao encontrada.' });
|
|
60
104
|
return true;
|
|
61
105
|
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { requireAdminAuth } from '../../middleware/requireAdminAuth.js';
|
|
2
|
+
import { createAdminApiRateLimit } from '../../middleware/rateLimit.js';
|
|
3
|
+
|
|
4
|
+
let stickerCatalogControllerPromise = null;
|
|
5
|
+
|
|
6
|
+
const loadStickerCatalogController = async () => {
|
|
7
|
+
if (!stickerCatalogControllerPromise) {
|
|
8
|
+
stickerCatalogControllerPromise = import('../../controllers/stickerCatalogController.js');
|
|
9
|
+
}
|
|
10
|
+
return stickerCatalogControllerPromise;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const normalizeBasePath = (value, fallback) => {
|
|
14
|
+
const raw = String(value || '').trim() || fallback;
|
|
15
|
+
const withLeadingSlash = raw.startsWith('/') ? raw : `/${raw}`;
|
|
16
|
+
const withoutTrailingSlash = withLeadingSlash.length > 1 && withLeadingSlash.endsWith('/') ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
17
|
+
return withoutTrailingSlash || fallback;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const startsWithPath = (pathname, prefix) => {
|
|
21
|
+
if (!pathname || !prefix) return false;
|
|
22
|
+
if (pathname === prefix) return true;
|
|
23
|
+
return pathname.startsWith(`${prefix}/`);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEFAULT_STICKER_API_BASE_PATH = '/api/sticker-packs';
|
|
27
|
+
const DEFAULT_MARKETPLACE_STATS_PATH = '/api/marketplace/stats';
|
|
28
|
+
const ALLOWED_METHODS = new Set(['GET', 'HEAD', 'POST', 'PATCH', 'DELETE']);
|
|
29
|
+
const adminApiRateLimit = createAdminApiRateLimit();
|
|
30
|
+
|
|
31
|
+
const sendMethodNotAllowed = (req, res) => {
|
|
32
|
+
if (res.writableEnded) return;
|
|
33
|
+
res.statusCode = 405;
|
|
34
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
35
|
+
if (req.method === 'HEAD') {
|
|
36
|
+
res.end();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const getStickerApiRouterConfig = async () => {
|
|
43
|
+
const controller = await loadStickerCatalogController();
|
|
44
|
+
const legacyConfig = (typeof controller?.getStickerCatalogConfig === 'function' ? controller.getStickerCatalogConfig() : null) || {};
|
|
45
|
+
return {
|
|
46
|
+
apiBasePath: normalizeBasePath(legacyConfig.apiBasePath, DEFAULT_STICKER_API_BASE_PATH),
|
|
47
|
+
marketplaceStatsPath: DEFAULT_MARKETPLACE_STATS_PATH,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const shouldHandleStickerApiPath = (pathname, stickerConfig = null) => {
|
|
52
|
+
const apiBasePath = normalizeBasePath(stickerConfig?.apiBasePath, DEFAULT_STICKER_API_BASE_PATH);
|
|
53
|
+
const marketplaceStatsPath = normalizeBasePath(stickerConfig?.marketplaceStatsPath, DEFAULT_MARKETPLACE_STATS_PATH);
|
|
54
|
+
return startsWithPath(pathname, apiBasePath) || startsWithPath(pathname, marketplaceStatsPath);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const maybeHandleStickerApiRequest = async (req, res, { pathname, url, config = null }) => {
|
|
58
|
+
const resolvedConfig = config || (await getStickerApiRouterConfig());
|
|
59
|
+
if (!shouldHandleStickerApiPath(pathname, resolvedConfig)) return false;
|
|
60
|
+
|
|
61
|
+
const method = String(req.method || '').toUpperCase();
|
|
62
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
63
|
+
sendMethodNotAllowed(req, res);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const apiBasePath = normalizeBasePath(resolvedConfig.apiBasePath, DEFAULT_STICKER_API_BASE_PATH);
|
|
68
|
+
const adminBasePath = `${apiBasePath}/admin`;
|
|
69
|
+
const adminSessionPath = `${adminBasePath}/session`;
|
|
70
|
+
|
|
71
|
+
if (startsWithPath(pathname, adminBasePath)) {
|
|
72
|
+
const allowedByRateLimit = adminApiRateLimit(req, res);
|
|
73
|
+
if (!allowedByRateLimit) return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (startsWithPath(pathname, adminBasePath) && pathname !== adminSessionPath) {
|
|
77
|
+
const allowed = requireAdminAuth(req, res);
|
|
78
|
+
if (!allowed) return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const controller = await loadStickerCatalogController();
|
|
82
|
+
if (typeof controller?.maybeHandleStickerCatalogRequest !== 'function') return false;
|
|
83
|
+
return controller.maybeHandleStickerCatalogRequest(req, res, { pathname, url });
|
|
84
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { safeJoin } from '../../utils/safePath.js';
|
|
5
|
+
|
|
6
|
+
let stickerCatalogControllerPromise = null;
|
|
7
|
+
|
|
8
|
+
const loadStickerCatalogController = async () => {
|
|
9
|
+
if (!stickerCatalogControllerPromise) {
|
|
10
|
+
stickerCatalogControllerPromise = import('../../controllers/stickerCatalogController.js');
|
|
11
|
+
}
|
|
12
|
+
return stickerCatalogControllerPromise;
|
|
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 startsWithPath = (pathname, prefix) => {
|
|
23
|
+
if (!pathname || !prefix) return false;
|
|
24
|
+
if (pathname === prefix) return true;
|
|
25
|
+
return pathname.startsWith(`${prefix}/`);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DEFAULT_DATA_PUBLIC_PATH = '/data';
|
|
29
|
+
const DEFAULT_DATA_PUBLIC_DIR = path.resolve(process.cwd(), 'data');
|
|
30
|
+
const ALLOWED_EXTENSIONS = new Set(['.webp', '.png', '.jpg', '.jpeg', '.gif', '.avif', '.bmp']);
|
|
31
|
+
|
|
32
|
+
const sendJson = (req, res, statusCode, payload) => {
|
|
33
|
+
if (res.writableEnded) return;
|
|
34
|
+
res.statusCode = statusCode;
|
|
35
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
36
|
+
if (req.method === 'HEAD') {
|
|
37
|
+
res.end();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
res.end(JSON.stringify(payload));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const resolveContentType = (extension) => {
|
|
44
|
+
if (extension === '.png') return 'image/png';
|
|
45
|
+
if (extension === '.jpg' || extension === '.jpeg') return 'image/jpeg';
|
|
46
|
+
if (extension === '.gif') return 'image/gif';
|
|
47
|
+
if (extension === '.avif') return 'image/avif';
|
|
48
|
+
if (extension === '.bmp') return 'image/bmp';
|
|
49
|
+
return 'image/webp';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const decodeRelativePath = (pathname, dataPublicPath) => {
|
|
53
|
+
const rawSuffix = pathname.slice(dataPublicPath.length).replace(/^\/+/, '');
|
|
54
|
+
if (!rawSuffix) return '';
|
|
55
|
+
|
|
56
|
+
const decodedSegments = rawSuffix
|
|
57
|
+
.split('/')
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map((segment) => decodeURIComponent(segment));
|
|
60
|
+
|
|
61
|
+
return decodedSegments.join('/');
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const getStickerDataRouterConfig = async () => {
|
|
65
|
+
const controller = await loadStickerCatalogController();
|
|
66
|
+
const legacyConfig = (typeof controller?.getStickerCatalogConfig === 'function' ? controller.getStickerCatalogConfig() : null) || {};
|
|
67
|
+
return {
|
|
68
|
+
dataPublicPath: normalizeBasePath(legacyConfig.dataPublicPath, DEFAULT_DATA_PUBLIC_PATH),
|
|
69
|
+
dataPublicDir: path.resolve(legacyConfig.dataPublicDir || DEFAULT_DATA_PUBLIC_DIR),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const shouldHandleStickerDataPath = (pathname, stickerConfig = null) => {
|
|
74
|
+
const dataPublicPath = normalizeBasePath(stickerConfig?.dataPublicPath, DEFAULT_DATA_PUBLIC_PATH);
|
|
75
|
+
return startsWithPath(pathname, dataPublicPath);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const maybeHandleStickerDataRequest = async (req, res, { pathname, config = null }) => {
|
|
79
|
+
const resolvedConfig = config || (await getStickerDataRouterConfig());
|
|
80
|
+
const dataPublicPath = normalizeBasePath(resolvedConfig.dataPublicPath, DEFAULT_DATA_PUBLIC_PATH);
|
|
81
|
+
|
|
82
|
+
if (!shouldHandleStickerDataPath(pathname, resolvedConfig)) return false;
|
|
83
|
+
|
|
84
|
+
if (!['GET', 'HEAD'].includes(req.method || '')) {
|
|
85
|
+
sendJson(req, res, 405, { error: 'Method Not Allowed' });
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let relativePath = '';
|
|
90
|
+
try {
|
|
91
|
+
relativePath = decodeRelativePath(pathname, dataPublicPath);
|
|
92
|
+
} catch {
|
|
93
|
+
sendJson(req, res, 400, { error: 'Invalid path encoding' });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!relativePath) {
|
|
98
|
+
sendJson(req, res, 400, { error: 'Invalid path' });
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const absolutePath = safeJoin(resolvedConfig.dataPublicDir, relativePath);
|
|
103
|
+
if (!absolutePath) {
|
|
104
|
+
sendJson(req, res, 400, { error: 'Invalid path' });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const extension = path.extname(absolutePath).toLowerCase();
|
|
109
|
+
if (!ALLOWED_EXTENSIONS.has(extension)) {
|
|
110
|
+
sendJson(req, res, 403, { error: 'Forbidden file type' });
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const fileStat = await fs.stat(absolutePath);
|
|
116
|
+
if (!fileStat.isFile()) {
|
|
117
|
+
sendJson(req, res, 404, { error: 'Not Found' });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const fileBuffer = req.method === 'HEAD' ? null : await fs.readFile(absolutePath);
|
|
122
|
+
res.statusCode = 200;
|
|
123
|
+
res.setHeader('Content-Type', resolveContentType(extension));
|
|
124
|
+
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
125
|
+
if (req.method === 'HEAD') {
|
|
126
|
+
res.end();
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
res.end(fileBuffer);
|
|
130
|
+
return true;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error?.code === 'ENOENT') {
|
|
133
|
+
sendJson(req, res, 404, { error: 'Not Found' });
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
sendJson(req, res, 500, { error: 'Read error' });
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
let stickerCatalogControllerPromise = null;
|
|
2
|
+
|
|
3
|
+
const loadStickerCatalogController = async () => {
|
|
4
|
+
if (!stickerCatalogControllerPromise) {
|
|
5
|
+
stickerCatalogControllerPromise = import('../../controllers/stickerCatalogController.js');
|
|
6
|
+
}
|
|
7
|
+
return stickerCatalogControllerPromise;
|
|
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_STICKER_WEB_PATH = '/stickers';
|
|
24
|
+
|
|
25
|
+
export const getStickerSiteRouterConfig = async () => {
|
|
26
|
+
const controller = await loadStickerCatalogController();
|
|
27
|
+
const legacyConfig = (typeof controller?.getStickerCatalogConfig === 'function' ? controller.getStickerCatalogConfig() : null) || {};
|
|
28
|
+
return {
|
|
29
|
+
...legacyConfig,
|
|
30
|
+
webPath: normalizeBasePath(legacyConfig.webPath, DEFAULT_STICKER_WEB_PATH),
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const shouldHandleStickerSitePath = (pathname, stickerConfig = null) => {
|
|
35
|
+
const resolvedWebPath = normalizeBasePath(stickerConfig?.webPath, DEFAULT_STICKER_WEB_PATH);
|
|
36
|
+
return pathname === '/sitemap.xml' || startsWithPath(pathname, resolvedWebPath);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const maybeHandleStickerSiteRequest = async (req, res, { pathname, url }) => {
|
|
40
|
+
const controller = await loadStickerCatalogController();
|
|
41
|
+
if (typeof controller?.maybeHandleStickerCatalogRequest !== 'function') return false;
|
|
42
|
+
return controller.maybeHandleStickerCatalogRequest(req, res, { pathname, url });
|
|
43
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
let userControllerPromise = null;
|
|
2
|
+
|
|
3
|
+
const loadUserController = async () => {
|
|
4
|
+
if (!userControllerPromise) {
|
|
5
|
+
userControllerPromise = import('../../controllers/userController.js');
|
|
6
|
+
}
|
|
7
|
+
return userControllerPromise;
|
|
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_WEB_PATH = '/user';
|
|
24
|
+
const DEFAULT_STICKER_API_BASE_PATH = '/api/sticker-packs';
|
|
25
|
+
|
|
26
|
+
export const buildUserApiPaths = (apiBasePath) => {
|
|
27
|
+
const resolvedApiBasePath = normalizeBasePath(apiBasePath, DEFAULT_STICKER_API_BASE_PATH);
|
|
28
|
+
return new Set([`${resolvedApiBasePath}/auth/google/session`, `${resolvedApiBasePath}/me`, `${resolvedApiBasePath}/bot-contact`]);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const getUserRouterConfig = async () => {
|
|
32
|
+
const controller = await loadUserController();
|
|
33
|
+
const legacyConfig = (typeof controller?.getUserRouteConfig === 'function' ? controller.getUserRouteConfig() : null) || {};
|
|
34
|
+
return {
|
|
35
|
+
webPath: normalizeBasePath(legacyConfig.webPath, DEFAULT_USER_WEB_PATH),
|
|
36
|
+
apiBasePath: normalizeBasePath(legacyConfig.apiBasePath, DEFAULT_STICKER_API_BASE_PATH),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const shouldHandleUserPath = (pathname, userConfig = null) => {
|
|
41
|
+
const resolvedConfig = userConfig || {
|
|
42
|
+
webPath: DEFAULT_USER_WEB_PATH,
|
|
43
|
+
apiBasePath: DEFAULT_STICKER_API_BASE_PATH,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (startsWithPath(pathname, resolvedConfig.webPath)) return true;
|
|
47
|
+
|
|
48
|
+
const userApiPaths = buildUserApiPaths(resolvedConfig.apiBasePath);
|
|
49
|
+
return userApiPaths.has(pathname);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const maybeHandleUserRequest = async (req, res, { pathname, url }) => {
|
|
53
|
+
const controller = await loadUserController();
|
|
54
|
+
if (typeof controller?.maybeHandleUserRequest !== 'function') return false;
|
|
55
|
+
return controller.maybeHandleUserRequest(req, res, { pathname, url });
|
|
56
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Junta um caminho relativo de forma segura dentro de um diretório base.
|
|
5
|
+
* Retorna `null` quando detectar tentativa de path traversal.
|
|
6
|
+
*/
|
|
7
|
+
export const safeJoin = (baseDir, unsafePath) => {
|
|
8
|
+
const baseAbsolutePath = path.resolve(String(baseDir || '.'));
|
|
9
|
+
const normalizedUnsafePath = String(unsafePath || '')
|
|
10
|
+
.replace(/\\/g, '/')
|
|
11
|
+
.replace(/^\/+/, '');
|
|
12
|
+
|
|
13
|
+
const normalizedRelativePath = path.posix.normalize(normalizedUnsafePath);
|
|
14
|
+
if (normalizedRelativePath === '..' || normalizedRelativePath.startsWith('../') || normalizedRelativePath.includes('/../')) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const safeRelativePath = normalizedRelativePath === '.' ? '' : normalizedRelativePath;
|
|
19
|
+
const resolvedPath = path.resolve(baseAbsolutePath, safeRelativePath);
|
|
20
|
+
|
|
21
|
+
if (resolvedPath !== baseAbsolutePath && !resolvedPath.startsWith(`${baseAbsolutePath}${path.sep}`)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return resolvedPath;
|
|
26
|
+
};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { sendMetricsResponse } from '../controllers/metricsController.js';
|
|
2
|
-
|
|
3
|
-
export const maybeHandleMetricsRoute = async (req, res, { pathname, metricsPath }) => {
|
|
4
|
-
if (!pathname.startsWith(metricsPath)) return false;
|
|
5
|
-
await sendMetricsResponse(res);
|
|
6
|
-
return true;
|
|
7
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
let stickerCatalogControllerPromise = null;
|
|
2
|
-
|
|
3
|
-
const loadStickerCatalogController = async () => {
|
|
4
|
-
if (!stickerCatalogControllerPromise) {
|
|
5
|
-
stickerCatalogControllerPromise = import('../controllers/stickerCatalogController.js');
|
|
6
|
-
}
|
|
7
|
-
return stickerCatalogControllerPromise;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export const getStickerCatalogRouteConfig = async () => {
|
|
11
|
-
const controller = await loadStickerCatalogController();
|
|
12
|
-
if (typeof controller.getStickerCatalogConfig !== 'function') return null;
|
|
13
|
-
return controller.getStickerCatalogConfig();
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export const maybeHandleStickerCatalogRoute = async (req, res, { pathname, url }) => {
|
|
17
|
-
const controller = await loadStickerCatalogController();
|
|
18
|
-
if (typeof controller.maybeHandleStickerCatalogRequest !== 'function') return false;
|
|
19
|
-
return controller.maybeHandleStickerCatalogRequest(req, res, { pathname, url });
|
|
20
|
-
};
|