mirror of
https://github.com/vale981/accounts-ui
synced 2025-03-04 09:21:41 -05:00
Inital commit.
This commit is contained in:
commit
504b70904c
17 changed files with 1097 additions and 0 deletions
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
*.swp
|
||||
*~
|
||||
*.iml
|
||||
.*.haste_cache.*
|
||||
.DS_Store
|
||||
.idea
|
||||
npm-debug.log
|
||||
node_modules
|
||||
dist
|
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# ChangeLog
|
||||
|
||||
### v1.0.0
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Studio Interact Inc. <info@studiointeract.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
101
README.md
Normal file
101
README.md
Normal file
|
@ -0,0 +1,101 @@
|
|||
# React Accounts UI for Meteor 1.3
|
||||
|
||||
Current version 1.0.0
|
||||
|
||||
## Features
|
||||
|
||||
1. **Easy to use**, mixing the ideas of useraccounts configuration and accounts-ui that everyone already knows.
|
||||
2. **Server Side Rendering** are supported, trough FlowRouter (SSR).
|
||||
3. **Components** are everywhere, and extensible by replacing them on Accounts.ui.
|
||||
4. **Basic routing** included.
|
||||
5. **Unstyled** is the default, no CSS included.
|
||||
|
||||
### Styled version
|
||||
|
||||
* Check back here later for styled version i.e. bootstrap, semantic-ui.
|
||||
|
||||
We're hoping package developers write extensions for these by importing like this:
|
||||
|
||||
```javascript
|
||||
import { Accounts } from 'meteor/studiointeract:react-accounts-ui';
|
||||
|
||||
/**
|
||||
* Form.propTypes = {
|
||||
* fields: React.PropTypes.object.isRequired,
|
||||
* buttons: React.PropTypes.object.isRequired,
|
||||
* error: React.PropTypes.string,
|
||||
* ready: React.PropTypes.bool
|
||||
* };
|
||||
*/
|
||||
class Form extends Accounts.ui.Form {
|
||||
render() {
|
||||
const { fields, buttons, error, message, ready = true} = this.props;
|
||||
return (
|
||||
<form className={ready ? "ready" : null} onSubmit={ evt => evt.preventDefault() } className="accounts-ui">
|
||||
<Accounts.ui.Fields fields={ fields } />
|
||||
<Accounts.ui.Buttons buttons={ buttons } />
|
||||
<Accounts.ui.FormMessage message={ message } />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
// Overwrite provided form.
|
||||
Accounts.ui.Form = Form;
|
||||
exports default Accounts;
|
||||
|
||||
```
|
||||
|
||||
### Available components
|
||||
|
||||
* Accounts.ui.LoginForm
|
||||
* Accounts.ui.Form
|
||||
* Accounts.ui.Fields
|
||||
* Accounts.ui.Field
|
||||
* Accounts.ui.Buttons
|
||||
* Accounts.ui.Button
|
||||
* Accounts.ui.FormMessage
|
||||
|
||||
## Installation
|
||||
|
||||
### Meteor 1.3
|
||||
|
||||
`meteor add studiointeract:react-accounts-ui`
|
||||
|
||||
> Configuration instructions coming shortly! In the meantime check the example below, and for full details about configuration options check in `./lib/accounts_ui.js`
|
||||
|
||||
### Meteor 1.2
|
||||
|
||||
> Not supported.
|
||||
|
||||
## Example setup using FlowRouter (Meteor 1.3)
|
||||
|
||||
```javascript
|
||||
|
||||
import { FlowRouter } from 'meteor/kadira:flow-router-ssr';
|
||||
import { Accounts } from 'meteor/studiointeract:react-accounts';
|
||||
import React from 'react';
|
||||
|
||||
Accounts.ui.config({
|
||||
passwordSignupFields: 'USERNAME_AND_OPTIONAL_EMAIL',
|
||||
onSubmitHook(error, state) {
|
||||
console.log('onSubmitHook')
|
||||
},
|
||||
preSignUpHook(password, info) {
|
||||
console.log('preSignUpHook')
|
||||
},
|
||||
postSignUpHook(userId, info) {
|
||||
console.log('postSignUpHook')
|
||||
},
|
||||
onSignedInHook: () => redirect('/general'),
|
||||
onSignedOutHook: () => redirect('/')
|
||||
});
|
||||
|
||||
FlowRouter.route("/login", {
|
||||
action(params) {
|
||||
mount(MainLayout, {
|
||||
content: <Accounts.ui.LoginForm />
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
```
|
178
imports/accounts_ui.js
Normal file
178
imports/accounts_ui.js
Normal file
|
@ -0,0 +1,178 @@
|
|||
import {Accounts} from 'meteor/accounts-base';
|
||||
import { redirect } from './helpers.js';
|
||||
|
||||
/**
|
||||
* @summary Accounts UI
|
||||
* @namespace
|
||||
* @memberOf Accounts
|
||||
*/
|
||||
Accounts.ui = {};
|
||||
|
||||
Accounts.ui._options = {
|
||||
requestPermissions: {},
|
||||
requestOfflineToken: {},
|
||||
forceApprovalPrompt: {},
|
||||
loginPath: '/login',
|
||||
onSubmitHook: [],
|
||||
preSignUpHook: [],
|
||||
postSignUpHook: [],
|
||||
loginHook: () => redirect(`${Accounts.ui._options.loginPath}`),
|
||||
signUpHook: () => redirect(`${Accounts.ui._options.loginPath}`),
|
||||
resetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
|
||||
changePasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
|
||||
onEnrollAccountHook: () => redirect(`${Accounts.ui._options.loginPath}`),
|
||||
onResetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
|
||||
onVerifyEmailHook: () => redirect(`${Accounts.ui._options.loginPath}`),
|
||||
onSignedInHook: () => redirect('/'),
|
||||
onSignedOutHook: () => redirect('/')
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Configure the behavior of [`{{> loginButtons}}`](#accountsui).
|
||||
* @locus Client
|
||||
* @param {Object} options
|
||||
* @param {Object} options.requestPermissions Which [permissions](#requestpermissions) to request from the user for each external service.
|
||||
* @param {Object} options.requestOfflineToken To ask the user for permission to act on their behalf when offline, map the relevant external service to `true`. Currently only supported with Google. See [Meteor.loginWithExternalService](#meteor_loginwithexternalservice) for more details.
|
||||
* @param {Object} options.forceApprovalPrompt If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google.
|
||||
* @param {String} options.passwordSignupFields Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default).
|
||||
*/
|
||||
Accounts.ui.config = function(options) {
|
||||
// validate options keys
|
||||
const VALID_KEYS = [
|
||||
'passwordSignupFields',
|
||||
'requestPermissions',
|
||||
'requestOfflineToken',
|
||||
'forceApprovalPrompt',
|
||||
'loginPath',
|
||||
'onSubmitHook',
|
||||
'preSignUpHook',
|
||||
'postSignUpHook',
|
||||
'loginHook',
|
||||
'signUpHook',
|
||||
'resetPasswordHook',
|
||||
'changePasswordHook',
|
||||
'onEnrollAccountHook',
|
||||
'onResetPasswordHook',
|
||||
'onVerifyEmailHook',
|
||||
'onSignedInHook',
|
||||
'onSignedOutHook'
|
||||
];
|
||||
|
||||
_.each(_.keys(options), function (key) {
|
||||
if (!_.contains(VALID_KEYS, key))
|
||||
throw new Error("Accounts.ui.config: Invalid key: " + key);
|
||||
});
|
||||
|
||||
// deal with `passwordSignupFields`
|
||||
if (options.passwordSignupFields) {
|
||||
if (_.contains([
|
||||
"USERNAME_AND_EMAIL",
|
||||
"USERNAME_AND_OPTIONAL_EMAIL",
|
||||
"USERNAME_ONLY",
|
||||
"EMAIL_ONLY"
|
||||
], options.passwordSignupFields)) {
|
||||
if (Accounts.ui._options.passwordSignupFields)
|
||||
throw new Error("Accounts.ui.config: Can't set `passwordSignupFields` more than once");
|
||||
else
|
||||
Accounts.ui._options.passwordSignupFields = options.passwordSignupFields;
|
||||
}
|
||||
else {
|
||||
throw new Error("Accounts.ui.config: Invalid option for `passwordSignupFields`: " + options.passwordSignupFields);
|
||||
}
|
||||
}
|
||||
|
||||
// deal with `requestPermissions`
|
||||
if (options.requestPermissions) {
|
||||
_.each(options.requestPermissions, function (scope, service) {
|
||||
if (Accounts.ui._options.requestPermissions[service]) {
|
||||
throw new Error("Accounts.ui.config: Can't set `requestPermissions` more than once for " + service);
|
||||
}
|
||||
else if (!(scope instanceof Array)) {
|
||||
throw new Error("Accounts.ui.config: Value for `requestPermissions` must be an array");
|
||||
}
|
||||
else {
|
||||
Accounts.ui._options.requestPermissions[service] = scope;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deal with `requestOfflineToken`
|
||||
if (options.requestOfflineToken) {
|
||||
_.each(options.requestOfflineToken, function (value, service) {
|
||||
if (service !== 'google')
|
||||
throw new Error("Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment.");
|
||||
|
||||
if (Accounts.ui._options.requestOfflineToken[service]) {
|
||||
throw new Error("Accounts.ui.config: Can't set `requestOfflineToken` more than once for " + service);
|
||||
}
|
||||
else {
|
||||
Accounts.ui._options.requestOfflineToken[service] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deal with `forceApprovalPrompt`
|
||||
if (options.forceApprovalPrompt) {
|
||||
_.each(options.forceApprovalPrompt, function (value, service) {
|
||||
if (service !== 'google')
|
||||
throw new Error("Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment.");
|
||||
|
||||
if (Accounts.ui._options.forceApprovalPrompt[service]) {
|
||||
throw new Error("Accounts.ui.config: Can't set `forceApprovalPrompt` more than once for " + service);
|
||||
}
|
||||
else {
|
||||
Accounts.ui._options.forceApprovalPrompt[service] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// deal with the hooks.
|
||||
for (let hook of ['onSubmitHook', 'preSignUpHook', 'postSignUpHook']) {
|
||||
if (options[hook]) {
|
||||
if (typeof options[hook] != 'function') {
|
||||
throw new Error(`Accounts.ui.config: "${hook}" not a function`);
|
||||
}
|
||||
else {
|
||||
Accounts.ui._options[hook].push(options[hook]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deal with `forceApprovalPrompt`.
|
||||
if (options.loginPath) {
|
||||
if (typeof options.loginPath != 'string') {
|
||||
Accounts.ui._options.loginPath = options.loginPath;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Accounts.ui.config: "loginPath" not an absolute or relative path`);
|
||||
}
|
||||
}
|
||||
|
||||
// deal with redirect hooks.
|
||||
for (let hook of [
|
||||
'loginHook',
|
||||
'signUpHook',
|
||||
'resetPasswordHook',
|
||||
'changePasswordHook',
|
||||
'onEnrollAccountHook',
|
||||
'onResetPasswordHook',
|
||||
'onVerifyEmailHook',
|
||||
'onSignedInHook',
|
||||
'onSignedOutHook']) {
|
||||
if (options[hook]) {
|
||||
if (typeof options[hook] == 'function') {
|
||||
Accounts.ui._options[hook] = options[hook];
|
||||
}
|
||||
else if (typeof options[hook] == 'string') {
|
||||
Accounts.ui._options[hook] = () => {
|
||||
location.href = options[hook];
|
||||
};
|
||||
}
|
||||
else {
|
||||
throw new Error(`Accounts.ui.config: "${hook}" not a function or an absolute or relative path`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default Accounts;
|
66
imports/helpers.js
Normal file
66
imports/helpers.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
export function t9n(str) {
|
||||
return str;
|
||||
};
|
||||
|
||||
export function getLoginServices() {
|
||||
// First look for OAuth services.
|
||||
const services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : [];
|
||||
|
||||
// Be equally kind to all login services. This also preserves
|
||||
// backwards-compatibility.
|
||||
services.sort();
|
||||
|
||||
return _.map(services, function(name){
|
||||
return {name: name};
|
||||
});
|
||||
};
|
||||
// Export getLoginServices using old style globals for accounts-base which
|
||||
// requires it.
|
||||
this.getLoginServices = getLoginServices;
|
||||
|
||||
export function loginResultCallback(redirect) {
|
||||
if (Meteor.isClient){
|
||||
if (typeof redirect === 'string'){
|
||||
window.location.href = redirect;
|
||||
}
|
||||
|
||||
if (typeof redirect === 'function'){
|
||||
redirect();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function passwordSignupFields() {
|
||||
return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY";
|
||||
};
|
||||
|
||||
export function validatePassword(password){
|
||||
if (password.length >= 6) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to dynamically inject react components.
|
||||
Global = this;
|
||||
export function Inject(component, ...args) {
|
||||
return Global[component] ? React.createElement.apply(React.createElement, [Global[component], ...args]) : '';
|
||||
}
|
||||
|
||||
export function redirect(path) {
|
||||
if (Meteor.isClient) {
|
||||
if (window.history) {
|
||||
window.history.replaceState( {} , 'redirect', path );
|
||||
}
|
||||
else {
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// capitalize = function(str){
|
||||
// str = str == null ? '' : String(str);
|
||||
//
|
||||
// return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
// };
|
96
imports/login_session.js
Normal file
96
imports/login_session.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
import {Accounts} from 'meteor/accounts-base';
|
||||
import {
|
||||
loginResultCallback,
|
||||
getLoginServices
|
||||
} from './helpers.js';
|
||||
|
||||
const VALID_KEYS = [
|
||||
'dropdownVisible',
|
||||
|
||||
// XXX consider replacing these with one key that has an enum for values.
|
||||
'inSignupFlow',
|
||||
'inForgotPasswordFlow',
|
||||
'inChangePasswordFlow',
|
||||
'inMessageOnlyFlow',
|
||||
|
||||
'errorMessage',
|
||||
'infoMessage',
|
||||
|
||||
// dialogs with messages (info and error)
|
||||
'resetPasswordToken',
|
||||
'enrollAccountToken',
|
||||
'justVerifiedEmail',
|
||||
'justResetPassword',
|
||||
|
||||
'configureLoginServiceDialogVisible',
|
||||
'configureLoginServiceDialogServiceName',
|
||||
'configureLoginServiceDialogSaveDisabled',
|
||||
'configureOnDesktopVisible'
|
||||
];
|
||||
|
||||
const validateKey = function (key) {
|
||||
if (!_.contains(VALID_KEYS, key))
|
||||
throw new Error("Invalid key in loginButtonsSession: " + key);
|
||||
};
|
||||
|
||||
const KEY_PREFIX = "Meteor.loginButtons.";
|
||||
|
||||
// XXX This should probably be package scope rather than exported
|
||||
// (there was even a comment to that effect here from before we had
|
||||
// namespacing) but accounts-ui-viewer uses it, so leave it as is for
|
||||
// now
|
||||
Accounts._loginButtonsSession = {
|
||||
set: function(key, value) {
|
||||
validateKey(key);
|
||||
if (_.contains(['errorMessage', 'infoMessage'], key))
|
||||
throw new Error("Don't set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage().");
|
||||
|
||||
this._set(key, value);
|
||||
},
|
||||
|
||||
_set: function(key, value) {
|
||||
Session.set(KEY_PREFIX + key, value);
|
||||
},
|
||||
|
||||
get: function(key) {
|
||||
validateKey(key);
|
||||
return Session.get(KEY_PREFIX + key);
|
||||
}
|
||||
};
|
||||
|
||||
if (Meteor.isClient){
|
||||
// In the login redirect flow, we'll have the result of the login
|
||||
// attempt at page load time when we're redirected back to the
|
||||
// application. Register a callback to update the UI (i.e. to close
|
||||
// the dialog on a successful login or display the error on a failed
|
||||
// login).
|
||||
//
|
||||
Accounts.onPageLoadLogin(function (attemptInfo) {
|
||||
// Ignore if we have a left over login attempt for a service that is no longer registered.
|
||||
if (_.contains(_.pluck(getLoginServices(), "name"), attemptInfo.type))
|
||||
loginResultCallback(attemptInfo.type, attemptInfo.error);
|
||||
});
|
||||
|
||||
let doneCallback;
|
||||
|
||||
Accounts.onResetPasswordLink(function (token, done) {
|
||||
Accounts._loginButtonsSession.set("resetPasswordToken", token);
|
||||
doneCallback = done;
|
||||
});
|
||||
|
||||
Accounts.onEnrollmentLink(function (token, done) {
|
||||
Accounts._loginButtonsSession.set("enrollAccountToken", token);
|
||||
doneCallback = done;
|
||||
});
|
||||
|
||||
Accounts.onEmailVerificationLink(function (token, done) {
|
||||
Accounts.verifyEmail(token, function (error) {
|
||||
if (! error) {
|
||||
Accounts._loginButtonsSession.set('justVerifiedEmail', true);
|
||||
}
|
||||
|
||||
done();
|
||||
// XXX show something if there was an error.
|
||||
});
|
||||
});
|
||||
}
|
19
imports/ui/components/Button.jsx
Normal file
19
imports/ui/components/Button.jsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
|
||||
export class Button extends React.Component {
|
||||
render () {
|
||||
const { label, type, disabled = false, onClick } = this.props;
|
||||
return type == 'link' ? (
|
||||
<a onClick={ onClick }>{ label }</a>
|
||||
) : (
|
||||
<button type={type} disabled={ disabled }
|
||||
onClick={ onClick }>{ label }</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
Button.propTypes = {
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
Accounts.ui.Button = Button;
|
13
imports/ui/components/Buttons.jsx
Normal file
13
imports/ui/components/Buttons.jsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import './Button.jsx';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
|
||||
export const Buttons = ({ buttons = {} }) => (
|
||||
<div className="buttons">
|
||||
{Object.keys(buttons).map((id, i) =>
|
||||
<Accounts.ui.Button {...buttons[id]} key={i} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Accounts.ui.Buttons = Buttons;
|
29
imports/ui/components/Field.jsx
Normal file
29
imports/ui/components/Field.jsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
|
||||
export class Field extends React.Component {
|
||||
componentDidMount() {
|
||||
// Trigger an onChange on inital load, to support browser prefilled values.
|
||||
const { onChange } = this.props;
|
||||
let node = ReactDOM.findDOMNode(this);
|
||||
let value = node.getElementsByTagName('input')[0].value;
|
||||
// Match the data format of a typical onChange event.
|
||||
onChange({ target: { value: value } });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { id, hint, label, type = 'text', onChange } = this.props;
|
||||
return (
|
||||
<div className="field">
|
||||
<label htmlFor={ id }>{ label }</label>
|
||||
<input id={ id } type={ type } onChange={ onChange } placeholder={ hint } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Field.propTypes = {
|
||||
onChange: React.PropTypes.func
|
||||
};
|
||||
|
||||
Accounts.ui.Field = Field;
|
13
imports/ui/components/Fields.jsx
Normal file
13
imports/ui/components/Fields.jsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import './Field.jsx';
|
||||
|
||||
export const Fields = ({ fields = {} }) => (
|
||||
<div className="fields">
|
||||
{Object.keys(fields).map((id, i) =>
|
||||
<Accounts.ui.Field {...fields[id]} key={i} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Accounts.ui.Fields = Fields;
|
26
imports/ui/components/Form.jsx
Normal file
26
imports/ui/components/Form.jsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import {Accounts} from 'meteor/accounts-base';
|
||||
import './Fields.jsx';
|
||||
import './Buttons.jsx';
|
||||
import './FormMessage.jsx';
|
||||
|
||||
export class Form extends React.Component {
|
||||
render() {
|
||||
const { fields, buttons, error, message, ready = true} = this.props;
|
||||
return (
|
||||
<form className={ready ? "ready" : null} onSubmit={ evt => evt.preventDefault() } className="accounts-ui">
|
||||
<Accounts.ui.Fields fields={ fields } />
|
||||
<Accounts.ui.Buttons buttons={ buttons } />
|
||||
<Accounts.ui.FormMessage message={ message } />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
Form.propTypes = {
|
||||
fields: React.PropTypes.object.isRequired,
|
||||
buttons: React.PropTypes.object.isRequired,
|
||||
error: React.PropTypes.string,
|
||||
ready: React.PropTypes.bool
|
||||
};
|
||||
|
||||
Accounts.ui.Form = Form;
|
8
imports/ui/components/FormMessage.jsx
Normal file
8
imports/ui/components/FormMessage.jsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
|
||||
export const FormMessage = ({ message }) => message ? (
|
||||
<div className="message">{ message }</div>
|
||||
) : null;
|
||||
|
||||
Accounts.ui.FormMessage = FormMessage;
|
458
imports/ui/components/LoginForm.jsx
Normal file
458
imports/ui/components/LoginForm.jsx
Normal file
|
@ -0,0 +1,458 @@
|
|||
import React from 'react';
|
||||
import Tracker from 'tracker-component';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import './Form.jsx';
|
||||
|
||||
import {
|
||||
passwordSignupFields,
|
||||
validatePassword,
|
||||
loginResultCallback,
|
||||
getLoginServices,
|
||||
t9n
|
||||
} from '../../helpers.js';
|
||||
|
||||
const STATES = {
|
||||
SIGN_IN: Symbol('SIGN_IN'),
|
||||
SIGN_UP: Symbol('SIGN_UP'),
|
||||
PASSWORD_CHANGE: Symbol('PASSWORD_CHANGE'),
|
||||
PASSWORD_RESET: Symbol('PASSWORD_RESET')
|
||||
};
|
||||
// Expose available states.
|
||||
Accounts.ui.STATES = STATES;
|
||||
|
||||
export class LoginForm extends Tracker.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Set inital state.
|
||||
this.state = {
|
||||
message: '',
|
||||
waiting: false,
|
||||
formState: Meteor.user() ? STATES.PASSWORD_CHANGE : STATES.SIGN_IN
|
||||
};
|
||||
|
||||
// Listen reactively.
|
||||
this.autorun(() => {
|
||||
this.setState({
|
||||
user: Meteor.user()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
validateUsername( username ){
|
||||
if ( username.length >= 3 ) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
this.showMessage( t9n("error.usernameTooShort") );
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
validateEmail( email ){
|
||||
if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '')
|
||||
return true;
|
||||
|
||||
if (email.indexOf('@') !== -1) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
this.showMessage(t9n("error.emailInvalid"));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getUsernameOrEmailField(){
|
||||
return {
|
||||
id: 'usernameOrEmail',
|
||||
hint: t9n('Enter username or email'),
|
||||
label: t9n('usernameOrEmail'),
|
||||
onChange: this.handleChange.bind(this, 'usernameOrEmail')
|
||||
};
|
||||
}
|
||||
|
||||
getUsernameField(){
|
||||
return {
|
||||
id: 'username',
|
||||
hint: t9n('Enter username'),
|
||||
label: t9n('username'),
|
||||
onChange: this.handleChange.bind(this, 'username')
|
||||
};
|
||||
}
|
||||
|
||||
getEmailField(){
|
||||
return {
|
||||
id: 'email',
|
||||
hint: t9n('Enter email'),
|
||||
label: t9n('email'),
|
||||
onChange: this.handleChange.bind(this, 'email')
|
||||
};
|
||||
}
|
||||
|
||||
getPasswordField(){
|
||||
return {
|
||||
id: 'password',
|
||||
hint: t9n('Enter password'),
|
||||
label: t9n('password'),
|
||||
type: 'password',
|
||||
onChange: this.handleChange.bind(this, 'password')
|
||||
};
|
||||
}
|
||||
|
||||
getNewPasswordField(){
|
||||
return {
|
||||
id: 'newPassword',
|
||||
hint: t9n('Enter newPassword'),
|
||||
label: t9n('newPassword'),
|
||||
type: 'password',
|
||||
onChange: this.handleChange.bind(this, 'password')
|
||||
};
|
||||
}
|
||||
|
||||
handleChange(field, evt) {
|
||||
let value = evt.target.value;
|
||||
switch (field) {
|
||||
case 'password': break;
|
||||
default:
|
||||
value = value.trim();
|
||||
break;
|
||||
}
|
||||
this.setState({ [field]: value });
|
||||
}
|
||||
|
||||
fields() {
|
||||
const loginFields = [];
|
||||
const { formState } = this.state;
|
||||
|
||||
if (formState == STATES.SIGN_IN) {
|
||||
if (_.contains(["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"],
|
||||
passwordSignupFields())) {
|
||||
loginFields.push(this.getUsernameOrEmailField());
|
||||
}
|
||||
|
||||
if (passwordSignupFields() === "USERNAME_ONLY") {
|
||||
loginFields.push(this.getUsernameField());
|
||||
}
|
||||
|
||||
if (passwordSignupFields() === "EMAIL_ONLY") {
|
||||
loginFields.push(this.getEmailField());
|
||||
}
|
||||
|
||||
loginFields.push(this.getPasswordField());
|
||||
}
|
||||
|
||||
if (formState == STATES.SIGN_UP) {
|
||||
if (_.contains(["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"],
|
||||
passwordSignupFields())) {
|
||||
loginFields.push(this.getUsernameField());
|
||||
}
|
||||
|
||||
if (_.contains(["USERNAME_AND_EMAIL", "EMAIL_ONLY"], passwordSignupFields())) {
|
||||
loginFields.push(this.getEmailField());
|
||||
}
|
||||
|
||||
loginFields.push(this.getPasswordField());
|
||||
}
|
||||
|
||||
if (formState == STATES.PASSWORD_RESET) {
|
||||
loginFields.push(this.getEmailField());
|
||||
}
|
||||
|
||||
if (this.showPasswordChangeForm()) {
|
||||
loginFields.push(this.getPasswordField());
|
||||
loginFields.push(this.getNewPasswordField());
|
||||
}
|
||||
|
||||
return _.indexBy(loginFields, 'id');
|
||||
}
|
||||
|
||||
buttons() {
|
||||
const { formState, waiting, user } = this.state;
|
||||
let loginButtons = [];
|
||||
|
||||
if (user) {
|
||||
loginButtons.push({
|
||||
id: 'signOut',
|
||||
label: t9n('signOut'),
|
||||
onClick: this.signOut.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
if (this.showCreateAccountLink()) {
|
||||
loginButtons.push({
|
||||
id: 'switchToSignUp',
|
||||
label: t9n('signUp'),
|
||||
type: 'link',
|
||||
onClick: this.switchToSignUp.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
if (formState == STATES.SIGN_UP || formState == STATES.PASSWORD_RESET) {
|
||||
loginButtons.push({
|
||||
id: 'switchToSignIn',
|
||||
label: t9n('signIn'),
|
||||
type: 'link',
|
||||
onClick: this.switchToSignIn.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
if (this.showForgotPasswordLink()) {
|
||||
loginButtons.push({
|
||||
id: 'switchToPasswordReset',
|
||||
label: t9n('resetYourPassword'),
|
||||
type: 'link',
|
||||
onClick: this.switchToPasswordReset.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
if (formState == STATES.SIGN_UP) {
|
||||
loginButtons.push({
|
||||
id: 'signUp',
|
||||
label: t9n('signUp'),
|
||||
type: 'submit',
|
||||
disabled: waiting,
|
||||
onClick: this.signUp.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
if (formState == STATES.SIGN_IN) {
|
||||
loginButtons.push({
|
||||
id: 'signIn',
|
||||
label: t9n('signIn'),
|
||||
type: 'submit',
|
||||
disabled: waiting,
|
||||
onClick: this.signIn.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
if (formState == STATES.PASSWORD_RESET) {
|
||||
loginButtons.push({
|
||||
id: 'emailResetLink',
|
||||
label: t9n('emailResetLink'),
|
||||
type: 'submit',
|
||||
disabled: waiting,
|
||||
onClick: this.passwordReset.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
if (this.showPasswordChangeForm()) {
|
||||
loginButtons.push({
|
||||
id: 'changePassword',
|
||||
label: t9n('changePassword'),
|
||||
disabled: waiting,
|
||||
onClick: this.passwordChange.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
// Sort the button array so that the submit button always comes first.
|
||||
loginButtons.sort((a, b) => {
|
||||
return (b.type == 'submit') - (a.type == 'submit');
|
||||
});
|
||||
|
||||
return _.indexBy(loginButtons, 'id');
|
||||
}
|
||||
|
||||
showPasswordChangeForm() {
|
||||
return(getLoginServices().indexOf('password') != -1
|
||||
&& this.state.formState == STATES.PASSWORD_CHANGE);
|
||||
}
|
||||
|
||||
showCreateAccountLink() {
|
||||
return this.state.formState == STATES.SIGN_IN && !Accounts._options.forbidClientAccountCreation;
|
||||
}
|
||||
|
||||
showForgotPasswordLink() {
|
||||
return !this.state.user
|
||||
&& this.state.formState != STATES.PASSWORD_RESET
|
||||
&& _.contains(
|
||||
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"],
|
||||
passwordSignupFields());
|
||||
}
|
||||
|
||||
switchToSignUp() {
|
||||
this.setState({ formState: STATES.SIGN_UP, message: '' });
|
||||
}
|
||||
|
||||
switchToSignIn() {
|
||||
this.setState({ formState: STATES.SIGN_IN, message: '' });
|
||||
}
|
||||
|
||||
switchToPasswordReset() {
|
||||
this.setState({ formState: STATES.PASSWORD_RESET, message: '' });
|
||||
}
|
||||
|
||||
signOut() {
|
||||
Meteor.logout();
|
||||
}
|
||||
|
||||
signIn() {
|
||||
const {
|
||||
username = null,
|
||||
email = null,
|
||||
usernameOrEmail = null,
|
||||
password
|
||||
} = this.state;
|
||||
|
||||
let loginSelector;
|
||||
|
||||
if (username !== null) {
|
||||
if (!this.validateUsername(username)) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
loginSelector = { username: username };
|
||||
}
|
||||
}
|
||||
else if (email !== null) {
|
||||
if (!this.validateEmail(email)) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
loginSelector = { email };
|
||||
}
|
||||
}
|
||||
else if (usernameOrEmail !== null) {
|
||||
// XXX not sure how we should validate this. but this seems good enough (for now),
|
||||
// since an email must have at least 3 characters anyways
|
||||
if (!this.validateUsername(usernameOrEmail)) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
loginSelector = usernameOrEmail;
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error("Unexpected -- no element to use as a login user selector");
|
||||
}
|
||||
|
||||
Meteor.loginWithPassword(loginSelector, password, (error, result) => {
|
||||
if (error) {
|
||||
this.showMessage(t9n(`error.accounts.${error.reason}`) || t9n("Unknown error"));
|
||||
}
|
||||
else {
|
||||
this.setState({ formState: STATES.PASSWORD_CHANGE });
|
||||
loginResultCallback(this.props.redirect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
signUp() {
|
||||
const options = {}; // to be passed to Accounts.createUser
|
||||
const {
|
||||
username = null,
|
||||
email = null,
|
||||
usernameOrEmail = null,
|
||||
password
|
||||
} = this.state;
|
||||
|
||||
if (username !== null) {
|
||||
if ( !this.validateUsername(username) ) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
options.username = username;
|
||||
}
|
||||
}
|
||||
|
||||
if (email !== null) {
|
||||
if ( !this.validateEmail(email) ){
|
||||
return;
|
||||
}
|
||||
else {
|
||||
options.email = email;
|
||||
}
|
||||
}
|
||||
|
||||
if (!validatePassword(password)) {
|
||||
this.showMessage(t9n("error.pwTooShort"));
|
||||
|
||||
return;
|
||||
}
|
||||
else {
|
||||
options.password = password;
|
||||
}
|
||||
|
||||
this.setState({waiting: true});
|
||||
|
||||
Accounts.createUser(options, (error)=>{
|
||||
if (error) {
|
||||
this.showMessage(t9n(`error.accounts.${error.reason}`) || t9n("Unknown error"));
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
formState: STATES.PASSWORD_CHANGE,
|
||||
message: ''
|
||||
});
|
||||
loginResultCallback(this.props.redirect);
|
||||
}
|
||||
|
||||
this.setState({ waiting: false });
|
||||
});
|
||||
}
|
||||
|
||||
passwordReset(){
|
||||
const {
|
||||
email = '',
|
||||
waiting
|
||||
} = this.state;
|
||||
|
||||
if (waiting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (email.indexOf('@') !== -1) {
|
||||
this.setState({ waiting: true });
|
||||
|
||||
Accounts.forgotPassword({ email: email }, (error) => {
|
||||
if (error) {
|
||||
this.showMessage(t9n(`error.accounts.${error.reason}`) || t9n("Unknown error"));
|
||||
}
|
||||
else {
|
||||
this.showMessage(t9n("info.emailSent"));
|
||||
}
|
||||
|
||||
this.setState({ waiting: false });
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.showMessage(t9n("error.emailInvalid"));
|
||||
}
|
||||
}
|
||||
|
||||
passwordChange() {
|
||||
const {
|
||||
password,
|
||||
newPassword
|
||||
} = this.state;
|
||||
|
||||
if ( !validatePassword(newPassword) ){
|
||||
this.showMessage(t9n("error.pwTooShort"));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Accounts.changePassword(password, newPassword, (error) => {
|
||||
if (error) {
|
||||
this.showMessage(t9n(`error.accounts.${error.reason}`) || t9n("Unknown error"));
|
||||
}
|
||||
else {
|
||||
this.showMessage(t9n('info.passwordChanged'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showMessage(message){
|
||||
message = message.trim();
|
||||
|
||||
if (message){
|
||||
this.setState({ message: message });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Accounts.ui.Form fields={this.fields()} buttons={this.buttons()} {...this.state} />;
|
||||
}
|
||||
}
|
||||
|
||||
Accounts.ui.LoginForm = LoginForm;
|
9
main.js
Normal file
9
main.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import {Accounts} from 'meteor/accounts-base';
|
||||
import './imports/accounts_ui.js';
|
||||
import './imports/login_session.js';
|
||||
import { redirect } from './imports/helpers.js';
|
||||
|
||||
import './imports/ui/components/LoginForm.jsx';
|
||||
|
||||
export { Accounts, redirect };
|
||||
export default Accounts;
|
23
package.js
Normal file
23
package.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
Package.describe({
|
||||
name: 'studiointeract:react-accounts-ui',
|
||||
version: '1.0.1',
|
||||
summary: 'Accounts UI for React Component in Meteor',
|
||||
git: 'https://github.com/studiointeract/react-accounts-ui',
|
||||
documentation: 'README.md'
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.versionsFrom('1.3-rc.1');
|
||||
api.use('ecmascript');
|
||||
api.use('tracker');
|
||||
api.use('underscore');
|
||||
api.use('accounts-base');
|
||||
api.use('check');
|
||||
|
||||
api.imply('accounts-base');
|
||||
|
||||
api.use('accounts-oauth', {weak: true});
|
||||
api.use('accounts-password', {weak: true});
|
||||
|
||||
api.mainModule('main.js');
|
||||
});
|
25
package.json
Normal file
25
package.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "react-accounts",
|
||||
"version": "1.0.1",
|
||||
"description": "Accounts UI for React Component in Meteor",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/studiointeract/tracker-component.git"
|
||||
},
|
||||
"keywords": [
|
||||
"react",
|
||||
"meteor",
|
||||
"tracker"
|
||||
],
|
||||
"author": "timbrandin",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/studiointeract/tracker-component/issues"
|
||||
},
|
||||
"homepage": "https://github.com/studiointeract/tracker-component",
|
||||
"dependencies": {
|
||||
"react": "^15.0.0-rc.2",
|
||||
"react-dom": "^15.0.0-rc.2",
|
||||
"tracker-component": "^1.3.3-rc.10"
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue