playbook_ui 9.7.0 → 9.10.0.pre.alpha2
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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_background/background.rb +0 -1
- data/app/pb_kits/playbook/pb_circle_chart/circle_chart.rb +9 -10
- data/app/pb_kits/playbook/pb_circle_icon_button/circle_icon_button.rb +4 -2
- data/app/pb_kits/playbook/pb_date/_date.jsx +3 -3
- data/app/pb_kits/playbook/pb_date/date.html.erb +2 -3
- data/app/pb_kits/playbook/pb_date/docs/_date_variants.html.erb +8 -0
- data/app/pb_kits/playbook/pb_date/docs/_date_variants.jsx +10 -0
- data/app/pb_kits/playbook/pb_date_stacked/date_stacked.rb +7 -9
- data/app/pb_kits/playbook/pb_distribution_bar/distribution_bar.rb +2 -2
- data/app/pb_kits/playbook/pb_filter/filter.rb +1 -1
- data/app/pb_kits/playbook/pb_fixed_confirmation_toast/_fixed_confirmation_toast.jsx +4 -2
- data/app/pb_kits/playbook/pb_fixed_confirmation_toast/_fixed_confirmation_toast.scss +7 -0
- data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_multi_line.html.erb +2 -1
- data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_multi_line.jsx +2 -1
- data/app/pb_kits/playbook/pb_fixed_confirmation_toast/docs/_fixed_confirmation_toast_multi_line.md +1 -1
- data/app/pb_kits/playbook/pb_fixed_confirmation_toast/fixed_confirmation_toast.rb +7 -1
- data/app/pb_kits/playbook/pb_flex/_flex_item.jsx +1 -1
- data/app/pb_kits/playbook/pb_flex/flex.rb +4 -4
- data/app/pb_kits/playbook/pb_form/form.rb +1 -1
- data/app/pb_kits/playbook/pb_form_group/_form_group.jsx +3 -1
- data/app/pb_kits/playbook/pb_form_group/_form_group.scss +8 -0
- data/app/pb_kits/playbook/pb_form_group/docs/_form_group_full_width.html.erb +13 -0
- data/app/pb_kits/playbook/pb_form_group/docs/_form_group_full_width.jsx +43 -0
- data/app/pb_kits/playbook/pb_form_group/docs/_form_group_full_width.md +1 -0
- data/app/pb_kits/playbook/pb_form_group/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_form_group/docs/index.js +1 -1
- data/app/pb_kits/playbook/pb_form_group/form_group.rb +10 -1
- data/app/pb_kits/playbook/pb_icon_stat_value/icon_stat_value.rb +0 -1
- data/app/pb_kits/playbook/pb_nav/_vertical_nav.scss +1 -1
- data/app/pb_kits/playbook/pb_nav/docs/_block_nav.html.erb +41 -5
- data/app/pb_kits/playbook/pb_nav/docs/_block_nav.jsx +44 -6
- data/app/pb_kits/playbook/pb_passphrase/_passphrase.jsx +12 -9
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.html.erb +1 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.jsx +24 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.md +3 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_default.jsx +1 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_passphrase/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_passphrase/passphrase.rb +2 -0
- data/app/pb_kits/playbook/pb_passphrase/passphrase.test.jsx +12 -0
- data/app/pb_kits/playbook/pb_passphrase/useHaveIBeenPwned.js +52 -0
- data/app/pb_kits/playbook/pb_passphrase/useZxcvbn.js +58 -0
- data/app/pb_kits/playbook/pb_progress_pills/progress_pills.rb +2 -1
- data/app/pb_kits/playbook/pb_progress_step/progress_step.rb +1 -1
- data/app/pb_kits/playbook/pb_progress_step/progress_step_item.rb +1 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +2 -2
- data/app/pb_kits/playbook/pb_selectable_card_icon/selectable_card_icon.rb +1 -1
- data/app/pb_kits/playbook/pb_selectable_icon/selectable_icon.rb +1 -1
- data/app/pb_kits/playbook/pb_stat_change/stat_change.rb +8 -12
- data/app/pb_kits/playbook/pb_table/docs/_table_side_highlight.md +1 -1
- data/app/pb_kits/playbook/pb_tooltip/tooltip.rb +1 -1
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_default.html.erb +1 -1
- data/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb +5 -1
- data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +5 -13
- data/app/pb_kits/playbook/pb_user_badge/user_badge.rb +1 -1
- data/lib/playbook/forms/builder/action_area.rb +2 -2
- data/lib/playbook/forms/builder/collection_select_field.rb +2 -2
- data/lib/playbook/forms/builder/select_field.rb +1 -1
- data/lib/playbook/kit_resolver.rb +1 -1
- data/lib/playbook/markdown/template_handler.rb +4 -2
- data/lib/playbook/pb_doc_helper.rb +4 -2
- data/lib/playbook/props.rb +1 -4
- data/lib/playbook/props/base.rb +1 -2
- data/lib/playbook/version.rb +1 -1
- metadata +38 -31
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Full Width is a prop that can be added to any of the Form Group options. This prop allows the Form Group to stretch the full width of the div.
|
|
@@ -3,6 +3,7 @@ examples:
|
|
|
3
3
|
rails:
|
|
4
4
|
- form_group_default: Default
|
|
5
5
|
- form_group_button: Button
|
|
6
|
+
- form_group_full_width: Full Width
|
|
6
7
|
- form_group_date_picker: Date Picker
|
|
7
8
|
# - form_group_typeahead: Typeahead
|
|
8
9
|
- form_group_select: Select
|
|
@@ -13,6 +14,7 @@ examples:
|
|
|
13
14
|
react:
|
|
14
15
|
- form_group_default: Default
|
|
15
16
|
- form_group_button: Button
|
|
17
|
+
- form_group_full_width: Full Width
|
|
16
18
|
- form_group_date_picker: Date Picker
|
|
17
19
|
# - form_group_typeahead: Typeahead
|
|
18
20
|
- form_group_select: Select
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { default as FormGroupDefault } from './_form_group_default.jsx'
|
|
2
2
|
export { default as FormGroupButton } from './_form_group_button.jsx'
|
|
3
|
+
export { default as FormGroupFullWidth } from './_form_group_full_width.jsx'
|
|
3
4
|
export { default as FormGroupDatePicker } from './_form_group_date_picker.jsx'
|
|
4
|
-
// export { default as FormGroupTypeahead } from './_form_group_typeahead.jsx'
|
|
5
5
|
export { default as FormGroupSelect } from './_form_group_select.jsx'
|
|
6
6
|
export { default as FormGroupSelectableCard } from './_form_group_selectable_card.jsx'
|
|
7
7
|
export { default as FormGroupSelectableCardIcon } from './_form_group_selectable_card_icon.jsx'
|
|
@@ -3,8 +3,17 @@
|
|
|
3
3
|
module Playbook
|
|
4
4
|
module PbFormGroup
|
|
5
5
|
class FormGroup < Playbook::KitBase
|
|
6
|
+
prop :full_width, type: Playbook::Props::Boolean,
|
|
7
|
+
default: false
|
|
8
|
+
|
|
6
9
|
def classname
|
|
7
|
-
generate_classname("pb_form_group_kit")
|
|
10
|
+
generate_classname("pb_form_group_kit", full_width_class)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def full_width_class
|
|
16
|
+
full_width ? "full" : nil
|
|
8
17
|
end
|
|
9
18
|
end
|
|
10
19
|
end
|
|
@@ -1,6 +1,42 @@
|
|
|
1
|
-
<%= pb_rails("nav", props: {title: "
|
|
2
|
-
<%= pb_rails("nav/item", props: { link: "#", active: true }) do%>
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
<%= pb_rails("nav", props: {title: "Users", link: "#"}) do %>
|
|
2
|
+
<%= pb_rails("nav/item", props: { link: "#", active: true }) do%>
|
|
3
|
+
<%= pb_rails("user", props: {
|
|
4
|
+
name: "Anna Black",
|
|
5
|
+
territory: "PHL",
|
|
6
|
+
title: "Remodeling Consultant",
|
|
7
|
+
orientation: "horizontal",
|
|
8
|
+
align: "left",
|
|
9
|
+
avatar_url: "https://randomuser.me/api/portraits/women/44.jpg"
|
|
10
|
+
}) %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<%= pb_rails("nav/item", props: { link: "#" }) do%>
|
|
13
|
+
<%= pb_rails("user", props: {
|
|
14
|
+
name: "Julie Hamilton",
|
|
15
|
+
territory: "PHL",
|
|
16
|
+
title: "Inside Sales Agent",
|
|
17
|
+
orientation: "horizontal",
|
|
18
|
+
align: "left",
|
|
19
|
+
avatar_url: "https://randomuser.me/api/portraits/women/45.jpg"
|
|
20
|
+
}) %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<%= pb_rails("nav/item", props: { link: "#" }) do%>
|
|
23
|
+
<%= pb_rails("user", props: {
|
|
24
|
+
name: "Dennis Wilks",
|
|
25
|
+
territory: "PHL",
|
|
26
|
+
title: "Senior Remodeling Consultant",
|
|
27
|
+
orientation: "horizontal",
|
|
28
|
+
align: "left",
|
|
29
|
+
avatar_url: "https://randomuser.me/api/portraits/men/44.jpg"
|
|
30
|
+
}) %>
|
|
31
|
+
<% end %>
|
|
32
|
+
<%= pb_rails("nav/item", props: { link: "#" }) do%>
|
|
33
|
+
<%= pb_rails("user", props: {
|
|
34
|
+
name: "Ronnie Martin",
|
|
35
|
+
territory: "PHL",
|
|
36
|
+
title: "Customer Development Representative",
|
|
37
|
+
orientation: "horizontal",
|
|
38
|
+
align: "left",
|
|
39
|
+
avatar_url: "https://randomuser.me/api/portraits/men/46.jpg"
|
|
40
|
+
}) %>
|
|
41
|
+
<% end %>
|
|
6
42
|
<% end %>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { Nav } from '../../'
|
|
2
|
+
import { Nav, User } from '../../'
|
|
3
3
|
import NavItem from '../_item.jsx'
|
|
4
4
|
|
|
5
5
|
const BlockNav = (props) => {
|
|
6
6
|
return (
|
|
7
7
|
<Nav
|
|
8
8
|
link="#"
|
|
9
|
-
title="
|
|
9
|
+
title="Users"
|
|
10
10
|
{...props}
|
|
11
11
|
>
|
|
12
12
|
<NavItem
|
|
@@ -14,11 +14,49 @@ const BlockNav = (props) => {
|
|
|
14
14
|
link="#"
|
|
15
15
|
{...props}
|
|
16
16
|
>
|
|
17
|
-
|
|
17
|
+
<User
|
|
18
|
+
align="left"
|
|
19
|
+
avatarUrl="https://randomuser.me/api/portraits/women/44.jpg"
|
|
20
|
+
name="Anna Black"
|
|
21
|
+
orientation="horizontal"
|
|
22
|
+
territory="PHL"
|
|
23
|
+
title="Remodeling Consultant"
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
</NavItem>
|
|
27
|
+
<NavItem link="#">
|
|
28
|
+
<User
|
|
29
|
+
align="left"
|
|
30
|
+
avatarUrl="https://randomuser.me/api/portraits/women/45.jpg"
|
|
31
|
+
name="Julie Hamilton"
|
|
32
|
+
orientation="horizontal"
|
|
33
|
+
territory="PHL"
|
|
34
|
+
title="Inside Sales Agent"
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
</NavItem>
|
|
38
|
+
<NavItem link="#">
|
|
39
|
+
<User
|
|
40
|
+
align="left"
|
|
41
|
+
avatarUrl="https://randomuser.me/api/portraits/men/44.jpg"
|
|
42
|
+
name="Dennis Wilks"
|
|
43
|
+
orientation="horizontal"
|
|
44
|
+
territory="PHL"
|
|
45
|
+
title="Senior Remodeling Consultant"
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
</NavItem>
|
|
49
|
+
<NavItem link="#">
|
|
50
|
+
<User
|
|
51
|
+
align="left"
|
|
52
|
+
avatarUrl="https://randomuser.me/api/portraits/men/46.jpg"
|
|
53
|
+
name="Ronnie Martin"
|
|
54
|
+
orientation="horizontal"
|
|
55
|
+
territory="PHL"
|
|
56
|
+
title="Customer Development Representative"
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
18
59
|
</NavItem>
|
|
19
|
-
<NavItem link="#">{'Music'}</NavItem>
|
|
20
|
-
<NavItem link="#">{'Video'}</NavItem>
|
|
21
|
-
<NavItem link="#">{'Files'}</NavItem>
|
|
22
60
|
</Nav>
|
|
23
61
|
)
|
|
24
62
|
}
|
|
@@ -5,12 +5,14 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
|
5
5
|
import classnames from 'classnames'
|
|
6
6
|
import { buildAriaProps, buildCss, buildDataProps } from '../utilities/props'
|
|
7
7
|
import { globalProps } from '../utilities/globalProps.js'
|
|
8
|
-
import
|
|
8
|
+
import useZxcvbn from './useZxcvbn'
|
|
9
|
+
import useHaveIBeenPwned from './useHaveIBeenPwned'
|
|
9
10
|
import { Body, Caption, Flex, Icon, PbReactPopover, ProgressSimple, TextInput } from '../'
|
|
10
11
|
|
|
11
12
|
type PassphraseProps = {
|
|
12
13
|
aria?: object,
|
|
13
14
|
averageThreshold?: number,
|
|
15
|
+
checkPwned?: boolean,
|
|
14
16
|
common?: boolean,
|
|
15
17
|
confirmation?: boolean,
|
|
16
18
|
className?: string,
|
|
@@ -33,6 +35,7 @@ const Passphrase = (props: PassphraseProps) => {
|
|
|
33
35
|
const {
|
|
34
36
|
aria = {},
|
|
35
37
|
averageThreshold = 2,
|
|
38
|
+
checkPwned = false,
|
|
36
39
|
className,
|
|
37
40
|
common = false,
|
|
38
41
|
confirmation = false,
|
|
@@ -41,7 +44,7 @@ const Passphrase = (props: PassphraseProps) => {
|
|
|
41
44
|
id,
|
|
42
45
|
inputProps = {},
|
|
43
46
|
label = confirmation ? 'Confirm Passphrase' : 'Passphrase',
|
|
44
|
-
minLength,
|
|
47
|
+
minLength = 12,
|
|
45
48
|
onChange = () => {},
|
|
46
49
|
showTipsBelow = 'always',
|
|
47
50
|
onStrengthChange,
|
|
@@ -50,6 +53,7 @@ const Passphrase = (props: PassphraseProps) => {
|
|
|
50
53
|
uncontrolled = false,
|
|
51
54
|
value = '',
|
|
52
55
|
} = props
|
|
56
|
+
const ariaProps = buildAriaProps(aria)
|
|
53
57
|
|
|
54
58
|
const [uncontrolledValue, setUncontrolledValue] = useState('')
|
|
55
59
|
|
|
@@ -68,16 +72,11 @@ const Passphrase = (props: PassphraseProps) => {
|
|
|
68
72
|
const [showPassphrase, setShowPassphrase] = useState(false)
|
|
69
73
|
const toggleShowPassphrase = () => setShowPassphrase(!showPassphrase)
|
|
70
74
|
|
|
71
|
-
const ariaProps = buildAriaProps(aria)
|
|
72
|
-
const dataProps = buildDataProps(data)
|
|
73
75
|
const classes = classnames(buildCss('pb_passphrase'), globalProps(props), className)
|
|
74
76
|
|
|
75
|
-
const
|
|
76
|
-
() => confirmation ? { test: () => ({}) } : zxcvbnPasswordScore({ averageThreshold, strongThreshold, minLength }),
|
|
77
|
-
[averageThreshold, confirmation, strongThreshold, minLength]
|
|
78
|
-
)
|
|
77
|
+
const isPwned = checkPwned ? useHaveIBeenPwned(displayValue, minLength) : false
|
|
79
78
|
|
|
80
|
-
const { percent: progressPercent, variant: progressVariant, text: strengthLabel, strength } =
|
|
79
|
+
const { percent: progressPercent, variant: progressVariant, text: strengthLabel, strength } = useZxcvbn({ passphrase: displayValue, common, isPwned, confirmation, averageThreshold, minLength, strongThreshold })
|
|
81
80
|
|
|
82
81
|
useEffect(() => {
|
|
83
82
|
if (typeof onStrengthChange === 'function') {
|
|
@@ -89,6 +88,10 @@ const Passphrase = (props: PassphraseProps) => {
|
|
|
89
88
|
(dark ? 'dark' : null),
|
|
90
89
|
(showTipsBelow === 'always' ? null : `show-below-${showTipsBelow}`),
|
|
91
90
|
)
|
|
91
|
+
const dataProps = useMemo(
|
|
92
|
+
() => (buildDataProps(Object.assign({}, data, { strength }))),
|
|
93
|
+
[data, strength]
|
|
94
|
+
)
|
|
92
95
|
|
|
93
96
|
const popoverReference = (
|
|
94
97
|
<a
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= pb_rails("passphrase", props: { check_pwned: true }) %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { Passphrase } from '../../'
|
|
3
|
+
|
|
4
|
+
const PassphraseBreached = (props) => {
|
|
5
|
+
const [input, setInput] = useState('')
|
|
6
|
+
|
|
7
|
+
const handleChange = (e) => setInput(e.target.value)
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<div>
|
|
12
|
+
<br />
|
|
13
|
+
<Passphrase
|
|
14
|
+
checkPwned
|
|
15
|
+
onChange={handleChange}
|
|
16
|
+
value={input}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
</>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default PassphraseBreached
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Use `checkPwned | checked_pwned` prop to enable checking against <a href='https://haveibeenpwned.com/Passwords'>HaveIBeenPwned's</a> API. As the passphrase is typed, it is checked against more than half a billion breached passwords, to help ensure its not compromised.
|
|
2
|
+
Should it fail, the feedback will express the passphrase is too common, prompting the user to change.
|
|
3
|
+
This uses their k-Anonymity model, so only the first 5 characters of a hashed copy of the passphrase are sent.
|
|
@@ -5,6 +5,7 @@ examples:
|
|
|
5
5
|
- passphrase_meter_settings: Meter Settings
|
|
6
6
|
- passphrase_input_props: Input Props
|
|
7
7
|
- passphrase_tips: Tips
|
|
8
|
+
- passphrase_breached: Breached Passphrases
|
|
8
9
|
|
|
9
10
|
react:
|
|
10
11
|
- passphrase_default: Default
|
|
@@ -13,3 +14,4 @@ examples:
|
|
|
13
14
|
- passphrase_tips: Tips
|
|
14
15
|
- passphrase_strength_change: Strength Change
|
|
15
16
|
- passphrase_common: Common Passphrases
|
|
17
|
+
- passphrase_breached: Breached Passphrases
|
|
@@ -4,3 +4,4 @@ export { default as PassphraseInputProps } from './_passphrase_input_props'
|
|
|
4
4
|
export { default as PassphraseTips } from './_passphrase_tips'
|
|
5
5
|
export { default as PassphraseStrengthChange } from './_passphrase_strength_change'
|
|
6
6
|
export { default as PassphraseCommon } from './_passphrase_common'
|
|
7
|
+
export { default as PassphraseBreached } from './_passphrase_breached'
|
|
@@ -4,6 +4,7 @@ module Playbook
|
|
|
4
4
|
module PbPassphrase
|
|
5
5
|
class Passphrase < Playbook::KitBase
|
|
6
6
|
prop :average_threshold
|
|
7
|
+
prop :check_pwned
|
|
7
8
|
prop :confirmation, type: Playbook::Props::Boolean, default: false
|
|
8
9
|
prop :input_props, type: Playbook::Props::Hash, default: {}
|
|
9
10
|
prop :label
|
|
@@ -18,6 +19,7 @@ module Playbook
|
|
|
18
19
|
|
|
19
20
|
def passphrase_options
|
|
20
21
|
{
|
|
22
|
+
checkPwned: check_pwned,
|
|
21
23
|
dark: dark,
|
|
22
24
|
id: id,
|
|
23
25
|
averageThreshold: average_threshold,
|
|
@@ -121,3 +121,15 @@ test('popover target does not show when tips are not given', () => {
|
|
|
121
121
|
const kit = screen.getByTestId(testId)
|
|
122
122
|
expect(kit.querySelector('[class^=pb_popover_reference_wrapper]')).toBeNull()
|
|
123
123
|
})
|
|
124
|
+
|
|
125
|
+
test('data-strength attribute exposes strength of password', () => {
|
|
126
|
+
render(
|
|
127
|
+
<Passphrase
|
|
128
|
+
data={{ testid: testId }}
|
|
129
|
+
value="correct horse battery staple"
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const kit = screen.getByTestId(testId)
|
|
134
|
+
expect(parseInt(kit.getAttribute('data-strength'))).toBeGreaterThan(0)
|
|
135
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
const checkHaveIBeenPwned = async function (passphrase) {
|
|
5
|
+
const buffer = new TextEncoder('utf-8').encode(passphrase)
|
|
6
|
+
const digest = await crypto.subtle.digest('SHA-1', buffer)
|
|
7
|
+
const hashArray = Array.from(new Uint8Array(digest))
|
|
8
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
9
|
+
|
|
10
|
+
const firstFive = hashHex.slice(0, 5)
|
|
11
|
+
const endOfHash = hashHex.slice(5)
|
|
12
|
+
|
|
13
|
+
const resp = await fetch(`https://api.pwnedpasswords.com/range/${firstFive}`)
|
|
14
|
+
const text = await resp.text()
|
|
15
|
+
|
|
16
|
+
const match = text.split('\n').some((line) => {
|
|
17
|
+
//Each line is <sha-1-hash-suffix>:<count of incidents>
|
|
18
|
+
return line.split(':')[0] === endOfHash.toUpperCase()
|
|
19
|
+
})
|
|
20
|
+
return match
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* If the input hasn't changed in <delay> ms,
|
|
25
|
+
* hit the haveibeenpwned api and check if the given passphrase is compromised
|
|
26
|
+
*/
|
|
27
|
+
export default function useHaveIBeenPwned(passphrase, minLength, delay = 400) {
|
|
28
|
+
const [isPwned, setIsPwned] = useState(false)
|
|
29
|
+
|
|
30
|
+
useEffect(
|
|
31
|
+
() => {
|
|
32
|
+
// only check the API for passphrases above the minimum size
|
|
33
|
+
if (passphrase.length < minLength) {
|
|
34
|
+
setIsPwned(false)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handler = setTimeout(() => {
|
|
39
|
+
checkHaveIBeenPwned(passphrase)
|
|
40
|
+
.then((pwned) => setIsPwned(pwned))
|
|
41
|
+
.catch(() => setIsPwned(false))
|
|
42
|
+
}, delay)
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
clearTimeout(handler)
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[passphrase, minLength, delay]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return isPwned
|
|
52
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import zxcvbn from 'zxcvbn'
|
|
3
|
+
|
|
4
|
+
export default function useZxcvbn(options) {
|
|
5
|
+
const { passphrase = '', common, isPwned, confirmation, averageThreshold, minLength, strongThreshold } = options
|
|
6
|
+
const calculator = useMemo(
|
|
7
|
+
() => confirmation ? () => ({ score: 0 }) : zxcvbn,
|
|
8
|
+
[confirmation]
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const [percent, setPercent] = useState('0')
|
|
12
|
+
const [variant, setVariant] = useState('negative')
|
|
13
|
+
const [text, setText] = useState('\u00A0') //nbsp to keep height constant
|
|
14
|
+
const [result, setResult] = useState({})
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (confirmation) return
|
|
18
|
+
|
|
19
|
+
setResult(calculator(passphrase))
|
|
20
|
+
const str = result.score
|
|
21
|
+
|
|
22
|
+
const noPassphrase = passphrase.length <= 0
|
|
23
|
+
const commonPassphrase = common || isPwned
|
|
24
|
+
const weakPassphrase = passphrase.length < minLength || str < averageThreshold
|
|
25
|
+
const averagePassphrase = str < strongThreshold
|
|
26
|
+
const strongPassphrase = str >= strongThreshold
|
|
27
|
+
|
|
28
|
+
if (noPassphrase) {
|
|
29
|
+
setPercent('0')
|
|
30
|
+
setVariant('negative')
|
|
31
|
+
setText('\u00A0') //nbsp to keep height constant
|
|
32
|
+
} else if (commonPassphrase) {
|
|
33
|
+
setPercent('25')
|
|
34
|
+
setVariant('negative')
|
|
35
|
+
setText('This passphrase is too common')
|
|
36
|
+
} else if (weakPassphrase) {
|
|
37
|
+
setPercent('25')
|
|
38
|
+
setVariant('negative')
|
|
39
|
+
setText('Too weak')
|
|
40
|
+
} else if (averagePassphrase){
|
|
41
|
+
setPercent('50')
|
|
42
|
+
setVariant('warning')
|
|
43
|
+
setText('Almost there, keep going!')
|
|
44
|
+
} else if (strongPassphrase) {
|
|
45
|
+
setPercent('100')
|
|
46
|
+
setVariant('positive')
|
|
47
|
+
setText('Success! Strong passphrase')
|
|
48
|
+
}
|
|
49
|
+
}, [passphrase, common, isPwned, averageThreshold, minLength, strongThreshold]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
strength: common || isPwned ? 0 : result.score,
|
|
54
|
+
percent,
|
|
55
|
+
variant,
|
|
56
|
+
text,
|
|
57
|
+
}
|
|
58
|
+
}
|