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
run: yarn lint
working-directory: packages/components
- name: Build components
run: yarn build
working-directory: packages/components
run: yarn build:components
- name: Test components
run: yarn test
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",
"version": "./scripts/VERSION.sh"
},
"dependencies": {
"@standardnotes/styles": "workspace:*"
},
"devDependencies": {
"@standardnotes/deterministic-zip": "^1.2.0",
"@standardnotes/eslint-config-extensions": "^1.0.4",
"@standardnotes/features": "^1.45.1",
"@standardnotes/styles": "workspace:*",
"copy-webpack-plugin": "^11.0.0",
"mini-css-extract-plugin": "^2.6.0",
"minimatch": "^5.1.0",

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
## 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/).
Previous versions of TokenVault retain the license they were released with.
Previous versions of TokenVault retain the license they were released with.

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 PropTypes from 'prop-types';
import { totp } from '@Lib/otp';
import CountdownPie from '@Components/CountdownPie';
import AuthMenu from '@Components/AuthMenu';
import DragIndicator from '../assets/svg/drag-indicator.svg';
import { getEntryColor, getVarColorForContrast, hexColorToRGB } from '@Lib/utils';
import AuthMenu from '@Components/AuthMenu'
import CountdownPie from '@Components/CountdownPie'
import { totp } from '@Lib/otp'
import { getEntryColor, getVarColorForContrast, hexColorToRGB } from '@Lib/utils'
import PropTypes from 'prop-types'
import React from 'react'
import DragIndicator from '../assets/svg/drag-indicator.svg'
export default class AuthEntry extends React.Component {
constructor(props) {
super(props);
super(props)
this.state = {
token: '',
@@ -16,106 +16,109 @@ export default class AuthEntry extends React.Component {
entryStyle: {
color: '',
backgroundColor: '',
}
};
},
}
this.updateToken();
this.updateToken()
}
getTimeLeft() {
const seconds = new Date().getSeconds();
return seconds > 29 ? 60 - seconds : 30 - seconds;
const seconds = new Date().getSeconds()
return seconds > 29 ? 60 - seconds : 30 - seconds
}
updateToken = async () => {
const { secret } = this.props.entry;
const token = await totp.gen(secret);
const { secret } = this.props.entry
if (!secret) {
return
}
const timeLeft = this.getTimeLeft();
const token = await totp.gen(secret)
const timeLeft = this.getTimeLeft()
this.setState({
token,
timeLeft
});
timeLeft,
})
this.timer = setTimeout(this.updateToken, timeLeft * 1000);
this.timer = setTimeout(this.updateToken, timeLeft * 1000)
}
componentDidMount() {
this.updateEntryStyle();
this.updateEntryStyle()
}
componentDidUpdate(prevProps) {
// If the secret changed make sure to recalculate token
if (prevProps.entry.secret !== this.props.entry.secret) {
clearTimeout(this.timer);
this.timer = setTimeout(this.updateToken, 0);
clearTimeout(this.timer)
this.timer = setTimeout(this.updateToken, 0)
}
if (prevProps.lastUpdated !== this.props.lastUpdated) {
this.updateEntryStyle(true);
this.updateEntryStyle(true)
}
}
componentWillUnmount() {
clearTimeout(this.timer);
clearTimeout(this.timer)
}
handleInputChange = event => {
const target = event.target;
const name = target.name;
handleInputChange = (event) => {
const target = event.target
const name = target.name
this.props.onEntryChange({
id: this.props.id,
name,
value: target.value
});
value: target.value,
})
}
copyToClipboard = (value) => {
const textField = document.createElement('textarea');
textField.innerText = value;
document.body.appendChild(textField);
textField.select();
document.execCommand('copy');
textField.remove();
this.props.onCopyValue();
const textField = document.createElement('textarea')
textField.innerText = value
document.body.appendChild(textField)
textField.select()
document.execCommand('copy')
textField.remove()
this.props.onCopyValue()
}
updateEntryStyle = (useDelay = false) => {
/**
* 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.
*/
const DELAY_BEFORE_READING_PROPERTIES = useDelay ? 0 : 50;
const DELAY_BEFORE_READING_PROPERTIES = useDelay ? 0 : 50
setTimeout(() => {
const { entryStyle } = this.state;
const entryColor = getEntryColor(document, this.props.entry);
const { entryStyle } = this.state
const entryColor = getEntryColor(document, this.props.entry)
if (entryColor) {
// The background color for the entry.
entryStyle.backgroundColor = entryColor;
entryStyle.backgroundColor = entryColor
const rgbColor = hexColorToRGB(entryColor);
const varColor = getVarColorForContrast(rgbColor);
const rgbColor = hexColorToRGB(entryColor)
const varColor = getVarColorForContrast(rgbColor)
// The foreground color for the entry.
entryStyle.color = `var(${varColor})`;
entryStyle.color = `var(${varColor})`
}
this.setState({
entryStyle
});
}, DELAY_BEFORE_READING_PROPERTIES);
entryStyle,
})
}, DELAY_BEFORE_READING_PROPERTIES)
}
render() {
const { service, account, notes, password } = this.props.entry;
const { id, onEdit, onRemove, canEdit, style, innerRef, ...divProps } = this.props;
const { token, timeLeft, entryStyle } = this.state;
const { service, account, notes, password, secret } = this.props.entry
const { id, onEdit, onRemove, canEdit, style, innerRef, ...divProps } = this.props
const { token, timeLeft, entryStyle } = this.state
delete divProps.onCopyValue;
delete divProps.lastUpdated;
delete divProps.onCopyValue
delete divProps.lastUpdated
return (
<div
@@ -123,14 +126,14 @@ export default class AuthEntry extends React.Component {
className="sk-notification sk-base-custom"
style={{
...entryStyle,
...style
...style,
}}
ref={innerRef}
>
<div className="auth-entry">
{canEdit && (
<div className="auth-drag-indicator-container">
<DragIndicator />
<DragIndicator className="grab-cursor" />
</div>
)}
<div className="auth-details">
@@ -146,27 +149,29 @@ export default class AuthEntry extends React.Component {
{password && (
<div className="auth-password-row">
<div className="auth-password" onClick={() => this.copyToClipboard(password)}>
{'•'.repeat(password.length)}
</div>
</div>
)}
</div>
</div>
<div className="auth-token-info">
<div className="auth-token" onClick={() => this.copyToClipboard(token)}>
<div>{token.slice(0, 3)}</div>
<div>{token.slice(3, 6)}</div>
{secret && (
<div className="auth-token-info">
<div className="auth-token" onClick={() => this.copyToClipboard(token)}>
<div>{token.slice(0, 3)}</div>
<div>{token.slice(3, 6)}</div>
</div>
<div className="auth-countdown">
<CountdownPie
token={token}
timeLeft={timeLeft}
total={30}
bgColor={entryStyle.backgroundColor}
fgColor={entryStyle.color}
/>
</div>
</div>
<div className="auth-countdown">
<CountdownPie
token={token}
timeLeft={timeLeft}
total={30}
bgColor={entryStyle.backgroundColor}
fgColor={entryStyle.color}
/>
</div>
</div>
)}
</div>
{canEdit && (
<div className="auth-options">
@@ -179,7 +184,7 @@ export default class AuthEntry extends React.Component {
)}
</div>
</div>
);
)
}
}
@@ -193,5 +198,5 @@ AuthEntry.propTypes = {
canEdit: PropTypes.bool.isRequired,
innerRef: PropTypes.func.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 {
constructor(props) {
super(props);
super(props)
this.state = {
show: false
};
show: false,
}
}
onToggle = () => {
this.setState({
show: !this.state.show
});
show: !this.state.show,
})
}
onEdit = () => {
this.onToggle();
this.props.onEdit();
this.onToggle()
this.props.onEdit()
}
onRemove = () => {
this.onToggle();
this.props.onRemove();
this.onToggle()
this.props.onRemove()
}
render() {
const { buttonColor } = this.props;
const { buttonColor } = this.props
const buttonStyle = {};
const buttonStyle = {}
if (buttonColor) {
buttonStyle.color = buttonColor;
buttonStyle.color = buttonColor
}
return (
@@ -39,24 +39,25 @@ export default class AuthMenu extends React.Component {
<div className="sk-button" onClick={this.onToggle} style={buttonStyle}>
<div className="sk-label"></div>
</div>
{this.state.show && (
<div className="auth-overlay" onClick={this.onToggle} />,
<div className="sk-menu-panel">
<div className="sk-menu-panel-row" onClick={this.onEdit}>
<div className="sk-label">Edit</div>
{this.state.show &&
((<div className="auth-overlay" onClick={this.onToggle} />),
(
<div className="sk-menu-panel">
<div className="sk-menu-panel-row" onClick={this.onEdit}>
<div className="sk-label">Edit</div>
</div>
<div className="sk-menu-panel-row" onClick={this.onRemove}>
<div className="sk-label">Remove</div>
</div>
</div>
<div className="sk-menu-panel-row" onClick={this.onRemove}>
<div className="sk-label">Remove</div>
</div>
</div>
)}
))}
</div>
);
)
}
}
AuthMenu.propTypes = {
onEdit: 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 }) => (
<div className="auth-overlay">
@@ -26,13 +25,13 @@ const ConfirmDialog = ({ title, message, onConfirm, onCancel }) => (
</div>
</div>
</div>
);
)
ConfirmDialog.propTypes = {
title: PropTypes.string.isRequired,
message: PropTypes.string.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 }) => (
<div
className={`auth-copy-notification ${isVisible ? 'visible' : 'hidden'}`}
>
<div className={`auth-copy-notification ${isVisible ? 'visible' : 'hidden'}`}>
<div className="sk-panel">
<div className="sk-font-small sk-bold">Copied value to clipboard.</div>
</div>
</div>
);
)
CopyNotification.propTypes = {
CopyNotification.propTypes = {
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(
token
)} {
const rotaAnimation = (token, offset) => `@keyframes rota_${animationName(token)} {
0% {
transform: rotate(${offset}deg);
}
@@ -13,11 +11,9 @@ const rotaAnimation = (token, offset) => `@keyframes rota_${animationName(
100% {
transform: rotate(360deg);
}
}`;
}`
const opaAnimation = (token, offset) => `@keyframes opa_${animationName(
token
)} {
const opaAnimation = (token, offset) => `@keyframes opa_${animationName(token)} {
0% {
opacity: 1;
}
@@ -26,12 +22,9 @@ const opaAnimation = (token, offset) => `@keyframes opa_${animationName(
100% {
opacity: 0;
}
}`;
}`
const opaReverseAnimation = (
token,
offset
) => `@keyframes opa_r_${animationName(token)} {
const opaReverseAnimation = (token, offset) => `@keyframes opa_r_${animationName(token)} {
0% {
opacity: 0;
}
@@ -40,81 +33,81 @@ const opaReverseAnimation = (
100% {
opacity: 1;
}
}`;
}`
function calculateOpaOffset(timeLeft, total) {
const percentage = calculatePercentage(timeLeft, total) * 100;
const percTo50 = 50 - percentage;
const percentage = calculatePercentage(timeLeft, total) * 100
const percTo50 = 50 - percentage
// 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) {
return calculatePercentage(timeLeft, total) * 360;
return calculatePercentage(timeLeft, total) * 360
}
function calculatePercentage(timeLeft, total) {
return (total - timeLeft) / total;
return (total - timeLeft) / total
}
function useRotateAnimation(token, timeLeft, total) {
useEffect(
function createRotateAnimation() {
const style = document.createElement('style');
document.head.appendChild(style);
const styleSheet = style.sheet;
const style = document.createElement('style')
document.head.appendChild(style)
const styleSheet = style.sheet
const rotaKeyframes = rotaAnimation(
token,
calculateRotaOffset(timeLeft, total)
);
const opaKeyframes = opaAnimation(token, calculateOpaOffset(timeLeft, total));
const opaReverseKeyframes = opaReverseAnimation(
token,
calculateOpaOffset(timeLeft, total)
);
const rotaKeyframes = rotaAnimation(token, calculateRotaOffset(timeLeft, total))
const opaKeyframes = opaAnimation(token, calculateOpaOffset(timeLeft, total))
const opaReverseKeyframes = opaReverseAnimation(token, calculateOpaOffset(timeLeft, total))
styleSheet.insertRule(rotaKeyframes, styleSheet.cssRules.length);
styleSheet.insertRule(opaKeyframes, styleSheet.cssRules.length);
styleSheet.insertRule(opaReverseKeyframes, styleSheet.cssRules.length);
styleSheet.insertRule(rotaKeyframes, styleSheet.cssRules.length)
styleSheet.insertRule(opaKeyframes, styleSheet.cssRules.length)
styleSheet.insertRule(opaReverseKeyframes, styleSheet.cssRules.length)
function cleanup() {
style.remove();
style.remove()
}
const timer = setTimeout(cleanup, timeLeft * 1000);
const timer = setTimeout(cleanup, timeLeft * 1000)
return () => {
clearTimeout(timer);
cleanup();
};
clearTimeout(timer)
cleanup()
}
},
[token, timeLeft, total]
);
[token, timeLeft, total],
)
}
const CountdownPie = ({ token, timeLeft, total, bgColor, fgColor }) => {
useRotateAnimation(token, timeLeft, total);
useRotateAnimation(token, timeLeft, total)
return (
<div className="countdown-pie" style={{
backgroundColor: bgColor
}}>
<div
className="countdown-pie"
style={{
backgroundColor: bgColor,
}}
>
<div
className="pie spinner"
style={{
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
className="pie filler"
style={{
animation: `opa_r_${animationName(token)} ${timeLeft}s steps(1, end)`,
backgroundColor: fgColor
backgroundColor: fgColor,
}}
/>
<div
@@ -124,15 +117,15 @@ const CountdownPie = ({ token, timeLeft, total, bgColor, fgColor }) => {
}}
/>
</div>
);
};
)
}
CountdownPie.propTypes = {
token: PropTypes.string.isRequired,
timeLeft: PropTypes.number.isRequired,
total: PropTypes.number.isRequired,
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 = () => (
<div className="auth-overlay">
<div className="auth-dialog sk-panel">
@@ -10,15 +8,14 @@ const DataErrorAlert = () => (
<div className="sk-panel-section sk-panel-hero">
<div className="sk-panel-row">
<div className="sk-h1">
The note you selected already has existing data that is not valid
with this editor. Please clear the note, or select a new one, and
try again.
The note you selected already has existing data that is not valid with this editor. Please clear the note,
or select a new one, and try again.
</div>
</div>
</div>
</div>
</div>
</div>
);
)
export default DataErrorAlert;
export default DataErrorAlert

View File

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

View File

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

View File

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

View File

@@ -1,48 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import AuthEntry from '@Components/AuthEntry';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import AuthEntry from '@Components/AuthEntry'
import PropTypes from 'prop-types'
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'
const reorderEntries = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
const result = Array.from(list)
const [removed] = result.splice(startIndex, 1)
result.splice(endIndex, 0, removed)
return result;
};
return result
}
const ViewEntries = ({ entries, onEdit, onRemove, onCopyValue, canEdit, updateEntries, searchValue, lastUpdated }) => {
const onDragEnd = (result) => {
const droppedOutsideList = !result.destination;
const droppedOutsideList = !result.destination
if (droppedOutsideList) {
return;
return
}
const orderedEntries = reorderEntries(
entries,
result.source.index,
result.destination.index
);
const orderedEntries = reorderEntries(entries, result.source.index, result.destination.index)
updateEntries(orderedEntries);
};
updateEntries(orderedEntries)
}
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable" isDropDisabled={!canEdit}>
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className="auth-list"
>
<div {...provided.droppableProps} ref={provided.innerRef} className="auth-list">
{entries.map((entry, index) => {
/**
* 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)) {
return;
return
}
return (
<Draggable
@@ -67,17 +58,17 @@ const ViewEntries = ({ entries, onEdit, onRemove, onCopyValue, canEdit, updateEn
/>
)}
</Draggable>
);
)
})}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
)
}
ViewEntries.propTypes = {
ViewEntries.propTypes = {
entries: PropTypes.arrayOf(PropTypes.object),
onEdit: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
@@ -85,7 +76,7 @@ ViewEntries.propTypes = {
canEdit: PropTypes.bool.isRequired,
lastUpdated: PropTypes.number.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 {
base32ToHex,
leftpad,
decToHex,
bufToHex,
hextoBuf,
hexToBytes
} from '@Lib/utils';
export { secretPattern, parseKeyUri } from '@Lib/utils';
import { base32ToHex, bufToHex, decToHex, hextoBuf, hexToBytes, leftpad } from '@Lib/utils'
export { parseKeyUri, secretPattern } from '@Lib/utils'
class Hotp {
/**
@@ -25,25 +18,25 @@ class Hotp {
*
*/
async gen(secret, opt) {
var key = base32ToHex(secret) || '';
opt = opt || {};
var counter = opt.counter || 0;
var key = base32ToHex(secret) || ''
opt = opt || {}
var counter = opt.counter || 0
var hexCounter = leftpad(decToHex(counter), 16, '0');
var digest = await this.createHmac('SHA-1', key, hexCounter);
var h = hexToBytes(digest);
var hexCounter = leftpad(decToHex(counter), 16, '0')
var digest = await this.createHmac('SHA-1', key, hexCounter)
var h = hexToBytes(digest)
// Truncate
var offset = h[h.length - 1] & 0xf;
var offset = h[h.length - 1] & 0xf
var v =
((h[offset] & 0x7f) << 24) |
((h[offset + 1] & 0xff) << 16) |
((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) {
opt = opt || {};
var window = opt.window || 50;
var counter = opt.counter || 0;
opt = opt || {}
var window = opt.window || 50
var counter = opt.counter || 0
// Now loop through from C to C + W to determine if there is
// a correct code
for (var i = counter - window; i <= counter + window; ++i) {
opt.counter = i;
opt.counter = i
if ((await this.gen(key, opt)) === token) {
// We have found a matching code, trigger callback
// and pass offset
return { delta: i - counter };
return { delta: i - counter }
}
}
// If we get to here then no codes have matched, return null
return null;
return null
}
async createHmac(alg, key, str) {
@@ -102,17 +95,17 @@ class Hotp {
{
// algorithm details
name: 'HMAC',
hash: { name: alg }
hash: { name: alg },
},
false, // export = false
['sign'] // what this key can do
);
const sig = await window.crypto.subtle.sign('HMAC', hmacKey, hextoBuf(str));
return bufToHex(sig);
['sign'], // what this key can do
)
const sig = await window.crypto.subtle.sign('HMAC', hmacKey, hextoBuf(str))
return bufToHex(sig)
}
}
export const hotp = new Hotp();
export const hotp = new Hotp()
class Totp {
/**
@@ -133,15 +126,15 @@ class Totp {
*
*/
async gen(key, opt) {
opt = opt || {};
var time = opt.time || 30;
var _t = Date.now();
opt = opt || {}
var time = opt.time || 30
var _t = Date.now()
// Determine the value of the counter, C
// 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) {
opt = opt || {};
var time = opt.time || 30;
var _t = Date.now();
opt = opt || {}
var time = opt.time || 30
var _t = Date.now()
// Determine the value of the counter, C
// 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';
export const secretPattern = `^[${base32chars}]{16,}$`;
const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
export const secretPattern = `^[${base32chars}]{16,}$`
export function hexToBytes(hex) {
var bytes = [];
var bytes = []
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) {
return (s < 15.5 ? '0' : '') + Math.round(s).toString(16);
return (s < 15.5 ? '0' : '') + Math.round(s).toString(16)
}
export function bufToHex(buf) {
return Array.prototype.map
.call(new Uint8Array(buf), x => ('00' + x.toString(16)).slice(-2))
.join('');
return Array.prototype.map.call(new Uint8Array(buf), (x) => ('00' + x.toString(16)).slice(-2)).join('')
}
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) {
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) {
var bits, chunk, hex, i, val;
bits = '';
hex = '';
i = 0;
var bits, chunk, hex, i, val
bits = ''
hex = ''
i = 0
while (i < base32.length) {
val = base32chars.indexOf(base32.charAt(i).toUpperCase());
bits += leftpad(val.toString(2), 5, '0');
i++;
val = base32chars.indexOf(base32.charAt(i).toUpperCase())
bits += leftpad(val.toString(2), 5, '0')
i++
}
i = 0;
i = 0
while (i + 4 <= bits.length) {
chunk = bits.substr(i, 4);
hex = hex + parseInt(chunk, 2).toString(16);
i += 4;
chunk = bits.substr(i, 4)
hex = hex + parseInt(chunk, 2).toString(16)
i += 4
}
return hex;
return hex
}
export function leftpad(str, len, pad) {
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) {
// 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
// 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) {
return null;
return null
}
// eslint-disable-next-line no-unused-vars
const [fullUri, type, fullLabel] = parts;
const [fullUri, type, fullLabel] = parts
// Sanity check type and label
if (!type || !fullLabel) {
return null;
return null
}
// Parse the label
const decodedLabel = decodeURIComponent(fullLabel);
const decodedLabel = decodeURIComponent(fullLabel)
const labelParts = decodedLabel.split(/: ?/);
const labelParts = decodedLabel.split(/: ?/)
const label =
labelParts && labelParts.length === 2
? { issuer: labelParts[0], account: labelParts[1] }
: { issuer: '', account: decodedLabel };
: { issuer: '', account: decodedLabel }
// 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]) => {
acc[key] = value;
acc[key] = value
return acc;
}, {});
return acc
}, {})
// 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) {
// 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;
hexColor = hexColor.replace(shortHandFormRegex, function(m, red, green, 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);
return result ? {
red: parseInt(result[1], 16),
green: parseInt(result[2], 16),
blue: parseInt(result[3], 16)
} : null;
const shortHandFormRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
hexColor = hexColor.replace(shortHandFormRegex, function (m, red, green, 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)
return result
? {
red: parseInt(result[1], 16),
green: parseInt(result[2], 16),
blue: parseInt(result[3], 16),
}
: null
}
export const defaultBgColor = '#FFF';
export const defaultBgColor = '#FFF'
/**
* 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) {
const styleKitColors = {
foreground: '--sn-stylekit-contrast-foreground-color',
background: '--sn-stylekit-contrast-background-color'
};
if (!backgroundColor) {
return styleKitColors.foreground;
background: '--sn-stylekit-contrast-background-color',
}
const colorContrast = Math.round(((parseInt(backgroundColor.red) * 299) + (parseInt(backgroundColor.green) * 587) + (parseInt(backgroundColor.blue) * 114)) / 1000);
return (colorContrast > 70) ? styleKitColors.background : styleKitColors.foreground;
if (!backgroundColor) {
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) {
return getComputedStyle(document.documentElement)
.getPropertyValue(propertyName).trim().toUpperCase();
return getComputedStyle(document.documentElement).getPropertyValue(propertyName).trim().toUpperCase()
}
export const contextualColors = [
'info',
'success',
'neutral',
'warning'
];
export const contextualColors = ['info', 'success', 'neutral', 'warning']
export function getContextualColor(document, colorName) {
if (!contextualColors.includes(colorName)) {
return;
return
}
return getPropertyValue(
document,
`--sn-stylekit-${colorName}-color`
);
return getPropertyValue(document, `--sn-stylekit-${colorName}-color`)
}
export function getEntryColor(document, entry) {
const { color } = entry;
const { color } = entry
if (!contextualColors.includes(color)) {
return color;
return color
}
return getContextualColor(document, color);
return getContextualColor(document, color)
}
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,
html {
font-family: var(--sn-stylekit-sans-serif-font);
height: 100%;
width: 100%;
margin: 0;
padding: 0;
font-size: var(--sn-stylekit-base-font-size);
background-color: transparent;
background-color: var(--sn-stylekit-contrast-background-color);
padding: 0 !important;
}
* {
// To prevent gray flash when focusing input on mobile Safari
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
font-family: var(--sn-stylekit-sans-serif-font);
}
.sn-component {
display: flex;
flex-direction: column;
height: 100vh;
@media screen and (max-width: 420px) {
min-height: -webkit-fill-available;
}
.sk-panel-content {
height: inherit !important;
@@ -127,7 +126,6 @@ html {
}
}
/* entry default styles */
.auth-entry {
display: flex;
@@ -291,7 +289,7 @@ html {
padding: 5px;
background: var(--sn-stylekit-contrast-background-color);
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;
cursor: pointer;
}
@@ -409,9 +407,37 @@ html {
}
// 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-repeat: no-repeat !important;
background-position: top 4px right 4px !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",
"start": "webpack serve --config webpack.dev.js --progress --hot",
"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": {
"main": "dist/index.html"
},
"lint-staged": {
"README.md": [
"prettier --write"
],
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
"prettier --write"
]
},
"devDependencies": {
"@babel/core": "^7.13.10",
"@babel/eslint-parser": "^7.13.10",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-transform-runtime": "^7.13.10",
"@babel/preset-env": "^7.13.10",
"@babel/preset-react": "^7.12.13",
"@standardnotes/editor-kit": "2.2.1",
"@standardnotes/eslint-config-extensions": "^1.0.1",
"@svgr/webpack": "^6.1.2",
"babel-loader": "^8.2.2",
"css-loader": "^5.1.3",
"eslint": "^7.21.0",
"eslint-plugin-react": "^7.22.0",
"html-webpack-plugin": "^5.3.1",
"immutability-helper": "^3.0.1",
"jsqr": "^1.2.0",
"mini-css-extract-plugin": "^1.3.9",
"@babel/core": "^7.18.5",
"@babel/eslint-parser": "^7.18.2",
"@babel/plugin-proposal-class-properties": "^7.17.12",
"@babel/plugin-transform-runtime": "^7.18.5",
"@babel/preset-env": "^7.18.2",
"@babel/preset-react": "^7.17.12",
"@standardnotes/editor-kit": "2.2.5",
"@standardnotes/eslint-config-extensions": "^1.0.4",
"@standardnotes/styles": "workspace:*",
"@svgr/webpack": "^6.2.1",
"babel-loader": "^8.2.5",
"css-loader": "^6.7.1",
"eslint": "*",
"eslint-plugin-react": "^7.30.0",
"html-webpack-plugin": "^5.5.0",
"immutability-helper": "^3.1.1",
"jsqr": "^1.4.0",
"mini-css-extract-plugin": "^2.6.1",
"node-sass": "*",
"notp": "^2.0.3",
"otplib": "^11.0.1",
"prop-types": "^15.7.2",
"react": "^17.0.1",
"otplib": "^12.0.1",
"prettier": "*",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.0",
"react-color": "^2.19.3",
"react-dom": "^17.0.1",
"regenerator-runtime": "^0.13.2",
"sass-loader": "^11.0.1",
"sn-stylekit": "2.1.0",
"style-loader": "~0.13.1",
"react-dom": "^18.2.0",
"sass-loader": "^13.0.0",
"style-loader": "~3.3.1",
"svg-url-loader": "^7.1.1",
"terser-webpack-plugin": "^5.1.1",
"terser-webpack-plugin": "^5.3.3",
"webpack": "*",
"webpack-cli": "*",
"webpack-dev-server": "*",
"webpack-merge": "^5.7.3"
"webpack-merge": "^5.8.0"
}
}

View File

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

View File

@@ -7,14 +7,17 @@ module.exports = merge(config, {
devtool: 'cheap-source-map',
devServer: {
port: 8001,
contentBase: path.resolve(__dirname, 'dist'),
disableHostCheck: true,
static: path.resolve(__dirname, 'dist'),
allowedHosts: "all",
historyApiFallback: true,
watchOptions: { aggregateTimeout: 300, poll: 1000 },
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'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;
}
.pr-4 {
padding-right: 1rem;
}
.pl-1 {
padding-left: 0.25rem;
}

187
yarn.lock
View File

@@ -87,7 +87,7 @@ __metadata:
languageName: node
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
resolution: "@babel/core@npm:7.18.5"
dependencies:
@@ -110,7 +110,7 @@ __metadata:
languageName: node
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
resolution: "@babel/eslint-parser@npm:7.18.2"
dependencies:
@@ -1296,7 +1296,7 @@ __metadata:
languageName: node
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
resolution: "@babel/plugin-transform-runtime@npm:7.18.5"
dependencies:
@@ -1517,7 +1517,7 @@ __metadata:
languageName: node
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
resolution: "@babel/preset-react@npm:7.17.12"
dependencies:
@@ -3905,6 +3905,54 @@ __metadata:
languageName: node
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":
version: 0.5.7
resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.7"
@@ -4949,41 +4997,41 @@ __metadata:
version: 0.0.0-use.local
resolution: "@standardnotes/authenticator@workspace:packages/components/src/packages/org.standardnotes.token-vault"
dependencies:
"@babel/core": ^7.13.10
"@babel/eslint-parser": ^7.13.10
"@babel/plugin-proposal-class-properties": ^7.13.0
"@babel/plugin-transform-runtime": ^7.13.10
"@babel/preset-env": ^7.13.10
"@babel/preset-react": ^7.12.13
"@standardnotes/editor-kit": 2.2.1
"@standardnotes/eslint-config-extensions": ^1.0.1
"@svgr/webpack": ^6.1.2
babel-loader: ^8.2.2
css-loader: ^5.1.3
eslint: ^7.21.0
eslint-plugin-react: ^7.22.0
html-webpack-plugin: ^5.3.1
immutability-helper: ^3.0.1
jsqr: ^1.2.0
mini-css-extract-plugin: ^1.3.9
"@babel/core": ^7.18.5
"@babel/eslint-parser": ^7.18.2
"@babel/plugin-proposal-class-properties": ^7.17.12
"@babel/plugin-transform-runtime": ^7.18.5
"@babel/preset-env": ^7.18.2
"@babel/preset-react": ^7.17.12
"@standardnotes/editor-kit": 2.2.5
"@standardnotes/eslint-config-extensions": ^1.0.4
"@standardnotes/styles": "workspace:*"
"@svgr/webpack": ^6.2.1
babel-loader: ^8.2.5
css-loader: ^6.7.1
eslint: "*"
eslint-plugin-react: ^7.30.0
html-webpack-plugin: ^5.5.0
immutability-helper: ^3.1.1
jsqr: ^1.4.0
mini-css-extract-plugin: ^2.6.1
node-sass: "*"
notp: ^2.0.3
otplib: ^11.0.1
prop-types: ^15.7.2
react: ^17.0.1
otplib: ^12.0.1
prettier: "*"
prop-types: ^15.8.1
react: ^18.2.0
react-beautiful-dnd: ^13.1.0
react-color: ^2.19.3
react-dom: ^17.0.1
regenerator-runtime: ^0.13.2
sass-loader: ^11.0.1
sn-stylekit: 2.1.0
style-loader: ~0.13.1
react-dom: ^18.2.0
sass-loader: ^13.0.0
style-loader: ~3.3.1
svg-url-loader: ^7.1.1
terser-webpack-plugin: ^5.1.1
terser-webpack-plugin: ^5.3.3
webpack: "*"
webpack-cli: "*"
webpack-dev-server: "*"
webpack-merge: ^5.7.3
webpack-merge: ^5.8.0
languageName: unknown
linkType: soft
@@ -5178,13 +5226,6 @@ __metadata:
languageName: unknown
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":
version: 2.2.3
resolution: "@standardnotes/editor-kit@npm:2.2.3"
@@ -5256,7 +5297,7 @@ __metadata:
languageName: node
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
resolution: "@standardnotes/eslint-config-extensions@npm:1.0.4"
peerDependencies:
@@ -6373,7 +6414,7 @@ __metadata:
languageName: node
linkType: hard
"@svgr/webpack@npm:^6.1.2, @svgr/webpack@npm:^6.2.1":
"@svgr/webpack@npm:^6.2.1":
version: 6.2.1
resolution: "@svgr/webpack@npm:6.2.1"
dependencies:
@@ -14789,7 +14830,7 @@ __metadata:
languageName: node
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
resolution: "eslint-plugin-react@npm:7.30.0"
dependencies:
@@ -14953,7 +14994,7 @@ __metadata:
languageName: node
linkType: hard
"eslint@npm:^7.20.0, eslint@npm:^7.21.0":
"eslint@npm:^7.20.0":
version: 7.32.0
resolution: "eslint@npm:7.32.0"
dependencies:
@@ -17851,7 +17892,7 @@ __metadata:
languageName: node
linkType: hard
"immutability-helper@npm:^3.0.1":
"immutability-helper@npm:^3.1.1":
version: 3.1.1
resolution: "immutability-helper@npm:3.1.1"
checksum: 6fdbf6d2123efa567263e904bbaff07aca0e24560d270d34967b03aab8ec20bd3e4057f394d59e50eb6c4718c9415591a6281692bb0aafd522ad72cf4887133f
@@ -20063,7 +20104,7 @@ __metadata:
languageName: node
linkType: hard
"jsqr@npm:^1.2.0":
"jsqr@npm:^1.4.0":
version: 1.4.0
resolution: "jsqr@npm:1.4.0"
checksum: 7c572971f90c42772e30d152bde63b84edf1164bde80c53942e6b2068ea31caf00ad704aa46cacc9e71645f52dbeddebc6e84ba15e883c678ee93cde690de339
@@ -20530,7 +20571,7 @@ __metadata:
languageName: node
linkType: hard
"loader-utils@npm:^1.0.2, loader-utils@npm:^1.1.0":
"loader-utils@npm:^1.1.0":
version: 1.4.0
resolution: "loader-utils@npm:1.4.0"
dependencies:
@@ -22349,7 +22390,7 @@ __metadata:
languageName: node
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
resolution: "mini-css-extract-plugin@npm:2.6.1"
dependencies:
@@ -23699,12 +23740,14 @@ __metadata:
languageName: node
linkType: hard
"otplib@npm:^11.0.1":
version: 11.0.1
resolution: "otplib@npm:11.0.1"
"otplib@npm:^12.0.1":
version: 12.0.1
resolution: "otplib@npm:12.0.1"
dependencies:
thirty-two: 1.0.2
checksum: 42225f1ccc4562fc062dfd0cbe4b0c527f56648775601175b638e54850c44a1dbe1770e5858a2e50216e5111bd4dd2776df3372a92f74a2fb41e7f3975dc0bbd
"@otplib/core": ^12.0.1
"@otplib/preset-default": ^12.0.1
"@otplib/preset-v11": ^12.0.1
checksum: 4a1b91cf1b8e920b50ad4bac2ef2a89126630c62daf68e9b32ff15106b2551db905d3b979955cf5f8f114da0a8883cec3d636901d65e793c1745bb4174e2a572
languageName: node
linkType: hard
@@ -26285,7 +26328,7 @@ __metadata:
languageName: node
linkType: hard
"react-dom@npm:^18.1.0":
"react-dom@npm:^18.1.0, react-dom@npm:^18.2.0":
version: 18.2.0
resolution: "react-dom@npm:18.2.0"
dependencies:
@@ -27075,7 +27118,7 @@ __metadata:
languageName: node
linkType: hard
"react@npm:^18.1.0":
"react@npm:^18.1.0, react@npm:^18.2.0":
version: 18.2.0
resolution: "react@npm:18.2.0"
dependencies:
@@ -28232,6 +28275,31 @@ __metadata:
languageName: node
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":
version: 9.0.3
resolution: "sass-loader@npm:9.0.3"
@@ -29798,15 +29866,6 @@ __metadata:
languageName: node
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":
version: 1.2.1
resolution: "style-loader@npm:1.2.1"
@@ -30276,7 +30335,7 @@ __metadata:
languageName: node
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
resolution: "terser-webpack-plugin@npm:5.3.3"
dependencies:
@@ -30337,7 +30396,7 @@ __metadata:
languageName: node
linkType: hard
"thirty-two@npm:1.0.2":
"thirty-two@npm:^1.0.2":
version: 1.0.2
resolution: "thirty-two@npm:1.0.2"
checksum: f6700b31d16ef942fdc0d14daed8a2f69ea8b60b0e85db8b83adf58d84bbeafe95a17d343ab55efaae571bb5148b62fc0ee12b04781323bf7af7d7e9693eec76