playbook_ui 11.12.1.pre.alpha.charts1 → 11.12.1.pre.alpha.passphrase1

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/index.js +1 -0
  3. data/app/pb_kits/playbook/pb_bar_graph/_bar_graph.jsx +111 -0
  4. data/app/pb_kits/playbook/pb_circle_chart/_circle_chart.jsx +151 -0
  5. data/app/pb_kits/playbook/pb_circle_chart/circle_chart.html.erb +21 -9
  6. data/app/pb_kits/playbook/pb_circle_chart/circle_chart.rb +47 -7
  7. data/app/pb_kits/playbook/pb_dashboard/{pbChartsDarkTheme.ts → pbChartsDarkTheme.js} +21 -6
  8. data/app/pb_kits/playbook/pb_dashboard/{pbChartsLightTheme.ts → pbChartsLightTheme.js} +21 -6
  9. data/app/pb_kits/playbook/pb_gauge/_gauge.jsx +112 -0
  10. data/app/pb_kits/playbook/pb_gauge/_gauge.scss +0 -4
  11. data/app/pb_kits/playbook/pb_gauge/docs/_gauge_complex.html.erb +1 -1
  12. data/app/pb_kits/playbook/pb_gauge/docs/_gauge_complex.jsx +8 -8
  13. data/app/pb_kits/playbook/pb_gauge/gauge.html.erb +11 -1
  14. data/app/pb_kits/playbook/pb_gauge/gauge.rb +8 -3
  15. data/app/pb_kits/playbook/pb_line_graph/_line_graph.jsx +113 -0
  16. data/app/pb_kits/playbook/pb_passphrase/_passphrase.jsx +56 -97
  17. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.html.erb +145 -1
  18. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.jsx +127 -3
  19. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_breached.md +11 -2
  20. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_common.html.erb +136 -0
  21. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_common.jsx +90 -8
  22. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_common.md +5 -0
  23. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_confirmation.html.erb +51 -0
  24. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_confirmation.jsx +39 -0
  25. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_default.html.erb +0 -2
  26. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_default.jsx +6 -20
  27. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_input_props.html.erb +2 -2
  28. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_input_props.jsx +1 -1
  29. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_meter_settings.html.erb +318 -5
  30. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_meter_settings.jsx +134 -48
  31. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_meter_settings.md +11 -5
  32. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_strength_change.html.erb +123 -0
  33. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_strength_change.jsx +96 -20
  34. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_strength_change.md +6 -2
  35. data/app/pb_kits/playbook/pb_passphrase/docs/example.yml +4 -0
  36. data/app/pb_kits/playbook/pb_passphrase/docs/index.js +1 -0
  37. data/app/pb_kits/playbook/pb_passphrase/passphrase.html.erb +1 -1
  38. data/app/pb_kits/playbook/pb_passphrase/passphrase.rb +5 -9
  39. data/app/pb_kits/playbook/pb_passphrase/passphrase.test.jsx +0 -47
  40. data/app/pb_kits/playbook/pb_treemap_chart/_treemap_chart.jsx +79 -0
  41. data/app/pb_kits/playbook/pb_treemap_chart/docs/_treemap_chart_tooltip.jsx +1 -1
  42. data/app/pb_kits/playbook/playbook-rails-react-bindings.js +0 -4
  43. data/app/pb_kits/playbook/playbook-rails.js +4 -0
  44. data/app/pb_kits/playbook/plugins/pb_chart.js +322 -0
  45. data/lib/playbook/version.rb +1 -1
  46. metadata +15 -16
  47. data/app/pb_kits/playbook/pb_bar_graph/_bar_graph.tsx +0 -145
  48. data/app/pb_kits/playbook/pb_circle_chart/ChartsTypes.ts +0 -2
  49. data/app/pb_kits/playbook/pb_circle_chart/_circle_chart.tsx +0 -216
  50. data/app/pb_kits/playbook/pb_dashboard/pbChartsColorsHelper.ts +0 -16
  51. data/app/pb_kits/playbook/pb_dashboard/themeTypes.ts +0 -16
  52. data/app/pb_kits/playbook/pb_gauge/_gauge.tsx +0 -213
  53. data/app/pb_kits/playbook/pb_line_graph/_line_graph.tsx +0 -148
  54. data/app/pb_kits/playbook/pb_passphrase/docs/_passphrase_default.md +0 -1
  55. data/app/pb_kits/playbook/pb_passphrase/passwordStrength.js +0 -55
  56. data/app/pb_kits/playbook/pb_passphrase/useHaveIBeenPwned.js +0 -52
  57. data/app/pb_kits/playbook/pb_passphrase/useZxcvbn.js +0 -58
  58. data/app/pb_kits/playbook/pb_treemap_chart/_treemap_chart.tsx +0 -111
