Browse Source

Now any email change will notify them so they can remember their emails.

master
Zed A. Shaw 1 week ago
parent
commit
ea012662c7
8 changed files with 94 additions and 13 deletions
  1. +9
    -7
      __tests__/api/user.spec.js
  2. +1
    -2
      __tests__/ui/user.spec.js
  3. +1
    -0
      lib/models.js
  4. +26
    -0
      scripts/services/auth.js
  5. +26
    -0
      src/emails/change_email.txt
  6. +10
    -0
      src/node_modules/mq.js
  7. +11
    -3
      src/routes/api/user/settings/email.json.js
  8. +10
    -1
      src/routes/user/EmailForm.svelte

+ 9
- 7
__tests__/api/user.spec.js View File

@@ -2,6 +2,7 @@ const t = require('@lib/testing');
const {User, knex} = require('@lib/models');
const host = require('@lib/secrets').root_url;
const { api_client, register_api_user} = require('../utils');
const assert = require('assert');


afterAll(() => {
@@ -53,20 +54,21 @@ const change_password = async (faker, api) => {
await api.post('/api/user/settings/password.json', {
password: 'testing', new_password: faker.password, repeat_password: faker.password
});

}

const change_email = async (faker, api) => {
// failed inputs missing
await t.throws(() => api.post('/api/user/settings/email.json', {
}), '401');
// wrong current email
await t.throws(() => api.post('/api/user/settings/email.json', {
password: `${faker.password}nope`, email: 'testing@testing.com'
password: `${faker.password}nope`, email: 'testing@testing.com', new_email: 'test@testing.com'
}), '403');

// works now
await api.post('/api/user/settings/email.json', {
password: faker.password, email: 'testing@testing.com'
password: faker.password, new_email: 'testing@testing.com', email: faker.email
});

}
@@ -81,9 +83,9 @@ it('User API can change identity settings', async () => {
// for now everyone just has one payment
expect(result.data.payments.length).toBe(1);

change_identity(faker, api);
change_password(faker, api);
change_email(faker, api);
await change_identity(faker, api);
await change_password(faker, api);
await change_email(faker, api);
});




+ 1
- 2
__tests__/ui/user.spec.js View File

@@ -34,6 +34,7 @@ it('Can update user information', async () => {
await submit(page, 'email')
await t.has_content(page, t.sel('email-form-error'), 'email must be a valid');

await page.type(t.sel('email-form-email'), main_user.email);
await page.type(t.sel('email-form-email'), 'a@a.com');
await page.type(t.sel('email-form-password'), main_user.password);
await submit(page, 'email')
@@ -55,8 +56,6 @@ it('Can update user information', async () => {
await page.type(t.sel('identity-form-full_name'), 'Zed A. Shaw');
await submit(page, 'identity');

await t.has_content(page, t.sel('identity-message'), 'Your identity has been changed');
} catch(error) {
console.error(error);
}


+ 1
- 0
lib/models.js View File

@@ -256,6 +256,7 @@ class User extends Model.from_table('users') {

async change_password(new_password) {
assert(new_password, "Password is required for change password.")
// remember that this.update handles all of the password and email hashing
return this.update({password: new_password});
}



+ 26
- 0
scripts/services/auth.js View File

@@ -26,6 +26,9 @@ const register_email = {
html: template('src/emails/register_email.html')
}

const change_email = {
text: template('src/emails/change_email.txt'),
}

const send_email = (mail_opts) => {
if(secrets.env == 'development') {
@@ -79,9 +82,21 @@ const send_reset_email = (to, user, reset_code, browser, ip_addr) => {
});
}

// change emails are text only for security
const send_change_email = (to, user, change_type, change_data, ip_addr) => {
send_email({
to,
from: '"Zed from LJSTHW" help@learnjsthehardway.com',
subject: "Your LJSTHW Account Has Changed",
text: change_email.text({user, change_type, change_data, company, ip_addr}),
email_type: 'change_account'
})
}

const reset_request = new Queue('reset_request', secrets.bull_config);
const reset_finish = new Queue('reset_finish', secrets.bull_config);
const register = new Queue('register', secrets.bull_config);
const change_account = new Queue('change_account', secrets.bull_config);

reset_request.process(async (job) => {
try {
@@ -135,4 +150,15 @@ register.process(async (job) => {
}
});

change_account.process(async (job) => {
try {
log.info("Received change account for user", job.data.email);
let user = await User.first({id: job.data.user_id});
assert(user, `No user found for user_id ${job.data.user_id}`);
send_change_email(job.data.email, user, job.data.change_type, job.data.change_data, job.data.ip_addr);
} catch(error) {
log.error(error, "change_account");
}
});

log.info("Started server with bull config:", secrets.bull_config);

+ 26
- 0
src/emails/change_email.txt View File

@@ -0,0 +1,26 @@
Your Account Has Changed
------------------------

Your account at <%- company.product %> was recently changed by a computer at IP Address <%- ip_addr %>.

The change made was:

<%= change_type %>

It was changed to:

<%= change_data %>

If you did not make this change then please email help@<%- company.website %> and tell them you did not
make these changes.

[NOTE: This email is in plain text format for security so you see it.
If you found it in your spam folder please let us know.]

------------------------
Copyright (C) 2019 <%- company.owner %>. All rights reserved.

<%- company.name %>
<%- company.street %>
<%- company.city %>, <%- company.state %>
<%- company.zip_code %>

+ 10
- 0
src/node_modules/mq.js View File

@@ -8,6 +8,7 @@ const reset_finish = new Queue('reset_finish', config.bull_config);
const register = new Queue('register', config.bull_config);
const receipt = new Queue('receipt', config.bull_config);
const invoice = new Queue('invoice', config.bull_config);
const change_account = new Queue('change_account', config.bull_config);

/* Given an email address and an ip address this will generate and return
* an authentication token for the session as well as message the auth service.
@@ -44,3 +45,12 @@ export function send_invoice(user_id, email, recipient) {
assert(recipient, `Must give a valid recipient.`);
const job = invoice.add({user_id, email, recipient});
}

export function send_change_account(user_id, email, change_type, change_data, ip_addr) {
assert(user_id, `Must give a valid user_id.`);
assert(email, `Must give a valid email.`);
assert(change_type, `Must give a valid change_type.`);
assert(change_data, `Must give a valid change_data.`);
assert(ip_addr, `Must give a valid ip_addr.`);
const job = change_account.add({user_id, email, change_type, change_data, ip_addr});
}

+ 11
- 3
src/routes/api/user/settings/email.json.js View File

@@ -2,6 +2,7 @@ import * as auth from 'auth';
import assert from 'assert';
import { log } from 'logging';
import { stores } from 'sabaton';
import { send_change_account } from 'mq';


export const post = auth.restricted(
@@ -10,13 +11,20 @@ export const post = auth.restricted(
const user = req.user;

try {
if(!$msg.password || !$msg.email) {
$app.error(401, "You need password and email to change your email.");
if(!$msg.password || !$msg.email || !$msg.new_email) {
$app.error(401, "You need password, current email, and new email.");
} else if(!await user.valid_password($msg.password)) {
$app.error(403, "That's not your current password.");
} else if(!await user.valid_email($msg.email)) {
log.error("INVALID EMAIL", $msg.email);
$app.error(403, "That's not your current email address.");
} else {
let count = await user.update({email: $msg.email});
$msg.new_email = $msg.new_email.toLowerCase();

let count = await user.update({email: $msg.new_email});
assert(count == 1, `Failed to update user ID ${user.id} expected count 1 got ${count}.`);
send_change_account(user.id, $msg.email, 'Email Changed', `New email: ${$msg.new_email}`, req.clientIp);
send_change_account(user.id, $msg.new_email, 'Email Changed', `New email: ${$msg.new_email}`, req.clientIp);
$app.done({ message: "OK"});
}
} catch (error) {


+ 10
- 1
src/routes/user/EmailForm.svelte View File

@@ -20,6 +20,7 @@

const rules = (data) => ({
email: 'required|email',
new_email: 'required:email',
password: 'required|min:6',
})

@@ -98,12 +99,20 @@
<div class="panel-body">
<div class="form-group" data-testid="email-group" class:has-error={ show_errors && errors.email }>
<label class="form-label" for="email-field">
New Email (Obfuscated) <a alt="Read the Privacy Policy" aria-label="Read the Privacy Policy" href="/privacy" on:click|preventDefault={toggle_privacy}><Icon name="help-circle" tooltip="Read the Radical Privacy Policy" tooltip_bottom={ true } /></a>
Current Email<a alt="Read the Privacy Policy" aria-label="Read the Privacy Policy" href="/privacy" on:click|preventDefault={toggle_privacy}><Icon name="help-circle" tooltip="Read the Radical Privacy Policy" tooltip_bottom={ true } /></a>
</label>
<input class="form-input" data-testid='email-form-email' type="email" id="email-field" bind:value={form.email} placeholder="Email">
<FormError visible={show_errors} testid='email-form-error' errors={ errors.email } />
</div>

<div class="form-group" data-testid="email-group" class:has-error={ show_errors && errors.new_email }>
<label class="form-label" for="new-email-field">
New Email (Obfuscated) <a alt="Read the Privacy Policy" aria-label="Read the Privacy Policy" href="/privacy" on:click|preventDefault={toggle_privacy}><Icon name="help-circle" tooltip="Read the Radical Privacy Policy" tooltip_bottom={ true } /></a>
</label>
<input class="form-input" data-testid='email-form-new-email' type="email" id="new-email-field" bind:value={form.new_email} placeholder="Email">
<FormError visible={show_errors} testid='new-email-form-error' errors={ errors.new_email } />
</div>

<div class="form-group" class:has-error={ show_errors && errors.password }>
<label class="form-label" for="password-field">
Password <Icon name="help-circle" tooltip="Make it secure"/>


Loading…
Cancel
Save