* Buttons for oauth services

* Option for "NO_PASSWORD" changed to "EMAIL_ONLY_NO_PASSWORD"
* Added new options to accounts-password "USERNAME_AND_EMAIL_NO_PASSWORD".
This commit is contained in:
Tim Brandin 2016-03-31 15:57:28 +02:00 committed by maz-dev
parent b0c43edcb6
commit 19864ef983
12 changed files with 191 additions and 49 deletions

View file

@ -1,5 +1,11 @@
# ChangeLog # ChangeLog
### v1.0.21
* Buttons for oauth services
* Option for "NO_PASSWORD" changed to "EMAIL_ONLY_NO_PASSWORD"
* Added new options to accounts-password "USERNAME_AND_EMAIL_NO_PASSWORD".
### v1.0.20 ### v1.0.20
* Clear the password when logging in or out. * Clear the password when logging in or out.

View file

@ -1,6 +1,6 @@
# React Accounts UI # React Accounts UI
Current version 1.0.20 Current version 1.0.21
## Features ## Features
@ -48,7 +48,7 @@ Configure the behavior of `<Accounts.ui.LoginForm />`
If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google. If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google.
* **passwordSignupFields**&nbsp;&nbsp;&nbsp; String * **passwordSignupFields**&nbsp;&nbsp;&nbsp; String
Which fields to display in the user creation form. One of `'USERNAME_AND_EMAIL'`, `'USERNAME_AND_OPTIONAL_EMAIL'`, `'USERNAME_ONLY'`, or `'EMAIL_ONLY'`, **`'NO_PASSWORD'`** (**default**). Which fields to display in the user creation form. One of `'USERNAME_AND_EMAIL'`, `'USERNAME_AND_OPTIONAL_EMAIL'`, `'USERNAME_ONLY'`, `'EMAIL_ONLY'`, `'USERNAME_AND_EMAIL_NO_PASSWORD'`, **`'EMAIL_ONLY_NO_PASSWORD'`** (**default**).
* **loginPath**&nbsp;&nbsp;&nbsp; String * **loginPath**&nbsp;&nbsp;&nbsp; String
Change the default path a user should be redirected to after a clicking a link in a mail provided to them from the accounts system, it could be a mail set to them when they have reset their password, verifying their email if the setting for `sendVerificationEmail` is turned on ([read more on accounts configuration ](http://docs.meteor.com/#/full/accounts_config)). Change the default path a user should be redirected to after a clicking a link in a mail provided to them from the accounts system, it could be a mail set to them when they have reset their password, verifying their email if the setting for `sendVerificationEmail` is turned on ([read more on accounts configuration ](http://docs.meteor.com/#/full/accounts_config)).

View file

@ -68,7 +68,8 @@ Accounts.ui.config = function(options) {
"USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_ONLY", "USERNAME_ONLY",
"EMAIL_ONLY", "EMAIL_ONLY",
"NO_PASSWORD" "EMAIL_ONLY_NO_PASSWORD",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], options.passwordSignupFields)) { ], options.passwordSignupFields)) {
Accounts.ui._options.passwordSignupFields = options.passwordSignupFields; Accounts.ui._options.passwordSignupFields = options.passwordSignupFields;
} }

View file

