@atproto/oauth-provider 0.1.0 → 0.1.2-rc.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.
Files changed (118) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/account/account-manager.d.ts +2 -2
  3. package/dist/account/account-manager.d.ts.map +1 -1
  4. package/dist/account/account-manager.js.map +1 -1
  5. package/dist/account/account-store.d.ts +19 -6
  6. package/dist/account/account-store.d.ts.map +1 -1
  7. package/dist/account/account-store.js +16 -1
  8. package/dist/account/account-store.js.map +1 -1
  9. package/dist/assets/app/bundle-manifest.json +3 -3
  10. package/dist/assets/app/main.css +1 -1
  11. package/dist/assets/app/main.js +3 -3
  12. package/dist/assets/app/main.js.map +1 -1
  13. package/dist/client/client-auth.d.ts.map +1 -1
  14. package/dist/client/client-auth.js +2 -2
  15. package/dist/client/client-auth.js.map +1 -1
  16. package/dist/client/client-manager.d.ts.map +1 -1
  17. package/dist/client/client-manager.js +3 -1
  18. package/dist/client/client-manager.js.map +1 -1
  19. package/dist/client/client.d.ts.map +1 -1
  20. package/dist/client/client.js +3 -3
  21. package/dist/client/client.js.map +1 -1
  22. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  23. package/dist/dpop/dpop-manager.js +3 -3
  24. package/dist/dpop/dpop-manager.js.map +1 -1
  25. package/dist/errors/invalid-token-error.d.ts.map +1 -1
  26. package/dist/errors/invalid-token-error.js +3 -2
  27. package/dist/errors/invalid-token-error.js.map +1 -1
  28. package/dist/errors/second-authentication-factor-required-error.d.ts +13 -0
  29. package/dist/errors/second-authentication-factor-required-error.d.ts.map +1 -0
  30. package/dist/errors/second-authentication-factor-required-error.js +23 -0
  31. package/dist/errors/second-authentication-factor-required-error.js.map +1 -0
  32. package/dist/lib/util/authorization-header.js +1 -1
  33. package/dist/lib/util/authorization-header.js.map +1 -1
  34. package/dist/metadata/build-metadata.d.ts.map +1 -1
  35. package/dist/metadata/build-metadata.js +2 -0
  36. package/dist/metadata/build-metadata.js.map +1 -1
  37. package/dist/oauth-errors.d.ts +1 -0
  38. package/dist/oauth-errors.d.ts.map +1 -1
  39. package/dist/oauth-errors.js +3 -1
  40. package/dist/oauth-errors.js.map +1 -1
  41. package/dist/oauth-provider.d.ts +101 -4
  42. package/dist/oauth-provider.d.ts.map +1 -1
  43. package/dist/oauth-provider.js +98 -110
  44. package/dist/oauth-provider.js.map +1 -1
  45. package/dist/output/build-authorize-data.d.ts +40 -0
  46. package/dist/output/build-authorize-data.d.ts.map +1 -0
  47. package/dist/output/build-authorize-data.js +22 -0
  48. package/dist/output/build-authorize-data.js.map +1 -0
  49. package/dist/output/build-error-payload.d.ts.map +1 -1
  50. package/dist/output/build-error-payload.js +4 -3
  51. package/dist/output/build-error-payload.js.map +1 -1
  52. package/dist/output/customization.d.ts +2 -12
  53. package/dist/output/customization.d.ts.map +1 -1
  54. package/dist/output/customization.js +59 -32
  55. package/dist/output/customization.js.map +1 -1
  56. package/dist/output/output-manager.d.ts +16 -0
  57. package/dist/output/output-manager.d.ts.map +1 -0
  58. package/dist/output/output-manager.js +69 -0
  59. package/dist/output/output-manager.js.map +1 -0
  60. package/dist/output/send-web-page.d.ts +1 -1
  61. package/dist/output/send-web-page.d.ts.map +1 -1
  62. package/dist/output/send-web-page.js +3 -2
  63. package/dist/output/send-web-page.js.map +1 -1
  64. package/package.json +7 -7
  65. package/src/account/account-manager.ts +2 -2
  66. package/src/account/account-store.ts +12 -6
  67. package/src/assets/app/components/accept-form.tsx +86 -83
  68. package/src/assets/app/components/account-picker.tsx +98 -79
  69. package/src/assets/app/components/button.tsx +34 -0
  70. package/src/assets/app/components/client-identifier.tsx +12 -13
  71. package/src/assets/app/components/fieldset.tsx +26 -0
  72. package/src/assets/app/components/form-card.tsx +47 -0
  73. package/src/assets/app/components/help-card.tsx +1 -1
  74. package/src/assets/app/components/icons/alert-icon.tsx +5 -0
  75. package/src/assets/app/components/icons/at-symbol-icon.tsx +5 -0
  76. package/src/assets/app/components/icons/caret-right-icon.tsx +5 -0
  77. package/src/assets/app/components/icons/lock-icon.tsx +5 -0
  78. package/src/assets/app/components/icons/token-icon.tsx +5 -0
  79. package/src/assets/app/components/icons/util.tsx +17 -0
  80. package/src/assets/app/components/info-card.tsx +45 -0
  81. package/src/assets/app/components/input-checkbox.tsx +47 -0
  82. package/src/assets/app/components/input-container.tsx +37 -0
  83. package/src/assets/app/components/input-layout.tsx +47 -0
  84. package/src/assets/app/components/input-text.tsx +69 -0
  85. package/src/assets/app/components/layout-title-page.tsx +33 -16
  86. package/src/assets/app/components/layout-welcome.tsx +30 -14
  87. package/src/assets/app/components/sign-in-form.tsx +214 -196
  88. package/src/assets/app/components/sign-up-account-form.tsx +101 -117
  89. package/src/assets/app/components/sign-up-disclaimer.tsx +1 -1
  90. package/src/assets/app/hooks/use-api.ts +2 -0
  91. package/src/assets/app/lib/api.ts +49 -14
  92. package/src/assets/app/lib/clsx.ts +6 -1
  93. package/src/assets/app/lib/util.ts +3 -0
  94. package/src/assets/app/main.css +2 -1
  95. package/src/assets/app/views/accept-view.tsx +4 -3
  96. package/src/assets/app/views/authorize-view.tsx +8 -4
  97. package/src/assets/app/views/error-view.tsx +24 -15
  98. package/src/assets/app/views/sign-in-view.tsx +5 -15
  99. package/src/assets/app/views/sign-up-view.tsx +3 -10
  100. package/src/assets/app/views/welcome-view.tsx +11 -18
  101. package/src/client/client-auth.ts +3 -2
  102. package/src/client/client-manager.ts +2 -1
  103. package/src/client/client.ts +3 -1
  104. package/src/dpop/dpop-manager.ts +3 -2
  105. package/src/errors/invalid-token-error.ts +3 -1
  106. package/src/errors/second-authentication-factor-required-error.ts +25 -0
  107. package/src/lib/util/authorization-header.ts +1 -1
  108. package/src/metadata/build-metadata.ts +3 -0
  109. package/src/oauth-errors.ts +1 -0
  110. package/src/oauth-provider.ts +110 -99
  111. package/src/output/{send-authorize-page.ts → build-authorize-data.ts} +3 -43
  112. package/src/output/build-error-payload.ts +3 -1
  113. package/src/output/customization.ts +67 -45
  114. package/src/output/output-manager.ts +87 -0
  115. package/src/output/send-web-page.ts +4 -3
  116. package/tailwind.config.js +14 -1
  117. package/src/assets/app/components/error-card.tsx +0 -41
  118. package/src/output/send-error-page.ts +0 -41
