@contember/react-client-tenant 2.0.0-alpha.13 → 2.0.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/development/index.cjs +18 -0
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +18 -0
- package/dist/development/index.js.map +1 -1
- package/dist/development/src/components/forms/LoginForm.cjs +1 -1
- package/dist/development/src/components/forms/LoginForm.cjs.map +1 -1
- package/dist/development/src/components/forms/LoginForm.js +1 -1
- package/dist/development/src/components/forms/LoginForm.js.map +1 -1
- package/dist/development/src/components/forms/PasswordResetForm.cjs +2 -1
- package/dist/development/src/components/forms/PasswordResetForm.cjs.map +1 -1
- package/dist/development/src/components/forms/PasswordResetForm.js +2 -1
- package/dist/development/src/components/forms/PasswordResetForm.js.map +1 -1
- package/dist/development/src/components/forms/PasswordlessSignInForm.cjs +79 -0
- package/dist/development/src/components/forms/PasswordlessSignInForm.cjs.map +1 -0
- package/dist/development/src/components/forms/PasswordlessSignInForm.js +79 -0
- package/dist/development/src/components/forms/PasswordlessSignInForm.js.map +1 -0
- package/dist/development/src/components/forms/PasswordlessSignInInitForm.cjs +54 -0
- package/dist/development/src/components/forms/PasswordlessSignInInitForm.cjs.map +1 -0
- package/dist/development/src/components/forms/PasswordlessSignInInitForm.js +54 -0
- package/dist/development/src/components/forms/PasswordlessSignInInitForm.js.map +1 -0
- package/dist/development/src/consts.cjs +5 -0
- package/dist/development/src/consts.cjs.map +1 -0
- package/dist/development/src/consts.js +5 -0
- package/dist/development/src/consts.js.map +1 -0
- package/dist/development/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.cjs +31 -0
- package/dist/development/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.cjs.map +1 -0
- package/dist/development/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.js +14 -0
- package/dist/development/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.js.map +1 -0
- package/dist/development/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.cjs +28 -0
- package/dist/development/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.cjs.map +1 -0
- package/dist/development/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.js +11 -0
- package/dist/development/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.js.map +1 -0
- package/dist/development/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.cjs +28 -0
- package/dist/development/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.cjs.map +1 -0
- package/dist/development/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.js +11 -0
- package/dist/development/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.js.map +1 -0
- package/dist/development/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.cjs +32 -0
- package/dist/development/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.cjs.map +1 -0
- package/dist/development/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.js +15 -0
- package/dist/development/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.js.map +1 -0
- package/dist/development/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.cjs +32 -0
- package/dist/development/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.cjs.map +1 -0
- package/dist/development/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.js +15 -0
- package/dist/development/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.js.map +1 -0
- package/dist/development/src/hooks/usePasswordlessOtpActivator.cjs +53 -0
- package/dist/development/src/hooks/usePasswordlessOtpActivator.cjs.map +1 -0
- package/dist/development/src/hooks/usePasswordlessOtpActivator.js +53 -0
- package/dist/development/src/hooks/usePasswordlessOtpActivator.js.map +1 -0
- package/dist/development/src/internal/hooks/useHandleIDPResponse.cjs +1 -1
- package/dist/development/src/internal/hooks/useHandleIDPResponse.cjs.map +1 -1
- package/dist/development/src/internal/hooks/useHandleIDPResponse.js +1 -1
- package/dist/development/src/internal/hooks/useHandleIDPResponse.js.map +1 -1
- package/dist/development/src/internal/utils/hashing.cjs +8 -0
- package/dist/development/src/internal/utils/hashing.cjs.map +1 -0
- package/dist/development/src/internal/utils/hashing.js +8 -0
- package/dist/development/src/internal/utils/hashing.js.map +1 -0
- package/dist/development/src/internal/utils/randomInt.cjs +29 -0
- package/dist/development/src/internal/utils/randomInt.cjs.map +1 -0
- package/dist/development/src/internal/utils/randomInt.js +29 -0
- package/dist/development/src/internal/utils/randomInt.js.map +1 -0
- package/dist/production/index.cjs +18 -0
- package/dist/production/index.cjs.map +1 -1
- package/dist/production/index.js +18 -0
- package/dist/production/index.js.map +1 -1
- package/dist/production/src/components/forms/LoginForm.cjs +1 -1
- package/dist/production/src/components/forms/LoginForm.cjs.map +1 -1
- package/dist/production/src/components/forms/LoginForm.js +1 -1
- package/dist/production/src/components/forms/LoginForm.js.map +1 -1
- package/dist/production/src/components/forms/PasswordResetForm.cjs +2 -1
- package/dist/production/src/components/forms/PasswordResetForm.cjs.map +1 -1
- package/dist/production/src/components/forms/PasswordResetForm.js +2 -1
- package/dist/production/src/components/forms/PasswordResetForm.js.map +1 -1
- package/dist/production/src/components/forms/PasswordlessSignInForm.cjs +71 -0
- package/dist/production/src/components/forms/PasswordlessSignInForm.cjs.map +1 -0
- package/dist/production/src/components/forms/PasswordlessSignInForm.js +71 -0
- package/dist/production/src/components/forms/PasswordlessSignInForm.js.map +1 -0
- package/dist/production/src/components/forms/PasswordlessSignInInitForm.cjs +46 -0
- package/dist/production/src/components/forms/PasswordlessSignInInitForm.cjs.map +1 -0
- package/dist/production/src/components/forms/PasswordlessSignInInitForm.js +46 -0
- package/dist/production/src/components/forms/PasswordlessSignInInitForm.js.map +1 -0
- package/dist/production/src/consts.cjs +5 -0
- package/dist/production/src/consts.cjs.map +1 -0
- package/dist/production/src/consts.js +5 -0
- package/dist/production/src/consts.js.map +1 -0
- package/dist/production/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.cjs +31 -0
- package/dist/production/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.cjs.map +1 -0
- package/dist/production/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.js +14 -0
- package/dist/production/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.js.map +1 -0
- package/dist/production/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.cjs +28 -0
- package/dist/production/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.cjs.map +1 -0
- package/dist/production/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.js +11 -0
- package/dist/production/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.js.map +1 -0
- package/dist/production/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.cjs +28 -0
- package/dist/production/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.cjs.map +1 -0
- package/dist/production/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.js +11 -0
- package/dist/production/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.js.map +1 -0
- package/dist/production/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.cjs +32 -0
- package/dist/production/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.cjs.map +1 -0
- package/dist/production/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.js +15 -0
- package/dist/production/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.js.map +1 -0
- package/dist/production/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.cjs +32 -0
- package/dist/production/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.cjs.map +1 -0
- package/dist/production/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.js +15 -0
- package/dist/production/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.js.map +1 -0
- package/dist/production/src/hooks/usePasswordlessOtpActivator.cjs +53 -0
- package/dist/production/src/hooks/usePasswordlessOtpActivator.cjs.map +1 -0
- package/dist/production/src/hooks/usePasswordlessOtpActivator.js +53 -0
- package/dist/production/src/hooks/usePasswordlessOtpActivator.js.map +1 -0
- package/dist/production/src/internal/hooks/useHandleIDPResponse.cjs +1 -1
- package/dist/production/src/internal/hooks/useHandleIDPResponse.cjs.map +1 -1
- package/dist/production/src/internal/hooks/useHandleIDPResponse.js +1 -1
- package/dist/production/src/internal/hooks/useHandleIDPResponse.js.map +1 -1
- package/dist/production/src/internal/utils/hashing.cjs +8 -0
- package/dist/production/src/internal/utils/hashing.cjs.map +1 -0
- package/dist/production/src/internal/utils/hashing.js +8 -0
- package/dist/production/src/internal/utils/hashing.js.map +1 -0
- package/dist/production/src/internal/utils/randomInt.cjs +29 -0
- package/dist/production/src/internal/utils/randomInt.cjs.map +1 -0
- package/dist/production/src/internal/utils/randomInt.js +29 -0
- package/dist/production/src/internal/utils/randomInt.js.map +1 -0
- package/dist/types/components/forms/PasswordlessSignInForm.d.ts +22 -0
- package/dist/types/components/forms/PasswordlessSignInForm.d.ts.map +1 -0
- package/dist/types/components/forms/PasswordlessSignInInitForm.d.ts +20 -0
- package/dist/types/components/forms/PasswordlessSignInInitForm.d.ts.map +1 -0
- package/dist/types/components/forms/index.d.ts +2 -0
- package/dist/types/components/forms/index.d.ts.map +1 -1
- package/dist/types/consts.d.ts +2 -0
- package/dist/types/consts.d.ts.map +1 -0
- package/dist/types/hooks/index.d.ts +1 -0
- package/dist/types/hooks/index.d.ts.map +1 -1
- package/dist/types/hooks/mutations/apiKey/useCreateSessionTokenMutation.d.ts +6 -0
- package/dist/types/hooks/mutations/apiKey/useCreateSessionTokenMutation.d.ts.map +1 -1
- package/dist/types/hooks/mutations/auth/useSignInMutation.d.ts +6 -0
- package/dist/types/hooks/mutations/auth/useSignInMutation.d.ts.map +1 -1
- package/dist/types/hooks/mutations/idp/useSignInIdpMutation.d.ts +6 -0
- package/dist/types/hooks/mutations/idp/useSignInIdpMutation.d.ts.map +1 -1
- package/dist/types/hooks/mutations/index.d.ts +1 -0
- package/dist/types/hooks/mutations/index.d.ts.map +1 -1
- package/dist/types/hooks/mutations/invite/useInviteMutation.d.ts +6 -0
- package/dist/types/hooks/mutations/invite/useInviteMutation.d.ts.map +1 -1
- package/dist/types/hooks/mutations/passwordless/index.d.ts +6 -0
- package/dist/types/hooks/mutations/passwordless/index.d.ts.map +1 -0
- package/dist/types/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.d.ts +8 -0
- package/dist/types/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.d.ts.map +1 -0
- package/dist/types/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.d.ts +3 -0
- package/dist/types/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.d.ts.map +1 -0
- package/dist/types/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.d.ts +3 -0
- package/dist/types/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.d.ts.map +1 -0
- package/dist/types/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.d.ts +19 -0
- package/dist/types/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.d.ts.map +1 -0
- package/dist/types/hooks/mutations/passwordless/useSignInPasswordlessMutation.d.ts +42 -0
- package/dist/types/hooks/mutations/passwordless/useSignInPasswordlessMutation.d.ts.map +1 -0
- package/dist/types/hooks/queries/useMeQuery.d.ts +2 -0
- package/dist/types/hooks/queries/useMeQuery.d.ts.map +1 -1
- package/dist/types/hooks/queries/useProjectMembersQuery.d.ts +2 -0
- package/dist/types/hooks/queries/useProjectMembersQuery.d.ts.map +1 -1
- package/dist/types/hooks/usePasswordlessOtpActivator.d.ts +19 -0
- package/dist/types/hooks/usePasswordlessOtpActivator.d.ts.map +1 -0
- package/dist/types/internal/utils/hashing.d.ts +2 -0
- package/dist/types/internal/utils/hashing.d.ts.map +1 -0
- package/dist/types/internal/utils/randomInt.d.ts +3 -0
- package/dist/types/internal/utils/randomInt.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/components/forms/LoginForm.tsx +1 -1
- package/src/components/forms/PasswordResetForm.tsx +1 -0
- package/src/components/forms/PasswordlessSignInForm.tsx +102 -0
- package/src/components/forms/PasswordlessSignInInitForm.tsx +68 -0
- package/src/components/forms/index.ts +2 -0
- package/src/consts.ts +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/mutations/index.ts +1 -0
- package/src/hooks/mutations/passwordless/index.ts +5 -0
- package/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.ts +19 -0
- package/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.ts +14 -0
- package/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.ts +14 -0
- package/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.ts +22 -0
- package/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.ts +24 -0
- package/src/hooks/usePasswordlessOtpActivator.ts +66 -0
- package/src/internal/hooks/useHandleIDPResponse.ts +1 -1
- package/src/internal/utils/hashing.ts +4 -0
- package/src/internal/utils/randomInt.ts +30 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/react-client-tenant",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.0.0-alpha.
|
|
4
|
+
"version": "2.0.0-alpha.15",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"main": "./dist/production/index.js",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"directory": "packages/react-client-tenant"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@contember/graphql-client-tenant": "2.0.0-alpha.
|
|
37
|
-
"@contember/react-client": "2.0.0-alpha.
|
|
38
|
-
"@contember/react-utils": "2.0.0-alpha.
|
|
36
|
+
"@contember/graphql-client-tenant": "2.0.0-alpha.15",
|
|
37
|
+
"@contember/react-client": "2.0.0-alpha.15",
|
|
38
|
+
"@contember/react-utils": "2.0.0-alpha.15",
|
|
39
39
|
"@radix-ui/primitive": "^1.0.1",
|
|
40
40
|
"@radix-ui/react-slot": "^1.0.2",
|
|
41
41
|
"graphql-ts-client-api": "^3.1.17"
|
|
@@ -41,7 +41,7 @@ const headers = {
|
|
|
41
41
|
'X-Contember-Token-Path': 'data.mutation.result.token',
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const DEFAULT_LOGIN_EXPIRATION = 14 * 24 *
|
|
44
|
+
const DEFAULT_LOGIN_EXPIRATION = 14 * 24 * 60 // 14 days
|
|
45
45
|
|
|
46
46
|
export const LoginForm = ({ children, expiration = DEFAULT_LOGIN_EXPIRATION, onSuccess }: LoginFormProps) => {
|
|
47
47
|
const login = useSignInMutation({ headers })
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ReactElement, useCallback, useMemo } from 'react'
|
|
2
|
+
import * as TenantApi from '@contember/graphql-client-tenant'
|
|
3
|
+
import { TenantForm } from './TenantForm'
|
|
4
|
+
import { FormContextValue, FormError, FormState } from '../../types/forms'
|
|
5
|
+
import { useForm } from '../../contexts'
|
|
6
|
+
import { SignInPasswordlessMutationResult, useSignInPasswordlessMutation } from '../../hooks'
|
|
7
|
+
import { useRedirectToBacklinkCallback } from '../../internal/hooks/useRedirectToBacklink'
|
|
8
|
+
import { useSetSessionToken } from '@contember/react-client'
|
|
9
|
+
|
|
10
|
+
export type PasswordlessSignInFormValues = {
|
|
11
|
+
token: string
|
|
12
|
+
otpToken: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PasswordlessSignInFormErrorCode =
|
|
16
|
+
| TenantApi.SignInPasswordlessErrorCode
|
|
17
|
+
| 'FIELD_REQUIRED'
|
|
18
|
+
| 'INVALID_VALUE'
|
|
19
|
+
| 'UNKNOWN_ERROR'
|
|
20
|
+
|
|
21
|
+
export type PasswordlessSignInFormState =
|
|
22
|
+
| FormState
|
|
23
|
+
| 'otp-required'
|
|
24
|
+
|
|
25
|
+
export type PasswordlessSignInFormError = FormError<PasswordlessSignInFormValues, PasswordlessSignInFormErrorCode>
|
|
26
|
+
|
|
27
|
+
export type PasswordlessSignInFormContextValue = FormContextValue<PasswordlessSignInFormValues, PasswordlessSignInFormErrorCode, PasswordlessSignInFormState>
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
export interface PasswordlessSignInFormProps {
|
|
31
|
+
requestId: string
|
|
32
|
+
validationType: TenantApi.PasswordlessValidationType
|
|
33
|
+
token?: string
|
|
34
|
+
expiration?: number
|
|
35
|
+
children: ReactElement
|
|
36
|
+
onSuccess?: () => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const usePasswordlessSignInForm = useForm as () => PasswordlessSignInFormContextValue
|
|
40
|
+
|
|
41
|
+
const headers = {
|
|
42
|
+
'X-Contember-Token-Path': 'data.mutation.result.token',
|
|
43
|
+
}
|
|
44
|
+
const DEFAULT_LOGIN_EXPIRATION = 14 * 24 * 60 // 14 days
|
|
45
|
+
|
|
46
|
+
export const PasswordlessSignInForm = ({ children, onSuccess, requestId, validationType, token, expiration = DEFAULT_LOGIN_EXPIRATION }: PasswordlessSignInFormProps) => {
|
|
47
|
+
|
|
48
|
+
const signIn = useSignInPasswordlessMutation({ headers })
|
|
49
|
+
const redirectToBacklink = useRedirectToBacklinkCallback()
|
|
50
|
+
const setSessionToken = useSetSessionToken()
|
|
51
|
+
return (
|
|
52
|
+
<TenantForm<PasswordlessSignInFormContextValue, SignInPasswordlessMutationResult>
|
|
53
|
+
onSuccess={args => {
|
|
54
|
+
onSuccess?.()
|
|
55
|
+
setSessionToken(args.result.token)
|
|
56
|
+
redirectToBacklink()
|
|
57
|
+
}}
|
|
58
|
+
errorMapping={errorToField}
|
|
59
|
+
initialValues={useMemo(() => ({
|
|
60
|
+
token: token ?? '',
|
|
61
|
+
otpToken: '',
|
|
62
|
+
}), [token])}
|
|
63
|
+
validate={({ values, state }) => {
|
|
64
|
+
if (!values.token) {
|
|
65
|
+
return [{ code: 'FIELD_REQUIRED', field: 'token' }]
|
|
66
|
+
}
|
|
67
|
+
if (!values.otpToken && state === 'otp-required') {
|
|
68
|
+
return [{ code: 'FIELD_REQUIRED', field: 'otpToken' }]
|
|
69
|
+
} else if (values.otpToken && !values.otpToken.match(/^\d{6}$/)) {
|
|
70
|
+
return [{ code: 'INVALID_VALUE', field: 'otpToken' }]
|
|
71
|
+
}
|
|
72
|
+
return []
|
|
73
|
+
}}
|
|
74
|
+
execute={useCallback(async ({ values }) => {
|
|
75
|
+
const result = await signIn({
|
|
76
|
+
requestId,
|
|
77
|
+
expiration,
|
|
78
|
+
token: values.token,
|
|
79
|
+
mfaOtp: values.otpToken || undefined,
|
|
80
|
+
validationType,
|
|
81
|
+
})
|
|
82
|
+
if (!result.ok && result.error === 'OTP_REQUIRED') {
|
|
83
|
+
return { ...result, state: 'otp-required' }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
}, [expiration, requestId, signIn, validationType])}
|
|
88
|
+
>
|
|
89
|
+
{children}
|
|
90
|
+
</TenantForm>
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
const errorToField: Record<TenantApi.SignInPasswordlessErrorCode, keyof PasswordlessSignInFormValues | undefined> = {
|
|
95
|
+
PERSON_DISABLED: undefined,
|
|
96
|
+
TOKEN_EXPIRED: 'token',
|
|
97
|
+
TOKEN_NOT_FOUND: 'token',
|
|
98
|
+
TOKEN_USED: 'token',
|
|
99
|
+
TOKEN_INVALID: 'token',
|
|
100
|
+
INVALID_OTP_TOKEN: 'otpToken',
|
|
101
|
+
OTP_REQUIRED: undefined,
|
|
102
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ReactElement, useCallback, useMemo } from 'react'
|
|
2
|
+
import * as TenantApi from '@contember/graphql-client-tenant'
|
|
3
|
+
import { TenantForm } from './TenantForm'
|
|
4
|
+
import { FormContextValue, FormError, FormState } from '../../types/forms'
|
|
5
|
+
import { useForm } from '../../contexts'
|
|
6
|
+
import { InitSignInPasswordlessMutationResult, useInitSignInPasswordlessMutation } from '../../hooks'
|
|
7
|
+
import { PASSWORDLESS_REQUEST_STORAGE_KEY } from '../../consts'
|
|
8
|
+
|
|
9
|
+
export type PasswordlessSignInInitFormValues = {
|
|
10
|
+
email: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type PasswordlessSignInInitFormErrorCode =
|
|
14
|
+
| TenantApi.InitSignInPasswordlessErrorCode
|
|
15
|
+
| 'FIELD_REQUIRED'
|
|
16
|
+
| 'INVALID_VALUE'
|
|
17
|
+
| 'UNKNOWN_ERROR'
|
|
18
|
+
|
|
19
|
+
export type PasswordlessSignInInitFormState = FormState
|
|
20
|
+
|
|
21
|
+
export type PasswordlessSignInInitFormError = FormError<PasswordlessSignInInitFormValues, PasswordlessSignInInitFormErrorCode>
|
|
22
|
+
|
|
23
|
+
export type PasswordlessSignInInitFormContextValue = FormContextValue<PasswordlessSignInInitFormValues, PasswordlessSignInInitFormErrorCode>
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export interface PasswordlessSignInInitFormProps {
|
|
27
|
+
children: ReactElement
|
|
28
|
+
onSuccess?: (args: { result: InitSignInPasswordlessMutationResult }) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const usePasswordlessSignInInitForm = useForm as () => PasswordlessSignInInitFormContextValue
|
|
32
|
+
|
|
33
|
+
export const PasswordlessSignInInitForm = ({ children, onSuccess }: PasswordlessSignInInitFormProps) => {
|
|
34
|
+
|
|
35
|
+
const initSignIn = useInitSignInPasswordlessMutation()
|
|
36
|
+
return (
|
|
37
|
+
<TenantForm<PasswordlessSignInInitFormContextValue, InitSignInPasswordlessMutationResult>
|
|
38
|
+
onSuccess={res => {
|
|
39
|
+
localStorage.setItem(PASSWORDLESS_REQUEST_STORAGE_KEY, res.result.requestId)
|
|
40
|
+
onSuccess?.(res)
|
|
41
|
+
}}
|
|
42
|
+
errorMapping={errorToField}
|
|
43
|
+
initialValues={useMemo(() => ({
|
|
44
|
+
email: '',
|
|
45
|
+
}), [])}
|
|
46
|
+
validate={({ values }) => {
|
|
47
|
+
if (!values.email) {
|
|
48
|
+
return [{ code: 'FIELD_REQUIRED', field: 'email' }]
|
|
49
|
+
} else if (!values.email.match(/^.+@.+$/)) {
|
|
50
|
+
return [{ code: 'INVALID_VALUE', field: 'email' }]
|
|
51
|
+
}
|
|
52
|
+
return []
|
|
53
|
+
}}
|
|
54
|
+
execute={useCallback(async ({ values }) => {
|
|
55
|
+
return await initSignIn({
|
|
56
|
+
email: values.email,
|
|
57
|
+
})
|
|
58
|
+
}, [initSignIn])}
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</TenantForm>
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
const errorToField: Record<TenantApi.InitSignInPasswordlessErrorCode, keyof PasswordlessSignInInitFormValues | undefined> = {
|
|
66
|
+
PERSON_NOT_FOUND: 'email',
|
|
67
|
+
PASSWORDLESS_DISABLED: undefined,
|
|
68
|
+
}
|
|
@@ -5,6 +5,8 @@ export * from './InviteForm'
|
|
|
5
5
|
export * from './LoginForm'
|
|
6
6
|
export * from './PasswordResetForm'
|
|
7
7
|
export * from './PasswordResetRequestForm'
|
|
8
|
+
export * from './PasswordlessSignInForm'
|
|
9
|
+
export * from './PasswordlessSignInInitForm'
|
|
8
10
|
export * from './OtpPrepareForm'
|
|
9
11
|
export * from './OtpConfirmForm'
|
|
10
12
|
export * from './UpdateProjectMemberForm'
|
package/src/consts.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PASSWORDLESS_REQUEST_STORAGE_KEY = 'passwordless_request'
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as TenantApi from '@contember/graphql-client-tenant'
|
|
2
|
+
import { LoginToken } from '../../useTenantApi'
|
|
3
|
+
import { createTenantMutation } from '../../useTenantMutation'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const ActivatePasswordlessOtpMutation = TenantApi
|
|
7
|
+
.mutation$
|
|
8
|
+
.activatePasswordlessOtp(
|
|
9
|
+
TenantApi
|
|
10
|
+
.activatePasswordlessOtpResponse$$
|
|
11
|
+
.error(TenantApi.activatePasswordlessOtpError$$),
|
|
12
|
+
options => options.alias('mutation'),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
export const useActivatePasswordlessOtpMutation = createTenantMutation(ActivatePasswordlessOtpMutation, {
|
|
16
|
+
apiToken: LoginToken,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export type ActivatePasswordlessOtpMutationVariables = Parameters<ReturnType<typeof useActivatePasswordlessOtpMutation>>[0]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as TenantApi from '@contember/graphql-client-tenant'
|
|
2
|
+
import { createTenantMutation } from '../../useTenantMutation'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const DisableMyPasswordlessMutation = TenantApi
|
|
6
|
+
.mutation$
|
|
7
|
+
.disableMyPasswordless(
|
|
8
|
+
TenantApi
|
|
9
|
+
.toggleMyPasswordlessResponse$$
|
|
10
|
+
.error(TenantApi.toggleMyPasswordlessError$$),
|
|
11
|
+
options => options.alias('mutation'),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export const useDisableMyPasswordlessMutation = createTenantMutation(DisableMyPasswordlessMutation)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as TenantApi from '@contember/graphql-client-tenant'
|
|
2
|
+
import { createTenantMutation } from '../../useTenantMutation'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const EnableMyPasswordlessMutation = TenantApi
|
|
6
|
+
.mutation$
|
|
7
|
+
.enableMyPasswordless(
|
|
8
|
+
TenantApi
|
|
9
|
+
.toggleMyPasswordlessResponse$$
|
|
10
|
+
.error(TenantApi.toggleMyPasswordlessError$$),
|
|
11
|
+
options => options.alias('mutation'),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export const useEnableMyPasswordlessMutation = createTenantMutation(EnableMyPasswordlessMutation)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as TenantApi from '@contember/graphql-client-tenant'
|
|
2
|
+
import { LoginToken } from '../../useTenantApi'
|
|
3
|
+
import { createTenantMutation } from '../../useTenantMutation'
|
|
4
|
+
import { ModelType } from 'graphql-ts-client-api'
|
|
5
|
+
|
|
6
|
+
const InitSignInPasswordlessMutationResult = TenantApi.initSignInPasswordlessResult$$
|
|
7
|
+
export type InitSignInPasswordlessMutationResult = ModelType<typeof InitSignInPasswordlessMutationResult>
|
|
8
|
+
const InitSignInPasswordlessMutation = TenantApi
|
|
9
|
+
.mutation$
|
|
10
|
+
.initSignInPasswordless(
|
|
11
|
+
TenantApi
|
|
12
|
+
.initSignInPasswordlessResponse$$
|
|
13
|
+
.error(TenantApi.initSignInPasswordlessError$$)
|
|
14
|
+
.result(InitSignInPasswordlessMutationResult),
|
|
15
|
+
options => options.alias('mutation'),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export const useInitSignInPasswordlessMutation = createTenantMutation(InitSignInPasswordlessMutation, {
|
|
19
|
+
apiToken: LoginToken,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export type InitSignInPasswordlessMutationVariables = Parameters<ReturnType<typeof useInitSignInPasswordlessMutation>>[0]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as TenantApi from '@contember/graphql-client-tenant'
|
|
2
|
+
import { LoginToken } from '../../useTenantApi'
|
|
3
|
+
import { createTenantMutation } from '../../useTenantMutation'
|
|
4
|
+
import { ModelType } from 'graphql-ts-client-api'
|
|
5
|
+
|
|
6
|
+
const signInResultFragment = TenantApi.signInPasswordlessResult$$.person(TenantApi.person$$)
|
|
7
|
+
|
|
8
|
+
export type SignInPasswordlessMutationResult = ModelType<typeof signInResultFragment>
|
|
9
|
+
|
|
10
|
+
const SignInPasswordlessMutation = TenantApi
|
|
11
|
+
.mutation$
|
|
12
|
+
.signInPasswordless(
|
|
13
|
+
TenantApi
|
|
14
|
+
.signInPasswordlessResponse$$
|
|
15
|
+
.error(TenantApi.signInPasswordlessError$$)
|
|
16
|
+
.result(signInResultFragment),
|
|
17
|
+
options => options.alias('mutation'),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
export const useSignInPasswordlessMutation = createTenantMutation(SignInPasswordlessMutation, {
|
|
21
|
+
apiToken: LoginToken,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export type SignInPasswordlessMutationVariables = Parameters<ReturnType<typeof useSignInPasswordlessMutation>>[0]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { PASSWORDLESS_REQUEST_STORAGE_KEY } from '../consts'
|
|
3
|
+
import { useActivatePasswordlessOtpMutation } from './mutations'
|
|
4
|
+
import { randomString } from '../internal/utils/randomInt'
|
|
5
|
+
import { calculateHash } from '../internal/utils/hashing'
|
|
6
|
+
import { ActivatePasswordlessOtpErrorCode } from '@contember/graphql-client-tenant'
|
|
7
|
+
|
|
8
|
+
const usePasswordlessParams = (): {localStorageRequestId: string | null; urlRequestId: string | null; urlToken: string | null} => {
|
|
9
|
+
const localStorageValue = localStorage.getItem(PASSWORDLESS_REQUEST_STORAGE_KEY) ?? null
|
|
10
|
+
const urlParams = new URLSearchParams(window.location.search)
|
|
11
|
+
const requestId = urlParams.get('request_id')
|
|
12
|
+
const token = urlParams.get('token')
|
|
13
|
+
|
|
14
|
+
return { localStorageRequestId: localStorageValue, urlRequestId: requestId, urlToken: token }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type PasswordlessResponseHandlerState =
|
|
18
|
+
| { type: 'empty' }
|
|
19
|
+
| { type: 'otp_activating' }
|
|
20
|
+
| { type: 'otp_activated'; otp: string }
|
|
21
|
+
| { type: 'otp_activation_failed'; error: ActivatePasswordlessOtpErrorCode | 'UNKNOWN_ERROR'}
|
|
22
|
+
| { type: 'can_proceed_to_login' }
|
|
23
|
+
|
|
24
|
+
export const usePasswordlessOtpActivator = ({ otpLength = 6, otpChars = '0123456789ABCDEF' }: {
|
|
25
|
+
otpLength?: number
|
|
26
|
+
otpChars?: string
|
|
27
|
+
} = {}) => {
|
|
28
|
+
const { localStorageRequestId, urlRequestId, urlToken } = usePasswordlessParams()
|
|
29
|
+
const activateOtp = useActivatePasswordlessOtpMutation()
|
|
30
|
+
|
|
31
|
+
const [state, setState] = useState<PasswordlessResponseHandlerState>(() => {
|
|
32
|
+
if (urlRequestId && urlToken) {
|
|
33
|
+
if (localStorageRequestId !== urlRequestId) {
|
|
34
|
+
return { type: 'otp_activating' }
|
|
35
|
+
} else {
|
|
36
|
+
return { type: 'can_proceed_to_login' }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { type: 'empty' }
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
(async () => {
|
|
44
|
+
if (state.type === 'otp_activating') {
|
|
45
|
+
const otpCode = randomString(otpLength, otpChars)
|
|
46
|
+
try {
|
|
47
|
+
const result = await activateOtp({
|
|
48
|
+
requestId: urlRequestId!,
|
|
49
|
+
token: urlToken!,
|
|
50
|
+
otpHash: await calculateHash('SHA-256', otpCode),
|
|
51
|
+
})
|
|
52
|
+
if (!result.ok) {
|
|
53
|
+
setState({ type: 'otp_activation_failed', error: result.error })
|
|
54
|
+
} else {
|
|
55
|
+
setState({ type: 'otp_activated', otp: otpCode })
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error(e)
|
|
59
|
+
setState({ type: 'otp_activation_failed', error: 'UNKNOWN_ERROR' })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
})()
|
|
63
|
+
}, [activateOtp, localStorageRequestId, otpChars, otpLength, state.type, urlRequestId, urlToken])
|
|
64
|
+
|
|
65
|
+
return state
|
|
66
|
+
}
|
|
@@ -18,7 +18,7 @@ const headers = {
|
|
|
18
18
|
'X-Contember-Token-Path': 'data.mutation.result.token',
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const DEFAULT_LOGIN_EXPIRATION = 14 * 24 *
|
|
21
|
+
const DEFAULT_LOGIN_EXPIRATION = 14 * 24 * 60 // 14 days
|
|
22
22
|
|
|
23
23
|
export const useHandleIDPResponse = ({ onLogin, expiration = DEFAULT_LOGIN_EXPIRATION, setState, hasOauthResponse }: UseHandleIDPResponseProps): void => {
|
|
24
24
|
const idpSignIn = useSignInIDPMutation({ headers })
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const calculateHash = async (algo: AlgorithmIdentifier, message: string): Promise<string> => {
|
|
2
|
+
const hashBuffer = await crypto.subtle.digest(algo, new TextEncoder().encode(message))
|
|
3
|
+
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
4
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const randomInt = (min: number, max: number) => {
|
|
2
|
+
if (min >= max) {
|
|
3
|
+
throw new Error('The min value must be less than the max value.')
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (max > 0xFFFFFFFF) {
|
|
7
|
+
throw new Error('The max value exceeds the limit for this implementation.')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const range = max - min + 1
|
|
11
|
+
const maxValidValue = Math.floor(0xFFFFFFFF / range) * range
|
|
12
|
+
let randomInt
|
|
13
|
+
|
|
14
|
+
do {
|
|
15
|
+
const array = new Uint32Array(1)
|
|
16
|
+
window.crypto.getRandomValues(array)
|
|
17
|
+
randomInt = array[0]
|
|
18
|
+
} while (randomInt >= maxValidValue)
|
|
19
|
+
|
|
20
|
+
return min + (randomInt % range)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export const randomString = (length: number, charset: string): string => {
|
|
25
|
+
let result = ''
|
|
26
|
+
for (let i = 0; i < length; i++) {
|
|
27
|
+
result += charset[randomInt(0, charset.length - 1)]
|
|
28
|
+
}
|
|
29
|
+
return result
|
|
30
|
+
}
|