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 (
);
}
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'