@shipfox/client-auth 6.0.3 → 8.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 +18 -0
- package/dist/components/password-login-form.d.ts +7 -0
- package/dist/components/password-login-form.d.ts.map +1 -0
- package/dist/components/password-login-form.js +164 -0
- package/dist/components/password-login-form.js.map +1 -0
- package/dist/continuation.d.ts +2 -1
- package/dist/continuation.d.ts.map +1 -1
- package/dist/continuation.js +2 -1
- package/dist/continuation.js.map +1 -1
- package/dist/pages/invitation-context.js +1 -1
- package/dist/pages/invitation-context.js.map +1 -1
- package/dist/pages/login-page.d.ts.map +1 -1
- package/dist/pages/login-page.js +12 -168
- package/dist/pages/login-page.js.map +1 -1
- package/dist/redirect-context.d.ts.map +1 -0
- package/dist/{components/redirect-context.js → redirect-context.js} +1 -1
- package/dist/redirect-context.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -3
- package/src/components/password-login-form.test.tsx +34 -0
- package/src/components/password-login-form.tsx +149 -0
- package/src/continuation.test.ts +2 -0
- package/src/continuation.ts +5 -1
- package/src/pages/invitation-context.ts +1 -1
- package/src/pages/login-page.tsx +4 -141
- package/src/{components/redirect-context.test.ts → redirect-context.test.ts} +13 -2
- package/src/{components/redirect-context.ts → redirect-context.ts} +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/components/redirect-context.d.ts.map +0 -1
- package/dist/components/redirect-context.js.map +0 -1
- /package/dist/{components/redirect-context.d.ts → redirect-context.d.ts} +0 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled:
|
|
2
|
+
Successfully compiled: 42 files with swc (198.18ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @shipfox/client-auth
|
|
2
2
|
|
|
3
|
+
## 8.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 25f5f42: Export the password login form from the continuation entrypoint for downstream route composition.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [289d686]
|
|
12
|
+
- @shipfox/client-shell@8.0.0
|
|
13
|
+
- @shipfox/client-invitations@8.0.0
|
|
14
|
+
|
|
15
|
+
## 7.0.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 61309db: Adds a Node-safe redirect-context entrypoint for shared authentication continuation parsing.
|
|
20
|
+
|
|
3
21
|
## 6.0.3
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface PasswordLoginFormProps {
|
|
3
|
+
children?: ReactNode;
|
|
4
|
+
invitationEmail?: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
export declare function PasswordLoginForm({ children, invitationEmail }?: PasswordLoginFormProps): import("react").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=password-login-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password-login-form.d.ts","sourceRoot":"","sources":["../../src/components/password-login-form.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAC,KAAK,SAAS,EAA8B,MAAM,OAAO,CAAC;AAKlE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,wBAAgB,iBAAiB,CAAC,EAAC,QAAQ,EAAE,eAAe,EAAC,GAAE,sBAA2B,+BAmIzF"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { loginBodySchema } from '@shipfox/api-auth-dto';
|
|
3
|
+
import { Button } from '@shipfox/react-ui/button';
|
|
4
|
+
import { Callout } from '@shipfox/react-ui/callout';
|
|
5
|
+
import { FormField, FormFieldInput, fieldError } from '@shipfox/react-ui/form-field';
|
|
6
|
+
import { Icon } from '@shipfox/react-ui/icon';
|
|
7
|
+
import { useForm } from '@tanstack/react-form';
|
|
8
|
+
import { useAtom } from 'jotai';
|
|
9
|
+
import { useEffect, useRef, useState } from 'react';
|
|
10
|
+
import { useLoginAuth } from '#hooks/api/login-auth.js';
|
|
11
|
+
import { loginErrorToFormError } from '#pages/form-errors.js';
|
|
12
|
+
import { authFormDraftAtom, initialAuthFormDraft } from '#state/auth.js';
|
|
13
|
+
export function PasswordLoginForm({ children, invitationEmail } = {}) {
|
|
14
|
+
const login = useLoginAuth();
|
|
15
|
+
const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);
|
|
16
|
+
const [formError, setFormError] = useState();
|
|
17
|
+
const draftRef = useRef(authFormDraft);
|
|
18
|
+
draftRef.current = authFormDraft;
|
|
19
|
+
// Set just before clearing the draft on success so the unmount cleanup
|
|
20
|
+
// below does not repersist the just-submitted credentials.
|
|
21
|
+
const skipDraftPersistRef = useRef(false);
|
|
22
|
+
const form = useForm({
|
|
23
|
+
defaultValues: {
|
|
24
|
+
email: authFormDraft.email,
|
|
25
|
+
password: authFormDraft.password
|
|
26
|
+
},
|
|
27
|
+
onSubmit: async ({ value })=>{
|
|
28
|
+
setFormError(undefined);
|
|
29
|
+
try {
|
|
30
|
+
await login.mutateAsync(value);
|
|
31
|
+
skipDraftPersistRef.current = true;
|
|
32
|
+
setAuthFormDraft(initialAuthFormDraft);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const mapped = loginErrorToFormError(error);
|
|
35
|
+
if (mapped.kind === 'field') {
|
|
36
|
+
form.setFieldMeta(mapped.field, (prev)=>({
|
|
37
|
+
...prev,
|
|
38
|
+
errorMap: {
|
|
39
|
+
...prev.errorMap,
|
|
40
|
+
onServer: mapped.message
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
} else {
|
|
44
|
+
setFormError(mapped.message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
useEffect(()=>{
|
|
50
|
+
if (invitationEmail && form.state.values.email !== invitationEmail) {
|
|
51
|
+
form.setFieldValue('email', invitationEmail);
|
|
52
|
+
setAuthFormDraft((current)=>({
|
|
53
|
+
...current,
|
|
54
|
+
email: invitationEmail
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
}, [
|
|
58
|
+
form,
|
|
59
|
+
invitationEmail,
|
|
60
|
+
setAuthFormDraft
|
|
61
|
+
]);
|
|
62
|
+
// Sync TanStack Form values back into the Jotai draft on unmount so a
|
|
63
|
+
// navigation to /signup or /reset preserves what the user typed. Skipped
|
|
64
|
+
// after a successful login because we just intentionally cleared the draft.
|
|
65
|
+
useEffect(()=>{
|
|
66
|
+
return ()=>{
|
|
67
|
+
if (skipDraftPersistRef.current) return;
|
|
68
|
+
const { email, password } = form.state.values;
|
|
69
|
+
if (email !== draftRef.current.email || password !== draftRef.current.password) {
|
|
70
|
+
setAuthFormDraft({
|
|
71
|
+
email,
|
|
72
|
+
password
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}, [
|
|
77
|
+
form,
|
|
78
|
+
setAuthFormDraft
|
|
79
|
+
]);
|
|
80
|
+
function persistDraft() {
|
|
81
|
+
const { email, password } = form.state.values;
|
|
82
|
+
setAuthFormDraft({
|
|
83
|
+
email,
|
|
84
|
+
password
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return /*#__PURE__*/ _jsxs("form", {
|
|
88
|
+
className: "flex flex-col gap-18",
|
|
89
|
+
noValidate: true,
|
|
90
|
+
onSubmit: (event)=>{
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
void form.handleSubmit();
|
|
94
|
+
},
|
|
95
|
+
children: [
|
|
96
|
+
formError ? /*#__PURE__*/ _jsx(Callout, {
|
|
97
|
+
role: "alert",
|
|
98
|
+
type: "error",
|
|
99
|
+
children: formError
|
|
100
|
+
}) : null,
|
|
101
|
+
/*#__PURE__*/ _jsx(form.Field, {
|
|
102
|
+
name: "email",
|
|
103
|
+
validators: {
|
|
104
|
+
onBlur: loginBodySchema.shape.email,
|
|
105
|
+
onSubmit: loginBodySchema.shape.email
|
|
106
|
+
},
|
|
107
|
+
children: (field)=>/*#__PURE__*/ _jsx(FormField, {
|
|
108
|
+
label: "Email",
|
|
109
|
+
id: "email",
|
|
110
|
+
error: fieldError(field),
|
|
111
|
+
children: /*#__PURE__*/ _jsx(FormFieldInput, {
|
|
112
|
+
autoComplete: "email",
|
|
113
|
+
name: "email",
|
|
114
|
+
type: "email",
|
|
115
|
+
value: field.state.value,
|
|
116
|
+
onChange: (event)=>field.handleChange(event.target.value),
|
|
117
|
+
onBlur: ()=>{
|
|
118
|
+
field.handleBlur();
|
|
119
|
+
persistDraft();
|
|
120
|
+
},
|
|
121
|
+
readOnly: Boolean(invitationEmail),
|
|
122
|
+
iconRight: invitationEmail ? /*#__PURE__*/ _jsx(Icon, {
|
|
123
|
+
"aria-hidden": "true",
|
|
124
|
+
className: "size-16 text-foreground-neutral-disabled",
|
|
125
|
+
name: "lockLine"
|
|
126
|
+
}) : undefined
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
}),
|
|
130
|
+
/*#__PURE__*/ _jsx(form.Field, {
|
|
131
|
+
name: "password",
|
|
132
|
+
validators: {
|
|
133
|
+
onBlur: loginBodySchema.shape.password,
|
|
134
|
+
onSubmit: loginBodySchema.shape.password
|
|
135
|
+
},
|
|
136
|
+
children: (field)=>/*#__PURE__*/ _jsx(FormField, {
|
|
137
|
+
label: "Password",
|
|
138
|
+
id: "password",
|
|
139
|
+
error: fieldError(field),
|
|
140
|
+
children: /*#__PURE__*/ _jsx(FormFieldInput, {
|
|
141
|
+
autoComplete: "current-password",
|
|
142
|
+
name: "password",
|
|
143
|
+
type: "password",
|
|
144
|
+
value: field.state.value,
|
|
145
|
+
onChange: (event)=>field.handleChange(event.target.value),
|
|
146
|
+
onBlur: ()=>{
|
|
147
|
+
field.handleBlur();
|
|
148
|
+
persistDraft();
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
}),
|
|
153
|
+
children,
|
|
154
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
155
|
+
className: "w-full",
|
|
156
|
+
isLoading: login.isPending,
|
|
157
|
+
type: "submit",
|
|
158
|
+
children: login.isPending ? 'Logging in...' : 'Log in'
|
|
159
|
+
})
|
|
160
|
+
]
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=password-login-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/password-login-form.tsx"],"sourcesContent":["import {loginBodySchema} from '@shipfox/api-auth-dto';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {useForm} from '@tanstack/react-form';\nimport {useAtom} from 'jotai';\nimport {type ReactNode, useEffect, useRef, useState} from 'react';\nimport {useLoginAuth} from '#hooks/api/login-auth.js';\nimport {loginErrorToFormError} from '#pages/form-errors.js';\nimport {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';\n\nexport interface PasswordLoginFormProps {\n children?: ReactNode;\n invitationEmail?: string | undefined;\n}\n\nexport function PasswordLoginForm({children, invitationEmail}: PasswordLoginFormProps = {}) {\n const login = useLoginAuth();\n const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);\n const [formError, setFormError] = useState<string | undefined>();\n const draftRef = useRef(authFormDraft);\n draftRef.current = authFormDraft;\n // Set just before clearing the draft on success so the unmount cleanup\n // below does not repersist the just-submitted credentials.\n const skipDraftPersistRef = useRef(false);\n\n const form = useForm({\n defaultValues: {email: authFormDraft.email, password: authFormDraft.password},\n onSubmit: async ({value}) => {\n setFormError(undefined);\n try {\n await login.mutateAsync(value);\n skipDraftPersistRef.current = true;\n setAuthFormDraft(initialAuthFormDraft);\n } catch (error) {\n const mapped = loginErrorToFormError(error);\n if (mapped.kind === 'field') {\n form.setFieldMeta(mapped.field, (prev) => ({\n ...prev,\n errorMap: {...prev.errorMap, onServer: mapped.message},\n }));\n } else {\n setFormError(mapped.message);\n }\n }\n },\n });\n\n useEffect(() => {\n if (invitationEmail && form.state.values.email !== invitationEmail) {\n form.setFieldValue('email', invitationEmail);\n setAuthFormDraft((current) => ({...current, email: invitationEmail}));\n }\n }, [form, invitationEmail, setAuthFormDraft]);\n\n // Sync TanStack Form values back into the Jotai draft on unmount so a\n // navigation to /signup or /reset preserves what the user typed. Skipped\n // after a successful login because we just intentionally cleared the draft.\n useEffect(() => {\n return () => {\n if (skipDraftPersistRef.current) return;\n const {email, password} = form.state.values;\n if (email !== draftRef.current.email || password !== draftRef.current.password) {\n setAuthFormDraft({email, password});\n }\n };\n }, [form, setAuthFormDraft]);\n\n function persistDraft() {\n const {email, password} = form.state.values;\n setAuthFormDraft({email, password});\n }\n\n return (\n <form\n className=\"flex flex-col gap-18\"\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void form.handleSubmit();\n }}\n >\n {formError ? (\n <Callout role=\"alert\" type=\"error\">\n {formError}\n </Callout>\n ) : null}\n <form.Field\n name=\"email\"\n validators={{onBlur: loginBodySchema.shape.email, onSubmit: loginBodySchema.shape.email}}\n >\n {(field) => (\n <FormField label=\"Email\" id=\"email\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"email\"\n name=\"email\"\n type=\"email\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n readOnly={Boolean(invitationEmail)}\n iconRight={\n invitationEmail ? (\n <Icon\n aria-hidden=\"true\"\n className=\"size-16 text-foreground-neutral-disabled\"\n name=\"lockLine\"\n />\n ) : undefined\n }\n />\n </FormField>\n )}\n </form.Field>\n <form.Field\n name=\"password\"\n validators={{\n onBlur: loginBodySchema.shape.password,\n onSubmit: loginBodySchema.shape.password,\n }}\n >\n {(field) => (\n <FormField label=\"Password\" id=\"password\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"current-password\"\n name=\"password\"\n type=\"password\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n />\n </FormField>\n )}\n </form.Field>\n {children}\n <Button className=\"w-full\" isLoading={login.isPending} type=\"submit\">\n {login.isPending ? 'Logging in...' : 'Log in'}\n </Button>\n </form>\n );\n}\n"],"names":["loginBodySchema","Button","Callout","FormField","FormFieldInput","fieldError","Icon","useForm","useAtom","useEffect","useRef","useState","useLoginAuth","loginErrorToFormError","authFormDraftAtom","initialAuthFormDraft","PasswordLoginForm","children","invitationEmail","login","authFormDraft","setAuthFormDraft","formError","setFormError","draftRef","current","skipDraftPersistRef","form","defaultValues","email","password","onSubmit","value","undefined","mutateAsync","error","mapped","kind","setFieldMeta","field","prev","errorMap","onServer","message","state","values","setFieldValue","persistDraft","className","noValidate","event","preventDefault","stopPropagation","handleSubmit","role","type","Field","name","validators","onBlur","shape","label","id","autoComplete","onChange","handleChange","target","handleBlur","readOnly","Boolean","iconRight","aria-hidden","isLoading","isPending"],"mappings":"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"}
|
package/dist/continuation.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { AuthActions, AuthShell, type AuthShellProps } from '@shipfox/client-shell/runtime';
|
|
2
2
|
export { EmailCodeVerification, type EmailCodeVerificationProps, } from './components/email-code-verification.js';
|
|
3
|
-
export {
|
|
3
|
+
export { PasswordLoginForm, type PasswordLoginFormProps, } from './components/password-login-form.js';
|
|
4
|
+
export { parseRedirectContext, type RedirectContext } from './redirect-context.js';
|
|
4
5
|
//# sourceMappingURL=continuation.d.ts.map
|
|
@@ -1 +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,+BAA+B,CAAC;AAC1F,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAC,oBAAoB,EAAE,KAAK,eAAe,EAAC,MAAM,
|
|
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,+BAA+B,CAAC;AAC1F,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,iBAAiB,EACjB,KAAK,sBAAsB,GAC5B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAC,oBAAoB,EAAE,KAAK,eAAe,EAAC,MAAM,uBAAuB,CAAC"}
|
package/dist/continuation.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { AuthActions, AuthShell } from '@shipfox/client-shell/runtime';
|
|
2
2
|
export { EmailCodeVerification } from './components/email-code-verification.js';
|
|
3
|
-
export {
|
|
3
|
+
export { PasswordLoginForm } from './components/password-login-form.js';
|
|
4
|
+
export { parseRedirectContext } from './redirect-context.js';
|
|
4
5
|
|
|
5
6
|
//# sourceMappingURL=continuation.js.map
|
package/dist/continuation.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/continuation.ts"],"sourcesContent":["export {AuthActions, AuthShell, type AuthShellProps} from '@shipfox/client-shell/runtime';\nexport {\n EmailCodeVerification,\n type EmailCodeVerificationProps,\n} from './components/email-code-verification.js';\nexport {parseRedirectContext, type RedirectContext} from './
|
|
1
|
+
{"version":3,"sources":["../src/continuation.ts"],"sourcesContent":["export {AuthActions, AuthShell, type AuthShellProps} from '@shipfox/client-shell/runtime';\nexport {\n EmailCodeVerification,\n type EmailCodeVerificationProps,\n} from './components/email-code-verification.js';\nexport {\n PasswordLoginForm,\n type PasswordLoginFormProps,\n} from './components/password-login-form.js';\nexport {parseRedirectContext, type RedirectContext} from './redirect-context.js';\n"],"names":["AuthActions","AuthShell","EmailCodeVerification","PasswordLoginForm","parseRedirectContext"],"mappings":"AAAA,SAAQA,WAAW,EAAEC,SAAS,QAA4B,gCAAgC;AAC1F,SACEC,qBAAqB,QAEhB,0CAA0C;AACjD,SACEC,iBAAiB,QAEZ,sCAAsC;AAC7C,SAAQC,oBAAoB,QAA6B,wBAAwB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pendingInvitation, usePreviewInvitation } from '@shipfox/client-invitations';
|
|
2
|
-
import { parseRedirectContext } from '#/
|
|
2
|
+
import { parseRedirectContext } from '#/redirect-context.js';
|
|
3
3
|
export function extractInvitationToken(redirect) {
|
|
4
4
|
return parseRedirectContext(redirect).invitationToken;
|
|
5
5
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/invitation-context.ts"],"sourcesContent":["import {\n type InvitationPreview,\n pendingInvitation,\n usePreviewInvitation,\n} from '@shipfox/client-invitations';\nimport {parseRedirectContext} from '#/
|
|
1
|
+
{"version":3,"sources":["../../src/pages/invitation-context.ts"],"sourcesContent":["import {\n type InvitationPreview,\n pendingInvitation,\n usePreviewInvitation,\n} from '@shipfox/client-invitations';\nimport {parseRedirectContext} from '#/redirect-context.js';\n\nexport function extractInvitationToken(redirect: unknown): string | undefined {\n return parseRedirectContext(redirect).invitationToken;\n}\n\nexport function useInvitationContext(token: string | undefined) {\n return usePreviewInvitation(token);\n}\n\nexport {type InvitationPreview, pendingInvitation};\n"],"names":["pendingInvitation","usePreviewInvitation","parseRedirectContext","extractInvitationToken","redirect","invitationToken","useInvitationContext","token"],"mappings":"AAAA,SAEEA,iBAAiB,EACjBC,oBAAoB,QACf,8BAA8B;AACrC,SAAQC,oBAAoB,QAAO,wBAAwB;AAE3D,OAAO,SAASC,uBAAuBC,QAAiB;IACtD,OAAOF,qBAAqBE,UAAUC,eAAe;AACvD;AAEA,OAAO,SAASC,qBAAqBC,KAAyB;IAC5D,OAAON,qBAAqBM;AAC9B;AAEA,SAAgCP,iBAAiB,GAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-page.d.ts","sourceRoot":"","sources":["../../src/pages/login-page.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"login-page.d.ts","sourceRoot":"","sources":["../../src/pages/login-page.tsx"],"names":[],"mappings":"AAYA,wBAAgB,SAAS,gCAmCxB"}
|
package/dist/pages/login-page.js
CHANGED
|
@@ -1,190 +1,34 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { loginBodySchema } from '@shipfox/api-auth-dto';
|
|
3
2
|
import { AuthShell, useRouteSearch } from '@shipfox/client-shell/runtime';
|
|
4
|
-
import {
|
|
5
|
-
import { Callout } from '@shipfox/react-ui/callout';
|
|
6
|
-
import { FormField, FormFieldInput, fieldError } from '@shipfox/react-ui/form-field';
|
|
7
|
-
import { Icon } from '@shipfox/react-ui/icon';
|
|
3
|
+
import { ButtonLink } from '@shipfox/react-ui/button';
|
|
8
4
|
import { Text } from '@shipfox/react-ui/typography';
|
|
9
|
-
import { useForm } from '@tanstack/react-form';
|
|
10
5
|
import { Link } from '@tanstack/react-router';
|
|
11
|
-
import {
|
|
12
|
-
import { useEffect, useRef, useState } from 'react';
|
|
13
|
-
import { useLoginAuth } from '#hooks/api/login-auth.js';
|
|
14
|
-
import { authFormDraftAtom, initialAuthFormDraft } from '#state/auth.js';
|
|
6
|
+
import { PasswordLoginForm } from '#components/password-login-form.js';
|
|
15
7
|
import { validateRedirectSearch } from '../routes/inputs.js';
|
|
16
|
-
import { loginErrorToFormError } from './form-errors.js';
|
|
17
8
|
import { extractInvitationToken, pendingInvitation, useInvitationContext } from './invitation-context.js';
|
|
18
9
|
export function LoginPage() {
|
|
19
|
-
const login = useLoginAuth();
|
|
20
10
|
const search = useRouteSearch(validateRedirectSearch);
|
|
21
11
|
const invitationToken = extractInvitationToken(search.redirect);
|
|
22
12
|
const invitationPreview = useInvitationContext(invitationToken);
|
|
23
13
|
const invitationPending = pendingInvitation(invitationPreview.data);
|
|
24
|
-
const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);
|
|
25
|
-
const [formError, setFormError] = useState();
|
|
26
|
-
const draftRef = useRef(authFormDraft);
|
|
27
|
-
draftRef.current = authFormDraft;
|
|
28
|
-
// Set just before clearing the draft on success so the unmount cleanup
|
|
29
|
-
// below does not repersist the just-submitted credentials.
|
|
30
|
-
const skipDraftPersistRef = useRef(false);
|
|
31
|
-
const form = useForm({
|
|
32
|
-
defaultValues: {
|
|
33
|
-
email: authFormDraft.email,
|
|
34
|
-
password: authFormDraft.password
|
|
35
|
-
},
|
|
36
|
-
onSubmit: async ({ value })=>{
|
|
37
|
-
setFormError(undefined);
|
|
38
|
-
try {
|
|
39
|
-
await login.mutateAsync(value);
|
|
40
|
-
skipDraftPersistRef.current = true;
|
|
41
|
-
setAuthFormDraft(initialAuthFormDraft);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
const mapped = loginErrorToFormError(error);
|
|
44
|
-
if (mapped.kind === 'field') {
|
|
45
|
-
form.setFieldMeta(mapped.field, (prev)=>({
|
|
46
|
-
...prev,
|
|
47
|
-
errorMap: {
|
|
48
|
-
...prev.errorMap,
|
|
49
|
-
onServer: mapped.message
|
|
50
|
-
}
|
|
51
|
-
}));
|
|
52
|
-
} else {
|
|
53
|
-
setFormError(mapped.message);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
// Lock the email field when arriving from an invitation link so the user
|
|
59
|
-
// can't log in as a different account than the one the invitation targets.
|
|
60
|
-
useEffect(()=>{
|
|
61
|
-
if (invitationPending && form.state.values.email !== invitationPending.email) {
|
|
62
|
-
form.setFieldValue('email', invitationPending.email);
|
|
63
|
-
setAuthFormDraft((current)=>({
|
|
64
|
-
...current,
|
|
65
|
-
email: invitationPending.email
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
68
|
-
}, [
|
|
69
|
-
invitationPending,
|
|
70
|
-
form,
|
|
71
|
-
setAuthFormDraft
|
|
72
|
-
]);
|
|
73
|
-
// Sync TanStack Form values back into the Jotai draft on unmount so a
|
|
74
|
-
// navigation to /signup or /reset preserves what the user typed. Skipped
|
|
75
|
-
// after a successful login because we just intentionally cleared the draft.
|
|
76
|
-
useEffect(()=>{
|
|
77
|
-
return ()=>{
|
|
78
|
-
if (skipDraftPersistRef.current) return;
|
|
79
|
-
const { email, password } = form.state.values;
|
|
80
|
-
if (email !== draftRef.current.email || password !== draftRef.current.password) {
|
|
81
|
-
setAuthFormDraft({
|
|
82
|
-
email,
|
|
83
|
-
password
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
}, [
|
|
88
|
-
form,
|
|
89
|
-
setAuthFormDraft
|
|
90
|
-
]);
|
|
91
|
-
const isInvitationEmailLocked = Boolean(invitationPending);
|
|
92
14
|
const invitationRedirect = invitationToken ? `/invitations/accept?token=${encodeURIComponent(invitationToken)}` : undefined;
|
|
93
15
|
const headerTitle = invitationPending ? `Join ${invitationPending.workspaceName}` : 'Connect to Shipfox';
|
|
94
16
|
const headerDescription = invitationPending ? 'Log in to accept your invitation.' : 'Log in to access Shipfox.';
|
|
95
|
-
function persistDraft() {
|
|
96
|
-
const { email, password } = form.state.values;
|
|
97
|
-
setAuthFormDraft({
|
|
98
|
-
email,
|
|
99
|
-
password
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
17
|
return /*#__PURE__*/ _jsxs(AuthShell, {
|
|
103
18
|
title: headerTitle,
|
|
104
19
|
description: headerDescription,
|
|
105
20
|
children: [
|
|
106
|
-
/*#__PURE__*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
formError ? /*#__PURE__*/ _jsx(Callout, {
|
|
116
|
-
role: "alert",
|
|
117
|
-
type: "error",
|
|
118
|
-
children: formError
|
|
119
|
-
}) : null,
|
|
120
|
-
/*#__PURE__*/ _jsx(form.Field, {
|
|
121
|
-
name: "email",
|
|
122
|
-
validators: {
|
|
123
|
-
onBlur: loginBodySchema.shape.email,
|
|
124
|
-
onSubmit: loginBodySchema.shape.email
|
|
125
|
-
},
|
|
126
|
-
children: (field)=>/*#__PURE__*/ _jsx(FormField, {
|
|
127
|
-
label: "Email",
|
|
128
|
-
id: "email",
|
|
129
|
-
error: fieldError(field),
|
|
130
|
-
children: /*#__PURE__*/ _jsx(FormFieldInput, {
|
|
131
|
-
autoComplete: "email",
|
|
132
|
-
name: "email",
|
|
133
|
-
type: "email",
|
|
134
|
-
value: field.state.value,
|
|
135
|
-
onChange: (event)=>field.handleChange(event.target.value),
|
|
136
|
-
onBlur: ()=>{
|
|
137
|
-
field.handleBlur();
|
|
138
|
-
persistDraft();
|
|
139
|
-
},
|
|
140
|
-
readOnly: isInvitationEmailLocked,
|
|
141
|
-
iconRight: isInvitationEmailLocked ? /*#__PURE__*/ _jsx(Icon, {
|
|
142
|
-
"aria-hidden": "true",
|
|
143
|
-
className: "size-16 text-foreground-neutral-disabled",
|
|
144
|
-
name: "lockLine"
|
|
145
|
-
}) : undefined
|
|
146
|
-
})
|
|
147
|
-
})
|
|
148
|
-
}),
|
|
149
|
-
/*#__PURE__*/ _jsx(form.Field, {
|
|
150
|
-
name: "password",
|
|
151
|
-
validators: {
|
|
152
|
-
onBlur: loginBodySchema.shape.password,
|
|
153
|
-
onSubmit: loginBodySchema.shape.password
|
|
154
|
-
},
|
|
155
|
-
children: (field)=>/*#__PURE__*/ _jsx(FormField, {
|
|
156
|
-
label: "Password",
|
|
157
|
-
id: "password",
|
|
158
|
-
error: fieldError(field),
|
|
159
|
-
children: /*#__PURE__*/ _jsx(FormFieldInput, {
|
|
160
|
-
autoComplete: "current-password",
|
|
161
|
-
name: "password",
|
|
162
|
-
type: "password",
|
|
163
|
-
value: field.state.value,
|
|
164
|
-
onChange: (event)=>field.handleChange(event.target.value),
|
|
165
|
-
onBlur: ()=>{
|
|
166
|
-
field.handleBlur();
|
|
167
|
-
persistDraft();
|
|
168
|
-
}
|
|
169
|
-
})
|
|
170
|
-
})
|
|
171
|
-
}),
|
|
172
|
-
/*#__PURE__*/ _jsx(ButtonLink, {
|
|
173
|
-
asChild: true,
|
|
174
|
-
variant: "subtle",
|
|
175
|
-
className: "-mt-8 self-end",
|
|
176
|
-
children: /*#__PURE__*/ _jsx(Link, {
|
|
177
|
-
to: "/auth/reset",
|
|
178
|
-
children: "Forgot password?"
|
|
179
|
-
})
|
|
180
|
-
}),
|
|
181
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
182
|
-
className: "w-full",
|
|
183
|
-
isLoading: login.isPending,
|
|
184
|
-
type: "submit",
|
|
185
|
-
children: login.isPending ? 'Logging in...' : 'Log in'
|
|
21
|
+
/*#__PURE__*/ _jsx(PasswordLoginForm, {
|
|
22
|
+
invitationEmail: invitationPending?.email,
|
|
23
|
+
children: /*#__PURE__*/ _jsx(ButtonLink, {
|
|
24
|
+
asChild: true,
|
|
25
|
+
variant: "subtle",
|
|
26
|
+
className: "-mt-8 self-end",
|
|
27
|
+
children: /*#__PURE__*/ _jsx(Link, {
|
|
28
|
+
to: "/auth/reset",
|
|
29
|
+
children: "Forgot password?"
|
|
186
30
|
})
|
|
187
|
-
|
|
31
|
+
})
|
|
188
32
|
}),
|
|
189
33
|
/*#__PURE__*/ _jsxs(Text, {
|
|
190
34
|
size: "sm",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/login-page.tsx"],"sourcesContent":["import {loginBodySchema} from '@shipfox/api-auth-dto';\nimport {AuthShell, useRouteSearch} from '@shipfox/client-shell/runtime';\nimport {Button, ButtonLink} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {Text} from '@shipfox/react-ui/typography';\nimport {useForm} from '@tanstack/react-form';\nimport {Link} from '@tanstack/react-router';\nimport {useAtom} from 'jotai';\nimport {useEffect, useRef, useState} from 'react';\nimport {useLoginAuth} from '#hooks/api/login-auth.js';\nimport {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';\nimport {validateRedirectSearch} from '../routes/inputs.js';\nimport {loginErrorToFormError} from './form-errors.js';\nimport {\n extractInvitationToken,\n pendingInvitation,\n useInvitationContext,\n} from './invitation-context.js';\n\nexport function LoginPage() {\n const login = useLoginAuth();\n const search = useRouteSearch(validateRedirectSearch);\n const invitationToken = extractInvitationToken(search.redirect);\n const invitationPreview = useInvitationContext(invitationToken);\n const invitationPending = pendingInvitation(invitationPreview.data);\n const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);\n const [formError, setFormError] = useState<string | undefined>();\n const draftRef = useRef(authFormDraft);\n draftRef.current = authFormDraft;\n // Set just before clearing the draft on success so the unmount cleanup\n // below does not repersist the just-submitted credentials.\n const skipDraftPersistRef = useRef(false);\n\n const form = useForm({\n defaultValues: {email: authFormDraft.email, password: authFormDraft.password},\n onSubmit: async ({value}) => {\n setFormError(undefined);\n try {\n await login.mutateAsync(value);\n skipDraftPersistRef.current = true;\n setAuthFormDraft(initialAuthFormDraft);\n } catch (error) {\n const mapped = loginErrorToFormError(error);\n if (mapped.kind === 'field') {\n form.setFieldMeta(mapped.field, (prev) => ({\n ...prev,\n errorMap: {...prev.errorMap, onServer: mapped.message},\n }));\n } else {\n setFormError(mapped.message);\n }\n }\n },\n });\n\n // Lock the email field when arriving from an invitation link so the user\n // can't log in as a different account than the one the invitation targets.\n useEffect(() => {\n if (invitationPending && form.state.values.email !== invitationPending.email) {\n form.setFieldValue('email', invitationPending.email);\n setAuthFormDraft((current) => ({...current, email: invitationPending.email}));\n }\n }, [invitationPending, form, setAuthFormDraft]);\n\n // Sync TanStack Form values back into the Jotai draft on unmount so a\n // navigation to /signup or /reset preserves what the user typed. Skipped\n // after a successful login because we just intentionally cleared the draft.\n useEffect(() => {\n return () => {\n if (skipDraftPersistRef.current) return;\n const {email, password} = form.state.values;\n if (email !== draftRef.current.email || password !== draftRef.current.password) {\n setAuthFormDraft({email, password});\n }\n };\n }, [form, setAuthFormDraft]);\n\n const isInvitationEmailLocked = Boolean(invitationPending);\n const invitationRedirect = invitationToken\n ? `/invitations/accept?token=${encodeURIComponent(invitationToken)}`\n : undefined;\n const headerTitle = invitationPending\n ? `Join ${invitationPending.workspaceName}`\n : 'Connect to Shipfox';\n const headerDescription = invitationPending\n ? 'Log in to accept your invitation.'\n : 'Log in to access Shipfox.';\n\n function persistDraft() {\n const {email, password} = form.state.values;\n setAuthFormDraft({email, password});\n }\n\n return (\n <AuthShell title={headerTitle} description={headerDescription}>\n <form\n className=\"flex flex-col gap-18\"\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void form.handleSubmit();\n }}\n >\n {formError ? (\n <Callout role=\"alert\" type=\"error\">\n {formError}\n </Callout>\n ) : null}\n <form.Field\n name=\"email\"\n validators={{onBlur: loginBodySchema.shape.email, onSubmit: loginBodySchema.shape.email}}\n >\n {(field) => (\n <FormField label=\"Email\" id=\"email\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"email\"\n name=\"email\"\n type=\"email\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n readOnly={isInvitationEmailLocked}\n iconRight={\n isInvitationEmailLocked ? (\n <Icon\n aria-hidden=\"true\"\n className=\"size-16 text-foreground-neutral-disabled\"\n name=\"lockLine\"\n />\n ) : undefined\n }\n />\n </FormField>\n )}\n </form.Field>\n <form.Field\n name=\"password\"\n validators={{\n onBlur: loginBodySchema.shape.password,\n onSubmit: loginBodySchema.shape.password,\n }}\n >\n {(field) => (\n <FormField label=\"Password\" id=\"password\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"current-password\"\n name=\"password\"\n type=\"password\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n />\n </FormField>\n )}\n </form.Field>\n <ButtonLink asChild variant=\"subtle\" className=\"-mt-8 self-end\">\n <Link to=\"/auth/reset\">Forgot password?</Link>\n </ButtonLink>\n <Button className=\"w-full\" isLoading={login.isPending} type=\"submit\">\n {login.isPending ? 'Logging in...' : 'Log in'}\n </Button>\n </form>\n <Text size=\"sm\" className=\"text-center text-foreground-neutral-subtle\">\n New to Shipfox?{' '}\n <ButtonLink asChild variant=\"interactive\" underline>\n <Link\n to=\"/auth/signup\"\n search={invitationRedirect ? {redirect: invitationRedirect} : undefined}\n >\n Create an account\n </Link>\n </ButtonLink>\n </Text>\n </AuthShell>\n );\n}\n"],"names":["loginBodySchema","AuthShell","useRouteSearch","Button","ButtonLink","Callout","FormField","FormFieldInput","fieldError","Icon","Text","useForm","Link","useAtom","useEffect","useRef","useState","useLoginAuth","authFormDraftAtom","initialAuthFormDraft","validateRedirectSearch","loginErrorToFormError","extractInvitationToken","pendingInvitation","useInvitationContext","LoginPage","login","search","invitationToken","redirect","invitationPreview","invitationPending","data","authFormDraft","setAuthFormDraft","formError","setFormError","draftRef","current","skipDraftPersistRef","form","defaultValues","email","password","onSubmit","value","undefined","mutateAsync","error","mapped","kind","setFieldMeta","field","prev","errorMap","onServer","message","state","values","setFieldValue","isInvitationEmailLocked","Boolean","invitationRedirect","encodeURIComponent","headerTitle","workspaceName","headerDescription","persistDraft","title","description","className","noValidate","event","preventDefault","stopPropagation","handleSubmit","role","type","Field","name","validators","onBlur","shape","label","id","autoComplete","onChange","handleChange","target","handleBlur","readOnly","iconRight","aria-hidden","asChild","variant","to","isLoading","isPending","size","underline"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/pages/login-page.tsx"],"sourcesContent":["import {AuthShell, useRouteSearch} from '@shipfox/client-shell/runtime';\nimport {ButtonLink} from '@shipfox/react-ui/button';\nimport {Text} from '@shipfox/react-ui/typography';\nimport {Link} from '@tanstack/react-router';\nimport {PasswordLoginForm} from '#components/password-login-form.js';\nimport {validateRedirectSearch} from '../routes/inputs.js';\nimport {\n extractInvitationToken,\n pendingInvitation,\n useInvitationContext,\n} from './invitation-context.js';\n\nexport function LoginPage() {\n const search = useRouteSearch(validateRedirectSearch);\n const invitationToken = extractInvitationToken(search.redirect);\n const invitationPreview = useInvitationContext(invitationToken);\n const invitationPending = pendingInvitation(invitationPreview.data);\n const invitationRedirect = invitationToken\n ? `/invitations/accept?token=${encodeURIComponent(invitationToken)}`\n : undefined;\n const headerTitle = invitationPending\n ? `Join ${invitationPending.workspaceName}`\n : 'Connect to Shipfox';\n const headerDescription = invitationPending\n ? 'Log in to accept your invitation.'\n : 'Log in to access Shipfox.';\n\n return (\n <AuthShell title={headerTitle} description={headerDescription}>\n <PasswordLoginForm invitationEmail={invitationPending?.email}>\n <ButtonLink asChild variant=\"subtle\" className=\"-mt-8 self-end\">\n <Link to=\"/auth/reset\">Forgot password?</Link>\n </ButtonLink>\n </PasswordLoginForm>\n <Text size=\"sm\" className=\"text-center text-foreground-neutral-subtle\">\n New to Shipfox?{' '}\n <ButtonLink asChild variant=\"interactive\" underline>\n <Link\n to=\"/auth/signup\"\n search={invitationRedirect ? {redirect: invitationRedirect} : undefined}\n >\n Create an account\n </Link>\n </ButtonLink>\n </Text>\n </AuthShell>\n );\n}\n"],"names":["AuthShell","useRouteSearch","ButtonLink","Text","Link","PasswordLoginForm","validateRedirectSearch","extractInvitationToken","pendingInvitation","useInvitationContext","LoginPage","search","invitationToken","redirect","invitationPreview","invitationPending","data","invitationRedirect","encodeURIComponent","undefined","headerTitle","workspaceName","headerDescription","title","description","invitationEmail","email","asChild","variant","className","to","size","underline"],"mappings":";AAAA,SAAQA,SAAS,EAAEC,cAAc,QAAO,gCAAgC;AACxE,SAAQC,UAAU,QAAO,2BAA2B;AACpD,SAAQC,IAAI,QAAO,+BAA+B;AAClD,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,iBAAiB,QAAO,qCAAqC;AACrE,SAAQC,sBAAsB,QAAO,sBAAsB;AAC3D,SACEC,sBAAsB,EACtBC,iBAAiB,EACjBC,oBAAoB,QACf,0BAA0B;AAEjC,OAAO,SAASC;IACd,MAAMC,SAASV,eAAeK;IAC9B,MAAMM,kBAAkBL,uBAAuBI,OAAOE,QAAQ;IAC9D,MAAMC,oBAAoBL,qBAAqBG;IAC/C,MAAMG,oBAAoBP,kBAAkBM,kBAAkBE,IAAI;IAClE,MAAMC,qBAAqBL,kBACvB,CAAC,0BAA0B,EAAEM,mBAAmBN,kBAAkB,GAClEO;IACJ,MAAMC,cAAcL,oBAChB,CAAC,KAAK,EAAEA,kBAAkBM,aAAa,EAAE,GACzC;IACJ,MAAMC,oBAAoBP,oBACtB,sCACA;IAEJ,qBACE,MAACf;QAAUuB,OAAOH;QAAaI,aAAaF;;0BAC1C,KAACjB;gBAAkBoB,iBAAiBV,mBAAmBW;0BACrD,cAAA,KAACxB;oBAAWyB,OAAO;oBAACC,SAAQ;oBAASC,WAAU;8BAC7C,cAAA,KAACzB;wBAAK0B,IAAG;kCAAc;;;;0BAG3B,MAAC3B;gBAAK4B,MAAK;gBAAKF,WAAU;;oBAA6C;oBACrD;kCAChB,KAAC3B;wBAAWyB,OAAO;wBAACC,SAAQ;wBAAcI,SAAS;kCACjD,cAAA,KAAC5B;4BACC0B,IAAG;4BACHnB,QAAQM,qBAAqB;gCAACJ,UAAUI;4BAAkB,IAAIE;sCAC/D;;;;;;;AAOX"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-context.d.ts","sourceRoot":"","sources":["../src/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,CAQpE"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAuthPath, resolveRedirectPath } from './redirect-target.js';
|
|
1
|
+
import { isAuthPath, resolveRedirectPath } from './components/redirect-target.js';
|
|
2
2
|
const INVITATION_ACCEPT_PATH = '/invitations/accept';
|
|
3
3
|
/**
|
|
4
4
|
* Separates a safe post-authentication destination from an invitation token.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/redirect-context.ts"],"sourcesContent":["import {isAuthPath, resolveRedirectPath} from './components/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 resolved = resolveRedirectPath(value);\n if (!resolved || isAuthPath(resolved.pathname)) return {};\n\n if (resolved.pathname !== INVITATION_ACCEPT_PATH) return {returnTo: resolved.redirect};\n\n const invitationToken = resolved.target.searchParams.get('token');\n return invitationToken ? {invitationToken} : {};\n}\n"],"names":["isAuthPath","resolveRedirectPath","INVITATION_ACCEPT_PATH","parseRedirectContext","value","resolved","pathname","returnTo","redirect","invitationToken","target","searchParams","get"],"mappings":"AAAA,SAAQA,UAAU,EAAEC,mBAAmB,QAAO,kCAAkC;AAEhF,MAAMC,yBAAyB;AAO/B;;;;CAIC,GACD,OAAO,SAASC,qBAAqBC,KAAc;IACjD,MAAMC,WAAWJ,oBAAoBG;IACrC,IAAI,CAACC,YAAYL,WAAWK,SAASC,QAAQ,GAAG,OAAO,CAAC;IAExD,IAAID,SAASC,QAAQ,KAAKJ,wBAAwB,OAAO;QAACK,UAAUF,SAASG,QAAQ;IAAA;IAErF,MAAMC,kBAAkBJ,SAASK,MAAM,CAACC,YAAY,CAACC,GAAG,CAAC;IACzD,OAAOH,kBAAkB;QAACA;IAAe,IAAI,CAAC;AAChD"}
|