@payez/next-mvp 4.5.6 → 4.7.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/dist/api-handlers/auth/credential-login-email-code.d.ts +24 -0
- package/dist/api-handlers/auth/credential-login-email-code.js +83 -0
- package/dist/api-handlers/auth/credential-login.d.ts +41 -0
- package/dist/api-handlers/auth/credential-login.js +858 -0
- package/dist/api-handlers/oauth/resume.d.ts +16 -0
- package/dist/api-handlers/oauth/resume.js +207 -0
- package/dist/auth/better-auth.d.ts +14 -2
- package/dist/auth/better-auth.js +59 -34
- package/dist/auth/idp-oauth.d.ts +106 -0
- package/dist/auth/idp-oauth.js +280 -0
- package/dist/auth/safe-callback.js +15 -2
- package/dist/auth/session-cookie-clear.d.ts +7 -0
- package/dist/auth/session-cookie-clear.js +24 -0
- package/dist/auth/session-reset-link.d.ts +43 -0
- package/dist/auth/session-reset-link.js +144 -0
- package/dist/components/auth/CredentialSignInForm.d.ts +33 -0
- package/dist/components/auth/CredentialSignInForm.js +134 -0
- package/dist/lib/authorize-resume.d.ts +87 -0
- package/dist/lib/authorize-resume.js +190 -0
- package/dist/lib/bearer-fetch.d.ts +17 -0
- package/dist/lib/bearer-fetch.js +32 -0
- package/dist/lib/code-step-copy.d.ts +53 -0
- package/dist/lib/code-step-copy.js +73 -0
- package/dist/lib/credential-sign-in-client.d.ts +59 -0
- package/dist/lib/credential-sign-in-client.js +89 -0
- package/dist/lib/device-id.d.ts +37 -0
- package/dist/lib/device-id.js +70 -0
- package/dist/lib/lock-window.d.ts +29 -0
- package/dist/lib/lock-window.js +53 -0
- package/dist/lib/login-error-message.d.ts +18 -0
- package/dist/lib/login-error-message.js +47 -0
- package/dist/lib/twofa-rejection.d.ts +20 -0
- package/dist/lib/twofa-rejection.js +73 -0
- package/dist/middleware/create-middleware.d.ts +25 -1
- package/dist/middleware/create-middleware.js +75 -27
- package/dist/pages/login/page.js +11 -37
- package/dist/routes/auth/credential-login-email-code.d.ts +6 -0
- package/dist/routes/auth/credential-login-email-code.js +10 -0
- package/dist/routes/auth/credential-login.d.ts +6 -0
- package/dist/routes/auth/credential-login.js +10 -0
- package/dist/routes/auth/session-reset.d.ts +21 -0
- package/dist/routes/auth/session-reset.js +40 -0
- package/dist/routes/oauth/resume.d.ts +11 -0
- package/dist/routes/oauth/resume.js +16 -0
- package/package.json +1102 -1042
- package/src/api-handlers/auth/credential-login-email-code.ts +92 -0
- package/src/api-handlers/auth/credential-login.ts +948 -0
- package/src/api-handlers/oauth/resume.ts +182 -0
- package/src/auth/better-auth.ts +81 -5
- package/src/auth/idp-oauth.ts +310 -0
- package/src/auth/safe-callback.ts +16 -2
- package/src/auth/session-cookie-clear.ts +30 -0
- package/src/auth/session-reset-link.ts +159 -0
- package/src/components/auth/CredentialSignInForm.tsx +240 -0
- package/src/lib/authorize-resume.ts +187 -0
- package/src/lib/bearer-fetch.ts +31 -0
- package/src/lib/code-step-copy.ts +83 -0
- package/src/lib/credential-sign-in-client.ts +101 -0
- package/src/lib/device-id.ts +67 -0
- package/src/lib/lock-window.ts +49 -0
- package/src/lib/login-error-message.ts +41 -0
- package/src/lib/twofa-rejection.ts +91 -0
- package/src/middleware/create-middleware.ts +124 -30
- package/src/pages/login/page.tsx +20 -34
- package/src/routes/auth/credential-login-email-code.ts +6 -0
- package/src/routes/auth/credential-login.ts +6 -0
- package/src/routes/auth/session-reset.ts +37 -0
- package/src/routes/oauth/resume.ts +11 -0
|
@@ -28,6 +28,8 @@ const safe_callback_1 = require("../auth/safe-callback");
|
|
|
28
28
|
const internal_api_url_1 = require("../edge/internal-api-url");
|
|
29
29
|
const app_slug_1 = require("../lib/app-slug");
|
|
30
30
|
const rbac_check_1 = require("./rbac-check");
|
|
31
|
+
const session_cookie_clear_1 = require("../auth/session-cookie-clear");
|
|
32
|
+
const session_reset_link_1 = require("../auth/session-reset-link");
|
|
31
33
|
// =============================================================================
|
|
32
34
|
// DEFAULT IMPLEMENTATIONS
|
|
33
35
|
// =============================================================================
|
|
@@ -58,8 +60,12 @@ const defaultLogger = {
|
|
|
58
60
|
// =============================================================================
|
|
59
61
|
const LOGIN_PAGE = '/account-auth/login';
|
|
60
62
|
const VERIFY_CODE_PAGE = '/account-auth/verify-code';
|
|
63
|
+
// PAY-2315 (4.7.1): /login is a public alias of the login page. It used to take the 'public: allow' arm and never reached the
|
|
64
|
+
// 'already_authenticated' redirect, so a signed-in user was shown the sign-in form. It is the login page for every arm now.
|
|
65
|
+
// (safe-callback refuses /login as a destination, so this cannot redirect to itself.)
|
|
66
|
+
const LOGIN_ALIAS = '/login';
|
|
61
67
|
function isLoginPage(pathname) {
|
|
62
|
-
return pathname === LOGIN_PAGE;
|
|
68
|
+
return pathname === LOGIN_PAGE || pathname === LOGIN_ALIAS;
|
|
63
69
|
}
|
|
64
70
|
/**
|
|
65
71
|
* Ensures callback URLs never point to auth pages (prevents redirect loops) AND
|
|
@@ -105,24 +111,7 @@ function isPrefetchRequest(request) {
|
|
|
105
111
|
function clearSessionCookies(response, request) {
|
|
106
112
|
if (isPrefetchRequest(request))
|
|
107
113
|
return;
|
|
108
|
-
|
|
109
|
-
const secureCookie = (0, app_slug_1.getSecureSessionCookieName)();
|
|
110
|
-
// Clear main cookies
|
|
111
|
-
response.cookies.set(sessionCookie, '', { path: '/', expires: new Date(0), httpOnly: true, sameSite: 'lax' });
|
|
112
|
-
response.cookies.set(secureCookie, '', { path: '/', expires: new Date(0), httpOnly: true, sameSite: 'lax', secure: true });
|
|
113
|
-
// Clear any chunked cookies (NextAuth chunks large cookies)
|
|
114
|
-
for (const [name] of request.cookies) {
|
|
115
|
-
if (name.startsWith(`${sessionCookie}.`) || name.startsWith(`${secureCookie}.`)) {
|
|
116
|
-
const isSecure = name.startsWith('__Secure-');
|
|
117
|
-
response.cookies.set(name, '', {
|
|
118
|
-
path: '/',
|
|
119
|
-
expires: new Date(0),
|
|
120
|
-
httpOnly: true,
|
|
121
|
-
sameSite: 'lax',
|
|
122
|
-
...(isSecure && { secure: true })
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
114
|
+
(0, session_cookie_clear_1.expireSessionCookies)(response, Array.from(request.cookies, ([name]) => name));
|
|
126
115
|
}
|
|
127
116
|
/** Create redirect response, optionally clearing cookies */
|
|
128
117
|
function redirectTo(request, location, clearCookies = false) {
|
|
@@ -132,6 +121,28 @@ function redirectTo(request, location, clearCookies = false) {
|
|
|
132
121
|
}
|
|
133
122
|
return response;
|
|
134
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* A redirect that must also END the session. With the reset route configured, a page request is sent through it
|
|
126
|
+
* WITHOUT any Set-Cookie here: the route sees the flight headers this middleware cannot (Next strips them before
|
|
127
|
+
* middleware), answers a prefetch 204 + no-store, and clears on the real navigation. `/api/` requests and apps
|
|
128
|
+
* without the route keep the direct clear exactly as before.
|
|
129
|
+
*/
|
|
130
|
+
async function clearingRedirect(request, location, reset) {
|
|
131
|
+
if (!reset || request.nextUrl.pathname.startsWith('/api/')) {
|
|
132
|
+
return redirectTo(request, location, true);
|
|
133
|
+
}
|
|
134
|
+
const target = new URL(location, request.url);
|
|
135
|
+
const params = (0, session_reset_link_1.normaliseResetParams)({
|
|
136
|
+
callbackUrl: target.searchParams.get('callbackUrl'),
|
|
137
|
+
reason: target.searchParams.get('reason'),
|
|
138
|
+
error: target.searchParams.get('error'),
|
|
139
|
+
});
|
|
140
|
+
// Bound to THIS request's session cookie: the route clears only when its own request carries the same one.
|
|
141
|
+
const bound = (0, session_reset_link_1.boundSessionCookie)((n) => request.cookies.get(n)?.value, (0, app_slug_1.getSessionCookieName)(), (0, app_slug_1.getSecureSessionCookieName)());
|
|
142
|
+
const response = server_1.NextResponse.redirect(new URL(await (0, session_reset_link_1.signedResetHref)(reset.route, params, reset.secret, bound), request.url));
|
|
143
|
+
response.headers.set('cache-control', 'no-store');
|
|
144
|
+
return response;
|
|
145
|
+
}
|
|
135
146
|
/**
|
|
136
147
|
* Terminal response for a FAILED token refresh.
|
|
137
148
|
*
|
|
@@ -150,7 +161,7 @@ function redirectTo(request, location, clearCookies = false) {
|
|
|
150
161
|
* higher would intercept 'refresh_needed' and disable middleware token refresh across
|
|
151
162
|
* the whole API surface (card 340024 C1).
|
|
152
163
|
*/
|
|
153
|
-
function refreshFailureResponse(request, location, errorCode, clearCookies = false) {
|
|
164
|
+
async function refreshFailureResponse(request, location, errorCode, clearCookies = false, reset) {
|
|
154
165
|
if (request.nextUrl.pathname.startsWith('/api/')) {
|
|
155
166
|
const response = server_1.NextResponse.json({ error: errorCode }, { status: 401 });
|
|
156
167
|
if (clearCookies) {
|
|
@@ -158,7 +169,7 @@ function refreshFailureResponse(request, location, errorCode, clearCookies = fal
|
|
|
158
169
|
}
|
|
159
170
|
return response;
|
|
160
171
|
}
|
|
161
|
-
return
|
|
172
|
+
return clearCookies ? clearingRedirect(request, location, reset) : redirectTo(request, location);
|
|
162
173
|
}
|
|
163
174
|
/**
|
|
164
175
|
* PAY-2147: the terminal answer when the middleware CANNOT TELL whether the session is good
|
|
@@ -359,6 +370,8 @@ function redirect2FA(callbackUrl) {
|
|
|
359
370
|
// =============================================================================
|
|
360
371
|
// MIDDLEWARE FACTORY
|
|
361
372
|
// =============================================================================
|
|
373
|
+
/** PAY-2218: the stripped-flight-headers warning is logged once per process, not once per request. */
|
|
374
|
+
let flightHeadersStrippedWarned = false;
|
|
362
375
|
function createMvpMiddleware(options = {}) {
|
|
363
376
|
// Resolve options with defaults
|
|
364
377
|
const cb = options.circuitBreaker || defaultCircuitBreaker;
|
|
@@ -369,6 +382,29 @@ function createMvpMiddleware(options = {}) {
|
|
|
369
382
|
const rbacExemptPaths = options.rbacExemptPaths || [];
|
|
370
383
|
// PAY-1882 step 4: default 'closed' (Jon's option A / BAPert 66316).
|
|
371
384
|
const revocationFailMode = options.revocationFailMode || 'closed';
|
|
385
|
+
// PAY-2302 cut 1c: strictly `=== true`, so a truthy string in a config file cannot switch revocation off.
|
|
386
|
+
const noRevocationSource = options.noRevocationSource === true;
|
|
387
|
+
if (noRevocationSource) {
|
|
388
|
+
log.warn('[next-mvp] noRevocationSource is ON: sessions of this app CANNOT be revoked server-side (no vibe client / HMAC key). ' +
|
|
389
|
+
"revocationStatus 'not_configured' is treated as viable; 'unknown' and 'revoked' still sign the user out. " +
|
|
390
|
+
'Remove this option once the environment has a vibe client id and HMAC key.');
|
|
391
|
+
}
|
|
392
|
+
const sessionResetRoute = options.sessionResetRoute === true ? session_reset_link_1.DEFAULT_SESSION_RESET_ROUTE
|
|
393
|
+
: typeof options.sessionResetRoute === 'string' && options.sessionResetRoute ? options.sessionResetRoute
|
|
394
|
+
: undefined;
|
|
395
|
+
let resetSecretMissingLogged = false;
|
|
396
|
+
const resolveReset = () => {
|
|
397
|
+
if (!sessionResetRoute)
|
|
398
|
+
return undefined;
|
|
399
|
+
const secret = (0, session_reset_link_1.resetSecret)();
|
|
400
|
+
if (secret)
|
|
401
|
+
return { route: sessionResetRoute, secret };
|
|
402
|
+
if (!resetSecretMissingLogged) {
|
|
403
|
+
resetSecretMissingLogged = true;
|
|
404
|
+
log.error('sessionResetRoute is set but no BETTER_AUTH_SECRET/NEXTAUTH_SECRET is available to sign the hand-off; clearing in middleware instead', { sessionResetRoute });
|
|
405
|
+
}
|
|
406
|
+
return undefined;
|
|
407
|
+
};
|
|
372
408
|
return async function middleware(request) {
|
|
373
409
|
const { pathname, searchParams } = request.nextUrl;
|
|
374
410
|
// =========================================================================
|
|
@@ -387,6 +423,12 @@ function createMvpMiddleware(options = {}) {
|
|
|
387
423
|
return server_1.NextResponse.next();
|
|
388
424
|
}
|
|
389
425
|
}
|
|
426
|
+
// PAY-2218: a router request (it carries `next-url`) that arrives WITHOUT `rsc` had its flight headers stripped
|
|
427
|
+
// by Next before middleware ran, so isPrefetchRequest() cannot see a router prefetch on this app. Say so once.
|
|
428
|
+
if (!sessionResetRoute && !flightHeadersStrippedWarned && request.headers.has('next-url') && !request.headers.has('rsc')) {
|
|
429
|
+
flightHeadersStrippedWarned = true;
|
|
430
|
+
log.warn('Next strips router prefetch headers before middleware on this app, so a session-ending redirect can fire on a prefetch. Mount @payez/next-mvp/routes/auth/session-reset and set sessionResetRoute (PAY-2218).', { pathname });
|
|
431
|
+
}
|
|
390
432
|
const isPublic = (0, route_config_1.isUnauthenticatedRoute)(pathname);
|
|
391
433
|
const isLogin = isLoginPage(pathname);
|
|
392
434
|
// --- Optimization: Skip viability check for unauthenticated public browsing ---
|
|
@@ -399,7 +441,7 @@ function createMvpMiddleware(options = {}) {
|
|
|
399
441
|
// Has cookie - continue to viability check for 2FA enforcement
|
|
400
442
|
}
|
|
401
443
|
// --- Fetch session viability from internal API ---
|
|
402
|
-
const { sessionStatus, sessionPointer } = await checkViability(request, viabilityEndpoint, log, revocationFailMode);
|
|
444
|
+
const { sessionStatus, sessionPointer } = await checkViability(request, viabilityEndpoint, log, revocationFailMode, noRevocationSource);
|
|
403
445
|
// --- Stale session (cookie exists but session doesn't): OBSERVE ONLY, DO NOT DECIDE ---
|
|
404
446
|
//
|
|
405
447
|
// *** CARD 349607. THIS BLOCK USED TO `return` HERE, AND THAT RETURN WAS A BYPASS. ***
|
|
@@ -456,6 +498,7 @@ function createMvpMiddleware(options = {}) {
|
|
|
456
498
|
logger: log,
|
|
457
499
|
refreshEndpoint,
|
|
458
500
|
rbacExemptPaths,
|
|
501
|
+
reset: resolveReset(),
|
|
459
502
|
onRefreshSuccess: options.onRefreshSuccess,
|
|
460
503
|
onRefreshFailure: options.onRefreshFailure,
|
|
461
504
|
});
|
|
@@ -473,13 +516,16 @@ function createMvpMiddleware(options = {}) {
|
|
|
473
516
|
* 'unknown'/'not_configured'. The booleans are read ONLY when revocationStatus is
|
|
474
517
|
* absent (idealresume.online's older boolean-only route).
|
|
475
518
|
*/
|
|
476
|
-
function decideForceInvalid(data, revocationFailMode) {
|
|
519
|
+
function decideForceInvalid(data, revocationFailMode, noRevocationSource = false) {
|
|
477
520
|
const status = data.revocationStatus;
|
|
478
521
|
if (typeof status === 'string') {
|
|
479
522
|
if (status === 'revoked')
|
|
480
523
|
return true;
|
|
481
524
|
if (status === 'active')
|
|
482
525
|
return false;
|
|
526
|
+
// PAY-2302 cut 1c: an app that declared it has no revocation source. ONLY the exact 'not_configured' string.
|
|
527
|
+
if (status === 'not_configured' && noRevocationSource === true)
|
|
528
|
+
return false;
|
|
483
529
|
// S1 (NightHawk 66409): ANY other string - 'unknown', 'not_configured', a
|
|
484
530
|
// typo like 'Revoked', 'error', or '' - is treated as 'unknown' (fail mode
|
|
485
531
|
// applies), NEVER as 'active'. Returning false here would let the first
|
|
@@ -502,7 +548,7 @@ function hasSessionCookie(request) {
|
|
|
502
548
|
return !!(request.cookies.get((0, app_slug_1.getSessionCookieName)())?.value || request.cookies.get((0, app_slug_1.getSecureSessionCookieName)())?.value);
|
|
503
549
|
}
|
|
504
550
|
/** Check session viability via internal API */
|
|
505
|
-
async function checkViability(request, endpoint, log, revocationFailMode) {
|
|
551
|
+
async function checkViability(request, endpoint, log, revocationFailMode, noRevocationSource = false) {
|
|
506
552
|
const sessionStatus = {
|
|
507
553
|
exists: false,
|
|
508
554
|
forceInvalid: false,
|
|
@@ -574,7 +620,7 @@ async function checkViability(request, endpoint, log, revocationFailMode) {
|
|
|
574
620
|
// revocations status, decide from it + failMode and ignore the folded
|
|
575
621
|
// booleans for unknown/not_configured. The booleans are read ONLY when
|
|
576
622
|
// revocationStatus is ABSENT (idealresume.online's older boolean route).
|
|
577
|
-
sessionStatus.forceInvalid = decideForceInvalid(data, revocationFailMode);
|
|
623
|
+
sessionStatus.forceInvalid = decideForceInvalid(data, revocationFailMode, noRevocationSource);
|
|
578
624
|
// PAY-2216: a STALE session - the cookie resolved to a token, but no store holds the session - used to set
|
|
579
625
|
// pointer.exists=false, so makeAuthDecision took no_session (login, cookie NOT cleared) and its stale_session
|
|
580
626
|
// arm (clearCookies) was unreachable: a dead cookie lingered and re-ran viability on every request.
|
|
@@ -613,7 +659,9 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
|
|
|
613
659
|
case 'allow':
|
|
614
660
|
return handleAllow(request, pathname, sessionPointer, sessionStatus, opts.rbacExemptPaths);
|
|
615
661
|
case 'redirect':
|
|
616
|
-
return
|
|
662
|
+
return decision.clearCookies
|
|
663
|
+
? clearingRedirect(request, decision.location, opts.reset)
|
|
664
|
+
: redirectTo(request, decision.location);
|
|
617
665
|
case 'service_error':
|
|
618
666
|
return serviceUnavailableResponse(request);
|
|
619
667
|
case 'refresh_needed':
|
|
@@ -719,7 +767,7 @@ async function handleRefresh(request, safeCallback, opts) {
|
|
|
719
767
|
clearingCookies: isPermanentFailure
|
|
720
768
|
});
|
|
721
769
|
onRefreshFailure?.(response.status, false);
|
|
722
|
-
return refreshFailureResponse(request, `${LOGIN_PAGE}?callbackUrl=${encodeURIComponent(safeCallback)}&error=${errorParam}`, errorParam, isPermanentFailure);
|
|
770
|
+
return refreshFailureResponse(request, `${LOGIN_PAGE}?callbackUrl=${encodeURIComponent(safeCallback)}&error=${errorParam}`, errorParam, isPermanentFailure, opts.reset);
|
|
723
771
|
}
|
|
724
772
|
catch (error) {
|
|
725
773
|
const isNetwork = cb.isNetworkError(error);
|
package/dist/pages/login/page.js
CHANGED
|
@@ -29,6 +29,7 @@ const better_auth_client_1 = require("../../client/better-auth-client");
|
|
|
29
29
|
const navigation_1 = require("next/navigation");
|
|
30
30
|
const react_2 = require("react");
|
|
31
31
|
const useTheme_1 = require("../../theme/useTheme");
|
|
32
|
+
const login_error_message_1 = require("../../lib/login-error-message");
|
|
32
33
|
function LoginForm({ altAuthSlot }) {
|
|
33
34
|
const searchParams = (0, navigation_1.useSearchParams)();
|
|
34
35
|
const callbackUrl = searchParams?.get('callbackUrl') || '/dashboard';
|
|
@@ -110,46 +111,19 @@ function LoginForm({ altAuthSlot }) {
|
|
|
110
111
|
// Atomic state management - only ONE can be true at a time
|
|
111
112
|
const [isSubmitting, setIsSubmitting] = (0, react_1.useState)(false);
|
|
112
113
|
const [loginSuccess, setLoginSuccess] = (0, react_1.useState)(false);
|
|
113
|
-
|
|
114
|
+
// undefined = the user has not acted yet (show what the URL says); null/string = an explicit state set by a handler below.
|
|
115
|
+
const [loginError, setLoginError] = (0, react_1.useState)(undefined);
|
|
114
116
|
const [loading, setLoading] = (0, react_1.useState)(false);
|
|
115
117
|
const [showRecoveryOptions, setShowRecoveryOptions] = (0, react_1.useState)(false);
|
|
116
118
|
// Force show form after timeout if stuck in loading
|
|
117
119
|
const [forceShowForm, setForceShowForm] = (0, react_1.useState)(false);
|
|
118
|
-
//
|
|
120
|
+
// PAY-2319 (4.7.2): what the URL says, derived at render (no effect) so it is shown on the FIRST paint and in a static render.
|
|
121
|
+
// 4.7.1 mapped these in effects that only set loginError, which was never rendered. Unknown codes are NOT echoed (see lib/login-error-message).
|
|
122
|
+
const urlMessage = (0, login_error_message_1.oauthFailureMessage)(oauthError) ?? (0, login_error_message_1.friendlyLoginError)(urlError);
|
|
123
|
+
const shownError = loginError !== undefined ? loginError : urlMessage;
|
|
119
124
|
(0, react_1.useEffect)(() => {
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
setLoginError(`${name} sign-in did not complete. Please try again.`);
|
|
123
|
-
}
|
|
124
|
-
}, [oauthError]);
|
|
125
|
-
// Handle URL errors with user-friendly messages
|
|
126
|
-
(0, react_1.useEffect)(() => {
|
|
127
|
-
if (urlError) {
|
|
128
|
-
let friendlyError = urlError;
|
|
129
|
-
if (urlError === 'SessionExpired') {
|
|
130
|
-
// Session expiration is normal - don't show as error
|
|
131
|
-
console.log('[LOGIN] SessionExpired detected - normal behavior');
|
|
132
|
-
friendlyError = null;
|
|
133
|
-
}
|
|
134
|
-
else if (urlError === 'TwoFactorTimeout') {
|
|
135
|
-
// PAY-884: the sign-in expired while the verification code was outstanding. Unlike
|
|
136
|
-
// SessionExpired this is NOT silent - the user was mid-2FA and must know why they are here.
|
|
137
|
-
friendlyError = 'Your verification step timed out before the code was confirmed. Please sign in again.';
|
|
138
|
-
}
|
|
139
|
-
else if (urlError === 'ServiceUnavailable') {
|
|
140
|
-
friendlyError = 'Authentication service is temporarily unavailable. Please try again in a few minutes.';
|
|
141
|
-
}
|
|
142
|
-
else if (urlError.startsWith("Unexpected token")) {
|
|
143
|
-
friendlyError = 'An authentication error occurred. Please try signing in again.';
|
|
144
|
-
}
|
|
145
|
-
else if (urlError.includes('not valid JSON')) {
|
|
146
|
-
friendlyError = 'An authentication error occurred. Please try signing in again.';
|
|
147
|
-
}
|
|
148
|
-
else if (urlError === 'CredentialsSignin') {
|
|
149
|
-
friendlyError = 'Invalid email or password. Please try again.';
|
|
150
|
-
}
|
|
151
|
-
setLoginError(friendlyError);
|
|
152
|
-
}
|
|
125
|
+
if (urlError === 'SessionExpired')
|
|
126
|
+
console.log('[LOGIN] SessionExpired detected - normal behavior');
|
|
153
127
|
}, [urlError]);
|
|
154
128
|
// Timeout to prevent infinite loading states
|
|
155
129
|
(0, react_1.useEffect)(() => {
|
|
@@ -175,7 +149,7 @@ function LoginForm({ altAuthSlot }) {
|
|
|
175
149
|
// authClient.signIn.email, which better-auth always rejected because this
|
|
176
150
|
// package never configures emailAndPassword. See the note at the render site.
|
|
177
151
|
// Show loading spinner for initial loads
|
|
178
|
-
if (!forceShowForm && !
|
|
152
|
+
if (!forceShowForm && !shownError && (loading || (status === 'loading' && !session))) {
|
|
179
153
|
return ((0, jsx_runtime_1.jsxs)("div", { className: `flex flex-col items-center justify-center min-h-screen ${colors.background}`, children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin h-6 w-6", style: { color: buttonColor }, viewBox: "0 0 24 24", fill: "none", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" })] }), (0, jsx_runtime_1.jsx)("p", { className: `mt-4 text-sm ${colors.muted}`, children: status === 'loading' ? 'Loading...' : 'Authenticating...' })] }));
|
|
180
154
|
}
|
|
181
155
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: `
|
|
@@ -206,7 +180,7 @@ function LoginForm({ altAuthSlot }) {
|
|
|
206
180
|
.themed-status-submitting svg {
|
|
207
181
|
color: ${buttonColor} !important;
|
|
208
182
|
}
|
|
209
|
-
` } }), (0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center px-4 ${colors.background}`, children: (0, jsx_runtime_1.jsx)("div", { className: "max-w-md w-full", children: (0, jsx_runtime_1.jsxs)("div", { className: `rounded-2xl shadow-xl p-8 ${colors.card} ${colors.border} text-gray-800`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-center mb-8", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-3xl font-bold mb-2", style: { color: 'inherit' }, children: branding.tagline || 'Welcome Back' }), (0, jsx_runtime_1.jsx)("p", { className: colors.muted, children: "Sign in to your account" })] }), googleEnabled && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => void handleGoogleSignIn(), disabled: googleLoading || isSubmitting, "data-testid": "google-sign-in", className: "w-full px-4 py-3 mb-6 rounded-lg border border-gray-300 font-semibold transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50", style: { backgroundColor: 'white', color: '#1f2937' }, children: [googleLoading ? ((0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin w-5 h-5", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" })] })) : ((0, jsx_runtime_1.jsxs)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", children: [(0, jsx_runtime_1.jsx)("path", { d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z", fill: "#4285F4" }), (0, jsx_runtime_1.jsx)("path", { d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", fill: "#34A853" }), (0, jsx_runtime_1.jsx)("path", { d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z", fill: "#FBBC05" }), (0, jsx_runtime_1.jsx)("path", { d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", fill: "#EA4335" })] })), googleLoading ? 'Redirecting to Google…' : 'Continue with Google'] }) })), altAuthSlot && (0, jsx_runtime_1.jsx)("div", { className: "mt-4", children: altAuthSlot }), (0, jsx_runtime_1.jsx)("div", { className: `mt-6 text-center text-sm ${colors.muted}`, children: (0, jsx_runtime_1.jsx)("a", { href: "/account-auth/recovery", className: "themed-link hover:underline", children: "Start account recovery" }) })] }) }) })] }));
|
|
183
|
+
` } }), (0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center px-4 ${colors.background}`, children: (0, jsx_runtime_1.jsx)("div", { className: "max-w-md w-full", children: (0, jsx_runtime_1.jsxs)("div", { className: `rounded-2xl shadow-xl p-8 ${colors.card} ${colors.border} text-gray-800`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-center mb-8", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-3xl font-bold mb-2", style: { color: 'inherit' }, children: branding.tagline || 'Welcome Back' }), (0, jsx_runtime_1.jsx)("p", { className: colors.muted, children: "Sign in to your account" })] }), shownError && ((0, jsx_runtime_1.jsx)("div", { role: "alert", "data-testid": "login-error", className: "mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700", children: shownError })), googleEnabled && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => void handleGoogleSignIn(), disabled: googleLoading || isSubmitting, "data-testid": "google-sign-in", className: "w-full px-4 py-3 mb-6 rounded-lg border border-gray-300 font-semibold transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50", style: { backgroundColor: 'white', color: '#1f2937' }, children: [googleLoading ? ((0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin w-5 h-5", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" })] })) : ((0, jsx_runtime_1.jsxs)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", children: [(0, jsx_runtime_1.jsx)("path", { d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z", fill: "#4285F4" }), (0, jsx_runtime_1.jsx)("path", { d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", fill: "#34A853" }), (0, jsx_runtime_1.jsx)("path", { d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z", fill: "#FBBC05" }), (0, jsx_runtime_1.jsx)("path", { d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", fill: "#EA4335" })] })), googleLoading ? 'Redirecting to Google…' : 'Continue with Google'] }) })), altAuthSlot && (0, jsx_runtime_1.jsx)("div", { className: "mt-4", children: altAuthSlot }), (0, jsx_runtime_1.jsx)("div", { className: `mt-6 text-center text-sm ${colors.muted}`, children: (0, jsx_runtime_1.jsx)("a", { href: "/account-auth/recovery", className: "themed-link hover:underline", children: "Start account recovery" }) })] }) }) })] }));
|
|
210
184
|
}
|
|
211
185
|
function LoginPageFallback() {
|
|
212
186
|
const colors = (0, useTheme_1.useColors)();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-2302 cut 1: ask the IdP to email a code when the authenticator lane failed on our side.
|
|
3
|
+
* The handler lives in api-handlers/auth/credential-login-email-code.ts (moved from VibeID-Customer-Portal src/app/api/auth/admin-login/email-code/route.ts). A consuming app's route file
|
|
4
|
+
* re-exports POST and writes `export const runtime = 'nodejs'` literally.
|
|
5
|
+
*/
|
|
6
|
+
export { POST } from '../../api-handlers/auth/credential-login-email-code';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* PAY-2302 cut 1: ask the IdP to email a code when the authenticator lane failed on our side.
|
|
6
|
+
* The handler lives in api-handlers/auth/credential-login-email-code.ts (moved from VibeID-Customer-Portal src/app/api/auth/admin-login/email-code/route.ts). A consuming app's route file
|
|
7
|
+
* re-exports POST and writes `export const runtime = 'nodejs'` literally.
|
|
8
|
+
*/
|
|
9
|
+
var credential_login_email_code_1 = require("../../api-handlers/auth/credential-login-email-code");
|
|
10
|
+
Object.defineProperty(exports, "POST", { enumerable: true, get: function () { return credential_login_email_code_1.POST; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-2302 cut 1: password sign-in route (IdP ExternalAuth/login, session written via lib/session-store, 2FA-outstanding handling).
|
|
3
|
+
* The handler lives in api-handlers/auth/credential-login.ts (moved from VibeID-Customer-Portal src/app/api/auth/admin-login/route.ts). A consuming app's route file
|
|
4
|
+
* re-exports POST and writes `export const runtime = 'nodejs'` literally.
|
|
5
|
+
*/
|
|
6
|
+
export { POST } from '../../api-handlers/auth/credential-login';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* PAY-2302 cut 1: password sign-in route (IdP ExternalAuth/login, session written via lib/session-store, 2FA-outstanding handling).
|
|
6
|
+
* The handler lives in api-handlers/auth/credential-login.ts (moved from VibeID-Customer-Portal src/app/api/auth/admin-login/route.ts). A consuming app's route file
|
|
7
|
+
* re-exports POST and writes `export const runtime = 'nodejs'` literally.
|
|
8
|
+
*/
|
|
9
|
+
var credential_login_1 = require("../../api-handlers/auth/credential-login");
|
|
10
|
+
Object.defineProperty(exports, "POST", { enumerable: true, get: function () { return credential_login_1.POST; } });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-2218 PR 2: the session-reset route. Mount it at app/api/auth/session-reset/route.ts:
|
|
3
|
+
*
|
|
4
|
+
* export { GET } from '@payez/next-mvp/routes/auth/session-reset';
|
|
5
|
+
*
|
|
6
|
+
* and turn it on with createMvpMiddleware({ sessionResetRoute: true }). The middleware then sends a page's
|
|
7
|
+
* session-ending redirect (stale session, force-invalidated, pre-2FA timeout, refresh 401/403) here instead of
|
|
8
|
+
* clearing the cookies itself. See src/auth/session-reset-link.ts for the measurements behind each rule.
|
|
9
|
+
*
|
|
10
|
+
* - Router PREFETCH (the route sees `next-router-prefetch`, which Next strips before middleware): 204 + no-store,
|
|
11
|
+
* no redirect, no Set-Cookie. A redirect here would be cached by the 15.5.x router and reused by the click, so
|
|
12
|
+
* the real navigation would never come back to clear.
|
|
13
|
+
* - Real navigation with a valid signed hand-off: expire the session cookies and 307 to the login page.
|
|
14
|
+
* - Anything unsigned, tampered, expired, or bound to a DIFFERENT session than this request's cookie: 307 to the
|
|
15
|
+
* login page and touch NO cookie (nobody can link a signed-in user here to log them out; a second use is a no-op).
|
|
16
|
+
*
|
|
17
|
+
* The login URL is always rebuilt from allowlisted parts; the route never redirects to a URL it was given.
|
|
18
|
+
*/
|
|
19
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
20
|
+
export declare const dynamic = "force-dynamic";
|
|
21
|
+
export declare function GET(request: NextRequest): Promise<NextResponse>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dynamic = void 0;
|
|
4
|
+
exports.GET = GET;
|
|
5
|
+
/**
|
|
6
|
+
* PAY-2218 PR 2: the session-reset route. Mount it at app/api/auth/session-reset/route.ts:
|
|
7
|
+
*
|
|
8
|
+
* export { GET } from '@payez/next-mvp/routes/auth/session-reset';
|
|
9
|
+
*
|
|
10
|
+
* and turn it on with createMvpMiddleware({ sessionResetRoute: true }). The middleware then sends a page's
|
|
11
|
+
* session-ending redirect (stale session, force-invalidated, pre-2FA timeout, refresh 401/403) here instead of
|
|
12
|
+
* clearing the cookies itself. See src/auth/session-reset-link.ts for the measurements behind each rule.
|
|
13
|
+
*
|
|
14
|
+
* - Router PREFETCH (the route sees `next-router-prefetch`, which Next strips before middleware): 204 + no-store,
|
|
15
|
+
* no redirect, no Set-Cookie. A redirect here would be cached by the 15.5.x router and reused by the click, so
|
|
16
|
+
* the real navigation would never come back to clear.
|
|
17
|
+
* - Real navigation with a valid signed hand-off: expire the session cookies and 307 to the login page.
|
|
18
|
+
* - Anything unsigned, tampered, expired, or bound to a DIFFERENT session than this request's cookie: 307 to the
|
|
19
|
+
* login page and touch NO cookie (nobody can link a signed-in user here to log them out; a second use is a no-op).
|
|
20
|
+
*
|
|
21
|
+
* The login URL is always rebuilt from allowlisted parts; the route never redirects to a URL it was given.
|
|
22
|
+
*/
|
|
23
|
+
const server_1 = require("next/server");
|
|
24
|
+
const create_middleware_1 = require("../../middleware/create-middleware");
|
|
25
|
+
const session_cookie_clear_1 = require("../../auth/session-cookie-clear");
|
|
26
|
+
const app_slug_1 = require("../../lib/app-slug");
|
|
27
|
+
const session_reset_link_1 = require("../../auth/session-reset-link");
|
|
28
|
+
exports.dynamic = 'force-dynamic';
|
|
29
|
+
async function GET(request) {
|
|
30
|
+
if ((0, create_middleware_1.isPrefetchRequest)(request)) {
|
|
31
|
+
return new server_1.NextResponse(null, { status: 204, headers: { 'cache-control': 'no-store' } });
|
|
32
|
+
}
|
|
33
|
+
const bound = (0, session_reset_link_1.boundSessionCookie)((n) => request.cookies.get(n)?.value, (0, app_slug_1.getSessionCookieName)(), (0, app_slug_1.getSecureSessionCookieName)());
|
|
34
|
+
const { params, signed } = await (0, session_reset_link_1.readResetRequest)(request.nextUrl.searchParams, (0, session_reset_link_1.resetSecret)(), bound);
|
|
35
|
+
const response = server_1.NextResponse.redirect(new URL((0, session_reset_link_1.loginLocation)(params), request.url), 307);
|
|
36
|
+
response.headers.set('cache-control', 'no-store');
|
|
37
|
+
if (signed)
|
|
38
|
+
(0, session_cookie_clear_1.expireSessionCookies)(response, Array.from(request.cookies, ([name]) => name));
|
|
39
|
+
return response;
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-2302 cut 2: post-2FA OAuth authorize resume route. The handler lives in api-handlers/oauth/resume.ts
|
|
3
|
+
* (moved from VibeID-Customer-Portal src/app/api/oauth/resume/route.ts). A consuming app's route file re-exports GET
|
|
4
|
+
* and writes `export const runtime = 'nodejs'` literally.
|
|
5
|
+
*
|
|
6
|
+
* 4.7.3: `createOAuthResumeHandler({ refusalLocation })` and `DEFAULT_REFUSAL_LOCATION` are re-exported HERE because a consumer can only
|
|
7
|
+
* import what `package.json` `exports` lists, and this route module is the entry. An app whose sign-in page is not
|
|
8
|
+
* /account-auth/login (the portal uses /sign-in) writes `export const GET = createOAuthResumeHandler({ refusalLocation: '/sign-in?error=AUTHORIZE_RESUME_REFUSED' });`.
|
|
9
|
+
*/
|
|
10
|
+
export { GET, createOAuthResumeHandler, DEFAULT_REFUSAL_LOCATION } from '../../api-handlers/oauth/resume';
|
|
11
|
+
export type { OAuthResumeOptions, OAuthResumeDeps } from '../../api-handlers/oauth/resume';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_REFUSAL_LOCATION = exports.createOAuthResumeHandler = exports.GET = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* PAY-2302 cut 2: post-2FA OAuth authorize resume route. The handler lives in api-handlers/oauth/resume.ts
|
|
6
|
+
* (moved from VibeID-Customer-Portal src/app/api/oauth/resume/route.ts). A consuming app's route file re-exports GET
|
|
7
|
+
* and writes `export const runtime = 'nodejs'` literally.
|
|
8
|
+
*
|
|
9
|
+
* 4.7.3: `createOAuthResumeHandler({ refusalLocation })` and `DEFAULT_REFUSAL_LOCATION` are re-exported HERE because a consumer can only
|
|
10
|
+
* import what `package.json` `exports` lists, and this route module is the entry. An app whose sign-in page is not
|
|
11
|
+
* /account-auth/login (the portal uses /sign-in) writes `export const GET = createOAuthResumeHandler({ refusalLocation: '/sign-in?error=AUTHORIZE_RESUME_REFUSED' });`.
|
|
12
|
+
*/
|
|
13
|
+
var resume_1 = require("../../api-handlers/oauth/resume");
|
|
14
|
+
Object.defineProperty(exports, "GET", { enumerable: true, get: function () { return resume_1.GET; } });
|
|
15
|
+
Object.defineProperty(exports, "createOAuthResumeHandler", { enumerable: true, get: function () { return resume_1.createOAuthResumeHandler; } });
|
|
16
|
+
Object.defineProperty(exports, "DEFAULT_REFUSAL_LOCATION", { enumerable: true, get: function () { return resume_1.DEFAULT_REFUSAL_LOCATION; } });
|