feat: optional secret field (#1115)

* fix: add grab cursor to drag indicator

* feat: optional secret field

* chore: increment version

* chore: format files

* fix: import React automatically

* fix: qr code button not clickable

* fix: show entry types after title

* fix: remove stylekit references

* fix: import react

* chore: update yarn.lock

* fix: update webpack dev config

* fix: styles import path

* fix: skalert import

* fix: misc

* fix: build components using build:components script

* fix(tmp): @standardnotes/styles as a dependency for @standardnotes/components-meta

Co-authored-by: Johnny Almonte <johnny243@users.noreply.github.com>
This commit is contained in:
Johnny A
2022-06-22 10:32:43 -04:00
committed by GitHub
parent 6f6cbff855
commit 7c0938b877
36 changed files with 830 additions and 764 deletions

View File

@@ -23,9 +23,10 @@ jobs:
- name: Lint components - name: Lint components
run: yarn lint run: yarn lint
working-directory: packages/components working-directory: packages/components
- name: Build components - name: Build components
run: yarn build run: yarn build:components
working-directory: packages/components
- name: Test components - name: Test components
run: yarn test run: yarn test
working-directory: packages/components working-directory: packages/components

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -17,11 +17,13 @@
"package": "node scripts/package.mjs", "package": "node scripts/package.mjs",
"version": "./scripts/VERSION.sh" "version": "./scripts/VERSION.sh"
}, },
"dependencies": {
"@standardnotes/styles": "workspace:*"
},
"devDependencies": { "devDependencies": {
"@standardnotes/deterministic-zip": "^1.2.0", "@standardnotes/deterministic-zip": "^1.2.0",
"@standardnotes/eslint-config-extensions": "^1.0.4", "@standardnotes/eslint-config-extensions": "^1.0.4",
"@standardnotes/features": "^1.45.1", "@standardnotes/features": "^1.45.1",
"@standardnotes/styles": "workspace:*",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"mini-css-extract-plugin": "^2.6.0", "mini-css-extract-plugin": "^2.6.0",
"minimatch": "^5.1.0", "minimatch": "^5.1.0",

View File

@@ -3,10 +3,16 @@
[ [
"@babel/preset-env", "@babel/preset-env",
{ {
"modules": false "modules": false,
"targets": "defaults"
} }
], ],
"@babel/preset-react" [
"@babel/preset-react",
{
"runtime": "automatic"
}
]
], ],
"plugins": [ "plugins": [
"@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-class-properties",

View File

@@ -22,5 +22,9 @@
"react": { "react": {
"version": "detect" "version": "detect"
} }
},
"rules": {
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off"
} }
} }

View File

@@ -1,6 +1,6 @@
## LICENSE ## LICENSE
As of version 2.0, TokenVault houses a public-source, but not open-source presence. This means you are free to browse the code and even use the code *privately*, but you may not redistribute, repackage, or otherwise use its public distributable/release assets on GitHub, for free or for profit. As of version 2.0, TokenVault houses a public-source, but not open-source presence. This means you are free to browse the code and even use the code _privately_, but you may not redistribute, repackage, or otherwise use its public distributable/release assets on GitHub, for free or for profit.
For more information, read [this blog post](https://blog.standardnotes.com/why-tokenvault-is-going-public-source/). For more information, read [this blog post](https://blog.standardnotes.com/why-tokenvault-is-going-public-source/).

View File

@@ -1,14 +0,0 @@
import 'regenerator-runtime/runtime';
import React from 'react';
import Home from '@Components/Home';
export default class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return <Home />;
}
}

View File

@@ -1,14 +1,14 @@
import React from 'react'; import AuthMenu from '@Components/AuthMenu'
import PropTypes from 'prop-types'; import CountdownPie from '@Components/CountdownPie'
import { totp } from '@Lib/otp'; import { totp } from '@Lib/otp'
import CountdownPie from '@Components/CountdownPie'; import { getEntryColor, getVarColorForContrast, hexColorToRGB } from '@Lib/utils'
import AuthMenu from '@Components/AuthMenu'; import PropTypes from 'prop-types'
import DragIndicator from '../assets/svg/drag-indicator.svg'; import React from 'react'
import { getEntryColor, getVarColorForContrast, hexColorToRGB } from '@Lib/utils'; import DragIndicator from '../assets/svg/drag-indicator.svg'
export default class AuthEntry extends React.Component { export default class AuthEntry extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props)
this.state = { this.state = {
token: '', token: '',
@@ -16,69 +16,72 @@ export default class AuthEntry extends React.Component {
entryStyle: { entryStyle: {
color: '', color: '',
backgroundColor: '', backgroundColor: '',
},
} }
};
this.updateToken(); this.updateToken()
} }
getTimeLeft() { getTimeLeft() {
const seconds = new Date().getSeconds(); const seconds = new Date().getSeconds()
return seconds > 29 ? 60 - seconds : 30 - seconds; return seconds > 29 ? 60 - seconds : 30 - seconds
} }
updateToken = async () => { updateToken = async () => {
const { secret } = this.props.entry; const { secret } = this.props.entry
const token = await totp.gen(secret); if (!secret) {
return
}
const timeLeft = this.getTimeLeft(); const token = await totp.gen(secret)
const timeLeft = this.getTimeLeft()
this.setState({ this.setState({
token, token,
timeLeft timeLeft,
}); })
this.timer = setTimeout(this.updateToken, timeLeft * 1000); this.timer = setTimeout(this.updateToken, timeLeft * 1000)
} }
componentDidMount() { componentDidMount() {
this.updateEntryStyle(); this.updateEntryStyle()
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
// If the secret changed make sure to recalculate token // If the secret changed make sure to recalculate token
if (prevProps.entry.secret !== this.props.entry.secret) { if (prevProps.entry.secret !== this.props.entry.secret) {
clearTimeout(this.timer); clearTimeout(this.timer)
this.timer = setTimeout(this.updateToken, 0); this.timer = setTimeout(this.updateToken, 0)
} }
if (prevProps.lastUpdated !== this.props.lastUpdated) { if (prevProps.lastUpdated !== this.props.lastUpdated) {
this.updateEntryStyle(true); this.updateEntryStyle(true)
} }
} }
componentWillUnmount() { componentWillUnmount() {
clearTimeout(this.timer); clearTimeout(this.timer)
} }
handleInputChange = event => { handleInputChange = (event) => {
const target = event.target; const target = event.target
const name = target.name; const name = target.name
this.props.onEntryChange({ this.props.onEntryChange({
id: this.props.id, id: this.props.id,
name, name,
value: target.value value: target.value,
}); })
} }
copyToClipboard = (value) => { copyToClipboard = (value) => {
const textField = document.createElement('textarea'); const textField = document.createElement('textarea')
textField.innerText = value; textField.innerText = value
document.body.appendChild(textField); document.body.appendChild(textField)
textField.select(); textField.select()
document.execCommand('copy'); document.execCommand('copy')
textField.remove(); textField.remove()
this.props.onCopyValue(); this.props.onCopyValue()
} }
updateEntryStyle = (useDelay = false) => { updateEntryStyle = (useDelay = false) => {
@@ -86,36 +89,36 @@ export default class AuthEntry extends React.Component {
* A short amount of time to wait in order to prevent reading * A short amount of time to wait in order to prevent reading
* stale information from the DOM after a theme is activated. * stale information from the DOM after a theme is activated.
*/ */
const DELAY_BEFORE_READING_PROPERTIES = useDelay ? 0 : 50; const DELAY_BEFORE_READING_PROPERTIES = useDelay ? 0 : 50
setTimeout(() => { setTimeout(() => {
const { entryStyle } = this.state; const { entryStyle } = this.state
const entryColor = getEntryColor(document, this.props.entry); const entryColor = getEntryColor(document, this.props.entry)
if (entryColor) { if (entryColor) {
// The background color for the entry. // The background color for the entry.
entryStyle.backgroundColor = entryColor; entryStyle.backgroundColor = entryColor
const rgbColor = hexColorToRGB(entryColor); const rgbColor = hexColorToRGB(entryColor)
const varColor = getVarColorForContrast(rgbColor); const varColor = getVarColorForContrast(rgbColor)
// The foreground color for the entry. // The foreground color for the entry.
entryStyle.color = `var(${varColor})`; entryStyle.color = `var(${varColor})`
} }
this.setState({ this.setState({
entryStyle entryStyle,
}); })
}, DELAY_BEFORE_READING_PROPERTIES); }, DELAY_BEFORE_READING_PROPERTIES)
} }
render() { render() {
const { service, account, notes, password } = this.props.entry; const { service, account, notes, password, secret } = this.props.entry
const { id, onEdit, onRemove, canEdit, style, innerRef, ...divProps } = this.props; const { id, onEdit, onRemove, canEdit, style, innerRef, ...divProps } = this.props
const { token, timeLeft, entryStyle } = this.state; const { token, timeLeft, entryStyle } = this.state
delete divProps.onCopyValue; delete divProps.onCopyValue
delete divProps.lastUpdated; delete divProps.lastUpdated
return ( return (
<div <div
@@ -123,14 +126,14 @@ export default class AuthEntry extends React.Component {
className="sk-notification sk-base-custom" className="sk-notification sk-base-custom"
style={{ style={{
...entryStyle, ...entryStyle,
...style ...style,
}} }}
ref={innerRef} ref={innerRef}
> >
<div className="auth-entry"> <div className="auth-entry">
{canEdit && ( {canEdit && (
<div className="auth-drag-indicator-container"> <div className="auth-drag-indicator-container">
<DragIndicator /> <DragIndicator className="grab-cursor" />
</div> </div>
)} )}
<div className="auth-details"> <div className="auth-details">
@@ -146,12 +149,13 @@ export default class AuthEntry extends React.Component {
{password && ( {password && (
<div className="auth-password-row"> <div className="auth-password-row">
<div className="auth-password" onClick={() => this.copyToClipboard(password)}> <div className="auth-password" onClick={() => this.copyToClipboard(password)}>
{'•'.repeat(password.length)}
</div> </div>
</div> </div>
)} )}
</div> </div>
</div> </div>
{secret && (
<div className="auth-token-info"> <div className="auth-token-info">
<div className="auth-token" onClick={() => this.copyToClipboard(token)}> <div className="auth-token" onClick={() => this.copyToClipboard(token)}>
<div>{token.slice(0, 3)}</div> <div>{token.slice(0, 3)}</div>
@@ -167,6 +171,7 @@ export default class AuthEntry extends React.Component {
/> />
</div> </div>
</div> </div>
)}
</div> </div>
{canEdit && ( {canEdit && (
<div className="auth-options"> <div className="auth-options">
@@ -179,7 +184,7 @@ export default class AuthEntry extends React.Component {
)} )}
</div> </div>
</div> </div>
); )
} }
} }
@@ -193,5 +198,5 @@ AuthEntry.propTypes = {
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
innerRef: PropTypes.func.isRequired, innerRef: PropTypes.func.isRequired,
lastUpdated: PropTypes.number.isRequired, lastUpdated: PropTypes.number.isRequired,
style: PropTypes.object.isRequired style: PropTypes.object.isRequired,
}; }

View File

@@ -1,37 +1,37 @@
import React from 'react'; import PropTypes from 'prop-types'
import PropTypes from 'prop-types'; import React from 'react'
export default class AuthMenu extends React.Component { export default class AuthMenu extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props)
this.state = { this.state = {
show: false show: false,
}; }
} }
onToggle = () => { onToggle = () => {
this.setState({ this.setState({
show: !this.state.show show: !this.state.show,
}); })
} }
onEdit = () => { onEdit = () => {
this.onToggle(); this.onToggle()
this.props.onEdit(); this.props.onEdit()
} }
onRemove = () => { onRemove = () => {
this.onToggle(); this.onToggle()
this.props.onRemove(); this.props.onRemove()
} }
render() { render() {
const { buttonColor } = this.props; const { buttonColor } = this.props
const buttonStyle = {}; const buttonStyle = {}
if (buttonColor) { if (buttonColor) {
buttonStyle.color = buttonColor; buttonStyle.color = buttonColor
} }
return ( return (
@@ -39,8 +39,9 @@ export default class AuthMenu extends React.Component {
<div className="sk-button" onClick={this.onToggle} style={buttonStyle}> <div className="sk-button" onClick={this.onToggle} style={buttonStyle}>
<div className="sk-label"></div> <div className="sk-label"></div>
</div> </div>
{this.state.show && ( {this.state.show &&
<div className="auth-overlay" onClick={this.onToggle} />, ((<div className="auth-overlay" onClick={this.onToggle} />),
(
<div className="sk-menu-panel"> <div className="sk-menu-panel">
<div className="sk-menu-panel-row" onClick={this.onEdit}> <div className="sk-menu-panel-row" onClick={this.onEdit}>
<div className="sk-label">Edit</div> <div className="sk-label">Edit</div>
@@ -49,14 +50,14 @@ export default class AuthMenu extends React.Component {
<div className="sk-label">Remove</div> <div className="sk-label">Remove</div>
</div> </div>
</div> </div>
)} ))}
</div> </div>
); )
} }
} }
AuthMenu.propTypes = { AuthMenu.propTypes = {
onEdit: PropTypes.func.isRequired, onEdit: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired,
buttonColor: PropTypes.string buttonColor: PropTypes.string,
}; }

