Skip to content

Commit

Permalink
Add reset and forgot password functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
snewcomer authored and joshsmith committed May 30, 2017
1 parent 6a299ab commit a9c7c7c
Show file tree
Hide file tree
Showing 25 changed files with 620 additions and 40 deletions.
49 changes: 49 additions & 0 deletions app/components/password/forgot-password.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Ember from 'ember';
const { Component, set, get, inject: { service } } = Ember;
import { task } from 'ember-concurrency';

export default Component.extend({
classNames: ['form--centered', 'forgot-password-form'],

/**
* @property flashMessages
* @type Ember.Service
*/
flashMessages: service(),

/**
* @property email
* @default String
*/
email: '',
/**
* @property error
*/
error: null,

/**
* @property forgotPasswordTask
* @param email
*/
forgotPasswordTask: task(function* (email) {
try {
yield get(this, 'forgotPassword')(email);
get(this, 'flashMessages').clearMessages().success("Check your email for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder or double-check whether you have an account with this email.");
set(this, 'error', null);
} catch(e) {
set(this, 'error', e);
}
}),

actions: {

/**
* @method forgotPassword
* @param email
*/
forgotPassword(email) {
return get(this, 'forgotPasswordTask').perform(email);
}

}
});
55 changes: 55 additions & 0 deletions app/components/password/reset-password.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Ember from 'ember';
const { Component, set, get, inject: { service } } = Ember;
import { task } from 'ember-concurrency';

export default Component.extend({
classNames: ['form--centered', 'reset-password-form'],

/**
* @property flashMessages
* @type Ember.Service
*/
flashMessages: service(),
/**
* @property password
* @default String
*/
password: '',
/**
* @property passwordConfirmation
* @default String
*/
passwordConfirmation: '',
/**
* @property error
*/
error: null,

/**
* @property resetPasswordTask
* @param password
* @param passwordConfirmation
*/
resetPasswordTask: task(function* (password, passwordConfirmation) {
try {
yield get(this, 'resetPassword')(password, passwordConfirmation);
get(this, 'flashMessages').clearMessages().success("Your password has been reset and you're now signed in.");
set(this, 'error', null);
} catch(e) {
set(this, 'error', e);
}
}),

actions: {

/**
* @method resetPassword
* @param password
* @param passwordConfirmation
*/
resetPassword(password, passwordConfirmation) {
return get(this, 'resetPasswordTask').perform(password, passwordConfirmation);
}

}
});
5 changes: 5 additions & 0 deletions app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ AppRouter.map(function() {
path: '/projects'
});

this.route('password', function() {
this.route('reset');
this.route('forgot');
});

this.route('settings', function() {
this.route('profile');
});
Expand Down
20 changes: 20 additions & 0 deletions app/routes/password/forgot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Ember from 'ember';
const { Route, get, inject: { service } } = Ember;

export default Route.extend({

ajax: service(),

actions: {
forgotPassword(email) {
return get(this, 'ajax').request('/password/forgot', {
method: 'POST',
data: {
email
}
}).then(() => {
this.transitionTo('index');
});
}
}
});
34 changes: 34 additions & 0 deletions app/routes/password/reset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Ember from 'ember';
const { Route, get, set, inject: { service } } = Ember;

export default Route.extend({
queryParams: {
token: { refreshModel: true }
},

ajax: service(),
session: service(),

model(params) {
return params.token;
},

setupController(controller, model) {
set(controller, 'token', model);
},

actions: {
resetPassword(password, passwordConfirmation) {
return get(this, 'ajax').request('/password/reset', {
method: 'POST',
data: {
token: get(this, 'controller.token'),
password,
'password-confirmation': passwordConfirmation
}
}).then((response) => {
return get(this, 'session').authenticate('authenticator:jwt', { identification: response.email, password });
});
}
}
});
2 changes: 1 addition & 1 deletion app/styles/layout/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ form {
margin: 5px;
}

