reset password: loading states for edit account and sign in, adds reset password, adds validations to protect against light malice

This commit is contained in:
@wwwjim 2020-07-23 01:57:44 -07:00
parent 02ef7d71a6
commit d11fe19bab
9 changed files with 213 additions and 33 deletions

28
common/validations.js Normal file
View File

@ -0,0 +1,28 @@
import * as Strings from "~/common/strings";
const USERNAME_REGEX = new RegExp("^[a-zA-Z0-9_]{0,}[a-zA-Z]+[0-9]*$");
const MIN_PASSWORD_LENGTH = 8;
export const username = (text) => {
if (Strings.isEmpty(text)) {
return false;
}
if (!USERNAME_REGEX.test(text)) {
return false;
}
return true;
};
export const password = (text) => {
if (Strings.isEmpty(text)) {
return false;
}
if (text.length < MIN_PASSWORD_LENGTH) {
return false;
}
return true;
};

View File

@ -58,10 +58,6 @@ const STYLES_BUTTON_PRIMARY = css`
`;
export const ButtonPrimary = (props) => {
if (props.type === "label") {
return <label css={STYLES_BUTTON_PRIMARY} {...props} />;
}
if (props.loading) {
return (
<button css={STYLES_BUTTON_PRIMARY} style={props.style}>
@ -70,6 +66,19 @@ export const ButtonPrimary = (props) => {
);
}
if (props.type === "label") {
return (
<label
css={STYLES_BUTTON_PRIMARY}
style={props.style}
onClick={props.onClick}
children={props.children}
type={props.label}
htmlFor={props.htmlFor}
/>
);
}
return (
<button
css={STYLES_BUTTON_PRIMARY}
@ -100,10 +109,6 @@ const STYLES_BUTTON_PRIMARY_FULL = css`
`;
export const ButtonPrimaryFull = (props) => {
if (props.type === "label") {
return <label css={STYLES_BUTTON_PRIMARY_FULL} {...props} />;
}
if (props.loading) {
return (
<button css={STYLES_BUTTON_PRIMARY_FULL} style={props.style}>
@ -112,6 +117,19 @@ export const ButtonPrimaryFull = (props) => {
);
}
if (props.type === "label") {
return (
<label
css={STYLES_BUTTON_PRIMARY_FULL}
style={props.style}
onClick={props.onClick}
children={props.children}
type={props.label}
htmlFor={props.htmlFor}
/>
);
}
return (
<button
css={STYLES_BUTTON_PRIMARY_FULL}

View File

@ -1,6 +1,6 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ id, data, username }) => {
export default async ({ id, data, username, salt, password }) => {
const updateObject = {};
if (data) {
@ -11,6 +11,14 @@ export default async ({ id, data, username }) => {
updateObject.username = username;
}
if (salt) {
updateObject.salt = salt;
}
if (password) {
updateObject.password = password;
}
return await runQuery({
label: "UPDATE_USER_BY_ID",
queryFn: async (DB) => {

View File

@ -12,6 +12,7 @@ const initCORS = MW.init(MW.CORS);
export default async (req, res) => {
initCORS(req, res);
// NOTE(jim): We don't need to validate here.
if (Strings.isEmpty(req.body.data.username)) {
return res.status(500).send({ decorator: "SERVER_SIGN_IN", error: true });
}
@ -46,6 +47,9 @@ export default async (req, res) => {
Environment.LOCAL_PASSWORD_SECRET
);
console.log(phaseThree);
console.log(user.password);
if (phaseThree !== user.password) {
return res
.status(403)

View File

@ -1,8 +1,8 @@
import * as Environment from "~/node_common/environment";
import * as MW from "~/node_common/middleware";
import * as Data from "~/node_common/data";
import * as Strings from "~/common/strings";
import * as Environment from "~/node_common/environment";
import * as Utilities from "~/node_common/utilities";
import * as Validations from "~/common/validations";
import PG from "~/node_common/powergate";
import JWT from "jsonwebtoken";
@ -25,12 +25,16 @@ export default async (req, res) => {
.json({ decorator: "SERVER_EXISTING_USER_ALREADY", error: true });
}
if (Strings.isEmpty(req.body.data.username)) {
return res.status(500).send({ error: "A username was not provided." });
if (!Validations.username(req.body.data.username)) {
return res
.status(500)
.send({ decorator: "SERVER_INVALID_USERNAME", error: true });
}
if (Strings.isEmpty(req.body.data.password)) {
return res.status(500).send({ error: "A password was not provided." });
if (!Validations.password(req.body.data.password)) {
return res
.status(500)
.send({ decorator: "SERVER_INVALID_PASSWORD", error: true });
}
// TODO(jim): Do not expose how many times you are salting

View File

@ -2,7 +2,6 @@ import * as Environment from "~/node_common/environment";
import * as MW from "~/node_common/middleware";
import * as Data from "~/node_common/data";
import * as Utilities from "~/node_common/utilities";
import * as Strings from "~/common/strings";
import { Buckets } from "@textile/hub";
import { Libp2pCryptoIdentity } from "@textile/threads-core";

View File

@ -1,10 +1,12 @@
import * as Environment from "~/node_common/environment";
import * as MW from "~/node_common/middleware";
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 DB from "~/node_common/database";
import PG from "~/node_common/powergate";
import BCrypt from "bcrypt";
const initCORS = MW.init(MW.CORS);
const initAuth = MW.init(MW.RequireCookieAuthentication);
@ -26,13 +28,13 @@ export default async (req, res) => {
if (!user) {
return res
.status(200)
.status(500)
.json({ decorator: "SERVER_USER_UPDATE", error: true });
}
if (user.error) {
return res
.status(200)
.status(500)
.json({ decorator: "SERVER_USER_UPDATE", error: true });
}
@ -56,6 +58,27 @@ export default async (req, res) => {
}
}
// TODO(jim): Do not expose how many times you are salting
// in OSS, add a random value as an environment variable.
if (req.body.type == "CHANGE_PASSWORD") {
if (!Validations.password(req.body.password)) {
return res
.status(500)
.json({ decorator: "SERVER_INVALID_PASSWORD", error: true });
}
const salt = await BCrypt.genSalt(13);
const hash = await BCrypt.hash(req.body.password, salt);
const double = await BCrypt.hash(hash, salt);
const triple = await BCrypt.hash(double, Environment.LOCAL_PASSWORD_SECRET);
await Data.updateUserById({
id: user.id,
salt,
password: triple,
});
}
// TODO(jim): POWERGATE_ISSUE 0.2.0
// Should work when our hosted Powergate works.
if (req.body.type === "SET_DEFAULT_STORAGE_CONFIG") {

View File

@ -1,6 +1,7 @@
import * as React from "react";
import * as System from "~/components/system";
import * as Actions from "~/common/actions";
import * as Validations from "~/common/validations";
import { css } from "@emotion/react";
@ -25,9 +26,18 @@ const delay = (time) =>
);
export default class SceneEditAccount extends React.Component {
state = { username: this.props.viewer.username, deleting: false };
state = {
username: this.props.viewer.username,
password: "",
confirm: "",
deleting: false,
changingPassword: false,
changingUsername: false,
changingAvatar: false,
};
_handleUpload = async (e) => {
this.setState({ changingAvatar: true });
e.persist();
let file = e.target.files[0];
@ -53,15 +63,48 @@ export default class SceneEditAccount extends React.Component {
data: { photo: `https://hub.textile.io${json.data.ipfs}` },
});
this.props.onRehydrate();
await this.props.onRehydrate();
this.setState({ changingAvatar: false });
};
_handleSave = async (e) => {
this.setState({ changingUsername: true });
if (!Validations.username(this.state.username)) {
alert("TODO: Not a valid username");
this.setState({ changingUsername: false });
return;
}
await Actions.updateViewer({
username: this.state.username,
});
this.props.onRehydrate();
await this.props.onRehydrate();
this.setState({ changingUsername: false });
};
_handleChangePassword = async (e) => {
this.setState({ changingPassword: true });
if (this.state.password !== this.state.confirm) {
alert("TODO: Error message for non-matching passwords");
this.setState({ changingPassword: false });
return;
}
if (!Validations.password(this.state.password)) {
alert("TODO: Not a valid password");
this.setState({ changingPassword: false });
return;
}
await Actions.updateViewer({
type: "CHANGE_PASSWORD",
password: this.state.password,
});
this.setState({ changingPassword: false, password: "", confirm: "" });
};
_handleDelete = async (e) => {
@ -110,8 +153,9 @@ export default class SceneEditAccount extends React.Component {
style={{ margin: "0 16px 16px 0" }}
type="label"
htmlFor="file"
loading={this.state.changingAvatar}
>
Upload
Pick avatar
</System.ButtonPrimary>
</div>
@ -134,8 +178,46 @@ export default class SceneEditAccount extends React.Component {
/>
<div style={{ marginTop: 24 }}>
<System.ButtonPrimary onClick={this._handleSave}>
Save
<System.ButtonPrimary
onClick={this._handleSave}
loading={this.state.changingUsername}
>
Change username
</System.ButtonPrimary>
</div>
<System.DescriptionGroup
style={{ marginTop: 48 }}
label="Reset password"
description="Your new password must be a minimum of four characters."
/>
<System.Input
containerStyle={{ marginTop: 24 }}
label="New password"
name="password"
type="password"
value={this.state.password}
placeholder="Your new password"
onChange={this._handleChange}
/>
<System.Input
containerStyle={{ marginTop: 24 }}
label="Confirm password"
name="confirm"
type="password"
value={this.state.confirm}
placeholder="Confirm it!"
onChange={this._handleChange}
/>
<div style={{ marginTop: 24 }}>
<System.ButtonPrimary
onClick={this._handleChangePassword}
loading={this.state.changingPassword}
>
Change password
</System.ButtonPrimary>
</div>

View File

@ -2,7 +2,7 @@ import * as React from "react";
import * as Actions from "~/common/actions";
import * as System from "~/components/system";
import * as Constants from "~/common/constants";
import * as Strings from "~/common/strings";
import * as Validations from "~/common/validations";
import { css } from "@emotion/react";
@ -76,16 +76,21 @@ export default class SceneSignIn extends React.Component {
// TODO(jim):
// Lets add some proper error messages here.
if (Strings.isEmpty(this.state.username)) {
alert("TODO: No username");
if (!Validations.username(this.state.username)) {
alert(
"TODO: Your username was invalid, only characters and numbers allowed."
);
this.setState({ loading: false });
return;
}
if (Strings.isEmpty(this.state.password)) {
alert("TODO: No password");
this.setState({ loading: false });
return;
if (!Validations.password(this.state.password)) {
alert("TODO: Your password must be at least 8 characters.");
// TODO(jim):
// Let it slide because this rule is new.
// this.setState({ loading: false });
// return;
}
const response = await this.props.onAuthenticate({
@ -117,7 +122,8 @@ export default class SceneSignIn extends React.Component {
Version {Constants.values.version}
<br />
Public Test Preview <br />
Warning: Entire Network/Database Will Be Wiped
Warning: THE Entire Network & Database Will Be Wiped
<br />
</div>
<System.Input
@ -126,6 +132,10 @@ export default class SceneSignIn extends React.Component {
value={this.state.username}
onChange={this._handleChange}
/>
<div css={STYLES_CODE_PREVIEW} style={{ marginTop: 8 }}>
Usernames should only have characters or numbers.
</div>
<System.Input
containerStyle={{ marginTop: 24 }}
label="Password"
@ -134,6 +144,10 @@ export default class SceneSignIn extends React.Component {
value={this.state.password}
onChange={this._handleChange}
/>
<div css={STYLES_CODE_PREVIEW} style={{ marginTop: 8 }}>
Password should be at least 8 characters
</div>
<System.ButtonPrimaryFull
style={{ marginTop: 48 }}
onClick={!this.state.loading ? this._handleSubmit : () => {}}