@ -7,14 +7,30 @@ import { Accounts } from 'meteor/accounts-base';
// Method called by a user to request a password reset email. This is // Method called by a user to request a password reset email. This is
// the start of the reset process. // the start of the reset process.
Meteor.methods({loginWithoutPassword: function (options) { Meteor.methods({loginWithoutPassword: function ({ email, username = null }) {
check(options, {email: String}); if (username !== null) {
check(username, String);
var user = Meteor.users.findOne({"emails.address": options.email}); var user = Meteor.users.findOne({ $or: [{
if (!user) "username": username, "emails.address": { $exists: 1 }
throw new Meteor.Error(403, "User not found"); }, {
"emails.address": email
}]
});
if (!user)
throw new Meteor.Error(403, "User not found");
Accounts.sendLoginEmail(user._id, options.email); email = user.emails[0].address;
}
else {
check(email, String);
var user = Meteor.users.findOne({ "emails.address": email });
if (!user)
throw new Meteor.Error(403, "User not found");
}
Accounts.sendLoginEmail(user._id, email);
}}); }});
/** /**
@ -84,17 +100,20 @@ Accounts.sendLoginEmail = function (userId, address) {
Email.send(options); Email.send(options);
}; };
Accounts.emailTemplates.loginNoPassword = { // Check for installed accounts-password dependency.
subject: function(user) { if (Accounts.emailTemplates) {
return "Login on " + Accounts.emailTemplates.siteName; Accounts.emailTemplates.loginNoPassword = {
}, subject: function(user) {
text: function(user, url) { return "Login on " + Accounts.emailTemplates.siteName;
var greeting = (user.profile && user.profile.name) ? },
("Hello " + user.profile.name + ",") : "Hello,"; text: function(user, url) {
return `${greeting} var greeting = (user.profile && user.profile.name) ?
("Hello " + user.profile.name + ",") : "Hello,";
return `${greeting}
To login, simply click the link below. To login, simply click the link below.
${url} ${url}
Thanks. Thanks.
`; `;
} }
}; };
}

View file

@ -22,6 +22,11 @@ export function getLoginServices() {
// requires it. // requires it.
this.getLoginServices = getLoginServices; this.getLoginServices = getLoginServices;
export function hasPasswordService() {
// First look for OAuth services.
return !!Package['accounts-password'];
};
export function loginResultCallback(redirect, error) { export function loginResultCallback(redirect, error) {
if (Meteor.isClient) { if (Meteor.isClient) {
if (typeof redirect === 'string'){ if (typeof redirect === 'string'){
@ -35,7 +40,7 @@ export function loginResultCallback(redirect, error) {
}; };
export function passwordSignupFields() { export function passwordSignupFields() {
return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY"; return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY_NO_PASSWORD";
}; };
export function validatePassword(password){ export function validatePassword(password){
@ -63,3 +68,9 @@ export function redirect(redirect) {
} }
} }
} }
export function capitalize(string) {
return string.replace(/\-/, ' ').split(' ').map(word => {
return word.charAt(0).toUpperCase() + word.slice(1);
}).join(' ');
}

View file

@ -4,5 +4,11 @@ T9n.map('en', {
'Enter newPassword': 'Enter new password', 'Enter newPassword': 'Enter new password',
'Enter email': 'Enter email', 'Enter email': 'Enter email',
'Enter username': 'Enter username', 'Enter username': 'Enter username',
'Enter username or email': 'Enter username or email' 'Enter username or email': 'Enter username or email',
error: {
accounts: {
"Invalid email or username": "Invalid email or username"
}
},
'or use': 'Or use'
}); });

View file

@ -4,5 +4,11 @@ T9n.map('sv', {
'Enter newPassword': 'Mata in nytt lösenord', 'Enter newPassword': 'Mata in nytt lösenord',
'Enter email': 'Mata in e-post', 'Enter email': 'Mata in e-post',
'Enter username': 'Mata in användarnamn', 'Enter username': 'Mata in användarnamn',
'Enter username or email': 'Mata in användarnamn eller e-post' 'Enter username or email': 'Mata in användarnamn eller e-post',
error: {
accounts: {
"Invalid email or username": "Ogiltig e-postadress eller användarnamn"
}
},
'or use': 'Eller använd'
}); });

View file

@ -1,20 +1,31 @@
import React from 'react'; import React from 'react';
import {Accounts} from 'meteor/accounts-base'; import {Accounts} from 'meteor/accounts-base';
import './SocialButtons.jsx';
import './Fields.jsx'; import './Fields.jsx';
import './Buttons.jsx'; import './Buttons.jsx';
import './FormMessage.jsx'; import './FormMessage.jsx';
import './PasswordOrService.jsx';
import './SocialButtons.jsx';
export class Form extends React.Component { export class Form extends React.Component {
render() { render() {
const { oauthServices, fields, buttons, error, message, ready = true, className } = this.props; const {
hasPasswordService,
oauthServices,
fields,
buttons,
error,
message,
ready = true,
className
} = this.props;
return ( return (
<form className={[className, ready ? "ready" : null].join(' ')} <form className={[className, ready ? "ready" : null].join(' ')}
onSubmit={ evt => evt.preventDefault() } className="accounts-ui"> onSubmit={ evt => evt.preventDefault() } className="accounts-ui">
<Accounts.ui.SocialButtons oauthServices={ oauthServices } />
<Accounts.ui.Fields fields={ fields } /> <Accounts.ui.Fields fields={ fields } />
<Accounts.ui.Buttons buttons={ buttons } /> <Accounts.ui.Buttons buttons={ buttons } />
<Accounts.ui.FormMessage message={ message } /> <Accounts.ui.PasswordOrService oauthServices={ oauthServices } />
<Accounts.ui.SocialButtons oauthServices={ oauthServices } />
<Accounts.ui.FormMessage {...message} />
</form> </form>
); );
} }

View file

@ -10,7 +10,9 @@ import {
passwordSignupFields, passwordSignupFields,
validatePassword, validatePassword,
loginResultCallback, loginResultCallback,
getLoginServices getLoginServices,
hasPasswordService,
capitalize
} from '../../helpers.js'; } from '../../helpers.js';
export class LoginForm extends Tracker.Component { export class LoginForm extends Tracker.Component {
@ -155,9 +157,12 @@ export class LoginForm extends Tracker.Component {
const loginFields = []; const loginFields = [];
const { formState } = this.state; const { formState } = this.state;
if (formState == STATES.SIGN_IN) { if (hasPasswordService() && formState == STATES.SIGN_IN) {
if (_.contains(["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"], if (_.contains([
passwordSignupFields())) { "USERNAME_AND_EMAIL",
"USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getUsernameOrEmailField()); loginFields.push(this.getUsernameOrEmailField());
} }
@ -165,30 +170,48 @@ export class LoginForm extends Tracker.Component {
loginFields.push(this.getUsernameField()); loginFields.push(this.getUsernameField());
} }
if (_.contains(["EMAIL_ONLY", "NO_PASSWORD"], passwordSignupFields())) { if (_.contains([
"EMAIL_ONLY",
"EMAIL_ONLY_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getEmailField()); loginFields.push(this.getEmailField());
} }
if (passwordSignupFields() !== "NO_PASSWORD") { if (!_.contains([
"EMAIL_ONLY_NO_PASSWORD",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getPasswordField()); loginFields.push(this.getPasswordField());
} }
} }
if (formState == STATES.SIGN_UP) { if (hasPasswordService() && formState == STATES.SIGN_UP) {
if (_.contains(["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], if (_.contains([
passwordSignupFields())) { "USERNAME_AND_EMAIL",
"USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_ONLY",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getUsernameField()); loginFields.push(this.getUsernameField());
} }
if (_.contains(["USERNAME_AND_EMAIL", "EMAIL_ONLY", "NO_PASSWORD"], passwordSignupFields())) { if (_.contains([
loginFields.push(Object.assign(this.getEmailField())); "USERNAME_AND_EMAIL",
"EMAIL_ONLY",
"EMAIL_ONLY_NO_PASSWORD",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getEmailField());
} }
if (passwordSignupFields() !== "USERNAME_AND_OPTIONAL_EMAIL") { if (_.contains(["USERNAME_AND_OPTIONAL_EMAIL"], passwordSignupFields())) {
loginFields.push(Object.assign(this.getEmailField(), {required: false})); loginFields.push(Object.assign(this.getEmailField(), {required: false}));
} }
if (passwordSignupFields() !== "NO_PASSWORD") { if (!_.contains([
"EMAIL_ONLY_NO_PASSWORD",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getPasswordField()); loginFields.push(this.getPasswordField());
} }
} }
@ -261,9 +284,10 @@ export class LoginForm extends Tracker.Component {
loginButtons.push({ loginButtons.push({
id: 'signUp', id: 'signUp',
label: T9n.get('signUp'), label: T9n.get('signUp'),
type: 'submit', type: hasPasswordService() ? 'submit' : 'link',
className: 'active',
disabled: waiting, disabled: waiting,
onClick: this.signUp.bind(this) onClick: hasPasswordService() ? this.signUp.bind(this) : null
}); });
} }
@ -271,9 +295,10 @@ export class LoginForm extends Tracker.Component {
loginButtons.push({ loginButtons.push({
id: 'signIn', id: 'signIn',
label: T9n.get('signIn'), label: T9n.get('signIn'),
type: 'submit', type: hasPasswordService() ? 'submit' : 'link',
className: 'active',
disabled: waiting, disabled: waiting,
onClick: this.signIn.bind(this) onClick: hasPasswordService() ? this.signIn.bind(this) : null
}); });
} }
@ -306,6 +331,8 @@ export class LoginForm extends Tracker.Component {
// Sort the button array so that the submit button always comes first. // Sort the button array so that the submit button always comes first.
loginButtons.sort((a, b) => { loginButtons.sort((a, b) => {
return a.label.localeCompare(b.label);
}).sort((a, b) => {
return (b.type == 'submit') - (a.type == 'submit'); return (b.type == 'submit') - (a.type == 'submit');
}); });
@ -383,7 +410,7 @@ export class LoginForm extends Tracker.Component {
return; return;
} }
else { else {
if (passwordSignupFields() === "NO_PASSWORD") { if (_.contains([ "EMAIL_ONLY_NO_PASSWORD" ], passwordSignupFields())) {
this.loginWithoutPassword(); this.loginWithoutPassword();
return; return;
} }
@ -397,6 +424,10 @@ export class LoginForm extends Tracker.Component {
return; return;
} }
else { else {
if (_.contains([ "USERNAME_AND_EMAIL_NO_PASSWORD" ], passwordSignupFields())) {
this.loginWithoutPassword();
return;
}
loginSelector = usernameOrEmail; loginSelector = usernameOrEmail;
} }
} }
@ -425,7 +456,7 @@ export class LoginForm extends Tracker.Component {
Accounts.oauth.serviceNames().map((service) => { Accounts.oauth.serviceNames().map((service) => {
oauthButtons.push({ oauthButtons.push({
id: service, id: service,
label: service, label: capitalize(service),
disabled: waiting, disabled: waiting,
type: 'submit', type: 'submit',
onClick: this.oauthSignIn.bind(this, service) onClick: this.oauthSignIn.bind(this, service)
@ -435,6 +466,7 @@ export class LoginForm extends Tracker.Component {
} }
return _.indexBy(oauthButtons, 'id'); return _.indexBy(oauthButtons, 'id');
} }
oauthSignIn(service) { oauthSignIn(service) {
const { formState, waiting, user } = this.state; const { formState, waiting, user } = this.state;
//Thanks Josh Owens for this one. //Thanks Josh Owens for this one.
@ -482,7 +514,10 @@ export class LoginForm extends Tracker.Component {
} }
} }
if (passwordSignupFields() === "NO_PASSWORD") { if (_.contains([
"EMAIL_ONLY_NO_PASSWORD",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
// Generate a random password. // Generate a random password.
options.password = Meteor.uuid(); options.password = Meteor.uuid();
} }
@ -534,6 +569,7 @@ export class LoginForm extends Tracker.Component {
loginWithoutPassword(){ loginWithoutPassword(){
const { const {
email = '', email = '',
usernameOrEmail = '',
waiting waiting
} = this.state; } = this.state;
@ -555,8 +591,27 @@ export class LoginForm extends Tracker.Component {
this.setState({ waiting: false }); this.setState({ waiting: false });
}); });
} }
else if (this.validateUsername(usernameOrEmail)) {
this.setState({ waiting: true });
Accounts.loginWithoutPassword({ email: usernameOrEmail, username: usernameOrEmail }, (error) => {
if (error) {
this.showMessage(T9n.get(`error.accounts.${error.reason}`) || T9n.get("Unknown error"), 'error');
}
else {
this.showMessage(T9n.get("info.emailSent"), 'success', 5000);
}
this.setState({ waiting: false });
});
}
else { else {
this.showMessage(T9n.get("error.accounts.Invalid email"), 'warning'); if (_.contains([ "USERNAME_AND_EMAIL_NO_PASSWORD" ], passwordSignupFields())) {
this.showMessage(T9n.get("error.accounts.Invalid email or username"), 'warning');
}
else {
this.showMessage(T9n.get("error.accounts.Invalid email"), 'warning');
}
} }
} }

View file

@ -0,0 +1,28 @@
import React from 'react';
import { Accounts } from 'meteor/accounts-base';
import { T9n } from 'meteor/softwarerero:accounts-t9n';
export class PasswordOrService extends React.Component {
render () {
let {
oauthServices = {},
className,
style = {}
} = this.props;
let labels = Object.keys(oauthServices).map(service => oauthServices[service].label);
if (labels.length > 2) {
labels = [];
}
if (labels.length) {
return (
<div style={ style } className={ className }>
{ `${T9n.get('or use')} ${ labels.join(' / ') }` }
</div>
);
}
return null;
}
}
Accounts.ui.PasswordOrService = PasswordOrService;

View file

@ -16,5 +16,4 @@ export class SocialButtons extends React.Component {
} }
} }
Accounts.ui.SocialButtons = SocialButtons; Accounts.ui.SocialButtons = SocialButtons;

View file

@ -1,6 +1,6 @@
Package.describe({ Package.describe({
name: 'studiointeract:react-accounts-ui', name: 'studiointeract:react-accounts-ui',
version: '1.0.20', version: '1.0.21',
summary: 'Accounts UI for React in Meteor 1.3', summary: 'Accounts UI for React in Meteor 1.3',
git: 'https://github.com/studiointeract/react-accounts-ui', git: 'https://github.com/studiointeract/react-accounts-ui',
documentation: 'README.md' documentation: 'README.md'