@standardnotes/authenticator 2.3.5

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.
@@ -0,0 +1,365 @@
1
+ import ConfirmDialog from '@Components/ConfirmDialog'
2
+ import DataErrorAlert from '@Components/DataErrorAlert'
3
+ import EditEntry from '@Components/EditEntry'
4
+ import ViewEntries from '@Components/ViewEntries'
5
+ import EditorKit from '@standardnotes/editor-kit'
6
+ import update from 'immutability-helper'
7
+ import React from 'react'
8
+ import ReorderIcon from '../assets/svg/reorder-icon.svg'
9
+ import CopyNotification from './CopyNotification'
10
+
11
+ const initialState = {
12
+ text: '',
13
+ entries: [],
14
+ parseError: false,
15
+ editMode: false,
16
+ editEntry: null,
17
+ confirmRemove: false,
18
+ confirmReorder: false,
19
+ displayCopy: false,
20
+ canEdit: true,
21
+ searchValue: '',
22
+ lastUpdated: 0,
23
+ }
24
+
25
+ export default class Home extends React.Component {
26
+ constructor(props) {
27
+ super(props)
28
+ this.configureEditorKit()
29
+ this.state = initialState
30
+ }
31
+
32
+ configureEditorKit() {
33
+ const delegate = {
34
+ setEditorRawText: (text) => {
35
+ let parseError = false
36
+ let entries = []
37
+
38
+ if (text) {
39
+ try {
40
+ entries = this.parseNote(text)
41
+ } catch (e) {
42
+ // Couldn't parse the content
43
+ parseError = true
44
+ this.setState({
45
+ parseError: true,
46
+ })
47
+ }
48
+ }
49
+
50
+ this.setState({
51
+ ...initialState,
52
+ text,
53
+ parseError,
54
+ entries,
55
+ })
56
+ },
57
+ generateCustomPreview: (text) => {
58
+ let entries = []
59
+ try {
60
+ entries = this.parseNote(text)
61
+ } finally {
62
+ // eslint-disable-next-line no-unsafe-finally
63
+ return {
64
+ html: `<div><strong>${entries.length}</strong> TokenVault Entries </div>`,
65
+ plain: `${entries.length} TokenVault Entries`,
66
+ }
67
+ }
68
+ },
69
+ clearUndoHistory: () => {},
70
+ getElementsBySelector: () => [],
71
+ onNoteLockToggle: (isLocked) => {
72
+ this.setState({
73
+ canEdit: !isLocked,
74
+ })
75
+ },
76
+ onThemesChange: () => {
77
+ this.setState({
78
+ lastUpdated: Date.now(),
79
+ })
80
+ },
81
+ handleRequestForContentHeight: () => {
82
+ return document.body.scrollHeight
83
+ },
84
+ }
85
+
86
+ this.editorKit = new EditorKit(delegate, {
87
+ mode: 'json',
88
+ })
89
+ }
90
+
91
+ parseNote(text) {
92
+ const entries = JSON.parse(text)
93
+
94
+ if (entries instanceof Array) {
95
+ if (entries.length === 0) {
96
+ return []
97
+ }
98
+
99
+ for (const entry of entries) {
100
+ if (!('service' in entry)) {
101
+ throw Error('Service key is missing for an entry.')
102
+ }
103
+
104
+ if (!('secret' in entry) && !('password' in entry)) {
105
+ throw Error('An entry does not have a secret key or a password.')
106
+ }
107
+ }
108
+
109
+ return entries
110
+ }
111
+
112
+ return []
113
+ }
114
+
115
+ saveNote(entries) {
116
+ this.editorKit.onEditorValueChanged(JSON.stringify(entries, null, 2))
117
+ }
118
+
119
+ // Entry operations
120
+ addEntry = (entry) => {
121
+ this.setState((state) => {
122
+ const entries = state.entries.concat([entry])
123
+ this.saveNote(entries)
124
+
125
+ return {
126
+ editMode: false,
127
+ editEntry: null,
128
+ entries,
129
+ }
130
+ })
131
+ }
132
+
133
+ editEntry = ({ id, entry }) => {
134
+ this.setState((state) => {
135
+ const entries = update(state.entries, { [id]: { $set: entry } })
136
+ this.saveNote(entries)
137
+
138
+ return {
139
+ editMode: false,
140
+ editEntry: null,
141
+ entries,
142
+ }
143
+ })
144
+ }
145
+
146
+ removeEntry = (id) => {
147
+ this.setState((state) => {
148
+ const entries = update(state.entries, { $splice: [[id, 1]] })
149
+ this.saveNote(entries)
150
+
151
+ return {
152
+ confirmRemove: false,
153
+ editEntry: null,
154
+ entries,
155
+ }
156
+ })
157
+ }
158
+
159
+ // Event Handlers
160
+ onAddNew = () => {
161
+ if (!this.state.canEdit) {
162
+ return
163
+ }
164
+ this.setState({
165
+ editMode: true,
166
+ editEntry: null,
167
+ })
168
+ }
169
+
170
+ onEdit = (id) => {
171
+ if (!this.state.canEdit) {
172
+ return
173
+ }
174
+ this.setState((state) => ({
175
+ editMode: true,
176
+ editEntry: {
177
+ id,
178
+ entry: state.entries[id],
179
+ },
180
+ }))
181
+ }
182
+
183
+ onCancel = () => {
184
+ this.setState({
185
+ confirmRemove: false,
186
+ confirmReorder: false,
187
+ editMode: false,
188
+ editEntry: null,
189
+ })
190
+ }
191
+
192
+ onRemove = (id) => {
193
+ if (!this.state.canEdit) {
194
+ return
195
+ }
196
+ this.setState((state) => ({
197
+ confirmRemove: true,
198
+ editEntry: {
199
+ id,
200
+ entry: state.entries[id],
201
+ },
202
+ }))
203
+ }
204
+
205
+ onSave = ({ id, entry }) => {
206
+ // If there's no ID it's a new note
207
+ if (id != null) {
208
+ this.editEntry({ id, entry })
209
+ } else {
210
+ this.addEntry(entry)
211
+ }
212
+ }
213
+
214
+ onCopyValue = () => {
215
+ this.setState({
216
+ displayCopy: true,
217
+ })
218
+
219
+ if (this.clearTooltipTimer) {
220
+ clearTimeout(this.clearTooltipTimer)
221
+ }
222
+
223
+ this.clearTooltipTimer = setTimeout(() => {
224
+ this.setState({
225
+ displayCopy: false,
226
+ })
227
+ }, 2000)
228
+ }
229
+
230
+ updateEntries = (entries) => {
231
+ this.saveNote(entries)
232
+ this.setState({
233
+ entries,
234
+ })
235
+ }
236
+
237
+ onReorderEntries = () => {
238
+ if (!this.state.canEdit) {
239
+ return
240
+ }
241
+ this.setState({
242
+ confirmReorder: true,
243
+ })
244
+ }
245
+
246
+ onSearchChange = (event) => {
247
+ const target = event.target
248
+ this.setState({
249
+ searchValue: target.value.toLowerCase(),
250
+ })
251
+ }
252
+
253
+ clearSearchValue = () => {
254
+ this.setState({
255
+ searchValue: '',
256
+ })
257
+ }
258
+
259
+ reorderEntries = () => {
260
+ const { entries } = this.state
261
+ const orderedEntries = entries.sort((a, b) => {
262
+ const serviceA = a.service.toLowerCase()
263
+ const serviceB = b.service.toLowerCase()
264
+ return serviceA < serviceB ? -1 : serviceA > serviceB ? 1 : 0
265
+ })
266
+ this.saveNote(orderedEntries)
267
+ this.setState({
268
+ entries: orderedEntries,
269
+ confirmReorder: false,
270
+ })
271
+ }
272
+
273
+ render() {
274
+ const editEntry = this.state.editEntry || {}
275
+ const {
276
+ canEdit,
277
+ displayCopy,
278
+ parseError,
279
+ editMode,
280
+ entries,
281
+ confirmRemove,
282
+ confirmReorder,
283
+ searchValue,
284
+ lastUpdated,
285
+ } = this.state
286
+
287
+ if (parseError) {
288
+ return (
289
+ <div className="sn-component">
290
+ <DataErrorAlert />
291
+ </div>
292
+ )
293
+ }
294
+
295
+ return (
296
+ <div className="sn-component">
297
+ <CopyNotification isVisible={displayCopy} />
298
+ {!editMode && (
299
+ <div id="header">
300
+ <div className={`sk-horizontal-group left align-items-center ${!canEdit && 'full-width'}`}>
301
+ <input
302
+ name="search"
303
+ className="sk-input contrast search-bar"
304
+ placeholder="Search entries..."
305
+ value={searchValue}
306
+ onChange={this.onSearchChange}
307
+ autoComplete="off"
308
+ type="text"
309
+ />
310
+ {searchValue && (
311
+ <div onClick={this.clearSearchValue} className="sk-button danger">
312
+ <div className="sk-label">✕</div>
313
+ </div>
314
+ )}
315
+ </div>
316
+ {canEdit && (
317
+ <div className="sk-horizontal-group right">
318
+ <div className="sk-button-group stretch">
319
+ <div onClick={this.onReorderEntries} className="sk-button info">
320
+ <ReorderIcon />
321
+ </div>
322
+ <div onClick={this.onAddNew} className="sk-button info">
323
+ <div className="sk-label">Add new</div>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ )}
328
+ </div>
329
+ )}
330
+ <div id="content">
331
+ {editMode ? (
332
+ <EditEntry id={editEntry.id} entry={editEntry.entry} onSave={this.onSave} onCancel={this.onCancel} />
333
+ ) : (
334
+ <ViewEntries
335
+ entries={entries}
336
+ searchValue={searchValue}
337
+ onEdit={this.onEdit}
338
+ onRemove={this.onRemove}
339
+ onCopyValue={this.onCopyValue}
340
+ canEdit={canEdit}
341
+ lastUpdated={lastUpdated}
342
+ updateEntries={this.updateEntries}
343
+ />
344
+ )}
345
+ {confirmRemove && (
346
+ <ConfirmDialog
347
+ title={`Remove ${editEntry.entry.service}`}
348
+ message="Are you sure you want to remove this entry?"
349
+ onConfirm={() => this.removeEntry(editEntry.id)}
350
+ onCancel={this.onCancel}
351
+ />
352
+ )}
353
+ {confirmReorder && (
354
+ <ConfirmDialog
355
+ title={'Auto-sort entries'}
356
+ message="Are you sure you want to auto-sort all entries alphabetically based on service name?"
357
+ onConfirm={this.reorderEntries}
358
+ onCancel={this.onCancel}
359
+ />
360
+ )}
361
+ </div>
362
+ </div>
363
+ )
364
+ }
365
+ }
@@ -0,0 +1,91 @@
1
+ import { parseKeyUri } from '@Lib/otp'
2
+ import jsQR from 'jsqr'
3
+ import PropTypes from 'prop-types'
4
+ import React from 'react'
5
+
6
+ const convertToGrayScale = (imageData) => {
7
+ if (!imageData) {
8
+ return
9
+ }
10
+
11
+ for (let i = 0; i < imageData.data.length; i += 4) {
12
+ const count = imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]
13
+ let color = 0
14
+
15
+ if (count > 510) {
16
+ color = 255
17
+ } else if (count > 255) {
18
+ color = 127.5
19
+ }
20
+
21
+ imageData.data[i] = color
22
+ imageData.data[i + 1] = color
23
+ imageData.data[i + 2] = color
24
+ imageData.data[i + 3] = 255
25
+ }
26
+
27
+ return imageData
28
+ }
29
+
30
+ export default class QRCodeReader extends React.Component {
31
+ constructor() {
32
+ super()
33
+
34
+ this.fileInputRef = React.createRef(null)
35
+ }
36
+
37
+ onImageSelected = (evt) => {
38
+ const file = evt.target.files[0]
39
+ const url = URL.createObjectURL(file)
40
+ const img = new Image()
41
+ const self = this
42
+
43
+ img.onload = function () {
44
+ URL.revokeObjectURL(this.src)
45
+
46
+ const canvas = document.createElement('canvas')
47
+ const context = canvas.getContext('2d')
48
+ canvas.width = this.width
49
+ canvas.height = this.height
50
+ context.drawImage(this, 0, 0)
51
+
52
+ let imageData = context.getImageData(0, 0, this.width, this.height)
53
+ imageData = convertToGrayScale(imageData)
54
+
55
+ const code = jsQR(imageData.data, imageData.width, imageData.height)
56
+
57
+ const { onError, onSuccess } = self.props
58
+
59
+ if (code) {
60
+ const otpData = parseKeyUri(code.data)
61
+ if (otpData.type !== 'totp') {
62
+ onError(`The '${otpData.type}' type is not supported.`)
63
+ } else {
64
+ onSuccess(otpData)
65
+ }
66
+ } else {
67
+ onError('Error reading QR code from image. Please try again.')
68
+ }
69
+ }
70
+
71
+ img.src = url
72
+
73
+ return false
74
+ }
75
+
76
+ render() {
77
+ return (
78
+ <div className="qr-code-reader-container">
79
+ <div className="sk-button info" onClick={() => this.fileInputRef.current.click()}>
80
+ <div className="sk-label">Upload QR Code</div>
81
+ <input type="file" style={{ display: 'none' }} ref={this.fileInputRef} onChange={this.onImageSelected} />
82
+ </div>
83
+ </div>
84
+ )
85
+ }
86
+ }
87
+
88
+ QRCodeReader.propTypes = {
89
+ onError: PropTypes.func.isRequired,
90
+ onSuccess: PropTypes.func.isRequired,
91
+ }
@@ -0,0 +1,82 @@
1
+ import AuthEntry from '@Components/AuthEntry'
2
+ import PropTypes from 'prop-types'
3
+ import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'
4
+
5
+ const reorderEntries = (list, startIndex, endIndex) => {
6
+ const result = Array.from(list)
7
+ const [removed] = result.splice(startIndex, 1)
8
+ result.splice(endIndex, 0, removed)
9
+
10
+ return result
11
+ }
12
+
13
+ const ViewEntries = ({ entries, onEdit, onRemove, onCopyValue, canEdit, updateEntries, searchValue, lastUpdated }) => {
14
+ const onDragEnd = (result) => {
15
+ const droppedOutsideList = !result.destination
16
+ if (droppedOutsideList) {
17
+ return
18
+ }
19
+
20
+ const orderedEntries = reorderEntries(entries, result.source.index, result.destination.index)
21
+
22
+ updateEntries(orderedEntries)
23
+ }
24
+
25
+ return (
26
+ <DragDropContext onDragEnd={onDragEnd}>
27
+ <Droppable droppableId="droppable" isDropDisabled={!canEdit}>
28
+ {(provided) => (
29
+ <div {...provided.droppableProps} ref={provided.innerRef} className="auth-list">
30
+ {entries.map((entry, index) => {
31
+ /**
32
+ * Filtering entries by account, service and notes properties.
33
+ */
34
+ const combinedString = `${entry.account}${entry.service}${entry.notes}`.toLowerCase()
35
+ if (searchValue && !combinedString.includes(searchValue)) {
36
+ return
37
+ }
38
+ return (
39
+ <Draggable
40
+ key={`${entry.service}-${index}`}
41
+ draggableId={`${entry.service}-${index}`}
42
+ index={index}
43
+ isDragDisabled={!canEdit}
44
+ >
45
+ {(provided) => (
46
+ <AuthEntry
47
+ {...provided.draggableProps}
48
+ {...provided.dragHandleProps}
49
+ innerRef={provided.innerRef}
50
+ key={index}
51
+ id={index}
52
+ entry={entry}
53
+ onEdit={onEdit}
54
+ onRemove={onRemove}
55
+ onCopyValue={onCopyValue}
56
+ canEdit={canEdit}
57
+ lastUpdated={lastUpdated}
58
+ />
59
+ )}
60
+ </Draggable>
61
+ )
62
+ })}
63
+ {provided.placeholder}
64
+ </div>
65
+ )}
66
+ </Droppable>
67
+ </DragDropContext>
68
+ )
69
+ }
70
+
71
+ ViewEntries.propTypes = {
72
+ entries: PropTypes.arrayOf(PropTypes.object),
73
+ onEdit: PropTypes.func.isRequired,
74
+ onRemove: PropTypes.func.isRequired,
75
+ onCopyValue: PropTypes.func.isRequired,
76
+ canEdit: PropTypes.bool.isRequired,
77
+ lastUpdated: PropTypes.number.isRequired,
78
+ updateEntries: PropTypes.func.isRequired,
79
+ searchValue: PropTypes.string,
80
+ }
81
+
82
+ export default ViewEntries
package/app/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import Home from '@Components/Home'
2
+ import ReactDOM from 'react-dom'
3
+
4
+ ReactDOM.render(<Home />, document.body.appendChild(document.createElement('div')))