@payez/next-mvp 4.4.8 → 4.5.0
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 +53 -0
- package/dist/api-handlers/account/reset-password.js +3 -1
- package/dist/api-handlers/onboarding/send-email-code.d.ts +11 -0
- package/dist/api-handlers/onboarding/send-email-code.js +11 -0
- package/dist/api-handlers/onboarding/set-password.d.ts +12 -0
- package/dist/api-handlers/onboarding/set-password.js +15 -0
- package/dist/api-handlers/onboarding/verify-code.d.ts +11 -0
- package/dist/api-handlers/onboarding/verify-code.js +14 -0
- package/dist/api-handlers/onboarding/verify-welcome-email.d.ts +10 -0
- package/dist/api-handlers/onboarding/verify-welcome-email.js +11 -0
- package/dist/api-handlers/session/viability.js +32 -0
- package/dist/auth/password-reset-bridge.d.ts +70 -0
- package/dist/auth/password-reset-bridge.js +99 -0
- package/dist/auth/route-config.js +17 -0
- package/dist/components/auth/FederatedAuthSection.js +50 -21
- package/dist/components/auth/PasswordResetBridge.d.ts +17 -0
- package/dist/components/auth/PasswordResetBridge.js +32 -0
- package/dist/hooks/useAvailableProviders.js +39 -17
- package/dist/lib/onboarding-proxy.d.ts +30 -0
- package/dist/lib/onboarding-proxy.js +62 -0
- package/dist/lib/safe-error-response.d.ts +8 -0
- package/dist/lib/safe-error-response.js +27 -0
- package/dist/middleware/create-middleware.d.ts +41 -0
- package/dist/middleware/create-middleware.js +74 -2
- package/dist/pages/login/page.js +1 -1
- package/dist/pages/onboarding/index.d.ts +11 -0
- package/dist/pages/onboarding/index.js +20 -0
- package/dist/pages/onboarding/page.d.ts +1 -0
- package/dist/pages/onboarding/page.js +118 -0
- package/dist/pages/recovery/page.js +20 -7
- package/dist/routes/auth/viability.d.ts +5 -0
- package/dist/routes/auth/viability.js +53 -0
- package/dist/routes/onboarding/index.d.ts +15 -0
- package/dist/routes/onboarding/index.js +22 -0
- package/dist/routes/onboarding/send-email-code.d.ts +2 -0
- package/dist/routes/onboarding/send-email-code.js +6 -0
- package/dist/routes/onboarding/set-password.d.ts +2 -0
- package/dist/routes/onboarding/set-password.js +6 -0
- package/dist/routes/onboarding/verify-code.d.ts +2 -0
- package/dist/routes/onboarding/verify-code.js +6 -0
- package/dist/routes/onboarding/verify-welcome-email.d.ts +2 -0
- package/dist/routes/onboarding/verify-welcome-email.js +6 -0
- package/dist/utils/api.d.ts +6 -0
- package/dist/utils/api.js +10 -2
- package/dist/vibe/sessions.d.ts +88 -1
- package/dist/vibe/sessions.js +260 -19
- package/package.json +58 -2
- package/scripts/check-publish-eol.mjs +135 -0
- package/src/api-handlers/account/reset-password.ts +4 -2
- package/src/api-handlers/onboarding/send-email-code.ts +18 -0
- package/src/api-handlers/onboarding/set-password.ts +21 -0
- package/src/api-handlers/onboarding/verify-code.ts +20 -0
- package/src/api-handlers/onboarding/verify-welcome-email.ts +17 -0
- package/src/api-handlers/session/viability.ts +31 -0
- package/src/auth/password-reset-bridge.ts +98 -0
- package/src/auth/route-config.ts +17 -0
- package/src/components/auth/FederatedAuthSection.tsx +56 -22
- package/src/components/auth/PasswordResetBridge.tsx +30 -0
- package/src/hooks/useAvailableProviders.ts +41 -18
- package/src/lib/onboarding-proxy.ts +57 -0
- package/src/lib/safe-error-response.ts +23 -0
- package/src/middleware/create-middleware.ts +106 -2
- package/src/pages/login/page.tsx +4 -7
- package/src/pages/onboarding/index.ts +11 -0
- package/src/pages/onboarding/page.tsx +212 -0
- package/src/pages/recovery/page.tsx +24 -8
- package/src/routes/auth/viability.ts +52 -0
- package/src/routes/onboarding/index.ts +15 -0
- package/src/routes/onboarding/send-email-code.ts +2 -0
- package/src/routes/onboarding/set-password.ts +2 -0
- package/src/routes/onboarding/verify-code.ts +2 -0
- package/src/routes/onboarding/verify-welcome-email.ts +2 -0
- package/src/utils/api.ts +10 -1
- package/src/vibe/document-unwrap.ts +103 -103
- package/src/vibe/sessions.ts +291 -24
package/README.md
CHANGED
|
@@ -256,6 +256,59 @@ export default function VerifyCodePageWrapper() {
|
|
|
256
256
|
}
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
+
#### What your app must serve (PAY-2121)
|
|
260
|
+
|
|
261
|
+
The IdP emails links that land on paths your app must host. Two of them are
|
|
262
|
+
conventions the kit mounts for you; wire them or the link 404s at your site:
|
|
263
|
+
|
|
264
|
+
**Invitation / onboarding** - `app/account-auth/onboarding/page.tsx`:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// The IdP's welcome email links to
|
|
268
|
+
// {your site}/account-auth/onboarding?client_id=&onboard_token=
|
|
269
|
+
export { default } from '@payez/next-mvp/pages/onboarding';
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
and the API it posts to (token-driven, no session) - one mount file each:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// app/api/onboarding/verify-code/route.ts
|
|
276
|
+
export { POST } from '@payez/next-mvp/routes/onboarding/verify-code';
|
|
277
|
+
// app/api/onboarding/send-email-code/route.ts
|
|
278
|
+
export { POST } from '@payez/next-mvp/routes/onboarding/send-email-code';
|
|
279
|
+
// app/api/onboarding/verify-welcome-email/route.ts
|
|
280
|
+
export { POST } from '@payez/next-mvp/routes/onboarding/verify-welcome-email';
|
|
281
|
+
// app/api/onboarding/set-password/route.ts
|
|
282
|
+
export { POST } from '@payez/next-mvp/routes/onboarding/set-password';
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Post-verify reset fragment on `/`** - mount the bridge on your home page:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// app/page.tsx
|
|
289
|
+
import { PasswordResetBridge } from '@payez/next-mvp/components/auth/PasswordResetBridge';
|
|
290
|
+
|
|
291
|
+
export default function Home() {
|
|
292
|
+
return (<><PasswordResetBridge />{/* your home page */}</>);
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
When the IdP's verification redirect lands on `/#password_reset_required=true&reset_token=…&email=…`
|
|
297
|
+
(a fragment, for a passwordless account), the bridge performs one replace to
|
|
298
|
+
`/account-auth/recovery?token=…&email=…` and clears the hash. With any other
|
|
299
|
+
fragment it renders nothing. `npx @payez/create-next-app` writes all of the above.
|
|
300
|
+
|
|
301
|
+
**Registration** has two doors, and the kit ships neither as a page:
|
|
302
|
+
|
|
303
|
+
- **Owner-initiated invite** (shipped here): an existing owner invites a user; the invite
|
|
304
|
+
email lands on `/account-auth/onboarding` (above), which the kit now serves.
|
|
305
|
+
- **Self-serve sign-up**: the kit exposes the IdP's ProgressiveAuth flow as API endpoints
|
|
306
|
+
(`/api/ProgressiveAuth/step1`..`step5`, then `/api/ProgressiveAuth/verify-email`;
|
|
307
|
+
`API_ENDPOINTS.progressiveAuth` in `config/env.ts`). The kit ships **no** registration
|
|
308
|
+
page, so a developer who wants self-serve sign-up authors a page that drives that flow,
|
|
309
|
+
respecting the tenant's `allow_public_registration` / waitlist state. The login page no
|
|
310
|
+
longer links a register page, because the kit does not ship one.
|
|
311
|
+
|
|
259
312
|
### 6. Protect Routes with Authentication
|
|
260
313
|
|
|
261
314
|
**Server-side protection:**
|
|
@@ -72,7 +72,9 @@ async function POST(req) {
|
|
|
72
72
|
if (!upstream.ok) {
|
|
73
73
|
// PAY-1591: a password-policy refusal also carries the KNOWN rule codes so the page can say which rules failed.
|
|
74
74
|
const rules = (0, safe_error_response_1.projectPasswordPolicyRules)(data);
|
|
75
|
-
|
|
75
|
+
// PAY-2103: a breached-password refusal is a distinct 400; project a BOOLEAN so the anonymous
|
|
76
|
+
// route carries no code text and the page picks its own sentence.
|
|
77
|
+
return server_1.NextResponse.json({ ...(0, safe_error_response_1.idpErrorEnvelope)(data, 'Failed to reset password'), ...(rules !== undefined ? { password_policy: { rules } } : {}), ...((0, safe_error_response_1.projectResetTokenInvalid)(data) ? { reset_token_invalid: true } : {}), ...((0, safe_error_response_1.projectPasswordBreached)(data) ? { password_breached: true } : {}) }, { status: upstream.status });
|
|
76
78
|
}
|
|
77
79
|
return server_1.NextResponse.json(data, { status: upstream.status });
|
|
78
80
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/onboarding/send-email-code (PAY-2121).
|
|
3
|
+
*
|
|
4
|
+
* Body: { onboard_token } - token-driven, no session. Thin proxy to the IdP's
|
|
5
|
+
* POST /api/Onboarding/send-email-code. The IdP derives the recipient from the
|
|
6
|
+
* token's BOUND email only, never from client input, so a tampered link cannot
|
|
7
|
+
* redirect the code. Also the resend leg the verify card's cooldown fires
|
|
8
|
+
* against.
|
|
9
|
+
*/
|
|
10
|
+
import { NextRequest } from 'next/server';
|
|
11
|
+
export declare function POST(req: NextRequest): Promise<import("next/server").NextResponse<unknown>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = POST;
|
|
4
|
+
const onboarding_proxy_1 = require("../../lib/onboarding-proxy");
|
|
5
|
+
async function POST(req) {
|
|
6
|
+
const body = await req.json().catch(() => ({}));
|
|
7
|
+
const onboardToken = body?.onboard_token;
|
|
8
|
+
if (!onboardToken)
|
|
9
|
+
return (0, onboarding_proxy_1.onboardingBadRequest)('MISSING_TOKEN', 'onboard_token is required');
|
|
10
|
+
return (0, onboarding_proxy_1.onboardingProxy)(req, '/api/Onboarding/send-email-code');
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/onboarding/set-password (PAY-2121).
|
|
3
|
+
*
|
|
4
|
+
* Body: { onboard_token, email, reset_token, new_password } - the locked
|
|
5
|
+
* contract (IdP OnboardingDtos.cs SetPasswordRequest, all four snake_case).
|
|
6
|
+
* email + reset_token come from the verify-code response; the IdP re-checks
|
|
7
|
+
* email against the token's bound user before consuming the single-use token.
|
|
8
|
+
* On success the IdP sets the password AND completes onboarding (issues tokens).
|
|
9
|
+
* Thin proxy - status and body pass through verbatim.
|
|
10
|
+
*/
|
|
11
|
+
import { NextRequest } from 'next/server';
|
|
12
|
+
export declare function POST(req: NextRequest): Promise<import("next/server").NextResponse<unknown>>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = POST;
|
|
4
|
+
const onboarding_proxy_1 = require("../../lib/onboarding-proxy");
|
|
5
|
+
async function POST(req) {
|
|
6
|
+
const body = await req.json().catch(() => ({}));
|
|
7
|
+
const b = body;
|
|
8
|
+
if (!b?.onboard_token)
|
|
9
|
+
return (0, onboarding_proxy_1.onboardingBadRequest)('MISSING_TOKEN', 'onboard_token is required');
|
|
10
|
+
if (!b?.email || !b?.reset_token)
|
|
11
|
+
return (0, onboarding_proxy_1.onboardingBadRequest)('MISSING_RESET_CONTEXT', 'email and reset_token (from verify-code) are required');
|
|
12
|
+
if (!b?.new_password)
|
|
13
|
+
return (0, onboarding_proxy_1.onboardingBadRequest)('MISSING_PASSWORD', 'new_password is required');
|
|
14
|
+
return (0, onboarding_proxy_1.onboardingProxy)(req, '/api/Onboarding/set-password');
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/onboarding/verify-code (PAY-2121).
|
|
3
|
+
*
|
|
4
|
+
* Body: { onboard_token, code } - token-driven, no session. Thin proxy to the
|
|
5
|
+
* IdP's POST /api/Onboarding/verify-code, which answers
|
|
6
|
+
* { success, reset_token, email, client_key } on success (OnboardingDtos.cs
|
|
7
|
+
* VerifyCodeResponse). reset_token + email are what the set-password step needs
|
|
8
|
+
* next. The onboard_token IS the auth; there is no session to attach.
|
|
9
|
+
*/
|
|
10
|
+
import { NextRequest } from 'next/server';
|
|
11
|
+
export declare function POST(req: NextRequest): Promise<import("next/server").NextResponse<unknown>>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = POST;
|
|
4
|
+
const onboarding_proxy_1 = require("../../lib/onboarding-proxy");
|
|
5
|
+
async function POST(req) {
|
|
6
|
+
const body = await req.json().catch(() => ({}));
|
|
7
|
+
const onboardToken = body?.onboard_token;
|
|
8
|
+
const code = body?.code;
|
|
9
|
+
if (!onboardToken)
|
|
10
|
+
return (0, onboarding_proxy_1.onboardingBadRequest)('MISSING_TOKEN', 'onboard_token is required');
|
|
11
|
+
if (!code)
|
|
12
|
+
return (0, onboarding_proxy_1.onboardingBadRequest)('MISSING_CODE', 'code is required');
|
|
13
|
+
return (0, onboarding_proxy_1.onboardingProxy)(req, '/api/Onboarding/verify-code');
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/onboarding/verify-welcome-email (PAY-2121).
|
|
3
|
+
*
|
|
4
|
+
* Body: { onboard_token } - token-driven, no session. Thin proxy to the IdP's
|
|
5
|
+
* POST /api/Onboarding/verify-welcome-email, which answers the masked contact
|
|
6
|
+
* info + expiry the verify card displays. The onboarding page calls this to
|
|
7
|
+
* render its "check your email / here is what we know" state.
|
|
8
|
+
*/
|
|
9
|
+
import { NextRequest } from 'next/server';
|
|
10
|
+
export declare function POST(req: NextRequest): Promise<import("next/server").NextResponse<unknown>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = POST;
|
|
4
|
+
const onboarding_proxy_1 = require("../../lib/onboarding-proxy");
|
|
5
|
+
async function POST(req) {
|
|
6
|
+
const body = await req.json().catch(() => ({}));
|
|
7
|
+
const onboardToken = body?.onboard_token;
|
|
8
|
+
if (!onboardToken)
|
|
9
|
+
return (0, onboarding_proxy_1.onboardingBadRequest)('MISSING_TOKEN', 'onboard_token is required');
|
|
10
|
+
return (0, onboarding_proxy_1.onboardingProxy)(req, '/api/Onboarding/verify-welcome-email');
|
|
11
|
+
}
|
|
@@ -12,6 +12,7 @@ const auth_1 = require("../../server/auth");
|
|
|
12
12
|
const startup_init_1 = require("../../lib/startup-init");
|
|
13
13
|
const idp_client_config_1 = require("../../lib/idp-client-config");
|
|
14
14
|
const token_log_id_1 = require("../../lib/token-log-id");
|
|
15
|
+
const sessions_1 = require("../../vibe/sessions");
|
|
15
16
|
async function GET(req) {
|
|
16
17
|
try {
|
|
17
18
|
// Ensure initialization is complete (auth signing secret resolved from IDP)
|
|
@@ -124,12 +125,43 @@ async function GET(req) {
|
|
|
124
125
|
hoursExpiredAgo: ((Date.now() - mfaExpiresAt) / (1000 * 60 * 60)).toFixed(1)
|
|
125
126
|
});
|
|
126
127
|
}
|
|
128
|
+
// --- Session revocation status (PAY-1882 step 4 producer half) ---
|
|
129
|
+
// The four-state status (BAPert 66320). The middleware fails CLOSED by
|
|
130
|
+
// default on 'unknown' (a failed Vibe read) and always on 'revoked';
|
|
131
|
+
// 'active' and 'not_configured' stay viable. A consumer that does not
|
|
132
|
+
// read revocationStatus is unaffected - absent is not revoked.
|
|
133
|
+
let revocationStatus = 'not_configured';
|
|
134
|
+
// BAPert 66480 condition 2 / ruling (ii) 66700: the numeric IdP id comes from
|
|
135
|
+
// the token's own `sub` claim FIRST (session.userId can be an email).
|
|
136
|
+
const revocationUserId = (0, sessions_1.resolveNumericIdpUserId)(sessionData, sessionData.idpAccessToken);
|
|
137
|
+
if (revocationUserId > 0) {
|
|
138
|
+
try {
|
|
139
|
+
revocationStatus = await (0, sessions_1.getRevocationStatus)(revocationUserId, sessionToken);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// getRevocationStatus does not throw by contract; fail closed if it ever does.
|
|
143
|
+
revocationStatus = 'unknown';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// N1 (NightHawk 66392): a session EXISTS but its userId is not a usable
|
|
148
|
+
// numeric IdP id, so the check could not be run. That is 'unknown'
|
|
149
|
+
// (could not determine), NOT 'not_configured' (no revocation source) -
|
|
150
|
+
// the two must not be conflated.
|
|
151
|
+
// BAPert 66480 condition 1: emit ONE WARN (shape only) per session.
|
|
152
|
+
revocationStatus = (0, sessions_1.warnNonNumericUserId)(sessionToken, sessionData.userId);
|
|
153
|
+
}
|
|
127
154
|
const response = {
|
|
128
155
|
authenticated: true,
|
|
129
156
|
sessionTokenId: (0, token_log_id_1.tokenLogId)(sessionToken), // PAY-1481: correlation id for middleware tracking, never the token
|
|
130
157
|
// 2FA fields - critical for middleware redirect logic
|
|
131
158
|
requires2FA, // From cached client config (client-wide setting)
|
|
132
159
|
twoFactorComplete: effectiveTwoFactorComplete, // From session, BUT respects MFA TTL
|
|
160
|
+
// Session revocation (PAY-1882 step 4): the four-state status, plus the
|
|
161
|
+
// boolean the middleware's existing force-invalid branch already reads.
|
|
162
|
+
revocationStatus,
|
|
163
|
+
forceInvalid: revocationStatus === 'revoked' || revocationStatus === 'unknown',
|
|
164
|
+
revoked: revocationStatus === 'revoked',
|
|
133
165
|
// Cards 141688/141741 — the IDP's routing decision, relayed verbatim.
|
|
134
166
|
//
|
|
135
167
|
// THIS IS THE SECOND IMPLEMENTATION OF VIABILITY. The other is
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-2121: the fragment bridge on `/`.
|
|
3
|
+
*
|
|
4
|
+
* The IdP's post-verify redirect for a passwordless account lands on
|
|
5
|
+
* ORIGIN/#password_reset_required=true&reset_token=<token>&email=<addr>
|
|
6
|
+
* - a FRAGMENT, never a query (ProgressiveAuthController.cs:1204-1222). A fresh
|
|
7
|
+
* @payez/next-mvp app has no fragment handling at all, so the user lands on `/`
|
|
8
|
+
* and nothing happens; their reset is lost.
|
|
9
|
+
*
|
|
10
|
+
* This is the PURE half (data in, data out) so a test can execute it without a
|
|
11
|
+
* DOM. The client component (PasswordResetBridge) calls it on mount with
|
|
12
|
+
* `window.location.hash`. The kit's reset surface is the existing
|
|
13
|
+
* `/account-auth/recovery` page, which reads `?email=` and a reset token - so the
|
|
14
|
+
* bridge rewrites the fragment into that surface's OWN query shape and navigates
|
|
15
|
+
* once. It invents no new reset flow.
|
|
16
|
+
*
|
|
17
|
+
* CONTRACT:
|
|
18
|
+
* '#password_reset_required=true&reset_token=T&email=a%40b.co' -> '/account-auth/recovery?token=T&email=a%40b.co'
|
|
19
|
+
* any other fragment (no flag, flag != true, no token) -> null (leave `/` alone)
|
|
20
|
+
*/
|
|
21
|
+
/** The reset surface the bridge hands off to, by its existing path. */
|
|
22
|
+
export declare const RECOVERY_PATH = "/account-auth/recovery";
|
|
23
|
+
/**
|
|
24
|
+
* PAY-2121 (BAPert 70576): the recovery page's ENTRY decision, as a pure function.
|
|
25
|
+
*
|
|
26
|
+
* The bridge now sends a VERIFIED passwordless user to
|
|
27
|
+
* /account-auth/recovery?token=<reset_token>&email=<addr>
|
|
28
|
+
* with a REAL reset token in hand. If the page then showed its request-a-code
|
|
29
|
+
* step, it would throw that token away and the flow would still dead-end - just
|
|
30
|
+
* one page later. So when a token is present the page must start at the
|
|
31
|
+
* set-password step and submit straight through resetPasswordWithToken.
|
|
32
|
+
*
|
|
33
|
+
* token present -> { step: 'set-password', resetToken: <token> }
|
|
34
|
+
* no token -> { step: 'initiate', resetToken: null } (today's behaviour)
|
|
35
|
+
*/
|
|
36
|
+
export declare function recoveryEntry(token: string | null | undefined, email: string | null | undefined): {
|
|
37
|
+
step: 'set-password' | 'initiate';
|
|
38
|
+
resetToken: string | null;
|
|
39
|
+
email: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* PAY-2121 (BAPert 70582): the set-password SUBMIT precondition, as a pure
|
|
43
|
+
* function. The old guard was `if (!passwordResetToken || !recoverySession) return;`.
|
|
44
|
+
* Removing the session half (so a deep-linked reset can submit) must NOT let a
|
|
45
|
+
* submit with NO token reach the API carrying an empty token - which is exactly
|
|
46
|
+
* what a guard-less submit would do. This decides it, in one testable place:
|
|
47
|
+
*
|
|
48
|
+
* resetSubmitTarget(resetToken, sessionEmail, queryEmail):
|
|
49
|
+
* - no token (after trim) -> null (REFUSE: never call the API with an empty token)
|
|
50
|
+
* - token, session email present -> { token, email: sessionEmail }
|
|
51
|
+
* - token, only a query email -> { token, email: queryEmail } (the deep-link case)
|
|
52
|
+
* - token, but NO email anywhere -> null (REFUSE: nothing to reset)
|
|
53
|
+
*
|
|
54
|
+
* Returns the exact { token, email } the caller must pass to
|
|
55
|
+
* accountApi.resetPasswordWithToken, or null to refuse BEFORE any request.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resetSubmitTarget(resetToken: string | null | undefined, sessionEmail: string | null | undefined, queryEmail: string | null | undefined): {
|
|
58
|
+
token: string;
|
|
59
|
+
email: string;
|
|
60
|
+
} | null;
|
|
61
|
+
/**
|
|
62
|
+
* The redirect the fragment asks for, or null when the fragment is not a reset
|
|
63
|
+
* request. `hash` is `window.location.hash` (leading '#' optional).
|
|
64
|
+
*
|
|
65
|
+
* Returns null - leaves `/` untouched - when:
|
|
66
|
+
* - the fragment is empty;
|
|
67
|
+
* - `password_reset_required` is absent or not exactly 'true';
|
|
68
|
+
* - no `reset_token` is present (nothing to reset with; not a real link).
|
|
69
|
+
*/
|
|
70
|
+
export declare function passwordResetRedirect(hash: string | null | undefined): string | null;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PAY-2121: the fragment bridge on `/`.
|
|
4
|
+
*
|
|
5
|
+
* The IdP's post-verify redirect for a passwordless account lands on
|
|
6
|
+
* ORIGIN/#password_reset_required=true&reset_token=<token>&email=<addr>
|
|
7
|
+
* - a FRAGMENT, never a query (ProgressiveAuthController.cs:1204-1222). A fresh
|
|
8
|
+
* @payez/next-mvp app has no fragment handling at all, so the user lands on `/`
|
|
9
|
+
* and nothing happens; their reset is lost.
|
|
10
|
+
*
|
|
11
|
+
* This is the PURE half (data in, data out) so a test can execute it without a
|
|
12
|
+
* DOM. The client component (PasswordResetBridge) calls it on mount with
|
|
13
|
+
* `window.location.hash`. The kit's reset surface is the existing
|
|
14
|
+
* `/account-auth/recovery` page, which reads `?email=` and a reset token - so the
|
|
15
|
+
* bridge rewrites the fragment into that surface's OWN query shape and navigates
|
|
16
|
+
* once. It invents no new reset flow.
|
|
17
|
+
*
|
|
18
|
+
* CONTRACT:
|
|
19
|
+
* '#password_reset_required=true&reset_token=T&email=a%40b.co' -> '/account-auth/recovery?token=T&email=a%40b.co'
|
|
20
|
+
* any other fragment (no flag, flag != true, no token) -> null (leave `/` alone)
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.RECOVERY_PATH = void 0;
|
|
24
|
+
exports.recoveryEntry = recoveryEntry;
|
|
25
|
+
exports.resetSubmitTarget = resetSubmitTarget;
|
|
26
|
+
exports.passwordResetRedirect = passwordResetRedirect;
|
|
27
|
+
/** The reset surface the bridge hands off to, by its existing path. */
|
|
28
|
+
exports.RECOVERY_PATH = '/account-auth/recovery';
|
|
29
|
+
/**
|
|
30
|
+
* PAY-2121 (BAPert 70576): the recovery page's ENTRY decision, as a pure function.
|
|
31
|
+
*
|
|
32
|
+
* The bridge now sends a VERIFIED passwordless user to
|
|
33
|
+
* /account-auth/recovery?token=<reset_token>&email=<addr>
|
|
34
|
+
* with a REAL reset token in hand. If the page then showed its request-a-code
|
|
35
|
+
* step, it would throw that token away and the flow would still dead-end - just
|
|
36
|
+
* one page later. So when a token is present the page must start at the
|
|
37
|
+
* set-password step and submit straight through resetPasswordWithToken.
|
|
38
|
+
*
|
|
39
|
+
* token present -> { step: 'set-password', resetToken: <token> }
|
|
40
|
+
* no token -> { step: 'initiate', resetToken: null } (today's behaviour)
|
|
41
|
+
*/
|
|
42
|
+
function recoveryEntry(token, email) {
|
|
43
|
+
const tok = (token ?? '').trim();
|
|
44
|
+
return {
|
|
45
|
+
step: tok ? 'set-password' : 'initiate',
|
|
46
|
+
resetToken: tok || null,
|
|
47
|
+
email: (email ?? '').trim(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* PAY-2121 (BAPert 70582): the set-password SUBMIT precondition, as a pure
|
|
52
|
+
* function. The old guard was `if (!passwordResetToken || !recoverySession) return;`.
|
|
53
|
+
* Removing the session half (so a deep-linked reset can submit) must NOT let a
|
|
54
|
+
* submit with NO token reach the API carrying an empty token - which is exactly
|
|
55
|
+
* what a guard-less submit would do. This decides it, in one testable place:
|
|
56
|
+
*
|
|
57
|
+
* resetSubmitTarget(resetToken, sessionEmail, queryEmail):
|
|
58
|
+
* - no token (after trim) -> null (REFUSE: never call the API with an empty token)
|
|
59
|
+
* - token, session email present -> { token, email: sessionEmail }
|
|
60
|
+
* - token, only a query email -> { token, email: queryEmail } (the deep-link case)
|
|
61
|
+
* - token, but NO email anywhere -> null (REFUSE: nothing to reset)
|
|
62
|
+
*
|
|
63
|
+
* Returns the exact { token, email } the caller must pass to
|
|
64
|
+
* accountApi.resetPasswordWithToken, or null to refuse BEFORE any request.
|
|
65
|
+
*/
|
|
66
|
+
function resetSubmitTarget(resetToken, sessionEmail, queryEmail) {
|
|
67
|
+
const tok = (resetToken ?? '').trim();
|
|
68
|
+
if (!tok)
|
|
69
|
+
return null;
|
|
70
|
+
const email = ((sessionEmail ?? '').trim() || (queryEmail ?? '').trim());
|
|
71
|
+
if (!email)
|
|
72
|
+
return null;
|
|
73
|
+
return { token: tok, email };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The redirect the fragment asks for, or null when the fragment is not a reset
|
|
77
|
+
* request. `hash` is `window.location.hash` (leading '#' optional).
|
|
78
|
+
*
|
|
79
|
+
* Returns null - leaves `/` untouched - when:
|
|
80
|
+
* - the fragment is empty;
|
|
81
|
+
* - `password_reset_required` is absent or not exactly 'true';
|
|
82
|
+
* - no `reset_token` is present (nothing to reset with; not a real link).
|
|
83
|
+
*/
|
|
84
|
+
function passwordResetRedirect(hash) {
|
|
85
|
+
const raw = (hash ?? '').replace(/^#/, '');
|
|
86
|
+
if (!raw)
|
|
87
|
+
return null;
|
|
88
|
+
const params = new URLSearchParams(raw);
|
|
89
|
+
if (params.get('password_reset_required') !== 'true')
|
|
90
|
+
return null;
|
|
91
|
+
const token = params.get('reset_token') ?? '';
|
|
92
|
+
if (!token)
|
|
93
|
+
return null;
|
|
94
|
+
const email = params.get('email') ?? '';
|
|
95
|
+
const qs = new URLSearchParams({ token });
|
|
96
|
+
if (email)
|
|
97
|
+
qs.set('email', email);
|
|
98
|
+
return `${exports.RECOVERY_PATH}?${qs.toString()}`;
|
|
99
|
+
}
|
|
@@ -46,6 +46,23 @@ const defaultUnauthenticatedRoutes = [
|
|
|
46
46
|
allowDuringCircuitBreakerOpen: true,
|
|
47
47
|
requiresRateLimit: false
|
|
48
48
|
},
|
|
49
|
+
// PAY-2121: the IdP emails an invite link to /account-auth/onboarding and a
|
|
50
|
+
// post-verify reset FRAGMENT on '/'. Both must be reachable unauthenticated -
|
|
51
|
+
// the invitee has no session (the onboard_token IS the credential), and the
|
|
52
|
+
// fragment lands on the already-public root. Listed explicitly, not left to
|
|
53
|
+
// the /account-auth/* wildcard, so the two conventional paths are visible here.
|
|
54
|
+
{
|
|
55
|
+
pattern: '/account-auth/onboarding',
|
|
56
|
+
description: 'IdP invite/onboarding landing page (token-driven, no session)',
|
|
57
|
+
allowDuringCircuitBreakerOpen: true,
|
|
58
|
+
requiresRateLimit: false
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
pattern: '/api/onboarding/*',
|
|
62
|
+
description: 'Onboarding API (token-driven, no session)',
|
|
63
|
+
allowDuringCircuitBreakerOpen: true,
|
|
64
|
+
requiresRateLimit: true
|
|
65
|
+
},
|
|
49
66
|
{
|
|
50
67
|
pattern: '/api/auth/*',
|
|
51
68
|
description: 'Authentication API endpoints',
|
|
@@ -3,31 +3,54 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.FederatedAuthSection = FederatedAuthSection;
|
|
5
5
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
/**
|
|
7
|
+
* PAY-2104 — PER-PROVIDER BRAND TREATMENT, NOT ONE GENERIC BUTTON.
|
|
8
|
+
*
|
|
9
|
+
* Apple and Facebook both publish binding rules for their sign-in buttons and
|
|
10
|
+
* neither is satisfied by the generic "Continue with X" control this section
|
|
11
|
+
* used to render for every provider:
|
|
12
|
+
* - APPLE (App Store Review Guidelines 4.8 / "Sign in with Apple" HIG): the
|
|
13
|
+
* button MUST carry the Apple logo and one of Apple's own wordings. The
|
|
14
|
+
* approved primary wording is "Sign in with Apple" ("Continue with Apple"
|
|
15
|
+
* is the permitted alternative). "Continue with Apple" with no logo, which
|
|
16
|
+
* is what the generic path produced, is non-compliant and is what App Store
|
|
17
|
+
* review rejects. Black or white only.
|
|
18
|
+
* - FACEBOOK (Meta Platform Terms / brand guidelines): "Continue with
|
|
19
|
+
* Facebook", the Facebook "f" mark, and the brand blue (#1877F2) or white
|
|
20
|
+
* are the sanctioned treatments.
|
|
21
|
+
* Google/Microsoft/GitHub keep the neutral control — they impose no such rule,
|
|
22
|
+
* and inventing brand colours for them would be a change nobody asked for.
|
|
23
|
+
*
|
|
24
|
+
* The mark inherits `currentColor` so it is legible on both the white neutral
|
|
25
|
+
* buttons and the black/blue branded ones without a second SVG per provider.
|
|
26
|
+
*/
|
|
27
|
+
const PROVIDER_BRAND = {
|
|
28
|
+
google: { label: 'Continue with Google', background: 'var(--bg-default, #ffffff)', color: 'var(--text-primary, #111827)', border: 'var(--border-default, #e5e7eb)' },
|
|
29
|
+
microsoft: { label: 'Continue with Microsoft', background: 'var(--bg-default, #ffffff)', color: 'var(--text-primary, #111827)', border: 'var(--border-default, #e5e7eb)' },
|
|
30
|
+
github: { label: 'Continue with GitHub', background: 'var(--bg-default, #ffffff)', color: 'var(--text-primary, #111827)', border: 'var(--border-default, #e5e7eb)' },
|
|
31
|
+
// Meta brand blue; white mark on blue, per the brand guidelines.
|
|
32
|
+
facebook: { label: 'Continue with Facebook', background: '#1877F2', color: '#ffffff', border: '#1877F2' },
|
|
33
|
+
// Apple HIG: black (or white) field, "Sign in with Apple", Apple logo.
|
|
34
|
+
apple: { label: 'Sign in with Apple', background: '#000000', color: '#ffffff', border: '#000000' },
|
|
35
|
+
};
|
|
6
36
|
const providerIcons = {
|
|
7
37
|
google: {
|
|
8
|
-
|
|
9
|
-
color: '#4285F4',
|
|
10
|
-
svg: ((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" })] })),
|
|
38
|
+
svg: ((0, jsx_runtime_1.jsxs)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", 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" })] })),
|
|
11
39
|
},
|
|
12
40
|
microsoft: {
|
|
13
|
-
|
|
14
|
-
color: '#0078D4',
|
|
15
|
-
svg: ((0, jsx_runtime_1.jsxs)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", children: [(0, jsx_runtime_1.jsx)("rect", { x: "1", y: "1", width: "9", height: "9", fill: "#F25022" }), (0, jsx_runtime_1.jsx)("rect", { x: "12", y: "1", width: "9", height: "9", fill: "#7FBA00" }), (0, jsx_runtime_1.jsx)("rect", { x: "1", y: "12", width: "9", height: "9", fill: "#00A4EF" }), (0, jsx_runtime_1.jsx)("rect", { x: "12", y: "12", width: "9", height: "9", fill: "#FFB900" })] })),
|
|
41
|
+
svg: ((0, jsx_runtime_1.jsxs)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("rect", { x: "1", y: "1", width: "9", height: "9", fill: "#F25022" }), (0, jsx_runtime_1.jsx)("rect", { x: "12", y: "1", width: "9", height: "9", fill: "#7FBA00" }), (0, jsx_runtime_1.jsx)("rect", { x: "1", y: "12", width: "9", height: "9", fill: "#00A4EF" }), (0, jsx_runtime_1.jsx)("rect", { x: "12", y: "12", width: "9", height: "9", fill: "#FFB900" })] })),
|
|
16
42
|
},
|
|
17
43
|
facebook: {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
// currentColor: white on the Meta-blue button. A hardcoded #1877F2 fill on a
|
|
45
|
+
// #1877F2 field is an invisible mark — the exact way a branded button ships
|
|
46
|
+
// looking like a blank one.
|
|
47
|
+
svg: ((0, jsx_runtime_1.jsx)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" }) })),
|
|
21
48
|
},
|
|
22
49
|
github: {
|
|
23
|
-
|
|
24
|
-
color: '#000000',
|
|
25
|
-
svg: ((0, jsx_runtime_1.jsx)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", children: (0, jsx_runtime_1.jsx)("path", { d: "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" }) })),
|
|
50
|
+
svg: ((0, jsx_runtime_1.jsx)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" }) })),
|
|
26
51
|
},
|
|
27
52
|
apple: {
|
|
28
|
-
|
|
29
|
-
color: '#000000',
|
|
30
|
-
svg: ((0, jsx_runtime_1.jsx)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", children: (0, jsx_runtime_1.jsx)("path", { d: "M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" }) })),
|
|
53
|
+
svg: ((0, jsx_runtime_1.jsx)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" }) })),
|
|
31
54
|
},
|
|
32
55
|
};
|
|
33
56
|
function FederatedAuthSection({ providers, onProviderClick, isLoading }) {
|
|
@@ -35,11 +58,17 @@ function FederatedAuthSection({ providers, onProviderClick, isLoading }) {
|
|
|
35
58
|
return null;
|
|
36
59
|
}
|
|
37
60
|
return ((0, jsx_runtime_1.jsx)("div", { className: "space-y-3", children: providers.map((provider) => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
const icon = providerIcons[provider];
|
|
62
|
+
const brand = PROVIDER_BRAND[provider] ?? {
|
|
63
|
+
label: `Continue with ${provider}`,
|
|
64
|
+
background: 'var(--bg-default, #ffffff)',
|
|
65
|
+
color: 'var(--text-primary, #111827)',
|
|
66
|
+
border: 'var(--border-default, #e5e7eb)',
|
|
67
|
+
};
|
|
68
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => onProviderClick(provider), disabled: isLoading, "data-provider": provider, className: `w-full px-4 py-3 rounded-lg border-2 font-semibold transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed`, style: {
|
|
69
|
+
border: `1px solid ${brand.border}`,
|
|
70
|
+
color: brand.color,
|
|
71
|
+
background: brand.background,
|
|
72
|
+
}, children: [icon.svg, brand.label] }, provider));
|
|
44
73
|
}) }));
|
|
45
74
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAY-2121: mount this once on your app's `/` page. When the IdP's post-verify
|
|
3
|
+
* redirect lands on `/#password_reset_required=true&reset_token=<t>&email=<e>`
|
|
4
|
+
* (a FRAGMENT), this performs ONE replace to the kit's reset surface and clears
|
|
5
|
+
* the hash. With any other fragment it renders nothing and leaves `/` alone.
|
|
6
|
+
*
|
|
7
|
+
* It renders nothing and reads no user data - it only inspects the fragment.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // app/page.tsx
|
|
11
|
+
* import { PasswordResetBridge } from '@payez/next-mvp/components/auth/PasswordResetBridge';
|
|
12
|
+
* export default function Home() {
|
|
13
|
+
* return (<><PasswordResetBridge /></>);
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
export declare function PasswordResetBridge(): null;
|
|
17
|
+
export default PasswordResetBridge;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.PasswordResetBridge = PasswordResetBridge;
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const password_reset_bridge_1 = require("../../auth/password-reset-bridge");
|
|
7
|
+
/**
|
|
8
|
+
* PAY-2121: mount this once on your app's `/` page. When the IdP's post-verify
|
|
9
|
+
* redirect lands on `/#password_reset_required=true&reset_token=<t>&email=<e>`
|
|
10
|
+
* (a FRAGMENT), this performs ONE replace to the kit's reset surface and clears
|
|
11
|
+
* the hash. With any other fragment it renders nothing and leaves `/` alone.
|
|
12
|
+
*
|
|
13
|
+
* It renders nothing and reads no user data - it only inspects the fragment.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // app/page.tsx
|
|
17
|
+
* import { PasswordResetBridge } from '@payez/next-mvp/components/auth/PasswordResetBridge';
|
|
18
|
+
* export default function Home() {
|
|
19
|
+
* return (<><PasswordResetBridge /></>);
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
function PasswordResetBridge() {
|
|
23
|
+
(0, react_1.useEffect)(() => {
|
|
24
|
+
if (typeof window === 'undefined')
|
|
25
|
+
return;
|
|
26
|
+
const target = (0, password_reset_bridge_1.passwordResetRedirect)(window.location.hash);
|
|
27
|
+
if (target)
|
|
28
|
+
window.location.replace(target);
|
|
29
|
+
}, []);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
exports.default = PasswordResetBridge;
|