@payez/next-mvp 4.1.7 → 4.1.9
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 +792 -792
- package/dist/auth/better-auth.js +15 -3
- package/dist/components/account/MobileNavDrawer.js +18 -18
- package/dist/components/account/UserAvatarMenu.js +1 -1
- package/dist/components/admin/AlertSettingsTab.js +40 -40
- package/dist/components/admin/LoggingSettingsTab.js +28 -28
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/package.json +1 -1
- package/scripts/dev-broker.ps1 +35 -35
- package/scripts/dev-local.ps1 +45 -45
- package/src/api/auth-handler.ts +548 -548
- package/src/api-handlers/account/change-password.ts +142 -142
- package/src/api-handlers/account/update-phone.ts +79 -79
- package/src/api-handlers/account/verify-email.ts +125 -125
- package/src/api-handlers/account/verify-sms.ts +125 -125
- package/src/api-handlers/admin/analytics.ts +443 -443
- package/src/api-handlers/admin/audit.ts +223 -223
- package/src/api-handlers/admin/index.ts +63 -63
- package/src/api-handlers/admin/redis-sessions.ts +251 -251
- package/src/api-handlers/admin/sessions.ts +318 -318
- package/src/api-handlers/admin/site-logs.ts +365 -365
- package/src/api-handlers/admin/users.ts +242 -242
- package/src/api-handlers/admin/vibe-data.ts +324 -324
- package/src/api-handlers/auth/refresh.ts +682 -682
- package/src/api-handlers/auth/signout.ts +199 -199
- package/src/api-handlers/auth/status.ts +20 -20
- package/src/api-handlers/auth/update-session.ts +110 -110
- package/src/api-handlers/auth/verify-code.ts +112 -112
- package/src/api-handlers/session/refresh-viability.ts +36 -36
- package/src/api-handlers/test/force-expire.ts +60 -60
- package/src/auth/auth-decision.ts +229 -229
- package/src/auth/better-auth.ts +15 -3
- package/src/auth/route-config.ts +219 -219
- package/src/auth/types/auth-types.ts +555 -555
- package/src/auth/types/index.ts +7 -7
- package/src/auth/utils/idp-client.ts +445 -445
- package/src/auth/utils/index.ts +6 -6
- package/src/auth/utils/token-utils.ts +243 -243
- package/src/client/AuthContext.tsx +143 -143
- package/src/client/fetch-with-auth.ts +47 -47
- package/src/components/SessionSync.tsx +138 -138
- package/src/components/SignalRHealthCheck.tsx +131 -131
- package/src/components/account/MobileNavDrawer.tsx +305 -305
- package/src/components/account/UserAvatarMenu.tsx +266 -266
- package/src/components/account/index.ts +13 -13
- package/src/components/admin/AlertSettingsTab.tsx +728 -728
- package/src/components/admin/AnalyticsTab.tsx +703 -703
- package/src/components/admin/DataBrowserTab.tsx +505 -505
- package/src/components/admin/LoggingSettingsTab.tsx +665 -665
- package/src/components/admin/SessionsTab.tsx +414 -414
- package/src/components/admin/StatsTab.tsx +379 -379
- package/src/components/admin/VibeAdminLayout.tsx +187 -187
- package/src/components/admin/index.ts +59 -59
- package/src/components/auth/FederatedAuthSection.tsx +95 -95
- package/src/components/auth/ModeAwareLoginPage.tsx +135 -135
- package/src/components/auth/ModeAwareSignupPage.tsx +267 -267
- package/src/components/auth/TraditionalAuthSection.tsx +99 -99
- package/src/components/recovery/CompleteStep.tsx +36 -36
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
- package/src/components/recovery/SelectMethodStep.tsx +73 -73
- package/src/components/recovery/SetPasswordStep.tsx +97 -97
- package/src/components/recovery/VerifyCodeStep.tsx +90 -90
- package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
- package/src/components/reserved/ReservedStatusBox.tsx +118 -118
- package/src/components/ui/BetaBadge.tsx +58 -58
- package/src/components/ui/Footer.tsx +93 -93
- package/src/config/env.ts +57 -57
- package/src/config/logger.ts +62 -62
- package/src/config/vibe-log-transport.ts +264 -264
- package/src/hooks/useAuth.ts +117 -117
- package/src/hooks/useAuthSettings.ts +97 -97
- package/src/hooks/useAvailableProviders.ts +120 -120
- package/src/hooks/usePasswordValidation.ts +127 -127
- package/src/hooks/usePublicAuthSettings.ts +149 -149
- package/src/hooks/useSessionExpiration.ts +101 -101
- package/src/hooks/useViabilitySession.ts +336 -336
- package/src/index.ts +60 -60
- package/src/lib/api-handler.ts +643 -643
- package/src/lib/app-slug.ts +178 -178
- package/src/lib/geolocation.ts +265 -265
- package/src/lib/idp-fetch.ts +110 -110
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/roles.ts +177 -177
- package/src/lib/site-logger.ts +245 -245
- package/src/lib/standardized-client-api.ts +901 -901
- package/src/lib/test-aware-get-token.ts +41 -41
- package/src/lib/user-agent-parser.ts +252 -252
- package/src/logging/api/admin-analytics.ts +51 -51
- package/src/logging/api/audit-log.ts +53 -53
- package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
- package/src/logging/components/AuditLogViewer.tsx +125 -125
- package/src/logging/components/ErrorMetricsCard.tsx +98 -98
- package/src/logging/components/HealthMetricsCard.tsx +70 -70
- package/src/logging/hooks/useAdminAnalytics.ts +22 -22
- package/src/logging/hooks/useAuditLog.ts +24 -24
- package/src/logging/hooks/useErrorMetrics.ts +40 -40
- package/src/logging/hooks/useHealthMetrics.ts +44 -44
- package/src/logging/index.ts +18 -18
- package/src/logging/types/analytics.ts +81 -81
- package/src/logging/types/audit.ts +31 -31
- package/src/logging/types/index.ts +3 -3
- package/src/middleware/create-middleware.ts +663 -663
- package/src/middleware/rbac-check.ts +279 -279
- package/src/middleware/twofa-presets.ts +224 -224
- package/src/pages/admin-login/page.tsx +221 -221
- package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
- package/src/pages/admin-page-permissions/index.ts +7 -7
- package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
- package/src/pages/admin-roles/modals.tsx +469 -469
- package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
- package/src/pages/client-admin/index.ts +33 -33
- package/src/pages/coming-soon/page.tsx +83 -83
- package/src/pages/login/page.tsx +456 -456
- package/src/pages/profile/EnhancedProfilePage.tsx +485 -485
- package/src/pages/profile/index.ts +9 -9
- package/src/pages/profile/page.tsx +166 -166
- package/src/pages/recovery/page.tsx +234 -234
- package/src/pages/roles/MyRolesPage.tsx +211 -211
- package/src/pages/roles/components.tsx +294 -294
- package/src/pages/roles/index.ts +17 -17
- package/src/pages/security/EnhancedSecurityPage.tsx +565 -565
- package/src/pages/security/index.ts +9 -9
- package/src/pages/security/page.tsx +507 -507
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
- package/src/pages/settings/index.ts +9 -9
- package/src/pages/settings/page.tsx +47 -47
- package/src/pages/showcase/ShowcasePage.tsx +532 -532
- package/src/pages/showcase/index.ts +13 -13
- package/src/pages/test-env/EmergencyLogoutPage.tsx +180 -180
- package/src/pages/test-env/JwtInspectPage.tsx +456 -456
- package/src/pages/test-env/RefreshTokenPage.tsx +157 -157
- package/src/pages/test-env/TestEnvPage.tsx +118 -118
- package/src/pages/test-env/index.ts +25 -25
- package/src/pages/verify-code/page.tsx +652 -652
- package/src/routes/account/update-phone.ts +13 -13
- package/src/routes/auth/logout.ts +108 -108
- package/src/routes/auth/nextauth.ts +45 -45
- package/src/routes/auth/settings.ts +75 -75
- package/src/routes/auth/viability.ts +224 -224
- package/src/server/auth-guard.ts +170 -170
- package/src/server/slim-middleware.ts +117 -117
- package/src/server/with-auth.ts +93 -93
- package/src/stores/authStore.ts +1899 -1899
- package/src/templates/instrumentation.ts +41 -41
- package/src/utils/logout.ts +30 -30
- package/src/vibe/client.ts +590 -590
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +185 -185
- package/src/vibe/generic.ts +431 -431
- package/src/vibe/hooks/index.ts +367 -367
- package/src/vibe/index.ts +132 -132
- package/src/vibe/sessions.ts +551 -551
- package/src/vibe/types.ts +577 -577
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { NextRequest } from 'next/server';
|
|
2
|
-
import { logger } from '../config/logger';
|
|
3
|
-
import { getSession } from '../server/auth';
|
|
4
|
-
import { getSessionCookieName } from './app-slug';
|
|
5
|
-
|
|
6
|
-
export async function getTokenTestAware(req: NextRequest): Promise<any> {
|
|
7
|
-
if (process.env.TEST_MODE === 'true') {
|
|
8
|
-
try {
|
|
9
|
-
let secret = process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET;
|
|
10
|
-
if (!secret || secret.trim() === '') {
|
|
11
|
-
const { getIDPClientConfig } = await import('./idp-client-config');
|
|
12
|
-
const idpConfig = await getIDPClientConfig();
|
|
13
|
-
secret = idpConfig.authSecret;
|
|
14
|
-
}
|
|
15
|
-
// Use app-slug prefixed cookie name
|
|
16
|
-
const cookieName = getSessionCookieName();
|
|
17
|
-
const cookies = req.headers.get('cookie'); if (!cookies) { logger.debug('[GET_TOKEN] No cookies in request'); return null; }
|
|
18
|
-
const cookieValue = cookies.split(';').find(c => c.trim().startsWith(`${cookieName}=`))?.split('=')[1];
|
|
19
|
-
if (!cookieValue) { logger.debug('[GET_TOKEN] Session token cookie not found'); return null; }
|
|
20
|
-
const { jwtVerify } = await import('jose');
|
|
21
|
-
const secretKey = new TextEncoder().encode(secret);
|
|
22
|
-
const { payload } = await jwtVerify(cookieValue, secretKey);
|
|
23
|
-
logger.debug('[GET_TOKEN] TEST_MODE token decoded:', { hasPayload: !!payload, redisSessionId: (payload as any).redisSessionId, sub: (payload as any).sub });
|
|
24
|
-
return payload;
|
|
25
|
-
} catch (error) { logger.error('[GET_TOKEN] TEST_MODE token decode error:', { error: error instanceof Error ? error.message : String(error) }); return null; }
|
|
26
|
-
}
|
|
27
|
-
// Production path: Better Auth session
|
|
28
|
-
const session = await getSession(req);
|
|
29
|
-
if (session?.user && session?.session?.token) {
|
|
30
|
-
return {
|
|
31
|
-
sub: session.user.id,
|
|
32
|
-
email: session.user.email,
|
|
33
|
-
name: session.user.name,
|
|
34
|
-
sessionToken: session.session.token,
|
|
35
|
-
roles: session.user.roles || [],
|
|
36
|
-
...session.user,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { logger } from '../config/logger';
|
|
3
|
+
import { getSession } from '../server/auth';
|
|
4
|
+
import { getSessionCookieName } from './app-slug';
|
|
5
|
+
|
|
6
|
+
export async function getTokenTestAware(req: NextRequest): Promise<any> {
|
|
7
|
+
if (process.env.TEST_MODE === 'true') {
|
|
8
|
+
try {
|
|
9
|
+
let secret = process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET;
|
|
10
|
+
if (!secret || secret.trim() === '') {
|
|
11
|
+
const { getIDPClientConfig } = await import('./idp-client-config');
|
|
12
|
+
const idpConfig = await getIDPClientConfig();
|
|
13
|
+
secret = idpConfig.authSecret;
|
|
14
|
+
}
|
|
15
|
+
// Use app-slug prefixed cookie name
|
|
16
|
+
const cookieName = getSessionCookieName();
|
|
17
|
+
const cookies = req.headers.get('cookie'); if (!cookies) { logger.debug('[GET_TOKEN] No cookies in request'); return null; }
|
|
18
|
+
const cookieValue = cookies.split(';').find(c => c.trim().startsWith(`${cookieName}=`))?.split('=')[1];
|
|
19
|
+
if (!cookieValue) { logger.debug('[GET_TOKEN] Session token cookie not found'); return null; }
|
|
20
|
+
const { jwtVerify } = await import('jose');
|
|
21
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
22
|
+
const { payload } = await jwtVerify(cookieValue, secretKey);
|
|
23
|
+
logger.debug('[GET_TOKEN] TEST_MODE token decoded:', { hasPayload: !!payload, redisSessionId: (payload as any).redisSessionId, sub: (payload as any).sub });
|
|
24
|
+
return payload;
|
|
25
|
+
} catch (error) { logger.error('[GET_TOKEN] TEST_MODE token decode error:', { error: error instanceof Error ? error.message : String(error) }); return null; }
|
|
26
|
+
}
|
|
27
|
+
// Production path: Better Auth session
|
|
28
|
+
const session = await getSession(req);
|
|
29
|
+
if (session?.user && session?.session?.token) {
|
|
30
|
+
return {
|
|
31
|
+
sub: session.user.id,
|
|
32
|
+
email: session.user.email,
|
|
33
|
+
name: session.user.name,
|
|
34
|
+
sessionToken: session.session.token,
|
|
35
|
+
roles: session.user.roles || [],
|
|
36
|
+
...session.user,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
@@ -1,252 +1,252 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* =============================================================================
|
|
3
|
-
* USER AGENT PARSER
|
|
4
|
-
* =============================================================================
|
|
5
|
-
*
|
|
6
|
-
* Lightweight user agent parsing without external dependencies.
|
|
7
|
-
* Extracts device type, browser, and OS information from user agent strings.
|
|
8
|
-
*
|
|
9
|
-
* USAGE:
|
|
10
|
-
* ------
|
|
11
|
-
* import { parseUserAgent, DeviceInfo } from '@payez/next-mvp/lib/user-agent-parser';
|
|
12
|
-
*
|
|
13
|
-
* const info = parseUserAgent(request.headers.get('user-agent'));
|
|
14
|
-
* console.log(info.browser); // 'Chrome'
|
|
15
|
-
* console.log(info.deviceType); // 'desktop'
|
|
16
|
-
*
|
|
17
|
-
* =============================================================================
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
// -----------------------------------------------------------------------------
|
|
21
|
-
// TYPES
|
|
22
|
-
// -----------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
export type DeviceType = 'desktop' | 'mobile' | 'tablet' | 'unknown';
|
|
25
|
-
|
|
26
|
-
export interface DeviceInfo {
|
|
27
|
-
deviceType: DeviceType;
|
|
28
|
-
browser: string;
|
|
29
|
-
browserVersion?: string;
|
|
30
|
-
os: string;
|
|
31
|
-
osVersion?: string;
|
|
32
|
-
isMobile: boolean;
|
|
33
|
-
isTablet: boolean;
|
|
34
|
-
isDesktop: boolean;
|
|
35
|
-
isBot: boolean;
|
|
36
|
-
raw: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// -----------------------------------------------------------------------------
|
|
40
|
-
// DETECTION PATTERNS
|
|
41
|
-
// -----------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
const MOBILE_KEYWORDS = [
|
|
44
|
-
'Mobile',
|
|
45
|
-
'Android',
|
|
46
|
-
'iPhone',
|
|
47
|
-
'iPod',
|
|
48
|
-
'BlackBerry',
|
|
49
|
-
'IEMobile',
|
|
50
|
-
'Opera Mini',
|
|
51
|
-
'Opera Mobi',
|
|
52
|
-
'Windows Phone',
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
const TABLET_KEYWORDS = [
|
|
56
|
-
'iPad',
|
|
57
|
-
'Tablet',
|
|
58
|
-
'PlayBook',
|
|
59
|
-
'Silk',
|
|
60
|
-
'Kindle',
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
const BOT_KEYWORDS = [
|
|
64
|
-
'bot',
|
|
65
|
-
'spider',
|
|
66
|
-
'crawler',
|
|
67
|
-
'slurp',
|
|
68
|
-
'googlebot',
|
|
69
|
-
'bingbot',
|
|
70
|
-
'yandex',
|
|
71
|
-
'baidu',
|
|
72
|
-
'duckduckbot',
|
|
73
|
-
'facebookexternalhit',
|
|
74
|
-
'twitterbot',
|
|
75
|
-
'linkedinbot',
|
|
76
|
-
'pinterest',
|
|
77
|
-
'semrush',
|
|
78
|
-
'ahref',
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
// Browser detection patterns (order matters - more specific first)
|
|
82
|
-
const BROWSER_PATTERNS: Array<{ name: string; pattern: RegExp }> = [
|
|
83
|
-
{ name: 'Edge', pattern: /Edg(?:e|A|iOS)?\/(\d+(?:\.\d+)*)/ },
|
|
84
|
-
{ name: 'Opera', pattern: /(?:OPR|Opera)\/(\d+(?:\.\d+)*)/ },
|
|
85
|
-
{ name: 'Samsung Browser', pattern: /SamsungBrowser\/(\d+(?:\.\d+)*)/ },
|
|
86
|
-
{ name: 'UC Browser', pattern: /UCBrowser\/(\d+(?:\.\d+)*)/ },
|
|
87
|
-
{ name: 'Firefox', pattern: /Firefox\/(\d+(?:\.\d+)*)/ },
|
|
88
|
-
{ name: 'Chrome', pattern: /Chrome\/(\d+(?:\.\d+)*)/ },
|
|
89
|
-
{ name: 'Safari', pattern: /Version\/(\d+(?:\.\d+)*).*Safari/ },
|
|
90
|
-
{ name: 'Safari', pattern: /Safari\/(\d+(?:\.\d+)*)/ },
|
|
91
|
-
{ name: 'IE', pattern: /(?:MSIE |rv:)(\d+(?:\.\d+)*)/ },
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
// OS detection patterns
|
|
95
|
-
const OS_PATTERNS: Array<{ name: string; pattern: RegExp; versionPattern?: RegExp }> = [
|
|
96
|
-
{ name: 'iOS', pattern: /iPhone|iPad|iPod/, versionPattern: /OS (\d+[_\.]\d+(?:[_\.]\d+)?)/ },
|
|
97
|
-
{ name: 'Android', pattern: /Android/, versionPattern: /Android (\d+(?:\.\d+)*)/ },
|
|
98
|
-
{ name: 'Windows', pattern: /Windows/, versionPattern: /Windows NT (\d+(?:\.\d+)*)/ },
|
|
99
|
-
{ name: 'macOS', pattern: /Mac OS X/, versionPattern: /Mac OS X (\d+[_\.]\d+(?:[_\.]\d+)?)/ },
|
|
100
|
-
{ name: 'Linux', pattern: /Linux/, versionPattern: undefined },
|
|
101
|
-
{ name: 'Chrome OS', pattern: /CrOS/, versionPattern: undefined },
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
// Windows NT version mapping
|
|
105
|
-
const WINDOWS_VERSIONS: Record<string, string> = {
|
|
106
|
-
'10.0': '10/11',
|
|
107
|
-
'6.3': '8.1',
|
|
108
|
-
'6.2': '8',
|
|
109
|
-
'6.1': '7',
|
|
110
|
-
'6.0': 'Vista',
|
|
111
|
-
'5.1': 'XP',
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// -----------------------------------------------------------------------------
|
|
115
|
-
// PARSER FUNCTION
|
|
116
|
-
// -----------------------------------------------------------------------------
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Parse a user agent string to extract device, browser, and OS information.
|
|
120
|
-
*
|
|
121
|
-
* @param userAgent - The user agent string from request headers
|
|
122
|
-
* @returns Parsed device information
|
|
123
|
-
*/
|
|
124
|
-
export function parseUserAgent(userAgent?: string | null): DeviceInfo {
|
|
125
|
-
const ua = userAgent || '';
|
|
126
|
-
const uaLower = ua.toLowerCase();
|
|
127
|
-
|
|
128
|
-
// Default result
|
|
129
|
-
const result: DeviceInfo = {
|
|
130
|
-
deviceType: 'unknown',
|
|
131
|
-
browser: 'Unknown',
|
|
132
|
-
os: 'Unknown',
|
|
133
|
-
isMobile: false,
|
|
134
|
-
isTablet: false,
|
|
135
|
-
isDesktop: false,
|
|
136
|
-
isBot: false,
|
|
137
|
-
raw: ua,
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
if (!ua) return result;
|
|
141
|
-
|
|
142
|
-
// Check for bots first
|
|
143
|
-
result.isBot = BOT_KEYWORDS.some(keyword => uaLower.includes(keyword));
|
|
144
|
-
|
|
145
|
-
// Detect device type
|
|
146
|
-
result.isTablet = TABLET_KEYWORDS.some(keyword => ua.includes(keyword));
|
|
147
|
-
result.isMobile = !result.isTablet && MOBILE_KEYWORDS.some(keyword => ua.includes(keyword));
|
|
148
|
-
result.isDesktop = !result.isMobile && !result.isTablet && !result.isBot;
|
|
149
|
-
|
|
150
|
-
if (result.isTablet) {
|
|
151
|
-
result.deviceType = 'tablet';
|
|
152
|
-
} else if (result.isMobile) {
|
|
153
|
-
result.deviceType = 'mobile';
|
|
154
|
-
} else if (result.isDesktop) {
|
|
155
|
-
result.deviceType = 'desktop';
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Detect browser
|
|
159
|
-
for (const { name, pattern } of BROWSER_PATTERNS) {
|
|
160
|
-
const match = ua.match(pattern);
|
|
161
|
-
if (match) {
|
|
162
|
-
result.browser = name;
|
|
163
|
-
result.browserVersion = match[1];
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Detect OS
|
|
169
|
-
for (const { name, pattern, versionPattern } of OS_PATTERNS) {
|
|
170
|
-
if (pattern.test(ua)) {
|
|
171
|
-
result.os = name;
|
|
172
|
-
if (versionPattern) {
|
|
173
|
-
const versionMatch = ua.match(versionPattern);
|
|
174
|
-
if (versionMatch) {
|
|
175
|
-
let version = versionMatch[1].replace(/_/g, '.');
|
|
176
|
-
|
|
177
|
-
// Map Windows NT versions to friendly names
|
|
178
|
-
if (name === 'Windows' && WINDOWS_VERSIONS[version]) {
|
|
179
|
-
version = WINDOWS_VERSIONS[version];
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
result.osVersion = version;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return result;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// -----------------------------------------------------------------------------
|
|
193
|
-
// HELPER FUNCTIONS
|
|
194
|
-
// -----------------------------------------------------------------------------
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Get a short, human-readable description of the device
|
|
198
|
-
*/
|
|
199
|
-
export function getDeviceDescription(info: DeviceInfo): string {
|
|
200
|
-
const parts: string[] = [];
|
|
201
|
-
|
|
202
|
-
if (info.browser !== 'Unknown') {
|
|
203
|
-
parts.push(info.browser);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (info.os !== 'Unknown') {
|
|
207
|
-
let osDesc = info.os;
|
|
208
|
-
if (info.osVersion) {
|
|
209
|
-
osDesc += ` ${info.osVersion}`;
|
|
210
|
-
}
|
|
211
|
-
parts.push(osDesc);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (parts.length === 0) {
|
|
215
|
-
if (info.isBot) return 'Bot';
|
|
216
|
-
return info.deviceType.charAt(0).toUpperCase() + info.deviceType.slice(1);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return parts.join(' / ');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Get device type icon (emoji)
|
|
224
|
-
*/
|
|
225
|
-
export function getDeviceIcon(deviceType: DeviceType): string {
|
|
226
|
-
switch (deviceType) {
|
|
227
|
-
case 'mobile':
|
|
228
|
-
return '📱';
|
|
229
|
-
case 'tablet':
|
|
230
|
-
return '📲';
|
|
231
|
-
case 'desktop':
|
|
232
|
-
return '💻';
|
|
233
|
-
default:
|
|
234
|
-
return '🔌';
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Get browser icon (emoji)
|
|
240
|
-
*/
|
|
241
|
-
export function getBrowserIcon(browser: string): string {
|
|
242
|
-
const browserLower = browser.toLowerCase();
|
|
243
|
-
|
|
244
|
-
if (browserLower.includes('chrome')) return '🌐';
|
|
245
|
-
if (browserLower.includes('firefox')) return '🦊';
|
|
246
|
-
if (browserLower.includes('safari')) return '🧭';
|
|
247
|
-
if (browserLower.includes('edge')) return '🌐';
|
|
248
|
-
if (browserLower.includes('opera')) return '🔴';
|
|
249
|
-
if (browserLower.includes('ie') || browserLower.includes('internet explorer')) return '🌐';
|
|
250
|
-
|
|
251
|
-
return '🌐';
|
|
252
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* USER AGENT PARSER
|
|
4
|
+
* =============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Lightweight user agent parsing without external dependencies.
|
|
7
|
+
* Extracts device type, browser, and OS information from user agent strings.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* ------
|
|
11
|
+
* import { parseUserAgent, DeviceInfo } from '@payez/next-mvp/lib/user-agent-parser';
|
|
12
|
+
*
|
|
13
|
+
* const info = parseUserAgent(request.headers.get('user-agent'));
|
|
14
|
+
* console.log(info.browser); // 'Chrome'
|
|
15
|
+
* console.log(info.deviceType); // 'desktop'
|
|
16
|
+
*
|
|
17
|
+
* =============================================================================
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// -----------------------------------------------------------------------------
|
|
21
|
+
// TYPES
|
|
22
|
+
// -----------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export type DeviceType = 'desktop' | 'mobile' | 'tablet' | 'unknown';
|
|
25
|
+
|
|
26
|
+
export interface DeviceInfo {
|
|
27
|
+
deviceType: DeviceType;
|
|
28
|
+
browser: string;
|
|
29
|
+
browserVersion?: string;
|
|
30
|
+
os: string;
|
|
31
|
+
osVersion?: string;
|
|
32
|
+
isMobile: boolean;
|
|
33
|
+
isTablet: boolean;
|
|
34
|
+
isDesktop: boolean;
|
|
35
|
+
isBot: boolean;
|
|
36
|
+
raw: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// -----------------------------------------------------------------------------
|
|
40
|
+
// DETECTION PATTERNS
|
|
41
|
+
// -----------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const MOBILE_KEYWORDS = [
|
|
44
|
+
'Mobile',
|
|
45
|
+
'Android',
|
|
46
|
+
'iPhone',
|
|
47
|
+
'iPod',
|
|
48
|
+
'BlackBerry',
|
|
49
|
+
'IEMobile',
|
|
50
|
+
'Opera Mini',
|
|
51
|
+
'Opera Mobi',
|
|
52
|
+
'Windows Phone',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const TABLET_KEYWORDS = [
|
|
56
|
+
'iPad',
|
|
57
|
+
'Tablet',
|
|
58
|
+
'PlayBook',
|
|
59
|
+
'Silk',
|
|
60
|
+
'Kindle',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const BOT_KEYWORDS = [
|
|
64
|
+
'bot',
|
|
65
|
+
'spider',
|
|
66
|
+
'crawler',
|
|
67
|
+
'slurp',
|
|
68
|
+
'googlebot',
|
|
69
|
+
'bingbot',
|
|
70
|
+
'yandex',
|
|
71
|
+
'baidu',
|
|
72
|
+
'duckduckbot',
|
|
73
|
+
'facebookexternalhit',
|
|
74
|
+
'twitterbot',
|
|
75
|
+
'linkedinbot',
|
|
76
|
+
'pinterest',
|
|
77
|
+
'semrush',
|
|
78
|
+
'ahref',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Browser detection patterns (order matters - more specific first)
|
|
82
|
+
const BROWSER_PATTERNS: Array<{ name: string; pattern: RegExp }> = [
|
|
83
|
+
{ name: 'Edge', pattern: /Edg(?:e|A|iOS)?\/(\d+(?:\.\d+)*)/ },
|
|
84
|
+
{ name: 'Opera', pattern: /(?:OPR|Opera)\/(\d+(?:\.\d+)*)/ },
|
|
85
|
+
{ name: 'Samsung Browser', pattern: /SamsungBrowser\/(\d+(?:\.\d+)*)/ },
|
|
86
|
+
{ name: 'UC Browser', pattern: /UCBrowser\/(\d+(?:\.\d+)*)/ },
|
|
87
|
+
{ name: 'Firefox', pattern: /Firefox\/(\d+(?:\.\d+)*)/ },
|
|
88
|
+
{ name: 'Chrome', pattern: /Chrome\/(\d+(?:\.\d+)*)/ },
|
|
89
|
+
{ name: 'Safari', pattern: /Version\/(\d+(?:\.\d+)*).*Safari/ },
|
|
90
|
+
{ name: 'Safari', pattern: /Safari\/(\d+(?:\.\d+)*)/ },
|
|
91
|
+
{ name: 'IE', pattern: /(?:MSIE |rv:)(\d+(?:\.\d+)*)/ },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// OS detection patterns
|
|
95
|
+
const OS_PATTERNS: Array<{ name: string; pattern: RegExp; versionPattern?: RegExp }> = [
|
|
96
|
+
{ name: 'iOS', pattern: /iPhone|iPad|iPod/, versionPattern: /OS (\d+[_\.]\d+(?:[_\.]\d+)?)/ },
|
|
97
|
+
{ name: 'Android', pattern: /Android/, versionPattern: /Android (\d+(?:\.\d+)*)/ },
|
|
98
|
+
{ name: 'Windows', pattern: /Windows/, versionPattern: /Windows NT (\d+(?:\.\d+)*)/ },
|
|
99
|
+
{ name: 'macOS', pattern: /Mac OS X/, versionPattern: /Mac OS X (\d+[_\.]\d+(?:[_\.]\d+)?)/ },
|
|
100
|
+
{ name: 'Linux', pattern: /Linux/, versionPattern: undefined },
|
|
101
|
+
{ name: 'Chrome OS', pattern: /CrOS/, versionPattern: undefined },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Windows NT version mapping
|
|
105
|
+
const WINDOWS_VERSIONS: Record<string, string> = {
|
|
106
|
+
'10.0': '10/11',
|
|
107
|
+
'6.3': '8.1',
|
|
108
|
+
'6.2': '8',
|
|
109
|
+
'6.1': '7',
|
|
110
|
+
'6.0': 'Vista',
|
|
111
|
+
'5.1': 'XP',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// -----------------------------------------------------------------------------
|
|
115
|
+
// PARSER FUNCTION
|
|
116
|
+
// -----------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse a user agent string to extract device, browser, and OS information.
|
|
120
|
+
*
|
|
121
|
+
* @param userAgent - The user agent string from request headers
|
|
122
|
+
* @returns Parsed device information
|
|
123
|
+
*/
|
|
124
|
+
export function parseUserAgent(userAgent?: string | null): DeviceInfo {
|
|
125
|
+
const ua = userAgent || '';
|
|
126
|
+
const uaLower = ua.toLowerCase();
|
|
127
|
+
|
|
128
|
+
// Default result
|
|
129
|
+
const result: DeviceInfo = {
|
|
130
|
+
deviceType: 'unknown',
|
|
131
|
+
browser: 'Unknown',
|
|
132
|
+
os: 'Unknown',
|
|
133
|
+
isMobile: false,
|
|
134
|
+
isTablet: false,
|
|
135
|
+
isDesktop: false,
|
|
136
|
+
isBot: false,
|
|
137
|
+
raw: ua,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (!ua) return result;
|
|
141
|
+
|
|
142
|
+
// Check for bots first
|
|
143
|
+
result.isBot = BOT_KEYWORDS.some(keyword => uaLower.includes(keyword));
|
|
144
|
+
|
|
145
|
+
// Detect device type
|
|
146
|
+
result.isTablet = TABLET_KEYWORDS.some(keyword => ua.includes(keyword));
|
|
147
|
+
result.isMobile = !result.isTablet && MOBILE_KEYWORDS.some(keyword => ua.includes(keyword));
|
|
148
|
+
result.isDesktop = !result.isMobile && !result.isTablet && !result.isBot;
|
|
149
|
+
|
|
150
|
+
if (result.isTablet) {
|
|
151
|
+
result.deviceType = 'tablet';
|
|
152
|
+
} else if (result.isMobile) {
|
|
153
|
+
result.deviceType = 'mobile';
|
|
154
|
+
} else if (result.isDesktop) {
|
|
155
|
+
result.deviceType = 'desktop';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Detect browser
|
|
159
|
+
for (const { name, pattern } of BROWSER_PATTERNS) {
|
|
160
|
+
const match = ua.match(pattern);
|
|
161
|
+
if (match) {
|
|
162
|
+
result.browser = name;
|
|
163
|
+
result.browserVersion = match[1];
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Detect OS
|
|
169
|
+
for (const { name, pattern, versionPattern } of OS_PATTERNS) {
|
|
170
|
+
if (pattern.test(ua)) {
|
|
171
|
+
result.os = name;
|
|
172
|
+
if (versionPattern) {
|
|
173
|
+
const versionMatch = ua.match(versionPattern);
|
|
174
|
+
if (versionMatch) {
|
|
175
|
+
let version = versionMatch[1].replace(/_/g, '.');
|
|
176
|
+
|
|
177
|
+
// Map Windows NT versions to friendly names
|
|
178
|
+
if (name === 'Windows' && WINDOWS_VERSIONS[version]) {
|
|
179
|
+
version = WINDOWS_VERSIONS[version];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
result.osVersion = version;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// -----------------------------------------------------------------------------
|
|
193
|
+
// HELPER FUNCTIONS
|
|
194
|
+
// -----------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get a short, human-readable description of the device
|
|
198
|
+
*/
|
|
199
|
+
export function getDeviceDescription(info: DeviceInfo): string {
|
|
200
|
+
const parts: string[] = [];
|
|
201
|
+
|
|
202
|
+
if (info.browser !== 'Unknown') {
|
|
203
|
+
parts.push(info.browser);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (info.os !== 'Unknown') {
|
|
207
|
+
let osDesc = info.os;
|
|
208
|
+
if (info.osVersion) {
|
|
209
|
+
osDesc += ` ${info.osVersion}`;
|
|
210
|
+
}
|
|
211
|
+
parts.push(osDesc);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (parts.length === 0) {
|
|
215
|
+
if (info.isBot) return 'Bot';
|
|
216
|
+
return info.deviceType.charAt(0).toUpperCase() + info.deviceType.slice(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return parts.join(' / ');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get device type icon (emoji)
|
|
224
|
+
*/
|
|
225
|
+
export function getDeviceIcon(deviceType: DeviceType): string {
|
|
226
|
+
switch (deviceType) {
|
|
227
|
+
case 'mobile':
|
|
228
|
+
return '📱';
|
|
229
|
+
case 'tablet':
|
|
230
|
+
return '📲';
|
|
231
|
+
case 'desktop':
|
|
232
|
+
return '💻';
|
|
233
|
+
default:
|
|
234
|
+
return '🔌';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get browser icon (emoji)
|
|
240
|
+
*/
|
|
241
|
+
export function getBrowserIcon(browser: string): string {
|
|
242
|
+
const browserLower = browser.toLowerCase();
|
|
243
|
+
|
|
244
|
+
if (browserLower.includes('chrome')) return '🌐';
|
|
245
|
+
if (browserLower.includes('firefox')) return '🦊';
|
|
246
|
+
if (browserLower.includes('safari')) return '🧭';
|
|
247
|
+
if (browserLower.includes('edge')) return '🌐';
|
|
248
|
+
if (browserLower.includes('opera')) return '🔴';
|
|
249
|
+
if (browserLower.includes('ie') || browserLower.includes('internet explorer')) return '🌐';
|
|
250
|
+
|
|
251
|
+
return '🌐';
|
|
252
|
+
}
|