diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f8b0db..645e3a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 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 * Clear the password when logging in or out. diff --git a/README.md b/README.md index 40ff4b2..5ee3a8f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # React Accounts UI -Current version 1.0.20 +Current version 1.0.21 ## Features @@ -48,7 +48,7 @@ Configure the behavior of `` If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google. * **passwordSignupFields**    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**    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)). diff --git a/imports/accounts_ui.js b/imports/accounts_ui.js index 2664d96..5ba040c 100644 --- a/imports/accounts_ui.js +++ b/imports/accounts_ui.js @@ -68,7 +68,8 @@ Accounts.ui.config = function(options) { "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY", "EMAIL_ONLY", - "NO_PASSWORD" + "EMAIL_ONLY_NO_PASSWORD", + "USERNAME_AND_EMAIL_NO_PASSWORD" ], options.passwordSignupFields)) { Accounts.ui._options.passwordSignupFields = options.passwordSignupFields; } diff --git a/imports/api/server/loginWithoutPassword.js b/imports/api/server/loginWithoutPassword.js index 04fe1cd..0fd4f90 100644 --- a/imports/api/server/loginWithoutPassword.js +++ b/imports/api/server/loginWithoutPassword.js @@ -7,14 +7,30 @@ import { Accounts } from 'meteor/accounts-base'; // Method called by a user to request a password reset email. This is // the start of the reset process. -Meteor.methods({loginWithoutPassword: function (options) { - check(options, {email: String}); +Meteor.methods({loginWithoutPassword: function ({ email, username = null }) { + if (username !== null) { + check(username, String); - var user = Meteor.users.findOne({"emails.address": options.email}); - if (!user) - throw new Meteor.Error(403, "User not found"); + var user = Meteor.users.findOne({ $or: [{ + "username": username, "emails.address": { $exists: 1 } + }, { + "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); }; -Accounts.emailTemplates.loginNoPassword = { - subject: function(user) { - return "Login on " + Accounts.emailTemplates.siteName; - }, - text: function(user, url) { - var greeting = (user.profile && user.profile.name) ? - ("Hello " + user.profile.name + ",") : "Hello,"; - return `${greeting} +// Check for installed accounts-password dependency. +if (Accounts.emailTemplates) { + Accounts.emailTemplates.loginNoPassword = { + subject: function(user) { + return "Login on " + Accounts.emailTemplates.siteName; + }, + text: function(user, url) { + var greeting = (user.profile && user.profile.name) ? + ("Hello " + user.profile.name + ",") : "Hello,"; + return `${greeting} To login, simply click the link below. ${url} Thanks. `; - } -}; + } + }; +} diff --git a/imports/helpers.js b/imports/helpers.js index 5469b3f..4603268 100644 --- a/imports/helpers.js +++ b/imports/helpers.js @@ -22,6 +22,11 @@ export function getLoginServices() { // requires it. this.getLoginServices = getLoginServices; +export function hasPasswordService() { + // First look for OAuth services. + return !!Package['accounts-password']; +}; + export function loginResultCallback(redirect, error) { if (Meteor.isClient) { if (typeof redirect === 'string'){ @@ -35,7 +40,7 @@ export function loginResultCallback(redirect, error) { }; export function passwordSignupFields() { - return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY"; + return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY_NO_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(' '); +} diff --git a/imports/startup/i18n/en.js b/imports/startup/i18n/en.js index 79bbad8..13e04ce 100644 --- a/imports/startup/i18n/en.js +++ b/imports/startup/i18n/en.js @@ -4,5 +4,11 @@ T9n.map('en', { 'Enter newPassword': 'Enter new password', 'Enter email': 'Enter email', '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' }); diff --git a/imports/startup/i18n/sv.js b/imports/startup/i18n/sv.js index 392447a..50364fc 100644 --- a/imports/startup/i18n/sv.js +++ b/imports/startup/i18n/sv.js @@ -4,5 +4,11 @@ T9n.map('sv', { 'Enter newPassword': 'Mata in nytt lösenord', 'Enter email': 'Mata in e-post', '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' }); diff --git a/imports/ui/components/Form.jsx b/imports/ui/components/Form.jsx index a78aa45..d4a9cce 100644 --- a/imports/ui/components/Form.jsx +++ b/imports/ui/components/Form.jsx @@ -1,20 +1,31 @@ import React from 'react'; import {Accounts} from 'meteor/accounts-base'; -import './SocialButtons.jsx'; import './Fields.jsx'; import './Buttons.jsx'; import './FormMessage.jsx'; +import './PasswordOrService.jsx'; +import './SocialButtons.jsx'; export class Form extends React.Component { 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 (
evt.preventDefault() } className="accounts-ui"> - - + + + ); } diff --git a/imports/ui/components/LoginForm.jsx b/imports/ui/components/LoginForm.jsx index 95dd280..6e024dd 100644 --- a/imports/ui/components/LoginForm.jsx +++ b/imports/ui/components/LoginForm.jsx @@ -10,7 +10,9 @@ import { passwordSignupFields, validatePassword, loginResultCallback, - getLoginServices + getLoginServices, + hasPasswordService, + capitalize } from '../../helpers.js'; export class LoginForm extends Tracker.Component { @@ -155,9 +157,12 @@ export class LoginForm extends Tracker.Component { const loginFields = []; const { formState } = this.state; - if (formState == STATES.SIGN_IN) { - if (_.contains(["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"], - passwordSignupFields())) { + if (hasPasswordService() && formState == STATES.SIGN_IN) { + if (_.contains([ + "USERNAME_AND_EMAIL", + "USERNAME_AND_OPTIONAL_EMAIL", + "USERNAME_AND_EMAIL_NO_PASSWORD" + ], passwordSignupFields())) { loginFields.push(this.getUsernameOrEmailField()); } @@ -165,30 +170,48 @@ export class LoginForm extends Tracker.Component { loginFields.push(this.getUsernameField()); } - if (_.contains(["EMAIL_ONLY", "NO_PASSWORD"], passwordSignupFields())) { + if (_.contains([ + "EMAIL_ONLY", + "EMAIL_ONLY_NO_PASSWORD" + ], passwordSignupFields())) { loginFields.push(this.getEmailField()); } - if (passwordSignupFields() !== "NO_PASSWORD") { + if (!_.contains([ + "EMAIL_ONLY_NO_PASSWORD", + "USERNAME_AND_EMAIL_NO_PASSWORD" + ], passwordSignupFields())) { loginFields.push(this.getPasswordField()); } } - if (formState == STATES.SIGN_UP) { - if (_.contains(["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], - passwordSignupFields())) { + if (hasPasswordService() && formState == STATES.SIGN_UP) { + if (_.contains([ + "USERNAME_AND_EMAIL", + "USERNAME_AND_OPTIONAL_EMAIL", + "USERNAME_ONLY", + "USERNAME_AND_EMAIL_NO_PASSWORD" + ], passwordSignupFields())) { loginFields.push(this.getUsernameField()); } - if (_.contains(["USERNAME_AND_EMAIL", "EMAIL_ONLY", "NO_PASSWORD"], passwordSignupFields())) { - loginFields.push(Object.assign(this.getEmailField())); + if (_.contains([ + "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})); } - if (passwordSignupFields() !== "NO_PASSWORD") { + if (!_.contains([ + "EMAIL_ONLY_NO_PASSWORD", + "USERNAME_AND_EMAIL_NO_PASSWORD" + ], passwordSignupFields())) { loginFields.push(this.getPasswordField()); } } @@ -261,9 +284,10 @@ export class LoginForm extends Tracker.Component { loginButtons.push({ id: 'signUp', label: T9n.get('signUp'), - type: 'submit', + type: hasPasswordService() ? 'submit' : 'link', + className: 'active', 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({ id: 'signIn', label: T9n.get('signIn'), - type: 'submit', + type: hasPasswordService() ? 'submit' : 'link', + className: 'active', 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. loginButtons.sort((a, b) => { + return a.label.localeCompare(b.label); + }).sort((a, b) => { return (b.type == 'submit') - (a.type == 'submit'); }); @@ -383,7 +410,7 @@ export class LoginForm extends Tracker.Component { return; } else { - if (passwordSignupFields() === "NO_PASSWORD") { + if (_.contains([ "EMAIL_ONLY_NO_PASSWORD" ], passwordSignupFields())) { this.loginWithoutPassword(); return; } @@ -397,6 +424,10 @@ export class LoginForm extends Tracker.Component { return; } else { + if (_.contains([ "USERNAME_AND_EMAIL_NO_PASSWORD" ], passwordSignupFields())) { + this.loginWithoutPassword(); + return; + } loginSelector = usernameOrEmail; } } @@ -425,7 +456,7 @@ export class LoginForm extends Tracker.Component { Accounts.oauth.serviceNames().map((service) => { oauthButtons.push({ id: service, - label: service, + label: capitalize(service), disabled: waiting, type: 'submit', onClick: this.oauthSignIn.bind(this, service) @@ -435,6 +466,7 @@ export class LoginForm extends Tracker.Component { } return _.indexBy(oauthButtons, 'id'); } + oauthSignIn(service) { const { formState, waiting, user } = this.state; //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. options.password = Meteor.uuid(); } @@ -534,6 +569,7 @@ export class LoginForm extends Tracker.Component { loginWithoutPassword(){ const { email = '', + usernameOrEmail = '', waiting } = this.state; @@ -555,8 +591,27 @@ export class LoginForm extends Tracker.Component { 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 { - 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'); + } } } diff --git a/imports/ui/components/PasswordOrService.jsx b/imports/ui/components/PasswordOrService.jsx new file mode 100644 index 0000000..a637aff --- /dev/null +++ b/imports/ui/components/PasswordOrService.jsx @@ -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 ( +
+ { `${T9n.get('or use')} ${ labels.join(' / ') }` } +
+ ); + } + return null; + } +} + +Accounts.ui.PasswordOrService = PasswordOrService; diff --git a/imports/ui/components/SocialButtons.jsx b/imports/ui/components/SocialButtons.jsx index 6bb2540..0e25ea9 100644 --- a/imports/ui/components/SocialButtons.jsx +++ b/imports/ui/components/SocialButtons.jsx @@ -16,5 +16,4 @@ export class SocialButtons extends React.Component { } } - Accounts.ui.SocialButtons = SocialButtons; diff --git a/package.js b/package.js index 244f6ab..d7471b6 100644 --- a/package.js +++ b/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'studiointeract:react-accounts-ui', - version: '1.0.20', + version: '1.0.21', summary: 'Accounts UI for React in Meteor 1.3', git: 'https://github.com/studiointeract/react-accounts-ui', documentation: 'README.md'