.login-form, .signup-form {
.login-form, .signup-form, .reset-password-form, .forgot-password-form {
.input-group {
margin: 0 0 1em 0;
}
Expand Down
4 changes: 2 additions & 2 deletions app/templates/components/error-formatter.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{#each messages as |message|}}
<p class="error">{{message}}</p>
<p class="error" data-test-id="error-msg">{{message}}</p>
{{else}}
<p class="error">{{defaultMessage}}</p>
<p class="error" data-test-id="default-msg">{{defaultMessage}}</p>
{{/each}}
4 changes: 4 additions & 0 deletions app/templates/components/login-form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
</button>
</div>

{{#link-to 'password.forgot' classNames="t-forgot-password"}}
Forgot your password?
{{/link-to}}

{{#if errors}}
{{#each errors.errors as |error|}}
<p class="error">{{error.detail}}</p>
Expand Down
23 changes: 23 additions & 0 deletions app/templates/components/password/forgot-password.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="container">
<form>
<h2>Reset your password</h2>

<p data-test-id="forgot-password-header">Enter your email and we'll send you a link to reset your password.</p>

<div class="input-group">
{{#auto-focus}}
{{input name="email" autocapitalize="off" type="text" value=email}}
{{/auto-focus}}
</div>

<div class="input-group">
<button id="reset-password" class="default" type="submit" disabled={{isLoading}} {{action "forgotPassword" email}}>
Send password reset email
</button>
</div>

{{#if error}}
{{error-formatter error=error}}
{{/if}}
</form>
</div>
27 changes: 27 additions & 0 deletions app/templates/components/password/reset-password.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="container">
<form>
<h2>Change your password</h2>

<div class="input-group">
<label data-test-id="password-label">Password</label>
{{#auto-focus}}
{{input id="password" autofocus="true" class="has-progress" name="password" type="password" autocomplete="off" value=password}}
{{/auto-focus}}
</div>

<div class="input-group">
<label data-test-id="password-confirmation-label">Confirm password</label>
{{input id="password-confirmation" class="has-progress" name="password" type="password" autocomplete="off" value=passwordConfirmation}}
</div>

<div class="input-group">
<button id="reset-password" class="default" type="submit" disabled={{resetPasswordTask.isRunning}} {{action "resetPassword" password passwordConfirmation}}>
Change password
</button>
</div>

{{#if error}}
{{error-formatter error=error}}
{{/if}}
</form>
</div>
3 changes: 3 additions & 0 deletions app/templates/password/forgot.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{password/forgot-password
forgotPassword=(route-action "forgotPassword")
}}
3 changes: 3 additions & 0 deletions app/templates/password/reset.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{password/reset-password
resetPassword=(route-action "resetPassword")
}}
39 changes: 39 additions & 0 deletions mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,45 @@ export default function() {
// PATCH /organizations/:id
this.patch('/organizations/:id');

/**
* Password
*/

// POST /password/forgot
this.post('/password/forgot', () => {

// just return something?
return new Mirage.Response(201, {}, {
email: 'test@test.com'
});
});

this.post('/password/reset', function(schema) {
let { password, 'password-confirmation': passwordConfirmation, token } = this.normalizedRequestAttrs();

let [matchedUser] = schema.users.where({ token }).models;

if (password === passwordConfirmation) {
return new Mirage.Response(201, {}, {
email: matchedUser.email,
token,
user_id: matchedUser.id
});
} else {
let errorDetail = 'Your passwords do not match';
return new Mirage.Response(422, {}, {
errors: [
{
id: 'VALIDATION_ERROR',
title: '422',
detail: errorDetail,
status: 422
}
]
});
}
});

/**
* Previews
*/
Expand Down
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,6 @@
"normalize.css": "^6.0.0",
"yuidoc-ember-theme": "^1.3.0"
},
"dependencies": {
"node-sass": "latest"
},
"engines": {
"node": ">= 4"
},
Expand Down
8 changes: 8 additions & 0 deletions tests/acceptance/login-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,11 @@ test('When authenticated, redirects from signup', function(assert) {
assert.equal(currentURL(), '/projects');
});
});

test('Can get to forgot password route from login page', function(assert) {
loginPage.visit();
loginPage.clickForgotPassword();
andThen(() => {
assert.equal(currentURL(), '/password/forgot');
});
});
Loading

0 comments on commit a9c7c7c

Please sign in to comment.