@@ -22,10 +22,15 @@ module Playbook
22
22
  prop :max, type: Playbook::Props::Numeric, default: 100
23
23
  prop :colors, type: Playbook::Props::Array, default: []
24
24
 
25
+ def chart_data_formatted
26
+ chart_data.map { |hash| hash[:y] = hash.delete :value }
27
+ chart_data
28
+ end
29
+
25
30
  def chart_options
26
31
  {
27
32
  id: id,
28
- chartData: chart_data,
33
+ chartData: chart_data_formatted,
29
34
  circumference: full_circle ? [0, 360] : [-100, 100],
30
35
  dark: dark ? "dark" : "",
31
36
  disableAnimation: disable_animation,
@@ -38,9 +43,9 @@ module Playbook
38
43
  showLabels: show_labels,
39
44
  style: style,
40
45
  tooltipHtml: tooltip_html,
41
- type: style,
46
+ type: "gauge",
42
47
  colors: colors,
43
- }
48
+ }.to_json.html_safe
44
49
  end
45
50
 
46
51
  def classname
@@ -0,0 +1,113 @@
1
+ /* @flow */
2
+
3
+ import React from 'react'
4
+ import classnames from 'classnames'
5
+
6
+ import { globalProps } from '../utilities/globalProps'
7
+ import pbChart from '../plugins/pb_chart'
8
+
9
+ type LineGraphProps = {
10
+ align?: "left" | "right" | "center",
11
+ axisTitle?: string,
12
+ dark?: Boolean,
13
+ xAxisCategories: array,
14
+ yAxisMin: number,
15
+ yAxisMax: number,
16
+ className?: string,
17
+ chartData: array<{
18
+ name: string,
19
+ data: array<number>,
20
+ }>,
21
+ gradient?: boolean,
22
+ id: string,
23
+ pointStart: number,
24
+ subTitle?: string,
25
+ title: string,
26
+ type?: string,
27
+ legend?: boolean,
28
+ toggleLegendClick?: boolean,
29
+ height?: string,
30
+ colors: array,
31
+ layout?: "horizontal" | "vertical" | "proximate",
32
+ verticalAlign?: "top" | "middle" | "bottom",
33
+ x?: number,
34
+ y?: number,
35
+ }
36
+
37
+ export default class LineGraph extends React.Component<LineGraphProps> {
38
+ static defaultProps = {
39
+ align: "center",
40
+ className: 'pb_bar_graph',
41
+ dark: false,
42
+ gradient: false,
43
+ type: 'line',
44
+ legend: false,
45
+ toggleLegendClick: true,
46
+ layout: "horizontal",
47
+ verticalAlign: "bottom",
48
+ x: 0,
49
+ y: 0,
50
+ }
51
+
52
+ componentDidMount() {
53
+ const {
54
+ align,
55
+ axisTitle,
56
+ dark,
57
+ xAxisCategories,
58
+ yAxisMin,
59
+ yAxisMax,
60
+ className,
61
+ chartData,
62
+ id,
63
+ pointStart,
64
+ subTitle,
65
+ title,
66
+ type,
67
+ legend,
68
+ toggleLegendClick,
69
+ height,
70
+ colors = [],
71
+ layout,
72
+ verticalAlign,
73
+ x,
74
+ y,
75
+ } = this.props
76
+
77
+ new pbChart(`.${className}`, {
78
+ align,
79
+ axisTitle: axisTitle,
80
+ chartData: chartData,
81
+ colors: colors,
82
+ dark,
83
+ id: id,
84
+ pointStart: pointStart,
85
+ subtitle: subTitle,
86
+ type,
87
+ title: title,
88
+ xAxisCategories: xAxisCategories,
89
+ yAxisMin: yAxisMin,
90
+ yAxisMax: yAxisMax,
91
+ legend: legend,
92
+ toggleLegendClick: toggleLegendClick,
93
+ height: height,
94
+ layout,
95
+ verticalAlign,
96
+ x,
97
+ y,
98
+ })
99
+ }
100
+
101
+ props: LineGraphProps
102
+
103
+ render() {
104
+ const { className, id } = this.props
105
+
106
+ return (
107
+ <div
108
+ className={classnames(globalProps(this.props), className)}
109
+ id={id}
110
+ />
111
+ )
112
+ }
113
+ }
@@ -1,14 +1,10 @@
1
1
  /* @flow */