View File

@@ -1,5 +1,4 @@
import React from 'react'; import PropTypes from 'prop-types'
import PropTypes from 'prop-types';
const ConfirmDialog = ({ title, message, onConfirm, onCancel }) => ( const ConfirmDialog = ({ title, message, onConfirm, onCancel }) => (
<div className="auth-overlay"> <div className="auth-overlay">
@@ -26,13 +25,13 @@ const ConfirmDialog = ({ title, message, onConfirm, onCancel }) => (
</div> </div>
</div> </div>
</div> </div>
); )
ConfirmDialog.propTypes = { ConfirmDialog.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
message: PropTypes.string.isRequired, message: PropTypes.string.isRequired,
onConfirm: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired onCancel: PropTypes.func.isRequired,
}; }
export default ConfirmDialog; export default ConfirmDialog

View File

@@ -1,18 +1,15 @@
import React from 'react'; import PropTypes from 'prop-types'
import PropTypes from 'prop-types';
const CopyNotification = ({ isVisible }) => ( const CopyNotification = ({ isVisible }) => (
<div <div className={`auth-copy-notification ${isVisible ? 'visible' : 'hidden'}`}>
className={`auth-copy-notification ${isVisible ? 'visible' : 'hidden'}`}
>
<div className="sk-panel"> <div className="sk-panel">
<div className="sk-font-small sk-bold">Copied value to clipboard.</div> <div className="sk-font-small sk-bold">Copied value to clipboard.</div>
</div> </div>
</div> </div>
); )
CopyNotification.propTypes = { CopyNotification.propTypes = {
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
}; }
export default CopyNotification; export default CopyNotification

View File

