@csntgao/uni-base 0.1.1 → 0.1.3
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.md +2 -2
- package/docs//350/264/246/346/210/267/344/270/216/347/231/273/345/275/225/347/273/204/344/273/266/345/212/250/346/200/201/346/216/245/345/205/245/350/257/264/346/230/216.md +3 -3
- package/package.json +1 -1
- package/packages/web-components/package.json +1 -1
- package/packages/web-components/src/user-login.ts +20 -3
- package/packages/web-components/tests/user-login.test.ts +138 -77
- package/restart +4 -0
package/README.md
CHANGED
|
@@ -218,8 +218,8 @@ function applyNotificationRealtimeEvent(rawEvent: unknown) {
|
|
|
218
218
|
headerSubtitle: '手机号验证后进入企业服务平台',
|
|
219
219
|
headerIconUrl: '/assets/login-shield.svg',
|
|
220
220
|
securityHint: '登录凭证仅用于企业工作台认证,请勿向他人透露验证码',
|
|
221
|
-
userAgreementUrl: '/
|
|
222
|
-
privacyAgreementUrl: '/
|
|
221
|
+
userAgreementUrl: 'https://website.hrs100.com/agreement/user-service-agreement.html',
|
|
222
|
+
privacyAgreementUrl: 'https://website.hrs100.com/agreement/qqxb-user-privacy.html',
|
|
223
223
|
forgotPasswordUrl: '/account/forgot-password',
|
|
224
224
|
initialMethod: 'verification-code',
|
|
225
225
|
})
|
|
@@ -139,8 +139,8 @@ userLogin.configure({
|
|
|
139
139
|
headerSubtitle: '手机号验证后进入企业服务平台',
|
|
140
140
|
headerIconUrl: 'https://static.example.com/login-shield.svg',
|
|
141
141
|
securityHint: '登录凭证仅用于企业工作台认证,请勿向他人透露验证码',
|
|
142
|
-
userAgreementUrl: '/
|
|
143
|
-
privacyAgreementUrl: '/
|
|
142
|
+
userAgreementUrl: 'https://website.hrs100.com/agreement/user-service-agreement.html',
|
|
143
|
+
privacyAgreementUrl: 'https://website.hrs100.com/agreement/qqxb-user-privacy.html',
|
|
144
144
|
forgotPasswordUrl: '/account/forgot-password',
|
|
145
145
|
initialMethod: 'verification-code',
|
|
146
146
|
})
|
|
@@ -151,7 +151,7 @@ userLogin.configure({
|
|
|
151
151
|
| `transport` | 是 | 完成登录接口调用;专用 transport 的 `setAccessToken()` 必须原子保存 JWT。 |
|
|
152
152
|
| `applicationName` | 是 | 目标应用稳定名称。 |
|
|
153
153
|
| `headerTitle`、`headerSubtitle`、`headerIconUrl`、`securityHint` | 否 | 外观文案与图标;不得包含凭据。 |
|
|
154
|
-
| `userAgreementUrl`、`privacyAgreementUrl`、`forgotPasswordUrl` | 否 |
|
|
154
|
+
| `userAgreementUrl`、`privacyAgreementUrl`、`forgotPasswordUrl` | 否 | 调用方页面地址;两个协议地址未传时使用亲亲小保官网协议页。 |
|
|
155
155
|
| `initialMethod` | 否 | `password`、`verification-code` 或 `qr-code`。 |
|
|
156
156
|
| `countdownSeconds`、`qrPollIntervalMs`、`initiallyAgreed`、`initiallyRememberSession` | 否 | 固定行为的可选初始值。 |
|
|
157
157
|
|
package/package.json
CHANGED
|
@@ -84,6 +84,13 @@ interface UserLoginBranding {
|
|
|
84
84
|
|
|
85
85
|
type QrViewState = 'idle' | 'loading' | UserLoginQrStatus | 'error'
|
|
86
86
|
|
|
87
|
+
export const USER_LOGIN_QR_LOGIN_ENABLED = false
|
|
88
|
+
|
|
89
|
+
const USER_LOGIN_DEFAULT_USER_AGREEMENT_URL =
|
|
90
|
+
'https://website.hrs100.com/agreement/user-service-agreement.html'
|
|
91
|
+
const USER_LOGIN_DEFAULT_PRIVACY_AGREEMENT_URL =
|
|
92
|
+
'https://website.hrs100.com/agreement/qqxb-user-privacy.html'
|
|
93
|
+
|
|
87
94
|
const EMPTY_ERRORS: FormErrors = {
|
|
88
95
|
mobile: '',
|
|
89
96
|
smsCode: '',
|
|
@@ -921,8 +928,14 @@ export class UniplatUserLogin extends LitElement {
|
|
|
921
928
|
this.config = {
|
|
922
929
|
transport: config.transport,
|
|
923
930
|
applicationName: normalizeApplicationName(config.applicationName),
|
|
924
|
-
userAgreementUrl: safeNavigationUrl(
|
|
925
|
-
|
|
931
|
+
userAgreementUrl: safeNavigationUrl(
|
|
932
|
+
config.userAgreementUrl ?? USER_LOGIN_DEFAULT_USER_AGREEMENT_URL,
|
|
933
|
+
'Agreement URL',
|
|
934
|
+
),
|
|
935
|
+
privacyAgreementUrl: safeNavigationUrl(
|
|
936
|
+
config.privacyAgreementUrl ?? USER_LOGIN_DEFAULT_PRIVACY_AGREEMENT_URL,
|
|
937
|
+
'Agreement URL',
|
|
938
|
+
),
|
|
926
939
|
forgotPasswordUrl: safeNavigationUrl(config.forgotPasswordUrl, 'Forgot-password URL'),
|
|
927
940
|
countdownSeconds,
|
|
928
941
|
qrPollIntervalMs,
|
|
@@ -1063,9 +1076,12 @@ export class UniplatUserLogin extends LitElement {
|
|
|
1063
1076
|
}
|
|
1064
1077
|
|
|
1065
1078
|
private renderTabs() {
|
|
1079
|
+
const visibleMethods = LOGIN_METHODS.filter(
|
|
1080
|
+
({ method }) => method !== 'qr-code' || USER_LOGIN_QR_LOGIN_ENABLED,
|
|
1081
|
+
)
|
|
1066
1082
|
return html`
|
|
1067
1083
|
<div class="tabs" part="title tabs" role="tablist" aria-label="登录方式">
|
|
1068
|
-
${
|
|
1084
|
+
${visibleMethods.map(({ method, label }) => {
|
|
1069
1085
|
const available = this.isMethodAvailable(method)
|
|
1070
1086
|
return html`
|
|
1071
1087
|
<button
|
|
@@ -1426,6 +1442,7 @@ export class UniplatUserLogin extends LitElement {
|
|
|
1426
1442
|
const transport = this.config?.transport
|
|
1427
1443
|
if (method === 'verification-code') return Boolean(transport)
|
|
1428
1444
|
if (method === 'password') return typeof transport?.loginWithPassword === 'function'
|
|
1445
|
+
if (!USER_LOGIN_QR_LOGIN_ENABLED) return false
|
|
1429
1446
|
return (
|
|
1430
1447
|
typeof transport?.createQrChallenge === 'function' &&
|
|
1431
1448
|
typeof transport.pollQrChallenge === 'function'
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type UserLoginErrorDetail,
|
|
9
9
|
type UserLoginTransport,
|
|
10
10
|
} from '../src'
|
|
11
|
+
import { USER_LOGIN_QR_LOGIN_ENABLED } from '../src/user-login'
|
|
11
12
|
|
|
12
13
|
async function settle(login: UniplatUserLogin): Promise<void> {
|
|
13
14
|
await Promise.resolve()
|
|
@@ -47,6 +48,7 @@ describe('<uniplat-user-login>', () => {
|
|
|
47
48
|
afterEach(() => {
|
|
48
49
|
document.body.replaceChildren()
|
|
49
50
|
vi.useRealTimers()
|
|
51
|
+
vi.restoreAllMocks()
|
|
50
52
|
})
|
|
51
53
|
|
|
52
54
|
it('registers, renders the verification login form and completes a safe login flow', async () => {
|
|
@@ -90,13 +92,15 @@ describe('<uniplat-user-login>', () => {
|
|
|
90
92
|
/\.security-icon::before\s*\{[^}]*left:\s*50%;[^}]*box-sizing:\s*border-box;[^}]*transform:\s*translateX\(-50%\);/s,
|
|
91
93
|
)
|
|
92
94
|
expect(login.shadowRoot?.textContent).toContain('验证码登录')
|
|
93
|
-
expect(
|
|
95
|
+
expect(USER_LOGIN_QR_LOGIN_ENABLED).toBe(false)
|
|
96
|
+
expect(login.shadowRoot?.querySelectorAll('[role="tab"]')).toHaveLength(2)
|
|
97
|
+
expect(login.shadowRoot?.textContent).not.toContain('扫码登录')
|
|
94
98
|
expect(
|
|
95
99
|
login.shadowRoot?.querySelector<HTMLButtonElement>('[role="tab"]:first-child')?.disabled,
|
|
96
100
|
).toBe(true)
|
|
97
101
|
expect(
|
|
98
102
|
login.shadowRoot?.querySelector<HTMLButtonElement>('[role="tab"]:last-child')?.disabled,
|
|
99
|
-
).toBe(
|
|
103
|
+
).toBe(false)
|
|
100
104
|
expect(login.shadowRoot?.querySelector('#uniplat-login-image-code')).toBeNull()
|
|
101
105
|
expect(login.shadowRoot?.querySelector('.captcha-image')).toBeNull()
|
|
102
106
|
expect(login.shadowRoot?.textContent).not.toContain('图形验证码')
|
|
@@ -275,6 +279,32 @@ describe('<uniplat-user-login>', () => {
|
|
|
275
279
|
).toThrow(TypeError)
|
|
276
280
|
})
|
|
277
281
|
|
|
282
|
+
it('opens the official user and privacy agreements by default', async () => {
|
|
283
|
+
const openWindow = vi.spyOn(window, 'open').mockReturnValue(null)
|
|
284
|
+
const login = document.createElement(USER_LOGIN_TAG)
|
|
285
|
+
login.configure({ transport: createTransport(), applicationName: 'HrSaas' })
|
|
286
|
+
document.body.append(login)
|
|
287
|
+
await settle(login)
|
|
288
|
+
|
|
289
|
+
const links = login.shadowRoot?.querySelectorAll<HTMLButtonElement>('.agreement-link')
|
|
290
|
+
expect(links).toHaveLength(2)
|
|
291
|
+
links?.[0]?.click()
|
|
292
|
+
links?.[1]?.click()
|
|
293
|
+
|
|
294
|
+
expect(openWindow).toHaveBeenNthCalledWith(
|
|
295
|
+
1,
|
|
296
|
+
'https://website.hrs100.com/agreement/user-service-agreement.html',
|
|
297
|
+
'_blank',
|
|
298
|
+
'noopener,noreferrer',
|
|
299
|
+
)
|
|
300
|
+
expect(openWindow).toHaveBeenNthCalledWith(
|
|
301
|
+
2,
|
|
302
|
+
'https://website.hrs100.com/agreement/qqxb-user-privacy.html',
|
|
303
|
+
'_blank',
|
|
304
|
+
'noopener,noreferrer',
|
|
305
|
+
)
|
|
306
|
+
})
|
|
307
|
+
|
|
278
308
|
it('maps transport failures to stable errors without exposing the original exception', async () => {
|
|
279
309
|
const sensitiveText = 'backend stack with jwt=should-never-render'
|
|
280
310
|
const transport = createTransport({
|
|
@@ -506,94 +536,125 @@ describe('<uniplat-user-login>', () => {
|
|
|
506
536
|
expect(passwordInput?.value).toBe('')
|
|
507
537
|
})
|
|
508
538
|
|
|
509
|
-
it('
|
|
510
|
-
vi.useFakeTimers()
|
|
539
|
+
it('hides QR login and falls back to verification login while the feature is disabled', async () => {
|
|
511
540
|
const createQrChallenge = vi.fn(async () => ({
|
|
512
|
-
challengeId: 'obviously-fake-
|
|
513
|
-
imageUrl:
|
|
514
|
-
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="120" height="120"%3E%3Crect width="120" height="120"/%3E%3C/svg%3E',
|
|
515
|
-
pollIntervalMs: 750,
|
|
541
|
+
challengeId: 'obviously-fake-hidden-challenge',
|
|
542
|
+
imageUrl: 'data:image/png;base64,b2J2aW91c2x5LWZha2U=',
|
|
516
543
|
}))
|
|
517
|
-
const pollQrChallenge = vi
|
|
518
|
-
.fn()
|
|
519
|
-
.mockResolvedValueOnce({ status: 'scanned' })
|
|
520
|
-
.mockResolvedValueOnce({
|
|
521
|
-
status: 'confirmed',
|
|
522
|
-
result: { applicationName: 'HrSaas' },
|
|
523
|
-
})
|
|
524
|
-
const transport = createTransport({ createQrChallenge, pollQrChallenge })
|
|
544
|
+
const pollQrChallenge = vi.fn(async () => ({ status: 'pending' as const }))
|
|
525
545
|
const login = document.createElement(USER_LOGIN_TAG)
|
|
526
|
-
const loginEvents: unknown[] = []
|
|
527
|
-
login.addEventListener('login-success', (event) => {
|
|
528
|
-
loginEvents.push((event as CustomEvent).detail)
|
|
529
|
-
})
|
|
530
546
|
login.configure({
|
|
531
|
-
transport,
|
|
547
|
+
transport: createTransport({ createQrChallenge, pollQrChallenge }),
|
|
532
548
|
applicationName: 'HrSaas',
|
|
533
549
|
initialMethod: 'qr-code',
|
|
534
|
-
qrPollIntervalMs: 500,
|
|
535
550
|
})
|
|
536
551
|
document.body.append(login)
|
|
537
552
|
await settle(login)
|
|
538
553
|
|
|
539
|
-
expect(
|
|
540
|
-
expect(login.shadowRoot?.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
expect(
|
|
546
|
-
expect(vi.getTimerCount()).toBe(1)
|
|
547
|
-
expect(login.shadowRoot?.innerHTML).not.toContain('obviously-fake-qr-challenge')
|
|
548
|
-
|
|
549
|
-
await vi.advanceTimersByTimeAsync(500)
|
|
554
|
+
expect(login.shadowRoot?.querySelectorAll('[role="tab"]')).toHaveLength(2)
|
|
555
|
+
expect(login.shadowRoot?.textContent).not.toContain('扫码登录')
|
|
556
|
+
expect(login.shadowRoot?.querySelector('.qr-panel')).toBeNull()
|
|
557
|
+
expect(
|
|
558
|
+
login.shadowRoot?.querySelector('[role="tab"][aria-selected="true"]')?.textContent,
|
|
559
|
+
).toContain('验证码登录')
|
|
560
|
+
expect(createQrChallenge).not.toHaveBeenCalled()
|
|
550
561
|
expect(pollQrChallenge).not.toHaveBeenCalled()
|
|
551
|
-
await vi.advanceTimersByTimeAsync(250)
|
|
552
|
-
await settle(login)
|
|
553
|
-
expect(login.shadowRoot?.textContent).toContain('扫码成功,请在手机上确认')
|
|
554
|
-
expect(vi.getTimerCount()).toBe(1)
|
|
555
|
-
expect(pollQrChallenge).toHaveBeenCalledWith('obviously-fake-qr-challenge', 'HrSaas')
|
|
556
|
-
|
|
557
|
-
await vi.advanceTimersByTimeAsync(750)
|
|
558
|
-
await settle(login)
|
|
559
|
-
expect(loginEvents).toEqual([{ applicationName: 'HrSaas' }])
|
|
560
|
-
expect(JSON.stringify(loginEvents)).not.toContain('obviously-fake-qr-challenge')
|
|
561
|
-
expect(vi.getTimerCount()).toBe(0)
|
|
562
562
|
})
|
|
563
563
|
|
|
564
|
-
it(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
564
|
+
it.skipIf(!USER_LOGIN_QR_LOGIN_ENABLED)(
|
|
565
|
+
'keeps QR status in one panel and clears polling after confirmation',
|
|
566
|
+
async () => {
|
|
567
|
+
vi.useFakeTimers()
|
|
568
|
+
const createQrChallenge = vi.fn(async () => ({
|
|
569
|
+
challengeId: 'obviously-fake-qr-challenge',
|
|
570
|
+
imageUrl:
|
|
571
|
+
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="120" height="120"%3E%3Crect width="120" height="120"/%3E%3C/svg%3E',
|
|
572
|
+
pollIntervalMs: 750,
|
|
573
|
+
}))
|
|
574
|
+
const pollQrChallenge = vi
|
|
575
|
+
.fn()
|
|
576
|
+
.mockResolvedValueOnce({ status: 'scanned' })
|
|
577
|
+
.mockResolvedValueOnce({
|
|
578
|
+
status: 'confirmed',
|
|
579
|
+
result: { applicationName: 'HrSaas' },
|
|
580
|
+
})
|
|
581
|
+
const transport = createTransport({ createQrChallenge, pollQrChallenge })
|
|
582
|
+
const login = document.createElement(USER_LOGIN_TAG)
|
|
583
|
+
const loginEvents: unknown[] = []
|
|
584
|
+
login.addEventListener('login-success', (event) => {
|
|
585
|
+
loginEvents.push((event as CustomEvent).detail)
|
|
586
|
+
})
|
|
587
|
+
login.configure({
|
|
588
|
+
transport,
|
|
589
|
+
applicationName: 'HrSaas',
|
|
590
|
+
initialMethod: 'qr-code',
|
|
591
|
+
qrPollIntervalMs: 500,
|
|
592
|
+
})
|
|
593
|
+
document.body.append(login)
|
|
594
|
+
await settle(login)
|
|
595
|
+
|
|
596
|
+
expect(createQrChallenge).toHaveBeenCalledWith({ applicationName: 'HrSaas' })
|
|
597
|
+
expect(login.shadowRoot?.querySelector<HTMLImageElement>('.qr-image')?.src).toContain(
|
|
598
|
+
'data:image/svg+xml',
|
|
599
|
+
)
|
|
600
|
+
expect(login.shadowRoot?.textContent).toContain('请使用微信扫描小程序码登录')
|
|
601
|
+
expect(login.shadowRoot?.textContent).toContain('未登录时先完成登录')
|
|
602
|
+
expect(login.shadowRoot?.textContent).toContain('电脑端同步登录')
|
|
603
|
+
expect(vi.getTimerCount()).toBe(1)
|
|
604
|
+
expect(login.shadowRoot?.innerHTML).not.toContain('obviously-fake-qr-challenge')
|
|
605
|
+
|
|
606
|
+
await vi.advanceTimersByTimeAsync(500)
|
|
607
|
+
expect(pollQrChallenge).not.toHaveBeenCalled()
|
|
608
|
+
await vi.advanceTimersByTimeAsync(250)
|
|
609
|
+
await settle(login)
|
|
610
|
+
expect(login.shadowRoot?.textContent).toContain('扫码成功,请在手机上确认')
|
|
611
|
+
expect(vi.getTimerCount()).toBe(1)
|
|
612
|
+
expect(pollQrChallenge).toHaveBeenCalledWith('obviously-fake-qr-challenge', 'HrSaas')
|
|
613
|
+
|
|
614
|
+
await vi.advanceTimersByTimeAsync(750)
|
|
615
|
+
await settle(login)
|
|
616
|
+
expect(loginEvents).toEqual([{ applicationName: 'HrSaas' }])
|
|
617
|
+
expect(JSON.stringify(loginEvents)).not.toContain('obviously-fake-qr-challenge')
|
|
618
|
+
expect(vi.getTimerCount()).toBe(0)
|
|
619
|
+
},
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
it.skipIf(!USER_LOGIN_QR_LOGIN_ENABLED)(
|
|
623
|
+
'stops polling and offers a refresh after QR login is rejected',
|
|
624
|
+
async () => {
|
|
625
|
+
vi.useFakeTimers()
|
|
626
|
+
const transport = createTransport({
|
|
627
|
+
createQrChallenge: vi.fn(async () => ({
|
|
628
|
+
challengeId: 'obviously-fake-rejected-challenge',
|
|
629
|
+
imageUrl: 'data:image/png;base64,b2J2aW91c2x5LWZha2UtcG5n',
|
|
630
|
+
pollIntervalMs: 500,
|
|
631
|
+
})),
|
|
632
|
+
pollQrChallenge: vi.fn(async () => ({ status: 'rejected' as const })),
|
|
633
|
+
})
|
|
634
|
+
const errors: UserLoginErrorDetail[] = []
|
|
635
|
+
const login = document.createElement(USER_LOGIN_TAG)
|
|
636
|
+
login.addEventListener('error', (event) => {
|
|
637
|
+
errors.push((event as unknown as CustomEvent<UserLoginErrorDetail>).detail)
|
|
638
|
+
})
|
|
639
|
+
login.configure({ transport, applicationName: 'HrSaas', initialMethod: 'qr-code' })
|
|
640
|
+
document.body.append(login)
|
|
641
|
+
await settle(login)
|
|
642
|
+
|
|
643
|
+
await vi.advanceTimersByTimeAsync(500)
|
|
644
|
+
await settle(login)
|
|
645
|
+
|
|
646
|
+
expect(login.shadowRoot?.textContent).toContain('登录已取消')
|
|
647
|
+
expect(login.shadowRoot?.textContent).toContain('已在手机上取消登录,请点击刷新。')
|
|
648
|
+
expect(vi.getTimerCount()).toBe(0)
|
|
649
|
+
expect(errors).toEqual([
|
|
650
|
+
{
|
|
651
|
+
code: 'QR_LOGIN_REJECTED',
|
|
652
|
+
message: '已在手机上取消登录,请点击刷新。',
|
|
653
|
+
recoverable: true,
|
|
654
|
+
},
|
|
655
|
+
])
|
|
656
|
+
},
|
|
657
|
+
)
|
|
597
658
|
|
|
598
659
|
it('maps a locked password account to a stable safe component error', async () => {
|
|
599
660
|
const transport = createTransport({
|
package/restart
ADDED