2
- /* eslint-disable react-hooks/rules-of-hooks */
3
2
 
4
- import React, { useCallback, useEffect, useMemo, useState } from 'react'
5
- import classnames from 'classnames'
3
+ import React, { useCallback, useMemo, useState } from "react"
4
+ import classnames from "classnames"
6
5
 
7
- import { buildAriaProps, buildCss, buildDataProps } from '../utilities/props'
8
- import { globalProps } from '../utilities/globalProps'
9
-
10
- import useZxcvbn from './useZxcvbn'
11
- import useHaveIBeenPwned from './useHaveIBeenPwned'
6
+ import { buildAriaProps, buildCss, buildDataProps } from "../utilities/props"
7
+ import { globalProps } from "../utilities/globalProps"
12
8
 
13
9
  import Body from '../pb_body/_body'
14
10
  import Caption from '../pb_caption/_caption'
@@ -16,14 +12,10 @@ import CircleIconButton from '../pb_circle_icon_button/_circle_icon_button'
16
12
  import Flex from '../pb_flex/_flex'
17
13
  import Icon from '../pb_icon/_icon'
18
14
  import PbReactPopover from '../pb_popover/_popover'
19
- import ProgressSimple from '../pb_progress_simple/_progress_simple'
20
15
  import TextInput from '../pb_text_input/_text_input'
21
16
 
