@shipfox/client-auth 2.0.0 → 4.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +40 -0
- package/dist/components/auth-shell.d.ts +6 -4
- package/dist/components/auth-shell.d.ts.map +1 -1
- package/dist/components/auth-shell.js +6 -1
- package/dist/components/auth-shell.js.map +1 -1
- package/dist/components/email-code-verification.d.ts +20 -0
- package/dist/components/email-code-verification.d.ts.map +1 -0
- package/dist/components/email-code-verification.js +136 -0
- package/dist/components/email-code-verification.js.map +1 -0
- package/dist/components/redirect-context.d.ts +11 -0
- package/dist/components/redirect-context.d.ts.map +1 -0
- package/dist/components/redirect-context.js +22 -0
- package/dist/components/redirect-context.js.map +1 -0
- package/dist/components/redirect-target.d.ts.map +1 -1
- package/dist/components/redirect-target.js +13 -12
- package/dist/components/redirect-target.js.map +1 -1
- package/dist/continuation.d.ts +4 -0
- package/dist/continuation.d.ts.map +1 -0
- package/dist/continuation.js +5 -0
- package/dist/continuation.js.map +1 -0
- package/dist/hooks/api/login-auth.d.ts.map +1 -1
- package/dist/hooks/api/login-auth.js +4 -28
- package/dist/hooks/api/login-auth.js.map +1 -1
- package/dist/hooks/api/logout-auth.d.ts.map +1 -1
- package/dist/hooks/api/logout-auth.js +4 -14
- package/dist/hooks/api/logout-auth.js.map +1 -1
- package/dist/hooks/api/password-reset-auth.d.ts.map +1 -1
- package/dist/hooks/api/password-reset-auth.js +4 -23
- package/dist/hooks/api/password-reset-auth.js.map +1 -1
- package/dist/hooks/api/verify-email-auth.d.ts.map +1 -1
- package/dist/hooks/api/verify-email-auth.js +4 -23
- package/dist/hooks/api/verify-email-auth.js.map +1 -1
- package/dist/pages/form-utils.js +1 -1
- package/dist/pages/form-utils.js.map +1 -1
- package/dist/pages/invitation-context.d.ts +0 -5
- package/dist/pages/invitation-context.d.ts.map +1 -1
- package/dist/pages/invitation-context.js +3 -19
- package/dist/pages/invitation-context.js.map +1 -1
- package/dist/pages/signup-page.d.ts.map +1 -1
- package/dist/pages/signup-page.js +50 -144
- package/dist/pages/signup-page.js.map +1 -1
- package/dist/state/auth.d.ts +1 -1
- package/dist/state/auth.d.ts.map +1 -1
- package/dist/state/auth.js +1 -1
- package/dist/state/auth.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +10 -40
- package/src/components/auth-provider.test.tsx +173 -3
- package/src/components/auth-shell.tsx +20 -5
- package/src/components/email-code-verification.test.tsx +159 -0
- package/src/components/email-code-verification.tsx +177 -0
- package/src/components/redirect-context.test.ts +51 -0
- package/src/components/redirect-context.ts +26 -0
- package/src/components/redirect-target.test.ts +8 -0
- package/src/components/redirect-target.ts +13 -12
- package/src/continuation.test.ts +15 -0
- package/src/continuation.ts +6 -0
- package/src/hooks/api/login-auth.ts +4 -28
- package/src/hooks/api/logout-auth.ts +4 -10
- package/src/hooks/api/password-reset-auth.ts +4 -23
- package/src/hooks/api/verify-email-auth.ts +4 -23
- package/src/pages/form-errors.test.ts +1 -1
- package/src/pages/form-utils.test.ts +1 -4
- package/src/pages/form-utils.ts +1 -1
- package/src/pages/invitation-context.ts +2 -20
- package/src/pages/signup-page.tsx +33 -129
- package/src/state/auth.ts +1 -0
- package/src/state/last-workspace.test.ts +16 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/pages/email-verification-resend-model.d.ts +0 -6
- package/dist/pages/email-verification-resend-model.d.ts.map +0 -1
- package/dist/pages/email-verification-resend-model.js +0 -11
- package/dist/pages/email-verification-resend-model.js.map +0 -1
- package/src/pages/email-verification-resend-model.test.ts +0 -42
- package/src/pages/email-verification-resend-model.ts +0 -15
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled:
|
|
2
|
+
Successfully compiled: 38 files with swc (507ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @shipfox/client-auth
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 20e4feb: Adds provider-neutral authentication continuation primitives for reusable shells, email-code verification, and redirect context.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 11b10f7: Prevents private React Query data from persisting across logout and authenticated user changes.
|
|
12
|
+
- Updated dependencies [2e5b718]
|
|
13
|
+
- Updated dependencies [769d919]
|
|
14
|
+
- Updated dependencies [6b4a575]
|
|
15
|
+
- Updated dependencies [11b10f7]
|
|
16
|
+
- Updated dependencies [781a45b]
|
|
17
|
+
- @shipfox/client-ui@4.0.0
|
|
18
|
+
- @shipfox/client-shell@4.0.0
|
|
19
|
+
- @shipfox/api-auth-dto@7.1.0
|
|
20
|
+
- @shipfox/client-api@4.0.0
|
|
21
|
+
|
|
22
|
+
## 3.0.1
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- ffc7fc9: Republishes the affected release set after recovering package publication.
|
|
27
|
+
- Updated dependencies [ffc7fc9]
|
|
28
|
+
- @shipfox/client-shell@3.0.1
|
|
29
|
+
- @shipfox/client-ui@3.0.1
|
|
30
|
+
- @shipfox/react-ui@0.3.5
|
|
31
|
+
|
|
32
|
+
## 3.0.0
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- Updated dependencies [cb58afe]
|
|
37
|
+
- Updated dependencies [d735fe3]
|
|
38
|
+
- Updated dependencies [5b06cd5]
|
|
39
|
+
- @shipfox/react-ui@0.3.4
|
|
40
|
+
- @shipfox/client-shell@3.0.0
|
|
41
|
+
- @shipfox/client-ui@3.0.0
|
|
42
|
+
|
|
3
43
|
## 2.0.0
|
|
4
44
|
|
|
5
45
|
### Major Changes
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { type HeaderProps } from '@shipfox/react-ui/typography';
|
|
2
|
+
import type { PropsWithChildren, ReactNode, Ref } from 'react';
|
|
3
|
+
export interface AuthShellProps {
|
|
3
4
|
title: string;
|
|
4
5
|
description: string;
|
|
5
6
|
children: ReactNode;
|
|
6
7
|
className?: string;
|
|
8
|
+
headingProps?: Omit<HeaderProps, 'children' | 'id'> | undefined;
|
|
9
|
+
headingRef?: Ref<HTMLHeadingElement> | undefined;
|
|
7
10
|
}
|
|
8
|
-
export declare function AuthShell({ title, description, children, className }: AuthShellProps): import("react").JSX.Element;
|
|
11
|
+
export declare function AuthShell({ title, description, children, className, headingProps, headingRef, }: AuthShellProps): import("react").JSX.Element;
|
|
9
12
|
export declare function AuthActions({ children }: PropsWithChildren): import("react").JSX.Element;
|
|
10
|
-
export {};
|
|
11
13
|
//# sourceMappingURL=auth-shell.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-shell.d.ts","sourceRoot":"","sources":["../../src/components/auth-shell.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth-shell.d.ts","sourceRoot":"","sources":["../../src/components/auth-shell.tsx"],"names":[],"mappings":"AACA,OAAO,EAAS,KAAK,WAAW,EAAO,MAAM,8BAA8B,CAAC;AAC5E,OAAO,KAAK,EAAC,iBAAiB,EAAE,SAAS,EAAE,GAAG,EAAC,MAAM,OAAO,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC;IAChE,UAAU,CAAC,EAAE,GAAG,CAAC,kBAAkB,CAAC,GAAG,SAAS,CAAC;CAClD;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,WAAW,EACX,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,UAAU,GACX,EAAE,cAAc,+BA2ChB;AAED,wBAAgB,WAAW,CAAC,EAAC,QAAQ,EAAC,EAAE,iBAAiB,+BAExD"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Icon } from '@shipfox/react-ui/icon';
|
|
3
3
|
import { Header, Text } from '@shipfox/react-ui/typography';
|
|
4
|
-
export function AuthShell({ title, description, children, className }) {
|
|
4
|
+
export function AuthShell({ title, description, children, className, headingProps, headingRef }) {
|
|
5
5
|
return /*#__PURE__*/ _jsxs("main", {
|
|
6
6
|
className: "relative flex min-h-screen items-center justify-center overflow-hidden bg-background-subtle-base px-24 py-32 max-[520px]:items-start max-[520px]:px-20 max-[520px]:pb-32 max-[520px]:pt-56",
|
|
7
7
|
children: [
|
|
@@ -36,6 +36,11 @@ export function AuthShell({ title, description, children, className }) {
|
|
|
36
36
|
/*#__PURE__*/ _jsx(Header, {
|
|
37
37
|
id: "auth-title",
|
|
38
38
|
variant: "h1",
|
|
39
|
+
tabIndex: -1,
|
|
40
|
+
...headingProps,
|
|
41
|
+
...headingRef ? {
|
|
42
|
+
ref: headingRef
|
|
43
|
+
} : {},
|
|
39
44
|
children: title
|
|
40
45
|
}),
|
|
41
46
|
/*#__PURE__*/ _jsx(Text, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/auth-shell.tsx"],"sourcesContent":["import {Icon} from '@shipfox/react-ui/icon';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport type {PropsWithChildren, ReactNode} from 'react';\n\
|
|
1
|
+
{"version":3,"sources":["../../src/components/auth-shell.tsx"],"sourcesContent":["import {Icon} from '@shipfox/react-ui/icon';\nimport {Header, type HeaderProps, Text} from '@shipfox/react-ui/typography';\nimport type {PropsWithChildren, ReactNode, Ref} from 'react';\n\nexport interface AuthShellProps {\n title: string;\n description: string;\n children: ReactNode;\n className?: string;\n headingProps?: Omit<HeaderProps, 'children' | 'id'> | undefined;\n headingRef?: Ref<HTMLHeadingElement> | undefined;\n}\n\nexport function AuthShell({\n title,\n description,\n children,\n className,\n headingProps,\n headingRef,\n}: AuthShellProps) {\n return (\n <main className=\"relative flex min-h-screen items-center justify-center overflow-hidden bg-background-subtle-base px-24 py-32 max-[520px]:items-start max-[520px]:px-20 max-[520px]:pb-32 max-[520px]:pt-56\">\n <div\n className=\"pointer-events-none absolute left-1/2 top-0 h-[380px] w-full max-w-[880px] -translate-x-1/2 -translate-y-[120px] opacity-55 max-[520px]:-translate-y-[170px] max-[520px]:opacity-35\"\n aria-hidden=\"true\"\n style={{\n maskImage:\n 'radial-gradient(ellipse 85% 100% at 50% 0%, #000 0%, #000 24%, transparent 78%)',\n WebkitMaskImage:\n 'radial-gradient(ellipse 85% 100% at 50% 0%, #000 0%, #000 24%, transparent 78%)',\n }}\n >\n <div className=\"h-full w-full bg-[radial-gradient(circle,rgba(230,62,0,0.48)_1.6px,transparent_1.8px)] bg-[length:44px_44px]\" />\n </div>\n\n <section\n className={className ?? 'relative flex w-full max-w-[384px] flex-col items-stretch gap-28'}\n aria-labelledby=\"auth-title\"\n >\n <div className=\"flex flex-col items-center gap-16\">\n <div className=\"flex size-64 items-center justify-center rounded-12 border border-border-neutral-base bg-background-neutral-base p-10 shadow-button-neutral\">\n <Icon name=\"shipfox\" className=\"size-42 text-background-highlight-interactive\" />\n </div>\n <div className=\"flex min-w-[128px] flex-col items-center gap-4 text-center\">\n <Header\n id=\"auth-title\"\n variant=\"h1\"\n tabIndex={-1}\n {...headingProps}\n {...(headingRef ? ({ref: headingRef} as HeaderProps) : {})}\n >\n {title}\n </Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-subtle\">\n {description}\n </Text>\n </div>\n </div>\n {children}\n </section>\n </main>\n );\n}\n\nexport function AuthActions({children}: PropsWithChildren) {\n return <div className=\"flex flex-col gap-20\">{children}</div>;\n}\n"],"names":["Icon","Header","Text","AuthShell","title","description","children","className","headingProps","headingRef","main","div","aria-hidden","style","maskImage","WebkitMaskImage","section","aria-labelledby","name","id","variant","tabIndex","ref","size","AuthActions"],"mappings":";AAAA,SAAQA,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,MAAM,EAAoBC,IAAI,QAAO,+BAA+B;AAY5E,OAAO,SAASC,UAAU,EACxBC,KAAK,EACLC,WAAW,EACXC,QAAQ,EACRC,SAAS,EACTC,YAAY,EACZC,UAAU,EACK;IACf,qBACE,MAACC;QAAKH,WAAU;;0BACd,KAACI;gBACCJ,WAAU;gBACVK,eAAY;gBACZC,OAAO;oBACLC,WACE;oBACFC,iBACE;gBACJ;0BAEA,cAAA,KAACJ;oBAAIJ,WAAU;;;0BAGjB,MAACS;gBACCT,WAAWA,aAAa;gBACxBU,mBAAgB;;kCAEhB,MAACN;wBAAIJ,WAAU;;0CACb,KAACI;gCAAIJ,WAAU;0CACb,cAAA,KAACP;oCAAKkB,MAAK;oCAAUX,WAAU;;;0CAEjC,MAACI;gCAAIJ,WAAU;;kDACb,KAACN;wCACCkB,IAAG;wCACHC,SAAQ;wCACRC,UAAU,CAAC;wCACV,GAAGb,YAAY;wCACf,GAAIC,aAAc;4CAACa,KAAKb;wCAAU,IAAoB,CAAC,CAAC;kDAExDL;;kDAEH,KAACF;wCAAKqB,MAAK;wCAAKhB,WAAU;kDACvBF;;;;;;oBAINC;;;;;AAIT;AAEA,OAAO,SAASkB,YAAY,EAAClB,QAAQ,EAAoB;IACvD,qBAAO,KAACK;QAAIJ,WAAU;kBAAwBD;;AAChD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HeaderProps } from '@shipfox/react-ui/typography';
|
|
2
|
+
import type { ReactNode, Ref } from 'react';
|
|
3
|
+
export interface EmailCodeVerificationProps {
|
|
4
|
+
destination: string;
|
|
5
|
+
expiresAt?: string | undefined;
|
|
6
|
+
nextResendAvailableAt?: string | undefined;
|
|
7
|
+
isResending?: boolean;
|
|
8
|
+
isVerifying?: boolean;
|
|
9
|
+
error?: string | undefined;
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
headingProps?: Omit<HeaderProps, 'children' | 'id'> | undefined;
|
|
14
|
+
headingRef?: Ref<HTMLHeadingElement> | undefined;
|
|
15
|
+
onResend: () => void | Promise<void>;
|
|
16
|
+
onUseAnotherEmail: () => void;
|
|
17
|
+
onVerify: (code: string) => void | Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare function EmailCodeVerification({ destination, expiresAt, nextResendAvailableAt, isResending, isVerifying, error, title, description, children, headingProps, headingRef, onResend, onUseAnotherEmail, onVerify, }: EmailCodeVerificationProps): import("react").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=email-code-verification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-code-verification.d.ts","sourceRoot":"","sources":["../../src/components/email-code-verification.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,8BAA8B,CAAC;AAC9D,OAAO,KAAK,EAAY,SAAS,EAAE,GAAG,EAAC,MAAM,OAAO,CAAC;AAMrD,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,YAAY,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC;IAChE,UAAU,CAAC,EAAE,GAAG,CAAC,kBAAkB,CAAC,GAAG,SAAS,CAAC;IACjD,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClD;AAQD,wBAAgB,qBAAqB,CAAC,EACpC,WAAW,EACX,SAAS,EACT,qBAAqB,EACrB,WAAmB,EACnB,WAAmB,EACnB,KAAK,EACL,KAA0B,EAC1B,WAA2E,EAC3E,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,iBAAiB,EACjB,QAAQ,GACT,EAAE,0BAA0B,+BAgI5B"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from '@shipfox/react-ui/button';
|
|
3
|
+
import { Callout } from '@shipfox/react-ui/callout';
|
|
4
|
+
import { FormField, FormFieldInput } from '@shipfox/react-ui/form-field';
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { AuthShell } from './auth-shell.js';
|
|
7
|
+
const EIGHT_DIGIT_CODE_RE = /^\d{8}$/u;
|
|
8
|
+
function timestamp(value) {
|
|
9
|
+
if (!value) return undefined;
|
|
10
|
+
const parsed = Date.parse(value);
|
|
11
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
12
|
+
}
|
|
13
|
+
export function EmailCodeVerification({ destination, expiresAt, nextResendAvailableAt, isResending = false, isVerifying = false, error, title = 'Check your email', description = `We sent an eight-digit verification code to ${destination}.`, children, headingProps, headingRef, onResend, onUseAnotherEmail, onVerify }) {
|
|
14
|
+
const [code, setCode] = useState('');
|
|
15
|
+
const [now, setNow] = useState(()=>Date.now());
|
|
16
|
+
const [codeError, setCodeError] = useState();
|
|
17
|
+
const localHeadingRef = useRef(null);
|
|
18
|
+
const resendAvailableAt = timestamp(nextResendAvailableAt);
|
|
19
|
+
const expiration = timestamp(expiresAt);
|
|
20
|
+
const resendRemainingSeconds = resendAvailableAt ? Math.max(0, Math.ceil((resendAvailableAt - now) / 1_000)) : 0;
|
|
21
|
+
const isResendCoolingDown = resendRemainingSeconds > 0;
|
|
22
|
+
const isExpired = expiration !== undefined && expiration <= now;
|
|
23
|
+
const isPending = isResending || isVerifying;
|
|
24
|
+
useEffect(()=>{
|
|
25
|
+
const isPending = (tickNow)=>resendAvailableAt !== undefined && resendAvailableAt > tickNow || expiration !== undefined && expiration > tickNow;
|
|
26
|
+
const tickNow = Date.now();
|
|
27
|
+
setNow(tickNow);
|
|
28
|
+
if (!isPending(tickNow)) return;
|
|
29
|
+
const interval = window.setInterval(()=>{
|
|
30
|
+
const nextTick = Date.now();
|
|
31
|
+
setNow(nextTick);
|
|
32
|
+
if (!isPending(nextTick)) {
|
|
33
|
+
window.clearInterval(interval);
|
|
34
|
+
}
|
|
35
|
+
}, 1_000);
|
|
36
|
+
return ()=>window.clearInterval(interval);
|
|
37
|
+
}, [
|
|
38
|
+
expiration,
|
|
39
|
+
resendAvailableAt
|
|
40
|
+
]);
|
|
41
|
+
useEffect(()=>{
|
|
42
|
+
localHeadingRef.current?.focus();
|
|
43
|
+
}, []);
|
|
44
|
+
const setHeadingRef = useCallback((heading)=>{
|
|
45
|
+
localHeadingRef.current = heading;
|
|
46
|
+
if (typeof headingRef === 'function') {
|
|
47
|
+
headingRef(heading);
|
|
48
|
+
} else if (headingRef) {
|
|
49
|
+
headingRef.current = heading;
|
|
50
|
+
}
|
|
51
|
+
}, [
|
|
52
|
+
headingRef
|
|
53
|
+
]);
|
|
54
|
+
function submit(event) {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
if (isExpired || isVerifying) return;
|
|
57
|
+
if (!validateCode()) return;
|
|
58
|
+
void onVerify(code);
|
|
59
|
+
}
|
|
60
|
+
function validateCode() {
|
|
61
|
+
const error = EIGHT_DIGIT_CODE_RE.test(code) ? undefined : 'Enter the eight-digit verification code.';
|
|
62
|
+
setCodeError(error);
|
|
63
|
+
return error === undefined;
|
|
64
|
+
}
|
|
65
|
+
return /*#__PURE__*/ _jsxs(AuthShell, {
|
|
66
|
+
title: title,
|
|
67
|
+
description: description,
|
|
68
|
+
headingProps: headingProps,
|
|
69
|
+
headingRef: setHeadingRef,
|
|
70
|
+
children: [
|
|
71
|
+
error ? /*#__PURE__*/ _jsx(Callout, {
|
|
72
|
+
role: "alert",
|
|
73
|
+
type: "error",
|
|
74
|
+
children: error
|
|
75
|
+
}) : null,
|
|
76
|
+
isExpired ? /*#__PURE__*/ _jsx(Callout, {
|
|
77
|
+
role: "alert",
|
|
78
|
+
type: "error",
|
|
79
|
+
children: "This verification code has expired. Use another email to try again."
|
|
80
|
+
}) : null,
|
|
81
|
+
/*#__PURE__*/ _jsxs("form", {
|
|
82
|
+
className: "flex flex-col gap-8",
|
|
83
|
+
noValidate: true,
|
|
84
|
+
onSubmit: submit,
|
|
85
|
+
children: [
|
|
86
|
+
/*#__PURE__*/ _jsx(FormField, {
|
|
87
|
+
label: "Verification code",
|
|
88
|
+
id: "verification-code",
|
|
89
|
+
error: codeError,
|
|
90
|
+
children: /*#__PURE__*/ _jsx(FormFieldInput, {
|
|
91
|
+
autoComplete: "one-time-code",
|
|
92
|
+
inputMode: "numeric",
|
|
93
|
+
maxLength: 8,
|
|
94
|
+
pattern: "[0-9]{8}",
|
|
95
|
+
value: code,
|
|
96
|
+
onChange: (event)=>{
|
|
97
|
+
setCode(event.target.value.replace(/\D/gu, '').slice(0, 8));
|
|
98
|
+
setCodeError(undefined);
|
|
99
|
+
},
|
|
100
|
+
onBlur: validateCode
|
|
101
|
+
})
|
|
102
|
+
}),
|
|
103
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
104
|
+
className: "w-full",
|
|
105
|
+
type: "submit",
|
|
106
|
+
disabled: isExpired,
|
|
107
|
+
isLoading: isVerifying,
|
|
108
|
+
children: "Verify email"
|
|
109
|
+
}),
|
|
110
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
111
|
+
"aria-disabled": isResendCoolingDown ? true : undefined,
|
|
112
|
+
className: "w-full aria-disabled:cursor-not-allowed aria-disabled:opacity-70",
|
|
113
|
+
variant: "secondary",
|
|
114
|
+
type: "button",
|
|
115
|
+
isLoading: isResending,
|
|
116
|
+
onClick: ()=>{
|
|
117
|
+
if (!isResendCoolingDown && !isResending) void onResend();
|
|
118
|
+
},
|
|
119
|
+
children: isResending ? 'Sending email...' : isResendCoolingDown ? `Resend in ${resendRemainingSeconds}s` : 'Resend verification email'
|
|
120
|
+
})
|
|
121
|
+
]
|
|
122
|
+
}),
|
|
123
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
124
|
+
className: "w-full",
|
|
125
|
+
variant: "transparent",
|
|
126
|
+
type: "button",
|
|
127
|
+
disabled: isPending,
|
|
128
|
+
onClick: onUseAnotherEmail,
|
|
129
|
+
children: "Use another email"
|
|
130
|
+
}),
|
|
131
|
+
children
|
|
132
|
+
]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
//# sourceMappingURL=email-code-verification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/email-code-verification.tsx"],"sourcesContent":["import {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FormField, FormFieldInput} from '@shipfox/react-ui/form-field';\nimport type {HeaderProps} from '@shipfox/react-ui/typography';\nimport type {FormEvent, ReactNode, Ref} from 'react';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {AuthShell} from './auth-shell.js';\n\nconst EIGHT_DIGIT_CODE_RE = /^\\d{8}$/u;\n\nexport interface EmailCodeVerificationProps {\n destination: string;\n expiresAt?: string | undefined;\n nextResendAvailableAt?: string | undefined;\n isResending?: boolean;\n isVerifying?: boolean;\n error?: string | undefined;\n title?: string;\n description?: string;\n children?: ReactNode;\n headingProps?: Omit<HeaderProps, 'children' | 'id'> | undefined;\n headingRef?: Ref<HTMLHeadingElement> | undefined;\n onResend: () => void | Promise<void>;\n onUseAnotherEmail: () => void;\n onVerify: (code: string) => void | Promise<void>;\n}\n\nfunction timestamp(value: string | undefined): number | undefined {\n if (!value) return undefined;\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n\nexport function EmailCodeVerification({\n destination,\n expiresAt,\n nextResendAvailableAt,\n isResending = false,\n isVerifying = false,\n error,\n title = 'Check your email',\n description = `We sent an eight-digit verification code to ${destination}.`,\n children,\n headingProps,\n headingRef,\n onResend,\n onUseAnotherEmail,\n onVerify,\n}: EmailCodeVerificationProps) {\n const [code, setCode] = useState('');\n const [now, setNow] = useState(() => Date.now());\n const [codeError, setCodeError] = useState<string>();\n const localHeadingRef = useRef<HTMLHeadingElement>(null);\n const resendAvailableAt = timestamp(nextResendAvailableAt);\n const expiration = timestamp(expiresAt);\n const resendRemainingSeconds = resendAvailableAt\n ? Math.max(0, Math.ceil((resendAvailableAt - now) / 1_000))\n : 0;\n const isResendCoolingDown = resendRemainingSeconds > 0;\n const isExpired = expiration !== undefined && expiration <= now;\n const isPending = isResending || isVerifying;\n\n useEffect(() => {\n const isPending = (tickNow: number) =>\n (resendAvailableAt !== undefined && resendAvailableAt > tickNow) ||\n (expiration !== undefined && expiration > tickNow);\n\n const tickNow = Date.now();\n setNow(tickNow);\n if (!isPending(tickNow)) return;\n\n const interval = window.setInterval(() => {\n const nextTick = Date.now();\n setNow(nextTick);\n if (!isPending(nextTick)) {\n window.clearInterval(interval);\n }\n }, 1_000);\n return () => window.clearInterval(interval);\n }, [expiration, resendAvailableAt]);\n\n useEffect(() => {\n localHeadingRef.current?.focus();\n }, []);\n\n const setHeadingRef = useCallback(\n (heading: HTMLHeadingElement | null) => {\n localHeadingRef.current = heading;\n if (typeof headingRef === 'function') {\n headingRef(heading);\n } else if (headingRef) {\n headingRef.current = heading;\n }\n },\n [headingRef],\n );\n\n function submit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n if (isExpired || isVerifying) return;\n if (!validateCode()) return;\n void onVerify(code);\n }\n\n function validateCode() {\n const error = EIGHT_DIGIT_CODE_RE.test(code)\n ? undefined\n : 'Enter the eight-digit verification code.';\n setCodeError(error);\n return error === undefined;\n }\n\n return (\n <AuthShell\n title={title}\n description={description}\n headingProps={headingProps}\n headingRef={setHeadingRef}\n >\n {error ? (\n <Callout role=\"alert\" type=\"error\">\n {error}\n </Callout>\n ) : null}\n {isExpired ? (\n <Callout role=\"alert\" type=\"error\">\n This verification code has expired. Use another email to try again.\n </Callout>\n ) : null}\n <form className=\"flex flex-col gap-8\" noValidate onSubmit={submit}>\n <FormField label=\"Verification code\" id=\"verification-code\" error={codeError}>\n <FormFieldInput\n autoComplete=\"one-time-code\"\n inputMode=\"numeric\"\n maxLength={8}\n pattern=\"[0-9]{8}\"\n value={code}\n onChange={(event) => {\n setCode(event.target.value.replace(/\\D/gu, '').slice(0, 8));\n setCodeError(undefined);\n }}\n onBlur={validateCode}\n />\n </FormField>\n <Button className=\"w-full\" type=\"submit\" disabled={isExpired} isLoading={isVerifying}>\n Verify email\n </Button>\n <Button\n aria-disabled={isResendCoolingDown ? true : undefined}\n className=\"w-full aria-disabled:cursor-not-allowed aria-disabled:opacity-70\"\n variant=\"secondary\"\n type=\"button\"\n isLoading={isResending}\n onClick={() => {\n if (!isResendCoolingDown && !isResending) void onResend();\n }}\n >\n {isResending\n ? 'Sending email...'\n : isResendCoolingDown\n ? `Resend in ${resendRemainingSeconds}s`\n : 'Resend verification email'}\n </Button>\n </form>\n <Button\n className=\"w-full\"\n variant=\"transparent\"\n type=\"button\"\n disabled={isPending}\n onClick={onUseAnotherEmail}\n >\n Use another email\n </Button>\n {children}\n </AuthShell>\n );\n}\n"],"names":["Button","Callout","FormField","FormFieldInput","useCallback","useEffect","useRef","useState","AuthShell","EIGHT_DIGIT_CODE_RE","timestamp","value","undefined","parsed","Date","parse","Number","isNaN","EmailCodeVerification","destination","expiresAt","nextResendAvailableAt","isResending","isVerifying","error","title","description","children","headingProps","headingRef","onResend","onUseAnotherEmail","onVerify","code","setCode","now","setNow","codeError","setCodeError","localHeadingRef","resendAvailableAt","expiration","resendRemainingSeconds","Math","max","ceil","isResendCoolingDown","isExpired","isPending","tickNow","interval","window","setInterval","nextTick","clearInterval","current","focus","setHeadingRef","heading","submit","event","preventDefault","validateCode","test","role","type","form","className","noValidate","onSubmit","label","id","autoComplete","inputMode","maxLength","pattern","onChange","target","replace","slice","onBlur","disabled","isLoading","aria-disabled","variant","onClick"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RedirectContext {
|
|
2
|
+
invitationToken?: string;
|
|
3
|
+
returnTo?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Separates a safe post-authentication destination from an invitation token.
|
|
7
|
+
* The token never remains in `returnTo`, so callers can keep it in their
|
|
8
|
+
* short-lived invitation flow instead of forwarding it through generic redirects.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseRedirectContext(value: unknown): RedirectContext;
|
|
11
|
+
//# sourceMappingURL=redirect-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-context.d.ts","sourceRoot":"","sources":["../../src/components/redirect-context.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAWpE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { sanitizeRedirectPath } from './redirect-target.js';
|
|
2
|
+
const INVITATION_ACCEPT_PATH = '/invitations/accept';
|
|
3
|
+
/**
|
|
4
|
+
* Separates a safe post-authentication destination from an invitation token.
|
|
5
|
+
* The token never remains in `returnTo`, so callers can keep it in their
|
|
6
|
+
* short-lived invitation flow instead of forwarding it through generic redirects.
|
|
7
|
+
*/ export function parseRedirectContext(value) {
|
|
8
|
+
const redirect = sanitizeRedirectPath(value);
|
|
9
|
+
if (!redirect) return {};
|
|
10
|
+
const decoded = decodeURIComponent(redirect);
|
|
11
|
+
const [path, queryString = ''] = decoded.split('?', 2);
|
|
12
|
+
if (path !== INVITATION_ACCEPT_PATH) return {
|
|
13
|
+
returnTo: redirect
|
|
14
|
+
};
|
|
15
|
+
const params = new URLSearchParams(queryString);
|
|
16
|
+
const invitationToken = params.get('token');
|
|
17
|
+
return invitationToken ? {
|
|
18
|
+
invitationToken
|
|
19
|
+
} : {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=redirect-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/redirect-context.ts"],"sourcesContent":["import {sanitizeRedirectPath} from './redirect-target.js';\n\nconst INVITATION_ACCEPT_PATH = '/invitations/accept';\n\nexport interface RedirectContext {\n invitationToken?: string;\n returnTo?: string;\n}\n\n/**\n * Separates a safe post-authentication destination from an invitation token.\n * The token never remains in `returnTo`, so callers can keep it in their\n * short-lived invitation flow instead of forwarding it through generic redirects.\n */\nexport function parseRedirectContext(value: unknown): RedirectContext {\n const redirect = sanitizeRedirectPath(value);\n if (!redirect) return {};\n\n const decoded = decodeURIComponent(redirect);\n const [path, queryString = ''] = decoded.split('?', 2);\n if (path !== INVITATION_ACCEPT_PATH) return {returnTo: redirect};\n\n const params = new URLSearchParams(queryString);\n const invitationToken = params.get('token');\n return invitationToken ? {invitationToken} : {};\n}\n"],"names":["sanitizeRedirectPath","INVITATION_ACCEPT_PATH","parseRedirectContext","value","redirect","decoded","decodeURIComponent","path","queryString","split","returnTo","params","URLSearchParams","invitationToken","get"],"mappings":"AAAA,SAAQA,oBAAoB,QAAO,uBAAuB;AAE1D,MAAMC,yBAAyB;AAO/B;;;;CAIC,GACD,OAAO,SAASC,qBAAqBC,KAAc;IACjD,MAAMC,WAAWJ,qBAAqBG;IACtC,IAAI,CAACC,UAAU,OAAO,CAAC;IAEvB,MAAMC,UAAUC,mBAAmBF;IACnC,MAAM,CAACG,MAAMC,cAAc,EAAE,CAAC,GAAGH,QAAQI,KAAK,CAAC,KAAK;IACpD,IAAIF,SAASN,wBAAwB,OAAO;QAACS,UAAUN;IAAQ;IAE/D,MAAMO,SAAS,IAAIC,gBAAgBJ;IACnC,MAAMK,kBAAkBF,OAAOG,GAAG,CAAC;IACnC,OAAOD,kBAAkB;QAACA;IAAe,IAAI,CAAC;AAChD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect-target.d.ts","sourceRoot":"","sources":["../../src/components/redirect-target.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"redirect-target.d.ts","sourceRoot":"","sources":["../../src/components/redirect-target.ts"],"names":[],"mappings":"AAIA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAiBvE"}
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
const
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// variants like `/%61uth/login` cannot bypass the `/auth/*` rejection.
|
|
1
|
+
const REDIRECT_ORIGIN = 'https://shipfox-redirect.invalid';
|
|
2
|
+
// Resolves and canonicalizes an internal path before returning it, so browser URL
|
|
3
|
+
// parsing cannot turn a seemingly safe path into an external or auth route.
|
|
5
4
|
export function sanitizeRedirectPath(value) {
|
|
6
|
-
if (typeof value !== 'string') return undefined;
|
|
5
|
+
if (typeof value !== 'string' || !value.startsWith('/')) return undefined;
|
|
7
6
|
let decoded;
|
|
8
7
|
try {
|
|
9
8
|
decoded = decodeURIComponent(value);
|
|
10
9
|
} catch {
|
|
11
10
|
return undefined;
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return
|
|
12
|
+
let target;
|
|
13
|
+
try {
|
|
14
|
+
target = new URL(decoded, REDIRECT_ORIGIN);
|
|
15
|
+
} catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (target.origin !== REDIRECT_ORIGIN) return undefined;
|
|
19
|
+
if (target.pathname === '/auth' || target.pathname.startsWith('/auth/')) return undefined;
|
|
20
|
+
return `${target.pathname}${target.search}${target.hash}`;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
//# sourceMappingURL=redirect-target.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/redirect-target.ts"],"sourcesContent":["const
|
|
1
|
+
{"version":3,"sources":["../../src/components/redirect-target.ts"],"sourcesContent":["const REDIRECT_ORIGIN = 'https://shipfox-redirect.invalid';\n\n// Resolves and canonicalizes an internal path before returning it, so browser URL\n// parsing cannot turn a seemingly safe path into an external or auth route.\nexport function sanitizeRedirectPath(value: unknown): string | undefined {\n if (typeof value !== 'string' || !value.startsWith('/')) return undefined;\n let decoded: string;\n try {\n decoded = decodeURIComponent(value);\n } catch {\n return undefined;\n }\n let target: URL;\n try {\n target = new URL(decoded, REDIRECT_ORIGIN);\n } catch {\n return undefined;\n }\n if (target.origin !== REDIRECT_ORIGIN) return undefined;\n if (target.pathname === '/auth' || target.pathname.startsWith('/auth/')) return undefined;\n return `${target.pathname}${target.search}${target.hash}`;\n}\n"],"names":["REDIRECT_ORIGIN","sanitizeRedirectPath","value","startsWith","undefined","decoded","decodeURIComponent","target","URL","origin","pathname","search","hash"],"mappings":"AAAA,MAAMA,kBAAkB;AAExB,kFAAkF;AAClF,4EAA4E;AAC5E,OAAO,SAASC,qBAAqBC,KAAc;IACjD,IAAI,OAAOA,UAAU,YAAY,CAACA,MAAMC,UAAU,CAAC,MAAM,OAAOC;IAChE,IAAIC;IACJ,IAAI;QACFA,UAAUC,mBAAmBJ;IAC/B,EAAE,OAAM;QACN,OAAOE;IACT;IACA,IAAIG;IACJ,IAAI;QACFA,SAAS,IAAIC,IAAIH,SAASL;IAC5B,EAAE,OAAM;QACN,OAAOI;IACT;IACA,IAAIG,OAAOE,MAAM,KAAKT,iBAAiB,OAAOI;IAC9C,IAAIG,OAAOG,QAAQ,KAAK,WAAWH,OAAOG,QAAQ,CAACP,UAAU,CAAC,WAAW,OAAOC;IAChF,OAAO,GAAGG,OAAOG,QAAQ,GAAGH,OAAOI,MAAM,GAAGJ,OAAOK,IAAI,EAAE;AAC3D"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { AuthActions, AuthShell, type AuthShellProps } from './components/auth-shell.js';
|
|
2
|
+
export { EmailCodeVerification, type EmailCodeVerificationProps, } from './components/email-code-verification.js';
|
|
3
|
+
export { parseRedirectContext, type RedirectContext } from './components/redirect-context.js';
|
|
4
|
+
//# sourceMappingURL=continuation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"continuation.d.ts","sourceRoot":"","sources":["../src/continuation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,4BAA4B,CAAC;AACvF,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAC,oBAAoB,EAAE,KAAK,eAAe,EAAC,MAAM,kCAAkC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/continuation.ts"],"sourcesContent":["export {AuthActions, AuthShell, type AuthShellProps} from './components/auth-shell.js';\nexport {\n EmailCodeVerification,\n type EmailCodeVerificationProps,\n} from './components/email-code-verification.js';\nexport {parseRedirectContext, type RedirectContext} from './components/redirect-context.js';\n"],"names":["AuthActions","AuthShell","EmailCodeVerification","parseRedirectContext"],"mappings":"AAAA,SAAQA,WAAW,EAAEC,SAAS,QAA4B,6BAA6B;AACvF,SACEC,qBAAqB,QAEhB,0CAA0C;AACjD,SAAQC,oBAAoB,QAA6B,mCAAmC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/login-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,YAAY,EAAmB,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"login-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/login-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,YAAY,EAAmB,MAAM,uBAAuB,CAAC;AAK1E,wBAAsB,SAAS,CAAC,IAAI,EAAE,YAAY;;;;;;;;;;;GAEjD;AAED,wBAAgB,YAAY;;;;;;;;;;;;;;YAO3B"}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { apiRequest } from '@shipfox/client-api';
|
|
2
|
-
import { useMutation
|
|
3
|
-
import {
|
|
4
|
-
import { authStateAtom, toAuthenticatedState } from '#state/auth.js';
|
|
5
|
-
import { authRefreshQueryKey } from './refresh-auth.js';
|
|
6
|
-
import { listUserWorkspaces, userWorkspacesQueryKey } from './workspace-auth.js';
|
|
2
|
+
import { useMutation } from '@tanstack/react-query';
|
|
3
|
+
import { useAuthTransition } from '#state/auth.js';
|
|
7
4
|
export async function loginAuth(body) {
|
|
8
5
|
return await apiRequest('/auth/login', {
|
|
9
6
|
method: 'POST',
|
|
@@ -11,31 +8,10 @@ export async function loginAuth(body) {
|
|
|
11
8
|
});
|
|
12
9
|
}
|
|
13
10
|
export function useLoginAuth() {
|
|
14
|
-
const
|
|
15
|
-
const setState = useSetAtom(authStateAtom);
|
|
11
|
+
const { enterAuthenticated } = useAuthTransition();
|
|
16
12
|
return useMutation({
|
|
17
13
|
mutationFn: loginAuth,
|
|
18
|
-
onSuccess:
|
|
19
|
-
queryClient.setQueryData(authRefreshQueryKey, result);
|
|
20
|
-
// Resolve workspaces before flipping auth state to authenticated. A
|
|
21
|
-
// single atomic setState avoids the intermediate window where the user
|
|
22
|
-
// appears authenticated with zero workspaces, which sent users with
|
|
23
|
-
// workspaces straight to `/setup/workspaces/new` after form login.
|
|
24
|
-
let memberships = [];
|
|
25
|
-
try {
|
|
26
|
-
const workspaces = await queryClient.fetchQuery({
|
|
27
|
-
queryKey: userWorkspacesQueryKey,
|
|
28
|
-
queryFn: ()=>listUserWorkspaces(result.token),
|
|
29
|
-
retry: false,
|
|
30
|
-
staleTime: 0
|
|
31
|
-
});
|
|
32
|
-
memberships = workspaces.memberships;
|
|
33
|
-
queryClient.setQueryData(userWorkspacesQueryKey, workspaces);
|
|
34
|
-
} catch {
|
|
35
|
-
// The user is authenticated even if workspace hydration fails.
|
|
36
|
-
}
|
|
37
|
-
setState(toAuthenticatedState(result, memberships));
|
|
38
|
-
}
|
|
14
|
+
onSuccess: enterAuthenticated
|
|
39
15
|
});
|
|
40
16
|
}
|
|
41
17
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/api/login-auth.ts"],"sourcesContent":["import type {LoginBodyDto, LoginResponseDto} from '@shipfox/api-auth-dto';\nimport {apiRequest} from '@shipfox/client-api';\nimport {useMutation
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/api/login-auth.ts"],"sourcesContent":["import type {LoginBodyDto, LoginResponseDto} from '@shipfox/api-auth-dto';\nimport {apiRequest} from '@shipfox/client-api';\nimport {useMutation} from '@tanstack/react-query';\nimport {useAuthTransition} from '#state/auth.js';\n\nexport async function loginAuth(body: LoginBodyDto) {\n return await apiRequest<LoginResponseDto>('/auth/login', {method: 'POST', body});\n}\n\nexport function useLoginAuth() {\n const {enterAuthenticated} = useAuthTransition();\n\n return useMutation({\n mutationFn: loginAuth,\n onSuccess: enterAuthenticated,\n });\n}\n"],"names":["apiRequest","useMutation","useAuthTransition","loginAuth","body","method","useLoginAuth","enterAuthenticated","mutationFn","onSuccess"],"mappings":"AACA,SAAQA,UAAU,QAAO,sBAAsB;AAC/C,SAAQC,WAAW,QAAO,wBAAwB;AAClD,SAAQC,iBAAiB,QAAO,iBAAiB;AAEjD,OAAO,eAAeC,UAAUC,IAAkB;IAChD,OAAO,MAAMJ,WAA6B,eAAe;QAACK,QAAQ;QAAQD;IAAI;AAChF;AAEA,OAAO,SAASE;IACd,MAAM,EAACC,kBAAkB,EAAC,GAAGL;IAE7B,OAAOD,YAAY;QACjBO,YAAYL;QACZM,WAAWF;IACb;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logout-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/logout-auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"logout-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/logout-auth.ts"],"names":[],"mappings":"AAYA,wBAAgB,aAAa,kFAO5B"}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { apiRequest } from '@shipfox/client-api';
|
|
2
|
-
import { useMutation
|
|
3
|
-
import {
|
|
4
|
-
import { authStateAtom } from '#state/auth.js';
|
|
5
|
-
import { authRefreshQueryKey } from './refresh-auth.js';
|
|
2
|
+
import { useMutation } from '@tanstack/react-query';
|
|
3
|
+
import { useAuthTransition } from '#state/auth.js';
|
|
6
4
|
async function logoutAuth() {
|
|
7
5
|
try {
|
|
8
6
|
await apiRequest('/auth/logout', {
|
|
@@ -14,18 +12,10 @@ async function logoutAuth() {
|
|
|
14
12
|
}
|
|
15
13
|
}
|
|
16
14
|
export function useLogoutAuth() {
|
|
17
|
-
const
|
|
18
|
-
const setState = useSetAtom(authStateAtom);
|
|
15
|
+
const { enterGuest } = useAuthTransition();
|
|
19
16
|
return useMutation({
|
|
20
17
|
mutationFn: logoutAuth,
|
|
21
|
-
onSettled:
|
|
22
|
-
setState({
|
|
23
|
-
status: 'guest'
|
|
24
|
-
});
|
|
25
|
-
queryClient.removeQueries({
|
|
26
|
-
queryKey: authRefreshQueryKey
|
|
27
|
-
});
|
|
28
|
-
}
|
|
18
|
+
onSettled: enterGuest
|
|
29
19
|
});
|
|
30
20
|
}
|
|
31
21
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/api/logout-auth.ts"],"sourcesContent":["import {apiRequest} from '@shipfox/client-api';\nimport {useMutation
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/api/logout-auth.ts"],"sourcesContent":["import {apiRequest} from '@shipfox/client-api';\nimport {useMutation} from '@tanstack/react-query';\nimport {useAuthTransition} from '#state/auth.js';\n\nasync function logoutAuth() {\n try {\n await apiRequest<void>('/auth/logout', {method: 'POST', body: {}});\n } catch {\n // Logout is best-effort: local session state must clear even if the API is offline.\n }\n}\n\nexport function useLogoutAuth() {\n const {enterGuest} = useAuthTransition();\n\n return useMutation({\n mutationFn: logoutAuth,\n onSettled: enterGuest,\n });\n}\n"],"names":["apiRequest","useMutation","useAuthTransition","logoutAuth","method","body","useLogoutAuth","enterGuest","mutationFn","onSettled"],"mappings":"AAAA,SAAQA,UAAU,QAAO,sBAAsB;AAC/C,SAAQC,WAAW,QAAO,wBAAwB;AAClD,SAAQC,iBAAiB,QAAO,iBAAiB;AAEjD,eAAeC;IACb,IAAI;QACF,MAAMH,WAAiB,gBAAgB;YAACI,QAAQ;YAAQC,MAAM,CAAC;QAAC;IAClE,EAAE,OAAM;IACN,oFAAoF;IACtF;AACF;AAEA,OAAO,SAASC;IACd,MAAM,EAACC,UAAU,EAAC,GAAGL;IAErB,OAAOD,YAAY;QACjBO,YAAYL;QACZM,WAAWF;IACb;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password-reset-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/password-reset-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAE3B,2BAA2B,EAC5B,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"password-reset-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/password-reset-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAE3B,2BAA2B,EAC5B,MAAM,uBAAuB,CAAC;AAK/B,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,2BAA2B,iBAE3E;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,2BAA2B;;;;;;;;;;;GAK3E;AAED,wBAAgB,2BAA2B;;YAE1C;AAED,wBAAgB,2BAA2B;;;;;;;;;;;;;;YAO1C"}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { apiRequest } from '@shipfox/client-api';
|
|
2
|
-
import { useMutation
|
|
3
|
-
import {
|
|
4
|
-
import { authStateAtom, toAuthenticatedState } from '#state/auth.js';
|
|
5
|
-
import { authRefreshQueryKey } from './refresh-auth.js';
|
|
6
|
-
import { listUserWorkspaces, userWorkspacesQueryKey } from './workspace-auth.js';
|
|
2
|
+
import { useMutation } from '@tanstack/react-query';
|
|
3
|
+
import { useAuthTransition } from '#state/auth.js';
|
|
7
4
|
export async function requestPasswordReset(body) {
|
|
8
5
|
await apiRequest('/auth/password-reset', {
|
|
9
6
|
method: 'POST',
|
|
@@ -22,26 +19,10 @@ export function useRequestPasswordResetAuth() {
|
|
|
22
19
|
});
|
|
23
20
|
}
|
|
24
21
|
export function useConfirmPasswordResetAuth() {
|
|
25
|
-
const
|
|
26
|
-
const setState = useSetAtom(authStateAtom);
|
|
22
|
+
const { enterAuthenticated } = useAuthTransition();
|
|
27
23
|
return useMutation({
|
|
28
24
|
mutationFn: confirmPasswordReset,
|
|
29
|
-
onSuccess:
|
|
30
|
-
setState(toAuthenticatedState(result));
|
|
31
|
-
queryClient.setQueryData(authRefreshQueryKey, result);
|
|
32
|
-
try {
|
|
33
|
-
const workspaces = await queryClient.fetchQuery({
|
|
34
|
-
queryKey: userWorkspacesQueryKey,
|
|
35
|
-
queryFn: ()=>listUserWorkspaces(result.token),
|
|
36
|
-
retry: false,
|
|
37
|
-
staleTime: 0
|
|
38
|
-
});
|
|
39
|
-
setState(toAuthenticatedState(result, workspaces.memberships));
|
|
40
|
-
queryClient.setQueryData(userWorkspacesQueryKey, workspaces);
|
|
41
|
-
} catch {
|
|
42
|
-
// The user is authenticated even if workspace hydration fails.
|
|
43
|
-
}
|
|
44
|
-
}
|
|
25
|
+
onSuccess: enterAuthenticated
|
|
45
26
|
});
|
|
46
27
|
}
|
|
47
28
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/api/password-reset-auth.ts"],"sourcesContent":["import type {\n PasswordResetConfirmBodyDto,\n PasswordResetConfirmResponseDto,\n PasswordResetRequestBodyDto,\n} from '@shipfox/api-auth-dto';\nimport {apiRequest} from '@shipfox/client-api';\nimport {useMutation
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/api/password-reset-auth.ts"],"sourcesContent":["import type {\n PasswordResetConfirmBodyDto,\n PasswordResetConfirmResponseDto,\n PasswordResetRequestBodyDto,\n} from '@shipfox/api-auth-dto';\nimport {apiRequest} from '@shipfox/client-api';\nimport {useMutation} from '@tanstack/react-query';\nimport {useAuthTransition} from '#state/auth.js';\n\nexport async function requestPasswordReset(body: PasswordResetRequestBodyDto) {\n await apiRequest<void>('/auth/password-reset', {method: 'POST', body});\n}\n\nexport async function confirmPasswordReset(body: PasswordResetConfirmBodyDto) {\n return await apiRequest<PasswordResetConfirmResponseDto>('/auth/password-reset/confirm', {\n method: 'POST',\n body,\n });\n}\n\nexport function useRequestPasswordResetAuth() {\n return useMutation({mutationFn: requestPasswordReset});\n}\n\nexport function useConfirmPasswordResetAuth() {\n const {enterAuthenticated} = useAuthTransition();\n\n return useMutation({\n mutationFn: confirmPasswordReset,\n onSuccess: enterAuthenticated,\n });\n}\n"],"names":["apiRequest","useMutation","useAuthTransition","requestPasswordReset","body","method","confirmPasswordReset","useRequestPasswordResetAuth","mutationFn","useConfirmPasswordResetAuth","enterAuthenticated","onSuccess"],"mappings":"AAKA,SAAQA,UAAU,QAAO,sBAAsB;AAC/C,SAAQC,WAAW,QAAO,wBAAwB;AAClD,SAAQC,iBAAiB,QAAO,iBAAiB;AAEjD,OAAO,eAAeC,qBAAqBC,IAAiC;IAC1E,MAAMJ,WAAiB,wBAAwB;QAACK,QAAQ;QAAQD;IAAI;AACtE;AAEA,OAAO,eAAeE,qBAAqBF,IAAiC;IAC1E,OAAO,MAAMJ,WAA4C,gCAAgC;QACvFK,QAAQ;QACRD;IACF;AACF;AAEA,OAAO,SAASG;IACd,OAAON,YAAY;QAACO,YAAYL;IAAoB;AACtD;AAEA,OAAO,SAASM;IACd,MAAM,EAACC,kBAAkB,EAAC,GAAGR;IAE7B,OAAOD,YAAY;QACjBO,YAAYF;QACZK,WAAWD;IACb;AACF"}
|