@@ -1,11 +1,9 @@
import React, { useEffect } from 'react'; import PropTypes from 'prop-types'
import PropTypes from 'prop-types'; import { useEffect } from 'react'
const animationName = (token) => `countdown${token}`; const animationName = (token) => `countdown${token}`
const rotaAnimation = (token, offset) => `@keyframes rota_${animationName( const rotaAnimation = (token, offset) => `@keyframes rota_${animationName(token)} {
token
)} {
0% { 0% {
transform: rotate(${offset}deg); transform: rotate(${offset}deg);
} }
@@ -13,11 +11,9 @@ const rotaAnimation = (token, offset) => `@keyframes rota_${animationName(
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
}`; }`
const opaAnimation = (token, offset) => `@keyframes opa_${animationName( const opaAnimation = (token, offset) => `@keyframes opa_${animationName(token)} {
token
)} {
0% { 0% {
opacity: 1; opacity: 1;
} }
@@ -26,12 +22,9 @@ const opaAnimation = (token, offset) => `@keyframes opa_${animationName(
100% { 100% {
opacity: 0; opacity: 0;
} }
}`; }`
const opaReverseAnimation = ( const opaReverseAnimation = (token, offset) => `@keyframes opa_r_${animationName(token)} {
token,
offset
) => `@keyframes opa_r_${animationName(token)} {
0% { 0% {
opacity: 0; opacity: 0;
} }
@@ -40,81 +33,81 @@ const opaReverseAnimation = (
100% { 100% {
opacity: 1; opacity: 1;
} }
}`; }`
function calculateOpaOffset(timeLeft, total) { function calculateOpaOffset(timeLeft, total) {
const percentage = calculatePercentage(timeLeft, total) * 100; const percentage = calculatePercentage(timeLeft, total) * 100
const percTo50 = 50 - percentage; const percTo50 = 50 - percentage
// 8 is an offset because the animation is not in sync otherwise // 8 is an offset because the animation is not in sync otherwise
return percTo50 < 0 ? 0 : Math.ceil(Math.min(percTo50 + 8, 50)); return percTo50 < 0 ? 0 : Math.ceil(Math.min(percTo50 + 8, 50))
} }
function calculateRotaOffset(timeLeft, total) { function calculateRotaOffset(timeLeft, total) {
return calculatePercentage(timeLeft, total) * 360; return calculatePercentage(timeLeft, total) * 360
} }
function calculatePercentage(timeLeft, total) { function calculatePercentage(timeLeft, total) {
return (total - timeLeft) / total; return (total - timeLeft) / total
} }
function useRotateAnimation(token, timeLeft, total) { function useRotateAnimation(token, timeLeft, total) {
useEffect( useEffect(
function createRotateAnimation() { function createRotateAnimation() {
const style = document.createElement('style'); const style = document.createElement('style')
document.head.appendChild(style); document.head.appendChild(style)
const styleSheet = style.sheet; const styleSheet = style.sheet
const rotaKeyframes = rotaAnimation( const rotaKeyframes = rotaAnimation(token, calculateRotaOffset(timeLeft, total))
token, const opaKeyframes = opaAnimation(token, calculateOpaOffset(timeLeft, total))
calculateRotaOffset(timeLeft, total) const opaReverseKeyframes = opaReverseAnimation(token, calculateOpaOffset(timeLeft, total))
);
const opaKeyframes = opaAnimation(token, calculateOpaOffset(timeLeft, total));
const opaReverseKeyframes = opaReverseAnimation(
token,
calculateOpaOffset(timeLeft, total)
);
styleSheet.insertRule(rotaKeyframes, styleSheet.cssRules.length); styleSheet.insertRule(rotaKeyframes, styleSheet.cssRules.length)
styleSheet.insertRule(opaKeyframes, styleSheet.cssRules.length); styleSheet.insertRule(opaKeyframes, styleSheet.cssRules.length)
styleSheet.insertRule(opaReverseKeyframes, styleSheet.cssRules.length); styleSheet.insertRule(opaReverseKeyframes, styleSheet.cssRules.length)
function cleanup() { function cleanup() {
style.remove(); style.remove()
} }
const timer = setTimeout(cleanup, timeLeft * 1000); const timer = setTimeout(cleanup, timeLeft * 1000)
return () => { return () => {
clearTimeout(timer); clearTimeout(timer)
cleanup(); cleanup()
}; }
}, },
[token, timeLeft, total] [token, timeLeft, total],
); )
} }
const CountdownPie = ({ token, timeLeft, total, bgColor, fgColor }) => { const CountdownPie = ({ token, timeLeft, total, bgColor, fgColor }) => {
useRotateAnimation(token, timeLeft, total); useRotateAnimation(token, timeLeft, total)
return ( return (
<div className="countdown-pie" style={{ <div
backgroundColor: bgColor className="countdown-pie"
}}> style={{
backgroundColor: bgColor,
}}
>
<div <div
className="pie spinner" className="pie spinner"
style={{ style={{
animation: `rota_${animationName(token)} ${timeLeft}s linear`, animation: `rota_${animationName(token)} ${timeLeft}s linear`,
backgroundColor: fgColor backgroundColor: fgColor,
}}
/>
<div
className="pie background"
style={{
backgroundColor: fgColor,
}} }}
/> />
<div className="pie background" style={{
backgroundColor: fgColor
}} />
<div <div
className="pie filler" className="pie filler"
style={{ style={{
animation: `opa_r_${animationName(token)} ${timeLeft}s steps(1, end)`, animation: `opa_r_${animationName(token)} ${timeLeft}s steps(1, end)`,
backgroundColor: fgColor backgroundColor: fgColor,
}} }}
/> />
<div <div
@@ -124,15 +117,15 @@ const CountdownPie = ({ token, timeLeft, total, bgColor, fgColor }) => {
}} }}
/> />
</div> </div>
); )
}; }
CountdownPie.propTypes = { CountdownPie.propTypes = {
token: PropTypes.string.isRequired, token: PropTypes.string.isRequired,
timeLeft: PropTypes.number.isRequired, timeLeft: PropTypes.number.isRequired,
total: PropTypes.number.isRequired, total: PropTypes.number.isRequired,
bgColor: PropTypes.string, bgColor: PropTypes.string,
fgColor: PropTypes.string fgColor: PropTypes.string,
}; }
export default CountdownPie; export default CountdownPie

View File

@@ -1,5 +1,3 @@
import React from 'react';
const DataErrorAlert = () => ( const DataErrorAlert = () => (
<div className="auth-overlay"> <div className="auth-overlay">
<div className="auth-dialog sk-panel"> <div className="auth-dialog sk-panel">
@@ -10,15 +8,14 @@ const DataErrorAlert = () => (
<div className="sk-panel-section sk-panel-hero"> <div className="sk-panel-section sk-panel-hero">
<div className="sk-panel-row"> <div className="sk-panel-row">
<div className="sk-h1"> <div className="sk-h1">
The note you selected already has existing data that is not valid The note you selected already has existing data that is not valid with this editor. Please clear the note,
with this editor. Please clear the note, or select a new one, and or select a new one, and try again.
try again.
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); )
export default DataErrorAlert; export default DataErrorAlert

View File

@@ -1,10 +1,10 @@
import React from 'react'; import QRCodeReader from '@Components/QRCodeReader'
import PropTypes from 'prop-types'; import { secretPattern } from '@Lib/otp'
import QRCodeReader from '@Components/QRCodeReader'; import { contextualColors, defaultBgColor, getAllContextualColors, getEntryColor } from '@Lib/utils'
import { secretPattern } from '@Lib/otp'; import { SKAlert } from '@standardnotes/styles'
import { TwitterPicker } from 'react-color'; import PropTypes from 'prop-types'
import { SKAlert } from 'sn-stylekit'; import React from 'react'
import { contextualColors, defaultBgColor, getAllContextualColors, getEntryColor } from '@Lib/utils'; import { TwitterPicker } from 'react-color'
export default class EditEntry extends React.Component { export default class EditEntry extends React.Component {
static defaultProps = { static defaultProps = {
@@ -12,93 +12,102 @@ export default class EditEntry extends React.Component {
service: '', service: '',
account: '', account: '',
secret: '', secret: '',
notes: '' notes: '',
},
} }
};
constructor(props) { constructor(props) {
super(props); super(props)
const { id, entry } = props
this.state = { this.state = {
id: this.props.id, id: id,
entry: this.props.entry, entry,
showColorPicker: false, showColorPicker: false,
qrCodeError: false qrCodeError: false,
}; is2fa: id !== undefined ? !!entry.secret : true,
}
} }
formatSecret(secret) { formatSecret(secret) {
return secret.replace(/\s/g, '').toUpperCase(); return secret.replace(/\s/g, '').toUpperCase()
} }
handleInputChange = event => { handleInputChange = (event) => {
const target = event.target; const target = event.target
const name = target.name; const name = target.name
const value = name === 'secret' ? const value = name === 'secret' ? this.formatSecret(target.value) : target.value
this.formatSecret(target.value) : target.value;
this.setState(state => ({ this.setState((state) => ({
entry: { entry: {
...state.entry, ...state.entry,
[name]: value [name]: value,
},
}))
} }
}));
};
handleSwatchClick = () => { handleSwatchClick = () => {
this.setState({ this.setState({
showColorPicker: !this.state.showColorPicker showColorPicker: !this.state.showColorPicker,
}); })
}; }
handleColorPickerClose = () => { handleColorPickerClose = () => {
this.setState({ this.setState({
showColorPicker: false showColorPicker: false,
}); })
}; }
removeColor = () => { removeColor = () => {
this.setState((state) => { this.setState((state) => {
delete state.entry.color; delete state.entry.color
return { return {
entry: state.entry entry: state.entry,
}; }
}); })
}; }
onSave = () => { onSave = () => {
const { id, entry } = this.state; const { id, entry, is2fa } = this.state
this.props.onSave({ id, entry }); this.props.onSave({
}; id,
entry: {
...entry,
secret: is2fa ? entry.secret : '',
},
})
}
onQRCodeSuccess = otpData => { onQRCodeSuccess = (otpData) => {
const { issuer: labelIssuer, account } = otpData.label; const { issuer: labelIssuer, account } = otpData.label
const { issuer: queryIssuer, secret } = otpData.query; const { issuer: queryIssuer, secret } = otpData.query
this.setState({ this.setState({
entry: { entry: {
service: labelIssuer || queryIssuer || '', service: labelIssuer || queryIssuer || '',
account, account,
secret: this.formatSecret(secret) secret: this.formatSecret(secret),
},
is2fa: true,
})
} }
});
};
onQRCodeError = message => { onQRCodeError = (message) => {
this.setState({ this.setState({
qrCodeError: message qrCodeError: message,
}); })
}; }
dismissQRCodeError = () => { dismissQRCodeError = () => {
this.setState({ this.setState({
qrCodeError: false qrCodeError: false,
}); })
}; }
render() { render() {
const { id, entry, showColorPicker, qrCodeError } = this.state; const { id, entry, showColorPicker, qrCodeError, is2fa } = this.state
const qrCodeAlert = new SKAlert({ const qrCodeAlert = new SKAlert({
title: 'Error', title: 'Error',
@@ -107,63 +116,69 @@ export default class EditEntry extends React.Component {
{ {
text: 'OK', text: 'OK',
style: 'info', style: 'info',
action: this.dismissQRCodeError action: this.dismissQRCodeError,
} },
] ],
}); })
if (qrCodeError) { if (qrCodeError) {
qrCodeAlert.present(); qrCodeAlert.present()
} }
const entryColor = getEntryColor(document, entry); const entryColor = getEntryColor(document, entry)
const swatchStyle = { const swatchStyle = {
width: '36px', width: '36px',
height: '14px', height: '14px',
borderRadius: '2px', borderRadius: '2px',
background: `${entryColor ?? defaultBgColor}`, background: `${entryColor ?? defaultBgColor}`,
}; }
const themeColors = getAllContextualColors(document); const themeColors = getAllContextualColors(document)
const defaultColorOptions = [ const defaultColorOptions = [...themeColors, '#658bdb', '#4CBBFC', '#FF794D', '#EF5276', '#91B73D', '#9B7ECF']
...themeColors,
'#658bdb',
'#4CBBFC',
'#FF794D',
'#EF5276',
'#91B73D',
'#9B7ECF'
];
const handleColorChange = (color) => { const handleColorChange = (color) => {
let selectedColor = color.hex.toUpperCase(); let selectedColor = color.hex.toUpperCase()
const colorIndex = defaultColorOptions.indexOf(selectedColor); const colorIndex = defaultColorOptions.indexOf(selectedColor)
if (colorIndex > -1 && colorIndex <= themeColors.length - 1) { if (colorIndex > -1 && colorIndex <= themeColors.length - 1) {
selectedColor = contextualColors[colorIndex]; selectedColor = contextualColors[colorIndex]
} }
this.setState(state => ({ this.setState((state) => ({
entry: { entry: {
...state.entry, ...state.entry,
color: selectedColor color: selectedColor,
},
}))
}
const handleTypeChange = ({ target }) => {
this.setState({
is2fa: target.value === '2fa',
})
} }
}));
};
return ( return (
<div className="auth-edit sk-panel"> <div className="auth-edit sk-panel">
<div className="sk-panel-content"> <div className="sk-panel-content">
<div className="sk-panel-section"> <div className="sk-panel-section">
<div className="sk-panel-section-title sk-panel-row"> <div className="sk-panel-section-title sk-panel-row">
{id != null ? 'Edit entry' : 'Add new entry'} <div className="sk-panel-row">
<div className="left-header">
<div className="sk-panel-section-title pr-4">{id != null ? 'Edit entry' : 'Add new entry'}</div>
<div className="sk-input-group" onChange={handleTypeChange}>
<label>
<input className="sk-input" type="radio" value="2fa" name="type" defaultChecked={is2fa} /> 2FA
</label>
<label>
<input className="sk-input" type="radio" value="password" name="type" defaultChecked={!is2fa} />{' '}
Password only
</label>
</div>
</div>
</div>
<div className="sk-panel-section-title sk-panel-row"> <div className="sk-panel-section-title sk-panel-row">
{id == null && ( {id == null && <QRCodeReader onSuccess={this.onQRCodeSuccess} onError={this.onQRCodeError} />}
<QRCodeReader
onSuccess={this.onQRCodeSuccess}
onError={this.onQRCodeError}
/>
)}
<> <>
{entryColor && ( {entryColor && (
<div className="sk-button danger" onClick={this.removeColor}> <div className="sk-button danger" onClick={this.removeColor}>
@@ -195,6 +210,7 @@ export default class EditEntry extends React.Component {
onChange={this.handleInputChange} onChange={this.handleInputChange}
type="text" type="text"
/> />
{is2fa && (
<input <input
name="secret" name="secret"
className="sk-input contrast" className="sk-input contrast"
@@ -205,6 +221,16 @@ export default class EditEntry extends React.Component {
pattern={secretPattern} pattern={secretPattern}
required required
/> />
)}
<input
name="password"
className="sk-input contrast"
placeholder={`Password ${is2fa ? '(optional)' : ''}`}
value={entry.password}
onChange={this.handleInputChange}
type="text"
required={!is2fa}
/>
<input <input
name="notes" name="notes"
className="sk-input contrast" className="sk-input contrast"
@@ -213,14 +239,6 @@ export default class EditEntry extends React.Component {
onChange={this.handleInputChange} onChange={this.handleInputChange}
type="text" type="text"
/> />
<input
name="password"
className="sk-input contrast"
placeholder="Password (optional)"
value={entry.password}
onChange={this.handleInputChange}
type="text"
/>
</div> </div>
{showColorPicker && ( {showColorPicker && (
<div className="color-picker-popover"> <div className="color-picker-popover">
@@ -231,12 +249,9 @@ export default class EditEntry extends React.Component {
onChangeComplete={handleColorChange} onChangeComplete={handleColorChange}
triangle="top-right" triangle="top-right"
onSwatchHover={(color, event) => { onSwatchHover={(color, event) => {
const hoveredColor = color.hex.toUpperCase(); const hoveredColor = color.hex.toUpperCase()
if (themeColors.includes(hoveredColor)) { if (themeColors.includes(hoveredColor)) {
event.target.setAttribute( event.target.setAttribute('title', 'This color will change depending on your active theme.')
'title',
'This color will change depending on your active theme.'
);
} }
}} }}
/> />
@@ -248,9 +263,7 @@ export default class EditEntry extends React.Component {
<div className="sk-label">Cancel</div> <div className="sk-label">Cancel</div>
</button> </button>
<button type="submit" className="sk-button info"> <button type="submit" className="sk-button info">
<div className="sk-label"> <div className="sk-label">{id != null ? 'Save' : 'Create'}</div>
{id != null ? 'Save' : 'Create'}
</div>
</button> </button>
</div> </div>
</div> </div>
@@ -258,7 +271,7 @@ export default class EditEntry extends React.Component {
</div> </div>
</div> </div>
</div> </div>
); )
} }
} }
@@ -266,5 +279,5 @@ EditEntry.propTypes = {
id: PropTypes.number, id: PropTypes.number,
entry: PropTypes.object.isRequired, entry: PropTypes.object.isRequired,
onSave: PropTypes.func.isRequired, onSave: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired onCancel: PropTypes.func.isRequired,
}; }

View File

@@ -1,12 +1,12 @@
import React from 'react'; import ConfirmDialog from '@Components/ConfirmDialog'
import update from 'immutability-helper'; import DataErrorAlert from '@Components/DataErrorAlert'
import EditEntry from '@Components/EditEntry'; import EditEntry from '@Components/EditEntry'
import ViewEntries from '@Components/ViewEntries'; import ViewEntries from '@Components/ViewEntries'
import ConfirmDialog from '@Components/ConfirmDialog'; import EditorKit from '@standardnotes/editor-kit'
import DataErrorAlert from '@Components/DataErrorAlert'; import update from 'immutability-helper'
import EditorKit from '@standardnotes/editor-kit'; import React from 'react'
import ReorderIcon from '../assets/svg/reorder-icon.svg'; import ReorderIcon from '../assets/svg/reorder-icon.svg'
import CopyNotification from './CopyNotification'; import CopyNotification from './CopyNotification'
const initialState = { const initialState = {
text: '', text: '',
@@ -19,31 +19,31 @@ const initialState = {
displayCopy: false, displayCopy: false,
canEdit: true, canEdit: true,
searchValue: '', searchValue: '',
lastUpdated: 0 lastUpdated: 0,
}; }
export default class Home extends React.Component { export default class Home extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props)
this.configureEditorKit(); this.configureEditorKit()
this.state = initialState; this.state = initialState
} }
configureEditorKit() { configureEditorKit() {
const delegate = { const delegate = {
setEditorRawText: text => { setEditorRawText: (text) => {
let parseError = false; let parseError = false
let entries = []; let entries = []
if (text) { if (text) {
try { try {
entries = this.parseNote(text); entries = this.parseNote(text)
} catch (e) { } catch (e) {
// Couldn't parse the content // Couldn't parse the content
parseError = true; parseError = true
this.setState({ this.setState({
parseError: true parseError: true,
}); })
} }
} }
@@ -51,225 +51,225 @@ export default class Home extends React.Component {
...initialState, ...initialState,
text, text,
parseError, parseError,
entries entries,
}); })
}, },
generateCustomPreview: text => { generateCustomPreview: (text) => {
let entries = []; let entries = []
try { try {
entries = this.parseNote(text); entries = this.parseNote(text)
} finally { } finally {
// eslint-disable-next-line no-unsafe-finally // eslint-disable-next-line no-unsafe-finally
return { return {
html: `<div><strong>${entries.length}</strong> TokenVault Entries </div>`, html: `<div><strong>${entries.length}</strong> TokenVault Entries </div>`,
plain: `${entries.length} TokenVault Entries`, plain: `${entries.length} TokenVault Entries`,
}; }
} }
}, },
clearUndoHistory: () => { }, clearUndoHistory: () => {},
getElementsBySelector: () => [], getElementsBySelector: () => [],
onNoteLockToggle: (isLocked) => { onNoteLockToggle: (isLocked) => {
this.setState({ this.setState({
canEdit: !isLocked canEdit: !isLocked,
}); })
}, },
onThemesChange: () => { onThemesChange: () => {
this.setState({ this.setState({
lastUpdated: Date.now(), lastUpdated: Date.now(),
}); })
},
} }
};
this.editorKit = new EditorKit(delegate, { this.editorKit = new EditorKit(delegate, {
mode: 'json', mode: 'json',
supportsFileSafe: false supportsFileSafe: false,
}); })
} }
parseNote(text) { parseNote(text) {
const entries = JSON.parse(text); const entries = JSON.parse(text)
if (entries instanceof Array) { if (entries instanceof Array) {
if (entries.length === 0) { if (entries.length === 0) {
return []; return []
} }
for (const entry of entries) { for (const entry of entries) {
if (!('service' in entry)) { if (!('service' in entry)) {
throw Error('Service key is missing for an entry.'); throw Error('Service key is missing for an entry.')
} }
if (!('secret' in entry)) { if (!('secret' in entry) && !('password' in entry)) {
throw Error('Secret key is missing for an entry.'); throw Error('An entry does not have a secret key or a password.')
} }
} }
return entries; return entries
} }
return []; return []
} }
saveNote(entries) { saveNote(entries) {
this.editorKit.onEditorValueChanged(JSON.stringify(entries, null, 2)); this.editorKit.onEditorValueChanged(JSON.stringify(entries, null, 2))
} }
// Entry operations // Entry operations
addEntry = entry => { addEntry = (entry) => {
this.setState(state => { this.setState((state) => {
const entries = state.entries.concat([entry]); const entries = state.entries.concat([entry])
this.saveNote(entries); this.saveNote(entries)
return { return {
editMode: false, editMode: false,
editEntry: null, editEntry: null,
entries entries,
}; }
}); })
}; }
editEntry = ({ id, entry }) => { editEntry = ({ id, entry }) => {
this.setState(state => { this.setState((state) => {
const entries = update(state.entries, { [id]: { $set: entry } }); const entries = update(state.entries, { [id]: { $set: entry } })
this.saveNote(entries); this.saveNote(entries)
return { return {
editMode: false, editMode: false,
editEntry: null, editEntry: null,
entries entries,
}; }
}); })
}; }
removeEntry = id => { removeEntry = (id) => {
this.setState(state => { this.setState((state) => {
const entries = update(state.entries, { $splice: [[id, 1]] }); const entries = update(state.entries, { $splice: [[id, 1]] })
this.saveNote(entries); this.saveNote(entries)
return { return {
confirmRemove: false, confirmRemove: false,
editEntry: null, editEntry: null,
entries entries,
}; }
}); })
}; }
// Event Handlers // Event Handlers
onAddNew = () => { onAddNew = () => {
if (!this.state.canEdit) { if (!this.state.canEdit) {
return; return
} }
this.setState({ this.setState({
editMode: true, editMode: true,
editEntry: null editEntry: null,
}); })
};
onEdit = id => {
if (!this.state.canEdit) {
return;
} }
this.setState(state => ({
onEdit = (id) => {
if (!this.state.canEdit) {
return
}
this.setState((state) => ({
editMode: true, editMode: true,
editEntry: { editEntry: {
id, id,
entry: state.entries[id] entry: state.entries[id],
},
}))
} }
}));
};
onCancel = () => { onCancel = () => {
this.setState({ this.setState({
confirmRemove: false, confirmRemove: false,
confirmReorder: false, confirmReorder: false,
editMode: false, editMode: false,
editEntry: null editEntry: null,
}); })
};
onRemove = id => {
if (!this.state.canEdit) {
return;
} }
this.setState(state => ({
onRemove = (id) => {
if (!this.state.canEdit) {
return
}
this.setState((state) => ({
confirmRemove: true, confirmRemove: true,
editEntry: { editEntry: {
id, id,
entry: state.entries[id] entry: state.entries[id],
},
}))
} }
}));
};
onSave = ({ id, entry }) => { onSave = ({ id, entry }) => {
// If there's no ID it's a new note // If there's no ID it's a new note
if (id != null) { if (id != null) {
this.editEntry({ id, entry }); this.editEntry({ id, entry })
} else { } else {
this.addEntry(entry); this.addEntry(entry)
}
} }
};
onCopyValue = () => { onCopyValue = () => {
this.setState({ this.setState({
displayCopy: true displayCopy: true,
}); })
if (this.clearTooltipTimer) { if (this.clearTooltipTimer) {
clearTimeout(this.clearTooltipTimer); clearTimeout(this.clearTooltipTimer)
} }
this.clearTooltipTimer = setTimeout(() => { this.clearTooltipTimer = setTimeout(() => {
this.setState({ this.setState({
displayCopy: false displayCopy: false,
}); })
}, 2000); }, 2000)
}; }
updateEntries = (entries) => { updateEntries = (entries) => {
this.saveNote(entries); this.saveNote(entries)
this.setState({ this.setState({
entries entries,
}); })
}; }
onReorderEntries = () => { onReorderEntries = () => {
if (!this.state.canEdit) { if (!this.state.canEdit) {
return; return
} }
this.setState({ this.setState({
confirmReorder: true confirmReorder: true,
}); })
}; }
onSearchChange = event => { onSearchChange = (event) => {
const target = event.target; const target = event.target
this.setState({ this.setState({
searchValue: target.value.toLowerCase() searchValue: target.value.toLowerCase(),
}); })
}; }
clearSearchValue = () => { clearSearchValue = () => {
this.setState({ this.setState({
searchValue: '' searchValue: '',
}); })
} }
reorderEntries = () => { reorderEntries = () => {
const { entries } = this.state; const { entries } = this.state
const orderedEntries = entries.sort((a, b) => { const orderedEntries = entries.sort((a, b) => {
const serviceA = a.service.toLowerCase(); const serviceA = a.service.toLowerCase()
const serviceB = b.service.toLowerCase(); const serviceB = b.service.toLowerCase()
return (serviceA < serviceB) ? -1 : (serviceA > serviceB) ? 1 : 0; return serviceA < serviceB ? -1 : serviceA > serviceB ? 1 : 0
}); })
this.saveNote(orderedEntries); this.saveNote(orderedEntries)
this.setState({ this.setState({
entries: orderedEntries, entries: orderedEntries,
confirmReorder: false confirmReorder: false,
}); })
}; }
render() { render() {
const editEntry = this.state.editEntry || {}; const editEntry = this.state.editEntry || {}
const { const {
canEdit, canEdit,
displayCopy, displayCopy,
@@ -279,15 +279,15 @@ export default class Home extends React.Component {
confirmRemove, confirmRemove,
confirmReorder, confirmReorder,
searchValue, searchValue,
lastUpdated lastUpdated,
} = this.state; } = this.state
if (parseError) { if (parseError) {
return ( return (
<div className="sn-component"> <div className="sn-component">
<DataErrorAlert /> <DataErrorAlert />
</div> </div>
); )
} }
return ( return (
@@ -327,12 +327,7 @@ export default class Home extends React.Component {
)} )}
<div id="content"> <div id="content">
{editMode ? ( {editMode ? (
<EditEntry <EditEntry id={editEntry.id} entry={editEntry.entry} onSave={this.onSave} onCancel={this.onCancel} />
id={editEntry.id}
entry={editEntry.entry}
onSave={this.onSave}
onCancel={this.onCancel}
/>
) : ( ) : (
<ViewEntries <ViewEntries
entries={entries} entries={entries}
@@ -363,6 +358,6 @@ export default class Home extends React.Component {
)} )}
</div> </div>
</div> </div>
); )
} }
} }

View File

@@ -1,91 +1,85 @@
import React from 'react'; import { parseKeyUri } from '@Lib/otp'
import PropTypes from 'prop-types'; import jsQR from 'jsqr'
import jsQR from 'jsqr'; import PropTypes from 'prop-types'
import { parseKeyUri } from '@Lib/otp'; import React from 'react'
const convertToGrayScale = (imageData) => { const convertToGrayScale = (imageData) => {
if (!imageData) { if (!imageData) {
return; return
} }
for (let i = 0; i < imageData.data.length; i += 4) { for (let i = 0; i < imageData.data.length; i += 4) {
const count = imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]; const count = imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]
let color = 0; let color = 0
if (count > 510) { if (count > 510) {
color = 255; color = 255
} else if (count > 255) { } else if (count > 255) {
color = 127.5; color = 127.5
} }
imageData.data[i] = color; imageData.data[i] = color
imageData.data[i + 1] = color; imageData.data[i + 1] = color
imageData.data[i + 2] = color; imageData.data[i + 2] = color
imageData.data[i + 3] = 255; imageData.data[i + 3] = 255
} }
return imageData; return imageData
}; }
export default class QRCodeReader extends React.Component { export default class QRCodeReader extends React.Component {
onImageSelected = evt => { onImageSelected = (evt) => {
const file = evt.target.files[0]; const file = evt.target.files[0]
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file)
const img = new Image(); const img = new Image()
const self = this; const self = this
img.onload = function() { img.onload = function () {
URL.revokeObjectURL(this.src); URL.revokeObjectURL(this.src)
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas')
const context = canvas.getContext('2d'); const context = canvas.getContext('2d')
canvas.width = this.width; canvas.width = this.width
canvas.height = this.height; canvas.height = this.height
context.drawImage(this, 0, 0); context.drawImage(this, 0, 0)
let imageData = context.getImageData(0, 0, this.width, this.height); let imageData = context.getImageData(0, 0, this.width, this.height)
imageData = convertToGrayScale(imageData); imageData = convertToGrayScale(imageData)
const code = jsQR(imageData.data, imageData.width, imageData.height); const code = jsQR(imageData.data, imageData.width, imageData.height)
const { onError, onSuccess } = self.props; const { onError, onSuccess } = self.props
if (code) { if (code) {
const otpData = parseKeyUri(code.data); const otpData = parseKeyUri(code.data)
if (otpData.type !== 'totp') { if (otpData.type !== 'totp') {
onError(`The '${otpData.type}' type is not supported.`); onError(`The '${otpData.type}' type is not supported.`)
} else { } else {
onSuccess(otpData); onSuccess(otpData)
} }
} else { } else {
onError('Error reading QR code from image. Please try again.'); onError('Error reading QR code from image. Please try again.')
}
} }
};
img.src = url; img.src = url
return false; return false
}; }
render() { render() {
return ( return (
<div className="qr-code-reader-container"> <div className="qr-code-reader-container">
<div className="sk-button info"> <div className="sk-button info">
<label className="no-style">
<input
type="file"
style={{ display: 'none' }}
onChange={this.onImageSelected}
/>
<div className="sk-label">Upload QR Code</div> <div className="sk-label">Upload QR Code</div>
</label> <input type="file" style={{ display: 'none' }} onChange={this.onImageSelected} />
</div> </div>
</div> </div>
); )
} }
} }
QRCodeReader.propTypes = { QRCodeReader.propTypes = {
onError: PropTypes.func.isRequired, onError: PropTypes.func.isRequired,
onSuccess: PropTypes.func.isRequired onSuccess: PropTypes.func.isRequired,
}; }

View File

@@ -1,48 +1,39 @@
import React from 'react'; import AuthEntry from '@Components/AuthEntry'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import AuthEntry from '@Components/AuthEntry'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
const reorderEntries = (list, startIndex, endIndex) => { const reorderEntries = (list, startIndex, endIndex) => {
const result = Array.from(list); const result = Array.from(list)
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1)
result.splice(endIndex, 0, removed); result.splice(endIndex, 0, removed)
return result; return result
}; }
const ViewEntries = ({ entries, onEdit, onRemove, onCopyValue, canEdit, updateEntries, searchValue, lastUpdated }) => { const ViewEntries = ({ entries, onEdit, onRemove, onCopyValue, canEdit, updateEntries, searchValue, lastUpdated }) => {
const onDragEnd = (result) => { const onDragEnd = (result) => {
const droppedOutsideList = !result.destination; const droppedOutsideList = !result.destination
if (droppedOutsideList) { if (droppedOutsideList) {
return; return
} }
const orderedEntries = reorderEntries( const orderedEntries = reorderEntries(entries, result.source.index, result.destination.index)
entries,
result.source.index,
result.destination.index
);
updateEntries(orderedEntries); updateEntries(orderedEntries)
}; }
return ( return (
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable" isDropDisabled={!canEdit}> <Droppable droppableId="droppable" isDropDisabled={!canEdit}>
{(provided) => ( {(provided) => (
<div <div {...provided.droppableProps} ref={provided.innerRef} className="auth-list">
{...provided.droppableProps}
ref={provided.innerRef}
className="auth-list"
>
{entries.map((entry, index) => { {entries.map((entry, index) => {
/** /**
* Filtering entries by account, service and notes properties. * Filtering entries by account, service and notes properties.
*/ */
const combinedString = `${entry.account}${entry.service}${entry.notes}`.toLowerCase(); const combinedString = `${entry.account}${entry.service}${entry.notes}`.toLowerCase()
if (searchValue && !combinedString.includes(searchValue)) { if (searchValue && !combinedString.includes(searchValue)) {
return; return
} }
return ( return (
<Draggable <Draggable
@@ -67,15 +58,15 @@ const ViewEntries = ({ entries, onEdit, onRemove, onCopyValue, canEdit, updateEn
/> />
)} )}
</Draggable> </Draggable>
); )
})} })}
{provided.placeholder} {provided.placeholder}
</div> </div>
)} )}
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
); )
}; }
ViewEntries.propTypes = { ViewEntries.propTypes = {
entries: PropTypes.arrayOf(PropTypes.object), entries: PropTypes.arrayOf(PropTypes.object),
@@ -85,7 +76,7 @@ ViewEntries.propTypes = {
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
lastUpdated: PropTypes.number.isRequired, lastUpdated: PropTypes.number.isRequired,
updateEntries: PropTypes.func.isRequired, updateEntries: PropTypes.func.isRequired,
searchValue: PropTypes.string searchValue: PropTypes.string,
}; }
export default ViewEntries; export default ViewEntries

View File

@@ -0,0 +1,4 @@
import Home from '@Components/Home'
import ReactDOM from 'react-dom'
ReactDOM.render(<Home />, document.body.appendChild(document.createElement('div')))

View File

@@ -1,12 +1,5 @@
import { import { base32ToHex, bufToHex, decToHex, hextoBuf, hexToBytes, leftpad } from '@Lib/utils'
base32ToHex, export { parseKeyUri, secretPattern } from '@Lib/utils'
leftpad,
decToHex,
bufToHex,
hextoBuf,
hexToBytes
} from '@Lib/utils';
export { secretPattern, parseKeyUri } from '@Lib/utils';
class Hotp { class Hotp {
/** /**
@@ -25,25 +18,25 @@ class Hotp {
* *
*/ */
async gen(secret, opt) { async gen(secret, opt) {
var key = base32ToHex(secret) || ''; var key = base32ToHex(secret) || ''
opt = opt || {}; opt = opt || {}
var counter = opt.counter || 0; var counter = opt.counter || 0
var hexCounter = leftpad(decToHex(counter), 16, '0'); var hexCounter = leftpad(decToHex(counter), 16, '0')
var digest = await this.createHmac('SHA-1', key, hexCounter); var digest = await this.createHmac('SHA-1', key, hexCounter)
var h = hexToBytes(digest); var h = hexToBytes(digest)
// Truncate // Truncate
var offset = h[h.length - 1] & 0xf; var offset = h[h.length - 1] & 0xf
var v = var v =
((h[offset] & 0x7f) << 24) | ((h[offset] & 0x7f) << 24) |
((h[offset + 1] & 0xff) << 16) | ((h[offset + 1] & 0xff) << 16) |
((h[offset + 2] & 0xff) << 8) | ((h[offset + 2] & 0xff) << 8) |
(h[offset + 3] & 0xff); (h[offset + 3] & 0xff)
v = (v % 1000000) + ''; v = (v % 1000000) + ''
return Array(7 - v.length).join('0') + v; return Array(7 - v.length).join('0') + v
} }
/** /**
@@ -76,23 +69,23 @@ class Hotp {
* *
*/ */
async verify(token, key, opt) { async verify(token, key, opt) {
opt = opt || {}; opt = opt || {}
var window = opt.window || 50; var window = opt.window || 50
var counter = opt.counter || 0; var counter = opt.counter || 0
// Now loop through from C to C + W to determine if there is // Now loop through from C to C + W to determine if there is
// a correct code // a correct code
for (var i = counter - window; i <= counter + window; ++i) { for (var i = counter - window; i <= counter + window; ++i) {
opt.counter = i; opt.counter = i
if ((await this.gen(key, opt)) === token) { if ((await this.gen(key, opt)) === token) {
// We have found a matching code, trigger callback // We have found a matching code, trigger callback
// and pass offset // and pass offset
return { delta: i - counter }; return { delta: i - counter }
} }
} }
// If we get to here then no codes have matched, return null // If we get to here then no codes have matched, return null
return null; return null
} }
async createHmac(alg, key, str) { async createHmac(alg, key, str) {
@@ -102,17 +95,17 @@ class Hotp {
{ {
// algorithm details // algorithm details
name: 'HMAC', name: 'HMAC',
hash: { name: alg } hash: { name: alg },
}, },
false, // export = false false, // export = false
['sign'] // what this key can do ['sign'], // what this key can do
); )
const sig = await window.crypto.subtle.sign('HMAC', hmacKey, hextoBuf(str)); const sig = await window.crypto.subtle.sign('HMAC', hmacKey, hextoBuf(str))
return bufToHex(sig); return bufToHex(sig)
} }
} }
export const hotp = new Hotp(); export const hotp = new Hotp()
class Totp { class Totp {
/** /**
@@ -133,15 +126,15 @@ class Totp {
* *
*/ */
async gen(key, opt) { async gen(key, opt) {
opt = opt || {}; opt = opt || {}
var time = opt.time || 30; var time = opt.time || 30
var _t = Date.now(); var _t = Date.now()
// Determine the value of the counter, C // Determine the value of the counter, C
// This is the number of time steps in seconds since T0 // This is the number of time steps in seconds since T0
opt.counter = Math.floor(_t / 1000 / time); opt.counter = Math.floor(_t / 1000 / time)
return hotp.gen(key, opt); return hotp.gen(key, opt)
} }
/** /**
@@ -176,16 +169,16 @@ class Totp {
* *
*/ */
async verify(token, key, opt) { async verify(token, key, opt) {
opt = opt || {}; opt = opt || {}
var time = opt.time || 30; var time = opt.time || 30
var _t = Date.now(); var _t = Date.now()
// Determine the value of the counter, C // Determine the value of the counter, C
// This is the number of time steps in seconds since T0 // This is the number of time steps in seconds since T0
opt.counter = Math.floor(_t / 1000 / time); opt.counter = Math.floor(_t / 1000 / time)
return hotp.verify(token, key, opt); return hotp.verify(token, key, opt)
} }
} }
export const totp = new Totp(); export const totp = new Totp()

View File

@@ -1,58 +1,56 @@
const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
export const secretPattern = `^[${base32chars}]{16,}$`; export const secretPattern = `^[${base32chars}]{16,}$`
export function hexToBytes(hex) { export function hexToBytes(hex) {
var bytes = []; var bytes = []
for (var c = 0, C = hex.length; c < C; c += 2) { for (var c = 0, C = hex.length; c < C; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16)); bytes.push(parseInt(hex.substr(c, 2), 16))
} }
return bytes; return bytes
} }
export function decToHex(s) { export function decToHex(s) {
return (s < 15.5 ? '0' : '') + Math.round(s).toString(16); return (s < 15.5 ? '0' : '') + Math.round(s).toString(16)
} }
export function bufToHex(buf) { export function bufToHex(buf) {
return Array.prototype.map return Array.prototype.map.call(new Uint8Array(buf), (x) => ('00' + x.toString(16)).slice(-2)).join('')
.call(new Uint8Array(buf), x => ('00' + x.toString(16)).slice(-2))
.join('');
} }
export function hextoBuf(hex) { export function hextoBuf(hex) {
var view = new Uint8Array(hex.length / 2); var view = new Uint8Array(hex.length / 2)
for (var i = 0; i < hex.length; i += 2) { for (var i = 0; i < hex.length; i += 2) {
view[i / 2] = parseInt(hex.substring(i, i + 2), 16); view[i / 2] = parseInt(hex.substring(i, i + 2), 16)
} }
return view.buffer; return view.buffer
} }
export function base32ToHex(base32) { export function base32ToHex(base32) {
var bits, chunk, hex, i, val; var bits, chunk, hex, i, val
bits = ''; bits = ''
hex = ''; hex = ''
i = 0; i = 0
while (i < base32.length) { while (i < base32.length) {
val = base32chars.indexOf(base32.charAt(i).toUpperCase()); val = base32chars.indexOf(base32.charAt(i).toUpperCase())
bits += leftpad(val.toString(2), 5, '0'); bits += leftpad(val.toString(2), 5, '0')
i++; i++
} }
i = 0; i = 0
while (i + 4 <= bits.length) { while (i + 4 <= bits.length) {
chunk = bits.substr(i, 4); chunk = bits.substr(i, 4)
hex = hex + parseInt(chunk, 2).toString(16); hex = hex + parseInt(chunk, 2).toString(16)
i += 4; i += 4
} }
return hex; return hex
} }
export function leftpad(str, len, pad) { export function leftpad(str, len, pad) {
if (len + 1 >= str.length) { if (len + 1 >= str.length) {
str = Array(len + 1 - str.length).join(pad) + str; str = Array(len + 1 - str.length).join(pad) + str
} }
return str; return str
} }
/** /**
@@ -76,45 +74,45 @@ export function leftpad(str, len, pad) {
*/ */
export function parseKeyUri(uri) { export function parseKeyUri(uri) {
// Quick sanity check // Quick sanity check
if (typeof uri !== 'string' || uri.length < 7) return null; if (typeof uri !== 'string' || uri.length < 7) return null
// I would like to just use new URL(), but the behavior is different between node and browsers, so // I would like to just use new URL(), but the behavior is different between node and browsers, so
// we have to do some of the work manually with regex. // we have to do some of the work manually with regex.
const parts = /otpauth:\/\/([A-Za-z]+)\/([^?]+)\??(.*)?/i.exec(uri); const parts = /otpauth:\/\/([A-Za-z]+)\/([^?]+)\??(.*)?/i.exec(uri)
if (!parts || parts.length < 3) { if (!parts || parts.length < 3) {
return null; return null
} }
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const [fullUri, type, fullLabel] = parts; const [fullUri, type, fullLabel] = parts
// Sanity check type and label // Sanity check type and label
if (!type || !fullLabel) { if (!type || !fullLabel) {
return null; return null
} }
// Parse the label // Parse the label
const decodedLabel = decodeURIComponent(fullLabel); const decodedLabel = decodeURIComponent(fullLabel)
const labelParts = decodedLabel.split(/: ?/); const labelParts = decodedLabel.split(/: ?/)
const label = const label =
labelParts && labelParts.length === 2 labelParts && labelParts.length === 2
? { issuer: labelParts[0], account: labelParts[1] } ? { issuer: labelParts[0], account: labelParts[1] }
: { issuer: '', account: decodedLabel }; : { issuer: '', account: decodedLabel }
// Parse query string // Parse query string
const qs = parts[3] ? new URLSearchParams(parts[3]) : []; const qs = parts[3] ? new URLSearchParams(parts[3]) : []
const query = [...qs].reduce((acc, [key, value]) => { const query = [...qs].reduce((acc, [key, value]) => {
acc[key] = value; acc[key] = value
return acc; return acc
}, {}); }, {})
// Returned the parsed parts of the URI // Returned the parsed parts of the URI
return { type: type.toLowerCase(), label, query }; return { type: type.toLowerCase(), label, query }
} }
/** /**
@@ -122,19 +120,21 @@ export function parseKeyUri(uri) {
*/ */
export function hexColorToRGB(hexColor) { export function hexColorToRGB(hexColor) {
// Expand the shorthand form (e.g. "0AB") to full form (e.g. "00AABB") // Expand the shorthand form (e.g. "0AB") to full form (e.g. "00AABB")
const shortHandFormRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; const shortHandFormRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
hexColor = hexColor.replace(shortHandFormRegex, function(m, red, green, blue) { hexColor = hexColor.replace(shortHandFormRegex, function (m, red, green, blue) {
return red + red + green + green + blue + blue; return red + red + green + green + blue + blue
}); })
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexColor); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexColor)
return result ? { return result
? {
red: parseInt(result[1], 16), red: parseInt(result[1], 16),
green: parseInt(result[2], 16), green: parseInt(result[2], 16),
blue: parseInt(result[3], 16) blue: parseInt(result[3], 16),
} : null; }
: null
} }
export const defaultBgColor = '#FFF'; export const defaultBgColor = '#FFF'
/** /**
* Gets the color variable to be used based on the calculated constrast of a color. * Gets the color variable to be used based on the calculated constrast of a color.
@@ -142,48 +142,44 @@ export const defaultBgColor = '#FFF';
export function getVarColorForContrast(backgroundColor) { export function getVarColorForContrast(backgroundColor) {
const styleKitColors = { const styleKitColors = {
foreground: '--sn-stylekit-contrast-foreground-color', foreground: '--sn-stylekit-contrast-foreground-color',
background: '--sn-stylekit-contrast-background-color' background: '--sn-stylekit-contrast-background-color',
};
if (!backgroundColor) {
return styleKitColors.foreground;
} }
const colorContrast = Math.round(((parseInt(backgroundColor.red) * 299) + (parseInt(backgroundColor.green) * 587) + (parseInt(backgroundColor.blue) * 114)) / 1000); if (!backgroundColor) {
return (colorContrast > 70) ? styleKitColors.background : styleKitColors.foreground; return styleKitColors.foreground
}
const colorContrast = Math.round(
(parseInt(backgroundColor.red) * 299 +
parseInt(backgroundColor.green) * 587 +
parseInt(backgroundColor.blue) * 114) /
1000,
)
return colorContrast > 70 ? styleKitColors.background : styleKitColors.foreground
} }
function getPropertyValue(document, propertyName) { function getPropertyValue(document, propertyName) {
return getComputedStyle(document.documentElement) return getComputedStyle(document.documentElement).getPropertyValue(propertyName).trim().toUpperCase()
.getPropertyValue(propertyName).trim().toUpperCase();
} }
export const contextualColors = [ export const contextualColors = ['info', 'success', 'neutral', 'warning']
'info',
'success',
'neutral',
'warning'
];
export function getContextualColor(document, colorName) { export function getContextualColor(document, colorName) {
if (!contextualColors.includes(colorName)) { if (!contextualColors.includes(colorName)) {
return; return
} }
return getPropertyValue( return getPropertyValue(document, `--sn-stylekit-${colorName}-color`)
document,
`--sn-stylekit-${colorName}-color`
);
} }
export function getEntryColor(document, entry) { export function getEntryColor(document, entry) {
const { color } = entry; const { color } = entry
if (!contextualColors.includes(color)) { if (!contextualColors.includes(color)) {
return color; return color
} }
return getContextualColor(document, color); return getContextualColor(document, color)
} }
export function getAllContextualColors(document) { export function getAllContextualColors(document) {
return contextualColors.map((colorName) => getContextualColor(document, colorName)); return contextualColors.map((colorName) => getContextualColor(document, colorName))
} }

View File

@@ -1,8 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<App />,
document.body.appendChild(document.createElement('div'))
);

View File

@@ -1,25 +1,24 @@
@import '~stylekit'; @import '~@standardnotes/styles/src/Styles/main.scss';
body, body,
html { html {
font-family: var(--sn-stylekit-sans-serif-font); background-color: var(--sn-stylekit-contrast-background-color);
height: 100%; padding: 0 !important;
width: 100%;
margin: 0;
padding: 0;
font-size: var(--sn-stylekit-base-font-size);
background-color: transparent;
} }
* { * {
// To prevent gray flash when focusing input on mobile Safari // To prevent gray flash when focusing input on mobile Safari
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
font-family: var(--sn-stylekit-sans-serif-font);
} }
.sn-component { .sn-component {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh;
@media screen and (max-width: 420px) {
min-height: -webkit-fill-available;
}
.sk-panel-content { .sk-panel-content {
height: inherit !important; height: inherit !important;
@@ -127,7 +126,6 @@ html {
} }
} }
/* entry default styles */ /* entry default styles */
.auth-entry { .auth-entry {
display: flex; display: flex;
@@ -291,7 +289,7 @@ html {
padding: 5px; padding: 5px;
background: var(--sn-stylekit-contrast-background-color); background: var(--sn-stylekit-contrast-background-color);
border-radius: 1px; border-radius: 1px;
box-shadow: 0 0 0 1px rgba(0,0,0,.1); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
} }
@@ -409,9 +407,37 @@ html {
} }
// Show palette icon on the first 4 color rectangles. // Show palette icon on the first 4 color rectangles.
div.twitter-picker > div:nth-child(3) > span:nth-child(-n+4) > div { div.twitter-picker > div:nth-child(3) > span:nth-child(-n + 4) > div {
background-image: url('../assets/svg/palette.svg') !important; background-image: url('../assets/svg/palette.svg') !important;
background-repeat: no-repeat !important; background-repeat: no-repeat !important;
background-position: top 4px right 4px !important; background-position: top 4px right 4px !important;
background-size: 12px 12px !important; background-size: 12px 12px !important;
} }
.grab-cursor {
cursor: grab;
}
.left-header {
display: flex;
@media screen and (max-width: 600px) {
flex-direction: column;
flex-wrap: wrap;
}
.sk-input-group {
> * {
display: inline-block;
vertical-align: middle;
&:not(:first-child) {
margin-left: 0 !important;
}
&:not(:last-child) {
margin-right: 0.73125rem;
}
}
}
}

