Browse Source

Moved all the code into models but need to refine even more.

pull/18/head
Zed A. Shaw 2 weeks ago
parent
commit
87e380fba5
13 changed files with 495 additions and 1162 deletions
  1. +10
    -47
      lib/data.js
  2. +104
    -10
      lib/models.js
  3. +4
    -1
      lib/testing.js
  4. +197
    -1035
      package-lock.json
  5. +3
    -4
      package.json
  6. +71
    -35
      services/auth.js
  7. +9
    -22
      src/node_modules/data.js
  8. +1
    -1
      src/routes/auth/index.svelte
  9. +0
    -2
      src/routes/index.svelte
  10. +0
    -2
      src/routes/modules/[slug]/[exercise]/index.svelte
  11. +1
    -3
      src/routes/register/index.svelte
  12. +40
    -0
      tests/models/auth.spec.js
  13. +55
    -0
      tests/models/user.spec.js

+ 10
- 47
lib/data.js View File

@@ -5,70 +5,33 @@ const { User, Auth } = require('../lib/models');
exports.find_token_with_email = async (email) => {
assert(email, `Valid email required: ${email}`);
try {
let res = await new Auth('email', email).fetch({require: false});
return res === null ? undefined : res.get('token');
} catch (error) {
log.error(error,"find_token_with_email");
return undefined;
}
let auth = await Auth.find_by_email(email);
log.debug("Auth returned by token", auth);
return auth ? auth.get('token') : undefined;
}
exports.create_auth_token = async (email, ip_addr, registration) => {
assert(email, `Valid email required: ${email}`);
log.debug(`Adding auth token for ${email} with registration ${registration}`);
let token = uuid4();
try {
let res = await new Auth({ email, ip_addr, registration, token }).save();
assert(res.get('token') === token, `Token ${token} just from the database does not match ${res.get('token')}`);
} catch (error) {
log.error(error, "create_auth_token");
}
return token;
let auth = await Auth.create(email, ip_addr, registration);
return auth.get('token');
}
exports.delete_auth_token = async (email) => {
assert(email, `Valid email required: ${email}`);
try {
let res = await new Auth().where({email}).destroy();
return res; // TODO: need to figure out the protocol on these
} catch (error) {
log.error(error, "delete_auth_token");
}
await Auth.destroy_by_email(email, false)
}
exports.is_valid_user = async (email) => {
// query the user in the database to see if they are registered and active
assert(email, `Valid email required: ${email}`);
try {
let user = await new User({email}).fetch({require: false});
// is the 2nd check necessary?
let is_valid = user && user.get("email") === email;
log.debug("User validity check for", email, "returned", user, "with is_valid", is_valid);
return {user: user.attributes, is_valid};
} catch(error) {
log.error(error, "is_valid_user");
return {user: undefined, is_valid: false};
}
let {user, is_valid} = await User.find_and_validate(email);
// TODO: need to come up with a better protocol on this
return {user: user !== null ? user.attributes : null, is_valid};
}
exports.delete_user = async (email) => {
assert(email, `Valid email required: ${email}`);
try {
let res = await new User().where({email}).destroy({require: false});
return res;
} catch (error) {
log.error(error, "delete_user");
}
return await User.delete_by_email(email, false);
}

+ 104
- 10
lib/models.js View File