@@ -1,43 +1,60 @@
1
1
  import { HTMLAttributes, ReactNode } from 'react'
2
2
  import { clsx } from '../lib/clsx'
3
+ import { Override } from '../lib/util'
3
4
 
4
- export type LayoutTitlePageProps = {
5
- title?: ReactNode
6
- subtitle?: ReactNode
7
- }
5
+ export type LayoutTitlePageProps = Override<
6
+ HTMLAttributes<HTMLDivElement>,
7
+ {
8
+ title?: ReactNode
9
+ subtitle?: ReactNode
10
+ }
11
+ >
8
12
 
9
13
  export function LayoutTitlePage({
10
14
  children,
11
15
  title,
12
16
  subtitle,
13
- ...attrs
14
- }: LayoutTitlePageProps &
15
- Omit<HTMLAttributes<HTMLDivElement>, keyof LayoutTitlePageProps>) {
17
+ className,
18
+ ...props
19
+ }: LayoutTitlePageProps) {
16
20
  return (
17
21
  <div
18
- {...attrs}
22
+ {...props}
19
23
  className={clsx(
20
- attrs.className,
21
- 'flex justify-center items-stretch min-h-screen bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100',
24
+ className,
25
+ 'flex flex-col items-center',
26
+ 'md:flex md:flex-row md:justify-stretch md:items-center',
27
+ 'min-h-screen min-w-screen',
28
+ 'bg-white text-slate-900',
29
+ 'dark:bg-slate-900 dark:text-slate-100',
22
30
  )}
23
31
  >
24
- <div className="w-1/2 hidden p-4 md:grid content-center justify-items-end text-right dark:bg-transparent dark:border-r bg-slate-100 dark:bg-slate-800 dark:border-slate-700">
32
+ <div
33
+ className={clsx(
34
+ 'px-6 pt-4',
35
+ 'md:max-w-lg',
36
+ 'md:grid md:content-center md:justify-items-end',
37
+ 'md:self-stretch',
38
+ 'md:w-1/2 md:max-w-fix md:p-4',
39
+ 'md:text-right',
40
+ 'md:dark:border-r md:dark:border-slate-700',
41
+ 'md:bg-slate-100 md:dark:bg-slate-800',
42
+ )}
43
+ >
25
44
  {title && (
26
- <h1 className="text-3xl lg:text-5xl mt-4 font-semibold mb-4 text-primary">
45
+ <h1 className="text-xl md:text-2xl lg:text-5xl md:mt-4 mb-4 font-semibold text-brand">
27
46
  {title}
28
47
  </h1>
29
48
  )}
30
49
 
31
50
  {subtitle && (
32
- <p className="max-w-xs text-slate-500 dark:text-slate-500">
51
+ <p className="hidden md:block max-w-xs text-slate-500 dark:text-slate-500">
33
52
  {subtitle}
34
53
  </p>
35
54
  )}
36
55
  </div>
37
56
 
38
- <div className="flex items-stretch md:items-center w-full justify-center px-6 md:justify-start md:px-12">
39
- {children}
40
- </div>
57
+ <div className="w-full px-6 md:max-w-3xl md:px-12">{children}</div>
41
58
  </div>
42
59
  )
43
60
  }