22
17
  type PassphraseProps = {
23
18
  aria?: object,
24
- averageThreshold?: number,
25
- checkPwned?: boolean,
26
- common?: boolean,
27
19
  confirmation?: boolean,
28
20
  className?: string,
29
21
  data?: object,
@@ -31,11 +23,8 @@ type PassphraseProps = {
31
23
  id?: string,
32
24
  inputProps?: {},
33
25
  label?: string,
34
- minLength?: number,
35
26
  onChange: (String) => void,
36
- showTipsBelow?: 'always' | 'xs' | 'sm' | 'md' | 'lg' | 'xl',
37
- onStrengthChange?: (number) => void,
38
- strongThreshold?: number,
27
+ showTipsBelow?: "always" | "xs" | "sm" | "md" | "lg" | "xl",
39
28
  tips?: Array<string>,
40
29
  uncontrolled?: boolean,
41
30
  value: string,
@@ -44,71 +33,56 @@ type PassphraseProps = {
44
33
  const Passphrase = (props: PassphraseProps) => {
45
34
  const {
46
35
  aria = {},
47
- averageThreshold = 2,
48
- checkPwned = false,
49
36
  className,
50
- common = false,
51
37
  confirmation = false,
52
- dark = false,
53
38
  data = {},
39
+ dark = false,
54
40
  id,
55
41
  inputProps = {},
56
- label = confirmation ? 'Confirm Passphrase' : 'Passphrase',
57
- minLength = 12,
42
+ label = confirmation ? "Confirm Passphrase" : "Passphrase",
58
43
  onChange = () => {},
59
- showTipsBelow = 'always',
60
- onStrengthChange,
61
- strongThreshold = 3,
44
+ showTipsBelow = "always",
62
45
  tips = [],
63
46
  uncontrolled = false,
64
- value = '',
47
+ value = "",
65
48
  } = props
66
- const ariaProps = buildAriaProps(aria)
67
49
 
68
- const [uncontrolledValue, setUncontrolledValue] = useState('')
50
+ const [uncontrolledValue, setUncontrolledValue] = useState("")
51
+ const [showPopover, setShowPopover] = useState(false)
52
+ const [showPassphrase, setShowPassphrase] = useState(false)
69
53
 
70
54
  const handleChange = useCallback(
71
- (e) => uncontrolled ? setUncontrolledValue(e.target.value) : onChange(e),
55
+ (e) => (uncontrolled ? setUncontrolledValue(e.target.value) : onChange(e)),
72
56
  [uncontrolled, onChange]
73
57
  )
74
58
 
75
59
  const displayValue = useMemo(
76
60
  () => (uncontrolled ? uncontrolledValue : value),
77
- [value, uncontrolledValue, uncontrolled],
61
+ [value, uncontrolledValue, uncontrolled]
78
62
  )
79
63
 
80
- const [showPopover, setShowPopover] = useState(false)
81
64
  const toggleShowPopover = () => setShowPopover(!showPopover)
82
65
  const handleShouldClosePopover = (shouldClosePopover) => {
83
66
  setShowPopover(!shouldClosePopover)
84
67
  }
85
68
 
86
- const [showPassphrase, setShowPassphrase] = useState(false)
87
69
  const toggleShowPassphrase = (e) => {
88
70
  e.preventDefault()
89
71
  setShowPassphrase(!showPassphrase)
90
72
  }
91
73
 
92
- const classes = classnames(buildCss('pb_passphrase'), globalProps(props), className)
93
-
94
- const isPwned = checkPwned ? useHaveIBeenPwned(displayValue, minLength) : false
95
-
96
- const { percent: progressPercent, variant: progressVariant, text: strengthLabel, strength } = useZxcvbn({ passphrase: displayValue, common, isPwned, confirmation, averageThreshold, minLength, strongThreshold })
97
-
98
- useEffect(() => {
99
- if (typeof onStrengthChange === 'function') {
100
- onStrengthChange(strength)
101
- }
102
- }, [strength])
103
-
104
74
  const tipClass = classnames(
105
- 'passphrase-popover',
106
- (showTipsBelow === 'always' ? null : `show-below-${showTipsBelow}`),
75
+ "passphrase-popover",
76
+ showTipsBelow === "always" ? null : `show-below-${showTipsBelow}`
107
77
  )
108
- const dataProps = useMemo(
109
- () => (buildDataProps(Object.assign({}, data, { strength }))),
110
- [data, strength]
78
+
79
+ const ariaProps = buildAriaProps(aria)
80
+ const classes = classnames(
81
+ buildCss("pb_passphrase"),
82
+ globalProps(props),
83
+ className
111
84
  )
85
+ const dataProps = buildDataProps(data)
112
86
 
113
87
  const popoverReference = (
114
88
  <CircleIconButton
@@ -121,17 +95,17 @@ const Passphrase = (props: PassphraseProps) => {
121
95
  )
122
96
 
123
97
  return (
124
- <div
125
- {...ariaProps}
126
- {...dataProps}
127
- className={classes}
98
+ <div
99
+ {...ariaProps}
100
+ {...dataProps}
101
+ className={classes}
128
102
  id={id}
129
103
  >
130
104
  <label>
131
105
  <Flex align="baseline">
132
- <Caption
133
- className="passphrase-label"
134
- text={label}
106
+ <Caption
107
+ className="passphrase-label"
108
+ text={label}
135
109
  />
136
110
  <If condition={tips.length > 0 && !confirmation}>
137
111
  <PbReactPopover
@@ -142,30 +116,28 @@ const Passphrase = (props: PassphraseProps) => {
142
116
  shouldClosePopover={handleShouldClosePopover}
143
117
  show={showPopover}
144
118
  >
145
- <Flex
146
- align="center"
119
+ <Flex
120
+ align="center"
147
121
  orientation="column"
148
122
  >
149
- <Caption
150
- marginBottom="xs"
151
- text="Tips for a good passphrase"
123
+ <Caption
124
+ marginBottom="xs"
125
+ text="Tips for a good passphrase"
152
126
  />
153
127
  <div>
154
- {
155
- tips.map((tip, i) => (
156
- <Caption
157
- key={i}
158
- marginBottom="xs"
159
- size="xs"
160
- >
161
- <Icon
162
- icon="shield-check"
163
- marginRight="xs"
164
- />
165
- {tip}
166
- </Caption>
167
- ))
168
- }
128
+ {tips.map((tip, i) => (
129
+ <Caption
130
+ key={i}
131
+ marginBottom="xs"
132
+ size="xs"
133
+ >
134
+ <Icon
135
+ icon="shield-check"
136
+ marginRight="xs"
137
+ />
138
+ {tip}
139
+ </Caption>
140
+ ))}
169
141
  </div>
170
142
  </Flex>
171
143
  </PbReactPopover>
@@ -178,23 +150,23 @@ const Passphrase = (props: PassphraseProps) => {
178
150
  marginBottom="xs"
179
151
  onChange={handleChange}
180
152
  placeholder="Enter a passphrase..."
181
- type={showPassphrase ? 'text' : 'password'}
153
+ type={showPassphrase ? "text" : "password"}
182
154
  value={displayValue}
183
155
  {...inputProps}
184
156
  />
185
- <span
186
- className="show-passphrase-icon"
157
+ <span
158
+ className="show-passphrase-icon"
187
159
  onClick={toggleShowPassphrase}
188
160
  >
189
161
  <Body
190
- className={showPassphrase ? 'hide-icon' : ''}
162
+ className={showPassphrase ? "hide-icon" : ""}
191
163
  color="light"
192
164
  dark={dark}
193
165
  >
194
166
  <Icon icon="eye-slash" />
195
167
  </Body>
196
168
  <Body
197
- className={showPassphrase ? '' : 'hide-icon'}
169
+ className={showPassphrase ? "" : "hide-icon"}
198
170
  color="light"
199
171
  dark={dark}
200
172
  >
@@ -203,21 +175,8 @@ const Passphrase = (props: PassphraseProps) => {
203
175
  </span>
204
176
  </div>
205
177
  </label>
206
- <If condition={!confirmation}>
207
- <ProgressSimple
208
- className={displayValue.length === 0 ? 'progress-empty-input' : null}
209
- dark={dark}
210
- percent={progressPercent}
211
- variant={progressVariant}
212
- />
213
- <Caption
214
- dark={dark}
215
- size="xs"
216
- text={strengthLabel}
217
- />
218
- </If>
219
178
  </div>
220
- )
221
- }
179
+ );
180
+ };
222
181
 
223
- export default Passphrase
182
+ export default Passphrase;
@@ -1 +1,145 @@
1
- <%= pb_rails("passphrase", props: { check_pwned: true }) %>
1
+ <%= pb_rails("passphrase", props: { label: "Passphrase", classname: "passphrase_breached" }) %>
2
+
3
+ <%= pb_rails("progress_simple", props: { percent: 0, id: "bar_breached" }) %>
4
+
5
+ <%= pb_rails("caption", props: { size: 'xs', text: "hello", id: "caption_breached" }) %>
6
+
7
+ <%= javascript_tag do %>
8
+ window.addEventListener("DOMContentLoaded", () => {
9
+
10
+ // variables for the kits you are targeting
11
+ const passphrase = document.querySelector(".passphrase_breached").querySelector("input")
12
+ const barVariant = document.getElementById("bar_breached")
13
+ const barPercent = document.getElementById("bar_breached").querySelector("div")
14
+ const caption = document.getElementById("caption_breached")
15
+
16
+ // hide the bar and captions
17
+ barVariant.style.display = 'none';
18
+ barPercent.style.display = 'none';
19
+ caption.style.display = 'none';
20
+
21
+
22
+ const handleStrengthCalculation = (settings) => {
23
+ const {
24
+ passphrase = "",
25
+ common = false,
26
+ isPwned = false,
27
+ averageThreshold = 2,
28
+ minLength = 12,
29
+ strongThreshold = 3,
30
+ } = settings
31
+
32
+ const resultByScore = {
33
+ 0: {
34
+ variant: 'negative',
35
+ label: '',
36
+ percent: 0,
37
+ },
38
+ 1: {
39
+ variant: 'negative',
40
+ label: 'This passphrase is too common',
41
+ percent: 25,
42
+ },
43
+ 2: {
44
+ variant: 'negative',
45
+ label: 'Too weak',
46
+ percent: 25,
47
+ },
48
+ 3: {
49
+ variant: 'warning',
50
+ label: 'Almost there, keep going!',
51
+ percent: 50,
52
+ },
53
+ 4: {
54
+ variant: 'positive',
55
+ label: 'Success! Strong passphrase',
56
+ percent: 100,
57
+ }
58
+ }
59
+
60
+ const { score } = zxcvbn(passphrase);
61
+
62
+ const noPassphrase = passphrase.length <= 0
63
+ const commonPassphrase = common || isPwned
64
+ const weakPassphrase = passphrase.length < minLength || score < averageThreshold
65
+ const averagePassphrase = score < strongThreshold
66
+ const strongPassphrase = score >= strongThreshold
67
+
68
+ if (noPassphrase) {
69
+ return {...resultByScore[0], score}
70
+ } else if (commonPassphrase) {
71
+ return {...resultByScore[1], score}
72
+ } else if (weakPassphrase) {
73
+ return {...resultByScore[2], score}
74
+ } else if (averagePassphrase){
75
+ return {...resultByScore[3], score}
76
+ } else if (strongPassphrase) {
77
+ return {...resultByScore[4], score}
78
+ }
79
+ }
80
+
81
+ // event listeners attached to the input field
82
+ passphrase.addEventListener('input', (e) => {
83
+ const passphrase = e.target.value;
84
+
85
+ let pwndMatch = false
86
+
87
+ const checkHaveIBeenPwned = async function (passphrase) {
88
+ const buffer = new TextEncoder('utf-8').encode(passphrase)
89
+ const digest = await crypto.subtle.digest('SHA-1', buffer)
90
+ const hashArray = Array.from(new Uint8Array(digest))
91
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
92
+
93
+ const firstFive = hashHex.slice(0, 5)
94
+ const endOfHash = hashHex.slice(5)
95
+
96
+ const resp = await fetch(`https://api.pwnedpasswords.com/range/${firstFive}`)
97
+ const text = await resp.text()
98
+
99
+ if (passphrase.length < 5) {
100
+ pwndMatch = false
101
+ }
102
+ else {
103
+ pwndMatch = text.split('\n').some((line) => {
104
+ return line.split(':')[0] === endOfHash.toUpperCase()
105
+ })
106
+ }
107
+
108
+ // pass in passphrase and isPwnd match to the handleStrengthCalculation and set that equal to result variable
109
+ const result = handleStrengthCalculation({ passphrase: passphrase, isPwned: pwndMatch });
110
+
111
+
112
+ // conditional statment to show or hide progress_simple bar and caption if user has entered a password
113
+ if (passphrase) {
114
+ barVariant.style.display = 'block';
115
+
116
+ barPercent.style.display = 'block';
117
+
118
+ caption.style.display = 'block';
119
+ } else {
120
+ barVariant.style.display = 'none';
121
+
122
+ barPercent.style.display = 'none';
123
+
124
+ caption.style.display = 'none';
125
+ }
126
+
127
+ // set the width of the progress_simple kit
128
+ barPercent.style.width = result.percent.toString()+ "%"
129
+
130
+
131
+ // set the variant of the progress_simple kit
132
+ barVariant.setAttribute("class", "pb_progress_simple_kit_"+ result.variant +"_left");
133
+
134
+
135
+ // set the text of the caption kit
136
+ caption.textContent = result.label
137
+ }
138
+
139
+ checkHaveIBeenPwned(passphrase)
140
+
141
+
142
+ });
143
+
144
+ })
145
+ <% end %>
@@ -1,22 +1,146 @@
1
- import React, { useState } from 'react'
1
+ import React, { useState, useEffect } from 'react'
2
2
 
3
- import Passphrase from '../_passphrase'
3
+ import { Caption, Passphrase, ProgressSimple } from '../..'
4
+ import zxcvbn from 'zxcvbn'
4
5
 
5
6
  const PassphraseBreached = (props) => {
6
7
  const [input, setInput] = useState('')
8
+ const [isPwned, setIsPwned] = useState(false)
9
+ const [checkStrength, setCheckStrength] = useState({
10
+ label: '',
11
+ percent: 0,
12
+ score: 0,
13
+ variant: ''
14
+ })
15
+
7
16
 
8
17
  const handleChange = (e) => setInput(e.target.value)
9
18
 
19
+ const checkHaveIBeenPwned = async function (passphrase) {
20
+ const buffer = new TextEncoder('utf-8').encode(passphrase)
21
+ const digest = await crypto.subtle.digest('SHA-1', buffer)
22
+ const hashArray = Array.from(new Uint8Array(digest))
23
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
24
+
25
+ const firstFive = hashHex.slice(0, 5)
26
+ const endOfHash = hashHex.slice(5)
27
+
28
+ const resp = await fetch(`https://api.pwnedpasswords.com/range/${firstFive}`)
29
+ const text = await resp.text()
30
+
31
+ const match = text.split('\n').some((line) => {
32
+ //Each line is <sha-1-hash-suffix>:<count of incidents>
33
+ return line.split(':')[0] === endOfHash.toUpperCase()
34
+ })
35
+ return match
36
+ }
37
+
38
+ const handleStrengthCalculation = (settings) => {
39
+ const {
40
+ passphrase = "",
41
+ common = false,
42
+ isPwned = false,
43
+ averageThreshold = 2,
44
+ minLength = 12,
45
+ strongThreshold = 3,
46
+ } = settings
47
+
48
+ const resultByScore = {
49
+ 0: {
50
+ variant: 'negative',
51
+ label: '',
52
+ percent: 0,
53
+ },
54
+ 1: {
55
+ variant: 'negative',
56
+ label: 'This passphrase is too common',
57
+ percent: 25,
58
+ },
59
+ 2: {
60
+ variant: 'negative',
61
+ label: 'Too weak',
62
+ percent: 25,
63
+ },
64
+ 3: {
65
+ variant: 'warning',
66
+ label: 'Almost there, keep going!',
67
+ percent: 50,
68
+ },
69
+ 4: {
70
+ variant: 'positive',
71
+ label: 'Success! Strong passphrase',
72
+ percent: 100,
73
+ }
74
+ }
75
+
76
+ const { score } = zxcvbn(passphrase);
77
+
78
+ const noPassphrase = passphrase.length <= 0
79
+ const commonPassphrase = common || isPwned
80
+ const weakPassphrase = passphrase.length < minLength || score < averageThreshold
81
+ const averagePassphrase = score < strongThreshold
82
+ const strongPassphrase = score >= strongThreshold
83
+
84
+ if (noPassphrase) {
85
+ return {...resultByScore[0], score}
86
+ } else if (commonPassphrase) {
87
+ return {...resultByScore[1], score}
88
+ } else if (weakPassphrase) {
89
+ return {...resultByScore[2], score}
90
+ } else if (averagePassphrase){
91
+ return {...resultByScore[3], score}
92
+ } else if (strongPassphrase) {
93
+ return {...resultByScore[4], score}
94
+ }
95
+ }
96
+
97
+ useEffect(
98
+ () => {
99
+ const delay = 400
100
+ const result = handleStrengthCalculation({ passphrase: input, isPwned: isPwned });
101
+
102
+ setCheckStrength({ ...result })
103
+
104
+ // only check the API for passphrases above the minimum size
105
+ if (input.length < 5) {
106
+ setIsPwned(false)
107
+ return
108
+ }
109
+
110
+ const handler = setTimeout(() => {
111
+ checkHaveIBeenPwned(input)
112
+ .then((pwned) => setIsPwned(pwned))
113
+ .catch(() => setIsPwned(false))
114
+ }, delay)
115
+
116
+ return () => {
117
+ clearTimeout(handler)
118
+ }
119
+ },
120
+ [input, isPwned]
121
+ )
122
+
10
123
  return (
11
124
  <>
12
125
  <div>
13
126
  <br />
14
127
  <Passphrase
15
- checkPwned
16
128
  onChange={handleChange}
17
129
  value={input}
18
130
  {...props}
19
131
  />
132
+ {checkStrength.percent > 0 ?
133
+ <>
134
+ <ProgressSimple
135
+ className={input.length === 0 ? "progress-empty-input" : null}
136
+ percent={checkStrength.percent}
137
+ variant={checkStrength.variant}
138
+ />
139
+ <Caption size='xs'
140
+ text={checkStrength.label}
141
+ />
142
+ </>
143
+ : null}
20
144
  </div>
21
145
  </>
22
146
  )