@@ -1,8 +1,10 @@
const assert = require('assert');
const knexfile = require('../knexfile');
const uuid4 = require('uuid4');
const { log } = require('../lib/logging');
const assert = require('assert');
let knex = require('knex')(knexfile.development);
const bookshelf = require('bookshelf')(knex);
function fix_booleans(me, attr) {
for(let a of attr) {
@@ -19,21 +21,113 @@ function fix_booleans(me, attr) {
}
}
const bookshelf = require('bookshelf')(knex);
exports.User = bookshelf.model('User', {
tableName: 'users',
initialize() {
this.constructor.__super__.initialize.apply(this, arguments);
fix_booleans(this, ['send_emails', 'tos_agree', 'verified', 'registered']);
exports.User = bookshelf.model('User',
{ // object methods
tableName: 'users',
initialize() {
this.constructor.__super__.initialize.apply(this, arguments);
fix_booleans(this, ['send_emails', 'tos_agree', 'verified', 'registered']);
},
},
{ // class methods
async find_and_validate(email) {
assert(email);
try {
let user = await new this({email}).fetch({require: false});
// TODO: why am I getting by email then checking the email? do more validation
let is_valid = user ? user.get("email") === email && user.get('verified') : false;
log.debug("User validity check for", email, "returned", user, "with is_valid", is_valid);
return {user, is_valid};
} catch(error) {
log.error(error, "is_valid_user");
return {user: undefined, is_valid: false};
}
},
async delete_by_email(email, require) {
assert(email);
try {
return new this().where({email}).destroy({require});
} catch (error) {
log.error(error, "delete_user");
return undefined;
}
},
async register(email, full_name, send_emails, tos_agree) {
try {
return await new this({
email: email,
verified: true,
full_name, send_emails, tos_agree, registered: true
}).save();
} catch (error) {
log.error(error);
throw error;
}
}
}
});
);
exports.Auth = bookshelf.model('Auth', {
tableName: 'auths',
initialize() {
this.constructor.__super__.initialize.apply(this, arguments);
fix_booleans(this, ['registration']);
},
},
{
async create(email, ip_addr, registration) {
assert(email && ip_addr);
let token = uuid4();
try {
return new this({ email, ip_addr, registration, token }).save();
} catch (error) {
log.error(error, "Auth.create");
throw error;
}
},
async find_by_email(email) {
assert(email);
// TODO: make a smaller query that just finds the token
try {
return new this({email}).fetch({require: false});
} catch (error) {
log.error(error,"find_token_with_email");
throw error;
}
},
async find_by_token(token) {
console.log("TOKEN", token);
assert(token);
try {
// TODO: validate the token format
return new this().where({token}).fetch({require: false});
} catch (error) {
log.error(error, 'find_by_token');
throw error;
}
},
async destroy_by_email(email, require) {
assert(email);
try {
return new this().where({email}).destroy({require: require});
} catch (error) {
log.error(error, "delete_auth_token");
throw error;
}
}
}
});
);

+ 4
- 1
lib/testing.js View File

@@ -3,8 +3,9 @@ const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const assert = require('assert');
const allowed_devices = Object.keys(devices);
const { log } = require('@lib/logging');

jest.setTimeout(16000);
jest.setTimeout(5000);

