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
174 changes: 174 additions & 0 deletions packages/files-ui/src/Components/Elements/MnemonicForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
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, hasMnemonicShare } = useThresholdKey()
const [mnemonic, setMnemonic] = useState("")
const [copied, setCopied] = useState(false)
const debouncedSwitchCopied = debounce(() => setCopied(false), 3000)

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
25 changes: 3 additions & 22 deletions packages/files-ui/src/Components/Modules/LoginModule/Complete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import clsx from "clsx"
import { CSFTheme } from "../../../Themes/types"
import CompleteSVG from "../../../Media/svgs/complete.svg"
import { useThresholdKey } from "../../../Contexts/ThresholdKeyContext"
import { SECURITY_QUESTIONS_MODULE_NAME } from "@tkey/security-questions"

const useStyles = makeStyles(({ breakpoints, constants, palette, zIndex }: CSFTheme) =>
createStyles({
Expand Down Expand Up @@ -81,28 +80,10 @@ interface IComplete {

const Complete = ({ className }: IComplete) => {
const classes = useStyles()
const { keyDetails, userInfo, resetShouldInitialize } = useThresholdKey()
const { userInfo, resetShouldInitialize, hasPasswordShare, hasMnemonicShare, browserShares } = useThresholdKey()

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

const browserShare =
shares.filter((s) => s.module === "webStorage")

// `shares` object above only contains security question and local device shares
// The service provider share as well as backup mnemonic do not appear in this share
// array. Note: Files accounts have one service provider by default.
// If an account has totalShares - shares.length === 1 this indicates that a
// mnemonic has not been set up for the account. If totalShares - shares.length === 2
// this indicates that a mnemonic has already been set up. "2" corresponds here to one
// service provider (default), and one mnemonic.
const hasMnemonicShare = keyDetails && (keyDetails.totalShares - shares.length > 1)
const hasPasswordShare = shares.filter((s) => s.module === SECURITY_QUESTIONS_MODULE_NAME).length > 0

return (
<div className={clsx(className, classes.root)}>
<img className={classes.background} src={CompleteSVG} alt="complete slide background" />
Expand Down Expand Up @@ -137,7 +118,7 @@ const Complete = ({ className }: IComplete) => {
</div>
<div className={clsx(
classes.option, {
"active": browserShare.length > 0
"active": browserShares.length > 0
}
)}>
<Typography>
Expand All @@ -146,7 +127,7 @@ const Complete = ({ className }: IComplete) => {
</Trans>
</Typography>
{
browserShare.length > 0 ? (
browserShares.length > 0 ? (
<CheckSvg />
) : (
<CloseSvg />
Expand Down
Loading