View File

@@ -7,46 +7,55 @@
"components:compile": "webpack --config webpack.prod.js", "components:compile": "webpack --config webpack.prod.js",
"start": "webpack serve --config webpack.dev.js --progress --hot", "start": "webpack serve --config webpack.dev.js --progress --hot",
"skip:components:lint": "eslint app/ --ext .js", "skip:components:lint": "eslint app/ --ext .js",
"components:lint:fix": "yarn lint --fix" "components:lint:fix": "eslint --fix",
"components:lint": "prettier --write 'app/**/*.{html,css,scss,js,jsx,ts,tsx,json}' README.md"
}, },
"sn": { "sn": {
"main": "dist/index.html" "main": "dist/index.html"
}, },
"lint-staged": {
"README.md": [
"prettier --write"
],
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
"prettier --write"
]
},
"devDependencies": { "devDependencies": {
"@babel/core": "^7.13.10", "@babel/core": "^7.18.5",
"@babel/eslint-parser": "^7.13.10", "@babel/eslint-parser": "^7.18.2",
"@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-class-properties": "^7.17.12",
"@babel/plugin-transform-runtime": "^7.13.10", "@babel/plugin-transform-runtime": "^7.18.5",
"@babel/preset-env": "^7.13.10", "@babel/preset-env": "^7.18.2",
"@babel/preset-react": "^7.12.13", "@babel/preset-react": "^7.17.12",
"@standardnotes/editor-kit": "2.2.1", "@standardnotes/editor-kit": "2.2.5",
"@standardnotes/eslint-config-extensions": "^1.0.1", "@standardnotes/eslint-config-extensions": "^1.0.4",
"@svgr/webpack": "^6.1.2", "@standardnotes/styles": "workspace:*",
"babel-loader": "^8.2.2", "@svgr/webpack": "^6.2.1",
"css-loader": "^5.1.3", "babel-loader": "^8.2.5",
"eslint": "^7.21.0", "css-loader": "^6.7.1",
"eslint-plugin-react": "^7.22.0", "eslint": "*",
"html-webpack-plugin": "^5.3.1", "eslint-plugin-react": "^7.30.0",
"immutability-helper": "^3.0.1", "html-webpack-plugin": "^5.5.0",
"jsqr": "^1.2.0", "immutability-helper": "^3.1.1",
"mini-css-extract-plugin": "^1.3.9", "jsqr": "^1.4.0",
"mini-css-extract-plugin": "^2.6.1",
"node-sass": "*", "node-sass": "*",
"notp": "^2.0.3", "notp": "^2.0.3",
"otplib": "^11.0.1", "otplib": "^12.0.1",
"prop-types": "^15.7.2", "prettier": "*",
"react": "^17.0.1", "prop-types": "^15.8.1",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-dom": "^17.0.1", "react-dom": "^18.2.0",
"regenerator-runtime": "^0.13.2", "sass-loader": "^13.0.0",
"sass-loader": "^11.0.1", "style-loader": "~3.3.1",
"sn-stylekit": "2.1.0",
"style-loader": "~0.13.1",
"svg-url-loader": "^7.1.1", "svg-url-loader": "^7.1.1",
"terser-webpack-plugin": "^5.1.1", "terser-webpack-plugin": "^5.3.3",
"webpack": "*", "webpack": "*",
"webpack-cli": "*", "webpack-cli": "*",
"webpack-dev-server": "*", "webpack-dev-server": "*",
"webpack-merge": "^5.7.3" "webpack-merge": "^5.8.0"
} }
} }

