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

Sign up to get free protection for your applications and to get access to all the features.
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
  )