@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.
Files changed (182) hide show
  1. package/dist/development/index.cjs +18 -0
  2. package/dist/development/index.cjs.map +1 -1
  3. package/dist/development/index.js +18 -0
  4. package/dist/development/index.js.map +1 -1
  5. package/dist/development/src/components/forms/LoginForm.cjs +1 -1
  6. package/dist/development/src/components/forms/LoginForm.cjs.map +1 -1
  7. package/dist/development/src/components/forms/LoginForm.js +1 -1
  8. package/dist/development/src/components/forms/LoginForm.js.map +1 -1
  9. package/dist/development/src/components/forms/PasswordResetForm.cjs +2 -1
  10. package/dist/development/src/components/forms/PasswordResetForm.cjs.map +1 -1
  11. package/dist/development/src/components/forms/PasswordResetForm.js +2 -1
  12. package/dist/development/src/components/forms/PasswordResetForm.js.map +1 -1
  13. package/dist/development/src/components/forms/PasswordlessSignInForm.cjs +79 -0
  14. package/dist/development/src/components/forms/PasswordlessSignInForm.cjs.map +1 -0
  15. package/dist/development/src/components/forms/PasswordlessSignInForm.js +79 -0
  16. package/dist/development/src/components/forms/PasswordlessSignInForm.js.map +1 -0
  17. package/dist/development/src/components/forms/PasswordlessSignInInitForm.cjs +54 -0
  18. package/dist/development/src/components/forms/PasswordlessSignInInitForm.cjs.map +1 -0
  19. package/dist/development/src/components/forms/PasswordlessSignInInitForm.js +54 -0
  20. package/dist/development/src/components/forms/PasswordlessSignInInitForm.js.map +1 -0
  21. package/dist/development/src/consts.cjs +5 -0
  22. package/dist/development/src/consts.cjs.map +1 -0
  23. package/dist/development/src/consts.js +5 -0
  24. package/dist/development/src/consts.js.map +1 -0
  25. package/dist/development/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.cjs +31 -0
  26. package/dist/development/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.cjs.map +1 -0
  27. package/dist/development/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.js +14 -0
  28. package/dist/development/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.js.map +1 -0
  29. package/dist/development/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.cjs +28 -0
  30. package/dist/development/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.cjs.map +1 -0
  31. package/dist/development/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.js +11 -0
  32. package/dist/development/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.js.map +1 -0
  33. package/dist/development/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.cjs +28 -0
  34. package/dist/development/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.cjs.map +1 -0
  35. package/dist/development/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.js +11 -0
  36. package/dist/development/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.js.map +1 -0
  37. package/dist/development/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.cjs +32 -0
  38. package/dist/development/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.cjs.map +1 -0
  39. package/dist/development/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.js +15 -0
  40. package/dist/development/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.js.map +1 -0
  41. package/dist/development/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.cjs +32 -0
  42. package/dist/development/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.cjs.map +1 -0
  43. package/dist/development/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.js +15 -0
  44. package/dist/development/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.js.map +1 -0
  45. package/dist/development/src/hooks/usePasswordlessOtpActivator.cjs +53 -0
  46. package/dist/development/src/hooks/usePasswordlessOtpActivator.cjs.map +1 -0
  47. package/dist/development/src/hooks/usePasswordlessOtpActivator.js +53 -0
  48. package/dist/development/src/hooks/usePasswordlessOtpActivator.js.map +1 -0
  49. package/dist/development/src/internal/hooks/useHandleIDPResponse.cjs +1 -1
  50. package/dist/development/src/internal/hooks/useHandleIDPResponse.cjs.map +1 -1
  51. package/dist/development/src/internal/hooks/useHandleIDPResponse.js +1 -1
  52. package/dist/development/src/internal/hooks/useHandleIDPResponse.js.map +1 -1
  53. package/dist/development/src/internal/utils/hashing.cjs +8 -0
  54. package/dist/development/src/internal/utils/hashing.cjs.map +1 -0
  55. package/dist/development/src/internal/utils/hashing.js +8 -0
  56. package/dist/development/src/internal/utils/hashing.js.map +1 -0
  57. package/dist/development/src/internal/utils/randomInt.cjs +29 -0
  58. package/dist/development/src/internal/utils/randomInt.cjs.map +1 -0
  59. package/dist/development/src/internal/utils/randomInt.js +29 -0
  60. package/dist/development/src/internal/utils/randomInt.js.map +1 -0
  61. package/dist/production/index.cjs +18 -0
  62. package/dist/production/index.cjs.map +1 -1
  63. package/dist/production/index.js +18 -0
  64. package/dist/production/index.js.map +1 -1
  65. package/dist/production/src/components/forms/LoginForm.cjs +1 -1
  66. package/dist/production/src/components/forms/LoginForm.cjs.map +1 -1
  67. package/dist/production/src/components/forms/LoginForm.js +1 -1
  68. package/dist/production/src/components/forms/LoginForm.js.map +1 -1
  69. package/dist/production/src/components/forms/PasswordResetForm.cjs +2 -1
  70. package/dist/production/src/components/forms/PasswordResetForm.cjs.map +1 -1
  71. package/dist/production/src/components/forms/PasswordResetForm.js +2 -1
  72. package/dist/production/src/components/forms/PasswordResetForm.js.map +1 -1
  73. package/dist/production/src/components/forms/PasswordlessSignInForm.cjs +71 -0
  74. package/dist/production/src/components/forms/PasswordlessSignInForm.cjs.map +1 -0
  75. package/dist/production/src/components/forms/PasswordlessSignInForm.js +71 -0
  76. package/dist/production/src/components/forms/PasswordlessSignInForm.js.map +1 -0
  77. package/dist/production/src/components/forms/PasswordlessSignInInitForm.cjs +46 -0
  78. package/dist/production/src/components/forms/PasswordlessSignInInitForm.cjs.map +1 -0
  79. package/dist/production/src/components/forms/PasswordlessSignInInitForm.js +46 -0
  80. package/dist/production/src/components/forms/PasswordlessSignInInitForm.js.map +1 -0
  81. package/dist/production/src/consts.cjs +5 -0
  82. package/dist/production/src/consts.cjs.map +1 -0
  83. package/dist/production/src/consts.js +5 -0
  84. package/dist/production/src/consts.js.map +1 -0
  85. package/dist/production/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.cjs +31 -0
  86. package/dist/production/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.cjs.map +1 -0
  87. package/dist/production/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.js +14 -0
  88. package/dist/production/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.js.map +1 -0
  89. package/dist/production/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.cjs +28 -0
  90. package/dist/production/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.cjs.map +1 -0
  91. package/dist/production/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.js +11 -0
  92. package/dist/production/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.js.map +1 -0
  93. package/dist/production/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.cjs +28 -0
  94. package/dist/production/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.cjs.map +1 -0
  95. package/dist/production/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.js +11 -0
  96. package/dist/production/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.js.map +1 -0
  97. package/dist/production/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.cjs +32 -0
  98. package/dist/production/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.cjs.map +1 -0
  99. package/dist/production/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.js +15 -0
  100. package/dist/production/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.js.map +1 -0
  101. package/dist/production/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.cjs +32 -0
  102. package/dist/production/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.cjs.map +1 -0
  103. package/dist/production/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.js +15 -0
  104. package/dist/production/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.js.map +1 -0
  105. package/dist/production/src/hooks/usePasswordlessOtpActivator.cjs +53 -0
  106. package/dist/production/src/hooks/usePasswordlessOtpActivator.cjs.map +1 -0
  107. package/dist/production/src/hooks/usePasswordlessOtpActivator.js +53 -0
  108. package/dist/production/src/hooks/usePasswordlessOtpActivator.js.map +1 -0
  109. package/dist/production/src/internal/hooks/useHandleIDPResponse.cjs +1 -1
  110. package/dist/production/src/internal/hooks/useHandleIDPResponse.cjs.map +1 -1
  111. package/dist/production/src/internal/hooks/useHandleIDPResponse.js +1 -1
  112. package/dist/production/src/internal/hooks/useHandleIDPResponse.js.map +1 -1
  113. package/dist/production/src/internal/utils/hashing.cjs +8 -0
  114. package/dist/production/src/internal/utils/hashing.cjs.map +1 -0
  115. package/dist/production/src/internal/utils/hashing.js +8 -0
  116. package/dist/production/src/internal/utils/hashing.js.map +1 -0
  117. package/dist/production/src/internal/utils/randomInt.cjs +29 -0
  118. package/dist/production/src/internal/utils/randomInt.cjs.map +1 -0
  119. package/dist/production/src/internal/utils/randomInt.js +29 -0
  120. package/dist/production/src/internal/utils/randomInt.js.map +1 -0
  121. package/dist/types/components/forms/PasswordlessSignInForm.d.ts +22 -0
  122. package/dist/types/components/forms/PasswordlessSignInForm.d.ts.map +1 -0
  123. package/dist/types/components/forms/PasswordlessSignInInitForm.d.ts +20 -0
  124. package/dist/types/components/forms/PasswordlessSignInInitForm.d.ts.map +1 -0
  125. package/dist/types/components/forms/index.d.ts +2 -0
  126. package/dist/types/components/forms/index.d.ts.map +1 -1
  127. package/dist/types/consts.d.ts +2 -0
  128. package/dist/types/consts.d.ts.map +1 -0
  129. package/dist/types/hooks/index.d.ts +1 -0
  130. package/dist/types/hooks/index.d.ts.map +1 -1
  131. package/dist/types/hooks/mutations/apiKey/useCreateSessionTokenMutation.d.ts +6 -0
  132. package/dist/types/hooks/mutations/apiKey/useCreateSessionTokenMutation.d.ts.map +1 -1
  133. package/dist/types/hooks/mutations/auth/useSignInMutation.d.ts +6 -0
  134. package/dist/types/hooks/mutations/auth/useSignInMutation.d.ts.map +1 -1
  135. package/dist/types/hooks/mutations/idp/useSignInIdpMutation.d.ts +6 -0
  136. package/dist/types/hooks/mutations/idp/useSignInIdpMutation.d.ts.map +1 -1
  137. package/dist/types/hooks/mutations/index.d.ts +1 -0
  138. package/dist/types/hooks/mutations/index.d.ts.map +1 -1
  139. package/dist/types/hooks/mutations/invite/useInviteMutation.d.ts +6 -0
  140. package/dist/types/hooks/mutations/invite/useInviteMutation.d.ts.map +1 -1
  141. package/dist/types/hooks/mutations/passwordless/index.d.ts +6 -0
  142. package/dist/types/hooks/mutations/passwordless/index.d.ts.map +1 -0
  143. package/dist/types/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.d.ts +8 -0
  144. package/dist/types/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.d.ts.map +1 -0
  145. package/dist/types/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.d.ts +3 -0
  146. package/dist/types/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.d.ts.map +1 -0
  147. package/dist/types/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.d.ts +3 -0
  148. package/dist/types/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.d.ts.map +1 -0
  149. package/dist/types/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.d.ts +19 -0
  150. package/dist/types/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.d.ts.map +1 -0
  151. package/dist/types/hooks/mutations/passwordless/useSignInPasswordlessMutation.d.ts +42 -0
  152. package/dist/types/hooks/mutations/passwordless/useSignInPasswordlessMutation.d.ts.map +1 -0
  153. package/dist/types/hooks/queries/useMeQuery.d.ts +2 -0
  154. package/dist/types/hooks/queries/useMeQuery.d.ts.map +1 -1
  155. package/dist/types/hooks/queries/useProjectMembersQuery.d.ts +2 -0
  156. package/dist/types/hooks/queries/useProjectMembersQuery.d.ts.map +1 -1
  157. package/dist/types/hooks/usePasswordlessOtpActivator.d.ts +19 -0
  158. package/dist/types/hooks/usePasswordlessOtpActivator.d.ts.map +1 -0
  159. package/dist/types/internal/utils/hashing.d.ts +2 -0
  160. package/dist/types/internal/utils/hashing.d.ts.map +1 -0
  161. package/dist/types/internal/utils/randomInt.d.ts +3 -0
  162. package/dist/types/internal/utils/randomInt.d.ts.map +1 -0
  163. package/dist/types/tsconfig.tsbuildinfo +1 -1
  164. package/package.json +4 -4
  165. package/src/components/forms/LoginForm.tsx +1 -1
  166. package/src/components/forms/PasswordResetForm.tsx +1 -0
  167. package/src/components/forms/PasswordlessSignInForm.tsx +102 -0
  168. package/src/components/forms/PasswordlessSignInInitForm.tsx +68 -0
  169. package/src/components/forms/index.ts +2 -0
  170. package/src/consts.ts +1 -0
  171. package/src/hooks/index.ts +1 -0
  172. package/src/hooks/mutations/index.ts +1 -0
  173. package/src/hooks/mutations/passwordless/index.ts +5 -0
  174. package/src/hooks/mutations/passwordless/useActivatePasswordlessOtpMutation.ts +19 -0
  175. package/src/hooks/mutations/passwordless/useDisableMyPasswordlessMutation.ts +14 -0
  176. package/src/hooks/mutations/passwordless/useEnableMyPasswordlessMutation.ts +14 -0
  177. package/src/hooks/mutations/passwordless/useInitSignInPasswordlessMutation.ts +22 -0
  178. package/src/hooks/mutations/passwordless/useSignInPasswordlessMutation.ts +24 -0
  179. package/src/hooks/usePasswordlessOtpActivator.ts +66 -0
  180. package/src/internal/hooks/useHandleIDPResponse.ts +1 -1
  181. package/src/internal/utils/hashing.ts +4 -0
  182. 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.13",
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.13",
37
- "@contember/react-client": "2.0.0-alpha.13",
38
- "@contember/react-utils": "2.0.0-alpha.13",
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 * 3600 // 14 days
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 })
@@ -77,4 +77,5 @@ const errorToField: Record<TenantApi.ResetPasswordErrorCode, keyof PasswordReset
77
77
  TOKEN_EXPIRED: 'token',
78
78
  TOKEN_NOT_FOUND: 'token',
79
79
  TOKEN_USED: 'token',
80
+ TOKEN_INVALID: 'token',
80
81
  }
@@ -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'
@@ -5,3 +5,4 @@ export * from './useLogout'
5
5
  export * from './useTenantApi'
6
6
  export * from './useTenantQueryLoader'
7
7
  export * from './useTenantMutation'
8
+ export * from './usePasswordlessOtpActivator'
@@ -3,5 +3,6 @@ export * from './auth'
3
3
  export * from './invite'
4
4
  export * from './idp'
5
5
  export * from './memberships'
6
+ export * from './passwordless'
6
7
  export * from './otp'
7
8
  export * from './passwordReset'
@@ -0,0 +1,5 @@
1
+ export * from './useActivatePasswordlessOtpMutation'
2
+ export * from './useDisableMyPasswordlessMutation'
3
+ export * from './useEnableMyPasswordlessMutation'
4
+ export * from './useSignInPasswordlessMutation'
5
+ export * from './useInitSignInPasswordlessMutation'
@@ -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 * 3600 // 14 days
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
+ }