playbook_ui 9.8.0 → 9.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfe0ad928a2d058675961581c2e7579a7b2b686b7444c461b907d9976e46983b
4
- data.tar.gz: 7dd7e0d985acdff72829e21de5b09b94f823006cd26b588831096334ea645690
3
+ metadata.gz: cc6cf85248c6d0e0b9c74d63c9d1dffa2a99d8e5b8d1010a8b4622829fa77d66
4
+ data.tar.gz: dae909122997e3eea81a47c6b5b9260bb84d87316c82cc70b351bec7a75d4ad7
5
5
  SHA512:
6
- metadata.gz: c8682707eb5b82740167457664a027d16b1635181692900c4c62f0aba012ee6803a13c4fdcfe55296682cf7979688cf34ea36d15a5b6fb2085faa40c2f7e58a8
7
- data.tar.gz: c879311f4ffaa99290028fdf45579a595920fd8c9d953823c451d2891e9bac9b1302bc95d9f31dfb9a389f48657f36502f00b8a85eb15b3e29ce4775bca325f3
6
+ metadata.gz: 596e69c28e26d8ddcafc2d1006e8f72f56458a9a8eacc4811a918e0124ddffed05c75e61a8834630d041d742e1dd66d8cd97870b5b1d4ef74e7ce2b9b8f30d09
7
+ data.tar.gz: 9a71dd4b52c1da10cd03a288accf3c76e18dd8bc71aebe3e2818c7f27763cd65e0b069d9103c95c5d8cad56ec96411340419a3f729b139758c172d8628e6c8a9
@@ -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 { zxcvbnPasswordScore } from './passwordStrength.js'
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 calculator = useMemo(
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 } = calculator.test(displayValue, common)
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.
@@ -12,6 +12,7 @@ const PassphraseDefault = (props) => {
12
12
  <>
13
13
  <div>
14
14
  <Passphrase
15
+ id="my-passphrase"
15
16
  onChange={handleChange}
16
17
  value={input}
17
18
  {...props}
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Playbook
4
- VERSION = "9.8.0"
4
+ VERSION = "9.9.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: playbook_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.8.0
4
+ version: 9.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Power UX
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-04-21 00:00:00.000000000 Z
12
+ date: 2021-04-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: actionpack
@@ -1383,6 +1383,9 @@ files:
1383
1383
  - app/pb_kits/playbook/pb_online_status/online_status.rb
1384
1384
  - app/pb_kits/playbook/pb_passphrase/_passphrase.jsx
1385
1385
  - app/pb_kits/playbook/pb_passphrase/_passphrase.scss
1386
+ - app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.html.erb
1387
+ - app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.jsx
1388
+ - app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.md
1386
1389
  - app/pb_kits/playbook/pb_passphrase/docs/_passphrase_common.jsx
1387
1390
  - app/pb_kits/playbook/pb_passphrase/docs/_passphrase_default.html.erb
1388
1391
  - app/pb_kits/playbook/pb_passphrase/docs/_passphrase_default.jsx
@@ -1404,6 +1407,8 @@ files:
1404
1407
  - app/pb_kits/playbook/pb_passphrase/passphrase.rb
1405
1408
  - app/pb_kits/playbook/pb_passphrase/passphrase.test.jsx
1406
1409
  - app/pb_kits/playbook/pb_passphrase/passwordStrength.js
1410
+ - app/pb_kits/playbook/pb_passphrase/useHaveIBeenPwned.js
1411
+ - app/pb_kits/playbook/pb_passphrase/useZxcvbn.js
1407
1412
  - app/pb_kits/playbook/pb_person/_person.jsx
1408
1413
  - app/pb_kits/playbook/pb_person/_person.scss
1409
1414
  - app/pb_kits/playbook/pb_person/docs/_description.md
@@ -2133,8 +2138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
2133
2138
  - !ruby/object:Gem::Version
2134
2139
  version: '0'
2135
2140
  requirements: []
2136
- rubyforge_project:
2137
- rubygems_version: 2.7.3
2141
+ rubygems_version: 3.1.4
2138
2142
  signing_key:
2139
2143
  specification_version: 4
2140
2144
  summary: Playbook Design System