@h-ai/ui 0.1.0-alpha.59 → 0.1.0-alpha.61
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/README.en.md +47 -1
- package/README.ja.md +47 -1
- package/README.md +51 -1
- package/auto-import.d.ts +1 -0
- package/auto-import.js +1 -0
- package/dist/components/scenes/iam/AgreementNotice.svelte +48 -0
- package/dist/components/scenes/iam/AgreementNotice.svelte.d.ts +7 -0
- package/dist/components/scenes/iam/AuthPanel.svelte +123 -0
- package/dist/components/scenes/iam/AuthPanel.svelte.d.ts +4 -0
- package/dist/components/scenes/iam/AuthShell.svelte +11 -2
- package/dist/components/scenes/iam/AuthShell.svelte.d.ts +3 -0
- package/dist/components/scenes/iam/ChangePasswordForm.svelte +9 -3
- package/dist/components/scenes/iam/LoginForm.svelte +130 -53
- package/dist/components/scenes/iam/RegisterForm.svelte +15 -19
- package/dist/components/scenes/iam/ResetPasswordForm.svelte +9 -3
- package/dist/components/scenes/iam/auth-error-message.d.ts +4 -0
- package/dist/components/scenes/iam/auth-error-message.js +33 -0
- package/dist/components/scenes/iam/index.d.ts +1 -0
- package/dist/components/scenes/iam/index.js +1 -0
- package/dist/components/scenes/iam/password-utils.d.ts +4 -0
- package/dist/components/scenes/iam/password-utils.js +27 -4
- package/dist/components/scenes/types.d.ts +121 -0
- package/dist/messages/en-US.json +39 -6
- package/dist/messages/zh-CN.json +39 -6
- package/package.json +2 -2
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
-->
|
|
5
5
|
<script lang='ts'>
|
|
6
6
|
import type { DataAttributes } from '../../../types.js'
|
|
7
|
-
import type { LoginFormData, LoginFormProps } from '../types.js'
|
|
7
|
+
import type { AuthLoginMethod, LoginFormData, LoginFormProps } from '../types.js'
|
|
8
8
|
import { uiM } from '../../../messages.js'
|
|
9
9
|
import { cn, getDataAttributes } from '../../../utils.js'
|
|
10
10
|
import Alert from '../../compounds/Alert.svelte'
|
|
@@ -12,9 +12,15 @@
|
|
|
12
12
|
import Button from '../../primitives/Button.svelte'
|
|
13
13
|
import Checkbox from '../../primitives/Checkbox.svelte'
|
|
14
14
|
import Input from '../../primitives/Input.svelte'
|
|
15
|
+
import AgreementNotice from './AgreementNotice.svelte'
|
|
16
|
+
import { authErrorMessage } from './auth-error-message.js'
|
|
15
17
|
import PasswordInput from './PasswordInput.svelte'
|
|
16
18
|
|
|
17
19
|
const {
|
|
20
|
+
methods = ['password'],
|
|
21
|
+
otpLength = 6,
|
|
22
|
+
resendInterval = 60,
|
|
23
|
+
onsendotp,
|
|
18
24
|
loading = false,
|
|
19
25
|
disabled = false,
|
|
20
26
|
showTitle = false,
|
|
@@ -30,18 +36,65 @@
|
|
|
30
36
|
onsubmit,
|
|
31
37
|
onforgotpassword,
|
|
32
38
|
header,
|
|
39
|
+
extraFields,
|
|
33
40
|
footer,
|
|
34
41
|
...restProps
|
|
35
42
|
}: LoginFormProps & DataAttributes = $props()
|
|
36
43
|
|
|
37
44
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
38
|
-
const hasAgreements = $derived(
|
|
39
|
-
!!(agreements?.userAgreementUrl || agreements?.privacyPolicyUrl),
|
|
40
|
-
)
|
|
41
45
|
|
|
42
46
|
let username = $state('')
|
|
43
47
|
let password = $state('')
|
|
44
48
|
let rememberMe = $state(false)
|
|
49
|
+
let selectedMethod = $state<AuthLoginMethod>('password')
|
|
50
|
+
const method = $derived(methods.includes(selectedMethod) ? selectedMethod : methods[0])
|
|
51
|
+
const isOtp = $derived(method === 'email' || method === 'sms')
|
|
52
|
+
let code = $state('')
|
|
53
|
+
let sending = $state(false)
|
|
54
|
+
let sendError = $state('')
|
|
55
|
+
let resendAt = $state(0)
|
|
56
|
+
let now = $state(Date.now())
|
|
57
|
+
const remaining = $derived(Math.max(0, Math.ceil((resendAt - now) / 1000)))
|
|
58
|
+
// 倒计时只负责 UI 提示;服务端独立校验 resendInterval,刷新页面不能绕过限制。
|
|
59
|
+
$effect(() => {
|
|
60
|
+
if (resendAt <= Date.now())
|
|
61
|
+
return
|
|
62
|
+
const timer = setInterval(() => {
|
|
63
|
+
now = Date.now()
|
|
64
|
+
}, 1000)
|
|
65
|
+
return () => clearInterval(timer)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
function methodLabel(value: AuthLoginMethod): string {
|
|
69
|
+
if (value === 'ldap')
|
|
70
|
+
return uiM('login_method_ldap')
|
|
71
|
+
if (value === 'email')
|
|
72
|
+
return uiM('login_method_email')
|
|
73
|
+
if (value === 'sms')
|
|
74
|
+
return uiM('login_method_sms')
|
|
75
|
+
return uiM('login_method_password')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function sendOtp() {
|
|
79
|
+
if (!onsendotp || sending || remaining || !username.trim())
|
|
80
|
+
return
|
|
81
|
+
sending = true
|
|
82
|
+
sendError = ''
|
|
83
|
+
try {
|
|
84
|
+
const result = await onsendotp(username.trim())
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
sendError = authErrorMessage(result.error)
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
now = Date.now()
|
|
90
|
+
resendAt = now + resendInterval * 1000
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
sendError = uiM('auth_network_error')
|
|
95
|
+
}
|
|
96
|
+
finally { sending = false }
|
|
97
|
+
}
|
|
45
98
|
|
|
46
99
|
const formClass = $derived(
|
|
47
100
|
cn(
|
|
@@ -52,13 +105,15 @@
|
|
|
52
105
|
|
|
53
106
|
async function handleSubmit(e: SubmitEvent) {
|
|
54
107
|
e.preventDefault()
|
|
55
|
-
if (loading || disabled)
|
|
108
|
+
if (loading || disabled || !method)
|
|
56
109
|
return
|
|
57
110
|
|
|
58
111
|
const data: LoginFormData = {
|
|
59
112
|
username,
|
|
60
113
|
password,
|
|
61
114
|
rememberMe,
|
|
115
|
+
method,
|
|
116
|
+
code: isOtp ? code : undefined,
|
|
62
117
|
}
|
|
63
118
|
|
|
64
119
|
await onsubmit?.(data)
|
|
@@ -83,41 +138,73 @@
|
|
|
83
138
|
{/if}
|
|
84
139
|
|
|
85
140
|
<!-- 用户名 -->
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
141
|
+
{#if methods.length > 1}
|
|
142
|
+
<div class='grid grid-cols-2 gap-1 rounded-lg bg-base-200 p-1 sm:flex' aria-label={uiM('login_methods')}>
|
|
143
|
+
{#each methods as value (value)}
|
|
144
|
+
<button type='button' class={cn('flex-1 rounded-md px-2 py-2 text-sm transition-colors', method === value ? 'bg-base-100 font-medium shadow-sm' : 'text-base-content/60')} aria-pressed={method === value} disabled={loading || disabled} onclick={() => {
|
|
145
|
+
selectedMethod = value
|
|
146
|
+
password = ''
|
|
147
|
+
code = ''
|
|
148
|
+
sendError = ''
|
|
149
|
+
}}>
|
|
150
|
+
{methodLabel(value)}
|
|
151
|
+
</button>
|
|
152
|
+
{/each}
|
|
153
|
+
</div>
|
|
154
|
+
{/if}
|
|
155
|
+
{#if !method}
|
|
156
|
+
<Alert variant='warning'>{uiM('auth_unavailable')}</Alert>
|
|
157
|
+
{:else}
|
|
158
|
+
<div class='space-y-1.5'>
|
|
159
|
+
<label class='text-sm font-medium text-base-content/70' for='login-username'>
|
|
160
|
+
{method === 'email' ? uiM('register_email') : method === 'sms' ? uiM('register_phone') : uiM('login_username')}
|
|
161
|
+
</label>
|
|
162
|
+
<Input
|
|
163
|
+
id='login-username'
|
|
164
|
+
name='username'
|
|
165
|
+
type={method === 'email' ? 'email' : method === 'sms' ? 'tel' : 'text'}
|
|
166
|
+
placeholder={method === 'email' ? uiM('register_email_placeholder') : method === 'sms' ? uiM('register_phone_placeholder') : uiM('login_username_placeholder')}
|
|
167
|
+
bind:value={username}
|
|
168
|
+
autocomplete='username'
|
|
169
|
+
{disabled}
|
|
170
|
+
required
|
|
171
|
+
error={errors.username}
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<!-- 密码 -->
|
|
176
|
+
{#if isOtp}
|
|
177
|
+
<div class='space-y-1.5'>
|
|
178
|
+
<label class='text-sm font-medium text-base-content/70' for='login-code'>{uiM('login_code')}</label>
|
|
179
|
+
<div class='flex items-start gap-2'>
|
|
180
|
+
<Input id='login-code' name='code' bind:value={code} autocomplete='one-time-code' inputmode='numeric' maxlength={otpLength} pattern={`[0-9]{${otpLength}}`} placeholder={uiM('login_code_placeholder', { length: otpLength })} required {disabled} />
|
|
181
|
+
<Button type='button' variant='outline' loading={sending} disabled={loading || disabled || !username.trim() || remaining > 0 || !onsendotp} onclick={sendOtp}>
|
|
182
|
+
{remaining ? uiM('login_resend_wait', { seconds: remaining }) : uiM('login_send_code')}
|
|
183
|
+
</Button>
|
|
184
|
+
</div>
|
|
185
|
+
{#if sendError}<p role='alert' class='text-sm text-error'>{sendError}</p>{/if}
|
|
186
|
+
</div>
|
|
187
|
+
{:else}
|
|
188
|
+
<div class='space-y-1.5'>
|
|
189
|
+
<label class='text-sm font-medium text-base-content/70' for='login-password'>
|
|
190
|
+
{uiM('login_password')}
|
|
191
|
+
</label>
|
|
192
|
+
<PasswordInput
|
|
193
|
+
id='login-password'
|
|
194
|
+
bind:value={password}
|
|
195
|
+
placeholder={uiM('login_password_placeholder')}
|
|
196
|
+
autocomplete='current-password'
|
|
197
|
+
{disabled}
|
|
198
|
+
required
|
|
199
|
+
error={errors.password}
|
|
200
|
+
showStrength={false}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
{/if}
|
|
204
|
+
{/if}
|
|
118
205
|
|
|
119
206
|
<!-- 记住我 & 忘记密码 -->
|
|
120
|
-
{#if showRememberMe || showForgotPassword}
|
|
207
|
+
{#if method === 'password' && (showRememberMe || showForgotPassword)}
|
|
121
208
|
<div class='flex items-center justify-between'>
|
|
122
209
|
{#if showRememberMe}
|
|
123
210
|
<Checkbox
|
|
@@ -149,6 +236,9 @@
|
|
|
149
236
|
{/if}
|
|
150
237
|
|
|
151
238
|
<!-- 通用错误 -->
|
|
239
|
+
{#if extraFields}
|
|
240
|
+
<div class='login-form-extra-fields'>{@render extraFields()}</div>
|
|
241
|
+
{/if}
|
|
152
242
|
{#if errors.general}
|
|
153
243
|
<Alert variant='error'>
|
|
154
244
|
{errors.general}
|
|
@@ -160,27 +250,14 @@
|
|
|
160
250
|
type='submit'
|
|
161
251
|
variant='primary'
|
|
162
252
|
{loading}
|
|
163
|
-
disabled={loading || disabled}
|
|
253
|
+
disabled={loading || disabled || !method}
|
|
164
254
|
class='w-full'
|
|
165
255
|
>
|
|
166
256
|
{submitText || uiM('login_submit')}
|
|
167
257
|
</Button>
|
|
168
258
|
|
|
169
259
|
<!-- 协议提示 -->
|
|
170
|
-
{
|
|
171
|
-
<p class='text-xs text-base-content/50 text-center'>
|
|
172
|
-
{uiM('agreement_prefix')}
|
|
173
|
-
{#if agreements?.userAgreementUrl}
|
|
174
|
-
<a href={agreements.userAgreementUrl} target='_blank' rel='noopener noreferrer' class='link link-primary'>{uiM('agreement_user_agreement')}</a>
|
|
175
|
-
{/if}
|
|
176
|
-
{#if agreements?.userAgreementUrl && agreements?.privacyPolicyUrl}
|
|
177
|
-
{uiM('agreement_and')}
|
|
178
|
-
{/if}
|
|
179
|
-
{#if agreements?.privacyPolicyUrl}
|
|
180
|
-
<a href={agreements.privacyPolicyUrl} target='_blank' rel='noopener noreferrer' class='link link-primary'>{uiM('agreement_privacy_policy')}</a>
|
|
181
|
-
{/if}
|
|
182
|
-
</p>
|
|
183
|
-
{/if}
|
|
260
|
+
<AgreementNotice {agreements} />
|
|
184
261
|
|
|
185
262
|
<!-- 自定义底部 -->
|
|
186
263
|
{#if footer}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import Alert from '../../compounds/Alert.svelte'
|
|
11
11
|
import Button from '../../primitives/Button.svelte'
|
|
12
12
|
import Input from '../../primitives/Input.svelte'
|
|
13
|
+
import AgreementNotice from './AgreementNotice.svelte'
|
|
14
|
+
import { passwordPolicyError, passwordPolicyHint } from './password-utils.js'
|
|
13
15
|
import PasswordInput from './PasswordInput.svelte'
|
|
14
16
|
|
|
15
17
|
const {
|
|
@@ -19,6 +21,7 @@
|
|
|
19
21
|
showPasswordStrength = true,
|
|
20
22
|
requireConfirmPassword = true,
|
|
21
23
|
minPasswordLength = 8,
|
|
24
|
+
passwordPolicy,
|
|
22
25
|
fields = ['username', 'email', 'password'],
|
|
23
26
|
showLoginLink = false,
|
|
24
27
|
loginUrl = '/login',
|
|
@@ -28,14 +31,12 @@
|
|
|
28
31
|
errors = {},
|
|
29
32
|
onsubmit,
|
|
30
33
|
header,
|
|
34
|
+
extraFields,
|
|
31
35
|
footer,
|
|
32
36
|
...restProps
|
|
33
37
|
}: RegisterFormProps & DataAttributes = $props()
|
|
34
38
|
|
|
35
39
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
36
|
-
const hasAgreements = $derived(
|
|
37
|
-
!!(agreements?.userAgreementUrl || agreements?.privacyPolicyUrl),
|
|
38
|
-
)
|
|
39
40
|
|
|
40
41
|
// 表单数据
|
|
41
42
|
let username = $state('')
|
|
@@ -45,6 +46,8 @@
|
|
|
45
46
|
let confirmPassword = $state('')
|
|
46
47
|
let nickname = $state('')
|
|
47
48
|
|
|
49
|
+
const policyError = $derived(passwordPolicy && password ? passwordPolicyError(password, passwordPolicy) : '')
|
|
50
|
+
|
|
48
51
|
const formClass = $derived(
|
|
49
52
|
cn(
|
|
50
53
|
'register-form space-y-4',
|
|
@@ -112,7 +115,7 @@
|
|
|
112
115
|
|
|
113
116
|
async function handleSubmit(e: SubmitEvent) {
|
|
114
117
|
e.preventDefault()
|
|
115
|
-
if (loading || disabled)
|
|
118
|
+
if (policyError || loading || disabled)
|
|
116
119
|
return
|
|
117
120
|
if (!passwordsMatch)
|
|
118
121
|
return
|
|
@@ -174,6 +177,9 @@
|
|
|
174
177
|
</div>
|
|
175
178
|
{/if}
|
|
176
179
|
|
|
180
|
+
{#if passwordPolicy}<p class='text-xs text-base-content/60'>{passwordPolicyHint(passwordPolicy)}</p>{/if}
|
|
181
|
+
{#if policyError}<p role='alert' class='text-sm text-error'>{policyError}</p>{/if}
|
|
182
|
+
|
|
177
183
|
<!-- 动态字段 -->
|
|
178
184
|
{#each fields as field (field)}
|
|
179
185
|
{#if field === 'password'}
|
|
@@ -235,6 +241,9 @@
|
|
|
235
241
|
{/each}
|
|
236
242
|
|
|
237
243
|
<!-- 通用错误 -->
|
|
244
|
+
{#if extraFields}
|
|
245
|
+
<div class='register-form-extra-fields'>{@render extraFields()}</div>
|
|
246
|
+
{/if}
|
|
238
247
|
{#if errors.general}
|
|
239
248
|
<Alert variant='error'>
|
|
240
249
|
{errors.general}
|
|
@@ -246,27 +255,14 @@
|
|
|
246
255
|
type='submit'
|
|
247
256
|
variant='primary'
|
|
248
257
|
class='w-full'
|
|
249
|
-
disabled={loading || disabled || !passwordsMatch}
|
|
258
|
+
disabled={loading || disabled || !passwordsMatch || Boolean(policyError)}
|
|
250
259
|
loading={loading}
|
|
251
260
|
>
|
|
252
261
|
{submitText || uiM('register_submit')}
|
|
253
262
|
</Button>
|
|
254
263
|
|
|
255
264
|
<!-- 协议提示 -->
|
|
256
|
-
{
|
|
257
|
-
<p class='text-xs text-base-content/50 text-center'>
|
|
258
|
-
{uiM('agreement_prefix')}
|
|
259
|
-
{#if agreements?.userAgreementUrl}
|
|
260
|
-
<a href={agreements.userAgreementUrl} target='_blank' rel='noopener noreferrer' class='link link-primary'>{uiM('agreement_user_agreement')}</a>
|
|
261
|
-
{/if}
|
|
262
|
-
{#if agreements?.userAgreementUrl && agreements?.privacyPolicyUrl}
|
|
263
|
-
{uiM('agreement_and')}
|
|
264
|
-
{/if}
|
|
265
|
-
{#if agreements?.privacyPolicyUrl}
|
|
266
|
-
<a href={agreements.privacyPolicyUrl} target='_blank' rel='noopener noreferrer' class='link link-primary'>{uiM('agreement_privacy_policy')}</a>
|
|
267
|
-
{/if}
|
|
268
|
-
</p>
|
|
269
|
-
{/if}
|
|
265
|
+
<AgreementNotice {agreements} />
|
|
270
266
|
|
|
271
267
|
<!-- 自定义底部 -->
|
|
272
268
|
{#if footer}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import Alert from '../../compounds/Alert.svelte'
|
|
11
11
|
import Button from '../../primitives/Button.svelte'
|
|
12
12
|
import Input from '../../primitives/Input.svelte'
|
|
13
|
-
import { arePasswordsEqual } from './password-utils.js'
|
|
13
|
+
import { arePasswordsEqual, passwordPolicyError, passwordPolicyHint } from './password-utils.js'
|
|
14
14
|
import PasswordInput from './PasswordInput.svelte'
|
|
15
15
|
|
|
16
16
|
const {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
showCode = true,
|
|
22
22
|
showPasswordStrength = true,
|
|
23
23
|
minPasswordLength = 8,
|
|
24
|
+
passwordPolicy,
|
|
24
25
|
showBackLink = false,
|
|
25
26
|
loginUrl = '/login',
|
|
26
27
|
submitText,
|
|
@@ -35,6 +36,8 @@
|
|
|
35
36
|
let newPassword = $state('')
|
|
36
37
|
let confirmPassword = $state('')
|
|
37
38
|
|
|
39
|
+
const policyError = $derived(passwordPolicy && newPassword ? passwordPolicyError(newPassword, passwordPolicy) : '')
|
|
40
|
+
|
|
38
41
|
const formClass = $derived(
|
|
39
42
|
cn(
|
|
40
43
|
'reset-password-form space-y-4',
|
|
@@ -55,7 +58,7 @@
|
|
|
55
58
|
|
|
56
59
|
async function handleSubmit(e: SubmitEvent) {
|
|
57
60
|
e.preventDefault()
|
|
58
|
-
if (loading || disabled || !canSubmit)
|
|
61
|
+
if (policyError || loading || disabled || !canSubmit)
|
|
59
62
|
return
|
|
60
63
|
|
|
61
64
|
const data: ResetPasswordFormData = {
|
|
@@ -136,12 +139,15 @@
|
|
|
136
139
|
</Alert>
|
|
137
140
|
{/if}
|
|
138
141
|
|
|
142
|
+
{#if passwordPolicy}<p class='text-xs text-base-content/60'>{passwordPolicyHint(passwordPolicy)}</p>{/if}
|
|
143
|
+
{#if policyError}<p role='alert' class='text-sm text-error'>{policyError}</p>{/if}
|
|
144
|
+
|
|
139
145
|
<!-- 提交按钮 -->
|
|
140
146
|
<Button
|
|
141
147
|
type='submit'
|
|
142
148
|
variant='primary'
|
|
143
149
|
{loading}
|
|
144
|
-
disabled={loading || disabled || !canSubmit}
|
|
150
|
+
disabled={loading || disabled || !canSubmit || Boolean(policyError)}
|
|
145
151
|
class='w-full'
|
|
146
152
|
>
|
|
147
153
|
{submitText || uiM('reset_password_submit')}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { uiM } from '../../../messages.js';
|
|
2
|
+
const messages = {
|
|
3
|
+
'hai:iam:001': 'auth_error_credentials',
|
|
4
|
+
'hai:iam:002': 'auth_error_credentials',
|
|
5
|
+
'hai:iam:003': 'auth_error_credentials',
|
|
6
|
+
'hai:iam:004': 'auth_error_disabled',
|
|
7
|
+
'hai:iam:005': 'auth_error_locked',
|
|
8
|
+
'hai:iam:006': 'auth_error_duplicate',
|
|
9
|
+
'hai:iam:007': 'auth_error_password_expired',
|
|
10
|
+
'hai:iam:008': 'auth_error_password_policy',
|
|
11
|
+
'hai:iam:009': 'auth_error_otp',
|
|
12
|
+
'hai:iam:010': 'auth_error_service',
|
|
13
|
+
'hai:iam:011': 'auth_error_otp_wait',
|
|
14
|
+
'hai:iam:012': 'auth_unavailable',
|
|
15
|
+
'hai:iam:013': 'auth_unavailable',
|
|
16
|
+
'hai:iam:014': 'auth_unavailable',
|
|
17
|
+
'hai:iam:019': 'auth_error_reset_token',
|
|
18
|
+
'hai:iam:020': 'auth_error_reset_token',
|
|
19
|
+
'hai:iam:021': 'auth_error_reset_token',
|
|
20
|
+
'hai:iam:022': 'auth_error_otp',
|
|
21
|
+
'hai:iam:023': 'auth_error_busy',
|
|
22
|
+
'hai:iam:024': 'auth_unavailable',
|
|
23
|
+
'hai:iam:301': 'auth_error_service',
|
|
24
|
+
'hai:iam:302': 'auth_error_credentials',
|
|
25
|
+
'hai:iam:303': 'auth_error_service',
|
|
26
|
+
'hai:iam:401': 'auth_error_service',
|
|
27
|
+
'hai:iam:901': 'auth_error_service',
|
|
28
|
+
'hai:iam:999': 'auth_error_service',
|
|
29
|
+
};
|
|
30
|
+
export function authErrorMessage(error) {
|
|
31
|
+
const key = messages[error.code];
|
|
32
|
+
return key ? uiM(key) : error.message;
|
|
33
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 身份认证相关的场景化组件
|
|
5
5
|
* @module index
|
|
6
6
|
*/
|
|
7
|
+
export { default as AuthPanel } from './AuthPanel.svelte';
|
|
7
8
|
export { default as AuthShell } from './AuthShell.svelte';
|
|
8
9
|
export { default as ChangePasswordForm } from './ChangePasswordForm.svelte';
|
|
9
10
|
export { default as ForgotPasswordForm } from './ForgotPasswordForm.svelte';
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
* 规范化密码字符串
|
|
9
9
|
* - 使用 NFKC 统一兼容字符
|
|
10
10
|
*/
|
|
11
|
+
import type { PasswordPolicy } from '../types.js';
|
|
11
12
|
export declare function normalizePassword(value: string): string;
|
|
12
13
|
/**
|
|
13
14
|
* 比较两次密码是否一致
|
|
14
15
|
*/
|
|
15
16
|
export declare function arePasswordsEqual(password: string, confirmPassword: string): boolean;
|
|
17
|
+
/** 与 IAM 密码策略一致的前端提示;服务端仍执行最终校验。 */
|
|
18
|
+
export declare function passwordPolicyError(password: string, policy: PasswordPolicy): string;
|
|
19
|
+
export declare function passwordPolicyHint(policy: PasswordPolicy): string;
|
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
* 密码规范化与一致性比较工具
|
|
5
5
|
* @module password-utils
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
* 规范化密码字符串
|
|
9
|
-
* - 使用 NFKC 统一兼容字符
|
|
10
|
-
*/
|
|
7
|
+
import { uiM } from '../../../messages.js';
|
|
11
8
|
export function normalizePassword(value) {
|
|
12
9
|
return value.normalize('NFKC');
|
|
13
10
|
}
|
|
@@ -17,3 +14,29 @@ export function normalizePassword(value) {
|
|
|
17
14
|
export function arePasswordsEqual(password, confirmPassword) {
|
|
18
15
|
return normalizePassword(password) === normalizePassword(confirmPassword);
|
|
19
16
|
}
|
|
17
|
+
/** 与 IAM 密码策略一致的前端提示;服务端仍执行最终校验。 */
|
|
18
|
+
export function passwordPolicyError(password, policy) {
|
|
19
|
+
if (password.length < policy.minLength || password.length > policy.maxLength)
|
|
20
|
+
return uiM('password_policy_length', { min: policy.minLength, max: policy.maxLength });
|
|
21
|
+
if (policy.requireUppercase && !/[A-Z]/.test(password))
|
|
22
|
+
return uiM('password_policy_uppercase');
|
|
23
|
+
if (policy.requireLowercase && !/[a-z]/.test(password))
|
|
24
|
+
return uiM('password_policy_lowercase');
|
|
25
|
+
if (policy.requireNumber && !/\d/.test(password))
|
|
26
|
+
return uiM('password_policy_number');
|
|
27
|
+
if (policy.requireSpecialChar && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password))
|
|
28
|
+
return uiM('password_policy_special');
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
export function passwordPolicyHint(policy) {
|
|
32
|
+
const requirements = [uiM('password_policy_length', { min: policy.minLength, max: policy.maxLength })];
|
|
33
|
+
if (policy.requireUppercase)
|
|
34
|
+
requirements.push(uiM('password_policy_uppercase'));
|
|
35
|
+
if (policy.requireLowercase)
|
|
36
|
+
requirements.push(uiM('password_policy_lowercase'));
|
|
37
|
+
if (policy.requireNumber)
|
|
38
|
+
requirements.push(uiM('password_policy_number'));
|
|
39
|
+
if (policy.requireSpecialChar)
|
|
40
|
+
requirements.push(uiM('password_policy_special'));
|
|
41
|
+
return requirements.join(' · ');
|
|
42
|
+
}
|