fix(auth): radd missing code

This commit is contained in:
Aminejv 2021-07-29 20:01:41 +01:00
parent c6482e64e3
commit 9fd63292cf
26 changed files with 860 additions and 241 deletions

View File

@ -527,3 +527,24 @@ export const createSurvey = async (data) => {
body: JSON.stringify({ 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 resendPasswordResetVerification = async (data) => {
return await returnJSON(`/api/verifications/password-reset/resend`, {
...DEFAULT_OPTIONS,
body: JSON.stringify({ data }),
});
};

View File

@ -127,7 +127,8 @@ export const useForm = ({
let errors = {};
try {
setInternal((prev) => ({ ...prev, isValidating: true }));
errors = await validate(state.values, { ...state.errors });
console.log("submitting errors", state);
errors = await validate(state.values, {});
if (_hasError(errors)) return;
} catch (e) {
Logging.error(e);

View File

@ -277,6 +277,21 @@ export const copyText = (str) => {
return true;
};
//NOTE(martina): createUsername is like createSlug, except allowing _ instead of -, and instead of replacing invalid characters it simply removes them
export const createUsername = (text) => {
if (isEmpty(text)) {
return "";
}
text = text
.toString()
.toLowerCase()
.trim()
.replace(/[^a-z0-9_]/g, "");
return text;
};
// SOURCE(jim):
// https://gist.github.com/mathewbyrne/1280286
// modified to support chinese characters, base case, and german.

View File

@ -7,7 +7,7 @@ export const Loader = (props) => {
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
height={props.height}
{...props}
style={{ display: "block", ...props.style }}
>
<path d="M8 1.68237V4.34904" />

View File

@ -32,6 +32,10 @@ export const encryptPasswordClient = async (text) => {
return hash;
};
export const getRandomNumberBetween = (min, max) => {
return Math.round(Math.random() * (max - min) + min);
};
export const coerceToArray = (input) => {
if (!input) {
return [];

View File

@ -8,17 +8,18 @@ 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 { 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";
import { Toggle, SignUpPopover } from "~/components/core/Auth/components";
const STYLES_INITIAL_CONTAINER = css`
display: flex;
flex-direction: column;
flex-grow: 1;
`;
const STYLES_LINK_ITEM = (theme) => css`
display: block;
text-decoration: none;
@ -31,11 +32,9 @@ const STYLES_LINK_ITEM = (theme) => css`
color: ${theme.system.black};
transition: 200ms ease all;
word-wrap: break-word;
:visited {
color: ${theme.system.black};
}
:hover {
color: ${theme.system.blue};
}
@ -72,22 +71,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({
@ -171,61 +162,36 @@ export default function Initial({
// NOTE(amine): the input component internally is using 16px margin top
containerStyle={{ marginTop: "4px" }}
/>
<div style={{ marginTop: "auto" }}>
<a css={STYLES_LINK_ITEM} href="/terms" target="_blank">
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>
<SVG.RightArrow height="16px" style={{ marginRight: 4 }} /> Terms of service
</div>
</a>
<a css={STYLES_LINK_ITEM} style={{ marginTop: 4 }} href="/guidelines" target="_blank">
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>
<SVG.RightArrow height="16px" style={{ marginRight: 4 }} /> Community guidelines
</div>
</a>
</div>
</>
) : (
<AnimateSharedLayout>
<form {...getFormProps()}>
<Field
autoFocus
label="Sign up with email"
placeholder="Email"
type="email"
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 layout>
<System.ButtonPrimary
full
type="submit"
style={{ marginTop: "8px" }}
loading={isCheckingEmail}
>
Send verification code
</System.ButtonPrimary>
</motion.div>
</form>
<div style={{ marginTop: "auto" }}>
<a css={STYLES_LINK_ITEM} href="/terms" target="_blank">
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>
<SVG.RightArrow height="16px" style={{ marginRight: 4 }} /> Terms of service
</div>
</a>
<a css={STYLES_LINK_ITEM} style={{ marginTop: 4 }} href="/guidelines" target="_blank">
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>
<SVG.RightArrow height="16px" style={{ marginRight: 4 }} /> Community guidelines
</div>
</a>
</div>
<Field
autoFocus
label="Sign up with email"
placeholder="Email"
type="text"
name="email"
full
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
icon={
isCheckingEmail
? () => (
<LoaderSpinner
style={{
height: 16,
width: 16,
marginLeft: 16,
position: "relative",
right: 12,
}}
/>
)
: ArrowButton
}
// NOTE(amine): the input component internally is using 16px margin top
containerStyle={{ marginTop: "4px" }}
{...getSignupFielProps("email")}
/>
</AnimateSharedLayout>
)}
</div>

View File

@ -50,21 +50,20 @@ const useCheckUser = () => {
};
};
const createValidations = (validateUsername) => async (
{ username, password, acceptTerms },
errors
) => {
await validateUsername({ username }, errors);
const createValidations =
(validateUsername) =>
async ({ username, password, acceptTerms }, errors) => {
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.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 (!Validations.password(password)) errors.password = "Incorrect password";
if (!acceptTerms) errors.acceptTerms = "Must accept terms and conditions";
return errors;
};
if (!acceptTerms) errors.acceptTerms = "Must accept terms and conditions";
return errors;
};
export default function Signup({ verifyEmail, createUser, resendEmailVerification }) {
const [passwordValidations, setPasswordValidations] = React.useState(
@ -110,7 +109,7 @@ export default function Signup({ verifyEmail, createUser, resendEmailVerificatio
height: 16,
width: 16,
marginLeft: 16,
position: "absolute",
position: "relative",
right: 12,
}}
/>

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,16 @@ 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 = (theme) => css`
padding: 0;
margin: 0;
max-width: 224px;
text-align: center;
color: ${theme.semantic.textGrayDark};
max-width: 228px;
margin: 0 auto;
background-color: unset;
border: none;
`;
const useTwitterSignup = () => {
@ -27,7 +34,7 @@ const useTwitterSignup = () => {
};
const useCheckUser = () => {
const MESSAGE = "The username is taken.";
const MESSAGE = "The username is taken";
const usernamesAllowed = React.useRef([]);
const usernamesTaken = React.useRef([]);
@ -46,7 +53,7 @@ const useCheckUser = () => {
username,
});
if (response.data) {
errors.username = "The username is taken.";
errors.username = "The username is taken";
usernamesTaken.current.push(username);
return;
}
@ -54,22 +61,19 @@ const useCheckUser = () => {
};
};
const createValidations = (validateUsername) => async (
{ username, email, acceptTerms },
errors
) => {
await validateUsername({ username }, errors);
const createValidations =
(validateUsername) =>
async ({ username, acceptTerms }, errors) => {
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.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";
if (!acceptTerms) errors.acceptTerms = "Must accept terms and conditions";
return errors;
};
return errors;
};
const MotionLayout = ({ children, ...props }) => (
<motion.div layout {...props}>
@ -80,6 +84,8 @@ const MotionLayout = ({ children, ...props }) => (
export default function TwitterSignup({
initialEmail,
onSignup,
goToTwitterLinkingScene,
resendEmailVerification,
createVerification,
onSignupWithVerification,
}) {
@ -90,11 +96,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 +118,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 +130,7 @@ export default function TwitterSignup({
placeholder="Username"
name="username"
type="text"
success="The username is available."
success="The username is available"
icon={
isValidating
? () => (
@ -132,7 +139,7 @@ export default function TwitterSignup({
height: 16,
width: 16,
marginLeft: 16,
position: "absolute",
position: "relative",
right: 12,
}}
/>
@ -140,7 +147,7 @@ export default function TwitterSignup({
: null
}
{...getFieldProps("username")}
style={{ backgroundColor: "rgba(242,242,247,0.5)" }}
full
/>
<AnimateSharedLayout>
<Field
@ -150,8 +157,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 +172,18 @@ export default function TwitterSignup({
Create account
</System.ButtonPrimary>
</motion.div>
{(!initialEmail || initialEmail !== email) && (
<motion.div layout>
<System.P1 css={STYLES_SMALL} style={{ marginTop: 16 }}>
You will receive a code to verify your email at this address
</System.P1>
</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

@ -10,7 +10,7 @@ import { AnimateSharedLayout, motion } from "framer-motion";
import { LoaderSpinner } from "~/components/system/components/Loaders";
import { css } from "@emotion/react";
import { useField } from "~/common/hooks";
import { SignUpPopover } from "~/components/core/Auth/components";
import { SignUpPopover, ArrowButton } from "~/components/core/Auth/components";
const STYLES_HELPER = (theme) => css`
text-align: center;
@ -114,7 +114,7 @@ export default function Verification({ onVerify, title = DEFAULT_TITLE, onResend
<LoaderSpinner height="16px" />
</div>
)
: SVG.RightArrow
: ArrowButton
}
textStyle={{ width: "100% !important" }}
containerStyle={{ marginTop: "28px" }}

View File

@ -2,3 +2,4 @@ export { default as Toggle } from "~/components/core/Auth/components/Toggle";
export { default as SignUpPopover } from "~/components/core/Auth/components/SignUpPopover";
export { default as Verification } from "~/components/core/Auth/components/Verification";
export { default as AuthCheckBox } from "~/components/core/Auth/components/AuthCheckBox";
export { default as ArrowButton } from "~/components/core/Auth/components/ArrowButton";

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

@ -43,7 +43,7 @@ const STYLES_CIRCLE_SUCCESS = (theme) => css`
`;
const STYLES_INPUT = (theme) => css`
background-color: rgba(242, 242, 247, 0.7);
background-color: rgba(242, 242, 247, 0.5);
box-shadow: ${theme.shadow.lightLarge};
border-radius: 12px;
&::placeholder {
@ -53,6 +53,7 @@ const STYLES_INPUT = (theme) => css`
const STYLES_INPUT_ERROR = (theme) => css`
background-color: rgba(242, 242, 247, 0.5);
border: 1px solid ${theme.system.red};
border-radius: 8px;
&::placeholder {
color: ${theme.semantic.textGrayDark};
}
@ -60,6 +61,7 @@ const STYLES_INPUT_ERROR = (theme) => css`
const STYLES_INPUT_SUCCESS = (theme) => css`
background-color: rgba(242, 242, 247, 0.5);
border: 1px solid ${theme.system.green};
border-radius: 8px;
&::placeholder {
color: ${theme.semantic.textGrayDark};
}

View File

@ -204,7 +204,6 @@ const STYLES_LOADER_PROGRESS = css`
// export const LoaderSpinner = (props) => <div css={STYLES_LOADER_SPINNER} {...props} />;
const STYLES_LOADER_SPINNER = css`
display: inline-block;
animation: slate-client-animation-spin 1.5s cubic-bezier(0.5, 0.1, 0.4, 0.7) infinite;
@keyframes slate-client-animation-spin {
@ -221,10 +220,11 @@ const STYLES_LOADER_SPINNER = css`
export const LoaderProgress = (props) => <div css={STYLES_LOADER_PROGRESS} {...props} />;
export const LoaderSpinner = (props) => (
<span css={STYLES_LOADER_SPINNER}>
export const LoaderSpinner = ({ css, ...props }) => (
<span>
<SVG.Loader
{...props}
css={[STYLES_LOADER_SPINNER, css]}
style={{
display: "block",
color: Constants.system.blue,

View File

@ -118,7 +118,7 @@ export const parseAuthHeader = (value) => {
};
export const getFilecoinAPIFromUserToken = async ({ user }) => {
const textileKey = user.textileKey;
const { textileKey } = user;
const identity = await PrivateKey.fromString(textileKey);
const filecoin = await Filecoin.withKeyInfo(TEXTILE_KEY_INFO);
await filecoin.getToken(identity);

View File

@ -55,20 +55,37 @@ export default async (req, res) => {
return res.status(403).send({ decorator: "SERVER_TWITTER_LOGIN_ONLY", error: true });
}
const hash = await Utilities.encryptPassword(req.body.data.password, user.salt);
let userUpdates = { id: user.id, lastActive: new Date() };
let hash = await Utilities.encryptPassword(req.body.data.password, user.salt);
let updatePassword = user.authVersion === 1; //NOTE(martina): if they are v1, we may need to update their password
if (hash !== user.password) {
return res.status(403).send({ decorator: "SERVER_SIGN_IN_WRONG_CREDENTIALS", error: true });
//NOTE(martina): this was added to deal with a specific case where the passwords of some v1 users could either be the v1 hashing schema or v2 (so we try both)
if (user.authVersion === 1) {
const clientHash = await encryptPasswordClient(req.body.data.password);
hash = await Utilities.encryptPassword(clientHash, user.salt);
if (hash !== user.password) {
return res.status(403).send({ decorator: "SERVER_SIGN_IN_WRONG_CREDENTIALS", error: true });
}
updatePassword = false; //NOTE(martina): means the user's password is already v2 hashed, and doesn't need to be updated
} else {
return res.status(403).send({ decorator: "SERVER_SIGN_IN_WRONG_CREDENTIALS", error: true });
}
}
let userUpdates = {};
if (user.authVersion === 1) {
userUpdates.authVersion = 2;
userUpdates.revertedVersion = false;
}
if (updatePassword) {
const newHash = await encryptPasswordClient(req.body.data.password);
const doubledHash = await Utilities.encryptPassword(newHash, user.salt);
userUpdates = { id: user.id, lastActive: new Date(), authVersion: 2, password: doubledHash };
userUpdates.password = doubledHash;
}
await Data.updateUserById({ id: user.id, lastActive: new Date(), ...userUpdates });
await Data.updateUserById(userUpdates);
if (!user.email) {
return res

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

@ -3,9 +3,9 @@ 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 SearchManager from "~/node_common/managers/search";
import * as Constants from "~/node_common/constants";
import SearchManager from "~/node_common/managers/search";
import JWT from "jsonwebtoken";
export default async (req, res) => {
@ -68,7 +68,7 @@ export default async (req, res) => {
return res.status(201).send({ decorator: "SERVER_CREATE_USER_TWITTER_EXISTS" });
}
const newUsername = username.toLowerCase();
const newUsername = Strings.createUsername(username);
const newEmail = verification.email.toLowerCase();
// NOTE(Amine): If there is an account with the user's twitter email
@ -76,17 +76,13 @@ 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: newUsername });
if (userByUsername) {
return res.status(201).send({ decorator: "SERVER_CREATE_USER_USERNAME_TAKEN" });
}
const {
textileKey,
textileToken,
textileThreadID,
textileBucketCID,
} = await Utilities.createBucket({});
const { textileKey, textileToken, textileThreadID, textileBucketCID } =
await Utilities.createBucket({});
if (!textileKey || !textileToken || !textileThreadID || !textileBucketCID) {
return res

View File

@ -3,8 +3,8 @@ 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 SearchManager from "~/node_common/managers/search";
import SearchManager from "~/node_common/managers/search";
import JWT from "jsonwebtoken";
const COOKIE_NAME = "oauth_token";
@ -28,6 +28,9 @@ export default async (req, res) => {
return res.status(500).send({ decorator: "SERVER_CREATE_USER_INVALID_USERNAME", error: true });
}
const newUsername = Strings.createUsername(username);
const newEmail = email.toLowerCase();
const storedAuthToken = req.cookies[COOKIE_NAME];
// NOTE(amine): additional security check
@ -57,20 +60,13 @@ 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: newUsername });
if (userByUsername) {
return res.status(201).send({ decorator: "SERVER_CREATE_USER_USERNAME_TAKEN" });
}
const newUsername = username.toLowerCase();
const newEmail = email.toLowerCase();
const {
textileKey,
textileToken,
textileThreadID,
textileBucketCID,
} = await Utilities.createBucket({});
const { textileKey, textileToken, textileThreadID, textileBucketCID } =
await Utilities.createBucket({});
if (!textileKey || !textileToken || !textileThreadID || !textileBucketCID) {
return res

View File

@ -15,6 +15,6 @@ export default async (req, res) => {
return res.status(200).send({
decorator: "SERVER_CHECK_EMAIL",
data: { email: !!userByEmail.email, twitter: !!!userByEmail.password },
data: { email: !!userByEmail.email, twitter: !userByEmail.password },
});
};

View File

@ -1,13 +1,13 @@
import * as Environment from "~/node_common/environment";
import * as Data from "~/node_common/data";
import * as Utilities from "~/node_common/utilities";
import SearchManager from "~/node_common/managers/search";
import * as Validations from "~/common/validations";
import * as Strings from "~/common/strings";
import * as EmailManager from "~/node_common/managers/emails";
import * as Monitor from "~/node_common/monitor";
import BCrypt from "bcrypt";
import SearchManager from "~/node_common/managers/search";
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
@ -36,14 +36,17 @@ export default async (req, res) => {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_EMAIL_UNVERIFIED", error: true });
}
const newUsername = Strings.createUsername(req.body.data.username);
const newEmail = verification.email;
const existing = await Data.getUserByUsername({
username: req.body.data.username.toLowerCase(),
username: newUsername,
});
if (existing) {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_USERNAME_TAKEN", error: true });
}
const existingViaEmail = await Data.getUserByEmail({ email: verification.email });
const existingViaEmail = await Data.getUserByEmail({ email: newEmail });
if (existingViaEmail) {
return res.status(403).send({ decorator: "SERVER_CREATE_USER_EMAIL_TAKEN", error: true });
}
@ -52,15 +55,8 @@ export default async (req, res) => {
const salt = await BCrypt.genSalt(rounds);
const hash = await Utilities.encryptPassword(req.body.data.password, salt);
const newUsername = req.body.data.username.toLowerCase();
const newEmail = verification.email;
const {
textileKey,
textileToken,
textileThreadID,
textileBucketCID,
} = await Utilities.createBucket({});
const { textileKey, textileToken, textileThreadID, textileBucketCID } =
await Utilities.createBucket({});
if (!textileKey || !textileToken || !textileThreadID || !textileBucketCID) {
return res

View File

@ -0,0 +1,55 @@
import * as Data from "~/node_common/data";
import * as Strings from "~/common/strings";
import * as Environment from "~/node_common/environment";
import * as EmailManager from "~/node_common/managers/emails";
import * as Constants from "~/node_common/constants";
// NOTE(amine): this endpoint is rate limited in ./server.js,
export default async (req, res) => {
if (!Strings.isEmpty(Environment.ALLOWED_HOST) && req.headers.host !== Environment.ALLOWED_HOST) {
return res
.status(403)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_NOT_ALLOWED", error: true });
}
if (Strings.isEmpty(req.body.data.token)) {
return res
.status(500)
.send({ decorator: "SERVER_EMAIL_VERIFICATION_INVALID_TOKEN", error: true });
}
const verification = await Data.getVerificationBySid({
sid: req.body.data.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 });
}
const confTemplateId = "d-0bde6fd8eabf4ed4ae7fd409ddd532dd";
const slateEmail = "hello@slate.host";
const sentEmail = await EmailManager.sendTemplate({
to: verification.email,
from: slateEmail,
templateId: confTemplateId,
templateData: { confirmation_code: verification.pin },
});
if (sentEmail?.error) {
return res.status(500).send({ decorator: sentEmail.decorator, error: true });
}
return res.status(200).send({
decorator: "SERVER_EMAIL_VERIFICATION_RESEND_SUCCESS",
});
};

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: "" } }),
}),
@ -133,7 +137,7 @@ export const usePasswordReset = ({ onAuthenticate }) => {
};
const resendVerification = async () => {
const response = await Actions.resendVerification({
const response = await Actions.resendPasswordResetVerification({
token: verificationToken.current,
});
if (Events.hasError(response)) {
@ -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);
@ -262,7 +267,9 @@ export const useTwitter = ({ onAuthenticate, goToTwitterSignupScene }) => {
reject("getAuthTokenAndVerifier Error 2");
}
}
} catch (e) {}
} catch (e) {
Logging.error(e);
}
}, 500);
});
@ -294,7 +301,7 @@ export const useTwitter = ({ onAuthenticate, goToTwitterSignupScene }) => {
}
if (response.token) {
await onAuthenticate(response);
await onTwitterAuthenticate(response);
setIsLoggingIn(false);
return;
}
@ -308,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({
@ -317,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;
@ -332,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;
@ -363,6 +436,8 @@ export const useTwitter = ({ onAuthenticate, goToTwitterSignupScene }) => {
return {
isLoggingIn,
signin,
linkAccount,
linkAccountWithVerification,
signup,
signupWithVerification,
createVerification,

View File

@ -1,9 +1,16 @@
import * as React from "react";
import * as UserBehaviors from "~/common/user-behaviors";
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,
@ -13,9 +20,6 @@ import {
usePasswordReset,
} from "~/scenes/SceneAuth/hooks";
const background_image =
"https://slate.textile.io/ipfs/bafybeiddgkvf5ta6y5b7wamrxl33mtst4detegleblw4gfduhwm3sdwdra";
const STYLES_ROOT = css`
display: flex;
flex-direction: column;
@ -23,12 +27,10 @@ const STYLES_ROOT = css`
justify-content: space-between;
text-align: center;
font-size: 1rem;
min-height: 100vh;
width: 100vw;
position: absolute;
position: relative;
overflow: hidden;
background-image: url(${background_image});
background-repeat: no-repeat;
background-size: cover;
`;
@ -45,11 +47,23 @@ const STYLES_MIDDLE = css`
padding: 24px;
`;
const AUTH_BACKGROUNDS = [
"https://slate.textile.io/ipfs/bafybeigostprfkuuvuqlehutki32fnvshm2dyy4abqotmlffsca4f7qs7a",
"https://slate.textile.io/ipfs/bafybeicmokw3bl5six6u7eflbxcdblpgbx3fat24djrqg6n3hmbleidks4",
"https://slate.textile.io/ipfs/bafybeibkttaavlkjxgtafqndyrbgvwqcng67zvd4v36w7fvpajwmdgmxcu",
"https://slate.textile.io/ipfs/bafybeicpk7hkbeqdgbwkx3dltlz3akf3qbjpqgfphbnry4b6txnailtlpq",
"https://slate.textile.io/ipfs/bafybeibb2xknh3iwwetrro73hw3xfzjgwbi4n4c63wqmwt5hvaloqnh33u",
"https://slate.textile.io/ipfs/bafybeig4mij32vyda2jbh6zua3r2rkdpby6wtvninwgxvsejjdnls4wpc4",
"https://slate.textile.io/ipfs/bafybeihmoycn4a6zafd2k3fjcadskrxwvri5cwhabatzbyzteouh3s7igi",
"https://slate.textile.io/ipfs/bafybeigxssjsv3tmdhz4bj6vl2ca5c6rrhdkepw3mifvlllb7orpx5cfou",
];
const SigninScene = ({ onAuthenticate, onTwitterAuthenticate, page, ...props }) => {
const {
goToSigninScene,
goToSignupScene,
goToTwitterSignupScene,
goToTwitterLinkingScene,
goToResetPassword,
clearMessages,
goBack,
@ -62,7 +76,8 @@ const SigninScene = ({ onAuthenticate, onTwitterAuthenticate, page, ...props })
onAuthenticate,
});
const twitterProvider = useTwitter({
onAuthenticate: onTwitterAuthenticate,
onTwitterAuthenticate: onTwitterAuthenticate,
onAuthenticate,
goToTwitterSignupScene,
});
@ -114,12 +129,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 : "";
@ -135,16 +161,48 @@ const SigninScene = ({ onAuthenticate, onTwitterAuthenticate, page, ...props })
/>
);
};
const BackgroundGenerator = ({ children, isMobile, ...props }) => {
const background = React.useMemo(() => {
const backgroundIdx = Utilities.getRandomNumberBetween(0, AUTH_BACKGROUNDS.length - 1);
return AUTH_BACKGROUNDS[backgroundIdx];
}, []);
const WithCustomWrapper = (Component) => (props) =>
(
// 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 updateHeight = () => {
const windowInnerHeight = window.innerHeight;
setHeight(windowInnerHeight);
};
updateHeight();
// NOTE(amine): don't update height on mobile
if (isMobile) return;
window.addEventListener("resize", updateHeight);
return () => window.addEventListener("resize", updateHeight);
}, [isMobile]);
return (
<div style={{ backgroundImage: `url(${background})`, height }} {...props}>
{children}
</div>
);
};
const WithCustomWrapper = (Component) => (props) => {
return (
<WebsitePrototypeWrapper>
<div css={STYLES_ROOT}>
<BackgroundGenerator css={STYLES_ROOT} isMobile={props.isMobile}>
<div css={STYLES_MIDDLE}>
<Component {...props} />
</div>
</div>
</BackgroundGenerator>
</WebsitePrototypeWrapper>
);
};
export default WithCustomWrapper(SigninScene);

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,10 +13,12 @@ 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 Field from "~/components/core/Field";
import ProfilePhoto from "~/components/core/ProfilePhoto";
const STYLES_FILE_HIDDEN = css`
@ -41,10 +43,76 @@ 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.body,
photo: this.props.viewer.photo,
@ -55,7 +123,6 @@ export default class SceneEditAccount extends React.Component {
savingNameBio: false,
changingFilecoin: false,
modalShow: false,
showPassword: false,
};
_handleUpload = async (e) => {
@ -66,7 +133,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({
user: {
@ -78,7 +145,7 @@ export default class SceneEditAccount extends React.Component {
this.setState({ changingAvatar: false, photo: url });
};
_handleSave = async (e) => {
_handleSave = async () => {
if (!Validations.username(this.state.username)) {
Events.dispatchMessage({
message: "Please include only letters and numbers in your username",
@ -106,30 +173,7 @@ export default class SceneEditAccount extends React.Component {
};
_handleUsernameChange = (e) => {
this.setState({ [e.target.name]: e.target.value.toLowerCase() });
};
_handleChangePassword = async (e) => {
if (!Validations.password(this.state.password)) {
Events.dispatchMessage({
message: "Password does not meet requirements",
});
return;
}
this.setState({ changingPassword: true });
let response = await Actions.updateViewer({
type: "CHANGE_PASSWORD",
user: { 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) => {
@ -222,34 +266,10 @@ export default class SceneEditAccount extends React.Component {
</div>
) : null}
{tab === "security" ? (
<div>
<div css={STYLES_HEADER}>Change password</div>
<div>
Passwords should be at least 8 characters long, contain a mix of upper and lowercase
letters, and have at least 1 number and 1 symbol
</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>