@atproto/oauth-provider 0.1.0 → 0.1.2-rc.0

Sign up to get free protection for your applications and to get access to all the features.
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
  }