├── .gitignore ├── .versions ├── CHANGELOG.md ├── LICENSE ├── README.md ├── check-npm.js ├── imports ├── accounts_ui.js ├── api │ ├── client │ │ └── loginWithoutPassword.js │ └── server │ │ ├── loginWithoutPassword.js │ │ └── servicesListPublication.js ├── helpers.js ├── login_session.js └── ui │ └── components │ ├── Button.jsx │ ├── Buttons.jsx │ ├── Field.jsx │ ├── Fields.jsx │ ├── Form.jsx │ ├── FormMessage.jsx │ ├── FormMessages.jsx │ ├── LoginForm.jsx │ ├── PasswordOrService.jsx │ └── SocialButtons.jsx ├── main_client.js ├── main_server.js ├── package.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | .idea 7 | npm-debug.log 8 | node_modules 9 | dist 10 | *.icloud 11 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.4.2 2 | allow-deny@1.1.0 3 | babel-compiler@7.0.3 4 | babel-runtime@1.2.0 5 | base64@1.0.10 6 | binary-heap@1.0.10 7 | boilerplate-generator@1.4.0 8 | caching-compiler@1.1.9 9 | callback-hook@1.1.0 10 | check@1.3.0 11 | coffeescript@1.0.17 12 | ddp@1.4.0 13 | ddp-client@2.3.1 14 | ddp-common@1.4.0 15 | ddp-rate-limiter@1.0.7 16 | ddp-server@2.1.2 17 | deps@1.0.12 18 | diff-sequence@1.1.0 19 | dynamic-import@0.3.0 20 | ecmascript@0.10.0 21 | ecmascript-runtime@0.5.0 22 | ecmascript-runtime-client@0.6.0 23 | ecmascript-runtime-server@0.5.0 24 | ejson@1.1.0 25 | email@1.2.3 26 | geojson-utils@1.0.10 27 | http@1.4.0 28 | id-map@1.1.0 29 | localstorage@1.2.0 30 | logging@1.1.19 31 | meteor@1.8.2 32 | minimongo@1.4.3 33 | modules@0.11.3 34 | modules-runtime@0.9.1 35 | mongo@1.4.2 36 | mongo-dev-server@1.1.0 37 | mongo-id@1.0.6 38 | npm-mongo@2.2.33 39 | ordered-dict@1.1.0 40 | promise@0.10.1 41 | random@1.1.0 42 | rate-limit@1.0.8 43 | react-meteor-data@0.2.15 44 | reactive-dict@1.2.0 45 | reactive-var@1.0.11 46 | reload@1.2.0 47 | retry@1.1.0 48 | routepolicy@1.0.12 49 | service-configuration@1.0.11 50 | session@1.1.7 51 | socket-stream-client@0.1.0 52 | softwarerero:accounts-t9n@1.3.3 53 | std:accounts-ui@1.3.1 54 | tmeasday:check-npm-versions@0.3.0 55 | tracker@1.1.3 56 | underscore@1.0.10 57 | url@1.2.0 58 | webapp@1.5.0 59 | webapp-hashing@1.0.9 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | ### v1.3.1 4 | 08-Feb-2018 5 | 6 | * Fixed #126. 7 | 8 | ### v1.3 9 | 12-Nov-2017 10 | 11 | * Updated LoginForm to be compatible with react-meteor-data 0.2.15 #131. 12 | * Updated react-meteor-data dependency to 0.2.15. 13 | 14 | ### v1.2.23 15 | 15-Jun-2017 16 | 17 | * Removed manual calls to React.PropTypes #111 @jLouzado 18 | 19 | ### v1.2.22 20 | 15-Jun-2017 21 | 22 | * Fixed issue with faulty formState proptype. 23 | * Fixed issue with faulty object iteration. 24 | * Removed lodash dependency. 25 | * Removed tracker dependency. 26 | 27 | ### v1.2.21 28 | 26-May-2017 29 | 30 | * Added functionality to include your own translation function. 31 | * Replaced dependency on tracker-component with react-meteor-data. 32 | 33 | ### v1.2.20 34 | 13-March-2017 35 | 36 | * Fixed an issue with imports when using react router. 37 | 38 | ### v1.2.19 39 | 16-February-2017 40 | 41 | * Fixed an issue with imports when using latest version of react router. 42 | 43 | ### v1.2.18 44 | 6-February-2017 45 | 46 | * #94 - Better support for Server-Side Rendering & client-only code in React client-only lifecycle hook 47 | 48 | ### v1.2.17 49 | 7-January-2017 50 | 51 | * #55 - Create new form state for Enroll Account 52 | 53 | ### v1.2.16 54 | 7-January-2017 55 | 56 | * Added warning on misconfigured LoginForm usage, that could prevent users from 57 | resetting their password. 58 | 59 | ### v1.2.15 60 | 7-January-2017 61 | 62 | * #91 - Fixed: localStorage not defined in server side 63 | * Improving experience on successful reset password. 64 | * Reset faulty redirect to reset-password. 65 | 66 | ### v1.2.14 67 | 7-January-2017 68 | 69 | * Fixed issue with troublesome redirect in React Router when clicking link to 70 | reset password. 71 | 72 | ### v1.2.13 73 | 6-January-2017 74 | 75 | * Fixed issue with faulty duplicate use of componentDidMount in LoginForm. 76 | 77 | ### v1.2.12 78 | 6-January-2017 79 | 80 | * #49 - Support for inline field validation message 81 | * Added missing deprecation notices. 82 | 83 | ### v1.2.11 84 | 18-December-2016 85 | 86 | * #61 - BUG: Error «Need to set a username or email» when email is set 87 | * Solved #61 by adding functionality to remember entered values in localStorage, 88 | which also makes it possible to remember values between routes (i.e. when 89 | switching between /login and /register). 90 | 91 | ### v1.2.10 92 | 14-December-2016 93 | 94 | * #82 - Fix for empty `input.value` issue and form prefilled issues 95 | * #84 - Quick fix to redirect login/logout 96 | * #75 - Fix issue, when message is object in Accounts.ui.FormMessage 97 | * #58 - call onSubmitHook after all form submissions 98 | 99 | ### v1.2.9 100 | 10-November-2016 101 | 102 | * #73 – in constructor, we should use `props` and not `this.props` 103 | * #78 – Move react packages to peerDependencies 104 | * Added support for React Router Link in buttons. 105 | 106 | ### v1.2.8 107 | 26-October-2016 108 | 109 | * #70 – Added link to new material UI package. 110 | * #71 – make sure nextProps.formState actually exists before overwriting state 111 | 112 | ### v1.2.7 113 | 19-October-2016 114 | 115 | * Make sure `nextProps.formState` actually exists before overwriting 116 | `state.formState`. 117 | 118 | ### v1.2.6 119 | 2-June-2016 120 | 121 | * Allow form state to be set from prop formState when logged in #51 @todda00 122 | 123 | ### v1.2.4-5 124 | 28-May-2016 125 | 126 | * Adding missing configuration in oauth services. 127 | 128 | ### v1.2.2-3 129 | 24-May-2016 130 | 131 | * Solves issue with social redirect flow being redirected to a faulty urls: #36 132 | * Solves issue: Accounts.sendLoginEmail does not work if address is set: #42 133 | 134 | ### v1.2.1 135 | 10-May-2016 136 | 137 | * Solves issue with props not being passed down: #39 138 | 139 | ### v1.2.0 140 | 10-May-2016 141 | 142 | * Adding the hooks to be passed as props. 143 | 144 | ### v1.1.19 145 | 146 | * Improving hooks for server side rendered pages. 147 | * Improving so that browser pre-filled input values are pushed back to the form 148 | state. 149 | 150 | ### v1.1.18 151 | 152 | * Updated Tracker dependency. 153 | 154 | ### v1.1.17 155 | 156 | * Updated Tracker dependency. 157 | 158 | ### v1.1.16 159 | 160 | * Bumping version on check-npm-versions to solve #29 161 | 162 | ### v1.1.15 163 | 164 | * @SachaG added classes to the social buttons distinguishing which service. 165 | 166 | ### v1.1.14 167 | 168 | * @SachaG added tmeasday:check-npm-versions to check for the correct version of 169 | npm packages. 170 | * @ArthurPai updated T9n, which adds the Chinese language for accounts, so we 171 | can update it to v1.3.3 172 | * @ArthurPai fixed a forgotten update T9n translation in the PasswordOrService 173 | component. 174 | * @PolGuixe fixed the faulty meteor-developer account integration. 175 | 176 | ### v1.1.13 177 | 178 | * Fixed faulty language strings. 179 | 180 | ### v1.1.12 181 | 182 | * Updated to use the latest translations in softwarerero:accounts-t9n 183 | 184 | ### v1.1.11 185 | 186 | * Updated to softwarerero:accounts-t9n@1.3.1 187 | * Don't show change password link if using NO_PASSWORD. 188 | 189 | ### v1.1.10 190 | 191 | * Fixed a bug that caused the form when submitted to reload the page, related: 192 | https://github.com/studiointeract/accounts-ui/issues/18 193 | 194 | ### v1.1.9 195 | 196 | * Fixed a faulty default setting, that got replaced in 1.0.21. 197 | 198 | ### v1.1.8 199 | 200 | * Added notice on missing login services. 201 | 202 | ### v1.1.7 203 | 204 | * Upgraded dependency of softwarerero:accounts-t9n to 1.3.0, related: 205 | https://github.com/studiointeract/accounts-ui/issues/15 206 | 207 | ### v1.1.6 208 | 209 | * Removed server side version of onPostSignUpHook, related issues: 210 | https://github.com/studiointeract/accounts-ui/issues/17 211 | https://github.com/studiointeract/accounts-ui/issues/16 212 | 213 | ### v1.1.5 214 | 215 | * Improving and removing redundant logging. 216 | 217 | ### v1.1.4 218 | 219 | * Bugfixes for Telescope Nova 220 | 221 | ### v1.1.1-3 222 | 223 | * Bugfixes 224 | 225 | ### v1.1.0 226 | 227 | * Renamed package to std:accounts-ui 228 | 229 | ### v1.0.21 230 | 231 | * Buttons for oauth services 232 | * Option for "NO_PASSWORD" changed to "EMAIL_ONLY_NO_PASSWORD" 233 | * Added new options to accounts-password "USERNAME_AND_EMAIL_NO_PASSWORD". 234 | 235 | ### v1.0.20 236 | 237 | * Clear the password when logging in or out. 238 | 239 | ### v1.0.19 240 | 241 | * Added defaultValue to fields, so that when switching formState the form keeps 242 | the value it had from the other form. Which has always been a really great 243 | experience with Meteor accounts-ui. 244 | 245 | ### v1.0.18 246 | 247 | * Bug fixes 248 | 249 | ### v1.0.17 250 | 251 | * Added so that the formState responds to user logout from the terminal. 252 | 253 | ### v1.0.16 254 | 255 | * Bug fix 256 | 257 | ### v1.0.15 258 | 259 | * Added required boolean to Fields 260 | * Added type to message and changed to Object type 261 | * Added ready boolean to form. 262 | 263 | ### v1.0.12-14 264 | 265 | * Bug fixes 266 | 267 | ### v1.0.11 268 | 269 | * Bump version of Tracker.Component 270 | 271 | ### v1.0.10 272 | 273 | * Support for extending with more fields. 274 | 275 | ### v1.0.1-9 276 | 277 | * Bugfixes 278 | 279 | ### v1.0.0 280 | 281 | * Fully featured accounts-password 282 | * Support for "NO_PASSWORD". 283 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Studio Interact Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Accounts UI 2 | 3 | Current version 1.3.0 4 | 5 | ## Features 6 | 7 | 1. **[Easy to use](#using-react-accounts-ui)**, mixing the ideas of useraccounts configuration and accounts-ui that everyone already knows and loves. 8 | 2. **[Components](#available-components)** are everywhere, and extensible by replacing them on Accounts.ui. 9 | 3. **[Basic routing](#configuration)** included, redirections when the user clicks a link in an email or when signing in or out. 10 | 4. **[Unstyled](#styling)** is the default, no CSS included. 11 | 5. **[No password](#no-password-required)** sign up and sign in are included. 12 | 6. **[Extra fields](#extra-fields)** is now supported. 13 | 7. **[Extending](#create-your-own-styled-version)** to make your own custom form, for your app, or as a package, all components can be extended and customized. 14 | 8. **[States API](#example-setup-using-the-states-api)** makes it possible to use the form on different routes, say you want the login on one route and signup on another, just set the inital state and the links (either globally or per component by using the props). 15 | 9. **[React Router](#example-setup-using-react-router-meteor-13)** is fully supported, see the example how to use with React Router. 16 | 10. **[FlowRouter](#example-setup-using-flowrouter-meteor-13)** is fully supported, see the example how to use with FlowRouter. 17 | 11. **[Server Side Rendering](#example-setup-using-flowrouter-meteor-13)** is easily setup, see how it's done with FlowRouter (SSR). An example for React Router using [react-router-ssr](https://github.com/thereactivestack/meteor-react-router-ssr) coming shortly. 18 | 19 | ## Styling 20 | 21 | This package does not by standard come with any styling, you can easily [extend and make your own](#create-your-own-styled-version), here are a couple versions we've made for the typical use case: 22 | 23 | * [**Basic**](https://atmospherejs.com/std/accounts-basic) `std:accounts-basic` 24 | * [**Semantic UI**](https://atmospherejs.com/std/accounts-semantic) `std:accounts-semantic` 25 | * [**Bootstrap 3/4**](https://atmospherejs.com/std/accounts-bootstrap) `std:accounts-bootstrap` 26 | * [**Ionic**](https://atmospherejs.com/std/accounts-ionic) `std:accounts-ionic` 27 | * [**Material UI**](https://atmospherejs.com/zetoff/accounts-material-ui) `zetoff:accounts-material-ui` 28 | 29 | * Add your styled version here [Learn how](#create-your-own-styled-version) 30 | 31 | ## Installation 32 | 33 | `meteor add std:accounts-ui` 34 | 35 | ## Configuration 36 | 37 | We support the standard [configuration in the account-ui package](http://docs.meteor.com/#/full/accounts_ui_config). But have extended with some new options. 38 | 39 | ### Accounts.ui.config(options) 40 | 41 | `import { Accounts } from 'meteor/std:accounts-ui'` 42 | 43 | Configure the behavior of `` 44 | 45 | Example configuration: 46 | 47 | ```javascript 48 | Accounts.config({ 49 | sendVerificationEmail: true, 50 | forbidClientAccountCreation: false 51 | }); 52 | 53 | Accounts.ui.config({ 54 | passwordSignupFields: 'EMAIL_ONLY', 55 | loginPath: '/login', 56 | signUpPath: '/signup', 57 | resetPasswordPath: '/reset-password', 58 | profilePath: '/profile', 59 | onSignedInHook: () => FlowRouter.go('/general'), 60 | onSignedOutHook: () => FlowRouter.go('/login'), 61 | minimumPasswordLength: 6 62 | }); 63 | ``` 64 | 65 | ### Version 1.2 also supports passing hooks through props to the component. 66 | 67 | ```js 68 | import { Accounts } from 'meteor/std:accounts-ui'; 69 | 70 | console.log('user signed in') } 72 | /> 73 | ``` 74 | 75 | **_Options:_** 76 | 77 | * **requestPermissions**    Object 78 | Which [permissions](http://docs.meteor.com/#requestpermissions) to request from the user for each external service. 79 | 80 | * **requestOfflineToken**    Object 81 | 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](http://docs.meteor.com/#meteor_loginwithexternalservice) for more details. 82 | 83 | * **forceApprovalPrompt**    Boolean 84 | If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google. 85 | 86 | * **passwordSignupFields**    String 87 | Which fields to display in the user creation form. One of `'USERNAME_AND_EMAIL'`, `'USERNAME_AND_OPTIONAL_EMAIL'`, `'USERNAME_ONLY'`, `'EMAIL_ONLY'`, `'USERNAME_AND_EMAIL_NO_PASSWORD'`, **`'EMAIL_ONLY_NO_PASSWORD'`** (**default**). 88 | 89 | * **requireEmailVerification**    Boolean 90 | Set if the login *without password* should check if the user is verified before sending any login emails. Default is **false**. 91 | 92 | * **minimumPasswordLength**    Number 93 | Set the minimum number of password length for your application. Default is **7**. 94 | 95 | * **homeRoutePath**    String 96 | Set the path to where you would like the user to be redirected after a successful login or sign out. 97 | 98 | * **loginPath**    String 99 | Change the default path a user should be redirected to after a clicking a link in a mail provided to them from the accounts system, it could be a mail set to them when they have reset their password, verifying their email if the setting for `sendVerificationEmail` is turned on ([read more on accounts configuration ](http://docs.meteor.com/#/full/accounts_config)). Can also be set as a property to the LoginForm, for i18n routes or other customization. 100 | 101 | * **signUpPath**    String 102 | Set the path to where you would like the sign up links to link to rather than changing the state on the current page. Can also be set as a property to the LoginForm, for i18n routes or other customization. 103 | 104 | * **resetPasswordPath**    String 105 | Set the path to where you would like the link to reset password to go to rather than changing the state on the current page. Can also be set as a property to the LoginForm, for i18n routes or other customization. 106 | 107 | * **profilePath**    String 108 | Set the path to where you would like the link to the profile to go to rather than changing the state on the current page. Can also be set as a property to the LoginForm, for i18n routes or other customization. 109 | 110 | * **changePasswordPath**    String 111 | Set the path to where you would like the link to change password to go to rather than changing the state on the current page. Can also be set as a property to the LoginForm, for i18n routes or other customization. 112 | 113 | * **onSubmitHook**    function(error, state) 114 | Called when the LoginForm is being submitted: allows for custom actions to be taken on form submission. error contains possible errors occurred during the submission process, state specifies the LoginForm internal state from which the submission was triggered. A nice use case might be closing the modal or side-menu or dropdown showing LoginForm. You can get all the possible states by import `STATES` from this package. 115 | 116 | * **onPreSignUpHook**    function(options) 117 | Called just before submitting the LoginForm for sign-up: allows for custom actions on the data being submitted. A nice use could be extending the user profile object accessing options.profile. to be taken on form submission. The plain text password is also provided for any reasonable use. If you return a promise, the submission will wait until you resolve it. 118 | 119 | * **onPostSignUpHook**    func(options, user) 120 | Called client side, just after a successful user account creation, post submitting the form for sign-up: allows for custom actions on the data being submitted after we are sure a new user was successfully created. 121 | 122 | * **onResetPasswordHook**    function() 123 | Change the default redirect behavior when the user clicks the link to reset their email sent from the system, i.e. you want a custom path for the reset password form. Default is **loginPath**. 124 | 125 | * **onEnrollAccountHook**    function() 126 | Change the default redirect behavior when the user clicks the link to enroll for an account sent from the system, i.e. you want a custom path for the enrollment form. Learn more about [how to send enrollment emails](http://docs.meteor.com/#/full/accounts_sendenrollmentemail). Default is **loginPath**. 127 | 128 | * **onVerifyEmailHook**    function() 129 | Change the default redirect behavior when the user clicks the link to verify their email sent from the system, i.e. you want a custom path after the user verifies their email or login with `EMAIL_ONLY_NO_PASSWORD`. Default is **profilePath**. 130 | 131 | * **onSignedInHook**    function() 132 | Change the default redirect behavior when the user successfully login to your application, i.e. you want a custom path for the reset password form. Default is **profilePath**. 133 | 134 | * **onSignedOutHook**    function() 135 | Change the default redirect behavior when the user signs out using the LoginForm, i.e. you want a custom path after the user signs out. Default is **homeRoutePath**. 136 | 137 | * **emailPattern**    new RegExp() 138 | Change how emails are validated on the client, i.e. require specific domain or pattern for an email. Default is **new RegExp('[^@]+@[^@\.]{2,}\.[^\.@]+')**. 139 | 140 | ## No password required 141 | 142 | This package provides a state that makes it possible to create and manage accounts without a password. The idea is simple, you always verify your email, so to login you enter your mail and the system emails you a link to login. The mail that is sent can be changed if needed, just [how you alter the email templates in accounts-base](http://docs.meteor.com/#/full/accounts_emailtemplates). 143 | 144 | This is the default setting for **passwordSignupFields** in the [configuration](#configuration). 145 | 146 | ## Using React Accounts UI 147 | 148 | ### Example setup (Meteor 1.3) 149 | 150 | `meteor add accounts-password` 151 | `meteor add std:accounts-ui` 152 | 153 | ```javascript 154 | 155 | import React from 'react'; 156 | import { Accounts } from 'meteor/std:accounts-ui'; 157 | 158 | Accounts.ui.config({ 159 | passwordSignupFields: 'EMAIL_ONLY_NO_PASSWORD', 160 | loginPath: '/', 161 | }); 162 | 163 | if (Meteor.isClient) { 164 | ReactDOM.render(, document.body) 165 | } 166 | 167 | ``` 168 | 169 | ### Example setup using React Router (Meteor 1.3) 170 | 171 | Following the [Application Structure from the Meteor Guide](http://guide.meteor.com/v1.3/structure.html). 172 | 173 | `npm i --save react react-dom react-router` 174 | `meteor add accounts-password` 175 | `meteor add std:accounts-ui` 176 | 177 | ```javascript 178 | import React from 'react'; 179 | import { render } from 'react-dom'; 180 | import { Router, Route, IndexRoute, browserHistory } from 'react-router'; 181 | import { Accounts, STATES } from 'meteor/std:accounts-ui'; 182 | 183 | import { App } from '../../ui/layouts/app.jsx'; 184 | import { Index } from '../../ui/components/index.jsx'; 185 | 186 | import { Hello } from '../../ui/pages/hello.jsx'; 187 | import { Admin } from '../../ui/pages/admin.jsx'; 188 | import { NotFound } from '../../ui/pages/not-found.jsx'; 189 | 190 | Meteor.startup( () => { 191 | render( 192 | 193 | 194 | 195 | } /> 196 | } /> 197 | 198 | 199 | 200 | 201 | 202 | 203 | , 204 | document.getElementById( 'react-root' ) 205 | ); 206 | }); 207 | ``` 208 | 209 | You can learn more about the remaining components here in the tutorial on [React Router Basics](https://themeteorchef.com/snippets/react-router-basics/) by the Meteor Chef. 210 | 211 | 212 | ### Example setup using FlowRouter (Meteor 1.3) 213 | 214 | `npm i --save react react-dom` 215 | `meteor add accounts-password` 216 | `meteor add std:accounts-ui` 217 | `meteor add kadira:flow-router-ssr` 218 | 219 | ```javascript 220 | 221 | import React from 'react'; 222 | import { Accounts } from 'meteor/std:accounts-ui'; 223 | import { FlowRouter } from 'meteor/kadira:flow-router-ssr'; 224 | 225 | Accounts.ui.config({ 226 | passwordSignupFields: 'EMAIL_ONLY_NO_PASSWORD', 227 | loginPath: '/login', 228 | onSignedInHook: () => FlowRouter.go('/general'), 229 | onSignedOutHook: () => FlowRouter.go('/') 230 | }); 231 | 232 | FlowRouter.route("/login", { 233 | action(params) { 234 | mount(MainLayout, { 235 | content: 236 | }); 237 | } 238 | }); 239 | 240 | ``` 241 | 242 | ### Example setup using the STATES api. 243 | 244 | You can define the inital state you want in your route for the component, 245 | as set the path where the links in the component link to, for example below we 246 | have one route for /login and one for /signup. 247 | 248 | `meteor add accounts-password` 249 | `meteor add std:accounts-ui` 250 | `meteor add softwarerero:accounts-t9n` 251 | `meteor add kadira:flow-router-ssr` 252 | 253 | ```javascript 254 | import React from 'react'; 255 | import { Accounts, STATES } from 'meteor/std:accounts-ui'; 256 | import { T9n } from 'meteor/softwarerero:accounts-t9n'; 257 | 258 | T9n.setLanguage('en'); 259 | 260 | Accounts.config({ 261 | sendVerificationEmail: true, 262 | forbidClientAccountCreation: false 263 | }); 264 | 265 | Accounts.ui.config({ 266 | passwordSignupFields: 'USERNAME_AND_OPTIONAL_EMAIL', 267 | loginPath: '/login' 268 | }); 269 | 270 | FlowRouter.route("/login", { 271 | action(params) { 272 | mount(MainLayout, { 273 | content: 276 | }); 277 | } 278 | }); 279 | 280 | FlowRouter.route("/signup", { 281 | action(params) { 282 | mount(MainLayout, { 283 | content: 287 | }); 288 | } 289 | }); 290 | ``` 291 | 292 | ## Create your own styled version 293 | 294 | **To you who are a package author**, its easy to write extensions for `std:accounts-ui` by importing and export like the following example: 295 | 296 | ```javascript 297 | // package.js 298 | 299 | Package.describe({ 300 | name: 'author:accounts-bootstrap', 301 | version: '1.0.0', 302 | summary: 'Bootstrap – Accounts UI for React in Meteor 1.3', 303 | git: 'https://github.com/author/accounts-bootstrap', 304 | documentation: 'README.md' 305 | }); 306 | 307 | Package.onUse(function(api) { 308 | api.versionsFrom('1.3'); 309 | api.use('ecmascript'); 310 | api.use('std:accounts-ui'); 311 | 312 | api.imply('session'); 313 | 314 | api.mainModule('main.jsx'); 315 | }); 316 | ``` 317 | 318 | ```javascript 319 | // package.json 320 | 321 | { 322 | "name": "accounts-bootstrap", 323 | "description": "Bootstrap – Accounts UI for React in Meteor 1.3", 324 | "repository": { 325 | "type": "git", 326 | "url": "https://github.com/author/accounts-bootstrap.git" 327 | }, 328 | "keywords": [ 329 | "react", 330 | "meteor", 331 | "accounts", 332 | "tracker" 333 | ], 334 | "author": "author", 335 | "license": "MIT", 336 | "bugs": { 337 | "url": "https://github.com/author/accounts-bootstrap/issues" 338 | }, 339 | "homepage": "https://github.com/author/accounts-bootstrap", 340 | "dependencies": { 341 | "react": "^15.x", 342 | "react-dom": "^15.x", 343 | "tracker-component": "^1.3.13" 344 | } 345 | } 346 | 347 | ``` 348 | 349 | To install the dependencies added in your package.json run: 350 | `npm i` 351 | 352 | ```javascript 353 | // main.jsx 354 | 355 | import React from 'react'; 356 | import PropTypes from 'prop-types'; 357 | import { Accounts, STATES } from 'meteor/std:accounts-ui'; 358 | 359 | /** 360 | * Form.propTypes = { 361 | * fields: PropTypes.object.isRequired, 362 | * buttons: PropTypes.object.isRequired, 363 | * error: PropTypes.string, 364 | * ready: PropTypes.bool 365 | * }; 366 | */ 367 | class Form extends Accounts.ui.Form { 368 | render() { 369 | const { fields, buttons, error, message, ready = true} = this.props; 370 | return ( 371 |
evt.preventDefault() } className="accounts-ui"> 372 | 373 | 374 | 375 | 376 | ); 377 | } 378 | } 379 | 380 | class Buttons extends Accounts.ui.Buttons {} 381 | class Button extends Accounts.ui.Button {} 382 | class Fields extends Accounts.ui.Fields {} 383 | class Field extends Accounts.ui.Field {} 384 | class FormMessage extends Accounts.ui.FormMessage {} 385 | // Notice! Accounts.ui.LoginForm manages all state logic 386 | // at the moment, so avoid overwriting this one, but have 387 | // a look at it and learn how it works. And pull 388 | // requests altering how that works are welcome. 389 | 390 | // Alter provided default unstyled UI. 391 | Accounts.ui.Form = Form; 392 | Accounts.ui.Buttons = Buttons; 393 | Accounts.ui.Button = Button; 394 | Accounts.ui.Fields = Fields; 395 | Accounts.ui.Field = Field; 396 | Accounts.ui.FormMessage = FormMessage; 397 | 398 | // Export the themed version. 399 | export { Accounts, STATES }; 400 | export default Accounts; 401 | 402 | ``` 403 | 404 | ### Available components 405 | 406 | * Accounts.ui.LoginForm 407 | * Accounts.ui.Form 408 | * Accounts.ui.Fields 409 | * Accounts.ui.Field 410 | * Accounts.ui.Buttons 411 | * Accounts.ui.Button 412 | * Accounts.ui.FormMessage 413 | * Accounts.ui.PasswordOrService 414 | * Accounts.ui.SocialButtons 415 | 416 | ## Extra fields 417 | 418 | > Example provided by [@radzom](https://github.com/radzom). 419 | 420 | ```javascript 421 | import { Accounts, STATES } from 'meteor/std:accounts-ui'; 422 | 423 | class NewLogin extends Accounts.ui.LoginForm { 424 | fields() { 425 | const { formState } = this.state; 426 | if (formState == STATES.SIGN_UP) { 427 | return { 428 | firstname: { 429 | id: 'firstname', 430 | hint: 'Enter firstname', 431 | label: 'firstname', 432 | onChange: this.handleChange.bind(this, 'firstname') 433 | }, 434 | ...super.fields() 435 | }; 436 | } 437 | return super.fields(); 438 | } 439 | 440 | translate(text) { 441 | // Here you specify your own translation function, e.g. 442 | return this.props.t(text); 443 | } 444 | 445 | signUp(options = {}) { 446 | const { firstname = null } = this.state; 447 | if (firstname !== null) { 448 | options.profile = Object.assign(options.profile || {}, { 449 | firstname: firstname 450 | }); 451 | } 452 | super.signUp(options); 453 | } 454 | } 455 | ``` 456 | 457 | And on the server you can store the extra fields like this: 458 | 459 | ```javascript 460 | import { Accounts } from 'meteor/accounts-base'; 461 | 462 | Accounts.onCreateUser(function (options, user) { 463 | user.profile = options.profile || {}; 464 | user.roles = {}; 465 | return user; 466 | }); 467 | ``` 468 | 469 | ## Deprecations 470 | 471 | ### v1.2.11 472 | * The use of FormMessage in Form has been deprecated in favor of using 473 | FormMessages that handles multiple messages and errors. 474 | See example: [Form.jsx#L43](imports/ui/components/Form.jsx#L43) 475 | 476 | * Implementations of Accounts.ui.Field must render a message. 477 | See example: [Field.jsx#L](imports/ui/components/Field.jsx#L64-L67) 478 | 479 | ## Credits 480 | 481 | Made by the [creative folks at Studio Interact](http://studiointeract.com) and 482 | [all the wonderful people using and improving this package](https://github.com/studiointeract/accounts-ui/graphs/contributors). 483 | -------------------------------------------------------------------------------- /check-npm.js: -------------------------------------------------------------------------------- 1 | // import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; 2 | // 3 | // checkNpmVersions({ 4 | // "react": ">=0.14.7 || ^15.0.0-rc.2", 5 | // "react-dom": ">=0.14.7 || ^15.0.0-rc.2", 6 | // }); 7 | -------------------------------------------------------------------------------- /imports/accounts_ui.js: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base'; 2 | import { 3 | redirect, 4 | validatePassword, 5 | validateEmail, 6 | validateUsername, 7 | } from './helpers.js'; 8 | 9 | /** 10 | * @summary Accounts UI 11 | * @namespace 12 | * @memberOf Accounts 13 | */ 14 | Accounts.ui = {}; 15 | 16 | Accounts.ui._options = { 17 | requestPermissions: [], 18 | requestOfflineToken: {}, 19 | forceApprovalPrompt: {}, 20 | requireEmailVerification: false, 21 | passwordSignupFields: 'EMAIL_ONLY_NO_PASSWORD', 22 | minimumPasswordLength: 7, 23 | loginPath: '/', 24 | signUpPath: null, 25 | resetPasswordPath: null, 26 | profilePath: '/', 27 | changePasswordPath: null, 28 | homeRoutePath: '/', 29 | onSubmitHook: () => {}, 30 | onPreSignUpHook: () => new Promise(resolve => resolve()), 31 | onPostSignUpHook: () => {}, 32 | onEnrollAccountHook: () => redirect(`${Accounts.ui._options.loginPath}`), 33 | onResetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`), 34 | onVerifyEmailHook: () => redirect(`${Accounts.ui._options.profilePath}`), 35 | onSignedInHook: () => redirect(`${Accounts.ui._options.homeRoutePath}`), 36 | onSignedOutHook: () => redirect(`${Accounts.ui._options.homeRoutePath}`), 37 | emailPattern: new RegExp('[^@]+@[^@\.]{2,}\.[^\.@]+'), 38 | }; 39 | 40 | /** 41 | * @summary Configure the behavior of [``](#react-accounts-ui). 42 | * @anywhere 43 | * @param {Object} options 44 | * @param {Object} options.requestPermissions Which [permissions](#requestpermissions) to request from the user for each external service. 45 | * @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. 46 | * @param {Object} options.forceApprovalPrompt If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google. 47 | * @param {String} options.passwordSignupFields Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', '`EMAIL_ONLY`', or '`NO_PASSWORD`' (default). 48 | */ 49 | Accounts.ui.config = function(options) { 50 | // validate options keys 51 | const VALID_KEYS = [ 52 | 'passwordSignupFields', 53 | 'requestPermissions', 54 | 'requestOfflineToken', 55 | 'forbidClientAccountCreation', 56 | 'requireEmailVerification', 57 | 'minimumPasswordLength', 58 | 'loginPath', 59 | 'signUpPath', 60 | 'resetPasswordPath', 61 | 'profilePath', 62 | 'changePasswordPath', 63 | 'homeRoutePath', 64 | 'onSubmitHook', 65 | 'onPreSignUpHook', 66 | 'onPostSignUpHook', 67 | 'onEnrollAccountHook', 68 | 'onResetPasswordHook', 69 | 'onVerifyEmailHook', 70 | 'onSignedInHook', 71 | 'onSignedOutHook', 72 | 'validateField', 73 | 'emailPattern', 74 | ]; 75 | 76 | Object.keys(options).forEach(function (key) { 77 | if (!VALID_KEYS.includes(key)) 78 | throw new Error("Accounts.ui.config: Invalid key: " + key); 79 | }); 80 | 81 | // Deal with `passwordSignupFields` 82 | if (options.passwordSignupFields) { 83 | if ([ 84 | "USERNAME_AND_EMAIL", 85 | "USERNAME_AND_OPTIONAL_EMAIL", 86 | "USERNAME_ONLY", 87 | "EMAIL_ONLY", 88 | "EMAIL_ONLY_NO_PASSWORD", 89 | "USERNAME_AND_EMAIL_NO_PASSWORD" 90 | ].includes(options.passwordSignupFields)) { 91 | Accounts.ui._options.passwordSignupFields = options.passwordSignupFields; 92 | } 93 | else { 94 | throw new Error("Accounts.ui.config: Invalid option for `passwordSignupFields`: " + options.passwordSignupFields); 95 | } 96 | } 97 | 98 | // Deal with `requestPermissions` 99 | if (options.requestPermissions) { 100 | Object.keys(options.requestPermissions).forEach(service => { 101 | const scope = options.requestPermissions[service]; 102 | if (Accounts.ui._options.requestPermissions[service]) { 103 | throw new Error("Accounts.ui.config: Can't set `requestPermissions` more than once for " + service); 104 | } 105 | else if (!(scope instanceof Array)) { 106 | throw new Error("Accounts.ui.config: Value for `requestPermissions` must be an array"); 107 | } 108 | else { 109 | Accounts.ui._options.requestPermissions[service] = scope; 110 | } 111 | }); 112 | } 113 | 114 | // Deal with `requestOfflineToken` 115 | if (options.requestOfflineToken) { 116 | Object.keys(options.requestOfflineToken).forEach(service => { 117 | const value = options.requestOfflineToken[service]; 118 | if (service !== 'google') 119 | throw new Error("Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment."); 120 | 121 | if (Accounts.ui._options.requestOfflineToken[service]) { 122 | throw new Error("Accounts.ui.config: Can't set `requestOfflineToken` more than once for " + service); 123 | } 124 | else { 125 | Accounts.ui._options.requestOfflineToken[service] = value; 126 | } 127 | }); 128 | } 129 | 130 | // Deal with `forceApprovalPrompt` 131 | if (options.forceApprovalPrompt) { 132 | Object.keys(options.forceApprovalPrompt).forEach(service => { 133 | const value = options.forceApprovalPrompt[service]; 134 | if (service !== 'google') 135 | throw new Error("Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment."); 136 | 137 | if (Accounts.ui._options.forceApprovalPrompt[service]) { 138 | throw new Error("Accounts.ui.config: Can't set `forceApprovalPrompt` more than once for " + service); 139 | } 140 | else { 141 | Accounts.ui._options.forceApprovalPrompt[service] = value; 142 | } 143 | }); 144 | } 145 | 146 | // Deal with `requireEmailVerification` 147 | if (options.requireEmailVerification) { 148 | if (typeof options.requireEmailVerification != 'boolean') { 149 | throw new Error(`Accounts.ui.config: "requireEmailVerification" not a boolean`); 150 | } 151 | else { 152 | Accounts.ui._options.requireEmailVerification = options.requireEmailVerification; 153 | } 154 | } 155 | 156 | // Deal with `minimumPasswordLength` 157 | if (options.minimumPasswordLength) { 158 | if (typeof options.minimumPasswordLength != 'number') { 159 | throw new Error(`Accounts.ui.config: "minimumPasswordLength" not a number`); 160 | } 161 | else { 162 | Accounts.ui._options.minimumPasswordLength = options.minimumPasswordLength; 163 | } 164 | } 165 | 166 | // Deal with the hooks. 167 | for (let hook of [ 168 | 'onSubmitHook', 169 | 'onPreSignUpHook', 170 | 'onPostSignUpHook', 171 | ]) { 172 | if (options[hook]) { 173 | if (typeof options[hook] != 'function') { 174 | throw new Error(`Accounts.ui.config: "${hook}" not a function`); 175 | } 176 | else { 177 | Accounts.ui._options[hook] = options[hook]; 178 | } 179 | } 180 | } 181 | 182 | // Deal with pattern. 183 | for (let hook of [ 184 | 'emailPattern', 185 | ]) { 186 | if (options[hook]) { 187 | if (!(options[hook] instanceof RegExp)) { 188 | throw new Error(`Accounts.ui.config: "${hook}" not a Regular Expression`); 189 | } 190 | else { 191 | Accounts.ui._options[hook] = options[hook]; 192 | } 193 | } 194 | } 195 | 196 | // deal with the paths. 197 | for (let path of [ 198 | 'loginPath', 199 | 'signUpPath', 200 | 'resetPasswordPath', 201 | 'profilePath', 202 | 'changePasswordPath', 203 | 'homeRoutePath' 204 | ]) { 205 | if (typeof options[path] !== 'undefined') { 206 | if (options[path] !== null && typeof options[path] !== 'string') { 207 | throw new Error(`Accounts.ui.config: ${path} is not a string or null`); 208 | } 209 | else { 210 | Accounts.ui._options[path] = options[path]; 211 | } 212 | } 213 | } 214 | 215 | // deal with redirect hooks. 216 | for (let hook of [ 217 | 'onEnrollAccountHook', 218 | 'onResetPasswordHook', 219 | 'onVerifyEmailHook', 220 | 'onSignedInHook', 221 | 'onSignedOutHook']) { 222 | if (options[hook]) { 223 | if (typeof options[hook] == 'function') { 224 | Accounts.ui._options[hook] = options[hook]; 225 | } 226 | else if (typeof options[hook] == 'string') { 227 | Accounts.ui._options[hook] = () => redirect(options[hook]); 228 | } 229 | else { 230 | throw new Error(`Accounts.ui.config: "${hook}" not a function or an absolute or relative path`); 231 | } 232 | } 233 | } 234 | }; 235 | 236 | export default Accounts; 237 | -------------------------------------------------------------------------------- /imports/api/client/loginWithoutPassword.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Request a forgot password email. 3 | * @locus Client 4 | * @param {Object} options 5 | * @param {String} options.email The email address to send a password reset link. 6 | * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. 7 | */ 8 | Accounts.loginWithoutPassword = function(options, callback) { 9 | if (!options.email) 10 | throw new Error("Must pass options.email"); 11 | Accounts.connection.call("loginWithoutPassword", options, callback); 12 | }; 13 | -------------------------------------------------------------------------------- /imports/api/server/loginWithoutPassword.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { Accounts } from 'meteor/accounts-base'; 3 | 4 | /// 5 | /// LOGIN WITHOUT PASSWORD 6 | /// 7 | 8 | // Method called by a user to request a password reset email. This is 9 | // the start of the reset process. 10 | Meteor.methods({ 11 | loginWithoutPassword: function ({ email, username = null }) { 12 | if (username !== null) { 13 | check(username, String); 14 | 15 | var user = Meteor.users.findOne({ $or: [{ 16 | "username": username, "emails.address": { $exists: 1 } 17 | }, { 18 | "emails.address": email 19 | }] 20 | }); 21 | if (!user) 22 | throw new Meteor.Error(403, "User not found"); 23 | 24 | email = user.emails[0].address; 25 | } 26 | else { 27 | check(email, String); 28 | 29 | var user = Meteor.users.findOne({ "emails.address": email }); 30 | if (!user) 31 | throw new Meteor.Error(403, "User not found"); 32 | } 33 | 34 | if (Accounts.ui._options.requireEmailVerification) { 35 | if (!user.emails[0].verified) { 36 | throw new Meteor.Error(403, "Email not verified"); 37 | } 38 | } 39 | 40 | Accounts.sendLoginEmail(user._id, email); 41 | }, 42 | }); 43 | 44 | /** 45 | * @summary Send an email with a link the user can use verify their email address. 46 | * @locus Server 47 | * @param {String} userId The id of the user to send email to. 48 | * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first unverified email in the list. 49 | */ 50 | Accounts.sendLoginEmail = function (userId, address) { 51 | // XXX Also generate a link using which someone can delete this 52 | // account if they own said address but weren't those who created 53 | // this account. 54 | 55 | // Make sure the user exists, and address is one of their addresses. 56 | var user = Meteor.users.findOne(userId); 57 | if (!user) 58 | throw new Error("Can't find user"); 59 | // pick the first unverified address if we weren't passed an address. 60 | if (!address) { 61 | var email = (user.emails || []).find(({ verified }) => !verified); 62 | address = (email || {}).address; 63 | } 64 | // make sure we have a valid address 65 | if (!address || !(user.emails || []).map(({ address }) => address).includes(address)) 66 | throw new Error("No such email address for user."); 67 | 68 | 69 | var tokenRecord = { 70 | token: Random.secret(), 71 | address: address, 72 | when: new Date()}; 73 | Meteor.users.update( 74 | {_id: userId}, 75 | {$push: {'services.email.verificationTokens': tokenRecord}}); 76 | 77 | // before passing to template, update user object with new token 78 | Meteor._ensure(user, 'services', 'email'); 79 | if (!user.services.email.verificationTokens) { 80 | user.services.email.verificationTokens = []; 81 | } 82 | user.services.email.verificationTokens.push(tokenRecord); 83 | 84 | var loginUrl = Accounts.urls.verifyEmail(tokenRecord.token); 85 | 86 | var options = { 87 | to: address, 88 | from: Accounts.emailTemplates.loginNoPassword.from 89 | ? Accounts.emailTemplates.loginNoPassword.from(user) 90 | : Accounts.emailTemplates.from, 91 | subject: Accounts.emailTemplates.loginNoPassword.subject(user) 92 | }; 93 | 94 | if (typeof Accounts.emailTemplates.loginNoPassword.text === 'function') { 95 | options.text = 96 | Accounts.emailTemplates.loginNoPassword.text(user, loginUrl); 97 | } 98 | 99 | if (typeof Accounts.emailTemplates.loginNoPassword.html === 'function') 100 | options.html = 101 | Accounts.emailTemplates.loginNoPassword.html(user, loginUrl); 102 | 103 | if (typeof Accounts.emailTemplates.headers === 'object') { 104 | options.headers = Accounts.emailTemplates.headers; 105 | } 106 | 107 | Email.send(options); 108 | }; 109 | 110 | // Check for installed accounts-password dependency. 111 | if (Accounts.emailTemplates) { 112 | Accounts.emailTemplates.loginNoPassword = { 113 | subject: function(user) { 114 | return "Login on " + Accounts.emailTemplates.siteName; 115 | }, 116 | text: function(user, url) { 117 | var greeting = (user.profile && user.profile.name) ? 118 | ("Hello " + user.profile.name + ",") : "Hello,"; 119 | return `${greeting} 120 | To login, simply click the link below. 121 | ${url} 122 | Thanks. 123 | `; 124 | } 125 | }; 126 | } 127 | -------------------------------------------------------------------------------- /imports/api/server/servicesListPublication.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | import { getLoginServices } from '../../helpers.js'; 3 | 4 | Meteor.publish('servicesList', function() { 5 | let services = getLoginServices(); 6 | if (Package['accounts-password']) { 7 | services.push({name: 'password'}); 8 | } 9 | let fields = {}; 10 | // Publish the existing services for a user, only name or nothing else. 11 | services.forEach(service => fields[`services.${service.name}.name`] = 1); 12 | return Meteor.users.find({ _id: this.userId }, { fields: fields}); 13 | }); 14 | -------------------------------------------------------------------------------- /imports/helpers.js: -------------------------------------------------------------------------------- 1 | let browserHistory 2 | try { browserHistory = require('react-router').browserHistory } catch(e) {} 3 | export const loginButtonsSession = Accounts._loginButtonsSession; 4 | export const STATES = { 5 | SIGN_IN: Symbol('SIGN_IN'), 6 | SIGN_UP: Symbol('SIGN_UP'), 7 | PROFILE: Symbol('PROFILE'), 8 | PASSWORD_CHANGE: Symbol('PASSWORD_CHANGE'), 9 | PASSWORD_RESET: Symbol('PASSWORD_RESET'), 10 | ENROLL_ACCOUNT: Symbol('ENROLL_ACCOUNT') 11 | }; 12 | 13 | export function getLoginServices() { 14 | // First look for OAuth services. 15 | const services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : []; 16 | 17 | // Be equally kind to all login services. This also preserves 18 | // backwards-compatibility. 19 | services.sort(); 20 | 21 | return services.map(function(name){ 22 | return {name: name}; 23 | }); 24 | }; 25 | // Export getLoginServices using old style globals for accounts-base which 26 | // requires it. 27 | this.getLoginServices = getLoginServices; 28 | 29 | export function hasPasswordService() { 30 | // First look for OAuth services. 31 | return !!Package['accounts-password']; 32 | }; 33 | 34 | export function loginResultCallback(service, err) { 35 | if (!err) { 36 | 37 | } else if (err instanceof Accounts.LoginCancelledError) { 38 | // do nothing 39 | } else if (err instanceof ServiceConfiguration.ConfigError) { 40 | 41 | } else { 42 | //loginButtonsSession.errorMessage(err.reason || "Unknown error"); 43 | } 44 | 45 | if (Meteor.isClient) { 46 | if (typeof redirect === 'string'){ 47 | window.location.href = '/'; 48 | } 49 | 50 | if (typeof service === 'function'){ 51 | service(); 52 | } 53 | } 54 | }; 55 | 56 | export function passwordSignupFields() { 57 | return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY_NO_PASSWORD"; 58 | }; 59 | 60 | export function validateEmail(email, showMessage, clearMessage) { 61 | if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '') { 62 | return true; 63 | } 64 | if (Accounts.ui._options.emailPattern.test(email)) { 65 | return true; 66 | } else if (!email || email.length === 0) { 67 | showMessage("error.emailRequired", 'warning', false, 'email'); 68 | return false; 69 | } else { 70 | showMessage("error.accounts.Invalid email", 'warning', false, 'email'); 71 | return false; 72 | } 73 | } 74 | 75 | export function validatePassword(password = '', showMessage, clearMessage){ 76 | if (password.length >= Accounts.ui._options.minimumPasswordLength) { 77 | return true; 78 | } else { 79 | // const errMsg = T9n.get("error.minChar").replace(/7/, Accounts.ui._options.minimumPasswordLength); 80 | const errMsg = "error.minChar" 81 | showMessage(errMsg, 'warning', false, 'password'); 82 | return false; 83 | } 84 | }; 85 | 86 | export function validateUsername(username, showMessage, clearMessage, formState) { 87 | if ( username ) { 88 | return true; 89 | } else { 90 | const fieldName = (passwordSignupFields() === 'USERNAME_ONLY' || formState === STATES.SIGN_UP) ? 'username' : 'usernameOrEmail'; 91 | showMessage("error.usernameRequired", 'warning', false, fieldName); 92 | return false; 93 | } 94 | } 95 | 96 | export function redirect(redirect) { 97 | if (Meteor.isClient) { 98 | if (window.history) { 99 | // Run after all app specific redirects, i.e. to the login screen. 100 | Meteor.setTimeout(() => { 101 | if (Package['kadira:flow-router']) { 102 | Package['kadira:flow-router'].FlowRouter.go(redirect); 103 | } else if (Package['kadira:flow-router-ssr']) { 104 | Package['kadira:flow-router-ssr'].FlowRouter.go(redirect); 105 | } else if (browserHistory) { 106 | browserHistory.push(redirect); 107 | } else { 108 | window.history.pushState( {} , 'redirect', redirect ); 109 | } 110 | }, 100); 111 | } 112 | } 113 | } 114 | 115 | export function capitalize(string) { 116 | return string.replace(/\-/, ' ').split(' ').map(word => { 117 | return word.charAt(0).toUpperCase() + word.slice(1); 118 | }).join(' '); 119 | } 120 | -------------------------------------------------------------------------------- /imports/login_session.js: -------------------------------------------------------------------------------- 1 | import {Accounts} from 'meteor/accounts-base'; 2 | import { 3 | STATES, 4 | loginResultCallback, 5 | getLoginServices 6 | } from './helpers.js'; 7 | 8 | const VALID_KEYS = [ 9 | 'dropdownVisible', 10 | 11 | // XXX consider replacing these with one key that has an enum for values. 12 | 'inSignupFlow', 13 | 'inForgotPasswordFlow', 14 | 'inChangePasswordFlow', 15 | 'inMessageOnlyFlow', 16 | 17 | 'errorMessage', 18 | 'infoMessage', 19 | 20 | // dialogs with messages (info and error) 21 | 'resetPasswordToken', 22 | 'enrollAccountToken', 23 | 'justVerifiedEmail', 24 | 'justResetPassword', 25 | 26 | 'configureLoginServiceDialogVisible', 27 | 'configureLoginServiceDialogServiceName', 28 | 'configureLoginServiceDialogSaveDisabled', 29 | 'configureOnDesktopVisible' 30 | ]; 31 | 32 | export const validateKey = function (key) { 33 | if (!VALID_KEYS.includes(key)) 34 | throw new Error("Invalid key in loginButtonsSession: " + key); 35 | }; 36 | 37 | export const KEY_PREFIX = "Meteor.loginButtons."; 38 | 39 | // XXX This should probably be package scope rather than exported 40 | // (there was even a comment to that effect here from before we had 41 | // namespacing) but accounts-ui-viewer uses it, so leave it as is for 42 | // now 43 | Accounts._loginButtonsSession = { 44 | set: function(key, value) { 45 | validateKey(key); 46 | if (['errorMessage', 'infoMessage'].includes(key)) 47 | throw new Error("Don't set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage()."); 48 | 49 | this._set(key, value); 50 | }, 51 | 52 | _set: function(key, value) { 53 | Session.set(KEY_PREFIX + key, value); 54 | }, 55 | 56 | get: function(key) { 57 | validateKey(key); 58 | return Session.get(KEY_PREFIX + key); 59 | } 60 | }; 61 | 62 | if (Meteor.isClient){ 63 | // In the login redirect flow, we'll have the result of the login 64 | // attempt at page load time when we're redirected back to the 65 | // application. Register a callback to update the UI (i.e. to close 66 | // the dialog on a successful login or display the error on a failed 67 | // login). 68 | // 69 | Accounts.onPageLoadLogin(function (attemptInfo) { 70 | // Ignore if we have a left over login attempt for a service that is no longer registered. 71 | if (getLoginServices().map(({ name }) => name).includes(attemptInfo.type)) 72 | loginResultCallback(attemptInfo.type, attemptInfo.error); 73 | }); 74 | 75 | let doneCallback; 76 | 77 | Accounts.onResetPasswordLink(function (token, done) { 78 | Accounts._loginButtonsSession.set('resetPasswordToken', token); 79 | Session.set(KEY_PREFIX + 'state', 'resetPasswordToken'); 80 | doneCallback = done; 81 | 82 | Accounts.ui._options.onResetPasswordHook(); 83 | }); 84 | 85 | Accounts.onEnrollmentLink(function (token, done) { 86 | Accounts._loginButtonsSession.set('enrollAccountToken', token); 87 | Session.set(KEY_PREFIX + 'state', 'enrollAccountToken'); 88 | doneCallback = done; 89 | 90 | Accounts.ui._options.onEnrollAccountHook(); 91 | }); 92 | 93 | Accounts.onEmailVerificationLink(function (token, done) { 94 | Accounts.verifyEmail(token, function (error) { 95 | if (! error) { 96 | Accounts._loginButtonsSession.set('justVerifiedEmail', true); 97 | Session.set(KEY_PREFIX + 'state', 'justVerifiedEmail'); 98 | Accounts.ui._options.onSignedInHook(); 99 | } 100 | else { 101 | Accounts.ui._options.onVerifyEmailHook(); 102 | } 103 | 104 | done(); 105 | }); 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /imports/ui/components/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Accounts } from 'meteor/accounts-base'; 4 | 5 | let Link; 6 | try { Link = require('react-router').Link; } catch(e) {} 7 | 8 | export class Button extends React.Component { 9 | render () { 10 | const { 11 | label, 12 | href = null, 13 | type, 14 | disabled = false, 15 | className, 16 | onClick 17 | } = this.props; 18 | if (type == 'link') { 19 | // Support React Router. 20 | if (Link && href) { 21 | return { label }; 22 | } else { 23 | return { label }; 24 | } 25 | } 26 | return ; 30 | } 31 | } 32 | 33 | Button.propTypes = { 34 | onClick: PropTypes.func 35 | }; 36 | 37 | Accounts.ui.Button = Button; 38 | -------------------------------------------------------------------------------- /imports/ui/components/Buttons.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Button.jsx'; 3 | import { Accounts } from 'meteor/accounts-base'; 4 | 5 | export class Buttons extends React.Component { 6 | render () { 7 | let { buttons = {}, className = "buttons" } = this.props; 8 | return ( 9 |
10 | {Object.keys(buttons).map((id, i) => 11 | 12 | )} 13 |
14 | ); 15 | } 16 | }; 17 | 18 | Accounts.ui.Buttons = Buttons; 19 | -------------------------------------------------------------------------------- /imports/ui/components/Field.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Accounts } from 'meteor/accounts-base'; 4 | 5 | export class Field extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | mount: true 10 | }; 11 | } 12 | 13 | triggerUpdate() { 14 | // Trigger an onChange on inital load, to support browser prefilled values. 15 | const { onChange } = this.props; 16 | if (this.input && onChange) { 17 | onChange({ target: { value: this.input.value } }); 18 | } 19 | } 20 | 21 | componentDidMount() { 22 | this.triggerUpdate(); 23 | } 24 | 25 | componentDidUpdate(prevProps) { 26 | // Re-mount component so that we don't expose browser prefilled passwords if the component was 27 | // a password before and now something else. 28 | if (prevProps.id !== this.props.id) { 29 | this.setState({mount: false}); 30 | } 31 | else if (!this.state.mount) { 32 | this.setState({mount: true}); 33 | this.triggerUpdate(); 34 | } 35 | } 36 | 37 | render() { 38 | const { 39 | id, 40 | hint, 41 | label, 42 | type = 'text', 43 | onChange, 44 | required = false, 45 | className = "field", 46 | defaultValue = "", 47 | message, 48 | } = this.props; 49 | const { mount = true } = this.state; 50 | if (type == 'notice') { 51 | return
{ label }
; 52 | } 53 | return mount ? ( 54 |
55 | 56 | this.input = ref } 59 | type={ type } 60 | onChange={ onChange } 61 | placeholder={ hint } 62 | defaultValue={ defaultValue } 63 | /> 64 | {message && ( 65 | 66 | {message.message} 67 | )} 68 |
69 | ) : null; 70 | } 71 | } 72 | 73 | Field.propTypes = { 74 | onChange: PropTypes.func 75 | }; 76 | 77 | Accounts.ui.Field = Field; 78 | -------------------------------------------------------------------------------- /imports/ui/components/Fields.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Accounts } from 'meteor/accounts-base'; 3 | import './Field.jsx'; 4 | 5 | export class Fields extends React.Component { 6 | render () { 7 | let { fields = {}, className = "fields" } = this.props; 8 | return ( 9 |
10 | {Object.keys(fields).map((id, i) => 11 | 12 | )} 13 |
14 | ); 15 | } 16 | } 17 | 18 | Accounts.ui.Fields = Fields; 19 | -------------------------------------------------------------------------------- /imports/ui/components/Form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactDOM from 'react-dom'; 4 | import { Accounts } from 'meteor/accounts-base'; 5 | 6 | import './Fields.jsx'; 7 | import './Buttons.jsx'; 8 | import './FormMessage.jsx'; 9 | import './PasswordOrService.jsx'; 10 | import './SocialButtons.jsx'; 11 | import './FormMessages.jsx'; 12 | 13 | export class Form extends React.Component { 14 | componentDidMount() { 15 | let form = this.form; 16 | if (form) { 17 | form.addEventListener('submit', (e) => { 18 | e.preventDefault(); 19 | }); 20 | } 21 | } 22 | 23 | render() { 24 | const { 25 | hasPasswordService, 26 | oauthServices, 27 | fields, 28 | buttons, 29 | error, 30 | messages, 31 | translate, 32 | ready = true, 33 | className 34 | } = this.props; 35 | return ( 36 |
this.form = ref} 38 | className={[className, ready ? "ready" : null].join(' ')} 39 | className="accounts-ui" 40 | noValidate 41 | > 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | } 51 | 52 | Form.propTypes = { 53 | oauthServices: PropTypes.object, 54 | fields: PropTypes.object.isRequired, 55 | buttons: PropTypes.object.isRequired, 56 | translate: PropTypes.func.isRequired, 57 | error: PropTypes.string, 58 | ready: PropTypes.bool 59 | }; 60 | 61 | Accounts.ui.Form = Form; 62 | -------------------------------------------------------------------------------- /imports/ui/components/FormMessage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Accounts } from 'meteor/accounts-base'; 3 | 4 | function isObject(obj) { 5 | return obj === Object(obj); 6 | } 7 | 8 | export class FormMessage extends React.Component { 9 | render () { 10 | let { message, type, className = "message", style = {}, deprecated } = this.props; 11 | // XXX Check for deprecations. 12 | if (deprecated) { 13 | // Found backwords compatibility issue. 14 | console.warn('You are overriding Accounts.ui.Form and using FormMessage, the use of FormMessage in Form has been depreacted in v1.2.11, update your implementation to use FormMessages: https://github.com/studiointeract/accounts-ui/#deprecations'); 15 | } 16 | message = isObject(message) ? message.message : message; // If message is object, then try to get message from it 17 | return message ? ( 18 |
{ message }
20 | ) : null; 21 | } 22 | } 23 | 24 | Accounts.ui.FormMessage = FormMessage; 25 | -------------------------------------------------------------------------------- /imports/ui/components/FormMessages.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Accounts } from 'meteor/accounts-base'; 3 | 4 | export class FormMessages extends Component { 5 | render () { 6 | const { messages = [], className = "messages", style = {} } = this.props; 7 | return messages.length > 0 && ( 8 |
9 | {messages 10 | .filter(message => !('field' in message)) 11 | .map(({ message, type }, i) => 12 | 17 | )} 18 |
19 | ); 20 | } 21 | } 22 | 23 | Accounts.ui.FormMessages = FormMessages; 24 | -------------------------------------------------------------------------------- /imports/ui/components/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactDOM from 'react-dom'; 4 | import { withTracker } from 'meteor/react-meteor-data'; 5 | import { Accounts } from 'meteor/accounts-base'; 6 | import { T9n } from 'meteor/softwarerero:accounts-t9n'; 7 | import { KEY_PREFIX } from '../../login_session.js'; 8 | import './Form.jsx'; 9 | 10 | import { 11 | STATES, 12 | passwordSignupFields, 13 | validateEmail, 14 | validatePassword, 15 | validateUsername, 16 | loginResultCallback, 17 | getLoginServices, 18 | hasPasswordService, 19 | capitalize 20 | } from '../../helpers.js'; 21 | 22 | function indexBy(array, key) { 23 | const result = {}; 24 | array.forEach(function(obj) { 25 | result[obj[key]] = obj; 26 | }); 27 | return result; 28 | } 29 | 30 | class LoginForm extends Component { 31 | constructor(props) { 32 | super(props); 33 | let { 34 | formState, 35 | loginPath, 36 | signUpPath, 37 | resetPasswordPath, 38 | profilePath, 39 | changePasswordPath 40 | } = props; 41 | 42 | if (formState === STATES.SIGN_IN && Package['accounts-password']) { 43 | console.warn('Do not force the state to SIGN_IN on Accounts.ui.LoginForm, it will make it impossible to reset password in your app, this state is also the default state if logged out, so no need to force it.'); 44 | } 45 | 46 | // Set inital state. 47 | this.state = { 48 | messages: [], 49 | waiting: true, 50 | formState: formState ? formState : Accounts.user() ? STATES.PROFILE : STATES.SIGN_IN, 51 | onSubmitHook: props.onSubmitHook || Accounts.ui._options.onSubmitHook, 52 | onSignedInHook: props.onSignedInHook || Accounts.ui._options.onSignedInHook, 53 | onSignedOutHook: props.onSignedOutHook || Accounts.ui._options.onSignedOutHook, 54 | onPreSignUpHook: props.onPreSignUpHook || Accounts.ui._options.onPreSignUpHook, 55 | onPostSignUpHook: props.onPostSignUpHook || Accounts.ui._options.onPostSignUpHook, 56 | }; 57 | this.translate = this.translate.bind(this); 58 | } 59 | 60 | componentDidMount() { 61 | this.setState(prevState => ({ waiting: false, ready: true })); 62 | let changeState = Session.get(KEY_PREFIX + 'state'); 63 | switch (changeState) { 64 | case 'enrollAccountToken': 65 | this.setState(prevState => ({ 66 | formState: STATES.ENROLL_ACCOUNT 67 | })); 68 | Session.set(KEY_PREFIX + 'state', null); 69 | break; 70 | case 'resetPasswordToken': 71 | this.setState(prevState => ({ 72 | formState: STATES.PASSWORD_CHANGE 73 | })); 74 | Session.set(KEY_PREFIX + 'state', null); 75 | break; 76 | 77 | case 'justVerifiedEmail': 78 | this.setState(prevState => ({ 79 | formState: STATES.PROFILE 80 | })); 81 | Session.set(KEY_PREFIX + 'state', null); 82 | break; 83 | } 84 | 85 | // Add default field values once the form did mount on the client 86 | this.setState(prevState => ({ 87 | ...this.getDefaultFieldValues(), 88 | })); 89 | } 90 | 91 | componentWillReceiveProps(nextProps, nextContext) { 92 | if (nextProps.formState && nextProps.formState !== this.state.formState) { 93 | this.setState({ 94 | formState: nextProps.formState, 95 | ...this.getDefaultFieldValues(), 96 | }); 97 | } 98 | } 99 | 100 | componentDidUpdate(prevProps, prevState) { 101 | if (!prevProps.user !== !this.props.user) { 102 | this.setState({ 103 | formState: this.props.user ? STATES.PROFILE : STATES.SIGN_IN 104 | }); 105 | } 106 | } 107 | 108 | translate(text) { 109 | // if (this.props.t) { 110 | // return this.props.t(text); 111 | // } 112 | return T9n.get(text); 113 | } 114 | 115 | validateField(field, value) { 116 | const { formState } = this.state; 117 | switch(field) { 118 | case 'email': 119 | return validateEmail(value, 120 | this.showMessage.bind(this), 121 | this.clearMessage.bind(this), 122 | ); 123 | case 'password': 124 | return validatePassword(value, 125 | this.showMessage.bind(this), 126 | this.clearMessage.bind(this), 127 | ); 128 | case 'username': 129 | return validateUsername(value, 130 | this.showMessage.bind(this), 131 | this.clearMessage.bind(this), 132 | formState, 133 | ); 134 | } 135 | } 136 | 137 | getUsernameOrEmailField() { 138 | return { 139 | id: 'usernameOrEmail', 140 | hint: this.translate('enterUsernameOrEmail'), 141 | label: this.translate('usernameOrEmail'), 142 | required: true, 143 | defaultValue: this.state.username || "", 144 | onChange: this.handleChange.bind(this, 'usernameOrEmail'), 145 | message: this.getMessageForField('usernameOrEmail'), 146 | }; 147 | } 148 | 149 | getUsernameField() { 150 | return { 151 | id: 'username', 152 | hint: this.translate('enterUsername'), 153 | label: this.translate('username'), 154 | required: true, 155 | defaultValue: this.state.username || "", 156 | onChange: this.handleChange.bind(this, 'username'), 157 | message: this.getMessageForField('username'), 158 | }; 159 | } 160 | 161 | getEmailField() { 162 | return { 163 | id: 'email', 164 | hint: this.translate('enterEmail'), 165 | label: this.translate('email'), 166 | type: 'email', 167 | required: true, 168 | defaultValue: this.state.email || "", 169 | onChange: this.handleChange.bind(this, 'email'), 170 | message: this.getMessageForField('email'), 171 | }; 172 | } 173 | 174 | getPasswordField() { 175 | return { 176 | id: 'password', 177 | hint: this.translate('enterPassword'), 178 | label: this.translate('password'), 179 | type: 'password', 180 | required: true, 181 | defaultValue: this.state.password || "", 182 | onChange: this.handleChange.bind(this, 'password'), 183 | message: this.getMessageForField('password'), 184 | }; 185 | } 186 | 187 | getSetPasswordField() { 188 | return { 189 | id: 'newPassword', 190 | hint: this.translate('enterPassword'), 191 | label: this.translate('choosePassword'), 192 | type: 'password', 193 | required: true, 194 | onChange: this.handleChange.bind(this, 'newPassword') 195 | }; 196 | } 197 | 198 | getNewPasswordField() { 199 | return { 200 | id: 'newPassword', 201 | hint: this.translate('enterNewPassword'), 202 | label: this.translate('newPassword'), 203 | type: 'password', 204 | required: true, 205 | onChange: this.handleChange.bind(this, 'newPassword'), 206 | message: this.getMessageForField('newPassword'), 207 | }; 208 | } 209 | 210 | handleChange(field, evt) { 211 | let value = evt.target.value; 212 | switch (field) { 213 | case 'password': break; 214 | default: 215 | value = value.trim(); 216 | break; 217 | } 218 | this.setState({ [field]: value }); 219 | this.setDefaultFieldValues({ [field]: value }); 220 | } 221 | 222 | fields() { 223 | const loginFields = []; 224 | const { formState } = this.state; 225 | 226 | if (!hasPasswordService() && getLoginServices().length == 0) { 227 | loginFields.push({ 228 | label: 'No login service added, i.e. accounts-password', 229 | type: 'notice' 230 | }); 231 | } 232 | 233 | if (hasPasswordService() && formState == STATES.SIGN_IN) { 234 | if ([ 235 | "USERNAME_AND_EMAIL", 236 | "USERNAME_AND_OPTIONAL_EMAIL", 237 | "USERNAME_AND_EMAIL_NO_PASSWORD" 238 | ].includes(passwordSignupFields())) { 239 | loginFields.push(this.getUsernameOrEmailField()); 240 | } 241 | 242 | if (passwordSignupFields() === "USERNAME_ONLY") { 243 | loginFields.push(this.getUsernameField()); 244 | } 245 | 246 | if ([ 247 | "EMAIL_ONLY", 248 | "EMAIL_ONLY_NO_PASSWORD" 249 | ].includes(passwordSignupFields())) { 250 | loginFields.push(this.getEmailField()); 251 | } 252 | 253 | if (![ 254 | "EMAIL_ONLY_NO_PASSWORD", 255 | "USERNAME_AND_EMAIL_NO_PASSWORD" 256 | ].includes(passwordSignupFields())) { 257 | loginFields.push(this.getPasswordField()); 258 | } 259 | } 260 | 261 | if (hasPasswordService() && formState == STATES.SIGN_UP) { 262 | if ([ 263 | "USERNAME_AND_EMAIL", 264 | "USERNAME_AND_OPTIONAL_EMAIL", 265 | "USERNAME_ONLY", 266 | "USERNAME_AND_EMAIL_NO_PASSWORD" 267 | ].includes(passwordSignupFields())) { 268 | loginFields.push(this.getUsernameField()); 269 | } 270 | 271 | if ([ 272 | "USERNAME_AND_EMAIL", 273 | "EMAIL_ONLY", 274 | "EMAIL_ONLY_NO_PASSWORD", 275 | "USERNAME_AND_EMAIL_NO_PASSWORD" 276 | ].includes(passwordSignupFields())) { 277 | loginFields.push(this.getEmailField()); 278 | } 279 | 280 | if (["USERNAME_AND_OPTIONAL_EMAIL"].includes(passwordSignupFields())) { 281 | loginFields.push(Object.assign(this.getEmailField(), {required: false})); 282 | } 283 | 284 | if (![ 285 | "EMAIL_ONLY_NO_PASSWORD", 286 | "USERNAME_AND_EMAIL_NO_PASSWORD" 287 | ].includes(passwordSignupFields())) { 288 | loginFields.push(this.getPasswordField()); 289 | } 290 | } 291 | 292 | if (formState == STATES.PASSWORD_RESET) { 293 | loginFields.push(this.getEmailField()); 294 | } 295 | 296 | if (this.showPasswordChangeForm()) { 297 | if (Meteor.isClient && !Accounts._loginButtonsSession.get('resetPasswordToken')) { 298 | loginFields.push(this.getPasswordField()); 299 | } 300 | loginFields.push(this.getNewPasswordField()); 301 | } 302 | 303 | if (this.showEnrollAccountForm()) { 304 | loginFields.push(this.getSetPasswordField()); 305 | } 306 | return indexBy(loginFields, 'id'); 307 | } 308 | 309 | buttons() { 310 | const { 311 | loginPath = Accounts.ui._options.loginPath, 312 | signUpPath = Accounts.ui._options.signUpPath, 313 | resetPasswordPath = Accounts.ui._options.resetPasswordPath, 314 | changePasswordPath = Accounts.ui._options.changePasswordPath, 315 | profilePath = Accounts.ui._options.profilePath, 316 | } = this.props; 317 | const { user } = this.props; 318 | const { formState, waiting } = this.state; 319 | let loginButtons = []; 320 | 321 | if (user && formState == STATES.PROFILE) { 322 | loginButtons.push({ 323 | id: 'signOut', 324 | label: this.translate('signOut'), 325 | disabled: waiting, 326 | onClick: this.signOut.bind(this) 327 | }); 328 | } 329 | 330 | if (this.showCreateAccountLink()) { 331 | loginButtons.push({ 332 | id: 'switchToSignUp', 333 | label: this.translate('signUp'), 334 | type: 'link', 335 | href: signUpPath, 336 | onClick: this.switchToSignUp.bind(this) 337 | }); 338 | } 339 | 340 | if (formState == STATES.SIGN_UP || formState == STATES.PASSWORD_RESET) { 341 | loginButtons.push({ 342 | id: 'switchToSignIn', 343 | label: this.translate('signIn'), 344 | type: 'link', 345 | href: loginPath, 346 | onClick: this.switchToSignIn.bind(this) 347 | }); 348 | } 349 | 350 | if (this.showForgotPasswordLink()) { 351 | loginButtons.push({ 352 | id: 'switchToPasswordReset', 353 | label: this.translate('forgotPassword'), 354 | type: 'link', 355 | href: resetPasswordPath, 356 | onClick: this.switchToPasswordReset.bind(this) 357 | }); 358 | } 359 | 360 | if (user && ![ 361 | "EMAIL_ONLY_NO_PASSWORD", 362 | "USERNAME_AND_EMAIL_NO_PASSWORD" 363 | ].includes(passwordSignupFields()) 364 | && formState == STATES.PROFILE 365 | && (user.services && 'password' in user.services)) { 366 | loginButtons.push({ 367 | id: 'switchToChangePassword', 368 | label: this.translate('changePassword'), 369 | type: 'link', 370 | href: changePasswordPath, 371 | onClick: this.switchToChangePassword.bind(this) 372 | }); 373 | } 374 | 375 | if (formState == STATES.SIGN_UP) { 376 | loginButtons.push({ 377 | id: 'signUp', 378 | label: this.translate('signUp'), 379 | type: hasPasswordService() ? 'submit' : 'link', 380 | className: 'active', 381 | disabled: waiting, 382 | onClick: hasPasswordService() ? this.signUp.bind(this, {}) : null 383 | }); 384 | } 385 | 386 | if (this.showSignInLink()) { 387 | loginButtons.push({ 388 | id: 'signIn', 389 | label: this.translate('signIn'), 390 | type: hasPasswordService() ? 'submit' : 'link', 391 | className: 'active', 392 | disabled: waiting, 393 | onClick: hasPasswordService() ? this.signIn.bind(this) : null 394 | }); 395 | } 396 | 397 | if (formState == STATES.PASSWORD_RESET) { 398 | loginButtons.push({ 399 | id: 'emailResetLink', 400 | label: this.translate('resetYourPassword'), 401 | type: 'submit', 402 | disabled: waiting, 403 | onClick: this.passwordReset.bind(this) 404 | }); 405 | } 406 | 407 | if (this.showPasswordChangeForm() || this.showEnrollAccountForm()) { 408 | loginButtons.push({ 409 | id: 'changePassword', 410 | label: (this.showPasswordChangeForm() ? this.translate('changePassword') : this.translate('setPassword')), 411 | type: 'submit', 412 | disabled: waiting, 413 | onClick: this.passwordChange.bind(this) 414 | }); 415 | 416 | if (Accounts.user()) { 417 | loginButtons.push({ 418 | id: 'switchToSignOut', 419 | label: this.translate('cancel'), 420 | type: 'link', 421 | href: profilePath, 422 | onClick: this.switchToSignOut.bind(this) 423 | }); 424 | } else { 425 | loginButtons.push({ 426 | id: 'cancelResetPassword', 427 | label: this.translate('cancel'), 428 | type: 'link', 429 | onClick: this.cancelResetPassword.bind(this), 430 | }); 431 | } 432 | } 433 | 434 | // Sort the button array so that the submit button always comes first, and 435 | // buttons should also come before links. 436 | loginButtons.sort((a, b) => { 437 | return ( 438 | b.type == 'submit' && 439 | a.type != undefined) - ( 440 | a.type == 'submit' && 441 | b.type != undefined); 442 | }); 443 | 444 | return indexBy(loginButtons, 'id'); 445 | } 446 | 447 | showSignInLink(){ 448 | return this.state.formState == STATES.SIGN_IN && Package['accounts-password']; 449 | } 450 | 451 | showPasswordChangeForm() { 452 | return(Package['accounts-password'] 453 | && this.state.formState == STATES.PASSWORD_CHANGE); 454 | } 455 | 456 | showEnrollAccountForm() { 457 | return(Package['accounts-password'] 458 | && this.state.formState == STATES.ENROLL_ACCOUNT); 459 | } 460 | 461 | showCreateAccountLink() { 462 | return this.state.formState == STATES.SIGN_IN && !Accounts._options.forbidClientAccountCreation && Package['accounts-password']; 463 | } 464 | 465 | showForgotPasswordLink() { 466 | return !this.props.user 467 | && this.state.formState == STATES.SIGN_IN 468 | && ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"].includes(passwordSignupFields()); 469 | } 470 | 471 | /** 472 | * Helper to store field values while using the form. 473 | */ 474 | setDefaultFieldValues(defaults) { 475 | if (typeof defaults !== 'object') { 476 | throw new Error('Argument to setDefaultFieldValues is not of type object'); 477 | } else if (typeof localStorage !== 'undefined' && localStorage) { 478 | localStorage.setItem('accounts_ui', JSON.stringify({ 479 | passwordSignupFields: passwordSignupFields(), 480 | ...this.getDefaultFieldValues(), 481 | ...defaults, 482 | })); 483 | } 484 | } 485 | 486 | /** 487 | * Helper to get field values when switching states in the form. 488 | */ 489 | getDefaultFieldValues() { 490 | if (typeof localStorage !== 'undefined' && localStorage) { 491 | const defaultFieldValues = JSON.parse(localStorage.getItem('accounts_ui') || null); 492 | if (defaultFieldValues 493 | && defaultFieldValues.passwordSignupFields === passwordSignupFields()) { 494 | return defaultFieldValues; 495 | } 496 | } 497 | } 498 | 499 | /** 500 | * Helper to clear field values when signing in, up or out. 501 | */ 502 | clearDefaultFieldValues() { 503 | if (typeof localStorage !== 'undefined' && localStorage) { 504 | localStorage.removeItem('accounts_ui'); 505 | } 506 | } 507 | 508 | switchToSignUp(event) { 509 | event.preventDefault(); 510 | this.setState({ 511 | formState: STATES.SIGN_UP, 512 | ...this.getDefaultFieldValues(), 513 | }); 514 | this.clearMessages(); 515 | } 516 | 517 | switchToSignIn(event) { 518 | event.preventDefault(); 519 | this.setState({ 520 | formState: STATES.SIGN_IN, 521 | ...this.getDefaultFieldValues(), 522 | }); 523 | this.clearMessages(); 524 | } 525 | 526 | switchToPasswordReset(event) { 527 | event.preventDefault(); 528 | this.setState({ 529 | formState: STATES.PASSWORD_RESET, 530 | ...this.getDefaultFieldValues(), 531 | }); 532 | this.clearMessages(); 533 | } 534 | 535 | switchToChangePassword(event) { 536 | event.preventDefault(); 537 | this.setState({ 538 | formState: STATES.PASSWORD_CHANGE, 539 | ...this.getDefaultFieldValues(), 540 | }); 541 | this.clearMessages(); 542 | } 543 | 544 | switchToSignOut(event) { 545 | event.preventDefault(); 546 | this.setState({ 547 | formState: STATES.PROFILE, 548 | }); 549 | this.clearMessages(); 550 | } 551 | 552 | cancelResetPassword(event) { 553 | event.preventDefault(); 554 | Accounts._loginButtonsSession.set('resetPasswordToken', null); 555 | this.setState({ 556 | formState: STATES.SIGN_IN, 557 | messages: [], 558 | }); 559 | } 560 | 561 | signOut() { 562 | Meteor.logout(() => { 563 | this.setState({ 564 | formState: STATES.SIGN_IN, 565 | password: null, 566 | }); 567 | this.state.onSignedOutHook(); 568 | this.clearMessages(); 569 | this.clearDefaultFieldValues(); 570 | }); 571 | } 572 | 573 | signIn() { 574 | const { 575 | username = null, 576 | email = null, 577 | usernameOrEmail = null, 578 | password, 579 | formState, 580 | onSubmitHook 581 | } = this.state; 582 | let error = false; 583 | let loginSelector; 584 | this.clearMessages(); 585 | 586 | if (usernameOrEmail !== null) { 587 | if (!this.validateField('username', usernameOrEmail)) { 588 | if (this.state.formState == STATES.SIGN_UP) { 589 | this.state.onSubmitHook("error.accounts.usernameRequired", this.state.formState); 590 | } 591 | error = true; 592 | } 593 | else { 594 | if (["USERNAME_AND_EMAIL_NO_PASSWORD"].includes(passwordSignupFields())) { 595 | this.loginWithoutPassword(); 596 | return; 597 | } else { 598 | loginSelector = usernameOrEmail; 599 | } 600 | } 601 | } else if (username !== null) { 602 | if (!this.validateField('username', username)) { 603 | if (this.state.formState == STATES.SIGN_UP) { 604 | this.state.onSubmitHook("error.accounts.usernameRequired", this.state.formState); 605 | } 606 | error = true; 607 | } 608 | else { 609 | loginSelector = { username: username }; 610 | } 611 | } 612 | else if (usernameOrEmail == null) { 613 | if (!this.validateField('email', email)) { 614 | error = true; 615 | } 616 | else { 617 | if (["EMAIL_ONLY_NO_PASSWORD"].includes(passwordSignupFields())) { 618 | this.loginWithoutPassword(); 619 | error = true; 620 | } else { 621 | loginSelector = { email }; 622 | } 623 | } 624 | } 625 | if (!["EMAIL_ONLY_NO_PASSWORD"].includes(passwordSignupFields()) 626 | && !this.validateField('password', password)) { 627 | error = true; 628 | } 629 | 630 | if (!error) { 631 | Meteor.loginWithPassword(loginSelector, password, (error, result) => { 632 | onSubmitHook(error,formState); 633 | if (error) { 634 | this.showMessage(`error.accounts.${error.reason}` || "unknown_error", 'error'); 635 | } 636 | else { 637 | loginResultCallback(() => this.state.onSignedInHook()); 638 | this.setState({ 639 | formState: STATES.PROFILE, 640 | password: null, 641 | }); 642 | this.clearDefaultFieldValues(); 643 | } 644 | }); 645 | } 646 | } 647 | 648 | oauthButtons() { 649 | const { formState, waiting } = this.state; 650 | let oauthButtons = []; 651 | if (formState == STATES.SIGN_IN || formState == STATES.SIGN_UP ) { 652 | if(Accounts.oauth) { 653 | Accounts.oauth.serviceNames().map((service) => { 654 | oauthButtons.push({ 655 | id: service, 656 | label: capitalize(service), 657 | disabled: waiting, 658 | type: 'button', 659 | className: `btn-${service} ${service}`, 660 | onClick: this.oauthSignIn.bind(this, service) 661 | }); 662 | }); 663 | } 664 | } 665 | return indexBy(oauthButtons, 'id'); 666 | } 667 | 668 | oauthSignIn(serviceName) { 669 | const { user } = this.props; 670 | const { formState, waiting, onSubmitHook } = this.state; 671 | //Thanks Josh Owens for this one. 672 | function capitalService() { 673 | return serviceName.charAt(0).toUpperCase() + serviceName.slice(1); 674 | } 675 | 676 | if(serviceName === 'meteor-developer'){ 677 | serviceName = 'meteorDeveloperAccount'; 678 | } 679 | 680 | const loginWithService = Meteor["loginWith" + capitalService()]; 681 | 682 | let options = {}; // use default scope unless specified 683 | if (Accounts.ui._options.requestPermissions[serviceName]) 684 | options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName]; 685 | if (Accounts.ui._options.requestOfflineToken[serviceName]) 686 | options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName]; 687 | if (Accounts.ui._options.forceApprovalPrompt[serviceName]) 688 | options.forceApprovalPrompt = Accounts.ui._options.forceApprovalPrompt[serviceName]; 689 | 690 | this.clearMessages(); 691 | const self = this 692 | loginWithService(options, (error) => { 693 | onSubmitHook(error,formState); 694 | if (error) { 695 | this.showMessage(`error.accounts.${error.reason}` || "unknown_error"); 696 | } else { 697 | this.setState({ formState: STATES.PROFILE }); 698 | this.clearDefaultFieldValues(); 699 | loginResultCallback(() => { 700 | Meteor.setTimeout(() => this.state.onSignedInHook(), 100); 701 | }); 702 | } 703 | }); 704 | 705 | } 706 | 707 | signUp(options = {}) { 708 | const { 709 | username = null, 710 | email = null, 711 | usernameOrEmail = null, 712 | password, 713 | formState, 714 | onSubmitHook 715 | } = this.state; 716 | let error = false; 717 | this.clearMessages(); 718 | 719 | if (username !== null) { 720 | if ( !this.validateField('username', username) ) { 721 | if (this.state.formState == STATES.SIGN_UP) { 722 | this.state.onSubmitHook("error.accounts.usernameRequired", this.state.formState); 723 | } 724 | error = true; 725 | } else { 726 | options.username = username; 727 | } 728 | } else { 729 | if ([ 730 | "USERNAME_AND_EMAIL", 731 | "USERNAME_AND_EMAIL_NO_PASSWORD" 732 | ].includes(passwordSignupFields()) && !this.validateField('username', username) ) { 733 | if (this.state.formState == STATES.SIGN_UP) { 734 | this.state.onSubmitHook("error.accounts.usernameRequired", this.state.formState); 735 | } 736 | error = true; 737 | } 738 | } 739 | 740 | if (!this.validateField('email', email)){ 741 | error = true; 742 | } else { 743 | options.email = email; 744 | } 745 | 746 | if ([ 747 | "EMAIL_ONLY_NO_PASSWORD", 748 | "USERNAME_AND_EMAIL_NO_PASSWORD" 749 | ].includes(passwordSignupFields())) { 750 | // Generate a random password. 751 | options.password = Meteor.uuid(); 752 | } else if (!this.validateField('password', password)) { 753 | onSubmitHook("Invalid password", formState); 754 | error = true; 755 | } else { 756 | options.password = password; 757 | } 758 | 759 | const SignUp = function(_options) { 760 | Accounts.createUser(_options, (error) => { 761 | if (error) { 762 | this.showMessage(`error.accounts.${error.reason}` || "unknown_error", 'error'); 763 | if (this.translate(`error.accounts.${error.reason}`)) { 764 | onSubmitHook(`error.accounts.${error.reason}`, formState); 765 | } 766 | else { 767 | onSubmitHook("unknown_error", formState); 768 | } 769 | } 770 | else { 771 | onSubmitHook(null, formState); 772 | this.setState({ formState: STATES.PROFILE, password: null }); 773 | let user = Accounts.user(); 774 | loginResultCallback(this.state.onPostSignUpHook.bind(this, _options, user)); 775 | this.clearDefaultFieldValues(); 776 | } 777 | 778 | this.setState({ waiting: false }); 779 | }); 780 | }; 781 | 782 | if (!error) { 783 | this.setState({ waiting: true }); 784 | // Allow for Promises to return. 785 | let promise = this.state.onPreSignUpHook(options); 786 | if (promise instanceof Promise) { 787 | promise.then(SignUp.bind(this, options)); 788 | } 789 | else { 790 | SignUp(options); 791 | } 792 | } 793 | } 794 | 795 | loginWithoutPassword(){ 796 | const { 797 | email = '', 798 | usernameOrEmail = '', 799 | waiting, 800 | formState, 801 | onSubmitHook 802 | } = this.state; 803 | 804 | if (waiting) { 805 | return; 806 | } 807 | 808 | if (this.validateField('email', email)) { 809 | this.setState({ waiting: true }); 810 | 811 | Accounts.loginWithoutPassword({ email: email }, (error) => { 812 | if (error) { 813 | this.showMessage(`error.accounts.${error.reason}` || "unknown_error", 'error'); 814 | } 815 | else { 816 | this.showMessage(this.translate("info.emailSent"), 'success', 5000); 817 | this.clearDefaultFieldValues(); 818 | } 819 | onSubmitHook(error, formState); 820 | this.setState({ waiting: false }); 821 | }); 822 | } else if (this.validateField('username', usernameOrEmail)) { 823 | this.setState({ waiting: true }); 824 | 825 | Accounts.loginWithoutPassword({ email: usernameOrEmail, username: usernameOrEmail }, (error) => { 826 | if (error) { 827 | this.showMessage(`error.accounts.${error.reason}` || "unknown_error", 'error'); 828 | } 829 | else { 830 | this.showMessage(this.translate("info.emailSent"), 'success', 5000); 831 | this.clearDefaultFieldValues(); 832 | } 833 | onSubmitHook(error, formState); 834 | this.setState({ waiting: false }); 835 | }); 836 | } else { 837 | let errMsg = null; 838 | if (["USERNAME_AND_EMAIL_NO_PASSWORD"].includes(passwordSignupFields())) { 839 | errMsg = this.translate("error.accounts.invalid_email"); 840 | } 841 | else { 842 | errMsg = this.translate("error.accounts.invalid_email"); 843 | } 844 | this.showMessage(errMsg,'warning'); 845 | onSubmitHook(errMsg, formState); 846 | } 847 | } 848 | 849 | passwordReset() { 850 | const { 851 | email = '', 852 | waiting, 853 | formState, 854 | onSubmitHook 855 | } = this.state; 856 | 857 | if (waiting) { 858 | return; 859 | } 860 | 861 | this.clearMessages(); 862 | if (this.validateField('email', email)) { 863 | this.setState({ waiting: true }); 864 | 865 | Accounts.forgotPassword({ email: email }, (error) => { 866 | if (error) { 867 | this.showMessage(`error.accounts.${error.reason}` || "unknown_error", 'error'); 868 | } 869 | else { 870 | this.showMessage(this.translate("info.emailSent"), 'success', 5000); 871 | this.clearDefaultFieldValues(); 872 | } 873 | onSubmitHook(error, formState); 874 | this.setState({ waiting: false }); 875 | }); 876 | } 877 | } 878 | 879 | passwordChange() { 880 | const { 881 | password, 882 | newPassword, 883 | formState, 884 | onSubmitHook, 885 | onSignedInHook, 886 | } = this.state; 887 | 888 | if (!this.validateField('password', newPassword)){ 889 | onSubmitHook('err.minChar',formState); 890 | return; 891 | } 892 | 893 | let token = Accounts._loginButtonsSession.get('resetPasswordToken'); 894 | if (!token) { 895 | token = Accounts._loginButtonsSession.get('enrollAccountToken'); 896 | } 897 | if (token) { 898 | Accounts.resetPassword(token, newPassword, (error) => { 899 | if (error) { 900 | this.showMessage(`error.accounts.${error.reason}` || "unknown_error", 'error'); 901 | onSubmitHook(error, formState); 902 | } 903 | else { 904 | this.showMessage(this.translate('info.passwordChanged'), 'success', 5000); 905 | onSubmitHook(null, formState); 906 | this.setState({ formState: STATES.PROFILE }); 907 | Accounts._loginButtonsSession.set('resetPasswordToken', null); 908 | Accounts._loginButtonsSession.set('enrollAccountToken', null); 909 | onSignedInHook(); 910 | } 911 | }); 912 | } 913 | else { 914 | Accounts.changePassword(password, newPassword, (error) => { 915 | if (error) { 916 | this.showMessage(`error.accounts.${error.reason}` || "unknown_error", 'error'); 917 | onSubmitHook(error, formState); 918 | } 919 | else { 920 | this.showMessage('info.passwordChanged', 'success', 5000); 921 | onSubmitHook(null, formState); 922 | this.setState({ formState: STATES.PROFILE }); 923 | this.clearDefaultFieldValues(); 924 | } 925 | }); 926 | } 927 | } 928 | 929 | showMessage(message, type, clearTimeout, field){ 930 | message = this.translate(message).trim(); 931 | if (message) { 932 | this.setState(({ messages = [] }) => { 933 | messages.push({ 934 | message, 935 | type, 936 | ...(field && { field }), 937 | }); 938 | return { messages }; 939 | }); 940 | if (clearTimeout) { 941 | this.hideMessageTimout = setTimeout(() => { 942 | // Filter out the message that timed out. 943 | this.clearMessage(message); 944 | }, clearTimeout); 945 | } 946 | } 947 | } 948 | 949 | getMessageForField(field) { 950 | const { messages = [] } = this.state; 951 | return messages.find(({ field:key }) => key === field); 952 | } 953 | 954 | clearMessage(message) { 955 | if (message) { 956 | this.setState(({ messages = [] }) => ({ 957 | messages: messages.filter(({ message:a }) => a !== message), 958 | })); 959 | } 960 | } 961 | 962 | clearMessages() { 963 | if (this.hideMessageTimout) { 964 | clearTimeout(this.hideMessageTimout); 965 | } 966 | this.setState({ messages: [] }); 967 | } 968 | 969 | componentWillMount() { 970 | // XXX Check for backwards compatibility. 971 | if (Meteor.isClient) { 972 | const container = document.createElement('div'); 973 | ReactDOM.render(, container); 974 | if (container.getElementsByClassName('message').length == 0) { 975 | // Found backwards compatibility issue with 1.3.x 976 | console.warn(`Implementations of Accounts.ui.Field must render message in v1.2.11. 977 | https://github.com/studiointeract/accounts-ui/#deprecations`); 978 | } 979 | } 980 | } 981 | 982 | componentWillUnmount() { 983 | if (this.hideMessageTimout) { 984 | clearTimeout(this.hideMessageTimout); 985 | } 986 | } 987 | 988 | render() { 989 | this.oauthButtons(); 990 | // Backwords compatibility with v1.2.x. 991 | const { messages = [] } = this.state; 992 | const message = { 993 | deprecated: true, 994 | message: messages.map(({ message }) => message).join(', '), 995 | }; 996 | return ( 997 | this.translate(text)} 1004 | /> 1005 | ); 1006 | } 1007 | } 1008 | LoginForm.propTypes = { 1009 | formState: PropTypes.symbol, 1010 | loginPath: PropTypes.string, 1011 | signUpPath: PropTypes.string, 1012 | resetPasswordPath: PropTypes.string, 1013 | profilePath: PropTypes.string, 1014 | changePasswordPath: PropTypes.string, 1015 | }; 1016 | LoginForm.defaultProps = { 1017 | formState: null, 1018 | loginPath: null, 1019 | signUpPath: null, 1020 | resetPasswordPath: null, 1021 | profilePath: null, 1022 | changePasswordPath: null, 1023 | }; 1024 | 1025 | Accounts.ui.LoginForm = LoginForm; 1026 | 1027 | const LoginFormContainer = withTracker(() => { 1028 | // Listen for the user to login/logout and the services list to the user. 1029 | Meteor.subscribe('servicesList'); 1030 | return ({ 1031 | user: Accounts.user(), 1032 | }); 1033 | }, LoginForm); 1034 | Accounts.ui.LoginForm = LoginFormContainer; 1035 | export default LoginFormContainer 1036 | -------------------------------------------------------------------------------- /imports/ui/components/PasswordOrService.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Accounts } from 'meteor/accounts-base'; 4 | import { T9n } from 'meteor/softwarerero:accounts-t9n'; 5 | import { hasPasswordService } from '../../helpers.js'; 6 | 7 | export class PasswordOrService extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | hasPasswordService: hasPasswordService(), 12 | services: Object.keys(props.oauthServices).map(service => { 13 | return props.oauthServices[service].label 14 | }) 15 | }; 16 | } 17 | 18 | translate(text) { 19 | if (this.props.translate) { 20 | return this.props.translate(text); 21 | } 22 | return T9n.get(text); 23 | } 24 | 25 | render () { 26 | let { className = "password-or-service", style = {} } = this.props; 27 | let { hasPasswordService, services } = this.state; 28 | labels = services; 29 | if (services.length > 2) { 30 | labels = []; 31 | } 32 | 33 | if (hasPasswordService && services.length > 0) { 34 | return ( 35 |
36 | { `${this.translate('orUse')} ${ labels.join(' / ') }` } 37 |
38 | ); 39 | } 40 | return null; 41 | } 42 | } 43 | 44 | PasswordOrService.propTypes = { 45 | oauthServices: PropTypes.object 46 | }; 47 | 48 | Accounts.ui.PasswordOrService = PasswordOrService; 49 | -------------------------------------------------------------------------------- /imports/ui/components/SocialButtons.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Button.jsx'; 3 | import {Accounts} from 'meteor/accounts-base'; 4 | 5 | 6 | export class SocialButtons extends React.Component { 7 | render() { 8 | let { oauthServices = {}, className = "social-buttons" } = this.props; 9 | return( 10 |
11 | {Object.keys(oauthServices).map((id, i) => { 12 | return ; 13 | })} 14 |
15 | ); 16 | } 17 | } 18 | 19 | Accounts.ui.SocialButtons = SocialButtons; 20 | -------------------------------------------------------------------------------- /main_client.js: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base'; 2 | import './imports/accounts_ui.js'; 3 | import './imports/login_session.js'; 4 | import { STATES } from './imports/helpers.js'; 5 | import './imports/api/client/loginWithoutPassword.js'; 6 | import LoginForm from './imports/ui/components/LoginForm.jsx'; 7 | 8 | export { 9 | LoginForm as default, 10 | Accounts, 11 | STATES, 12 | }; 13 | -------------------------------------------------------------------------------- /main_server.js: -------------------------------------------------------------------------------- 1 | import { Accounts } from 'meteor/accounts-base'; 2 | import './imports/accounts_ui.js'; 3 | import './imports/login_session.js'; 4 | import { redirect, STATES } from './imports/helpers.js'; 5 | import './imports/api/server/loginWithoutPassword.js'; 6 | import './imports/api/server/servicesListPublication.js'; 7 | import LoginForm from './imports/ui/components/LoginForm.jsx'; 8 | 9 | export { 10 | LoginForm as default, 11 | Accounts, 12 | STATES, 13 | }; 14 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'std:accounts-ui', 3 | version: '1.3.1', 4 | summary: 'Accounts UI for React in Meteor 1.3+', 5 | git: 'https://github.com/studiointeract/accounts-ui', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('1.3'); 11 | api.use('ecmascript'); 12 | api.use('accounts-base'); 13 | api.use('check'); 14 | api.use('random'); 15 | api.use('email'); 16 | api.use('session'); 17 | api.use('react-meteor-data@0.2.15'); 18 | api.use('softwarerero:accounts-t9n'); 19 | api.use('tmeasday:check-npm-versions@0.3.0'); 20 | 21 | api.imply('accounts-base'); 22 | api.imply('softwarerero:accounts-t9n@1.3.3'); 23 | 24 | api.use('accounts-oauth', {weak: true}); 25 | api.use('accounts-password', {weak: true}); 26 | 27 | api.addFiles('check-npm.js', ['client', 'server']); 28 | 29 | api.mainModule('main_client.js', 'client'); 30 | api.mainModule('main_server.js', 'server'); 31 | }); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accounts-ui", 3 | "description": "Accounts UI for React Component in Meteor", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/studiointeract/accounts-ui.git" 7 | }, 8 | "keywords": [ 9 | "react", 10 | "meteor", 11 | "tracker" 12 | ], 13 | "author": "timbrandin", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/studiointeract/accounts-ui/issues" 17 | }, 18 | "homepage": "https://github.com/studiointeract/accounts-ui", 19 | "dependencies": { 20 | "prop-types": "^15.5.8" 21 | }, 22 | "peerDependencies": { 23 | "react": "^15.0.0", 24 | "react-dom": "^15.0.0", 25 | "react-addons-pure-render-mixin": "^15.0.0" 26 | } 27 | } 28 | --------------------------------------------------------------------------------