Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security settings #877

Merged
merged 20 commits into from
Apr 8, 2021
Merged
8 changes: 4 additions & 4 deletions packages/files-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
"@lingui/core": "^3.7.2",
"@lingui/react": "^3.7.2",
"@sentry/react": "^5.28.0",
"@tkey/default": "3.10.0",
"@tkey/security-questions": "3.10.0",
"@tkey/web-storage": "3.10.0",
"@toruslabs/torus-direct-web-sdk": "4.5.0",
"@tkey/default": "^3.12.0",
"@tkey/security-questions": "^3.12.0",
"@tkey/web-storage": "^3.12.0",
"@toruslabs/torus-direct-web-sdk": "^4.10.0",
"babel-loader": "8.1.0",
"babel-plugin-macros": "^2.8.0",
"babel-preset-env": "^1.7.0",
Expand Down
182 changes: 182 additions & 0 deletions packages/files-ui/src/Components/Elements/MnemonicForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import React, { useCallback } from "react"
import { Button, CopySvg, Loading, Typography } from "@chainsafe/common-components"
import { useState } from "react"
import { createStyles, debounce, makeStyles } from "@chainsafe/common-theme"
import { CSFTheme } from "../../Themes/types"
import { t, Trans } from "@lingui/macro"
import { useThresholdKey } from "../../Contexts/ThresholdKeyContext"
import clsx from "clsx"

const useStyles = makeStyles(({ animation, constants, palette, zIndex }: CSFTheme) =>
createStyles({
phraseSpace: {
cursor: "pointer",
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
minHeight: 123,
borderRadius: 10,
backgroundColor: constants.loginModule.itemBackground,
color: constants.loginModule.textColor,
padding: `${constants.generalUnit}px ${constants.generalUnit * 3}px`,
marginTop: constants.generalUnit * 3,
marginBottom: constants.generalUnit * 4
},
cta: {
textDecoration: "underline"
},
copyArea: {
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
"& > p": {
maxWidth: `calc(100% - (35px + ${constants.generalUnit * 3}px))`
}
},
copiedFlag: {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
left: "50%",
top: 0,
position: "absolute",
transform: "translate(-50%, -50%)",
zIndex: zIndex?.layer1,
transitionDuration: `${animation.transform}ms`,
opacity: 0,
visibility: "hidden",
backgroundColor: constants.loginModule.flagBg,
color: constants.loginModule.flagText,
padding: `${constants.generalUnit / 2}px ${constants.generalUnit}px`,
borderRadius: 2,
"&:after": {
transitionDuration: `${animation.transform}ms`,
content: "''",
position: "absolute",
top: "100%",
left: "50%",
transform: "translate(-50%,0)",
width: 0,
height: 0,
borderLeft: "5px solid transparent",
borderRight: "5px solid transparent",
borderTop: `5px solid ${constants.loginModule.flagBg}`
},
"&.active": {
opacity: 1,
visibility: "visible"
}
},
copyIcon: {
transitionDuration: `${animation.transform}ms`,
fill: constants.loginModule.iconColor,
height: 35,
width: 35,
marginLeft: constants.generalUnit * 3,
"&.active": {
fill: palette.success.main
}
},
loader: {
display: "flex",
alignItems: "center",
"& svg": {
marginRight: constants.generalUnit
}
}
})
)

interface Props {
buttonLabel?: string
onComplete: () => void
}

const MnemonicForm = ({ buttonLabel, onComplete }: Props) => {
const classes = useStyles()
const displayButtonLabel = buttonLabel || t`Continue`
const [isLoading, setIsLoading] = useState(false)
const { addMnemonicShare, keyDetails } = useThresholdKey()
const [mnemonic, setMnemonic] = useState("")
const [copied, setCopied] = useState(false)
const debouncedSwitchCopied = debounce(() => setCopied(false), 3000)

const shares = keyDetails
? Object.values(keyDetails.shareDescriptions).map((share) => {
return JSON.parse(share[0])
})
: []

const hasMnemonicShare = keyDetails && (keyDetails.totalShares - shares.length > 1)

const onSectionClick = useCallback(async () => {
if (mnemonic.length === 0) {
if (!hasMnemonicShare) {
setIsLoading(true)
const newMnemonic = await addMnemonicShare()
setMnemonic(newMnemonic)
setIsLoading(false)
}
} else {
try {
await navigator.clipboard.writeText(mnemonic)
setCopied(true)
debouncedSwitchCopied()
} catch (err) {
console.error(err)
}
}
}, [mnemonic, hasMnemonicShare, setIsLoading, addMnemonicShare, debouncedSwitchCopied])

return (
<>
<section className={clsx(classes.phraseSpace, "phraseSection")} onClick={onSectionClick}>
{ isLoading
? (
<Typography component="p" className={classes.loader}>
<Loading type="inherit" size={16} />
<Trans>
Generating...
</Trans>
</Typography>
)
: (
mnemonic.length === 0
? (
<Typography className={classes.cta} component="p">
<Trans>
Generate phrase
</Trans>
</Typography>
)
: (
<div className={classes.copyArea}>
<div className={clsx(classes.copiedFlag, { "active": copied })}>
<span>
<Trans>
Copied!
</Trans>
</span>
</div>
<Typography component="p">
{mnemonic}
</Typography>
<CopySvg className={clsx(classes.copyIcon, { "active": copied })} />
</div>
)
)}
</section>
{!!mnemonic.length && (
<Button onClick={onComplete}>
{displayButtonLabel}
</Button>
)}
</>
)
}

