move login and account to use the same auth component

This commit is contained in:
Víctor Losada Hernández 2024-12-09 13:55:06 +01:00
parent 37e2f7d7a5
commit e7ca0615a3
7 changed files with 345 additions and 583 deletions

View File

@ -40,6 +40,11 @@ const AccountActions = {
});
},
validateUsername: (username) => {
const regex = /^(?!.*@).{3,}$/;
return regex.test(username);
},
checkUsername: (username) => {
return new Promise((resolve, reject) => {
request
@ -51,26 +56,37 @@ const AccountActions = {
});
});
},
rename: (username, newUsername) => {
//.put(`http://localhost:8000/api/user/rename`) for local purposes
return new Promise((resolve, reject) => {
request
.put('/rename')
.send({ username, newUsername })
.end((err, res) => {
if (err) return reject(err);
rename: (username, newUsername, password) => {
console.log('attempting rename');
return AccountActions.login(username, password)
.then(() => {
return new Promise((resolve, reject) => {
request
.put(`https://homebrewery.naturalcrit.com/api/user/rename`)
.set('Homebrewery-Version', '3.16.1')
.put('/rename')
.send({ username, newUsername })
.end((err, res) => {
if (err) return reject(err);
return resolve(res.body);
console.log('correctly renamed, now relogging');
AccountActions.removeSession();
AccountActions.login(newUsername, password).then(() => {
setTimeout(() => {
window.location.reload();
}, 500);
});
request
.put('https://homebrewery.naturalcrit.com/api/user/rename')
.set('Homebrewery-Version', '3.16.1')
.send({ username, newUsername })
.end((err, res) => {
if (err) return reject(err);
return resolve(res.body);
});
});
});
});
})
.catch((err) => {
return Promise.reject(err);
});
},
createSession: (token) => {

View File

@ -1,32 +1,58 @@
const React = require('react');
const AccountActions = require('../account.actions.js');
const NaturalCritIcon = require('naturalcrit/components/naturalcritLogo.jsx');
const RenameForm = require('../loginPage/renameForm.jsx');
const AuthForm = require('../loginPage/authForm.jsx'); // Import AuthForm
class AccountPage extends React.Component {
constructor(props) {
super(props);
this.state = {
showLogin: false
showRenameForm: false,
processing: false,
errors: null,
};
this.toggleLogin = this.toggleLogin.bind(this);
this.handleRenameSuccess = this.handleRenameSuccess.bind(this); // Bind method
this.handleRename = this.handleRename.bind(this); // Bind handleRename here
this.toggleRenameForm = this.toggleRenameForm.bind(this); // Bind the method here
}
toggleLogin() {
this.setState({ showLogin: !this.state.showLogin });
toggleRenameForm() {
this.setState({ showRenameForm: !this.state.showRenameForm });
}
handleRenameSuccess(newUsername, password) {
console.log('handling rename, ', newUsername, password);
AccountActions.removeSession();
AccountActions.login(newUsername, password).then(() => {
this.setState({ showLogin: false });
window.location.reload();
}).catch(error => {
console.error('Login failed', error);
handleRename(newUsername, password) {
const regex = /^(?!.*@).{3,}$/;
if (!regex.test(newUsername)) {
this.setState({
processing: false,
errors: { msg: 'Username must be at least 3 characters long and not include @!?.' },
});
return Promise.reject('Invalid username');
}
//if (!confirm('Are you sure you want to rename your account?')) return Promise.reject('User canceled rename');
this.setState({
processing: true,
errors: null,
});
return AccountActions.rename(this.props.user.username, newUsername, password)
.then(() => {
this.setState({
processing: false,
errors: null,
showRenameForm: false,
});
})
.catch((err) => {
console.log(err);
this.setState({
processing: false,
errors: err,
});
return Promise.reject(err);
});
}
render() {
@ -51,17 +77,12 @@ class AccountPage extends React.Component {
}}>
Log Out
</button>
<button className="rename" onClick={this.toggleLogin}>
<button className="rename" onClick={this.toggleRenameForm}>
Change my username
</button>
<br />
<br />
{this.state.showLogin && (
<RenameForm
user={this.props.user}
onRenameSuccess={this.handleRenameSuccess}
/>
)}
{this.state.showRenameForm && <AuthForm actionType="rename" onSubmit={this.handleRename} />}
<small>Upcoming features will include account deletion and username changes.</small>
</div>
</div>

View File

@ -0,0 +1,184 @@
const React = require('react');
const cx = require('classnames');
const _ = require('lodash');
const AccountActions = require('../account.actions');
const AuthForm = React.createClass({
getDefaultProps: function () {
return {
onSubmit: () => Promise.resolve(),
user: null,
actionType: 'login', // 'login', 'signup', or 'rename'
};
},
getInitialState: function () {
return {
visible: false,
username: this.props.user && this.props.user.username ? this.props.user.username : '',
password: '',
processing: false,
checkingUsername: false,
usernameExists: false,
errors: null,
};
},
componentDidMount: function () {
console.log('mounting authform');
window.document.addEventListener('keydown', this.handleKeyDown);
},
componentWillUnmount: function () {
window.document.removeEventListener('keydown', this.handleKeyDown);
},
handleKeyDown: function (e) {
if (e.code === 'Enter') this.handleSubmit();
},
handleInputChange: function (field) {
return (e) => {
this.setState({ [field]: e.target.value }, () => {
if (field === 'username') this.checkUsername();
});
};
},
checkUsername: function () {
if (this.state.username === '') return;
this.setState({
checkingUsername: true,
});
this.debounceCheckUsername(this.state.username);
},
debounceCheckUsername: _.debounce(function () {
AccountActions.checkUsername(this.state.username).then((doesExist) => {
this.setState({
usernameExists: !!doesExist,
checkingUsername: false,
});
});
}, 1000),
isValid: function () {
const { username, password, usernameExists, processing } = this.state;
const { actionType } = this.props;
if (processing) return false;
if (actionType === 'login') return username && password;
if (actionType === 'signup' || actionType === 'rename') return username && password && !usernameExists;
return false;
},
handleSubmit: function () {
const { username, password } = this.state;
const { actionType, onSubmit } = this.props;
if (!this.isValid()) return;
this.setState({ processing: true, errors: null });
onSubmit(username, password, actionType)
.then(() => this.setState({ processing: false, errors: null }))
.catch((err) => {
console.error(err);
this.setState({
processing: false,
errors: err,
});
});
},
renderErrors: function () {
const { errors } = this.state;
if (!errors) return null;
return <div className="errors">{errors.msg || 'Something went wrong'}</div>;
},
renderUsernameValidation: function () {
const { checkingUsername, usernameExists, username } = this.state;
if (!username) return null;
let icon;
if (checkingUsername) icon = <i className="fa fa-spinner fa-spin" />;
else if (usernameExists) icon = <i className="fa fa-times red" />;
else icon = <i className="fa fa-check green" />;
return <div className="control">{icon}</div>;
},
renderButton: function () {
let className = '';
let text = '';
let icon = '';
if (this.state.processing) {
className = 'processing';
text = 'processing';
icon = 'fa-spinner fa-spin';
} else if (this.props.actionType === 'login') {
className = 'login';
text = 'login';
icon = 'fa-sign-in';
} else if (this.props.actionType === 'signup') {
className = 'signup';
text = 'signup';
icon = 'fa-user-plus';
} else if (this.props.actionType === 'rename') {
className = 'rename';
text = 'rename';
icon = 'fa-user-plus';
}
return (
<button className={cx('action', className)} disabled={!this.isValid()} onClick={this.handleSubmit}>
<i className={`fa ${icon}`} />
{text}
</button>
);
},
render: function () {
const { actionType } = this.props;
const { visible, username, password, processing } = this.state;
let buttonText;
if (processing) buttonText = 'Processing...';
else if (actionType === 'login') buttonText = 'Login';
else if (actionType === 'signup') buttonText = 'Signup';
else buttonText = 'Rename';
return (
<div className={`authForm ${actionType}`}>
<label className="field user">
Username
<input type="text" onChange={this.handleInputChange('username')} value={username} />
{this.props.actionType !== 'login' && this.renderUsernameValidation()}
</label>
<label className="field password">
Password
<input
type={cx({ text: visible, password: !visible })}
onChange={this.handleInputChange('password')}
value={password}
/>
<div className="control" onClick={() => this.setState({ visible: !visible })}>
<i className={cx('fa', { 'fa-eye': !visible, 'fa-eye-slash': visible })} />
</div>
</label>
{this.renderErrors()}
{this.renderButton()}
</div>
);
},
});
module.exports = AuthForm;

View File

@ -1,38 +1,6 @@
.authForm {
width: 400px;
width: 100%;
margin: 0 auto;
padding: 20px;
.switchView {
width: 100%;
&>div {
.animate(background-color);
display: inline-block;
width: 50%;
padding: 10px;
cursor: pointer;
background-color: transparent;
font-size: 0.8em;
font-weight: 800;
text-transform: uppercase;
i {
vertical-align: middle;
font-size: 2em;
}
}
.login:hover,
.login.selected {
background-color: fade(@blue, 20%);
}
.signup:hover,
.signup.selected {
background-color: fade(@green, 20%);
}
}
.field {
display: block;
@ -107,7 +75,7 @@
background-color: @blue;
}
&.signup {
&.signup, &.rename {
background-color: @green;
}
@ -117,24 +85,7 @@
}
}
button.google {
cursor: pointer;
width: 191px;
height: 46px;
border: none;
outline: none;
background-color: unset;
background-image: url('../assets/naturalcrit/styles/btn_google_signin_light_normal_web.png');
background-size: contain;
&:hover {
background-image: url('../assets/naturalcrit/styles/btn_google_signin_light_hover_web.png');
}
&:focus {
background-image: url('../assets/naturalcrit/styles/btn_google_signin_light_pressed_web.png');
}
}
.errors {
margin-bottom: 20px;

View File

@ -1,9 +1,8 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const NaturalCritIcon = require('naturalcrit/components/naturalcritLogo.jsx');
const AccountActions = require('../account.actions.js');
const AuthForm = require('./authForm.jsx');
const NaturalCritIcon = require('naturalcrit/components/naturalcritLogo.jsx');
const RedirectLocation = 'NC-REDIRECT-URL';
@ -16,32 +15,11 @@ const LoginPage = React.createClass({
},
getInitialState: function () {
return {
view: 'login', //or 'signup'
visible: false,
username: '',
password: '',
processing: false,
checkingUsername: false,
view: 'login', // or 'signup'
redirecting: false,
usernameExists: false,
errors: null,
success: false,
};
},
componentDidMount: function () {
window.document.addEventListener('keydown', (e) => {
if (e.code === 'Enter') this.handleClick();
});
this.handleRedirectURL();
},
componentWillUnmount: function () {
window.document.removeEventListener('keydown', this.handleKeyPress);
},
handleRedirectURL: function () {
if (!this.props.redirect) {
@ -50,28 +28,6 @@ const LoginPage = React.createClass({
return window.sessionStorage.setItem(RedirectLocation, this.props.redirect);
},
handleUserChange: function (e) {
this.setState({ username: e.target.value });
if (this.props.user && this.props.user.username) return;
this.setState(
{
usernameExists: true,
checkingUsername: true,
},
() => {
if (this.state.view === 'signup') this.checkUsername();
}
);
},
handlePassChange: function (e) {
this.setState({ password: e.target.value });
},
handleClick: function () {
if (!this.isValid()) return;
if (this.state.view === 'login') this.login();
if (this.state.view === 'signup') this.signup();
},
redirect: function () {
if (!this.props.redirect) return (window.location = '/');
this.setState(
@ -84,109 +40,30 @@ const LoginPage = React.createClass({
);
},
login: function () {
this.setState({
processing: true,
errors: null,
});
AccountActions.login(this.state.username, this.state.password)
.then((token) => {
this.setState(
{
processing: false,
errors: null,
success: true,
},
this.redirect
);
})
.catch((err) => {
console.log(err);
this.setState({
processing: false,
errors: err,
handleLoginSignup: function (username, password, view) {
if (view === 'login') {
return AccountActions.login(username, password)
.then((token) => {
this.setState({ redirecting: true }, this.redirect);
})
.catch((err) => {
this.setState({})
console.log(err);
return Promise.reject(err);
});
});
},
logout: function (e) {
e.preventDefault();
AccountActions.removeSession();
window.location.reload();
return false;
},
signup: function () {
this.setState({
processing: true,
errors: null,
});
AccountActions.signup(this.state.username, this.state.password)
.then((token) => {
this.setState(
{
processing: false,
errors: null,
success: true,
},
this.redirect
);
})
.catch((err) => {
console.log(err);
this.setState({
processing: false,
errors: err,
} else if (view === 'signup') {
return AccountActions.signup(username, password)
.then((token) => {
this.setState({ redirecting: true }, this.redirect);
})
.catch((err) => {
console.log(err);
return Promise.reject(err);
});
});
},
checkUsername: function () {
if (this.state.username === '') return;
const regex = /^(?!.*@).{3,}$/;
if (!regex.test(this.state.username)) {
this.setState({
processing: false,
errors: { username: 'Username must be at least 3 characters long.' },
});
return;
}
this.setState({
checkingUsername: true,
});
this.debounceCheckUsername();
},
debounceCheckUsername: _.debounce(function () {
AccountActions.checkUsername(this.state.username).then((doesExist) => {
this.setState({
usernameExists: !!doesExist,
checkingUsername: false,
});
});
}, 1000),
handleChangeView: function (newView) {
this.setState(
{
view: newView,
errors: null,
},
this.checkUsername
);
},
isValid: function () {
if (this.state.processing) return false;
if (this.state.view === 'login') {
return this.state.username && this.state.password;
} else if (this.state.view === 'signup') {
return this.state.username && this.state.password && !this.state.usernameExists;
}
},
linkGoogle: function () {
if (this.props.user) {
@ -208,88 +85,14 @@ const LoginPage = React.createClass({
window.location.href = '/auth/google';
},
// loginGoogle : function(){
// this.setState({
// processing : true,
// errors : null
// });
// console.log("about to log into google!");
// AccountActions.loginGoogle();
// },
// console.log("about to start login");
// AccountActions.login(this.state.username, this.state.password)
// .then((token) => {
// this.setState({
// processing : false,
// errors : null,
// success : true
// }, this.redirect);
// })
// .catch((err) => {
// console.log(err);
// this.setState({
// processing : false,
// errors : err
// });
// });
// },
renderErrors: function () {
if (!this.state.errors) return;
if (this.state.errors.msg) return <div className="errors">{this.state.errors.msg}</div>;
return <div className="errors">Something went wrong</div>;
},
renderUsernameValidation: function () {
if (this.state.view === 'login') return;
let icon = null;
if (this.state.checkingUsername) {
icon = <i className="fa fa-spinner fa-spin" />;
} else if (!this.state.username || this.state.usernameExists) {
icon = <i className="fa fa-times red" />;
} else if (!this.state.usernameExists) {
icon = <i className="fa fa-check green" />;
}
return <div className="control">{icon}</div>;
},
renderButton: function () {
let className = '';
let text = '';
let icon = '';
if (this.state.processing) {
className = 'processing';
text = 'processing';
icon = 'fa-spinner fa-spin';
} else if (this.state.view === 'login') {
className = 'login';
text = 'login';
icon = 'fa-sign-in';
} else if (this.state.view === 'signup') {
className = 'signup';
text = 'signup';
icon = 'fa-user-plus';
}
return (
<button
className={cx('action', className)}
disabled={!this.isValid() || (this.props.user && this.props.user.username)}
onClick={this.handleClick}>
<i className={`fa ${icon}`} />
{text}
</button>
);
handleChangeView: function (newView) {
this.setState({
view: newView,
});
},
renderLoggedIn: function () {
if (!this.props.user) return;
let loggedInGoogle = '';
if (!this.props.user.googleId) {
return (
<small>
@ -312,65 +115,28 @@ const LoginPage = React.createClass({
},
render: function () {
console.log(this.props.redirect);
return (
<div className="loginPage">
<NaturalCritIcon />
<div className="authForm">
<div className="content">
<div className="switchView">
<div
className={cx('login', { 'selected': this.state.view === 'login' })}
className={cx('login', { selected: this.state.view === 'login' })}
onClick={this.handleChangeView.bind(null, 'login')}>
<i className="fa fa-sign-in" /> Login
</div>
<div
className={cx('signup', { 'selected': this.state.view === 'signup' })}
className={cx('signup', { selected: this.state.view === 'signup' })}
onClick={this.handleChangeView.bind(null, 'signup')}>
<i className="fa fa-user-plus" /> Signup
</div>
</div>
<label className="field user">
username
<input
type="text"
title={this.state.view === 'signup' ? 'Min 3 characters, and cannot contain ?!¿@ .' : ''}
onChange={this.handleUserChange}
value={this.state.username}
/>
{this.renderUsernameValidation()}
{this.state.usernameExists && !this.state.checkingUsername && this.state.view === 'signup' ? (
<div className="userExists">User with that name already exists</div>
) : null}
</label>
<label className="field password">
password
<input
type={cx({ text: this.state.visible, password: !this.state.visible })}
onChange={this.handlePassChange}
value={this.state.password}
/>
<div
className="control"
onClick={() => {
this.setState({ visible: !this.state.visible });
}}>
<i
className={cx('fa', {
'fa-eye': !this.state.visible,
'fa-eye-slash': this.state.visible,
})}
/>
</div>
</label>
{this.renderErrors()}
{this.renderButton()}
<AuthForm actionType={this.state.view} onSubmit={this.handleLoginSignup} />
<div className="divider"> OR </div>
<button className="google" onClick={this.linkGoogle}></button>
</div>
<br />
<br />
<br />

View File

@ -2,16 +2,61 @@
text-align: center;
padding-top: 30px;
.authForm {
.content {
width: 400px;
margin: 0 auto;
padding: 20px;
padding-top: 50px;
}
.divider {
font-size: 1em;
font-weight: 800;
color: black;
text-transform: uppercase;
padding: 12px 20px 10px;
.switchView {
width: 100%;
&>div {
.animate(background-color);
display: inline-block;
width: 50%;
padding: 10px;
cursor: pointer;
background-color: transparent;
font-size: 0.8em;
font-weight: 800;
text-transform: uppercase;
i {
vertical-align: middle;
font-size: 2em;
}
}
.login:hover,
.login.selected {
background-color: fade(@blue, 20%);
}
.signup:hover,
.signup.selected {
background-color: fade(@green, 20%);
}
}
button.google {
cursor: pointer;
width: 191px;
height: 46px;
border: none;
outline: none;
background-color: unset;
background-image: url('../assets/naturalcrit/styles/btn_google_signin_light_normal_web.png');
background-size: contain;
&:hover {
background-image: url('../assets/naturalcrit/styles/btn_google_signin_light_hover_web.png');
}
&:focus {
background-image: url('../assets/naturalcrit/styles/btn_google_signin_light_pressed_web.png');
}
}
}
+.accountButton.login {

View File

@ -1,221 +0,0 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const NaturalCritIcon = require('naturalcrit/components/naturalcritLogo.jsx');
const AccountActions = require('../account.actions.js');
const RedirectLocation = 'NC-REDIRECT-URL';
const RenameForm = React.createClass({
getDefaultProps: function () {
return {
user: null,
onRenameSuccess: () => {},
};
},
getInitialState: function () {
return {
visible: false,
newUsername: '',
password: '',
processing: false,
checkingUsername: false,
redirecting: false,
usernameExists: false,
errors: null,
success: false,
};
},
componentDidMount: function () {
window.document.addEventListener('keydown', (e) => {
if (e.code === 'Enter') this.login();
});
},
componentWillUnmount: function () {
window.document.removeEventListener('keydown', this.handleKeyPress);
},
handleUserChange: function (e) {
this.setState({ username: e.target.value });
this.setState(
{
usernameExists: true,
checkingUsername: true,
},
() => {
if (this.state.view === 'signup') this.checkUsername();
}
);
},
handleNewUserChange: function (e) {
this.setState({ username: this.props.user.username, newUsername: e.target.value });
this.setState({
usernameExists: true,
checkingUsername: true,
});
this.checkUsername();
},
handlePassChange: function (e) {
this.setState({ password: e.target.value });
},
login: function () {
const regex = /^(?!.*@).{3,}$/;
if (!regex.test(this.state.newUsername)) {
this.setState({
processing: false,
errors: { msg: 'Username must be at least 3 characters long and not include @!?.' },
});
return;
}
if (!confirm('Are you sure you want to rename your account?')) return;
this.setState({
processing: true,
errors: null,
});
AccountActions.login(this.props.user.username, this.state.password)
.then((token) => {
this.setState({
processing: false,
errors: null,
success: true,
});
//check if username exists
if (this.state.newUsername) {
AccountActions.rename(this.props.user.username, this.state.newUsername)
.then((res) => {
this.props.onRenameSuccess(this.state.newUsername, this.state.password);
})
.catch((err) => {
console.log(err);
this.setState({
processing: false,
errors: err,
});
});
}
})
.catch((err) => {
console.log(err);
this.setState({
processing: false,
errors: err,
});
});
},
checkUsername: function () {
if (this.state.newUsername === '') return;
this.setState({
checkingUsername: true,
});
this.debounceCheckUsername(this.state.newUsername);
},
debounceCheckUsername: _.debounce(function () {
AccountActions.checkUsername(this.state.newUsername).then((doesExist) => {
this.setState({
usernameExists: !!doesExist,
checkingUsername: false,
});
});
}, 1000),
isValid: function () {
if (this.state.processing) return false;
return this.state.newUsername && this.state.password && !this.state.usernameExists;
},
renderErrors: function () {
if (!this.state.errors) return;
if (this.state.errors.msg) return <div className="errors">{this.state.errors.msg}</div>;
return <div className="errors">Something went wrong</div>;
},
renderUsernameValidation: function () {
let icon = null;
if (this.state.checkingUsername) {
icon = <i className="fa fa-spinner fa-spin" />;
} else if (!this.state.newUsername || this.state.usernameExists) {
icon = <i className="fa fa-times red" />;
} else if (!this.state.usernameExists) {
icon = <i className="fa fa-check green" />;
}
return <div className="control">{icon}</div>;
},
renderButton: function () {
let className = '';
let text = '';
let icon = '';
if (this.state.processing) {
className = 'processing';
text = 'processing';
icon = 'fa-spinner fa-spin';
} else {
className = 'signup';
text = 'Rename';
icon = 'fa-signature';
}
return (
<button className={cx('action', className)} disabled={!this.isValid()} onClick={this.login}>
<i className={`fa ${icon}`} />
{text}
</button>
);
},
render: function () {
return (
<div className="authForm">
<label className="field user">
new username
<input type="text" onChange={this.handleNewUserChange} value={this.state.newUsername} />
{this.renderUsernameValidation()}
{this.state.usernameExists && !this.state.checkingUsername ? (
<div className="userExists">User with that name already exists</div>
) : null}
</label>
<label className="field password">
password
<input
type={cx({ text: this.state.visible, password: !this.state.visible })}
onChange={this.handlePassChange}
value={this.state.password}
/>
<div
className="control"
onClick={() => {
this.setState({ visible: !this.state.visible });
}}>
<i
className={cx('fa', {
'fa-eye': !this.state.visible,
'fa-eye-slash': this.state.visible,
})}
/>
</div>
</label>
{this.renderErrors()}
{this.renderButton()}
</div>
);
},
});
module.exports = RenameForm;