├── .eslintrc ├── .gitignore ├── README.md ├── __tests__ ├── _client │ └── forms │ │ ├── baseForm.test.js │ │ ├── forgotPwd.test.js │ │ ├── index.js │ │ ├── signIn.test.js │ │ └── signUp.test.js ├── client.test.js ├── server.test.js └── setup.js ├── index.js ├── lib ├── AccountsReact.js ├── AccountsReactComponent │ ├── baseForm.js │ ├── changePwd.js │ ├── commonUtils │ │ ├── getModel.js │ │ ├── handleInputChange.js │ │ ├── index.js │ │ └── redirect.js │ ├── enrollAccount.js │ ├── forgotPwd.js │ ├── index.js │ ├── methods │ │ ├── ARCreateAccount.js │ │ ├── ARResendVerificationEmail.js │ │ └── index.js │ ├── resendVerification.js │ ├── resetPwd.js │ ├── signIn.js │ ├── signUp.js │ └── socialButtons.js ├── index.js └── utils │ ├── deepmerge │ ├── index.js │ └── is-mergeable-object.js │ ├── index.js │ ├── regExp.js │ ├── validateField.js │ └── validateForm.js ├── package-lock.json ├── package.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true, 8 | "modules": true, 9 | "experimentalObjectRestSpread": true 10 | } 11 | }, 12 | "plugins": ["react"], 13 | "extends": ["eslint:recommended", "plugin:react/recommended"], 14 | "globals": { 15 | "Meteor": true, 16 | "document": true, 17 | "describe": true, 18 | "it": true 19 | }, 20 | "rules": { 21 | "react/prop-types": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ** This package is not maintained anymore ** 2 | 3 | # **Meteor Accounts UI for React** 4 | 5 | `meteor add meteoreact:accounts` 6 | 7 | **A huge credit goes to the [`useraccounts`](https://github.com/meteor-useraccounts/core) package and the people behind it.** 8 | 9 | This package has been created to be used in one of my projects which was purely React. 10 | Although the original useraccounts package [can be used](https://www.meteor.com/tutorials/react/adding-user-accounts) in react, it depends on blaze and jquery which are both useless when developing with react. 11 | 12 | Right now, you might find that there are several features which hasn't been included in this package. Please open an issue if you need a feature and think it will benefit the community. 13 | 14 | * [Goals](#Goals) 15 | * [Setup](#Setup) 16 | * [Styled versions](#Styled) 17 | * [Routing](#Routing) 18 | * [States](#States) 19 | * [Configuration](#Configuration) 20 | * [Hooks](#Hooks) 21 | * [ReCaptcha](#ReCaptcha) 22 | * [OAuth](#OAuth) 23 | * [Redirects](#Redirects) 24 | * [Custom routes]('#Custom-Routes') 25 | * [Fields](#Fields) 26 | * [Add fields](#Add-Fields) 27 | * [Remove fields](#Remove-Fields) 28 | * [Edit fields](#Edit-Fields) 29 | * [Texts](#Texts) 30 | * [Override Styling](#Override-Styling) 31 | * [Contributing](#Contributing) 32 | 33 | 34 | 35 | 36 | ## Goals 37 | 38 | This package has multiple goals: 39 | 40 | 1. Be an almost identical fork of the great [useraccounts](https://github.com/meteor-useraccounts/core) package with the difference of being dependent only on react (no blaze/jquery/templating). 41 | 42 | 2. Allow an easy migration path for applications which already use the useraccounts package. 43 | 44 | 3. Make sense. The codebase should be understandable and easy to modify. 45 | 46 | 4. Stay actively maintained. 47 | 48 | 49 | 50 | 51 | ## Setup 52 | **Important** - Please note that you must provide a set of components either by using one of the versions below or [by adding your own](#Override-Styling) 53 | 54 | Also note that it's mandatory to call `AccountsReact.configure` on both client/server even with an empty object! 55 | 56 | 57 | 58 | ### Styled versions 59 | Pick the package that suit your app. ([Create it if it doesn't exist!](https://github.com/royGil/accounts-react/issues/6)) 60 | * [meteoreact:accounts-unstyled](https://github.com/royGil/accounts-unstyled) 61 | * [meteoreact:accounts-semantic](https://github.com/royGil/accounts-semantic) 62 | 63 | 64 | *If you've created a package and want to include it here, please open a pull request with a link to the package on [atmoshperejs](https://atmospherejs.com/)* 65 | 66 | 67 | 68 | ### Routing 69 | This package currently supports react-router. 70 | 71 | 72 | 73 | #### React Router 74 | 75 | If you want to use different paths for your routes see [custom routes](#Custom-Routes) 76 | 77 | ```javascript 78 | import React, { Component } from 'react' 79 | import { Redirect } from 'react-router' 80 | import { Route, Switch } from 'react-router-dom' 81 | import { AccountsReactComponent } from 'meteor/meteoreact:accounts' 82 | 83 | class Authentication extends Component { 84 | 85 | render () { 86 | const arState = this.arState 87 | 88 | return ( 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | ) 98 | } 99 | 100 | arState = ({ match, history }) => { 101 | const { path, params } = match 102 | 103 | // Cant change password if not logged in. 104 | if (Meteor.userId() && path !== '/change-password') { 105 | return () 106 | } 107 | 108 | return ( 109 | 114 | ) 115 | } 116 | } 117 | 118 | export default Authentication 119 | ``` 120 | 121 | 122 | 123 | ### States 124 | 125 | When you render AccountsReactComponent there are 3 ways to make it render the form you want 126 | 127 | * Pass a "state" prop 128 | ```javascript 129 | 132 | ``` 133 | 134 | * Pass a "route" prop 135 | ```javascript 136 | 139 | ``` 140 | You must pass a route that resolves to one of the possible states ([example](#Custom-Routes)) 141 | 142 | * Configure the "defaultState" key 143 | ```javascript 144 | AccountsReact.configure({ 145 | defaultState: 'signUp' 146 | }) 147 | ``` 148 | 149 | Currently available states are: 150 | 151 | | State | Details 152 | | --------- | ------- 153 | | changePwd | Set a new password (Must be logged in) 154 | | forgotPwd | Send a password reset email to an address 155 | | resetPwd | Set a new password (After reset, a "token" prop must be passed to AccountsReactComponent) 156 | | signIn | Login form 157 | | signUp | Registration form 158 | | resendVerification | Resend email with verification link 159 | 160 | 161 | 162 | ## Configuration 163 | 164 | * Configuration should be the same on both ends. A good place to put the configuration file is `imports/both/startup` (and import it on both ends) 165 | 166 | * Although it is valid to use different configurations for the client and the server you'd better avoid it in order to prevent possible unknown side-effects. 167 | However, it's perfectly fine to set client configurations (like texts) only on the client and vice versa. 168 | 169 | * Configuration must also run before Meteor's startup. 170 | 171 | The following is a list with details about each configurable option. 172 | 173 | | Option | Type | Default | Description | 174 | | --------------------------- | -------- | --------- | ----------- | 175 | | **Behaviour** | | | | 176 | |confirmPassword | Boolean | true | Ask the password twice for confirmation (only on sign up) 177 | | defaultState | String | 'signIn' | The state to use if no route has been declared (via route or prop) 178 | | disableForgotPassword | Boolean | false | Disable the option the call Accounts.forgotPassword 179 | | enablePasswordChange | Boolean | true | Make the changePwd state available, you can either set it to false or just don't set a route for it. 180 | | focusFirstInput | Boolean | !Meteor.isCordova | Whether to focus the first input when a form is rendered. 181 | | forbidClientAccountCreation | Boolean | false | Dont allow user creation on the client. If set to true - no sign up link/form will be available. 182 | | lowercaseUsername | Boolean | false | Transform username field to lowercase upon registration 183 | | loginAfterSignup | Boolean | true | Login automatically after sign up 184 | | overrideLoginErrors | Boolean | true | Show general error on failed login (without specifying which field was wrong) 185 | | sendVerificationEmail | Boolean | true | Send email verification after successful registration 186 | | setDenyRules | Boolean | true | Apply default deny rules on Meteor.users collection 187 | | disableConfigureLoginService | Boolean | true | Disable `configureLoginService()` insecure method 188 | | **Appearance** | | | 189 | | hideSignInLink | Boolean | false | When set to true, asks to never show the link to the sign in page 190 | | hideSignUpLink | Boolean | false | When set to true, asks to never show the link to the sign up page 191 | | showForgotPasswordLink | Boolean | false | Specifies whether to display a link to the forgot password page/form 192 | | showResendVerificationLink | Boolean | false | Specifies whether to display a link to the resend verification page/form 193 | | showLabels | Boolean | true | Specifies whether to display text labels above input elements 194 | | showPlaceholders | Boolean | true | Specifies whether to display place-holder text inside input elements 195 | | **Client side validation** | | | 196 | | continuousValidation | Boolean | false | Validate input as the user type (on every "onChange" event) 197 | | negativeValidation | Boolean | true | Validate input on every "onBlur" event (only after first data insertion) 198 | | [**Hooks**](#Hooks) | | | 199 | | onLoginHook | Function | A function to be called after a successful login 200 | | onLogoutHook | Function | | Triggered by calling AccountsReact.logout() 201 | | onSubmitHook | Function | | A function to be called after a form submission. It takes 2 arguments (error, state). 202 | | preSignupHook | Function | | A function to be called before calling the "createUser" method. It takes 2 arguments (password, info). Password is the raw password (before hashing), info is the object with all the data about the new user (you can modify it directly) 203 | | showReCaptcha | Boolean | false | Add reCaptcha mechanism to the sign up form ([details](#Recaptcha)) 204 | | [**OAuth**](#OAuth) | | 205 | | [**Redirects**](#Redirects) | | | 206 | | [**mapStateToRoute**](#Custom-Routes) | | | 207 | 208 | 209 | 210 | ### Hooks 211 | 212 | ```javascript 213 | const onLogoutHook = () => { 214 | // A good use case will be to redirect the user somewhere 215 | } 216 | 217 | const onSubmitHook = (err, state) => { 218 | if (!err) { 219 | if (state === 'signIn') { 220 | // 221 | } 222 | if (state === 'signUp') { 223 | // 224 | } 225 | } 226 | } 227 | 228 | const preSignupHook = (password, info) => { 229 | /* 230 | info structure might look like this 231 | { 232 | username, 233 | email, 234 | password (hashed), 235 | profile 236 | } 237 | */ 238 | } 239 | 240 | AccountsReact.configure({ 241 | onLogoutHook, 242 | onSubmitHook, 243 | preSignupHook 244 | }) 245 | ``` 246 | 247 | 248 | 249 | ### ReCaptcha 250 | 251 | First, obtain the necessary API keys from [here](https://www.google.com/recaptcha/admin#list) 252 | 253 | Choose one of the following ways to configure reCaptcha settings (**Make sure your secretKey never reach the client!**) 254 | 255 | * [**Meteor settings file**](https://docs.meteor.com/api/core.html#Meteor-settings) 256 | ```javascript 257 | "public": { 258 | "reCaptcha": { 259 | "siteKey": SITE KEY, 260 | // params 261 | } 262 | } 263 | "reCaptcha": { 264 | "secretKey": SECRET KEY 265 | } 266 | ``` 267 | ```javascript 268 | AccountsReact.configure({ 269 | showReCaptcha: true 270 | }) 271 | ``` 272 | 273 | * **Configuration Object** 274 | ```javascript 275 | AccountsReact.configure({ 276 | reCaptcha: { 277 | // params (except secretKey!) 278 | }, 279 | showReCaptcha: true 280 | }) 281 | ``` 282 | And on a server only file 283 | ```javascript 284 | AccountsReact.configure({ 285 | reCaptcha: { 286 | secretKey: SECRET KEY 287 | } 288 | }) 289 | ``` 290 | 291 | [List of available params (except callback)](https://developers.google.com/recaptcha/docs/display#render_param) 292 | 293 | 294 | 295 | ### OAuth 296 | 297 | You can specify whether to allow users to login with 3rd party service. 298 | The only requirements are that you add the `service-configuration` package (`meteor add service-configuration`) and the relevant packages for the services you want (e.g `accounts-google`. `accounts-facebook`, etc...) 299 | 300 | And configure the services you want to support (server side) 301 | 302 | ```javascript 303 | // google example 304 | ServiceConfiguration.configurations.update( 305 | { service: "google" }, 306 | { 307 | $set: { 308 | "loginStyle": "popup", 309 | "clientId": "-", 310 | "secret": "-" 311 | } 312 | } 313 | ) 314 | ``` 315 | You can also specify additional options for each service like so 316 | ```javascript 317 | AccountsReact.configure({ 318 | oauth: { 319 | 'google': { 320 | // options 321 | }, 322 | 'facebook': { 323 | // ... 324 | } 325 | } 326 | }) 327 | ``` 328 | You can find the available options [here](https://docs.meteor.com/api/accounts.html#Meteor-loginWith%3CExternalService%3E) 329 | 330 | 331 | 332 | ### Redirects 333 | 334 | You can specify directly what happens when a user clicks on a link that normally will redirect him to a different form (e.g "Forgot your password?" link). 335 | 336 | ```javascript 337 | AccountsReact.configure({ 338 | redirects: { 339 | toSignUp: () => {}, 340 | toSignIn: () => {}, 341 | toForgotPwd: () => {} 342 | } 343 | }) 344 | ``` 345 | 346 | Note that if you set any of the above, its your responsibility to take the user to a different route. 347 | If routing support (no internal state) is what you seek, you should probably just pass a "history" prop ([see example above](#React-Router-Example)) to AccountsReactComponent 348 | 349 | 350 | 351 | ### Custom Routes 352 | 353 | Behind the scenes, AccountsReactComponent will use an object called `mapStateToRoute` to map different routes to the desired states. 354 | 355 | The default object used is the following 356 | 357 | ```javascript 358 | mapStateToRoute: { 359 | signIn: '/sign-in', 360 | signUp: '/sign-up', 361 | forgotPwd: '/forgot-password', 362 | changePwd: '/change-password', 363 | resetPwd: '/reset-password', 364 | resendVerification: '/resend-verification' 365 | } 366 | ``` 367 | 368 | You can easily override it with 369 | 370 | ```javascript 371 | AccountsReact.configure({ 372 | mapStateToRoute: { 373 | signIn: '/login', 374 | signUp: '/register' 375 | // ... 376 | } 377 | }) 378 | ``` 379 | 380 | 381 | 382 | ### Fields 383 | 384 | Form fields are defined as objects in an array and can be easily customized to your needs. 385 | You can edit, add or remove fields directly or via one of the built in functions (addField, removeField ...) 386 | 387 | The supported properties are listed in the following table. 388 | **Note that you can also specify your own properties** 389 | 390 | | Property | Type | Required | Description 391 | | -------------------- | -----------------| -------- | ---------------------------------------- | 392 | | _id | String | X | A unique field's id/name (internal use only) to be also used as attribute name into Meteor.user().profile in case it identifies an additional sign up field. Usually all lowercase letters 393 | | type | String | X | Specifies the input element type. At the moment supported inputs are: password, email, text, select, radio 394 | | displayName | String | | The field's label text. The text label is shown only if showLabels options is set to true 395 | | errStr | String | | Error message to display in case of a false validation. 396 | | exclude | Boolean | | (On sign up only) If set to true the field will be excluded from the new user object 397 | | func | Function | | Specify a custom function for validation. (example below) 398 | | minLength | Integer | | If specified, requires the content of the field to be at least `minLength` characters 399 | | maxLength | Integer | | If specified, require the content of the field to be at most maxLength characters. 400 | | options | [Object] | | In case type property is set to "select" or "radio", this field must be set to an array of options to be used 401 | | placeholder | String | | The field's (input) placeholder text. The place-holder is shown only if showPlaceholders option is set to true 402 | | re | RegExp | | Specify a regular expression to validate against. (example below) 403 | | required | Boolean | | If set to true the corresponding field cannot be left blank 404 | | autocomplete | String | | `` autocomplete tag value 405 | 406 | **The original user accounts package supports several more properties. Pull requests are more then welcome!** 407 | 408 | You can see each state default fields [here](https://github.com/royGil/accounts-react/blob/master/lib/AccountsReact.js#L78) 409 | 410 | Examples of **func** and **re** properties. 411 | 412 | [func](https://github.com/royGil/accounts-react/blob/master/lib/AccountsReact.js#L137) 413 | ```javascript 414 | { 415 | _id: 'confirmPassword', 416 | displayName: 'Confirm password', 417 | type: 'password', 418 | placeholder: 'Re-enter your password', 419 | errStr: 'Password doesn\'t match', 420 | exclude: true, 421 | func: (fields, fieldObj, value, model, errorsArray) => { 422 | /* 423 | fields: Current form fields array 424 | fieldObj: This object 425 | value: This field's value 426 | model: Current form values object 427 | errorsArray: Current form errors array 428 | */ 429 | 430 | if (!this.config.confirmPassword) { 431 | return true 432 | } 433 | 434 | // check that passwords match 435 | const { password } = model 436 | const { _id, errStr } = fieldObj 437 | 438 | if (typeof password === 'string') { 439 | if (!value || (value !== password)) { 440 | errorsArray.push({ _id, errStr }) 441 | return 442 | } 443 | } 444 | 445 | return true 446 | } 447 | } 448 | ``` 449 | 450 | [re](https://github.com/royGil/accounts-react/blob/master/lib/AccountsReact.js#L118) 451 | ```javascript 452 | { 453 | _id: 'email', 454 | displayName: 'Email', 455 | placeholder: 'Enter your email', 456 | re: regExp.Email, 457 | errStr: 'Please enter a valid email' 458 | } 459 | ``` 460 | 461 | 462 | 463 | #### Add Fields 464 | To add additional fields, you must specify the state you want to mutate, and an array of object(s) containing your field's data. 465 | 466 | ```javascript 467 | import { AccountsReact } from 'meteor/meteoreact:accounts' 468 | 469 | AccountsReact.addFields('signUp', [ 470 | { 471 | _id: 'fullName', 472 | displayName: 'Full Name', 473 | placeholder: 'Enter your full name', 474 | minLength: 4, 475 | maxLength: 70, 476 | required: true, 477 | errStr: 'This field must contain at least 4 characters and no more than 70', 478 | autocomplete: 'name' 479 | } 480 | ]) 481 | ``` 482 | 483 | 484 | 485 | #### Remove Fields 486 | This functionality is not implemented yet, You can [help](https://github.com/royGil/accounts-react/issues/3) 487 | 488 | 489 | 490 | #### Edit Fields 491 | This functionality is not implemented yet, You can [help](https://github.com/royGil/accounts-react/issues/4) 492 | 493 | 494 | 495 | 496 | ### Texts 497 | 498 | Configuring the text to be used by the forms is done via the `AccountsReact.configure` function. 499 | 500 | The default configuration object contains a `texts` property which you can view [here](https://github.com/royGil/accounts-react/blob/master/lib/AccountsReact.js#L206) 501 | 502 | Here is an example of how to override those 503 | 504 | ```javascript 505 | import { AccountsReact } from 'meteor/meteoreact:accounts' 506 | AccountsReact.configure({ 507 | texts: { 508 | button: { 509 | changedPwd: 'Change your password!' 510 | }, 511 | info: { 512 | emailSent: 'Check your inbox!' 513 | }, 514 | loginForbiddenMessage: 'The username or password is incorrect' 515 | } 516 | }) 517 | ``` 518 | 519 | 520 | 521 | ## Override Styling 522 | 523 | Lets say that you are using `semantic-ui-react` and `meteoreact:accounts-semantic` and want to add a simple description below each input field. 524 | 525 | Instead of copying the full package into your local *packages* folder and directly change the code (which is totally legitimate and will work just fine!) you can look at the source code of that package and copy only the implementation of the input field into your project. 526 | 527 | From there you can edit (almost*) anything you'd like. Save the file when you are done and then add it like so: 528 | 529 | ```javascript 530 | import { AccountsReact } from 'meteor/meteoreact:accounts' 531 | import YourInputField from '...' 532 | 533 | AccountsReact.style({ 534 | InputField: YourInputField 535 | }) 536 | ``` 537 | 538 | You can override any of the following fields 539 | 540 | [`InputField`](https://github.com/royGil/accounts-semantic/blob/master/Input.js), 541 | [`SelectField`](https://github.com/royGil/accounts-semantic/blob/master/Select.js), 542 | [`RadioField`](https://github.com/royGil/accounts-semantic/blob/master/Radio.js), 543 | [`SubmitField`](https://github.com/royGil/accounts-semantic/blob/master/Submit.js), 544 | [`TitleField`](https://github.com/royGil/accounts-semantic/blob/master/Title.js), 545 | [`ErrorsField`](https://github.com/royGil/accounts-semantic/blob/master/Errors.js) 546 | 547 | *Dont edit or remove anything that might break the core functionality (like the onChange handlers for example) 548 | 549 | 550 | 551 | ## Contributing 552 | 553 | 1. Fork this repo 554 | 555 | 2. `git clone https://github.com/royGil/accounts-react-demo && cd accounts-react-demo` 556 | 557 | 3. `git clone https://github.com/{your_account}/accounts-react packages/meteoreact:accounts` 558 | 4. `meteor npm install` 559 | 560 | From this point you can make changes to the package folder and run the demo app to see them. 561 | 562 | _Note that if you want to test anything related to the social buttons you'll have to include a proper **settings.json** file ([see example below](#settings.json-example))_ 563 | 564 | To commit your changes 565 | 566 | 1. `cd packages/meteoreact:accounts` 567 | 568 | 2. `npm install` (so you can run tests) 569 | 570 | 3. `npm test` and make sure there are no errors 571 | 572 | 4. Push your changes (**from within the "meteoreact:accounts" folder!**) and create a PR from your fork on github. 573 | 574 | I'll appreciate if you write tests for your new commit but its not a requirement. 575 | 576 | ------------- 577 | 578 | 579 | 580 | ```javascript 581 | { 582 | "services": { 583 | "google": { 584 | "loginStyle": "popup", 585 | "clientId": "XXX", 586 | "secret": "XXX" 587 | }, 588 | "facebook": { 589 | "loginStyle": "popup", 590 | "appId": "XXX", 591 | "secret": "XXX" 592 | }, 593 | "github": { 594 | "loginStyle": "popup", 595 | "clientId": "XXX", 596 | "secret": "XXX" 597 | }, 598 | "twitter": { 599 | "loginStyle": "popup", 600 | "consumerKey": "XXX", 601 | "secret": "XXX" 602 | } 603 | } 604 | } 605 | 606 | ``` 607 | -------------------------------------------------------------------------------- /__tests__/_client/forms/baseForm.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow, mount } from 'enzyme' 3 | import { expect } from 'chai' 4 | import sinon from 'sinon' 5 | import BaseForm from '../../../lib/AccountsReactComponent/baseForm' 6 | import AccountsReact from '../../../lib/AccountsReact' 7 | 8 | const { config } = AccountsReact 9 | 10 | describe('', () => { 11 | 12 | // Mock props passed by each state component 13 | let props = { 14 | context: {}, 15 | currentState: 'signIn', 16 | values: {}, 17 | defaults: { 18 | ...config 19 | }, 20 | onSubmit, 21 | errors: [] 22 | } 23 | 24 | // Defined here so can be used with sinon.spy 25 | function onSubmit () {} 26 | 27 | // Reusable shallowed/mounted components. 28 | // Don't use those if a test need to to mounted with specific props 29 | const wrapperShallow = shallow() 30 | const wrapperMount = mount() 31 | 32 | it('should render as a form element', () => { 33 | 34 | expect(wrapperShallow.type()).to.equal('form') 35 | }) 36 | 37 | it('should render the title of the current state if defined', () => { 38 | 39 | expect(wrapperMount.find('h1').at(0)).to.have.length(1) 40 | }) 41 | 42 | it('should not render the title of the current state if not defined', () => { 43 | props.defaults.texts.title[props.currentState] = '' // remove text for the title 44 | const wrapper = mount() 45 | 46 | expect(wrapper.find('h1').at(0)).to.have.length(0) 47 | }) 48 | 49 | it('should render the fields of the current state', () => { 50 | const currentFieldsLength = config.fields[props.currentState].length 51 | 52 | expect(wrapperMount.find('.ar-fields').at(0).children()).to.have.length(currentFieldsLength) 53 | }) 54 | 55 | it('should not render a reCaptcha div container if reCaptcha is disabled', () => { 56 | // showReCaptcha is disabled by default so we check that first. 57 | 58 | expect(wrapperMount.find('#recaptcha-container').at(0)).to.have.length(0) 59 | }) 60 | 61 | it('should render a reCaptcha div container if reCaptcha is enabled', () => { 62 | props.defaults.showReCaptcha = true 63 | const wrapper = mount() 64 | 65 | expect(wrapper.find('#recaptcha-container').at(0)).to.have.length(1) 66 | }) 67 | 68 | it('should render a submit a button', () => { 69 | 70 | expect(wrapperMount.find('button').at(0)).to.have.length(1) 71 | }) 72 | 73 | it('should call the onSubmit function when the button is clicked', () => { 74 | const spy = sinon.spy() 75 | 76 | const wrapper = mount() 77 | const button = wrapper.find('button') 78 | 79 | button.simulate('click') 80 | 81 | expect(spy.calledOnce).to.equal(true) 82 | }) 83 | 84 | it('should render an empty div if there are no errors', () => { 85 | expect(wrapperMount.find('.ar-errors').children()).to.have.length(0) 86 | }) 87 | 88 | it('should render a div with errors if there are any', () => { 89 | const error = { _id: '__globals', errStr: 'Global error message' } 90 | const wrapper = mount() 91 | 92 | expect(wrapper.find('.ar-errors').children()).to.have.length(1) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /__tests__/_client/forms/forgotPwd.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow, mount } from 'enzyme' 3 | import { expect } from 'chai' 4 | import sinon from 'sinon' 5 | import ForgotPwd from '../../../lib/AccountsReactComponent/forgotPwd' 6 | import AccountsReact from '../../../lib/AccountsReact' 7 | 8 | const { config } = AccountsReact 9 | 10 | describe('ForgotPwd Form', () => { 11 | 12 | const getProps = (overrideDefaults = {}) => { 13 | return { 14 | currentState: 'forgotPwd', 15 | defaults: { 16 | ...config, 17 | ...overrideDefaults 18 | }, 19 | changeState: () => {} 20 | } 21 | } 22 | 23 | const shallowWrapper = shallow() 24 | const mountWrapper = mount() 25 | 26 | 27 | it('should render a form', () => { 28 | 29 | expect(mountWrapper.find('form').exists()).to.equal(true) 30 | }) 31 | 32 | it('should display an emailSent message if email was sent successfully', () => { 33 | const instance = mountWrapper.instance() 34 | 35 | expect(mountWrapper.find('.email-sent').exists()).to.equal(false) 36 | instance.setState({ emailSent: true }) 37 | mountWrapper.update() 38 | expect(mountWrapper.find('.email-sent').exists()).to.equal(true) 39 | }) 40 | 41 | describe('onSubmit', () => { 42 | 43 | it('should catch validation errors if any', () => { 44 | const instance = mountWrapper.instance() 45 | 46 | expect(instance.state.errors.length).to.equal(0) 47 | instance.onSubmit() 48 | expect(instance.state.errors.length).to.not.equal(0) 49 | }) 50 | 51 | it('should call sentPasswordResetLink if there are no validation errors', () => { 52 | const instance = mountWrapper.instance() 53 | const spy = sinon.spy(instance, 'sentPasswordResetLink') 54 | 55 | instance.setState({ email: 'valid@email.com' }) 56 | instance.onSubmit() 57 | 58 | expect(spy.calledOnce).to.equal(true) 59 | spy.restore() 60 | }) 61 | 62 | it('sentPasswordResetLink should call the onSubmit hook', (done) => { 63 | const wrapper = mount() 64 | const instance = wrapper.instance() 65 | const spy = sinon.spy(instance.props.defaults, 'onSubmitHook') 66 | 67 | instance.setState({ email: 'valid@email.com' }) 68 | instance.onSubmit() 69 | 70 | // onSubmitHook is called after server reponse 71 | setTimeout(() => { 72 | expect(spy.calledOnce).to.equal(true) 73 | done() 74 | }, 50) 75 | }) 76 | }) 77 | 78 | describe('links', () => { 79 | 80 | it('should render a signIn link if configured', () => { 81 | 82 | expect(mountWrapper.find('.signIn-link').exists()).to.equal(true) 83 | }) 84 | 85 | it('should not render a signIn link if not configured', () => { 86 | const _props = getProps({ hideSignInLink: true }) 87 | const wrapper = shallow() 88 | 89 | expect(wrapper.find('.signIn-link').exists()).to.equal(false) 90 | }) 91 | 92 | it('should call the redirect function when clicking on a link', () => { 93 | const instance = mountWrapper.instance() 94 | const spy = sinon.spy(instance, 'redirect') 95 | 96 | mountWrapper.find('.signIn-link').simulate('mouseDown') 97 | 98 | expect(spy.calledOnce).to.equal(true) 99 | spy.restore() 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /__tests__/_client/forms/index.js: -------------------------------------------------------------------------------- 1 | import './baseForm.test' 2 | import './signIn.test' 3 | import './signUp.test' 4 | import './forgotPwd.test' 5 | -------------------------------------------------------------------------------- /__tests__/_client/forms/signIn.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow, mount } from 'enzyme' 3 | import { expect } from 'chai' 4 | import sinon from 'sinon' 5 | import SignIn from '../../../lib/AccountsReactComponent/signIn' 6 | import AccountsReact from '../../../lib/AccountsReact' 7 | 8 | const { config } = AccountsReact 9 | 10 | describe('SignIn Form', () => { 11 | 12 | const getProps = (overrideDefaults = {}) => { 13 | return { 14 | currentState: 'signIn', 15 | defaults: { 16 | ...config, 17 | ...overrideDefaults 18 | }, 19 | changeState: () => {} 20 | } 21 | } 22 | 23 | const shallowWrapper = shallow() 24 | const mountWrapper = mount() 25 | 26 | 27 | it('should render a form', () => { 28 | 29 | expect(mountWrapper.find('form').exists()).to.equal(true) 30 | }) 31 | 32 | describe('onSubmit', () => { 33 | 34 | it('should catch validation errors if any', () => { 35 | const instance = mountWrapper.instance() 36 | 37 | expect(instance.state.errors.length).to.equal(0) 38 | instance.onSubmit() 39 | expect(instance.state.errors.length).to.not.equal(0) 40 | }) 41 | }) 42 | 43 | describe('links', () => { 44 | 45 | it('should render a signIn link if configured', () => { 46 | 47 | expect(mountWrapper.find('.signIn-link').exists()).to.equal(true) 48 | }) 49 | 50 | it('should not render a signIn link if not configured', () => { 51 | const _props = getProps({ hideSignUpLink: true }) 52 | const wrapper = shallow() 53 | 54 | expect(wrapper.find('.signInLink').exists()).to.equal(false) 55 | }) 56 | 57 | it('should render a forgotPwd link if configured', () => { 58 | const _props = getProps({ showForgotPasswordLink: true }) 59 | const wrapper = shallow() 60 | 61 | expect(wrapper.find('.forgotPwd-link').exists()).to.equal(true) 62 | }) 63 | 64 | it('should not render a forgotPwd link if not configured', () => { 65 | 66 | expect(mountWrapper.find('.forgotPwd-link').exists()).to.equal(false) 67 | }) 68 | 69 | it('should call the redirect function when clicking on a link', () => { 70 | const _props = getProps({ showForgotPasswordLink: true }) 71 | const wrapper = mount() 72 | const instance = wrapper.instance() 73 | const spy = sinon.spy(instance, 'redirect') 74 | 75 | wrapper.find('.signIn-link').simulate('mouseDown') 76 | wrapper.find('.forgotPwd-link').simulate('mouseDown') 77 | 78 | expect(spy.calledTwice).to.equal(true) 79 | }) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /__tests__/_client/forms/signUp.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow, mount } from 'enzyme' 3 | import { expect } from 'chai' 4 | import sinon from 'sinon' 5 | import SignUp from '../../../lib/AccountsReactComponent/signUp' 6 | import AccountsReact from '../../../lib/AccountsReact' 7 | 8 | const { config } = AccountsReact 9 | 10 | describe('SignUp Form', () => { 11 | 12 | const getProps = (overrideDefaults = {}) => { 13 | return { 14 | currentState: 'signUp', 15 | defaults: { 16 | ...config, 17 | ...overrideDefaults 18 | }, 19 | changeState: () => {} 20 | } 21 | } 22 | 23 | const shallowWrapper = shallow() 24 | const mountWrapper = mount() 25 | 26 | 27 | it('should render a form', () => { 28 | 29 | expect(mountWrapper.find('form').exists()).to.equal(true) 30 | }) 31 | 32 | describe('onSubmit', () => { 33 | 34 | it('should catch validation errors if any', () => { 35 | const instance = mountWrapper.instance() 36 | 37 | expect(instance.state.errors.length).to.equal(0) 38 | instance.onSubmit() 39 | expect(instance.state.errors.length).to.not.equal(0) 40 | }) 41 | }) 42 | 43 | describe('links', () => { 44 | 45 | it('should render a signIn link if configured', () => { 46 | 47 | expect(mountWrapper.find('.signIn-link').exists()).to.equal(true) 48 | }) 49 | 50 | it('should not render a signIn link if not configured', () => { 51 | const _props = getProps({ hideSignInLink: true }) 52 | const wrapper = mount() 53 | 54 | expect(wrapper.find('.signIn-link').exists()).to.equal(false) 55 | }) 56 | 57 | it('should call the redirect function when clicking on a link', () => { 58 | const instance = mountWrapper.instance() 59 | const spy = sinon.spy(instance, 'redirect') 60 | 61 | mountWrapper.find('.signIn-link').simulate('mouseDown') 62 | 63 | expect(spy.calledOnce).to.equal(true) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /__tests__/client.test.js: -------------------------------------------------------------------------------- 1 | import './setup' 2 | 3 | Meteor.startup(() => { 4 | /* Load tests only after meteor's startup to ensure the package has been entirley loaded */ 5 | require('./_client/forms') 6 | }) 7 | -------------------------------------------------------------------------------- /__tests__/server.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rGiladi/accounts-react/38aedc1b4aa135b8c2698c73ae2c8320c9b3dfc2/__tests__/server.test.js -------------------------------------------------------------------------------- /__tests__/setup.js: -------------------------------------------------------------------------------- 1 | import * as enzyme from "enzyme" 2 | import Adapter from "enzyme-adapter-react-16" 3 | 4 | enzyme.configure({ adapter: new Adapter() }) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib') 2 | -------------------------------------------------------------------------------- /lib/AccountsReact.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor' 2 | import { Accounts } from 'meteor/accounts-base' 3 | import regExp from './utils/regExp' 4 | import merge from './utils/deepmerge' 5 | import './AccountsReactComponent/methods' 6 | 7 | class AccountsReact_ { 8 | constructor () { 9 | this._init = false 10 | this.config = { 11 | 12 | /* ----------------------------- 13 | Behaviour 14 | ----------------------------- */ 15 | 16 | confirmPassword: true, 17 | defaultState: 'signIn', 18 | disableForgotPassword: false, 19 | enablePasswordChange: true, 20 | enableEnrollAccount: true, 21 | focusFirstInput: !Meteor.isCordova, 22 | forbidClientAccountCreation: false, 23 | lowercaseUsername: false, 24 | loginAfterSignup: true, 25 | overrideLoginErrors: true, 26 | passwordSignupFields: 'EMAIL_ONLY', 27 | sendVerificationEmail: true, 28 | setDenyRules: true, 29 | disableConfigureLoginService: true, 30 | 31 | /* ----------------------------- 32 | Appearance 33 | ----------------------------- */ 34 | 35 | hideSignInLink: false, 36 | hideSignUpLink: false, 37 | showForgotPasswordLink: false, 38 | showResendVerificationLink: false, 39 | showLabels: true, 40 | showPlaceholders: true, 41 | 42 | /* ----------------------------- 43 | Client Side Validation 44 | ----------------------------- */ 45 | 46 | continuousValidation: false, 47 | negativeValidation: true, 48 | 49 | /* ----------------------------- 50 | Hooks 51 | ----------------------------- */ 52 | 53 | onSubmitHook: (errors, state) => {}, 54 | preSignupHook: (password, info) => {}, 55 | 56 | /* ----------------------------- 57 | Redirects 58 | ----------------------------- */ 59 | 60 | redirects: { 61 | // toSignUp: () => {} 62 | }, 63 | 64 | 65 | /* ----------------------------- 66 | Routes 67 | ----------------------------- */ 68 | 69 | mapStateToRoute: { 70 | signIn: '/sign-in', 71 | signUp: '/sign-up', 72 | forgotPwd: '/forgot-password', 73 | changePwd: '/change-password', 74 | resetPwd: '/reset-password/:token', 75 | resendVerification: '/resend-verification', 76 | enrollAccount: '/enroll-account/:token', 77 | }, 78 | 79 | 80 | /* ----------------------------- 81 | Fields (States) 82 | ----------------------------- */ 83 | 84 | fields: { 85 | 86 | /* Sign In */ 87 | 88 | signIn: [ 89 | { 90 | _id: 'username', 91 | displayName: 'Username', 92 | placeholder: 'Enter your username' 93 | }, 94 | { 95 | _id: 'email', 96 | displayName: 'Email', 97 | placeholder: 'Enter your email', 98 | re: regExp.Email 99 | }, 100 | { 101 | _id: 'password', 102 | displayName: 'Password', 103 | type: 'password', 104 | placeholder: 'Enter your password', 105 | autocomplete: 'current-password' 106 | } 107 | ], 108 | 109 | /* Sign Up */ 110 | 111 | signUp: [ 112 | { 113 | _id: 'username', 114 | displayName: 'Username', 115 | placeholder:'Enter your username', 116 | minLength: 4, 117 | maxLength: 22, 118 | re: regExp.Username, 119 | errStr: 'Username must be between 4 and 22 characters', 120 | }, 121 | { 122 | _id: 'email', 123 | displayName: 'Email', 124 | placeholder: 'Enter your email', 125 | re: regExp.Email, 126 | errStr: 'Please enter a valid email' 127 | }, 128 | { 129 | _id: 'password', 130 | displayName: 'Password', 131 | type: 'password', 132 | placeholder: 'Enter your password', 133 | minLength: 6, 134 | maxLength: 32, 135 | errStr: 'Please enter a strong password between 6 and 32 characters', 136 | autocomplete: 'new-password' 137 | }, 138 | { 139 | _id: 'confirmPassword', 140 | displayName: 'Confirm password', 141 | type: 'password', 142 | placeholder: 'Re-enter your password', 143 | errStr: 'Password doesn\'t match', 144 | exclude: true, 145 | autocomplete: 'new-password', 146 | func: (fields, fieldObj, value, model, errorsArray) => { 147 | if (!this.config.confirmPassword) { 148 | return true 149 | } 150 | 151 | // check that passwords match 152 | const { password } = model 153 | const { _id, errStr } = fieldObj 154 | 155 | if (typeof password === 'string') { 156 | if (!value || (value !== password)) { 157 | errorsArray.push({ _id, errStr }) 158 | return 159 | } 160 | } 161 | 162 | return true 163 | } 164 | } 165 | ], 166 | 167 | /* Forgot Password */ 168 | 169 | forgotPwd: [ 170 | { 171 | _id: 'email', 172 | displayName: 'Email', 173 | placeholder: 'Enter your email', 174 | re: regExp.Email, 175 | errStr: 'Please enter a valid email' 176 | } 177 | ], 178 | 179 | /* Change Password */ 180 | 181 | changePwd: [ 182 | { 183 | _id: 'currentPassword', 184 | displayName: 'Current password', 185 | type: 'password', 186 | placeholder: 'Enter your current password', 187 | autocomplete: 'current-password' 188 | }, 189 | { 190 | _id: 'password', 191 | displayName: 'Password', 192 | type: 'password', 193 | placeholder: 'Enter a new password', 194 | minLength: 6, 195 | maxLength: 32, 196 | errStr: 'Please enter a strong password between 6 and 32 characters', 197 | autocomplete: 'new-password' 198 | } 199 | ], 200 | 201 | /* Reset Password */ 202 | 203 | resetPwd: [ 204 | { 205 | _id: 'password', 206 | displayName: 'New password', 207 | type: 'password', 208 | placeholder: 'Enter a new password', 209 | autocomplete: 'new-password' 210 | } 211 | ], 212 | 213 | /* Resend email verification */ 214 | 215 | resendVerification: [ 216 | { 217 | _id: 'email', 218 | displayName: 'Email', 219 | placeholder: 'Enter your email', 220 | re: regExp.Email, 221 | errStr: 'Please enter a valid email' 222 | } 223 | ], 224 | 225 | /* Enroll Account */ 226 | enrollAccount: [ 227 | { 228 | _id: 'password', 229 | displayName: 'Password', 230 | type: 'password', 231 | placeholder: 'Enter your password', 232 | minLength: 6, 233 | maxLength: 32, 234 | errStr: 'Please enter a strong password between 6 and 32 characters', 235 | autocomplete: 'new-password' 236 | }, 237 | { 238 | _id: 'confirmPassword', 239 | displayName: 'Confirm password', 240 | type: 'password', 241 | placeholder: 'Re-enter your password', 242 | errStr: 'Password doesn\'t match', 243 | exclude: true, 244 | autocomplete: 'new-password', 245 | } 246 | ] 247 | }, 248 | 249 | /* ----------------------------- 250 | Texts 251 | ----------------------------- */ 252 | 253 | texts: { 254 | button: { 255 | changePwd: 'Update New Password', 256 | forgotPwd: 'Send Reset Link', 257 | resetPwd: 'Save New Password', 258 | signIn: 'Login', 259 | signUp: 'Register', 260 | resendVerification: 'Send Verification Link', 261 | enrollAccount: 'Save Password', 262 | }, 263 | title: { 264 | changePwd: 'Change Password', 265 | forgotPwd: 'Forgot Password', 266 | resetPwd: 'Reset Password', 267 | signIn: 'Login', 268 | signUp: 'Create Your Account', 269 | resendVerification: 'Resend Verification Link', 270 | enrollAccount: 'Set Your Account Password', 271 | }, 272 | links: { 273 | toChangePwd: 'Change your password', 274 | toResetPwd: 'Reset your password', 275 | toForgotPwd: 'Forgot your password?', 276 | toSignIn: 'Already have an account? Sign in!', 277 | toSignUp: 'Don\'t have an account? Register', 278 | toResendVerification: 'Verification email lost? Resend' 279 | }, 280 | info: { 281 | emailSent: 'An email has been sent to your inbox', 282 | emailVerified: 'Your email has been verified', 283 | pwdChanged: 'Your password has been changed', 284 | pwdReset: 'A password reset link has been sent to your email!', 285 | pwdSet: 'Password updated!', 286 | signUpVerifyEmail: 'Successful Registration! Please check your email and follow the instructions', 287 | verificationEmailSent: 'A new email has been sent to you. If the email doesn\'t show up in your inbox, be sure to check your spam folder.', 288 | accountEnrolled: 'Your password has been set, you can now login', 289 | }, 290 | errors: { 291 | loginForbidden: 'There was a problem with your login', 292 | captchaVerification: 'There was a problem with the recaptcha verification, please try again', 293 | userNotFound: 'User not found', 294 | userAlreadyVerified: 'User already verified!' 295 | }, 296 | forgotPwdSubmitSuccess: 'A password reset link has been sent to your email!', 297 | loginForbiddenMessage: 'There was a problem with your login' 298 | }, 299 | 300 | showReCaptcha: false, 301 | tempReCaptchaResponse: '', 302 | oauth: {} 303 | } 304 | 305 | this.components = null 306 | } 307 | 308 | /* Set custom components */ 309 | 310 | style (components, override) { 311 | // Settings override to true assumes that all components types are defined. 312 | this.components = override ? components : { ...this.components, ...components } 313 | } 314 | 315 | /* Configuration */ 316 | 317 | configure (config) { 318 | this.config = merge(this.config, config) 319 | 320 | if (!this._init) { 321 | this.determineSignupFields() 322 | this.loadReCaptcha() 323 | this.setAccountCreationPolicy() 324 | this.overrideLoginErrors() 325 | this.disableMethods() 326 | this.setDenyRules() 327 | 328 | this._init = true 329 | } 330 | } 331 | 332 | /* Extend default fields */ 333 | 334 | addFields (state, fields) { 335 | try { 336 | let fieldsArray = this.config.fields[state] 337 | this.config.fields[state] = fieldsArray.concat(fields) 338 | } catch (ex) { 339 | throw new Error(ex) 340 | } 341 | } 342 | 343 | determineSignupFields () { 344 | const { 345 | signUp, 346 | signIn 347 | } = this.config.fields 348 | 349 | let signupFilteredFields; 350 | let signinFilteredFields; 351 | switch (this.config.passwordSignupFields) { 352 | case 'EMAIL_ONLY': 353 | signupFilteredFields = signUp.filter(field => field._id !== 'username') 354 | signinFilteredFields = signIn.filter(field => field._id !== 'username') 355 | break; 356 | case 'USERNAME_ONLY': 357 | signupFilteredFields = signUp.filter(field => field._id !== 'email') 358 | signinFilteredFields = signIn.filter(field => field._id !== 'email') 359 | break; 360 | case 'USERNAME_AND_OPTIONAL_EMAIL': 361 | signUp.forEach(field => { 362 | if (field._id === 'email') { 363 | field.required = false 364 | } 365 | }) 366 | signinFilteredFields = signIn.filter(field => field._id !== 'email') 367 | break 368 | case 'USERNAME_AND_EMAIL': 369 | // 370 | break 371 | default: 372 | throw new Error( 373 | 'passwordSignupFields must be set to one of ' + 374 | '[EMAIL_ONLY, USERNAME_ONLY, USERNAME_AND_OPTIONAL_EMAIL, USERNAME_AND_EMAL]' 375 | ) 376 | } 377 | 378 | if (signupFilteredFields) { 379 | this.config.fields.signUp = signupFilteredFields 380 | } 381 | if (signinFilteredFields) { 382 | this.config.fields.signIn = signinFilteredFields 383 | } 384 | } 385 | 386 | logout () { 387 | const { onLogoutHook } = this.config 388 | if (onLogoutHook) { 389 | onLogoutHook() 390 | } 391 | Meteor.logout() 392 | } 393 | 394 | loadReCaptcha () { 395 | if (this.config.showReCaptcha && Meteor.isClient) { 396 | // load recaptcha script 397 | const script = document.createElement('script'); 398 | document.body.appendChild(script) 399 | script.async = true 400 | script.src = 'https://www.google.com/recaptcha/api.js' 401 | 402 | // Register a recaptcha callback 403 | window.reCaptchaCallback = res => { 404 | this.config.tempReCaptchaResponse = res 405 | } 406 | } 407 | } 408 | 409 | setAccountCreationPolicy () { 410 | try { 411 | Accounts.config({ 412 | forbidClientAccountCreation: this.config.forbidClientAccountCreation 413 | }) 414 | } catch (ex) { 415 | // 416 | } 417 | } 418 | 419 | /* Server only */ 420 | 421 | overrideLoginErrors () { 422 | if (this.config.overrideLoginErrors && Meteor.isServer) { 423 | Accounts.validateLoginAttempt(attempt => { 424 | if (attempt.error) { 425 | var reason = attempt.error.reason 426 | if (reason === 'User not found' || reason === 'Incorrect password') { 427 | throw new Meteor.Error('Login Forbidden', this.config.texts.errors.loginForbidden) // Throw generalized error for failed login attempts 428 | } 429 | } 430 | return attempt.allowed 431 | }) 432 | } 433 | } 434 | 435 | disableMethods () { 436 | if (Meteor.isServer) { 437 | // Override methods directly. 438 | if (this.config.disableForgotPassword) { 439 | Meteor.server.method_handlers.forgotPassword = () => { 440 | throw new Meteor.Error('forgotPassword is disabled') 441 | } 442 | 443 | Meteor.server.method_handlers.resetPassword = () => { 444 | throw new Meteor.Error('resetPassword is disabled') 445 | } 446 | } 447 | 448 | if (!this.config.enablePasswordChange) { 449 | Meteor.server.method_handlers.changePassword = () => { 450 | throw new Meteor.Error('changePassword is disabled') 451 | } 452 | } 453 | 454 | if (!this.config.enableEnrollAccount) { 455 | Meteor.server.method_handlers.enrollAccount = () => { 456 | throw new Meteor.Error('enrollAccount is disabled') 457 | } 458 | } 459 | 460 | if (!this.config.sendVerificationEmail) { 461 | Meteor.server.method_handlers.verifyEmail = () => { 462 | throw new Meteor.Error('verifyEmail is disabled') 463 | } 464 | 465 | Accounts.sendVerificationEmail = () => { 466 | throw new Meteor.Error('disabled', 'sendVerificationEmail is disabled') 467 | } 468 | } 469 | 470 | if (this.config.disableConfigureLoginService) { 471 | Meteor.server.method_handlers.configureLoginService = () => { 472 | throw new Meteor.Error('configureLoginService is disabled') 473 | } 474 | } 475 | } 476 | } 477 | 478 | setDenyRules () { 479 | if (this.config.setDenyRules && Meteor.isServer) { 480 | Meteor.users.deny({ 481 | update () { return true }, 482 | remove () { return true }, 483 | insert () { return true } 484 | }) 485 | } 486 | } 487 | } 488 | 489 | const AccountsReact = new AccountsReact_() 490 | Meteor.startup(() => { 491 | // Automatically use an installed package. 492 | // Packages must be installed before this package in .meteor/packages 493 | 494 | const prefix = 'meteoreact:accounts-' 495 | const components = 496 | Package[prefix + 'unstyled'] || 497 | Package[prefix + 'semantic'] 498 | // ... 499 | 500 | AccountsReact.components = components 501 | }) 502 | 503 | export default AccountsReact 504 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/baseForm.js: -------------------------------------------------------------------------------- 1 | /* eslint key-spacing:0 padded-blocks: 0 */ 2 | import { Meteor } from 'meteor/meteor' 3 | import React, { Component } from 'react' 4 | import PropTypes from 'prop-types' 5 | import AccountsReact from '../AccountsReact' 6 | import { handleInputChange } from './commonUtils' 7 | 8 | class BaseForm extends Component { 9 | constructor (props) { 10 | super() 11 | this.handleInputChange = handleInputChange.bind(props.context) 12 | } 13 | 14 | componentDidMount () { 15 | // We must explicitly rerender recaptcha on every mount 16 | if (this.props.showReCaptcha) { // will be available only for signup form. 17 | let reCaptchaParams = this.props.defaults.reCaptcha || Meteor.settings.public.reCaptcha 18 | 19 | setTimeout(() => { 20 | window.grecaptcha.render('recaptcha-container', 21 | { ...reCaptchaParams, 22 | callback: window.reCaptchaCallback 23 | }, 24 | ) 25 | }, 1) 26 | } 27 | } 28 | 29 | render () { 30 | // State specifics 31 | const { 32 | currentState, 33 | values, 34 | defaults, 35 | onSubmit, 36 | errors 37 | } = this.props 38 | 39 | // Defaults 40 | const { 41 | texts, 42 | showReCaptcha, 43 | confirmPassword 44 | } = defaults 45 | 46 | const _fields = defaults.fields[currentState] 47 | const fields = confirmPassword ? _fields : _fields.filter(field => field._id !== 'confirmPassword') 48 | 49 | // texts 50 | const title = texts.title[currentState] 51 | const button = texts.button[currentState] 52 | 53 | // Components 54 | const { 55 | FormField, 56 | InputField, 57 | SelectField, 58 | RadioField, 59 | SubmitField, 60 | TitleField, 61 | ErrorsField 62 | } = AccountsReact.components 63 | 64 | // Global errors 65 | const globalErrors = errors ? errors.filter(errField => errField._id === '__globals') : [] 66 | 67 | return ( 68 | e.preventDefault()} className={`ar-${currentState}`}> 69 | 70 | {/* Title */} 71 | {title && } 72 | 73 | {/* Fields */} 74 |
75 | {fields.map((f, i) => { 76 | 77 | let Field = InputField // Defaults to input 78 | switch (f.type) { 79 | case 'select': Field = SelectField; break; 80 | case 'radio': Field = RadioField; break; 81 | } 82 | 83 | const props = { 84 | key: i, 85 | values, 86 | defaults, 87 | onChange: this.handleInputChange, 88 | error: errors ? errors.find((errField) => errField._id === f._id) : [], 89 | ...f 90 | } 91 | 92 | if (this.shouldFocusFirstInput(i)) { 93 | props.focusInput = true 94 | } 95 | 96 | return React.createElement(Field, props) 97 | })} 98 |
99 | 100 | {showReCaptcha &&
} 101 | 102 | {/* Submit Button */} 103 | 104 | 105 | {/* Errors Message */} 106 |
107 | {errors.length > 0 && } 108 |
109 | 110 | 111 | ) 112 | } 113 | 114 | shouldFocusFirstInput = index => { 115 | return this.props.defaults.focusFirstInput && index === 0 116 | } 117 | } 118 | 119 | BaseForm.propTypes = { 120 | context: PropTypes.object.isRequired, 121 | currentState: PropTypes.string.isRequired, 122 | values: PropTypes.object.isRequired, 123 | defaults: PropTypes.object.isRequired, 124 | onSubmit: PropTypes.func.isRequired, 125 | errors: PropTypes.array.isRequired 126 | } 127 | 128 | export default BaseForm 129 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/changePwd.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import BaseForm from './baseForm' 3 | import { validateForm } from '../utils/' 4 | import { getModel, redirect } from './commonUtils' 5 | import { changePassword } from './methods' 6 | 7 | class ChangePwd extends Component { 8 | constructor () { 9 | super() 10 | 11 | this.state = { 12 | passwordUpdated: false, 13 | errors: [] 14 | } 15 | 16 | this.getModel = getModel.bind(this) 17 | this.redirect = redirect.bind(this) 18 | } 19 | 20 | componentWillMount () { 21 | if (!Meteor.userId()) { 22 | this.redirect('signin', this.props.defaults.redirects.toSignIn) 23 | } 24 | } 25 | 26 | render () { 27 | const { 28 | currentState, 29 | defaults 30 | } = this.props 31 | 32 | const { 33 | texts 34 | } = defaults 35 | 36 | const { 37 | passwordUpdated, 38 | errors 39 | } = this.state 40 | 41 | const model = this.getModel() 42 | 43 | return ( 44 | 45 | 46 | 54 | 55 | {passwordUpdated &&

{texts.info.pwdChanged}

} 56 | 57 |
58 | ) 59 | } 60 | 61 | onSubmit = () => { 62 | // Validate form 63 | if (!validateForm(this.getModel(), this)) return 64 | 65 | const { currentPassword, password } = this.getModel() 66 | 67 | // Change password 68 | changePassword(currentPassword, password, err => { 69 | if (err) { 70 | this.setState({ errors: [{ _id: '__globals', errStr: err.reason }], passwordUpdated: false }) 71 | } else { 72 | this.setState({ errors: [], passwordUpdated: true }) 73 | } 74 | 75 | this.props.defaults.onSubmitHook(err, this.props.currentState) 76 | }) 77 | } 78 | } 79 | 80 | export default ChangePwd 81 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/commonUtils/getModel.js: -------------------------------------------------------------------------------- 1 | 2 | /* Generic getModel for the state components */ 3 | 4 | function getModel () { 5 | /* Get only form values from state, "this" is binded to the state's class */ 6 | 7 | const { 8 | currentState, 9 | defaults 10 | } = this.props 11 | 12 | const stateKeys = Object.keys(this.state) 13 | const fields = defaults.fields[currentState] 14 | const model = stateKeys 15 | .filter(key => fields.find(f => f._id === key)) // Only keys in the defined fields array 16 | .reduce((obj, key) => { // Create a new object 17 | obj[key] = this.state[key] 18 | return obj 19 | }, {}) 20 | 21 | return model 22 | } 23 | 24 | export default getModel 25 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/commonUtils/handleInputChange.js: -------------------------------------------------------------------------------- 1 | import { validateOnChange } from '../../utils/validateField' 2 | 3 | /* Define a change handler for fields */ 4 | 5 | function handleInputChange (e, _id) { 6 | // *this* is bound to calling components 7 | 8 | // Check if e is already a string value or an event object 9 | const value = typeof e === 'string' ? e : e.target.value 10 | 11 | if (fieldChangedAtLeastOnce(this.state, _id, value)) return 12 | 13 | const { 14 | currentState, 15 | defaults 16 | } = this.props 17 | 18 | const fields = defaults.fields[currentState] 19 | // if e is a string it means that it's a default value and doesn't need to pass validation 20 | if (typeof e !== 'string') { 21 | const errors = validateOnChange(e, _id, fields, this.getModel(), [...this.state.errors]) 22 | 23 | if (errors) { 24 | this.setState({ errors }) 25 | } 26 | } 27 | this.setState({ [_id]: value }) 28 | } 29 | 30 | function fieldChangedAtLeastOnce (state, _id, value) { 31 | return !state.hasOwnProperty(_id) && value === '' 32 | } 33 | 34 | export default handleInputChange 35 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/commonUtils/index.js: -------------------------------------------------------------------------------- 1 | import getModel from './getModel' 2 | import redirect from './redirect' 3 | import handleInputChange from './handleInputChange' 4 | 5 | export { 6 | getModel, 7 | redirect, 8 | handleInputChange 9 | } 10 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/commonUtils/redirect.js: -------------------------------------------------------------------------------- 1 | import AccountsReact from '../../AccountsReact' 2 | 3 | /* Redirect to different state after link clicked */ 4 | 5 | const redirect = function (toState, hook) { 6 | // *this* is bound to the calling components 7 | // Run hook function if set || push state via history || change state internally 8 | 9 | if (hook) { 10 | hook() 11 | } else if (this.props.history) { 12 | this.props.history.push(AccountsReact.config.mapStateToRoute[toState]) 13 | } else { 14 | this.props.changeState(toState) 15 | } 16 | 17 | return 18 | } 19 | 20 | export default redirect 21 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/enrollAccount.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import BaseForm from './baseForm' 3 | import { validateForm } from '../utils/' 4 | import { getModel, redirect } from './commonUtils' 5 | import { resetPassword } from './methods' 6 | 7 | class EnrollAccount extends Component { 8 | constructor () { 9 | super() 10 | 11 | this.state = { 12 | passwordSet: false, 13 | errors: [] 14 | } 15 | 16 | this.getModel = getModel.bind(this) 17 | this.redirect = redirect.bind(this) 18 | } 19 | 20 | render () { 21 | const { 22 | currentState, 23 | defaults 24 | } = this.props 25 | 26 | const { 27 | texts 28 | } = defaults 29 | 30 | const { 31 | passwordSet, 32 | errors 33 | } = this.state 34 | 35 | const model = this.getModel() 36 | 37 | return ( 38 | 39 | 40 | 48 | 49 | {passwordSet &&

{texts.info.accountEnrolled}

} 50 | 51 |
52 | ) 53 | } 54 | 55 | onSubmit = () => { 56 | // Validate form 57 | if (!validateForm(this.getModel(), this)) return 58 | 59 | const { password, confirmPassword } = this.getModel() 60 | 61 | // Change password 62 | resetPassword(this.props.token, password, err => { 63 | if (err) { 64 | this.setState({ errors: [{ _id: '__globals', errStr: err.reason }], passwordSet: false }) 65 | } else { 66 | this.setState({ errors: [], passwordSet: true }) 67 | } 68 | 69 | this.props.defaults.onSubmitHook(err, this.props.currentState) 70 | }) 71 | } 72 | } 73 | 74 | export default EnrollAccount 75 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/forgotPwd.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import BaseForm from './baseForm' 3 | import { validateForm } from '../utils/' 4 | import { getModel, redirect } from './commonUtils' 5 | import { forgotPassword } from './methods' 6 | 7 | class ForgotPassword extends Component { 8 | constructor () { 9 | super() 10 | this.state = { 11 | emailSent: false, 12 | errors: [] 13 | } 14 | 15 | this.getModel = getModel.bind(this) 16 | this.redirect = redirect.bind(this) 17 | } 18 | 19 | render () { 20 | const { 21 | currentState, 22 | defaults 23 | } = this.props 24 | 25 | const { 26 | texts, 27 | hideSignInLink 28 | } = defaults 29 | 30 | const { 31 | errors, 32 | emailSent 33 | } = this.state 34 | 35 | const model = this.getModel() 36 | 37 | return ( 38 | 39 | 40 | 48 | 49 | {emailSent &&

{texts.forgotPwdSubmitSuccess}

} 50 | 51 | {!hideSignInLink && ( 52 |
53 | {texts.links.toSignIn} 54 | 55 | )} 56 | 57 | ) 58 | } 59 | 60 | onSubmit = () => { 61 | // Validate form 62 | if (!validateForm(this.getModel(), this)) return 63 | 64 | this.sentPasswordResetLink() 65 | } 66 | 67 | sentPasswordResetLink = () => { 68 | // Send a reset link to the desired email 69 | 70 | forgotPassword({ email: this.state.email }, err => { 71 | if (err) { 72 | this.setState({ errors: [{ _id: '__globals', errStr: err.reason }], emailSent: false }) 73 | } else { 74 | this.setState({ errors: [], emailSent: true }) 75 | } 76 | 77 | this.props.defaults.onSubmitHook(err, this.props.currentState) 78 | }) 79 | } 80 | 81 | redirectToSignIn = () => { 82 | this.redirect('signIn', this.props.defaults.redirects.toSignIn) 83 | } 84 | } 85 | 86 | export default ForgotPassword 87 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/index.js: -------------------------------------------------------------------------------- 1 | import React, { createElement } from 'react' 2 | import PropTypes from 'prop-types' 3 | import SignIn from './signIn' 4 | import SignUp from './signUp' 5 | import ForgotPwd from './forgotPwd' 6 | import ChangePwd from './changePwd' 7 | import ResetPwd from './resetPwd' 8 | import ResendVerification from './resendVerification' 9 | import EnrollAccount from './enrollAccount' 10 | import AccountsReact from '../AccountsReact' 11 | import merge from '../utils/deepmerge' 12 | 13 | class AccountsReactComponent extends React.Component { 14 | 15 | state = { 16 | internalState: '' // If set - it will override state from props 17 | } 18 | 19 | render () { 20 | ensureComponentsExist() 21 | 22 | // State priority -> 1.internal 2. provided by route/state prop (from parent component) 3. default state from config 23 | let state = this.state.internalState || this.props.state 24 | if (!state) { 25 | const { mapStateToRoute } = AccountsReact.config 26 | const route = this.props.route 27 | 28 | if (route) { 29 | state = Object.keys(mapStateToRoute).find(key => mapStateToRoute[key] === route) 30 | } else { 31 | state = AccountsReact.config.defaultState 32 | } 33 | } 34 | 35 | let form 36 | switch (state) { 37 | case 'signIn': form = SignIn; break; 38 | case 'signUp': form = SignUp; break; 39 | case 'forgotPwd': form = ForgotPwd; break; 40 | case 'changePwd': form = ChangePwd; break; 41 | case 'resetPwd': form = ResetPwd; break; 42 | case 'resendVerification': form = ResendVerification; break; 43 | case 'enrollAccount': form = EnrollAccount; break; 44 | default: return null 45 | } 46 | 47 | const defaults = merge.all([ 48 | AccountsReact.config, 49 | this.props.config 50 | ]) 51 | 52 | if ((defaults.forbidClientAccountCreation && state === 'signUp') || 53 | (defaults.disableForgotPassword && (state === 'forgotPwd' || state === 'resetPwd')) || 54 | (!defaults.enablePasswordChange && state === 'changePwd') || 55 | (!defaults.sendVerificationEmail && state === 'resendVerification') || 56 | (!defaults.enableEnrollAccount && state === 'enrollAccount')) 57 | { 58 | return null 59 | } 60 | 61 | const props = { 62 | currentState: state, 63 | changeState: this.changeInternalState, 64 | history: this.props.history, 65 | token: this.props.token, 66 | defaults 67 | } 68 | 69 | return createElement(form, props) 70 | } 71 | 72 | changeInternalState = toState => { 73 | this.setState({ internalState: toState }) 74 | } 75 | } 76 | 77 | function ensureComponentsExist () { 78 | if (!AccountsReact.components) { 79 | throw new Error('Please ensure you have provided AccountsReact a set of components to use') 80 | } 81 | } 82 | 83 | AccountsReactComponent.defaultProps = { 84 | config: {} 85 | } 86 | 87 | AccountsReactComponent.propTypes = { 88 | state: PropTypes.string, 89 | route: PropTypes.string, 90 | config: PropTypes.object 91 | } 92 | 93 | export default AccountsReactComponent 94 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/methods/ARCreateAccount.js: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base' 2 | import { ValidatedMethod } from 'meteor/mdg:validated-method' 3 | import AccountsReact from '../../AccountsReact' 4 | import validateField from '../../utils/validateField' 5 | 6 | 7 | const ARCreateAccount = new ValidatedMethod({ 8 | name: 'ARCreateAccount', 9 | validate: ({ username, email, password, ...profile }) => { 10 | /* This validation runs on both client and server */ 11 | 12 | if (Meteor.userId()) { 13 | throw new Meteor.Error('Error', 'Already logged in') 14 | } 15 | 16 | let signupFields = AccountsReact.config.fields.signUp 17 | 18 | // Remove password confirmation if not set 19 | if (!AccountsReact.config.confirmPassword) { 20 | signupFields = signupFields.filter(f => f._id !== 'confirmPassword') 21 | } 22 | 23 | // Check that recaptcha token is included if necessary 24 | if (AccountsReact.config.showReCaptcha && !profile.tempReCaptchaResponse) { 25 | throw new Meteor.Error('ReCaptchaError', AccountsReact.config.texts.errors.captchaVerification) 26 | } 27 | 28 | const newUser = { 29 | username, 30 | email, 31 | password, 32 | ...profile // Flat profile object so each key:value pair gets validated as a field 33 | } 34 | 35 | let errors = [] 36 | signupFields.forEach(field => { 37 | validateField(signupFields, field, newUser[field._id], newUser, errors) 38 | }) 39 | 40 | if (errors.length > 0) { 41 | throw new Meteor.Error('ARCreateAccount', errors) 42 | } 43 | }, 44 | run (newUser) { 45 | const { 46 | username, 47 | email, 48 | password, 49 | ...profile 50 | } = newUser 51 | 52 | const userObject = { 53 | username, 54 | email, 55 | password, 56 | profile 57 | } 58 | 59 | // Unnecessary fields (used only for validation) 60 | delete userObject.profile.passwordConfirmation 61 | 62 | if (!username) { 63 | delete userObject.username 64 | } else if (!email) { 65 | delete userObject.email 66 | } 67 | 68 | // Create the user on the server only! 69 | if (Meteor.isServer) { 70 | if (AccountsReact.config.showReCaptcha) { 71 | const res = HTTP.post('https://www.google.com/recaptcha/api/siteverify', { 72 | params: { 73 | secret: AccountsReact.config.reCaptcha.secretKey || Meteor.settings.reCaptcha.secretKey, 74 | response: userObject.profile.tempReCaptchaResponse, 75 | remoteip: this.connection.clientAddress 76 | } 77 | }).data 78 | 79 | if (!res.success) { 80 | throw new Meteor.Error('ReCaptchaError', AccountsReact.config.texts.errors.captchaVerification) 81 | } 82 | 83 | delete userObject.profile.tempReCaptchaResponse 84 | } 85 | 86 | const userId = Accounts.createUser(userObject) 87 | 88 | if (!userId) { 89 | // safety belt. createUser is supposed to throw on error. send 500 error 90 | // instead of sending a verification email with empty userid. 91 | 92 | /* it was taken directly from useraccounts package */ 93 | throw new Error('createUser failed to insert new user') 94 | } 95 | 96 | if (userObject.email && AccountsReact.config.sendVerificationEmail) { 97 | Accounts.sendVerificationEmail(userId, userObject.email) 98 | } 99 | } 100 | } 101 | }) 102 | 103 | export default ARCreateAccount 104 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/methods/ARResendVerificationEmail.js: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base' 2 | import { ValidatedMethod } from 'meteor/mdg:validated-method' 3 | import { check } from 'meteor/check' 4 | import AccountsReact from '../../AccountsReact' 5 | 6 | // Based on https://github.com/meteor-useraccounts/core/blob/2e8986813b51f321f908d2f6211f6f81f76cd627/lib/server_methods.js#L124 7 | const ARResendVerificationEmail = new ValidatedMethod({ 8 | name: 'ARResendVerificationEmail', 9 | validate: ({ email }) => { 10 | /* This validation runs on both client and server */ 11 | 12 | if (Meteor.userId()) { 13 | throw new Meteor.Error('Error', 'Already logged in') 14 | } 15 | 16 | check(email, String); 17 | }, 18 | run ({ email }) { 19 | if (Meteor.isServer) { 20 | var user = Meteor.users.findOne({ "emails.address": email }); 21 | 22 | // Send the standard error back to the client if no user exist with this e-mail 23 | if (!user) { 24 | throw new Meteor.Error('UserNotFound', AccountsReact.config.texts.errors.userNotFound); 25 | } 26 | 27 | try { 28 | Accounts.sendVerificationEmail(user._id); 29 | } catch (error) { 30 | if (error.error === 'disabled') { 31 | throw error; 32 | } else { 33 | // Handle error when email already verified 34 | // https://github.com/dwinston/send-verification-email-bug 35 | throw new Meteor.Error('UserAlreadyVerified', AccountsReact.config.texts.errors.userAlreadyVerified); 36 | } 37 | } 38 | } 39 | } 40 | }) 41 | 42 | export default ARResendVerificationEmail 43 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/methods/index.js: -------------------------------------------------------------------------------- 1 | /* globals Meteor: true */ 2 | import { Accounts } from 'meteor/accounts-base' 3 | import ARCreateAccount from './ARCreateAccount' 4 | import ARResendVerificationEmail from './ARResendVerificationEmail' 5 | import AccountsReact from '../../AccountsReact' 6 | // Create user 7 | export const createUser = (newUser, callback) => { 8 | ARCreateAccount.call(newUser, callback) 9 | } 10 | 11 | // Login 12 | export const login = (username, email, password, callback) => { 13 | Meteor.loginWithPassword(username || email, password, err => { 14 | callback(err) 15 | }) 16 | } 17 | 18 | // Forgot password 19 | export const forgotPassword = (email, callback) => { 20 | Accounts.forgotPassword(email, callback) 21 | } 22 | 23 | // Change password 24 | export const changePassword = (oldPassword, newPassword, callback) => { 25 | Accounts.changePassword(oldPassword, newPassword, callback) 26 | } 27 | 28 | // Reset password 29 | export const resetPassword = (token, newPassword, callback) => { 30 | Accounts.resetPassword(token, newPassword, callback) 31 | } 32 | 33 | // Resend verification link 34 | export const resendVerification = (email, callback) => { 35 | ARResendVerificationEmail.call({ email }, callback) 36 | } 37 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/resendVerification.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import BaseForm from './baseForm' 3 | import { validateForm } from '../utils/' 4 | import { getModel, redirect } from './commonUtils' 5 | import { resendVerification } from './methods' 6 | 7 | class ResendVerification extends Component { 8 | constructor () { 9 | super() 10 | this.state = { 11 | emailSent: false, 12 | errors: [] 13 | } 14 | 15 | this.getModel = getModel.bind(this) 16 | this.redirect = redirect.bind(this) 17 | } 18 | 19 | render () { 20 | const { 21 | currentState, 22 | defaults 23 | } = this.props 24 | 25 | const { 26 | texts, 27 | hideSignInLink 28 | } = defaults 29 | 30 | const { 31 | errors, 32 | emailSent 33 | } = this.state 34 | 35 | const model = this.getModel() 36 | 37 | return ( 38 | 39 | 40 | 48 | 49 | {emailSent &&

{texts.info.emailSent}

} 50 | 51 | {!hideSignInLink && ( 52 | 53 | {texts.links.toSignIn} 54 | 55 | )} 56 |
57 | ) 58 | } 59 | 60 | onSubmit = () => { 61 | // Validate form 62 | if (!validateForm(this.getModel(), this)) return 63 | 64 | this.sendVerificationLink() 65 | } 66 | 67 | sendVerificationLink = () => { 68 | // Send the verification link to the desired email 69 | 70 | resendVerification(this.state.email, err => { 71 | if (err) { 72 | this.setState({ errors: [{ _id: '__globals', errStr: err.reason }], emailSent: false }) 73 | } else { 74 | this.setState({ errors: [], emailSent: true }) 75 | } 76 | 77 | this.props.defaults.onSubmitHook(err, this.props.currentState) 78 | }) 79 | } 80 | 81 | redirectToSignIn = () => { 82 | this.redirect('signIn', this.props.defaults.redirects.toSignIn) 83 | } 84 | } 85 | 86 | export default ResendVerification 87 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/resetPwd.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import BaseForm from './baseForm' 3 | import { validateForm } from '../utils/' 4 | import { getModel, redirect } from './commonUtils' 5 | import { resetPassword } from './methods' 6 | 7 | class ResetPwd extends Component { 8 | constructor () { 9 | super() 10 | this.state = { 11 | passwordUpdated: false, 12 | errors: [] 13 | } 14 | 15 | this.getModel = getModel.bind(this) 16 | this.redirect = redirect.bind(this) 17 | } 18 | 19 | render () { 20 | const { 21 | currentState, 22 | defaults 23 | } = this.props 24 | 25 | const { 26 | texts 27 | } = defaults 28 | 29 | const { 30 | passwordUpdated, 31 | errors 32 | } = this.state 33 | 34 | const model = this.getModel() 35 | 36 | return ( 37 | 38 | 39 | 47 | 48 | {passwordUpdated &&

{texts.info.pwdSet}

} 49 | 50 |
51 | ) 52 | } 53 | 54 | onSubmit = () => { 55 | // Validate form 56 | if (!validateForm(this.getModel(), this)) return 57 | 58 | const { password } = this.getModel() 59 | 60 | // Change password 61 | resetPassword(this.props.token, password, err => { 62 | if (err) { 63 | this.setState({ errors: [{ _id: '__globals', errStr: err.reason }], passwordUpdated: false }) 64 | } else { 65 | this.setState({ errors: [], passwordUpdated: true }) 66 | } 67 | 68 | this.props.defaults.onSubmitHook(err, this.props.currentState) 69 | }) 70 | } 71 | } 72 | 73 | export default ResetPwd 74 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/signIn.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import BaseForm from './baseForm' 3 | import { validateForm } from '../utils' 4 | import { getModel, redirect } from './commonUtils' 5 | import { login } from './methods' 6 | import SocialButtons from './socialButtons' 7 | 8 | class SignIn extends Component { 9 | constructor () { 10 | super() 11 | this.state = { 12 | errors: [] 13 | } 14 | 15 | this.getModel = getModel.bind(this) 16 | this.redirect = redirect.bind(this) 17 | } 18 | 19 | render () { 20 | const { 21 | currentState, 22 | defaults 23 | } = this.props 24 | 25 | const { 26 | texts, 27 | hideSignUpLink, 28 | showForgotPasswordLink, 29 | showResendVerificationLink, 30 | sendVerificationEmail, 31 | forbidClientAccountCreation, 32 | disableForgotPassword 33 | } = defaults 34 | 35 | const model = this.getModel() 36 | 37 | return ( 38 | 39 | 47 | 48 | {!forbidClientAccountCreation && ( 49 | 52 | )} 53 | 54 | {!forbidClientAccountCreation && !hideSignUpLink && ( 55 | 56 | {texts.links.toSignUp} 57 | 58 | )} 59 | {!disableForgotPassword && showForgotPasswordLink && ( 60 | 61 | {texts.links.toForgotPwd} 62 | 63 | )} 64 | {sendVerificationEmail && showResendVerificationLink && ( 65 | 66 | {texts.links.toResendVerification} 67 | 68 | )} 69 | 70 | ) 71 | } 72 | 73 | onSubmit = () => { 74 | /* Login */ 75 | const model = this.getModel() 76 | 77 | // Validate form 78 | if (!validateForm(model, this)) return 79 | 80 | const { username, email, password } = model 81 | 82 | // Login 83 | login(username, email, password, err => { 84 | if (err) { 85 | this.setState({ errors: [{ _id: '__globals', errStr: err.reason }] }) 86 | } else { 87 | const { onLoginHook } = this.props.defaults 88 | if (onLoginHook) { 89 | onLoginHook() 90 | } 91 | } 92 | }) 93 | } 94 | 95 | redirectToSignUp = () => { 96 | this.redirect('signUp', this.props.defaults.redirects.toSignUp) 97 | } 98 | 99 | redirectToForgotPwd = () => { 100 | this.redirect('forgotPwd', this.props.defaults.redirects.toForgotPwd) 101 | } 102 | 103 | redirectToResendVerification = () => { 104 | this.redirect('resendVerification', this.props.defaults.redirects.toResendVerification) 105 | } 106 | } 107 | 108 | const linkStyle = { 109 | display: 'block', 110 | cursor : 'pointer' 111 | } 112 | 113 | export default SignIn 114 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/signUp.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import { Accounts } from 'meteor/accounts-base' 3 | import AccountsReact from '../AccountsReact' 4 | import BaseForm from './baseForm' 5 | import { validateForm } from '../utils' 6 | import { getModel, redirect } from './commonUtils' 7 | import { createUser, login } from './methods' 8 | 9 | class SignUp extends Component { 10 | constructor () { 11 | super() 12 | this.state = { 13 | errors: [], 14 | signUpSuccessful: false 15 | } 16 | 17 | this.getModel = getModel.bind(this) 18 | this.redirect = redirect.bind(this) 19 | } 20 | 21 | render () { 22 | const { 23 | currentState, 24 | defaults 25 | } = this.props 26 | 27 | const { 28 | texts, 29 | hideSignInLink, 30 | showReCaptcha, 31 | sendVerificationEmail 32 | } = defaults 33 | 34 | const { 35 | signUpSuccessful 36 | } = this.state 37 | 38 | return ( 39 | 40 | 49 | 50 | {signUpSuccessful && sendVerificationEmail &&

{texts.info.signUpVerifyEmail}

} 51 | 52 | {!hideSignInLink && ( 53 | 54 | {texts.links.toSignIn} 55 | 56 | )} 57 |
58 | ) 59 | } 60 | 61 | onSubmit = () => { 62 | const model = this.getModel() 63 | // Validate form 64 | if (!validateForm(model, this)) { return } 65 | 66 | const { 67 | username, 68 | email, 69 | password, 70 | confirmPassword, // dont delete so it doesn't get included in profile object. 71 | ...profile 72 | } = this.getModel() 73 | 74 | // The user object to insert 75 | const newUser = { 76 | username, 77 | email, 78 | password: password ? Accounts._hashPassword(password) : '', 79 | ...profile 80 | } 81 | 82 | const { 83 | showReCaptcha, 84 | preSignupHook, 85 | onSubmitHook, 86 | loginAfterSignup 87 | } = this.props.defaults 88 | 89 | // Add recaptcha field 90 | if (showReCaptcha) { 91 | newUser.tempReCaptchaResponse = AccountsReact.config.tempReCaptchaResponse 92 | } 93 | 94 | preSignupHook(password, newUser) 95 | 96 | createUser(newUser, err => { 97 | if (err) { 98 | // validation errors suppose to be inside an array, if string then its a different error 99 | if (typeof err.reason !== 'string') { 100 | this.setState({ errors: err.reason }) 101 | } else { 102 | this.setState({ errors: [{ _id: '__globals', errStr: err.reason }] }) 103 | } 104 | } else { 105 | this.setState({ signUpSuccessful: true }) 106 | 107 | if (loginAfterSignup) { 108 | const { password } = this.getModel() 109 | const { username, email } = newUser 110 | 111 | login(username, email, password, err => { 112 | if (err) { return } // ? 113 | }) 114 | } 115 | } 116 | 117 | onSubmitHook(err, this.props.currentState) 118 | }) 119 | } 120 | 121 | redirectToSignIn = () => { 122 | this.redirect('signIn', this.props.defaults.redirects.toSignIn) 123 | } 124 | } 125 | 126 | const linkStyle = { 127 | display: 'block' 128 | } 129 | 130 | export default SignUp 131 | -------------------------------------------------------------------------------- /lib/AccountsReactComponent/socialButtons.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Accounts } from 'meteor/accounts-base' 3 | import AccountsReact from '../AccountsReact' 4 | 5 | class SocialButtons extends React.Component { 6 | 7 | render () { 8 | if (!Accounts.oauth) { 9 | return null 10 | } 11 | 12 | const services = Accounts.oauth.serviceNames() 13 | const { SubmitField } = AccountsReact.components 14 | return services && services.map((service, i) => { 15 | 16 | return ( 17 | this.loginWith(service)} 20 | social={service} 21 | text={service} 22 | /> 23 | ) 24 | }) 25 | } 26 | 27 | loginWith = service => { 28 | let _service = service[0].toUpperCase() + service.substr(1) 29 | 30 | if (service === 'meteor-developer') { 31 | _service = 'MeteorDeveloperAccount' 32 | } 33 | 34 | const options = AccountsReact.config.oauth[service] || {} 35 | Meteor['loginWith' + _service](options, err => { 36 | if (!err) { 37 | const { onLoginHook } = this.props.defaults 38 | if (onLoginHook) { 39 | onLoginHook() 40 | } 41 | } 42 | }) 43 | } 44 | } 45 | 46 | export default SocialButtons 47 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import AccountsReact from './AccountsReact' 2 | import AccountsReactComponent from './AccountsReactComponent' 3 | 4 | module.exports = { 5 | AccountsReact, 6 | AccountsReactComponent 7 | } 8 | -------------------------------------------------------------------------------- /lib/utils/deepmerge/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Credit to Kyle Mathews 3 | https://github.com/KyleAMathews/deepmerge 4 | */ 5 | 6 | import defaultIsMergeableObject from './is-mergeable-object' 7 | 8 | function emptyTarget(val) { 9 | return Array.isArray(val) ? [] : {} 10 | } 11 | 12 | function cloneUnlessOtherwiseSpecified(value, options) { 13 | return (options.clone !== false && options.isMergeableObject(value)) 14 | ? deepmerge(emptyTarget(value), value, options) 15 | : value 16 | } 17 | 18 | function defaultArrayMerge(target, source, options) { 19 | return target.concat(source).map(function(element) { 20 | return cloneUnlessOtherwiseSpecified(element, options) 21 | }) 22 | } 23 | 24 | function mergeObject(target, source, options) { 25 | var destination = {} 26 | if (options.isMergeableObject(target)) { 27 | Object.keys(target).forEach(function(key) { 28 | destination[key] = cloneUnlessOtherwiseSpecified(target[key], options) 29 | }) 30 | } 31 | Object.keys(source).forEach(function(key) { 32 | if (!options.isMergeableObject(source[key]) || !target[key]) { 33 | destination[key] = cloneUnlessOtherwiseSpecified(source[key], options) 34 | } else { 35 | destination[key] = deepmerge(target[key], source[key], options) 36 | } 37 | }) 38 | return destination 39 | } 40 | 41 | function deepmerge(target, source, options) { 42 | options = options || {} 43 | options.arrayMerge = options.arrayMerge || defaultArrayMerge 44 | options.isMergeableObject = options.isMergeableObject || defaultIsMergeableObject 45 | 46 | var sourceIsArray = Array.isArray(source) 47 | var targetIsArray = Array.isArray(target) 48 | var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray 49 | 50 | if (!sourceAndTargetTypesMatch) { 51 | return cloneUnlessOtherwiseSpecified(source, options) 52 | } else if (sourceIsArray) { 53 | return options.arrayMerge(target, source, options) 54 | } else { 55 | return mergeObject(target, source, options) 56 | } 57 | } 58 | 59 | deepmerge.all = function deepmergeAll(array, options) { 60 | if (!Array.isArray(array)) { 61 | throw new Error('first argument should be an array') 62 | } 63 | 64 | return array.reduce(function(prev, next) { 65 | return deepmerge(prev, next, options) 66 | }, {}) 67 | } 68 | 69 | module.exports = deepmerge 70 | -------------------------------------------------------------------------------- /lib/utils/deepmerge/is-mergeable-object.js: -------------------------------------------------------------------------------- 1 | /* 2 | Credit to Josh Duff 3 | https://github.com/TehShrike/is-mergeable-object 4 | */ 5 | 6 | module.exports = function isMergeableObject(value) { 7 | return isNonNullObject(value) 8 | && !isSpecial(value) 9 | } 10 | 11 | function isNonNullObject(value) { 12 | return !!value && typeof value === 'object' 13 | } 14 | 15 | function isSpecial(value) { 16 | var stringValue = Object.prototype.toString.call(value) 17 | 18 | return stringValue === '[object RegExp]' 19 | || stringValue === '[object Date]' 20 | || isReactElement(value) 21 | } 22 | 23 | // see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25 24 | var canUseSymbol = typeof Symbol === 'function' && Symbol.for 25 | var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7 26 | 27 | function isReactElement(value) { 28 | return value.$$typeof === REACT_ELEMENT_TYPE 29 | } 30 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | import regExp from './regExp' 2 | import validateField, { validateOnChange } from './validateField' 3 | import validateForm from './validateForm' 4 | 5 | export { 6 | regExp, 7 | validateField, 8 | validateOnChange, 9 | validateForm 10 | } 11 | -------------------------------------------------------------------------------- /lib/utils/regExp.js: -------------------------------------------------------------------------------- 1 | 2 | // Credit to aldeed/simple-schema package 3 | 4 | export default { 5 | // Emails with TLD 6 | Email: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+(?:\.[A-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[A-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[A-z0-9]{2,}(?:[a-z0-9-]*[a-z0-9])?$/, 7 | Username: /^[a-zA-Z0-9]+([_ -]?[a-zA-Z0-9])*$/ 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils/validateField.js: -------------------------------------------------------------------------------- 1 | import AccountsReact from '../AccountsReact' 2 | /* Validate fields specified in AccountsReact.config */ 3 | 4 | const validateField = (fields, fieldObj, value, model, errorsArray = []) => { 5 | const { 6 | _id, 7 | required, 8 | func, 9 | re, 10 | minLength, 11 | maxLength, 12 | errStr 13 | } = fieldObj 14 | 15 | // Validate through a function provided by the user 16 | if (func) { 17 | return func(fields, fieldObj, value, model, errorsArray) 18 | } 19 | 20 | // Make sure that a value exists and required is not false to continue validation 21 | if (!value) { 22 | if (required === false) { 23 | // Do nothing 24 | return true 25 | } else { 26 | errorsArray.push({ _id, errStr: errStr || `${_id} is required` }) 27 | return 28 | } 29 | } 30 | 31 | // Validate by regular exporession 32 | if (re && !re.test(value)) { 33 | errorsArray.push({ _id, errStr: errStr || `${value} is not valid as ${_id}` }) 34 | return 35 | } 36 | 37 | // Validate min length 38 | if (minLength && minLength > value.length) { 39 | errorsArray.push({ _id, errStr: errStr || `${_id} length must be at least ${minLength} characters` }) 40 | return 41 | } 42 | 43 | // Validate max length 44 | if (maxLength && maxLength < value.length) { 45 | errorsArray.push({ _id, errStr: errStr || `${_id} length must be no more than ${maxLength} characters` }) 46 | return 47 | } 48 | 49 | return true 50 | } 51 | 52 | /* Validate fields on change events when validateOnChange or validateOnFocusOut are set to true */ 53 | 54 | const validateOnChange = (e, _id, fields, model, errors) => { 55 | const { type, target } = e 56 | const { continuousValidation, negativeValidation } = AccountsReact.config 57 | 58 | // Check the conditions match settings 59 | if ((type === 'blur' && negativeValidation) || (type === 'change' && continuousValidation)) { 60 | const fieldObj = fields.find(f => f._id === _id) 61 | 62 | if (!validateField(fields, fieldObj, target.value, model, errors)) { 63 | return errors 64 | } else { 65 | // Make sure error object for the field doesn't stay after it is valid 66 | return errors.filter(err => err._id !== _id) 67 | } 68 | } 69 | } 70 | 71 | export { 72 | validateField as default, 73 | validateOnChange 74 | } 75 | -------------------------------------------------------------------------------- /lib/utils/validateForm.js: -------------------------------------------------------------------------------- 1 | import validateField from './validateField' 2 | 3 | // Generic form validation for the state components 4 | 5 | const validateForm = (model, context) => { 6 | let _errors = [] 7 | 8 | // Validate login credentials on client, "loginWithPassword" method will validate on server. 9 | const { 10 | currentState, 11 | defaults 12 | } = context.props 13 | 14 | const fields = defaults.fields[currentState] 15 | fields.forEach(field => { 16 | validateField(fields, field, model[field._id], model, _errors) 17 | }) 18 | 19 | if (_errors.length > 0) { 20 | context.setState({ errors: _errors }) 21 | return false 22 | } 23 | return true 24 | } 25 | 26 | export default validateForm 27 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'meteoreact:accounts', 3 | summary: 'Simple and intuative accounts view layer with react', 4 | version: '1.2.4', 5 | documentation: 'README.md', 6 | git: 'https://github.com/royGil/accounts-react' 7 | }) 8 | 9 | Package.onUse(api => { 10 | api.versionsFrom('1.6.1') 11 | 12 | api.use([ 13 | 'ecmascript', 14 | 'accounts-base', 15 | 'accounts-password', 16 | 'mdg:validated-method@1.1.0', 17 | 'check' 18 | ], ['client', 'server']) 19 | 20 | api.use('react-meteor-data@0.2.16', 'client') 21 | 22 | api.use('service-configuration', { weak: true }) 23 | api.use('http', 'server') 24 | 25 | api.mainModule('index.js', ['client', 'server']) 26 | }) 27 | 28 | Package.onTest(api => { 29 | api.use([ 30 | 'ecmascript', 31 | 'accounts-base', 32 | 'accounts-password', 33 | 'meteoreact:accounts', 34 | 'meteoreact:accounts-unstyled', 35 | 'mdg:validated-method@1.1.0', 36 | 'react-meteor-data@0.2.16', 37 | 'cultofcoders:mocha' 38 | ]) 39 | 40 | api.use('http', 'server') 41 | 42 | api.mainModule('__tests__/client.test.js', 'client') 43 | api.mainModule('__tests__/server.test.js', 'server') 44 | }) 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "useraccounts-react", 3 | "version": "1.0.0", 4 | "description": "Meteor's accounts view layer for react", 5 | "main": "index.js", 6 | "scripts": { 7 | "publish": "meteor publish", 8 | "test": "meteor test-packages ./ --driver-package cultofcoders:mocha --port 3006" 9 | }, 10 | "author": "Roy Giladi", 11 | "license": "MIT", 12 | "dependencies": { 13 | "babel-runtime": "^6.26.0", 14 | "deepmerge": "^2.1.0" 15 | }, 16 | "devDependencies": { 17 | "chai": "^4.1.2", 18 | "enzyme": "^3.3.0", 19 | "enzyme-adapter-react-16": "^1.1.1", 20 | "eslint": "^4.19.1", 21 | "eslint-plugin-import": "^2.9.0", 22 | "eslint-plugin-jsx-a11y": "^6.0.3", 23 | "eslint-plugin-react": "^7.7.0", 24 | "eslint-watch": "^3.1.3", 25 | "prop-types": "^15.6.1", 26 | "react": "^16.2.0", 27 | "react-dom": "^16.2.0", 28 | "sinon": "^4.5.0" 29 | } 30 | } 31 | --------------------------------------------------------------------------------