View File

@@ -1,11 +1,11 @@
const path = require('path'); const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = { module.exports = {
context: __dirname, context: __dirname,
entry: [ entry: [
path.resolve(__dirname, 'app/main.js'), path.resolve(__dirname, 'app/index.js'),
path.resolve(__dirname, 'app/stylesheets/main.scss') path.resolve(__dirname, 'app/stylesheets/main.scss')
], ],
output: { output: {
@@ -30,11 +30,8 @@ module.exports = {
}, },
{ {
test: /\.js[x]?$/, test: /\.js[x]?$/,
include: [
path.resolve(__dirname, 'app')
],
exclude: /node_modules/, exclude: /node_modules/,
use: ['babel-loader'] use: ['babel-loader'],
}, },
{ {
test: /\.svg$/i, test: /\.svg$/i,
@@ -60,7 +57,6 @@ module.exports = {
resolve: { resolve: {
extensions: ['.js', '.jsx'], extensions: ['.js', '.jsx'],
alias: { alias: {
stylekit: require.resolve('sn-stylekit/dist/stylekit.css'),
'@Components': path.resolve(__dirname, 'app/components'), '@Components': path.resolve(__dirname, 'app/components'),
'@Lib': path.resolve(__dirname, 'app/lib') '@Lib': path.resolve(__dirname, 'app/lib')
} }
@@ -75,4 +71,4 @@ module.exports = {
filename: 'index.html' filename: 'index.html'
}) })
] ]
}; }

View File

@@ -7,14 +7,17 @@ module.exports = merge(config, {
devtool: 'cheap-source-map', devtool: 'cheap-source-map',
devServer: { devServer: {
port: 8001, port: 8001,
contentBase: path.resolve(__dirname, 'dist'), static: path.resolve(__dirname, 'dist'),
disableHostCheck: true, allowedHosts: "all",
historyApiFallback: true, historyApiFallback: true,
watchOptions: { aggregateTimeout: 300, poll: 1000 },
headers: { headers: {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization' 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
} }
}, },
watchOptions: {
aggregateTimeout: 300,
poll: 1000
},
}); });

View File

@@ -94,6 +94,10 @@
padding-right: 0.5rem; padding-right: 0.5rem;
} }
.pr-4 {
padding-right: 1rem;
}
.pl-1 { .pl-1 {
padding-left: 0.25rem; padding-left: 0.25rem;
} }

187
yarn.lock
View File

@@ -87,7 +87,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/core@npm:^7.1.0, @babel/core@npm:^7.1.6, @babel/core@npm:^7.11.1, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.10, @babel/core@npm:^7.13.13, @babel/core@npm:^7.13.14, @babel/core@npm:^7.13.8, @babel/core@npm:^7.14.0, @babel/core@npm:^7.14.6, @babel/core@npm:^7.15.5, @babel/core@npm:^7.16.0, @babel/core@npm:^7.17.10, @babel/core@npm:^7.17.9, @babel/core@npm:^7.18.2, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.7, @babel/core@npm:^7.8.0": "@babel/core@npm:^7.1.0, @babel/core@npm:^7.1.6, @babel/core@npm:^7.11.1, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.10, @babel/core@npm:^7.13.13, @babel/core@npm:^7.13.14, @babel/core@npm:^7.13.8, @babel/core@npm:^7.14.0, @babel/core@npm:^7.14.6, @babel/core@npm:^7.15.5, @babel/core@npm:^7.16.0, @babel/core@npm:^7.17.10, @babel/core@npm:^7.17.9, @babel/core@npm:^7.18.2, @babel/core@npm:^7.18.5, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.7, @babel/core@npm:^7.8.0":
version: 7.18.5 version: 7.18.5
resolution: "@babel/core@npm:7.18.5" resolution: "@babel/core@npm:7.18.5"
dependencies: dependencies:
@@ -110,7 +110,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/eslint-parser@npm:^7.13.10, @babel/eslint-parser@npm:^7.13.14, @babel/eslint-parser@npm:^7.13.4, @babel/eslint-parser@npm:^7.13.8, @babel/eslint-parser@npm:^7.14.7, @babel/eslint-parser@npm:^7.16.3": "@babel/eslint-parser@npm:^7.13.10, @babel/eslint-parser@npm:^7.13.14, @babel/eslint-parser@npm:^7.13.4, @babel/eslint-parser@npm:^7.13.8, @babel/eslint-parser@npm:^7.14.7, @babel/eslint-parser@npm:^7.16.3, @babel/eslint-parser@npm:^7.18.2":
version: 7.18.2 version: 7.18.2
resolution: "@babel/eslint-parser@npm:7.18.2" resolution: "@babel/eslint-parser@npm:7.18.2"
dependencies: dependencies:
@@ -1296,7 +1296,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-runtime@npm:^7.0.0, @babel/plugin-transform-runtime@npm:^7.10.1, @babel/plugin-transform-runtime@npm:^7.11.0, @babel/plugin-transform-runtime@npm:^7.13.0, @babel/plugin-transform-runtime@npm:^7.13.10, @babel/plugin-transform-runtime@npm:^7.16.4, @babel/plugin-transform-runtime@npm:^7.18.2": "@babel/plugin-transform-runtime@npm:^7.0.0, @babel/plugin-transform-runtime@npm:^7.10.1, @babel/plugin-transform-runtime@npm:^7.11.0, @babel/plugin-transform-runtime@npm:^7.13.0, @babel/plugin-transform-runtime@npm:^7.16.4, @babel/plugin-transform-runtime@npm:^7.18.2, @babel/plugin-transform-runtime@npm:^7.18.5":
version: 7.18.5 version: 7.18.5
resolution: "@babel/plugin-transform-runtime@npm:7.18.5" resolution: "@babel/plugin-transform-runtime@npm:7.18.5"
dependencies: dependencies:
@@ -1517,7 +1517,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/preset-react@npm:^7.10.1, @babel/preset-react@npm:^7.10.4, @babel/preset-react@npm:^7.12.12, @babel/preset-react@npm:^7.12.13, @babel/preset-react@npm:^7.12.5, @babel/preset-react@npm:^7.13.13, @babel/preset-react@npm:^7.14.5, @babel/preset-react@npm:^7.16.0": "@babel/preset-react@npm:^7.10.1, @babel/preset-react@npm:^7.10.4, @babel/preset-react@npm:^7.12.12, @babel/preset-react@npm:^7.12.13, @babel/preset-react@npm:^7.12.5, @babel/preset-react@npm:^7.13.13, @babel/preset-react@npm:^7.14.5, @babel/preset-react@npm:^7.16.0, @babel/preset-react@npm:^7.17.12":
version: 7.17.12 version: 7.17.12
resolution: "@babel/preset-react@npm:7.17.12" resolution: "@babel/preset-react@npm:7.17.12"
dependencies: dependencies:
@@ -3905,6 +3905,54 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@otplib/core@npm:^12.0.1":
version: 12.0.1
resolution: "@otplib/core@npm:12.0.1"
checksum: b3c34bc20b31bc3f49cc0dc3c0eb070491c0101e8c1efa83cec48ca94158bd736aaca8187df667fc0c4a239d4ac52076bc44084bee04a50c80c3630caf77affa
languageName: node
linkType: hard
"@otplib/plugin-crypto@npm:^12.0.1":
version: 12.0.1
resolution: "@otplib/plugin-crypto@npm:12.0.1"
dependencies:
"@otplib/core": ^12.0.1
checksum: 6867c74ee8aca6c2db9670362cf51e44f3648602c39318bf537421242e33f0012a172acd43bbed9a21d706e535dc4c66aff965380673391e9fd74cf685b5b13a
languageName: node
linkType: hard
"@otplib/plugin-thirty-two@npm:^12.0.1":
version: 12.0.1
resolution: "@otplib/plugin-thirty-two@npm:12.0.1"
dependencies:
"@otplib/core": ^12.0.1
thirty-two: ^1.0.2
checksum: 920099e40d3e8c2941291c84c70064c2d86d0d1ed17230d650445d5463340e406bc413ddf2e40c374ddc4ee988ef1e3facacab9b5248b1ff361fd13df52bf88f
languageName: node
linkType: hard
"@otplib/preset-default@npm:^12.0.1":
version: 12.0.1
resolution: "@otplib/preset-default@npm:12.0.1"
dependencies:
"@otplib/core": ^12.0.1
"@otplib/plugin-crypto": ^12.0.1
"@otplib/plugin-thirty-two": ^12.0.1
checksum: 8133231384f6277f77eb8e42ef83bc32a8b01059bef147d1c358d9e9bfd292e1c239f581fe008367a48489dd68952b7ac0948e6c41412fc06079da2c91b71d16
languageName: node
linkType: hard
"@otplib/preset-v11@npm:^12.0.1":
version: 12.0.1
resolution: "@otplib/preset-v11@npm:12.0.1"
dependencies:
"@otplib/core": ^12.0.1
"@otplib/plugin-crypto": ^12.0.1
"@otplib/plugin-thirty-two": ^12.0.1
checksum: 367cb09397e617c21ec748d54e920ab43f1c5dfba70cbfd88edf73aecca399cf0c09fefe32518f79c7ee8a06e7058d14b200da378cc7d46af3cac4e22a153e2f
languageName: node
linkType: hard
"@pmmmwh/react-refresh-webpack-plugin@npm:^0.5.3": "@pmmmwh/react-refresh-webpack-plugin@npm:^0.5.3":
version: 0.5.7 version: 0.5.7
resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.7" resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.7"
@@ -4949,41 +4997,41 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@standardnotes/authenticator@workspace:packages/components/src/packages/org.standardnotes.token-vault" resolution: "@standardnotes/authenticator@workspace:packages/components/src/packages/org.standardnotes.token-vault"
dependencies: dependencies:
"@babel/core": ^7.13.10 "@babel/core": ^7.18.5
"@babel/eslint-parser": ^7.13.10 "@babel/eslint-parser": ^7.18.2
"@babel/plugin-proposal-class-properties": ^7.13.0 "@babel/plugin-proposal-class-properties": ^7.17.12
"@babel/plugin-transform-runtime": ^7.13.10 "@babel/plugin-transform-runtime": ^7.18.5
"@babel/preset-env": ^7.13.10 "@babel/preset-env": ^7.18.2
"@babel/preset-react": ^7.12.13 "@babel/preset-react": ^7.17.12
"@standardnotes/editor-kit": 2.2.1 "@standardnotes/editor-kit": 2.2.5
"@standardnotes/eslint-config-extensions": ^1.0.1 "@standardnotes/eslint-config-extensions": ^1.0.4
"@svgr/webpack": ^6.1.2 "@standardnotes/styles": "workspace:*"
babel-loader: ^8.2.2 "@svgr/webpack": ^6.2.1
css-loader: ^5.1.3 babel-loader: ^8.2.5
eslint: ^7.21.0 css-loader: ^6.7.1
eslint-plugin-react: ^7.22.0 eslint: "*"
html-webpack-plugin: ^5.3.1 eslint-plugin-react: ^7.30.0
immutability-helper: ^3.0.1 html-webpack-plugin: ^5.5.0
jsqr: ^1.2.0 immutability-helper: ^3.1.1
mini-css-extract-plugin: ^1.3.9 jsqr: ^1.4.0
mini-css-extract-plugin: ^2.6.1
node-sass: "*" node-sass: "*"
notp: ^2.0.3 notp: ^2.0.3
otplib: ^11.0.1 otplib: ^12.0.1
prop-types: ^15.7.2 prettier: "*"
react: ^17.0.1 prop-types: ^15.8.1
react: ^18.2.0
react-beautiful-dnd: ^13.1.0 react-beautiful-dnd: ^13.1.0
react-color: ^2.19.3 react-color: ^2.19.3
react-dom: ^17.0.1 react-dom: ^18.2.0
regenerator-runtime: ^0.13.2 sass-loader: ^13.0.0
sass-loader: ^11.0.1 style-loader: ~3.3.1
sn-stylekit: 2.1.0
style-loader: ~0.13.1
svg-url-loader: ^7.1.1 svg-url-loader: ^7.1.1
terser-webpack-plugin: ^5.1.1 terser-webpack-plugin: ^5.3.3
webpack: "*" webpack: "*"
webpack-cli: "*" webpack-cli: "*"
webpack-dev-server: "*" webpack-dev-server: "*"
webpack-merge: ^5.7.3 webpack-merge: ^5.8.0
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@@ -5178,13 +5226,6 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@standardnotes/editor-kit@npm:2.2.1":
version: 2.2.1
resolution: "@standardnotes/editor-kit@npm:2.2.1"
checksum: d856714b8d7c1b7704d1644fc30edab3536b3e1f0f277a009f2b2381e7df07f827e5a68c764b58192d26ea5385e71379c410e5509d71570b317cda04d56c5010
languageName: node
linkType: hard
"@standardnotes/editor-kit@npm:2.2.3": "@standardnotes/editor-kit@npm:2.2.3":
version: 2.2.3 version: 2.2.3
resolution: "@standardnotes/editor-kit@npm:2.2.3" resolution: "@standardnotes/editor-kit@npm:2.2.3"
@@ -5256,7 +5297,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@standardnotes/eslint-config-extensions@npm:^1.0.0, @standardnotes/eslint-config-extensions@npm:^1.0.1, @standardnotes/eslint-config-extensions@npm:^1.0.2, @standardnotes/eslint-config-extensions@npm:^1.0.4": "@standardnotes/eslint-config-extensions@npm:^1.0.0, @standardnotes/eslint-config-extensions@npm:^1.0.2, @standardnotes/eslint-config-extensions@npm:^1.0.4":
version: 1.0.4 version: 1.0.4
resolution: "@standardnotes/eslint-config-extensions@npm:1.0.4" resolution: "@standardnotes/eslint-config-extensions@npm:1.0.4"
peerDependencies: peerDependencies:
@@ -6373,7 +6414,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@svgr/webpack@npm:^6.1.2, @svgr/webpack@npm:^6.2.1": "@svgr/webpack@npm:^6.2.1":
version: 6.2.1 version: 6.2.1
resolution: "@svgr/webpack@npm:6.2.1" resolution: "@svgr/webpack@npm:6.2.1"
dependencies: dependencies:
@@ -14789,7 +14830,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint-plugin-react@npm:*, eslint-plugin-react@npm:^7.22.0, eslint-plugin-react@npm:^7.23.1, eslint-plugin-react@npm:^7.24.0, eslint-plugin-react@npm:^7.26.1, eslint-plugin-react@npm:^7.27.1, eslint-plugin-react@npm:^7.29.4": "eslint-plugin-react@npm:*, eslint-plugin-react@npm:^7.22.0, eslint-plugin-react@npm:^7.23.1, eslint-plugin-react@npm:^7.24.0, eslint-plugin-react@npm:^7.26.1, eslint-plugin-react@npm:^7.27.1, eslint-plugin-react@npm:^7.29.4, eslint-plugin-react@npm:^7.30.0":
version: 7.30.0 version: 7.30.0
resolution: "eslint-plugin-react@npm:7.30.0" resolution: "eslint-plugin-react@npm:7.30.0"
dependencies: dependencies:
@@ -14953,7 +14994,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint@npm:^7.20.0, eslint@npm:^7.21.0": "eslint@npm:^7.20.0":
version: 7.32.0 version: 7.32.0
resolution: "eslint@npm:7.32.0" resolution: "eslint@npm:7.32.0"
dependencies: dependencies:
@@ -17851,7 +17892,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"immutability-helper@npm:^3.0.1": "immutability-helper@npm:^3.1.1":
version: 3.1.1 version: 3.1.1
resolution: "immutability-helper@npm:3.1.1" resolution: "immutability-helper@npm:3.1.1"
checksum: 6fdbf6d2123efa567263e904bbaff07aca0e24560d270d34967b03aab8ec20bd3e4057f394d59e50eb6c4718c9415591a6281692bb0aafd522ad72cf4887133f checksum: 6fdbf6d2123efa567263e904bbaff07aca0e24560d270d34967b03aab8ec20bd3e4057f394d59e50eb6c4718c9415591a6281692bb0aafd522ad72cf4887133f
@@ -20063,7 +20104,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"jsqr@npm:^1.2.0": "jsqr@npm:^1.4.0":
version: 1.4.0 version: 1.4.0
resolution: "jsqr@npm:1.4.0" resolution: "jsqr@npm:1.4.0"
checksum: 7c572971f90c42772e30d152bde63b84edf1164bde80c53942e6b2068ea31caf00ad704aa46cacc9e71645f52dbeddebc6e84ba15e883c678ee93cde690de339 checksum: 7c572971f90c42772e30d152bde63b84edf1164bde80c53942e6b2068ea31caf00ad704aa46cacc9e71645f52dbeddebc6e84ba15e883c678ee93cde690de339
@@ -20530,7 +20571,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"loader-utils@npm:^1.0.2, loader-utils@npm:^1.1.0": "loader-utils@npm:^1.1.0":
version: 1.4.0 version: 1.4.0
resolution: "loader-utils@npm:1.4.0" resolution: "loader-utils@npm:1.4.0"
dependencies: dependencies:
@@ -22349,7 +22390,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mini-css-extract-plugin@npm:^2.0.0, mini-css-extract-plugin@npm:^2.4.5, mini-css-extract-plugin@npm:^2.5.3, mini-css-extract-plugin@npm:^2.6.0": "mini-css-extract-plugin@npm:^2.0.0, mini-css-extract-plugin@npm:^2.4.5, mini-css-extract-plugin@npm:^2.5.3, mini-css-extract-plugin@npm:^2.6.0, mini-css-extract-plugin@npm:^2.6.1":
version: 2.6.1 version: 2.6.1
resolution: "mini-css-extract-plugin@npm:2.6.1" resolution: "mini-css-extract-plugin@npm:2.6.1"
dependencies: dependencies:
@@ -23699,12 +23740,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"otplib@npm:^11.0.1": "otplib@npm:^12.0.1":
version: 11.0.1 version: 12.0.1
resolution: "otplib@npm:11.0.1" resolution: "otplib@npm:12.0.1"
dependencies: dependencies:
thirty-two: 1.0.2 "@otplib/core": ^12.0.1
checksum: 42225f1ccc4562fc062dfd0cbe4b0c527f56648775601175b638e54850c44a1dbe1770e5858a2e50216e5111bd4dd2776df3372a92f74a2fb41e7f3975dc0bbd "@otplib/preset-default": ^12.0.1
"@otplib/preset-v11": ^12.0.1
checksum: 4a1b91cf1b8e920b50ad4bac2ef2a89126630c62daf68e9b32ff15106b2551db905d3b979955cf5f8f114da0a8883cec3d636901d65e793c1745bb4174e2a572
languageName: node languageName: node
linkType: hard linkType: hard
@@ -26285,7 +26328,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-dom@npm:^18.1.0": "react-dom@npm:^18.1.0, react-dom@npm:^18.2.0":
version: 18.2.0 version: 18.2.0
resolution: "react-dom@npm:18.2.0" resolution: "react-dom@npm:18.2.0"
dependencies: dependencies:
@@ -27075,7 +27118,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react@npm:^18.1.0": "react@npm:^18.1.0, react@npm:^18.2.0":
version: 18.2.0 version: 18.2.0
resolution: "react@npm:18.2.0" resolution: "react@npm:18.2.0"
dependencies: dependencies:
@@ -28232,6 +28275,31 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sass-loader@npm:^13.0.0":
version: 13.0.0
resolution: "sass-loader@npm:13.0.0"
dependencies:
klona: ^2.0.4
neo-async: ^2.6.2
peerDependencies:
fibers: ">= 3.1.0"
node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
sass: ^1.3.0
sass-embedded: "*"
webpack: ^5.0.0
peerDependenciesMeta:
fibers:
optional: true
node-sass:
optional: true
sass:
optional: true
sass-embedded:
optional: true
checksum: f7af03813dccf0405eb02917cd10c97571ab81f42e9ea1f3da6d9e96991e076521809a452ad319d57c1a63273ce07c23ddfdbda5cd071a56d261dc28913afdaa
languageName: node
linkType: hard
"sass-loader@npm:^9.0.3": "sass-loader@npm:^9.0.3":
version: 9.0.3 version: 9.0.3
resolution: "sass-loader@npm:9.0.3" resolution: "sass-loader@npm:9.0.3"
@@ -29798,15 +29866,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"style-loader@npm:~0.13.1":
version: 0.13.2
resolution: "style-loader@npm:0.13.2"
dependencies:
loader-utils: ^1.0.2
checksum: 68bdfbf4e759abf6e5195880966ac9407b758ca9f1fd96dc2584554707d25d8dbbe1a9d6524a617513e7d28d9f51bdd754689c1e6ae12ac1c4ff62781f5e7ccc
languageName: node
linkType: hard
"style-loader@npm:~1.2.1": "style-loader@npm:~1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "style-loader@npm:1.2.1" resolution: "style-loader@npm:1.2.1"
@@ -30276,7 +30335,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"terser-webpack-plugin@npm:^5.1.1, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.1.4, terser-webpack-plugin@npm:^5.2.5, terser-webpack-plugin@npm:^5.3.1": "terser-webpack-plugin@npm:^5.1.1, terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.1.4, terser-webpack-plugin@npm:^5.2.5, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.3":
version: 5.3.3 version: 5.3.3
resolution: "terser-webpack-plugin@npm:5.3.3" resolution: "terser-webpack-plugin@npm:5.3.3"
dependencies: dependencies:
@@ -30337,7 +30396,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"thirty-two@npm:1.0.2": "thirty-two@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "thirty-two@npm:1.0.2" resolution: "thirty-two@npm:1.0.2"
checksum: f6700b31d16ef942fdc0d14daed8a2f69ea8b60b0e85db8b83adf58d84bbeafe95a17d343ab55efaae571bb5148b62fc0ee12b04781323bf7af7d7e9693eec76 checksum: f6700b31d16ef942fdc0d14daed8a2f69ea8b60b0e85db8b83adf58d84bbeafe95a17d343ab55efaae571bb5148b62fc0ee12b04781323bf7af7d7e9693eec76