Merge pull request #798 from filecoin-project/@aminejv/auth

Auth: link existing accounts with twitter
This commit is contained in:
martinalong 2021-06-23 15:24:09 -07:00 committed by GitHub
commit d609b99cf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 791 additions and 200 deletions

View File

@ -244,6 +244,20 @@ export const createUserViaTwitterWithVerification = async (data) => {
});
};
export const linkTwitterAccount = async (data) => {
return await returnJSON(`/api/twitter/link`, {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const linkTwitterAccountWithVerification = async (data) => {
return await returnJSON(`/api/twitter/link-with-verification`, {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data }),
});
};
export const updateViewer = async (data) => {
await Websockets.checkWebsocket();
return await returnJSON(`/api/users/update`, {

View File

@ -61,7 +61,7 @@ export const system = {
active: "#00BB00",
blurBlack: "#262626",
bgBlurGray: "#403F42",
bgBlurWhiteTRN: "rgba(255,255,255,0.3)",
bgBlurWhiteTRN: "rgba(255,255,255,0.7)",
grayLight2: "#AEAEB2",
grayLight5: "#E5E5EA",
};

View File

@ -27,6 +27,8 @@ export const useForm = ({
onSubmit,
validate,
initialValues,
// NOTE(amine): you can format the value of each input before onChange. ex format:{username: formatUsername}
format = {},
validateOnBlur = true,
validateOnSubmit = true,
}) => {
@ -39,16 +41,25 @@ export const useForm = ({
});
const _hasError = (obj) => Object.keys(obj).some((name) => obj[name]);
const _mergeEventHandlers = (events = []) => (e) =>
events.forEach((event) => {
if (event) event(e);
});
const formatInputValue = (e) => {
if (typeof format !== "object" || !(e.target.name in format)) return e.target.value;
const formatInput = format[e.target.name];
return formatInput ? formatInput(e.target.value) : e.target.value;
};
const _mergeEventHandlers =
(events = []) =>
(e) =>
events.forEach((event) => {
if (event) event(e);
});
/** ---------- NOTE(amine): Input Handlers ---------- */
const handleFieldChange = (e) =>
setState((prev) => ({
...prev,
values: { ...prev.values, [e.target.name]: e.target.value },
values: { ...prev.values, [e.target.name]: formatInputValue(e) },
errors: { ...prev.errors, [e.target.name]: undefined },
touched: { ...prev.touched, [e.target.name]: false },
}));
@ -162,10 +173,12 @@ export const useField = ({
touched: undefined,
});
const _mergeEventHandlers = (events = []) => (e) =>
events.forEach((event) => {
if (event) event(e);
});
const _mergeEventHandlers =
(events = []) =>
(e) =>
events.forEach((event) => {
if (event) event(e);
});
/** ---------- NOTE(amine): Input Handlers ---------- */
const handleFieldChange = (e) =>
@ -176,14 +189,14 @@ export const useField = ({
touched: false,
}));
const handleOnBlur = (e) => {
const handleOnBlur = () => {
// NOTE(amine): validate the inputs onBlur and touch the current input
let error = {};
if (validateOnBlur && validate) error = validate(state.value);
setState((prev) => ({ ...prev, touched: validateOnBlur, error }));
};
const handleFormOnSubmit = (e) => {
const handleFormOnSubmit = () => {
//NOTE(amine): touch all inputs
setState((prev) => ({ ...prev, touched: true }));
@ -219,3 +232,9 @@ export const useField = ({
return { getFieldProps, value: state.value, isSubmitting: state.isSubmitting };
};
export const useToggle = (initialState = false) => {
const [state, setState] = React.useState(initialState);
const toggleState = () => setState((prev) => !prev);
return [state, toggleState];
};

View File

@ -143,7 +143,10 @@ export const error = {
SERVER_TWITTER_OAUTH_NOT_ALLOWED: "You can only authenticate via twitter while on slate.host",
SERVER_TWITTER_LOGIN_ONLY:
"This login is associated with a Twitter account. Please continue with Twitter instead",
SERVER_TWITTER_LINKING_INVALID_USERNAME: "Please choose a valid username/email",
SERVER_TWITTER_LINKING_INVALID_PASSWORD: "Please choose a valid password",
SERVER_TWITTER_LINKING_WRONG_CREDENTIALS: "You have entered an invalid username or password",
SERVER_TWITTER_LINKING_FAILED: "SERVER_CREATE_USER_FAILED",
// Email Verifications
SERVER_EMAIL_VERIFICATION_INVALID_PIN: "Please enter a valid pin",
SERVER_EMAIL_VERIFICATION_FAILED:

View File

@ -5,7 +5,7 @@ import { css } from "@emotion/react";
/* TYPOGRAPHY */
export const HEADING_01 = css`
font-family: ${Constants.font.text};
font-family: ${Constants.font.medium};
font-size: 1.953rem;
font-weight: medium;
line-height: 1.5;
@ -21,25 +21,22 @@ export const HEADING_02 = css`
`;
export const HEADING_03 = css`
font-family: ${Constants.font.text};
font-family: ${Constants.font.medium};
font-size: 1.25rem;
font-weight: medium;
line-height: 1.5;
letter-spacing: -0.02px;
`;
export const HEADING_04 = css`
font-family: ${Constants.font.text};
font-family: ${Constants.font.medium};
font-size: 1rem;
font-weight: medium;
line-height: 1.5;
letter-spacing: -0.01px;
`;
export const HEADING_05 = css`
font-family: ${Constants.font.text};
font-family: ${Constants.font.medium};
font-size: 0.875rem;
font-weight: medium;
line-height: 1.5;
letter-spacing: -0.01px;
`;
@ -146,3 +143,14 @@ export const MOBILE_ONLY = css`
pointer-events: none;
}
`;
export const LINK = (theme) => css`
${HOVERABLE};
${HEADING_05};
&,
&:link,
&:visited {
color: ${theme.system.blue};
text-decoration: none;
}
`;

View File

@ -770,6 +770,7 @@ export default class ApplicationPage extends React.Component {
return (
<React.Fragment>
<ApplicationLayout
withPaddings={page.id !== "NAV_SIGN_IN"}
page={page}
onAction={this._handleAction}
header={headerElement}

View File

@ -1,6 +1,7 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import * as SVG from "~/common/svg";
import * as Utilities from "~/common/utilities";
import { css } from "@emotion/react";
import { GlobalTooltip } from "~/components/system/components/fragments/GlobalTooltip";
@ -118,6 +119,10 @@ export default class ApplicationLayout extends React.Component {
headerTop: 0,
};
static defaultProps = {
withPaddings: true,
};
componentDidMount = () => {
this.prevScrollPos = window.pageYOffset;
if (this.props.isMobile) {
@ -169,7 +174,7 @@ export default class ApplicationLayout extends React.Component {
}
return (
<React.Fragment>
<div css={STYLES_CONTENT}>
<div css={this.props.withPaddings && STYLES_CONTENT}>
<GlobalTooltip />
{this.props.header && (
<div

View File

@ -8,10 +8,12 @@ import * as Strings from "~/common/strings";
import * as Styles from "~/common/styles";
import { css } from "@emotion/react";
import { motion, AnimateSharedLayout } from "framer-motion";
import { useForm, useField } from "~/common/hooks";
import Field from "~/components/core/Field";
import { AnimateSharedLayout } from "framer-motion";
import { useField } from "~/common/hooks";
import { Toggle, SignUpPopover, ArrowButton } from "~/components/core/Auth/components";
import { LoaderSpinner } from "~/components/system/components/Loaders";
import Field from "~/components/core/Field";
const STYLES_INITIAL_CONTAINER = css`
display: flex;
@ -71,18 +73,14 @@ export default function Initial({
const { TOGGLE_OPTIONS, toggleValue, handleToggleChange } = useToggler(page);
// NOTE(amine): Signup view form
const { getFieldProps, getFormProps, isSubmitting: isCheckingEmail } = useForm({
const { getFieldProps: getSignupFielProps, isSubmitting: isCheckingEmail } = useField({
validateOnBlur: false,
initialValues: { email: initialEmail || "" },
validate: ({ email }, errors) => {
if (Strings.isEmpty(email)) {
errors.email = "Please provide an email";
} else if (!Validations.email(email)) {
errors.email = "Invalid email address";
}
return errors;
initialValues: initialEmail || "",
validate: (email) => {
if (Strings.isEmpty(email)) return "Please provide an email";
else if (!Validations.email(email)) return "Invalid email address";
},
onSubmit: async ({ email }) => {
onSubmit: async (email) => {
const response = await Actions.checkEmail({ email });
if (response?.data?.twitter) {
Events.dispatchMessage({
@ -183,30 +181,33 @@ export default function Initial({
</>
) : (
<AnimateSharedLayout>
<form {...getFormProps()}>
<Field
autoFocus
label="Sign up with email"
placeholder="Email"
type="text"
name="email"
full
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
// NOTE(amine): the input component internally is using 16px margin top
containerStyle={{ marginTop: "4px" }}
{...getFieldProps("email")}
/>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} layout>
<System.ButtonPrimary
full
type="submit"
style={{ marginTop: "16px" }}
loading={isCheckingEmail}
>
Send verification link
</System.ButtonPrimary>
</motion.div>
</form>
<Field
autoFocus
label="Sign up with email"
placeholder="Email"
type="text"
name="email"
full
icon={
isCheckingEmail
? () => (
<LoaderSpinner
style={{
height: 16,
width: 16,
marginLeft: 16,
position: "absolute",
right: 12,
}}
/>
)
: ArrowButton
}
// NOTE(amine): the input component internally is using 16px margin top
containerStyle={{ marginTop: "4px" }}
{...getSignupFielProps("email")}
/>
<div style={{ marginTop: "auto" }}>
<a css={STYLES_LINK_ITEM} href="/terms" target="_blank">
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>

View File

@ -123,7 +123,6 @@ export default function ResetPassword({
setPasswordValidations(validations);
},
})}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
onClickIcon={() => toggleShowPassword(!showPassword)}
icon={showPassword ? SVG.EyeOff : SVG.Eye}
/>
@ -161,7 +160,6 @@ export default function ResetPassword({
type="email"
full
{...getFieldProps("email")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<AnimateSharedLayout>
<motion.div layout>

View File

@ -163,7 +163,6 @@ export default function Signin({
type="email"
full
{...getEmailFieldProps("email")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<AnimateSharedLayout>
<motion.div layout>
@ -202,7 +201,6 @@ export default function Signin({
onClickIcon={() => toggleShowPassword(!showPassword)}
icon={showPassword ? SVG.EyeOff : SVG.Eye}
{...getFieldProps("password")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<AnimateSharedLayout>
<motion.div layout>

View File

@ -3,6 +3,7 @@ import * as System from "~/components/system";
import * as Validations from "~/common/validations";
import * as SVG from "~/common/svg";
import * as Actions from "~/common/actions";
import * as Strings from "~/common/strings";
import Field from "~/components/core/Field";
@ -23,12 +24,17 @@ const useSignup = () => {
};
const useCheckUser = () => {
const MESSAGE = "The username is taken.";
const MESSAGE = "That username is taken";
const usernamesAllowed = React.useRef([]);
const usernamesTaken = React.useRef([]);
return async ({ username }, errors) => {
if (!Validations.username(username)) {
errors.username = "Invalid username";
return;
}
if (usernamesAllowed.current.some((value) => value === username)) {
return;
}
@ -42,7 +48,7 @@ const useCheckUser = () => {
username,
});
if (response.data) {
errors.username = "The username is taken.";
errors.username = "That username is taken";
usernamesTaken.current.push(username);
return;
}
@ -56,10 +62,6 @@ const createValidations = (validateUsername) => async (
) => {
await validateUsername({ username }, errors);
if (!Validations.username(username)) errors.username = "Invalid username";
// Note(amine): username should not be an email
if (Validations.email(username)) errors.username = "Username shouldn't be an email";
if (!Validations.password(password)) errors.password = "Incorrect password";
if (!acceptTerms) errors.acceptTerms = "Must accept terms and conditions";
@ -77,6 +79,7 @@ export default function Signup({ verifyEmail, createUser, resendEmailVerificatio
const { getFieldProps, getFormProps, isSubmitting, isValidating } = useForm({
initialValues: { username: "", password: "", acceptTerms: false },
format: { username: Strings.createUsername },
validate: createValidations(validateUsername),
onSubmit: async ({ username, password }) => await createUser({ username, password }),
});
@ -101,7 +104,7 @@ export default function Signup({ verifyEmail, createUser, resendEmailVerificatio
placeholder="Username"
name="username"
type="text"
success="The username is available."
success="That username is available"
icon={
isValidating
? () => (
@ -119,7 +122,6 @@ export default function Signup({ verifyEmail, createUser, resendEmailVerificatio
}
full
{...getFieldProps("username")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<motion.div layout>
@ -136,7 +138,6 @@ export default function Signup({ verifyEmail, createUser, resendEmailVerificatio
setPasswordValidations(validations);
},
})}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
onClickIcon={() => toggleShowPassword(!showPassword)}
icon={showPassword ? SVG.EyeOff : SVG.Eye}
/>

View File

@ -0,0 +1,166 @@
/* eslint-disable jsx-a11y/no-autofocus */
import * as React from "react";
import * as Validations from "~/common/validations";
import * as System from "~/components/system";
import * as Strings from "~/common/strings";
import Field from "~/components/core/Field";
import { AnimateSharedLayout, motion } from "framer-motion";
import { useForm } from "~/common/hooks";
import { SignUpPopover, Verification, AuthCheckBox } from "~/components/core/Auth/components";
const useTwitterLinking = () => {
// NOTE(amine): can be either 'account' | 'email' | 'verificatiom'
const [scene, setScene] = React.useState("account");
const handlers = React.useMemo(
() => ({
goToEmailScene: () => setScene("email"),
goToVerificationScene: () => setScene("verification"),
}),
[]
);
return { ...handlers, scene };
};
const MotionLayout = ({ children, ...props }) => (
<motion.div layout {...props}>
{children}
</motion.div>
);
const handleValidation = async ({ username, password, acceptTerms }, errors) => {
if (!Validations.username(username) && !Validations.email(username))
errors.username = "Invalid username";
if (!Validations.legacyPassword(password)) errors.password = "Incorrect password";
if (!acceptTerms) errors.acceptTerms = "Must accept terms and conditions";
return errors;
};
export default function TwitterLinking({
linkAccount,
linkAccountWithVerification,
resendEmailVerification,
createVerification,
}) {
const { scene, goToVerificationScene, goToEmailScene } = useTwitterLinking();
const {
getFieldProps,
getFormProps,
values: { username },
isSubmitting,
} = useForm({
initialValues: { username: "", password: "", acceptTerms: false },
validate: handleValidation,
onSubmit: async ({ username, password }) => {
const response = await linkAccount({ username, password });
if (response.shouldMigrate) {
goToEmailScene();
}
},
});
const {
getFormProps: getEmailFormProps,
getFieldProps: getEmailFieldProps,
isSubmitting: isEmailFormSubmitting,
} = useForm({
initialValues: { email: "" },
validate: ({ email }, errors) => {
if (Strings.isEmpty(email)) {
errors.email = "Please provide an email";
} else if (!Validations.email(email)) {
errors.email = "Invalid email address";
}
return errors;
},
onSubmit: async ({ email }) => {
const response = await createVerification({ email });
if (!response) return;
goToVerificationScene();
},
});
if (scene === "verification") {
const handleVerification = async ({ pin }) => {
await linkAccountWithVerification({ pin });
};
return <Verification onVerify={handleVerification} onResend={resendEmailVerification} />;
}
if (scene === "email") {
return (
<div>
<SignUpPopover title={`Please add an email address for ${username}`}>
<form {...getEmailFormProps()} style={{ marginTop: 72 }}>
<Field
autoFocus
containerStyle={{ marginTop: 16 }}
placeholder="Email"
name="email"
type="email"
full
{...getEmailFieldProps("email")}
/>
<AnimateSharedLayout>
<motion.div layout>
<System.ButtonPrimary
full
type="submit"
loading={isEmailFormSubmitting}
style={{ marginTop: 16 }}
>
Send verification code
</System.ButtonPrimary>
</motion.div>
</AnimateSharedLayout>
</form>
</SignUpPopover>
</div>
);
}
return (
<SignUpPopover title="Create an account">
<form {...getFormProps()}>
<Field
autoFocus
containerStyle={{ marginTop: 41 }}
placeholder="Username"
name="username"
type="text"
full
{...getFieldProps("username")}
/>
<AnimateSharedLayout>
<Field
containerStyle={{ marginTop: 16 }}
containerAs={MotionLayout}
errorAs={MotionLayout}
placeholder="password"
name="password"
type="password"
full
{...getFieldProps("password")}
/>
<motion.div layout>
<AuthCheckBox style={{ marginTop: 16 }} {...getFieldProps("acceptTerms")} />
<System.ButtonPrimary
full
style={{ marginTop: 36 }}
loading={isSubmitting}
type="submit"
>
Connect to Twitter
</System.ButtonPrimary>
</motion.div>
</AnimateSharedLayout>
</form>
</SignUpPopover>
);
}

View File

@ -1,7 +1,10 @@
/* eslint-disable jsx-a11y/no-autofocus */
import * as React from "react";
import * as System from "~/components/system";
import * as Validations from "~/common/validations";
import * as Actions from "~/common/actions";
import * as Styles from "~/common/styles";
import * as Strings from "~/common/strings";
import Field from "~/components/core/Field";
@ -12,12 +15,14 @@ import { useForm } from "~/common/hooks";
import { SignUpPopover, Verification, AuthCheckBox } from "~/components/core/Auth/components";
const STYLES_SMALL = (theme) => css`
font-size: ${theme.typescale.lvlN1};
const STYLES_LINK = css`
padding: 0;
margin: 0;
max-width: 224px;
text-align: center;
color: ${theme.system.textGrayDark};
max-width: 228px;
margin: 0 auto;
background-color: unset;
border: none;
`;
const useTwitterSignup = () => {
@ -27,12 +32,17 @@ const useTwitterSignup = () => {
};
const useCheckUser = () => {
const MESSAGE = "The username is taken.";
const MESSAGE = "That username is taken";
const usernamesAllowed = React.useRef([]);
const usernamesTaken = React.useRef([]);
return async ({ username }, errors) => {
if (!Validations.username(username)) {
errors.username = "Invalid username";
return;
}
if (usernamesAllowed.current.some((value) => value === username)) {
return;
}
@ -46,7 +56,7 @@ const useCheckUser = () => {
username,
});
if (response.data) {
errors.username = "The username is taken.";
errors.username = "That username is taken";
usernamesTaken.current.push(username);
return;
}
@ -60,10 +70,6 @@ const createValidations = (validateUsername) => async (
) => {
await validateUsername({ username }, errors);
if (!Validations.username(username)) errors.username = "Invalid username";
// Note(amine): username should not be an email
if (Validations.email(username)) errors.username = "Username shouldn't be an email";
if (!Validations.email(email)) errors.email = "Invalid email";
if (!acceptTerms) errors.acceptTerms = "Must accept terms and conditions";
@ -80,6 +86,8 @@ const MotionLayout = ({ children, ...props }) => (
export default function TwitterSignup({
initialEmail,
onSignup,
goToTwitterLinkingScene,
resendEmailVerification,
createVerification,
onSignupWithVerification,
}) {
@ -90,11 +98,12 @@ export default function TwitterSignup({
const {
getFieldProps,
getFormProps,
values: { email, username },
values: { username },
isSubmitting,
isValidating,
} = useForm({
initialValues: { username: "", email: initialEmail, acceptTerms: false },
format: { username: Strings.createUsername },
validate: createValidations(validateUsername),
onSubmit: async ({ username, email }) => {
if (email !== initialEmail) {
@ -111,7 +120,7 @@ export default function TwitterSignup({
const handleVerification = async ({ pin }) => {
await onSignupWithVerification({ username, pin });
};
return <Verification onVerify={handleVerification} />;
return <Verification onVerify={handleVerification} onResend={resendEmailVerification} />;
}
return (
@ -123,7 +132,7 @@ export default function TwitterSignup({
placeholder="Username"
name="username"
type="text"
success="The username is available."
success="That username is available"
icon={
isValidating
? () => (
@ -140,7 +149,7 @@ export default function TwitterSignup({
: null
}
{...getFieldProps("username")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
full
/>
<AnimateSharedLayout>
<Field
@ -150,8 +159,8 @@ export default function TwitterSignup({
placeholder="Email"
name="email"
type="email"
full
{...getFieldProps("email")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
/>
<motion.div layout>
@ -165,13 +174,18 @@ export default function TwitterSignup({
Create account
</System.ButtonPrimary>
</motion.div>
{(!initialEmail || initialEmail !== email) && (
<motion.div layout>
<System.P css={STYLES_SMALL} style={{ marginTop: 16 }}>
You will receive a code to verify your email at this address
</System.P>
</motion.div>
)}
<motion.div layout>
<div style={{ textAlign: "center", marginTop: 24 }}>
<button
type="button"
onClick={goToTwitterLinkingScene}
css={[Styles.LINK, STYLES_LINK]}
>
Already have an account? Connect your account to Twitter.
</button>
</div>
</motion.div>
</AnimateSharedLayout>
</form>
</SignUpPopover>

View File

@ -1,5 +1,7 @@
import * as React from "react";
import * as System from "~/components/system";
import * as Constants from "~/common/constants";
import { css } from "@emotion/react";
const STYLES_CHECKBOX_LABEL = (theme) => css`
@ -28,20 +30,12 @@ const STYLES_CHECKBOX_ERROR = (theme) => css`
width: 16px;
`;
const STYLES_CHECKBOX_SUCCESS = (theme) => css`
background-color: rgba(242, 242, 247, 0.5);
border: 1px solid ${theme.system.green};
height: 16px;
width: 16px;
`;
const STYLES_CHECKBOX_WRAPPER = css`
align-items: center;
`;
export default function AuthCheckBox({ touched, error, ...props }) {
const showError = touched && error;
const showSuccess = touched && !error;
const STYLES_CHECKBOX = React.useMemo(() => {
if (showError) return STYLES_CHECKBOX_ERROR;
@ -53,6 +47,14 @@ export default function AuthCheckBox({ touched, error, ...props }) {
containerStyles={STYLES_CHECKBOX_WRAPPER}
labelStyles={STYLES_CHECKBOX_LABEL}
inputStyles={STYLES_CHECKBOX}
boxStyle={
props.value
? {
backgroundColor: Constants.system.brand,
boxShadow: `0 0 0 1px ${Constants.system.brand}`,
}
: { backgroundColor: Constants.system.bgBlurWhiteTRN }
}
{...props}
>
I agree to the Slate{" "}

View File

@ -113,7 +113,6 @@ export default function Verification({ onVerify, title = DEFAULT_TITLE, onResend
}
textStyle={{ width: "100% !important" }}
containerStyle={{ marginTop: "28px" }}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
name="pin"
type="pin"
{...getFieldProps()}

View File

@ -2,4 +2,5 @@ export { default as Signin } from "~/components/core/Auth/Signin";
export { default as Signup } from "~/components/core/Auth/Signup";
export { default as Initial } from "~/components/core/Auth/Initial";
export { default as TwitterSignup } from "~/components/core/Auth/TwitterSignup";
export { default as TwitterLinking } from "~/components/core/Auth/TwitterLinking";
export { default as ResetPassword } from "~/components/core/Auth/ResetPassword";

View File

@ -39,7 +39,8 @@ const STYLES_HEADER = css`
color: ${Constants.system.black};
font-size: ${Constants.typescale.lvl1};
font-family: ${Constants.font.semiBold};
word-break: break-all;
word-break: break-word;
white-space: pre-line;
`;
const STYLES_SUB_HEADER = css`

View File

@ -44,20 +44,25 @@ const STYLES_INPUT = (theme) => css`
background-color: rgba(242, 242, 247, 0.7);
box-shadow: ${theme.shadow.large};
border-radius: 8px;
box-shadow: 0 0 0 1px ${theme.system.white};
&::placeholder {
color: ${theme.system.textGrayDark};
}
`;
const STYLES_INPUT_ERROR = (theme) => css`
border: 1px solid ${theme.system.red};
`;
const STYLES_INPUT_SUCCESS = (theme) => css`
border: 1px solid ${theme.system.green};
box-shadow: 0 0 0 1px ${theme.system.red};
`;
const PasswordValidations = ({ validations }) => {
const STYLES_INPUT_SUCCESS = (theme) => css`
box-shadow: 0 0 0 1px ${theme.system.green};
`;
const PasswordValidations = ({ validations, full, color }) => {
return (
<div css={STYLES_PASSWORD_VALIDATIONS}>
<div
css={STYLES_PASSWORD_VALIDATIONS}
style={{ backgroundColor: color === "white" && "white", maxWidth: !full && "480px" }}
>
<P css={STYLES_SMALL_TEXT}>Passwords should</P>
<div css={STYLES_PASSWORD_VALIDATION}>
<div css={[STYLES_CIRCLE, validations.validLength && STYLES_CIRCLE_SUCCESS]} />
@ -91,6 +96,8 @@ export default function Field({
validations,
errorAs,
containerAs,
full,
color = "transparent",
...props
}) {
const showError = touched && error;
@ -106,10 +113,14 @@ export default function Field({
return (
<div>
<ContainerComponent>
<Input inputCss={[STYLES_INPUT, STYLES_STATUS]} {...props} />
<Input
inputCss={[color === "transparent" && STYLES_INPUT, STYLES_STATUS]}
full={full}
{...props}
/>
</ContainerComponent>
{props.name === "password" && validations && (
<PasswordValidations validations={validations} />
<PasswordValidations color={color} full={full} validations={validations} />
)}
{props.name !== "password" && (showError || showSuccess) && (
<ErrorWrapper>

View File

@ -0,0 +1,126 @@
import * as Environment from "~/node_common/environment";
import * as Data from "~/node_common/data";
import * as Utilities from "~/node_common/utilities";
import * as Strings from "~/common/strings";
import * as Validations from "~/common/validations";
import * as Constants from "~/common/constants";
import * as Logging from "~/common/logging";
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res.status(403).send({ decorator: "SERVER_TWITTER_OAUTH_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(req.body?.data?.username)) {
return res
.status(500)
.send({ decorator: "SERVER_TWITTER_LINKING_INVALID_USERNAME", error: true });
}
if (!Validations.legacyPassword(req.body.data.password)) {
return res
.status(500)
.send({ decorator: "SERVER_TWITTER_LINKING_INVALID_PASSWORD", error: true });
}
if (Strings.isEmpty(req.body.data.token)) {
return res.status(500).send({ decorator: "SERVER_TWITTER_OAUTH_NO_OAUTH_TOKEN", error: true });
}
if (!Validations.verificationPin(req.body.data.pin)) {
return res
.status(500)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_PIN", error: true });
}
const { token, password, username, pin } = req.body.data;
let user;
if (Validations.email(username)) {
try {
user = await Data.getUserByEmail({ email: username });
} catch (e) {
Logging.error(e);
}
} else {
try {
user = await Data.getUserByUsername({
username: req.body.data.username.toLowerCase(),
});
} catch (e) {
Logging.error(e);
}
}
if (!user || user.error) {
return res
.status(!user ? 404 : 500)
.send({ decorator: "SERVER_SIGN_IN_USER_NOT_FOUND", error: true });
}
// Note(amine): Twitter users won't have a password,
// we should think in the future how to handle this use case
if ((!user.salt || !user.password) && user.twitterId) {
return res.status(403).send({ decorator: "SERVER_TWITTER_ALREADY_LINKED", error: true });
}
const hash = await Utilities.encryptPassword(password, user.salt);
if (hash !== user.password) {
return res.status(403).send({ decorator: "SERVER_TWITTER_WRONG_CREDENTIALS", error: true });
}
const verification = await Data.getVerificationBySid({
sid: token,
});
if (!verification) {
return res.status(404).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.error) {
return res.status(404).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
const isTokenExpired =
new Date() - new Date(verification.createdAt) > Constants.TOKEN_EXPIRATION_TIME;
if (isTokenExpired) {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.type !== "email_twitter_verification") {
return res.status(401).send({ decorator: "SERVER_EMAIL_VERIFICATION_FAILED", error: true });
}
if (verification.pin !== pin) {
return res
.status(401)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_PIN", error: true });
}
const twitterUser = await Data.getTwitterToken({ token: verification.twitterToken });
if (!twitterUser) {
return res.status(401).send({ decorator: "SERVER_TWITTER_LINKING_FAILED", error: true });
}
if (!twitterUser) {
return res.status(401).send({ decorator: "SERVER_TWITTER_LINKING_FAILED", error: true });
}
const updates = await Data.updateUserById({
id: user.id,
lastActive: new Date(),
email: verification.email,
twitterId: twitterUser.id_str,
data: {
twitter: {
username: twitterUser.screen_name,
verified: twitterUser.verified,
},
},
});
if (updates.error) {
return res.status(401).send({ decorator: "SERVER_TWITTER_LINKING_FAILED", error: true });
}
return res.status(200).send({ decorator: "SERVER_TWITTER_LINKING" });
};

92
pages/api/twitter/link.js Normal file
View File

@ -0,0 +1,92 @@
import * as Environment from "~/node_common/environment";
import * as Data from "~/node_common/data";
import * as Utilities from "~/node_common/utilities";
import * as Strings from "~/common/strings";
import * as Validations from "~/common/validations";
import * as Logging from "~/common/logging";
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res.status(403).send({ decorator: "SERVER_TWITTER_OAUTH_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(req.body.data.token)) {
return res.status(500).send({ decorator: "SERVER_TWITTER_OAUTH_NO_OAUTH_TOKEN", error: true });
}
if (Strings.isEmpty(req.body?.data?.username)) {
return res
.status(400)
.send({ decorator: "SERVER_TWITTER_LINKING_INVALID_USERNAME", error: true });
}
if (!Validations.legacyPassword(req.body?.data?.password)) {
return res
.status(400)
.send({ decorator: "SERVER_TWITTER_LINKING_INVALID_PASSWORD", error: true });
}
const { username, password, token } = req.body.data;
let user;
if (Validations.email(username)) {
try {
user = await Data.getUserByEmail({ email: username });
} catch (e) {
Logging.error(e);
}
} else {
try {
user = await Data.getUserByUsername({
username: req.body.data.username.toLowerCase(),
});
} catch (e) {
Logging.error(e);
}
}
if (!user || user.error) {
return res
.status(!user ? 404 : 500)
.send({ decorator: "SERVER_SIGN_IN_USER_NOT_FOUND", error: true });
}
// Note(amine): Twitter users won't have a password,
// we should think in the future how to handle this use case
if ((!user.salt || !user.password) && user.twitterId) {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_TWITTER_EXISTS", error: true });
}
const hash = await Utilities.encryptPassword(password, user.salt);
if (hash !== user.password) {
return res
.status(403)
.send({ decorator: "SERVER_TWITTER_LINKING_WRONG_CREDENTIALS", error: true });
}
if (!user.email) {
return res.status(200).send({ shouldMigrate: true });
}
const twitterUser = await Data.getTwitterToken({ token: token });
if (!twitterUser) {
return res.status(401).send({ decorator: "SERVER_TWITTER_LINKING_FAILED", error: true });
}
const updates = await Data.updateUserById({
id: user.id,
lastActive: new Date(),
twitterId: twitterUser.id_str,
data: {
twitter: {
username: twitterUser.screen_name,
verified: twitterUser.verified,
},
},
});
if (updates.error) {
return res.status(401).send({ decorator: "SERVER_TWITTER_LINKING_FAILED", error: true });
}
return res.status(200).send({ decorator: "SERVER_TWITTER_LINKING" });
};

View File

@ -31,6 +31,8 @@ export default async (req, res) => {
return res.status(500).send({ decorator: "SERVER_CREATE_USER_INVALID_USERNAME", error: true });
}
const formattedUsername = Strings.createUsername(username);
const verification = await Data.getVerificationBySid({
sid: req.body.data.token,
});
@ -70,7 +72,6 @@ export default async (req, res) => {
return res.status(201).send({ decorator: "SERVER_CREATE_USER_TWITTER_EXISTS" });
}
const newUsername = username.toLowerCase();
const newEmail = verification.email.toLowerCase();
// NOTE(Amine): If there is an account with the user's twitter email
@ -78,7 +79,7 @@ export default async (req, res) => {
if (userByEmail) return res.status(201).send({ decorator: "SERVER_CREATE_USER_EMAIL_TAKEN" });
// NOTE(Amine): If there is an account with the provided username
const userByUsername = await Data.getUserByUsername({ username });
const userByUsername = await Data.getUserByUsername({ username: formattedUsername });
if (userByUsername) {
return res.status(201).send({ decorator: "SERVER_CREATE_USER_USERNAME_TAKEN" });
}
@ -92,7 +93,7 @@ export default async (req, res) => {
// Don't do this once you refactor.
const { buckets, bucketKey, bucketName } = await Utilities.getBucketAPIFromUserToken({
user: {
username: newUsername,
username: formattedUsername,
data: { tokens: { api } },
},
});
@ -110,7 +111,7 @@ export default async (req, res) => {
});
const user = await Data.createUser({
username: newUsername,
username: formattedUsername,
email: newEmail,
twitterId: twitterUser.id_str,
data: {

View File

@ -30,6 +30,8 @@ export default async (req, res) => {
return res.status(500).send({ decorator: "SERVER_CREATE_USER_INVALID_USERNAME", error: true });
}
const formattedUsername = Strings.createUsername(username);
const storedAuthToken = req.cookies[COOKIE_NAME];
// NOTE(amine): additional security check
@ -59,7 +61,7 @@ export default async (req, res) => {
}
// NOTE(Amine): If there is an account with the provided username
const userByUsername = await Data.getUserByUsername({ username });
const userByUsername = await Data.getUserByUsername({ username: formattedUsername });
if (userByUsername) {
return res.status(201).send({ decorator: "SERVER_CREATE_USER_USERNAME_TAKEN" });
}
@ -69,12 +71,11 @@ export default async (req, res) => {
const identity = await PrivateKey.fromRandom();
const api = identity.toString();
const newUsername = username.toLowerCase();
const newEmail = email.toLowerCase();
const { buckets, bucketKey, bucketName } = await Utilities.getBucketAPIFromUserToken({
user: {
username: newUsername,
username: formattedUsername,
data: { tokens: { api } },
},
});
@ -92,7 +93,7 @@ export default async (req, res) => {
});
const user = await Data.createUser({
username: newUsername,
username: formattedUsername,
email: newEmail,
twitterId: twitterUser.id_str,
data: {

View File

@ -29,6 +29,8 @@ export default async (req, res) => {
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_TOKEN", error: true });
}
const formattedUsername = Strings.createUsername(req.body.data.username);
const verification = await Data.getVerificationBySid({
sid: req.body.data.token,
});
@ -38,7 +40,7 @@ export default async (req, res) => {
}
const existing = await Data.getUserByUsername({
username: req.body.data.username.toLowerCase(),
username: formattedUsername,
});
if (existing) {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_USERNAME_TAKEN", error: true });
@ -60,12 +62,11 @@ export default async (req, res) => {
// TODO(jim):
// Don't do this once you refactor.
const newUsername = req.body.data.username.toLowerCase();
const newEmail = verification.email;
const { buckets, bucketKey, bucketName } = await Utilities.getBucketAPIFromUserToken({
user: {
username: newUsername,
username: formattedUsername,
data: { tokens: { api } },
},
});
@ -85,7 +86,7 @@ export default async (req, res) => {
const user = await Data.createUser({
password: hash,
salt,
username: newUsername,
username: formattedUsername,
email: newEmail,
data: {
photo,
@ -122,6 +123,4 @@ export default async (req, res) => {
from: slateEmail,
templateId: welcomeTemplateId,
});
Monitor.createUser({ user });
};

View File

@ -15,7 +15,10 @@ const AUTH_STATE_GRAPH = {
RESET_PASSWORD: "password_reset",
},
signup: {},
twitter_signup: {},
twitter_signup: {
LINK_EXISTING_ACCOUNT: "twitter_linking",
},
twitter_linking: {},
password_reset: { BACK: "signin" },
};
@ -40,6 +43,7 @@ export const useAuthFlow = () => {
goToTwitterSignupScene: ({ twitterEmail }) =>
send({ event: "SIGNUP_WITH_TWITTER", context: { twitterEmail } }),
goToResetPassword: () => send({ event: "RESET_PASSWORD" }),
goToTwitterLinkingScene: () => send({ event: "LINK_EXISTING_ACCOUNT" }),
goBack: () => send({ event: "BACK" }),
clearMessages: () => send({ ...state, context: { ...state.context, message: "" } }),
}),
@ -206,8 +210,9 @@ export const useSignup = ({ onAuthenticate }) => {
return { createVerification, verifyEmail, createUser, resendVerification };
};
export const useTwitter = ({ onAuthenticate, goToTwitterSignupScene }) => {
export const useTwitter = ({ onAuthenticate, onTwitterAuthenticate, goToTwitterSignupScene }) => {
const verificationToken = React.useRef();
const credentialsRef = React.useRef();
const popupRef = React.useRef();
const [isLoggingIn, setIsLoggingIn] = React.useState(false);
@ -296,7 +301,7 @@ export const useTwitter = ({ onAuthenticate, goToTwitterSignupScene }) => {
}
if (response.token) {
await onAuthenticate(response);
await onTwitterAuthenticate(response);
setIsLoggingIn(false);
return;
}
@ -310,6 +315,72 @@ export const useTwitter = ({ onAuthenticate, goToTwitterSignupScene }) => {
}
};
const linkAccount = async ({ username, password }) => {
const { authToken } = twitterTokens.current;
credentialsRef.current = { username, password };
const userVersionResponse = await Actions.getUserVersion({ username });
if (Events.hasError(userVersionResponse)) return;
let hashedPassword;
if (userVersionResponse?.data?.version === 2) {
hashedPassword = await Utilities.encryptPasswordClient(password);
} else {
hashedPassword = password;
}
// NOTE(amine): handling client hash if the user is v2
const response = await Actions.linkTwitterAccount({
username,
password: hashedPassword,
token: authToken,
});
if (response.shouldMigrate) {
return response;
}
if (Events.hasError(response)) {
return;
}
const authResponse = await onAuthenticate({ username, password: hashedPassword });
if (Events.hasError(authResponse)) {
return;
}
};
const linkAccountWithVerification = async ({ pin }) => {
const { username, password } = credentialsRef.current;
const userVersionResponse = await Actions.getUserVersion({ username });
if (Events.hasError(userVersionResponse)) return;
let hashedPassword;
if (userVersionResponse?.data?.version === 2) {
hashedPassword = await Utilities.encryptPasswordClient(password);
} else {
hashedPassword = password;
}
// NOTE(amine): handling client hash if the user is v2
const response = await Actions.linkTwitterAccountWithVerification({
username,
password: hashedPassword,
token: verificationToken.current,
pin,
});
if (Events.hasError(response)) {
return;
}
const authResponse = await onAuthenticate({ username, password: hashedPassword });
if (Events.hasError(authResponse)) {
return;
}
};
const signup = async ({ email = "", username = "" }) => {
const { authToken } = twitterTokens.current;
const response = await Actions.createUserViaTwitter({
@ -319,7 +390,7 @@ export const useTwitter = ({ onAuthenticate, goToTwitterSignupScene }) => {
});
if (Events.hasError(response)) return;
if (response.token) {
await onAuthenticate(response);
await onTwitterAuthenticate(response);
return;
}
return response;
@ -334,7 +405,7 @@ export const useTwitter = ({ onAuthenticate, goToTwitterSignupScene }) => {
if (Events.hasError(response)) return;
if (response.token) {
await onAuthenticate(response);
await onTwitterAuthenticate(response);
return;
}
return response;
@ -365,6 +436,8 @@ export const useTwitter = ({ onAuthenticate, goToTwitterSignupScene }) => {
return {
isLoggingIn,
signin,
linkAccount,
linkAccountWithVerification,
signup,
signupWithVerification,
createVerification,

View File

@ -3,7 +3,14 @@ import * as Utilities from "common/utilities";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import { css } from "@emotion/react";
import { Initial, Signin, Signup, TwitterSignup, ResetPassword } from "~/components/core/Auth";
import {
Initial,
Signin,
Signup,
TwitterSignup,
TwitterLinking,
ResetPassword,
} from "~/components/core/Auth";
import {
useAuthFlow,
@ -21,7 +28,7 @@ const STYLES_ROOT = css`
text-align: center;
font-size: 1rem;
min-height: 100vh;
height: 100vh;
width: 100vw;
position: relative;
overflow: hidden;
@ -57,6 +64,7 @@ const SigninScene = ({ onAuthenticate, onTwitterAuthenticate, page, ...props })
goToSigninScene,
goToSignupScene,
goToTwitterSignupScene,
goToTwitterLinkingScene,
goToResetPassword,
clearMessages,
goBack,
@ -69,7 +77,8 @@ const SigninScene = ({ onAuthenticate, onTwitterAuthenticate, page, ...props })
onAuthenticate,
});
const twitterProvider = useTwitter({
onAuthenticate: onTwitterAuthenticate,
onTwitterAuthenticate: onTwitterAuthenticate,
onAuthenticate,
goToTwitterSignupScene,
});
@ -121,12 +130,23 @@ const SigninScene = ({ onAuthenticate, onTwitterAuthenticate, page, ...props })
<TwitterSignup
initialEmail={context.twitterEmail}
createVerification={twitterProvider.createVerification}
resendEmailVerification={twitterProvider.resendEmailVerification}
resendEmailVerification={twitterProvider.resendVerification}
goToTwitterLinkingScene={goToTwitterLinkingScene}
onSignupWithVerification={twitterProvider.signupWithVerification}
onSignup={twitterProvider.signup}
/>
);
if (scene === "twitter_linking") {
return (
<TwitterLinking
linkAccount={twitterProvider.linkAccount}
linkAccountWithVerification={twitterProvider.linkAccountWithVerification}
resendEmailVerification={twitterProvider.resendVerification}
createVerification={twitterProvider.createVerification}
/>
);
}
// NOTE(amine): if the user goes back, we should prefill the email
const initialEmail =
prevScene === "signin" && context.emailOrUsername ? context.emailOrUsername : "";
@ -142,18 +162,36 @@ const SigninScene = ({ onAuthenticate, onTwitterAuthenticate, page, ...props })
/>
);
};
const BackgroundGenerator = ({ children, ...props }) => {
const background = React.useMemo(() => {
const backgroundIdx = Utilities.getRandomNumberBetween(0, AUTH_BACKGROUNDS.length - 1);
return AUTH_BACKGROUNDS[backgroundIdx];
}, []);
// NOTE(amine): fix for 100vh overflowing in mobile
// https://bugs.webkit.org/show_bug.cgi?id=141832
const [height, setHeight] = React.useState();
React.useLayoutEffect(() => {
if (!window) return;
const windowInnerHeight = window.innerHeight;
setHeight(windowInnerHeight);
}, []);
return (
<div style={{ backgroundImage: `url(${background})`, height }} {...props}>
{children}
</div>
);
};
const WithCustomWrapper = (Component) => (props) => {
const backgroundIdx = Utilities.getRandomNumberBetween(0, AUTH_BACKGROUNDS.length);
console.log(backgroundIdx);
const background = AUTH_BACKGROUNDS[backgroundIdx];
return (
<WebsitePrototypeWrapper>
<div style={{ backgroundImage: `url(${background})` }} css={STYLES_ROOT}>
<BackgroundGenerator css={STYLES_ROOT}>
<div css={STYLES_MIDDLE}>
<Component {...props} />
</div>
</div>
</BackgroundGenerator>
</WebsitePrototypeWrapper>
);
};

View File

@ -5,7 +5,7 @@ import * as Strings from "~/common/strings";
import * as Validations from "~/common/validations";
import * as Window from "~/common/window";
import * as Constants from "~/common/constants";
import * as FileUtilities from "~/common/file-utilities";
import * as Utilities from "~/common/utilities";
import * as UserBehaviors from "~/common/user-behaviors";
import * as Events from "~/common/custom-events";
import * as SVG from "~/common/svg";
@ -13,11 +13,13 @@ import * as SVG from "~/common/svg";
import { css } from "@emotion/react";
import { SecondaryTabGroup } from "~/components/core/TabGroup";
import { ConfirmationModal } from "~/components/core/ConfirmationModal";
import { useForm, useToggle } from "~/common/hooks";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import ScenePage from "~/components/core/ScenePage";
import ScenePageHeader from "~/components/core/ScenePageHeader";
import Avatar from "~/components/core/Avatar";
import Field from "~/components/core/Field";
const STYLES_FILE_HIDDEN = css`
height: 1px;
@ -41,25 +43,89 @@ const STYLES_HEADER = css`
margin-bottom: 16px;
`;
const SecuritySection = ({ onUpdateViewer, username }) => {
const [passwordValidations, setPasswordValidations] = React.useState(
Validations.passwordForm("")
);
const { getFieldProps, getFormProps, isSubmitting } = useForm({
initialValues: { password: "" },
validate: ({ password }, errors) => {
if (!Validations.password(password)) errors.password = "Incorrect password";
return errors;
},
onSubmit: async ({ password }) => {
const userVersionResponse = await Actions.getUserVersion({ username });
if (Events.hasError(userVersionResponse)) return;
let hashedPassword;
if (userVersionResponse?.data?.version === 2) {
hashedPassword = await Utilities.encryptPasswordClient(password);
} else {
hashedPassword = password;
}
let response = await onUpdateViewer({
type: "CHANGE_PASSWORD",
password: hashedPassword,
});
if (Events.hasError(response)) {
return;
}
Events.dispatchMessage({ message: "Password successfully updated!", status: "INFO" });
},
});
const [showPassword, togglePasswordVisibility] = useToggle(false);
return (
<div>
<div css={STYLES_HEADER}>Change password</div>
<div>Passwords must be a minimum of eight characters.</div>
<form {...getFormProps()}>
<Field
containerStyle={{ marginTop: 24 }}
placeholder="Your new password"
validations={passwordValidations}
color="white"
{...getFieldProps("password", {
onChange: (e) => {
const validations = Validations.passwordForm(e.target.value);
setPasswordValidations(validations);
},
})}
type={showPassword ? "text" : "password"}
onClickIcon={togglePasswordVisibility}
icon={showPassword ? SVG.EyeOff : SVG.Eye}
/>
<div style={{ marginTop: 24 }}>
<System.ButtonPrimary loading={isSubmitting} type="submit" style={{ width: "200px" }}>
Change password
</System.ButtonPrimary>
</div>
</form>
</div>
);
};
export default class SceneEditAccount extends React.Component {
state = {
username: this.props.viewer.username,
password: "",
confirm: "",
body: this.props.viewer.data.body,
photo: this.props.viewer.data.photo,
name: this.props.viewer.data.name,
deleting: false,
allow_filecoin_directory_listing: this.props.viewer.data.settings
?.allow_filecoin_directory_listing,
allow_filecoin_directory_listing:
this.props.viewer.data.settings?.allow_filecoin_directory_listing,
allow_automatic_data_storage: this.props.viewer.data.settings?.allow_automatic_data_storage,
allow_encrypted_data_storage: this.props.viewer.data.settings?.allow_encrypted_data_storage,
changingPassword: false,
changingAvatar: false,
savingNameBio: false,
changingFilecoin: false,
modalShow: false,
showPassword: false,
};
_handleUpload = async (e) => {
@ -70,7 +136,7 @@ export default class SceneEditAccount extends React.Component {
return;
}
const cid = file.cid;
const { cid } = file;
const url = Strings.getURLfromCID(cid);
let updateResponse = await Actions.updateViewer({
data: {
@ -82,7 +148,7 @@ export default class SceneEditAccount extends React.Component {
this.setState({ changingAvatar: false, photo: url });
};
_handleSaveFilecoin = async (e) => {
_handleSaveFilecoin = async () => {
this.setState({ changingFilecoin: true });
let response = await Actions.updateViewer({
@ -100,7 +166,7 @@ export default class SceneEditAccount extends React.Component {
this.setState({ changingFilecoin: false });
};
_handleSave = async (e) => {
_handleSave = async () => {
if (!Validations.username(this.state.username)) {
Events.dispatchMessage({
message: "Please include only letters and numbers in your username",
@ -126,33 +192,7 @@ export default class SceneEditAccount extends React.Component {
};
_handleUsernameChange = (e) => {
this.setState({ [e.target.name]: e.target.value.toLowerCase() });
};
_handleChangePassword = async (e) => {
if (this.state.password !== this.state.confirm) {
Events.dispatchMessage({ message: "Passwords did not match" });
return;
}
if (!Validations.password(this.state.password)) {
Events.dispatchMessage({ message: "Password length must be more than 8 characters" });
return;
}
this.setState({ changingPassword: true });
let response = await Actions.updateViewer({
type: "CHANGE_PASSWORD",
password: this.state.password,
});
if (Events.hasError(response)) {
this.setState({ changingPassword: false });
return;
}
this.setState({ changingPassword: false, password: "", confirm: "" });
this.setState({ [e.target.name]: Strings.createUsername(e.target.value) });
};
_handleDelete = async (res) => {
@ -296,31 +336,10 @@ export default class SceneEditAccount extends React.Component {
</div>
) : null}
{tab === "security" ? (
<div>
<div css={STYLES_HEADER}>Change password</div>
<div>Passwords must be a minimum of eight characters.</div>
<System.Input
containerStyle={{ marginTop: 24 }}
name="password"
type={this.state.showPassword ? "text" : "password"}
value={this.state.password}
placeholder="Your new password"
onChange={this._handleChange}
onClickIcon={() => this.setState({ showPassword: !this.state.showPassword })}
icon={this.state.showPassword ? SVG.EyeOff : SVG.Eye}
/>
<div style={{ marginTop: 24 }}>
<System.ButtonPrimary
onClick={this._handleChangePassword}
loading={this.state.changingPassword}
style={{ width: "200px" }}
>
Change password
</System.ButtonPrimary>
</div>
</div>
<SecuritySection
onUpdateViewer={Actions.updateViewer}
username={this.props.viewer.username}
/>
) : null}
{tab === "account" ? (
<div>