Skip to content

Commit

Permalink
Merge pull request #877 from ChainSafe/feat/tbaut-settings-security-846
Browse files Browse the repository at this point in the history
Security settings
  • Loading branch information
FSM1 authored Apr 8, 2021
2 parents 660c07e + 06b5a6f commit 336a1c3
Show file tree
Hide file tree
Showing 17 changed files with 1,292 additions and 668 deletions.
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
128 changes: 128 additions & 0 deletions packages/files-ui/src/Components/Elements/PasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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"
import clsx from "clsx"

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={clsx(classes.button, "passwordFormButton")}
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

0 comments on commit 336a1c3

Please sign in to comment.