@@ -1,15 +1,20 @@
1
- import { PropsWithChildren } from 'react'
1
+ import { HTMLAttributes } from 'react'
2
+ import { Override } from '../lib/util'
3
+ import { clsx } from '../lib/clsx'
2
4
 
3
- export type LayoutWelcomeProps = {
4
- name?: string
5
- logo?: string
6
- links?: Array<{
7
- title: string
8
- href: string
9
- rel?: string
10
- }>
11
- logoAlt?: string
12
- }
5
+ export type LayoutWelcomeProps = Override<
6
+ HTMLAttributes<HTMLDivElement>,
7
+ {
8
+ name?: string
9
+ logo?: string
10
+ links?: Array<{
11
+ title: string
12
+ href: string
13
+ rel?: string
14
+ }>
15
+ logoAlt?: string
16
+ }
17
+ >
13
18
 
14
19
  export function LayoutWelcome({
15
20
  name,
@@ -17,9 +22,20 @@ export function LayoutWelcome({
17
22
  logoAlt = name || 'Logo',
18
23
  links,
19
24
  children,
20
- }: PropsWithChildren<LayoutWelcomeProps>) {
25
+ className,
26
+ ...props
27
+ }: LayoutWelcomeProps) {
21
28
  return (
22
- <div className="min-h-screen w-full flex items-center justify-center flex-col bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100">
29
+ <div
30
+ className={clsx(
31
+ 'min-h-screen w-full',
32
+ 'flex items-center justify-center flex-col',
33
+ 'bg-white text-slate-900',
34
+ 'dark:bg-slate-900 dark:text-slate-100',
35
+ className,
36
+ )}
37
+ {...props}
38
+ >
23
39
  <div className="w-full max-w-screen-sm overflow-hidden flex-grow flex flex-col items-center justify-center">
24
40
  {logo && (
25
41
  <img
@@ -46,7 +62,7 @@ export function LayoutWelcome({
46
62
  href={link.href}
47
63
  rel={link.rel}
48
64
  target="_blank"
49
- className="m-2 md:m-4 text-xs md:text-sm text-primary hover:underline"
65
+ className="m-2 md:m-4 text-xs md:text-sm text-brand hover:underline"
50
66
  >
51
67
  {link.title}
52
68
  </a>
@@ -1,13 +1,20 @@
1
- import {
2
- FormHTMLAttributes,
3
- ReactNode,
4
- SyntheticEvent,
5
- useCallback,
6
- useState,
7
- } from 'react'
1
+ import { ReactNode, SyntheticEvent, useCallback, useState } from 'react'
8
2
 
3
+ import {
4
+ InvalidCredentialsError,
5
+ SecondAuthenticationFactorRequiredError,
6
+ } from '../lib/api'
9
7
  import { clsx } from '../lib/clsx'
10
- import { ErrorCard } from './error-card'
8
+ import { Override } from '../lib/util'
9
+ import { Button } from './button'
10
+ import { FormCard, FormCardProps } from './form-card'
11
+ import { AtSymbolIcon } from './icons/at-symbol-icon'
12
+ import { LockIcon } from './icons/lock-icon'
13
+ import { InfoCard } from './info-card'
14
+ import { InputCheckbox } from './input-checkbox'
15
+ import { InputText } from './input-text'
16
+ import { TokenIcon } from './icons/token-icon'
17
+ import { Fieldset } from './fieldset'
11
18
 
12
19
  export type SignInFormOutput = {
13
20
  username: string
@@ -15,41 +22,51 @@ export type SignInFormOutput = {
15
22
  remember?: boolean
16
23
  }
17
24
 
18
- export type SignInFormProps = {
19
- title?: ReactNode
25
+ export type SignInFormProps = Override<
26
+ FormCardProps,
27
+ {
28
+ onSubmit: (credentials: SignInFormOutput) => void | PromiseLike<void>
29
+ submitLabel?: ReactNode
30
+ submitAria?: string
20
31
 
21
- onSubmit: (credentials: SignInFormOutput) => void | PromiseLike<void>
22
- submitLabel?: ReactNode
23
- submitAria?: string
32
+ onCancel?: () => void
33
+ cancelLabel?: ReactNode
34
+ cancelAria?: string
24
35
 
25
- onCancel?: () => void
26
- cancelLabel?: ReactNode
27
- cancelAria?: string
36
+ accountSection?: ReactNode
37
+ sessionSection?: ReactNode
38
+ secondFactorSection?: ReactNode
28
39
 
29
- usernameDefault?: string
30
- usernameReadonly?: boolean
31
- usernameLabel?: string
32
- usernamePlaceholder?: string
33
- usernameAria?: string
34
- usernamePattern?: string
35
- usernameTitle?: string
40
+ usernameDefault?: string
41
+ usernameReadonly?: boolean
42
+ usernameLabel?: string
43
+ usernamePlaceholder?: string
44
+ usernameAria?: string
45
+ usernamePattern?: string
46
+ usernameFormat?: string
36
47
 
37
- passwordLabel?: string
38
- passwordPlaceholder?: string
39
- passwordWarning?: ReactNode
40
- passwordAria?: string
41
- passwordPattern?: string
42
- passwordTitle?: string
48
+ passwordLabel?: string
49
+ passwordPlaceholder?: string
50
+ passwordWarning?: ReactNode
51
+ passwordAria?: string
52
+ passwordPattern?: string
53
+ passwordFormat?: string
43
54
 
44
- rememberVisible?: boolean
45
- rememberDefault?: boolean
46
- rememberLabel?: string
47
- rememberAria?: string
48
- }
55
+ secondFactorLabel?: string
56
+ secondFactorPlaceholder?: string
57
+ secondFactorAria?: string
58
+ secondFactorPattern?: string
59
+ secondFactorFormat?: string
60
+ secondFactorHint?: string
49
61
 
50
- export function SignInForm({
51
- title = 'Sign in',
62
+ rememberVisible?: boolean
63
+ rememberDefault?: boolean
64
+ rememberLabel?: string
65
+ rememberAria?: string
66
+ }
67
+ >
52
68
 
69
+ export function SignInForm({
53
70
  onSubmit,
54
71
  submitAria = 'Next',
55
72
  submitLabel = submitAria,
@@ -58,45 +75,63 @@ export function SignInForm({
58
75
  cancelAria = 'Cancel',
59
76
  cancelLabel = cancelAria,
60
77
 
78
+ accountSection = 'Account',
79
+ sessionSection = 'Session',
80
+ secondFactorSection = '2FA Confirmation',
81
+
61
82
  usernameDefault = '',
62
83
  usernameReadonly = false,
63
- usernameLabel = 'Email address or handle',
84
+ usernameLabel = 'Username or email address',
64
85
  usernameAria = usernameLabel,
65
86
  usernamePlaceholder = usernameLabel,
66
- usernamePattern,
67
- usernameTitle = 'Username must not be empty',
87
+ usernamePattern = undefined,
88
+ usernameFormat = 'valid email address or username',
68
89
 
69
90
  passwordLabel = 'Password',
70
91
  passwordAria = passwordLabel,
71
92
  passwordPlaceholder = passwordLabel,
72
- passwordPattern,
73
- passwordTitle = 'Password must not be empty',
93
+ passwordPattern = undefined,
94
+ passwordFormat = 'non empty string',
74
95
  passwordWarning = (
75
96
  <>
76
- <p className="font-bold">Warning</p>
77
- <p className="text-sm">
97
+ <p className="font-bold text-brand leading-8">Warning</p>
98
+ <p>
78
99
  Please verify the domain name of the website before entering your
79
100
  password. Never enter your password on a domain you do not trust.
80
101
  </p>
81
102
  </>
82
103
  ),
83
104
 
105
+ secondFactorLabel = 'Confirmation code',
106
+ secondFactorAria = secondFactorLabel,
107
+ secondFactorPlaceholder = secondFactorLabel,
108
+ secondFactorPattern = '^[A-Z0-9]{5}-[A-Z0-9]{5}$',
109
+ secondFactorFormat = 'XXXXX-XXXXX',
110
+ secondFactorHint = 'Check your $1 email for a login code and enter it here.',
111
+
84
112
  rememberVisible = true,
85
113
  rememberDefault = false,
86
114
  rememberLabel = 'Remember this account on this device',
87
115
  rememberAria = rememberLabel,
88
116
 
89
- className,
90
- ...attrs
91
- }: SignInFormProps &
92
- Omit<
93
- FormHTMLAttributes<HTMLFormElement>,
94
- keyof SignInFormProps | 'children'
95
- >) {
117
+ ...props
118
+ }: SignInFormProps) {
96
119
  const [focused, setFocused] = useState(false)
97
120
  const [loading, setLoading] = useState(false)
121
+ const [secondFactor, setSecondFactor] = useState<null | {
122
+ type: 'emailOtp'
123
+ hint: string
124
+ }>(null)
125
+
98
126
  const [errorMessage, setErrorMessage] = useState<string | null>(null)
99
127
 
128
+ const resetState = useCallback(() => {
129
+ setSecondFactor(null)
130
+ setErrorMessage(null)
131
+ }, [])
132
+
133
+ const passwordReadonly = secondFactor != null
134
+
100
135
  const doSubmit = useCallback(
101
136
  async (
102
137
  event: SyntheticEvent<
@@ -104,187 +139,170 @@ export function SignInForm({
104
139
  username: HTMLInputElement
105
140
  password: HTMLInputElement
106
141
  remember?: HTMLInputElement
142
+ secondFactor?: HTMLInputElement
107
143
  },
108
144
  SubmitEvent
109
145
  >,
110
146
  ) => {
111
147
  event.preventDefault()
112
148
 
113
- const credentials = {
149
+ const credentials: SignInFormOutput = {
114
150
  username: event.currentTarget.username.value,
115
151
  password: event.currentTarget.password.value,
116
152
  remember: event.currentTarget.remember?.checked,
117
153
  }
118
154
 
155
+ if (secondFactor) {
156
+ const element = event.currentTarget.secondFactor
157
+ if (!element) throw new Error('Second factor input not found')
158
+ credentials[secondFactor.type] = element.value
159
+ }
160
+
119
161
  setLoading(true)
120
162
  setErrorMessage(null)
121
163
  try {
122
164
  await onSubmit(credentials)
123
165
  } catch (err) {
124
- setErrorMessage(parseErrorMessage(err))
166
+ if (err instanceof SecondAuthenticationFactorRequiredError) {
167
+ setSecondFactor({
168
+ type: err.type,
169
+ hint: err.hint,
170
+ })
171
+ } else {
172
+ setErrorMessage(parseErrorMessage(err))
173
+ }
125
174
  } finally {
126
175
  setLoading(false)
127
176
  }
128
177
  },
129
- [onSubmit, setErrorMessage, setLoading],
178
+ [secondFactor, onSubmit],
130
179
  )
131
180
 
132
181
  return (
133
- <form
134
- {...attrs}
135
- className={clsx('flex flex-col', className)}
182
+ <FormCard
136
183
  onSubmit={doSubmit}
137
- >
138
- <p className="font-medium p-4">{title}</p>
139
- <fieldset
140
- className="rounded-md border border-solid border-slate-200 dark:border-slate-700 text-neutral-700 dark:text-neutral-100"
141
- disabled={loading}
142
- >
143
- <div className="relative p-1 flex flex-wrap items-center justify-stretch">
144
- <span className="w-8 text-center text-base leading-[1.6]">@</span>
145
- <input
146
- name="username"
147
- type="text"
148
- onChange={() => setErrorMessage(null)}
149
- className="relative m-0 block w-[1px] min-w-0 flex-auto px-3 py-[0.25rem] leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100 disabled:text-gray-500"
150
- placeholder={usernamePlaceholder}
151
- aria-label={usernameAria}
152
- autoCapitalize="none"
153
- autoCorrect="off"
154
- autoComplete="username"
155
- spellCheck="false"
156
- dir="auto"
157
- enterKeyHint="next"
158
- required
159
- defaultValue={usernameDefault}
160
- readOnly={usernameReadonly}
161
- disabled={usernameReadonly}
162
- pattern={usernamePattern}
163
- title={usernameTitle}
164
- />
165
- </div>
166
-
167
- <hr className="border-slate-200 dark:border-slate-700" />
168
-
169
- <div className="relative p-1 flex flex-wrap items-center justify-stretch">
170
- <span className="w-8 text-center text-2xl leading-[1.6]">*</span>
171
- <input
172
- name="password"
173
- type="password"
174
- onChange={() => setErrorMessage(null)}
175
- onFocus={() => setFocused(true)}
176
- onBlur={() => setTimeout(setFocused, 100, false)}
177
- className="relative m-0 block w-[1px] min-w-0 flex-auto px-3 py-[0.25rem] leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100"
178
- placeholder={passwordPlaceholder}
179
- aria-label={passwordAria}
180
- autoCapitalize="none"
181
- autoCorrect="off"
182
- autoComplete="current-password"
183
- dir="auto"
184
- enterKeyHint="done"
185
- spellCheck="false"
186
- required
187
- pattern={passwordPattern}
188
- title={passwordTitle}
189
- />
190
- </div>
191
-
192
- {passwordWarning && (
193
- <>
194
- <hr
195
- className="border-slate-200 dark:border-slate-700 transition-all"
196
- style={{ borderTopWidth: focused ? '1px' : '0px' }}
197
- />
198
- <div
199
- className="bg-slate-100 dark:bg-slate-800 overflow-hidden transition-all"
200
- style={{
201
- display: 'grid',
202
- gridTemplateRows: focused ? '1fr' : '0fr',
203
- }}
204
- >
205
- <div className="flex items-center justify-start overflow-hidden">
206
- <div className="py-1 px-2">
207
- <svg
208
- className="fill-current h-4 w-4 text-error"
209
- xmlns="http://www.w3.org/2000/svg"
210
- viewBox="0 0 20 20"
211
- >
212
- <path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM9 11V9h2v6H9v-4zm0-6h2v2H9V5z" />
213
- </svg>
214
- </div>
215
- <div className="py-2 px-4">{passwordWarning}</div>
216
- </div>
217
- </div>
218
- </>
219
- )}
220
-
221
- {rememberVisible && (
222
- <>
223
- <hr className="border-slate-200 dark:border-slate-700" />
224
-
225
- <div className="relative p-1 flex flex-wrap items-center justify-stretch">
226
- <span className="w-8 flex items-center justify-center">
227
- <input
228
- className="text-primary"
229
- id="remember"
230
- name="remember"
231
- type="checkbox"
232
- defaultChecked={rememberDefault}
233
- aria-label={rememberAria}
234
- onChange={() => setErrorMessage(null)}
235
- />
236
- </span>
237
-
238
- <label
239
- htmlFor="remember"
240
- className="relative m-0 block w-[1px] min-w-0 flex-auto px-3 py-[0.25rem] leading-[1.6]"
241
- >
242
- {rememberLabel}
243
- </label>
244
- </div>
245
- </>
246
- )}
247
- </fieldset>
248
-
249
- {errorMessage && <ErrorCard className="mt-4" message={errorMessage} />}
250
-
251
- <div className="flex-auto" />
252
-
253
- <div className="p-4 flex flex-wrap items-center justify-start">
254
- <button
255
- className="py-2 bg-transparent text-primary rounded-md font-semibold order-last"
184
+ error={errorMessage}
185
+ cancel={
186
+ onCancel && (
187
+ <Button aria-label={cancelAria} onClick={onCancel}>
188
+ {cancelLabel}
189
+ </Button>
190
+ )
191
+ }
192
+ actions={
193
+ <Button
194
+ color="brand"
256
195
  type="submit"
257
- role="Button"
258
196
  aria-label={submitAria}
259
- disabled={loading}
197
+ loading={loading}
260
198
  >
261
199
  {submitLabel}
262
- </button>
200
+ </Button>
201
+ }
202
+ {...props}
203
+ >
204
+ <Fieldset title={accountSection} disabled={loading}>
205
+ <InputText
206
+ icon={<AtSymbolIcon className="w-5" />}
207
+ name="username"
208
+ type="text"
209
+ onChange={resetState}
210
+ placeholder={usernamePlaceholder}
211
+ aria-label={usernameAria}
212
+ autoCapitalize="none"
213
+ autoCorrect="off"
214
+ autoComplete="username"
215
+ spellCheck="false"
216
+ dir="auto"
217
+ enterKeyHint="next"
218
+ required
219
+ defaultValue={usernameDefault}
220
+ readOnly={usernameReadonly}
221
+ disabled={usernameReadonly}
222
+ pattern={usernamePattern}
223
+ title={usernameFormat}
224
+ />
225
+
226
+ <InputText
227
+ icon={<LockIcon className="w-5" />}
228
+ name="password"
229
+ type="password"
230
+ onChange={resetState}
231
+ onFocus={() => setFocused(true)}
232
+ onBlur={() => setTimeout(setFocused, 100, false)}
233
+ placeholder={passwordPlaceholder}
234
+ aria-label={passwordAria}
235
+ autoCapitalize="none"
236
+ autoCorrect="off"
237
+ autoComplete="current-password"
238
+ dir="auto"
239
+ enterKeyHint="done"
240
+ spellCheck="false"
241
+ required
242
+ readOnly={passwordReadonly}
243
+ disabled={passwordReadonly}
244
+ pattern={passwordPattern}
245
+ title={passwordFormat}
246
+ />
263
247
 
264
- {onCancel && (
265
- <button
266
- className="py-2 bg-transparent text-primary rounded-md font-light"
267
- type="button"
268
- role="Button"
269
- aria-label={cancelAria}
270
- onClick={onCancel}
248
+ {passwordWarning && (
249
+ <div
250
+ className={clsx(
251
+ 'transition-all delay-300 duration-300 overflow-hidden',
252
+ focused ? 'max-h-80' : 'max-h-0 -z-10 !mt-0',
253
+ )}
271
254
  >
272
- {cancelLabel}
273
- </button>
255
+ <InfoCard role="status">{passwordWarning}</InfoCard>
256
+ </div>
274
257
  )}
258
+ </Fieldset>
275
259
 
276
- <div className="flex-auto" />
277
- </div>
278
- </form>
260
+ {rememberVisible && (
261
+ <Fieldset key="remember" title={sessionSection} disabled={loading}>
262
+ <InputCheckbox
263
+ name="remember"
264
+ defaultChecked={rememberDefault}
265
+ aria-label={rememberAria}
266
+ >
267
+ {rememberLabel}
268
+ </InputCheckbox>
269
+ </Fieldset>
270
+ )}
271
+
272
+ {secondFactor && (
273
+ <Fieldset key="2fa" title={secondFactorSection} disabled={loading}>
274
+ <div>
275
+ <InputText
276
+ icon={<TokenIcon className="w-5" />}
277
+ name="secondFactor"
278
+ type="text"
279
+ placeholder={secondFactorPlaceholder}
280
+ aria-label={secondFactorAria}
281
+ autoCapitalize="none"
282
+ autoCorrect="off"
283
+ autoComplete="off"
284
+ spellCheck="false"
285
+ dir="auto"
286
+ enterKeyHint="done"
287
+ required
288
+ pattern={secondFactorPattern}
289
+ title={secondFactorFormat}
290
+ autoFocus={true}
291
+ />
292
+ <p className="text-slate-600 dark:text-slate-400 text-sm">
293
+ {secondFactorHint.replaceAll('$1', secondFactor.hint)}
294
+ </p>
295
+ </div>
296
+ </Fieldset>
297
+ )}
298
+ </FormCard>
279
299
  )
280
300
  }
281
301
 
282
302
  function parseErrorMessage(err: unknown): string {
283
- console.error('Sign-in failed:', err)
284
- switch ((err as any)?.message) {
285
- case 'Invalid credentials':
286
- return 'Invalid username or password'
287
- default:
288
- return 'An unknown error occurred'
303
+ if (err instanceof InvalidCredentialsError) {
304
+ return 'Invalid username or password'
289
305
  }
306
+
307
+ return 'An unknown error occurred'
290
308
  }