export default MnemonicForm
127 changes: 127 additions & 0 deletions packages/files-ui/src/Components/Elements/PasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, { useCallback, useMemo } from "react"
import { Button, FormikTextInput } from "@chainsafe/common-components"
import { Form, Formik } from "formik"
import { useState } from "react"
import * as yup from "yup"
import { createStyles, makeStyles } from "@chainsafe/common-theme"
import { CSFTheme } from "../../Themes/types"
import zxcvbn from "zxcvbn"
import { t } from "@lingui/macro"
import StrengthIndicator from "../Modules/MasterKeySequence/SequenceSlides/StrengthIndicator"

const useStyles = makeStyles(({ breakpoints, constants }: CSFTheme) =>
createStyles({
input: {
margin: 0,
width: "100%",
marginBottom: constants.generalUnit * 1.5
},
inputLabel: {
fontSize: "16px",
lineHeight: "24px",
marginBottom: constants.generalUnit
},
button: {
[breakpoints.up("md")]: {
marginTop: constants.generalUnit * 10
},
[breakpoints.down("md")]: {
marginTop: constants.generalUnit
}
}
})
)

interface Props {
buttonLabel?: string
setPassword: (password: string) => Promise<void>
}

const PasswordForm = ({ buttonLabel, setPassword }: Props) => {
const [loading, setLoading] = useState(false)
const classes = useStyles()
const displayLabel = buttonLabel || t`Set Password`
const passwordValidation = useMemo(() => yup.object().shape({
password: yup
.string()
.test(
"Complexity",
t`Password needs to be more complex`,
async (val: string | null | undefined | object) => {
if (val === undefined) {
return false
}

const complexity = zxcvbn(`${val}`)
if (complexity.score >= 2) {
return true
}
return false
}
)
.required(t`Please provide a password`),
confirmPassword: yup
.string()
.oneOf(
[yup.ref("password"), undefined],
t`Passwords must match`
)
.required(t`Password confirmation is required`)
})
, [])

const onSubmit = useCallback((values, helpers) => {
helpers.setSubmitting(true)
setLoading(true)
setPassword(values.password)
.then(() => {
setLoading(false)
helpers.setSubmitting(false)
})
.catch ((e) => {
setLoading(false)
helpers.setSubmitting(false)
console.error(e)
})
}, [setPassword])

return (
<Formik
initialValues={{
password: "",
confirmPassword: ""
}}
validationSchema={passwordValidation}
onSubmit={onSubmit}
>
<Form>
<FormikTextInput
type="password"
className={classes.input}
name="password"
label={t`Password:`}
labelClassName={classes.inputLabel}
captionMessage={<StrengthIndicator fieldName="password" />}
/>
<FormikTextInput
type="password"
className={classes.input}
name="confirmPassword"
label={t`Confirm Password:`}
labelClassName={classes.inputLabel}
/>
<Button
className={classes.button}
fullsize
type="submit"
loading={loading}
disabled={loading}
>
{displayLabel}
</Button>
</Form>
</Formik>
)
}

export default PasswordForm
2 changes: 1 addition & 1 deletion packages/files-ui/src/Components/FilesRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const ROUTE_LINKS = {
PurchasePlan: "/purchase"
}

export const SETTINGS_PATHS = ["profile", "plan"] as const
export const SETTINGS_PATHS = ["profile", "plan", "security"] as const
export type SettingsPath = typeof SETTINGS_PATHS[number]

const FilesRoutes = () => {
Expand Down
Loading