blurts-server/db/DB.js

449 строки
13 KiB
JavaScript

"use strict";
// eslint-disable-next-line node/no-extraneous-require
const uuidv4 = require("uuid/v4");
const Knex = require("knex");
const { attachPaginate } = require("knex-paginate");
const { FluentError } = require("../locale-utils");
const AppConstants = require("../app-constants");
const { FXA } = require("../lib/fxa");
const HIBP = require("../hibp");
const Basket = require("../basket");
const getSha1 = require("../sha1-utils");
const mozlog = require("../log");
const knexConfig = require("./knexfile");
let knex = Knex(knexConfig);
attachPaginate();
const log = mozlog("DB");
const DB = {
async getSubscriberByToken(token) {
const res = await knex("subscribers")
.where("primary_verification_token", "=", token);
return res[0];
},
async getEmailByToken(token) {
const res = await knex("email_addresses")
.where("verification_token", "=", token);
return res[0];
},
async getEmailById(emailAddressId) {
const res = await knex("email_addresses")
.where("id", "=", emailAddressId);
return res[0];
},
async getSubscriberByTokenAndHash(token, emailSha1) {
const res = await knex.table("subscribers")
.first()
.where({
"primary_verification_token": token,
"primary_sha1": emailSha1,
});
return res;
},
async joinEmailAddressesToSubscriber(subscriber) {
if (subscriber) {
const emailAddressRecords = await knex("email_addresses").where({
"subscriber_id": subscriber.id,
});
subscriber.email_addresses = emailAddressRecords.map(
emailAddress=>({id: emailAddress.id, email: emailAddress.email})
);
}
return subscriber;
},
async getSubscriberById(id) {
const [subscriber] = await knex("subscribers").where({
"id": id,
});
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber);
return subscriberAndEmails;
},
async getSubscriberByFxaUid(uid) {
const [subscriber] = await knex("subscribers").where({
"fxa_uid": uid,
});
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber);
return subscriberAndEmails;
},
async getPreFxaSubscribersPage(pagination) {
return await knex("subscribers")
.whereRaw("(fxa_uid = '') IS NOT FALSE")
.paginate(pagination);
},
async getSubscriberByEmail(email) {
const [subscriber] = await knex("subscribers").where({
"primary_email": email,
"primary_verified": true,
});
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber);
return subscriberAndEmails;
},
async getEmailAddressRecordByEmail(email) {
const emailAddresses = await knex("email_addresses").where({
"email": email, verified: true,
});
if (!emailAddresses) {
return null;
}
if (emailAddresses.length > 1) {
// TODO: handle multiple emails in separate(?) subscriber accounts?
log.warn("getEmailAddressRecordByEmail", {msg: "found the same email multiple times"});
}
return emailAddresses[0];
},
async addSubscriberUnverifiedEmailHash(user, email) {
const res = await knex("email_addresses").insert({
subscriber_id: user.id,
email: email,
sha1: getSha1(email),
verification_token: uuidv4(),
verified: false,
}).returning("*");
return res[0];
},
async resetUnverifiedEmailAddress(emailAddressId) {
const newVerificationToken = uuidv4();
const res = await knex("email_addresses")
.update({
verification_token: newVerificationToken,
updated_at: knex.fn.now(),
})
.where("id", emailAddressId)
.returning("*");
return res[0];
},
async verifyEmailHash(token) {
const unverifiedEmail = await this.getEmailByToken(token);
if (!unverifiedEmail) {
throw new FluentError("Error message for this verification email timed out or something went wrong.");
}
const verifiedEmail = await this._verifyNewEmail(unverifiedEmail);
return verifiedEmail[0];
},
// TODO: refactor into an upsert? https://jaketrent.com/post/upsert-knexjs/
// Used internally, ideally should not be called by consumers.
async _getSha1EntryAndDo(sha1, aFoundCallback, aNotFoundCallback) {
const existingEntries = await knex("subscribers")
.where("primary_sha1", sha1);
if (existingEntries.length && aFoundCallback) {
return await aFoundCallback(existingEntries[0]);
}
if (!existingEntries.length && aNotFoundCallback) {
return await aNotFoundCallback();
}
},
// Used internally.
async _addEmailHash(sha1, email, signup_language, verified = false) {
try {
return await this._getSha1EntryAndDo(sha1, async aEntry => {
// Entry existed, patch the email value if supplied.
if (email) {
const res = await knex("subscribers")
.update({
primary_email: email,
primary_sha1: getSha1(email.toLowerCase()),
primary_verified: verified,
updated_at: knex.fn.now(),
})
.where("id", "=", aEntry.id)
.returning("*");
return res[0];
}
return aEntry;
}, async () => {
// Always add a verification_token value
const verification_token = uuidv4();
const res = await knex("subscribers")
.insert({ primary_sha1: getSha1(email.toLowerCase()), primary_email: email, signup_language, primary_verification_token: verification_token, primary_verified: verified })
.returning("*");
return res[0];
});
} catch (e) {
throw new FluentError("error-could-not-add-email");
}
},
/**
* Add a subscriber:
* 1. Add a record to subscribers
* 2. Immediately call _verifySubscriber
* 3. For FxA subscriber, add refresh token and profile data
*
* @param {string} email to add
* @param {string} signupLanguage from Accept-Language
* @param {string} fxaAccessToken from Firefox Account Oauth
* @param {string} fxaRefreshToken from Firefox Account Oauth
* @param {string} fxaProfileData from Firefox Account
* @returns {object} subscriber knex object added to DB
*/
async addSubscriber(email, signupLanguage, fxaAccessToken=null, fxaRefreshToken=null, fxaProfileData=null) {
const emailHash = await this._addEmailHash(getSha1(email), email, signupLanguage, true);
const verified = await this._verifySubscriber(emailHash);
const verifiedSubscriber = Array.isArray(verified) ? verified[0] : null;
if (fxaRefreshToken || fxaProfileData) {
return this._updateFxAData(verifiedSubscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData);
}
return verifiedSubscriber;
},
/**
* When an email is verified, convert it into a subscriber:
* 1. Subscribe the hash to HIBP
* 2. Update our subscribers record to verified
* 3. (if opted in) Subscribe the email to Fx newsletter
*
* @param {object} emailHash knex object in DB
* @returns {object} verified subscriber knex object in DB
*/
async _verifySubscriber(emailHash) {
// TODO: move this "up" into controllers/users ?
await HIBP.subscribeHash(emailHash.primary_sha1);
const verifiedSubscriber = await knex("subscribers")
.where("primary_email", "=", emailHash.primary_email)
.update({
primary_verified: true,
updated_at: knex.fn.now(),
})
.returning("*");
// TODO: move this "up" into controllers/users ?
if (emailHash.fx_newsletter) {
Basket.subscribe(emailHash.primary_email);
}
return verifiedSubscriber;
},
// Verifies new emails added by existing users
async _verifyNewEmail(emailHash) {
await HIBP.subscribeHash(emailHash.sha1);
const verifiedEmail = await knex("email_addresses")
.where("id", "=", emailHash.id)
.update({
verified: true,
})
.returning("*");
return verifiedEmail;
},
async getUserEmails(userId) {
const userEmails = await knex("email_addresses")
.where("subscriber_id", "=", userId)
.returning("*");
return userEmails;
},
/**
* Update fxa_refresh_token and fxa_profile_json for subscriber
*
* @param {object} subscriber knex object in DB
* @param {string} fxaAccessToken from Firefox Account Oauth
* @param {string} fxaRefreshToken from Firefox Account Oauth
* @param {string} fxaProfileData from Firefox Account
* @returns {object} updated subscriber knex object in DB
*/
async _updateFxAData(subscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData) {
const fxaUID = JSON.parse(fxaProfileData).uid;
const updated = await knex("subscribers")
.where("id", "=", subscriber.id)
.update({
fxa_uid: fxaUID,
fxa_access_token: fxaAccessToken,
fxa_refresh_token: fxaRefreshToken,
fxa_profile_json: fxaProfileData,
})
.returning("*");
const updatedSubscriber = Array.isArray(updated) ? updated[0] : null;
if (updatedSubscriber) {
FXA.destroyOAuthToken({refresh_token: subscriber.fxa_refresh_token});
}
return updatedSubscriber;
},
async updateFxAProfileData(subscriber, fxaProfileData) {
await knex("subscribers").where("id", subscriber.id)
.update({
fxa_profile_json: fxaProfileData,
});
return this.getSubscriberById(subscriber.id);
},
async setBreachesLastShownNow(subscriber) {
// TODO: turn 2 db queries into a single query (also see #942)
const nowDateTime = new Date();
const nowTimeStamp = nowDateTime.toISOString();
await knex("subscribers")
.where("id", "=", subscriber.id)
.update({
breaches_last_shown: nowTimeStamp,
});
return this.getSubscriberByEmail(subscriber.primary_email);
},
async setAllEmailsToPrimary(subscriber, allEmailsToPrimary) {
const updated = await knex("subscribers")
.where("id", subscriber.id)
.update({
all_emails_to_primary: allEmailsToPrimary,
})
.returning("*");
const updatedSubscriber = Array.isArray(updated) ? updated[0] : null;
return updatedSubscriber;
},
async setBreachesResolved(options) {
const { user, updatedResolvedBreaches } = options;
await knex("subscribers")
.where("id", user.id)
.update({
breaches_resolved: updatedResolvedBreaches,
});
return this.getSubscriberByEmail(user.primary_email);
},
async setWaitlistsJoined(options) {
const { user, updatedWaitlistsJoined } = options;
await knex("subscribers")
.where("id", user.id)
.update({
waitlists_joined: updatedWaitlistsJoined,
});
return this.getSubscriberByEmail(user.primary_email);
},
async removeSubscriber(subscriber) {
await knex("email_addresses").where({"subscriber_id": subscriber.id}).del();
await knex("subscribers").where({"id": subscriber.id}).del();
},
// This is used by SES callbacks to remove email addresses when recipients
// perma-bounce or mark our emails as spam
// Removes from either subscribers or email_addresses as necessary
async removeEmail(email) {
const subscriber = await this.getSubscriberByEmail(email);
if (!subscriber) {
const emailAddress = await this.getEmailAddressRecordByEmail(email);
if (!emailAddress) {
log.warn("removed-subscriber-not-found");
return;
}
await knex("email_addresses")
.where({
"email": email,
"verified": true,
})
.del();
return;
}
// This can fail if a subscriber has more email_addresses and marks
// a primary email as spam, but we should let it fail so we can see it
// in the logs
await knex("subscribers")
.where({
"primary_verification_token": subscriber.primary_verification_token,
"primary_sha1": subscriber.primary_sha1,
})
.del();
return;
},
async removeSubscriberByToken(token, emailSha1) {
const subscriber = await this.getSubscriberByTokenAndHash(token, emailSha1);
if (!subscriber) {
return false;
}
await knex("subscribers")
.where({
"primary_verification_token": subscriber.primary_verification_token,
"primary_sha1": subscriber.primary_sha1,
})
.del();
return subscriber;
},
async removeOneSecondaryEmail(emailId) {
await knex("email_addresses")
.where({
"id": emailId,
})
.del();
return;
},
async getSubscribersByHashes(hashes) {
return await knex("subscribers").whereIn("primary_sha1", hashes).andWhere("primary_verified", "=", true);
},
async getEmailAddressesByHashes(hashes) {
return await knex("email_addresses")
.join("subscribers", "email_addresses.subscriber_id", "=", "subscribers.id")
.whereIn("email_addresses.sha1", hashes)
.andWhere("email_addresses.verified", "=", true);
},
async deleteUnverifiedSubscribers() {
const expiredDateTime = new Date(Date.now() - AppConstants.DELETE_UNVERIFIED_SUBSCRIBERS_TIMER * 1000);
const expiredTimeStamp = expiredDateTime.toISOString();
await knex("subscribers")
.where("primary_verified", false)
.andWhere("created_at", "<", expiredTimeStamp)
.del();
},
async deleteSubscriberByFxAUID(fxaUID) {
await knex("subscribers").where("fxa_uid", fxaUID).del();
},
async deleteEmailAddressesByUid(uid) {
await knex("email_addresses").where({"subscriber_id": uid}).del();
},
async createConnection() {
if (knex === null) {
knex = Knex(knexConfig);
}
},
async destroyConnection() {
await knex.destroy();
knex = null;
},
};
module.exports = DB;