const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
@@ -12,10 +13,12 @@ const sleep = (ms) => {

const has_content = async (page, where, what) => {
try {
log.debug("has_content", page.url(), where, what);
await page.waitForSelector(where);
let html = await page.$eval(where, e => e.innerHTML);
expect(html).toContain(what);
} catch (err) {
log.error("Content not found", page.url(), where, what);
throw err;
}
}

+ 197
- 1035
package-lock.json
File diff suppressed because it is too large
View File


+ 3
- 4
package.json View File

@@ -7,14 +7,14 @@
"build": "sapper build --legacy -p 7778",
"export": "sapper export --legacy -p 7778",
"start": "node __sapper__/build",
"test": "jest",
"testwatch": "jest --watch --notify",
"test": "NODE_OPTIONS='--unhandled-rejections=strict' jest --detectOpenHandles --runInBand --bail",
"testwatch": "NODE_OPTIONS='--unhandled-rejections=strict' jest --watch --notify --detectOpenHandles --runInBand",
"deploy": "npx sapper export && rsync -azzv __sapper__/export/ $DEP_USER@learnjsthehardway.com:/srv/http/www/learnjsthehardway.com/"
},
"dependencies": {
"arangojs": "^6.13.0",
"body-parser": "^1.19.0",
"bookshelf": "^1.1.0",
"bull": "^3.13.0",
"compression": "^1.7.1",
"cookie-session": "^1.4.0",
"cross-fetch": "^3.0.4",
@@ -23,7 +23,6 @@
"express-session": "^1.17.0",
"form-urlencoded": "^4.1.3",
"knex": "^0.20.12",
"kue": "^0.11.6",
"morgan": "^1.9.1",
"node-fetch": "^2.6.0",
"nodemailer": "^6.4.5",

+ 71
- 35
services/auth.js View File

@@ -1,8 +1,10 @@
const nodemailer = require('nodemailer');
const Queue = require('bull');
const db = require('../lib/data');
const { User, Auth } = require('../lib/models');
const { log } = require('../lib/logging');
const train = require('../lib/train');
const assert = require('assert');

// TODO: make a real transport when env PROD=true
let transporter = nodemailer.createTransport({
@@ -36,50 +38,84 @@ const subscribe = new Queue('subscribe', 'redis://127.0.0.1:6379');
const unsubscribe = new Queue('unsubscribe', 'redis://127.0.0.1:6379');


auth_request.process(async (job) => {
log.debug("Received job", job.id);
let authtoken;
let options;
auth_request.process(async (job, done) => {
try {
log.debug("Received job", job.id);
let authtoken;
let options;

// lookup the person in the user's table, and if they aren't there then
// send a registration email instead of a login email
let {user, is_valid} = await db.is_valid_user(job.data.to)
// lookup the person in the user's table, and if they aren't there then
// send a registration email instead of a login email
let {user, is_valid} = await db.is_valid_user(job.data.to)
log.debug("Processing auth_request user", user, "is_valid", is_valid);

if(is_valid === true) {
log.debug("User", job.data.to, "is valid sending an authentication email.");
authtoken = await db.create_auth_token(job.data.to, job.data.ip_addr, false);
options = gen_login_email(job.data.to, authtoken);
} else {
log.debug("User", job.data.to, "is NOT valid sending a REGISTRATION email.");
authtoken = await db.create_auth_token(job.data.to, job.data.ip_addr, true);
options = gen_reg_email(job.data.to, authtoken);
}
if(is_valid === true) {
log.debug("User", job.data.to, "is valid sending an authentication email.");
authtoken = await db.create_auth_token(job.data.to, job.data.ip_addr, false);
log.debug("Auth token is", authtoken);
options = gen_login_email(job.data.to, authtoken);
} else {
log.debug("User", job.data.to, "is NOT valid sending a REGISTRATION email.");
authtoken = await db.create_auth_token(job.data.to, job.data.ip_addr, true);
log.debug("Auth token is", authtoken);
options = gen_reg_email(job.data.to, authtoken);
}

let testtoken = await Auth.find_by_token(authtoken);
assert(testtoken, "The token is not actually in the databse.");

transporter.sendMail(options, function(error, info){
if (error) {
log.error(error);
} else {
log.debug(info.envelope);
log.debug(info.messageId);
info.message.pipe(process.stdout);
}
});
transporter.sendMail(options, function(error, info){
if (error) {
log.error(error);
} else {
log.debug(info.envelope);
log.debug(info.messageId);
info.message.pipe(process.stdout);
}
});
} catch (error) {
log.error(error, "auth_request");
done(error);
} finally {
done();
}
});

delete_auth.process(async (job) => {
log.info("Received delete request", job.data.email);
await db.delete_auth_token(job.data.email);
delete_auth.process(async (job, done) => {
try {
log.info("Received delete request", job.data.email);
await db.delete_auth_token(job.data.email);
} catch (error) {
log.error(error, "delete_auth");
done(error);
} finally {
done();
}
});

subscribe.process(async (job) => {
log.info("Received subscribe for user", job.data.email);
log.debug("Job data for subscribe", job);
await train.subscribe(job.data.email);
subscribe.process(async (job, done) => {
try {
log.info("Received subscribe for user", job.data.email);
log.debug("Job data for subscribe", job);
await train.subscribe(job.data.email);
} catch (error) {
log.error(error, "subscribe");
done(error);
} finally {
done();
}
});

unsubscribe.process(async (job) => {
log.info("Received unsubscribe for user", job.data.email);
await train.unsubscribe(job.data.email);
unsubscribe.process(async (job, done) => {
try {
log.info("Received unsubscribe for user", job.data.email);
await train.unsubscribe(job.data.email);
} catch (error) {
log.error(error, "unsubscribe");
done(error);
} finally {
done();
}
});

log.debug("Started server....");

+ 9
- 22
src/node_modules/data.js View File

@@ -5,13 +5,8 @@ const { User, Auth } = require('../../../lib/models');

// temporary until I can swap it all out
export async function get_auth_token(token) {
try {
let res = await new Auth({token}).fetch({require: false});

return res !== null ? res.attributes : undefined;
} catch (error) {
log.error(error, 'get_auth_token');
}
let auth = await Auth.find_by_token(token);
return auth ? auth.attributes : undefined;
}

export async function update_user(key, full_name, send_emails, email) {
@@ -25,21 +20,13 @@ export async function register_user(user, full_name, send_emails, tos_agree)
// email should be validated already before getting here, so we just need to add them
log.debug("Registering user", {user, full_name, send_emails, tos_agree});

try {
let new_user = await new User({
email: user.email,
verified: true,
full_name, send_emails, tos_agree, registered: true
}).save();

if(new_user.get('send_emails')) {
subscribe_user(new_user.get('email'));
}

return new_user;
} catch (error) {
log.error(error);
throw error;
let new_user = await User.register(user.email, full_name, send_emails, tos_agree);

if(new_user.get('send_emails')) {
log.debug("Sending Queue the subscribe to train request.");
subscribe_user(new_user.get('email'));
}

return new_user;
}


+ 1
- 1
src/routes/auth/index.svelte View File

@@ -23,7 +23,7 @@
error = 'The email address you typed is not a valid email address.'
email_sent = false;
} else {
// TODO: should the email also be validated on kue handler?
// TODO: should the email also be validated on bull handler?
const response = await post('auth/request', { email });

if(response.user) {

+ 0
- 2
src/routes/index.svelte View File

@@ -12,8 +12,6 @@

setTimeout(() => visible = true, 300);

console.log($session.user);

const register = async (event) => {
// TODO: is there a better way to talk to the other page?
$session.email_from_index = email;

+ 0
- 2
src/routes/modules/[slug]/[exercise]/index.svelte View File

@@ -43,8 +43,6 @@
let raw_md = await res.text();
let content = md.render(raw_md);

console.log(params.slug);

if (res.status === 200) {
return { toc, content, module: params.slug, exercise: params.exercise };
} else {

+ 1
- 3
src/routes/register/index.svelte View File

@@ -24,13 +24,11 @@ export let segment;
const user_email = $session.user.email;


$: form_valid = (tos_agree && full_name);
$: form_valid = (tos_agree === true && full_name);

const register = async (event) => {
console.log("Registration called");
let user = $session.user;
let res = await post('/register.json', { user, full_name, send_emails, tos_agree });
console.log("Registration returned", res);

if(res.user) {
console.log('New user is', res.user);

+ 40
- 0
tests/models/auth.spec.js View File

@@ -0,0 +1,40 @@
const t = require('@lib/testing');
const {Auth} = require('@lib/models');


afterAll(() => {
t.close()
});

it('Create auth works', async () => {
let user = t.fake_person();
let auth = await Auth.create(user.email, '127.0.0.1', false);
expect(auth.get('email')).toBe(user.email);
expect(auth.get('token')).toBeDefined();
expect(auth.get('registration')).toBe(false);
auth.destroy();
});


it('Can find by email or token', async () => {
let user = t.fake_person();
let auth = await Auth.create(user.email, '127.0.0.1', true);
let found = await Auth.find_by_email(user.email);
expect(found.get("token")).toBe(auth.get("token"));
expect(found.get("email")).toBe(auth.get("email"));

let found2 = await Auth.find_by_token(auth.get("token"));
expect(found2.get("token")).toBe(found.get("token"));
expect(found2.get("email")).toBe(found.get("email"));

found2.destroy();
});

it('Can destroy by email', async() => {
let user = t.fake_person();
let auth = await Auth.create(user.email, '127.0.0.1', true);
let del = await Auth.destroy_by_email(user.email, true);
let found = await Auth.find_by_email(user.email);
expect(found).toBe(null);
});


+ 55
- 0
tests/models/user.spec.js View File

@@ -0,0 +1,55 @@
const t = require('@lib/testing');
const { User } = require('@lib/models');

afterAll(() => {
t.close()
});

it('Can verify is_valid', async () => {
const faker = t.fake_person();
let user = await new User({
email: faker.email, full_name: faker.name,
send_emails: true, registered: true, verified: true,
tos_agree: true
}).save();

let found = await User.find_and_validate(faker.email);
expect(found.user).toBeDefined();
expect(found.user.get('email')).toBe(user.get('email'));
expect(found.is_valid).toBe(true);

user.destroy();
});

it('Can delete by an email', async () => {
const faker = t.fake_person();

let user = await new User({
email: faker.email, full_name: faker.name,
send_emails: true, registered: true, verified: true,
tos_agree: true
}).save();

let res = await User.delete_by_email(faker.email, false);
expect(res).toBeDefined();

let found = await User.find_and_validate(faker.email);
expect(found).toBeDefined();
expect(found.user).toBe(null);
expect(found.is_valid).toBe(false);
});

it('Can register a new user', async () => {
const faker = t.fake_person();

let user = await User.register(faker.email, faker.name, false, true);
expect(user).toBeDefined();
expect(user.get('email')).toBe(faker.email);

let found = await User.find_and_validate(faker.email);
expect(found).toBeDefined();
expect(found.user).toBeDefined();
expect(found.user.get('email')).toBe(user.get('email'));

user.destroy();
});

Loading…
Cancel
Save