Merge branch 'develop' into feature/error-output
This commit is contained in:
commit
a1accc11a9
20 changed files with 590 additions and 106 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -19,3 +19,4 @@ localhost.key
|
||||||
privkey.pem
|
privkey.pem
|
||||||
|
|
||||||
storybook-static
|
storybook-static
|
||||||
|
duplicates.json
|
||||||
|
|
|
@ -7,7 +7,7 @@ const fallbackLng = ['en-US'];
|
||||||
const availableLanguages = ['en-US', 'es-419'];
|
const availableLanguages = ['en-US', 'es-419'];
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
loadPath: 'locales/{{lng}}/translations.json',
|
loadPath: '/locales/{{lng}}/translations.json',
|
||||||
requestOptions: { // used for fetch, can also be a function (payload) => ({ method: 'GET' })
|
requestOptions: { // used for fetch, can also be a function (payload) => ({ method: 'GET' })
|
||||||
mode: 'no-cors'
|
mode: 'no-cors'
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,6 +16,8 @@ import {
|
||||||
import { clearState, saveState } from '../../../persistState';
|
import { clearState, saveState } from '../../../persistState';
|
||||||
|
|
||||||
const ROOT_URL = getConfig('API_URL');
|
const ROOT_URL = getConfig('API_URL');
|
||||||
|
const S3_BUCKET_URL_BASE = getConfig('S3_BUCKET_URL_BASE');
|
||||||
|
const S3_BUCKET = getConfig('S3_BUCKET');
|
||||||
|
|
||||||
export function setProject(project) {
|
export function setProject(project) {
|
||||||
return {
|
return {
|
||||||
|
@ -287,7 +289,7 @@ export function cloneProject(id) {
|
||||||
|
|
||||||
// duplicate all files hosted on S3
|
// duplicate all files hosted on S3
|
||||||
each(newFiles, (file, callback) => {
|
each(newFiles, (file, callback) => {
|
||||||
if (file.url && file.url.includes('amazonaws')) {
|
if (file.url && (file.url.includes(S3_BUCKET_URL_BASE) || file.url.includes(S3_BUCKET))) {
|
||||||
const formParams = {
|
const formParams = {
|
||||||
url: file.url
|
url: file.url
|
||||||
};
|
};
|
||||||
|
|
23
package-lock.json
generated
23
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "p5.js-web-editor",
|
"name": "p5.js-web-editor",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -32207,17 +32207,15 @@
|
||||||
"integrity": "sha1-nYqSjH8sN1E8LQZOV7Pjw1bp+rs="
|
"integrity": "sha1-nYqSjH8sN1E8LQZOV7Pjw1bp+rs="
|
||||||
},
|
},
|
||||||
"react-redux": {
|
"react-redux": {
|
||||||
"version": "5.1.2",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz",
|
||||||
"integrity": "sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==",
|
"integrity": "sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.1.2",
|
"@babel/runtime": "^7.5.5",
|
||||||
"hoist-non-react-statics": "^3.3.0",
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
"invariant": "^2.2.4",
|
"loose-envify": "^1.4.0",
|
||||||
"loose-envify": "^1.1.0",
|
"prop-types": "^15.7.2",
|
||||||
"prop-types": "^15.6.1",
|
"react-is": "^16.9.0"
|
||||||
"react-is": "^16.6.0",
|
|
||||||
"react-lifecycles-compat": "^3.0.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hoist-non-react-statics": {
|
"hoist-non-react-statics": {
|
||||||
|
@ -32227,6 +32225,11 @@
|
||||||
"requires": {
|
"requires": {
|
||||||
"react-is": "^16.7.0"
|
"react-is": "^16.7.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "p5.js-web-editor",
|
"name": "p5.js-web-editor",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"description": "The web editor for p5.js.",
|
"description": "The web editor for p5.js.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
|
@ -200,7 +200,7 @@
|
||||||
"react-helmet": "^5.1.3",
|
"react-helmet": "^5.1.3",
|
||||||
"react-hot-loader": "^4.12.19",
|
"react-hot-loader": "^4.12.19",
|
||||||
"react-i18next": "^11.5.0",
|
"react-i18next": "^11.5.0",
|
||||||
"react-redux": "^5.1.2",
|
"react-redux": "^7.2.0",
|
||||||
"react-router": "^3.2.5",
|
"react-router": "^3.2.5",
|
||||||
"react-split-pane": "^0.1.89",
|
"react-split-pane": "^0.1.89",
|
||||||
"react-tabs": "^2.3.1",
|
"react-tabs": "^2.3.1",
|
||||||
|
|
|
@ -24,7 +24,7 @@ passport.deserializeUser((id, done) => {
|
||||||
* Sign in using Email/Username and Password.
|
* Sign in using Email/Username and Password.
|
||||||
*/
|
*/
|
||||||
passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
|
passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
|
||||||
User.findByMailOrName(email.toLowerCase())
|
User.findByEmailOrUsername(email)
|
||||||
.then((user) => { // eslint-disable-line consistent-return
|
.then((user) => { // eslint-disable-line consistent-return
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return done(null, false, { msg: `Email ${email} not found.` });
|
return done(null, false, { msg: `Email ${email} not found.` });
|
||||||
|
@ -43,7 +43,7 @@ passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, don
|
||||||
* Authentificate using Basic Auth (Username + Api Key)
|
* Authentificate using Basic Auth (Username + Api Key)
|
||||||
*/
|
*/
|
||||||
passport.use(new BasicStrategy((userid, key, done) => {
|
passport.use(new BasicStrategy((userid, key, done) => {
|
||||||
User.findOne({ username: userid }, (err, user) => { // eslint-disable-line consistent-return
|
User.findByUsername(userid, (err, user) => { // eslint-disable-line consistent-return
|
||||||
if (err) { return done(err); }
|
if (err) { return done(err); }
|
||||||
if (!user) { return done(null, false); }
|
if (!user) { return done(null, false); }
|
||||||
user.findMatchingKey(key, (innerErr, isMatch, keyDocument) => {
|
user.findMatchingKey(key, (innerErr, isMatch, keyDocument) => {
|
||||||
|
@ -98,9 +98,7 @@ passport.use(new GitHubStrategy({
|
||||||
const emails = getVerifiedEmails(profile.emails);
|
const emails = getVerifiedEmails(profile.emails);
|
||||||
const primaryEmail = getPrimaryEmail(profile.emails);
|
const primaryEmail = getPrimaryEmail(profile.emails);
|
||||||
|
|
||||||
User.findOne({
|
User.findByEmail(emails, (findByEmailErr, existingEmailUser) => {
|
||||||
email: { $in: emails },
|
|
||||||
}, (findByEmailErr, existingEmailUser) => {
|
|
||||||
if (existingEmailUser) {
|
if (existingEmailUser) {
|
||||||
existingEmailUser.email = existingEmailUser.email || primaryEmail;
|
existingEmailUser.email = existingEmailUser.email || primaryEmail;
|
||||||
existingEmailUser.github = profile.id;
|
existingEmailUser.github = profile.id;
|
||||||
|
@ -141,11 +139,9 @@ passport.use(new GoogleStrategy({
|
||||||
|
|
||||||
const primaryEmail = profile._json.emails[0].value;
|
const primaryEmail = profile._json.emails[0].value;
|
||||||
|
|
||||||
User.findOne({
|
User.findByEmail(primaryEmail, (findByEmailErr, existingEmailUser) => {
|
||||||
email: primaryEmail,
|
|
||||||
}, (findByEmailErr, existingEmailUser) => {
|
|
||||||
let username = profile._json.emails[0].value.split('@')[0];
|
let username = profile._json.emails[0].value.split('@')[0];
|
||||||
User.findOne({ username }, (findByUsernameErr, existingUsernameUser) => {
|
User.findByUsername(username, (findByUsernameErr, existingUsernameUser) => {
|
||||||
if (existingUsernameUser) {
|
if (existingUsernameUser) {
|
||||||
const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)];
|
const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)];
|
||||||
username = slugify(`${username} ${adj}`);
|
username = slugify(`${username} ${adj}`);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export const getObjectKey = jest.mock();
|
export const getObjectKey = jest.mock();
|
||||||
export const deleteObjectsFromS3 = jest.fn();
|
export const deleteObjectsFromS3 = jest.fn();
|
||||||
export const signS3 = jest.fn();
|
export const signS3 = jest.fn();
|
||||||
export const copyObjectInS3 = jest.fn();
|
export const copyObjectInS3RequestHandler = jest.fn();
|
||||||
export const listObjectsInS3ForUser = jest.fn();
|
export const listObjectsInS3ForUser = jest.fn();
|
||||||
|
|
|
@ -28,7 +28,7 @@ function getExtension(filename) {
|
||||||
export function getObjectKey(url) {
|
export function getObjectKey(url) {
|
||||||
const urlArray = url.split('/');
|
const urlArray = url.split('/');
|
||||||
let objectKey;
|
let objectKey;
|
||||||
if (urlArray.length === 6) {
|
if (urlArray.length === 5) {
|
||||||
const key = urlArray.pop();
|
const key = urlArray.pop();
|
||||||
const userId = urlArray.pop();
|
const userId = urlArray.pop();
|
||||||
objectKey = `${userId}/${key}`;
|
objectKey = `${userId}/${key}`;
|
||||||
|
@ -98,24 +98,72 @@ export function signS3(req, res) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyObjectInS3(req, res) {
|
export function copyObjectInS3(url, userId) {
|
||||||
const { url } = req.body;
|
return new Promise((resolve, reject) => {
|
||||||
const objectKey = getObjectKey(url);
|
const objectKey = getObjectKey(url);
|
||||||
const fileExtension = getExtension(objectKey);
|
const fileExtension = getExtension(objectKey);
|
||||||
const newFilename = uuid.v4() + fileExtension;
|
const newFilename = uuid.v4() + fileExtension;
|
||||||
const userId = req.user.id;
|
const headParams = {
|
||||||
const params = {
|
Bucket: `${process.env.S3_BUCKET}`,
|
||||||
Bucket: `${process.env.S3_BUCKET}`,
|
Key: `${objectKey}`
|
||||||
CopySource: `${process.env.S3_BUCKET}/${objectKey}`,
|
};
|
||||||
Key: `${userId}/${newFilename}`,
|
client.s3.headObject(headParams, (headErr) => {
|
||||||
ACL: 'public-read'
|
if (headErr) {
|
||||||
};
|
reject(new Error(`Object with key ${process.env.S3_BUCKET}/${objectKey} does not exist.`));
|
||||||
const copy = client.copyObject(params);
|
return;
|
||||||
copy.on('err', (err) => {
|
}
|
||||||
console.log(err);
|
const params = {
|
||||||
|
Bucket: `${process.env.S3_BUCKET}`,
|
||||||
|
CopySource: `${process.env.S3_BUCKET}/${objectKey}`,
|
||||||
|
Key: `${userId}/${newFilename}`,
|
||||||
|
ACL: 'public-read'
|
||||||
|
};
|
||||||
|
const copy = client.copyObject(params);
|
||||||
|
copy.on('err', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
copy.on('end', (data) => {
|
||||||
|
resolve(`${s3Bucket}${userId}/${newFilename}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
copy.on('end', (data) => {
|
}
|
||||||
res.json({ url: `${s3Bucket}${userId}/${newFilename}` });
|
|
||||||
|
export function copyObjectInS3RequestHandler(req, res) {
|
||||||
|
const { url } = req.body;
|
||||||
|
copyObjectInS3(url, req.user.id).then((newUrl) => {
|
||||||
|
res.json({ url: newUrl });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveObjectToUserInS3(url, userId) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const objectKey = getObjectKey(url);
|
||||||
|
const fileExtension = getExtension(objectKey);
|
||||||
|
const newFilename = uuid.v4() + fileExtension;
|
||||||
|
const headParams = {
|
||||||
|
Bucket: `${process.env.S3_BUCKET}`,
|
||||||
|
Key: `${objectKey}`
|
||||||
|
};
|
||||||
|
client.s3.headObject(headParams, (headErr) => {
|
||||||
|
if (headErr) {
|
||||||
|
reject(new Error(`Object with key ${process.env.S3_BUCKET}/${objectKey} does not exist.`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const params = {
|
||||||
|
Bucket: `${process.env.S3_BUCKET}`,
|
||||||
|
CopySource: `${process.env.S3_BUCKET}/${objectKey}`,
|
||||||
|
Key: `${userId}/${newFilename}`,
|
||||||
|
ACL: 'public-read'
|
||||||
|
};
|
||||||
|
const move = client.moveObject(params);
|
||||||
|
move.on('err', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
move.on('end', (data) => {
|
||||||
|
resolve(`${s3Bucket}${userId}/${newFilename}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default function collectionForUserExists(username, collectionId, callback
|
||||||
}
|
}
|
||||||
|
|
||||||
function findUser() {
|
function findUser() {
|
||||||
return User.findOne({ username });
|
return User.findByUsername(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findCollection(owner) {
|
function findCollection(owner) {
|
||||||
|
|
|
@ -3,7 +3,8 @@ import User from '../../models/user';
|
||||||
|
|
||||||
async function getOwnerUserId(req) {
|
async function getOwnerUserId(req) {
|
||||||
if (req.params.username) {
|
if (req.params.username) {
|
||||||
const user = await User.findOne({ username: req.params.username });
|
const user =
|
||||||
|
await User.findByUsername(req.params.username);
|
||||||
if (user && user._id) {
|
if (user && user._id) {
|
||||||
return user._id;
|
return user._id;
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ export function updateProject(req, res) {
|
||||||
|
|
||||||
export function getProject(req, res) {
|
export function getProject(req, res) {
|
||||||
const { project_id: projectId, username } = req.params;
|
const { project_id: projectId, username } = req.params;
|
||||||
User.findOne({ username }, (err, user) => { // eslint-disable-line
|
User.findByUsername(username, (err, user) => { // eslint-disable-line
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).send({ message: 'Project with that username does not exist' });
|
return res.status(404).send({ message: 'Project with that username does not exist' });
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,7 @@ export function projectExists(projectId, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function projectForUserExists(username, projectId, callback) {
|
export function projectForUserExists(username, projectId, callback) {
|
||||||
User.findOne({ username }, (err, user) => {
|
User.findByUsername(username, (err, user) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
callback(false);
|
callback(false);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -7,7 +7,7 @@ const UserNotFoundError = createApplicationErrorClass('UserNotFoundError');
|
||||||
|
|
||||||
function getProjectsForUserName(username) {
|
function getProjectsForUserName(username) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
User.findOne({ username }, (err, user) => {
|
User.findByUsername(username, (err, user) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -30,71 +30,63 @@ const random = (done) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function findUserByUsername(username, cb) {
|
export function findUserByUsername(username, cb) {
|
||||||
User.findOne(
|
User.findByUsername(username, (err, user) => {
|
||||||
{ username },
|
cb(user);
|
||||||
(err, user) => {
|
});
|
||||||
cb(user);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createUser(req, res, next) {
|
export function createUser(req, res, next) {
|
||||||
|
const { username, email } = req.body;
|
||||||
|
const { password } = req.body;
|
||||||
|
const emailLowerCase = email.toLowerCase();
|
||||||
const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours
|
const EMAIL_VERIFY_TOKEN_EXPIRY_TIME = Date.now() + (3600000 * 24); // 24 hours
|
||||||
random((tokenError, token) => {
|
random((tokenError, token) => {
|
||||||
const user = new User({
|
const user = new User({
|
||||||
username: req.body.username,
|
username,
|
||||||
email: req.body.email,
|
email: emailLowerCase,
|
||||||
password: req.body.password,
|
password,
|
||||||
verified: User.EmailConfirmation.Sent,
|
verified: User.EmailConfirmation.Sent,
|
||||||
verifiedToken: token,
|
verifiedToken: token,
|
||||||
verifiedTokenExpires: EMAIL_VERIFY_TOKEN_EXPIRY_TIME,
|
verifiedTokenExpires: EMAIL_VERIFY_TOKEN_EXPIRY_TIME,
|
||||||
});
|
});
|
||||||
|
|
||||||
User.findOne(
|
User.findByEmailAndUsername(email, username, (err, existingUser) => {
|
||||||
{
|
if (err) {
|
||||||
$or: [
|
res.status(404).send({ error: err });
|
||||||
{ email: req.body.email },
|
return;
|
||||||
{ username: req.body.username }
|
}
|
||||||
]
|
|
||||||
},
|
|
||||||
(err, existingUser) => {
|
|
||||||
if (err) {
|
|
||||||
res.status(404).send({ error: err });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
const fieldInUse = existingUser.email === req.body.email ? 'Email' : 'Username';
|
const fieldInUse = existingUser.email.toLowerCase() === emailLowerCase ? 'Email' : 'Username';
|
||||||
res.status(422).send({ error: `${fieldInUse} is in use` });
|
res.status(422).send({ error: `${fieldInUse} is in use` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
user.save((saveErr) => {
|
||||||
|
if (saveErr) {
|
||||||
|
next(saveErr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
user.save((saveErr) => {
|
req.logIn(user, (loginErr) => {
|
||||||
if (saveErr) {
|
if (loginErr) {
|
||||||
next(saveErr);
|
next(loginErr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
req.logIn(user, (loginErr) => {
|
|
||||||
if (loginErr) {
|
|
||||||
next(loginErr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
const mailOptions = renderEmailConfirmation({
|
const mailOptions = renderEmailConfirmation({
|
||||||
body: {
|
body: {
|
||||||
domain: `${protocol}://${req.headers.host}`,
|
domain: `${protocol}://${req.headers.host}`,
|
||||||
link: `${protocol}://${req.headers.host}/verify?t=${token}`
|
link: `${protocol}://${req.headers.host}/verify?t=${token}`
|
||||||
},
|
},
|
||||||
to: req.user.email,
|
to: req.user.email,
|
||||||
});
|
});
|
||||||
|
|
||||||
mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars
|
mail.send(mailOptions, (mailErr, result) => { // eslint-disable-line no-unused-vars
|
||||||
res.json(userResponse(req.user));
|
res.json(userResponse(req.user));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +95,10 @@ export function duplicateUserCheck(req, res) {
|
||||||
const value = req.query[checkType];
|
const value = req.query[checkType];
|
||||||
const query = {};
|
const query = {};
|
||||||
query[checkType] = value;
|
query[checkType] = value;
|
||||||
User.findOne(query, (err, user) => {
|
// Don't want to use findByEmailOrUsername here, because in this case we do
|
||||||
|
// want to use case-insensitive search for usernames to prevent username
|
||||||
|
// duplicates, which overrides the default behavior.
|
||||||
|
User.findOne(query).collation({ locale: 'en', strength: 2 }).exec((err, user) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
return res.json({
|
return res.json({
|
||||||
exists: true,
|
exists: true,
|
||||||
|
@ -147,7 +142,7 @@ export function resetPasswordInitiate(req, res) {
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
random,
|
random,
|
||||||
(token, done) => {
|
(token, done) => {
|
||||||
User.findOne({ email: req.body.email }, (err, user) => {
|
User.findByEmail(req.body.email, (err, user) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
|
res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
|
||||||
return;
|
return;
|
||||||
|
@ -277,7 +272,7 @@ export function updatePassword(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function userExists(username, callback) {
|
export function userExists(username, callback) {
|
||||||
User.findOne({ username }, (err, user) => (
|
User.findByUsername(username, (err, user) => (
|
||||||
user ? callback(true) : callback(false)
|
user ? callback(true) : callback(false)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
237
server/migrations/emailConsolidation.js
Normal file
237
server/migrations/emailConsolidation.js
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
import fs from 'fs';
|
||||||
|
import User from '../models/user';
|
||||||
|
import Project from '../models/project';
|
||||||
|
import Collection from '../models/collection';
|
||||||
|
import { moveObjectToUserInS3, copyObjectInS3 } from '../controllers/aws.controller';
|
||||||
|
import mail from '../utils/mail';
|
||||||
|
import { renderAccountConsolidation } from '../views/mail';
|
||||||
|
|
||||||
|
|
||||||
|
const mongoConnectionString = process.env.MONGO_URL;
|
||||||
|
const { ObjectId } = mongoose.Types;
|
||||||
|
// Connect to MongoDB
|
||||||
|
mongoose.Promise = global.Promise;
|
||||||
|
mongoose.connect(mongoConnectionString, { useNewUrlParser: true, useUnifiedTopology: true });
|
||||||
|
mongoose.set('useCreateIndex', true);
|
||||||
|
mongoose.connection.on('error', () => {
|
||||||
|
console.error('MongoDB Connection Error. Please make sure that MongoDB is running.');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Requires the MongoDB Node.js Driver
|
||||||
|
* https://mongodb.github.io/node-mongodb-native
|
||||||
|
*/
|
||||||
|
|
||||||
|
const agg = [
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
email: {
|
||||||
|
$toLower: [
|
||||||
|
'$email'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
$group: {
|
||||||
|
_id: '$email',
|
||||||
|
total: {
|
||||||
|
$sum: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
$match: {
|
||||||
|
total: {
|
||||||
|
$gt: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
$sort: {
|
||||||
|
total: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
// steps to make this work
|
||||||
|
// iterate through the results
|
||||||
|
// check if any files are on AWS
|
||||||
|
// if so, move them to the right user bucket
|
||||||
|
// then, update the user to currentUser
|
||||||
|
// then, after updating all of the projects
|
||||||
|
// also update the collections
|
||||||
|
// delete other users
|
||||||
|
// update user email so it is all lowercase
|
||||||
|
// then, send the email
|
||||||
|
// then, figure out how to iterate through all of the users.
|
||||||
|
|
||||||
|
// create list of duplicate users
|
||||||
|
// User.aggregate(agg).then((result) => {
|
||||||
|
// return fs.writeFile('duplicates.json', JSON.stringify(result), () => {
|
||||||
|
// console.log('File written.');
|
||||||
|
// process.exit(0);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
let currentUser = null;
|
||||||
|
let duplicates = null;
|
||||||
|
|
||||||
|
fs.readFile('duplicates.json', async (err, file) => {
|
||||||
|
const result = JSON.parse(file);
|
||||||
|
for (let i = 3000; i < result.length; i += 1) {
|
||||||
|
console.log('Index: ', i);
|
||||||
|
const email = result[i]._id;
|
||||||
|
console.log(email);
|
||||||
|
await consolidateAccount(email); // eslint-disable-line
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function consolidateAccount(email) {
|
||||||
|
return User.find({ email }).collation({ locale: 'en', strength: 2 })
|
||||||
|
.sort({ createdAt: 1 }).exec()
|
||||||
|
.then((result) => {
|
||||||
|
[currentUser, ...duplicates] = result;
|
||||||
|
console.log('Current User: ', currentUser._id, ' ', currentUser.email);
|
||||||
|
duplicates = duplicates.map(dup => dup._id);
|
||||||
|
console.log('Duplicates: ', duplicates);
|
||||||
|
return Project.find({
|
||||||
|
user: { $in: duplicates }
|
||||||
|
}).exec();
|
||||||
|
}).then((sketches) => {
|
||||||
|
const saveSketchPromises = [];
|
||||||
|
sketches.forEach((sketch) => {
|
||||||
|
console.log('SketchId: ', sketch._id);
|
||||||
|
console.log('UserId: ', sketch.user);
|
||||||
|
const moveSketchFilesPromises = [];
|
||||||
|
sketch.files.forEach((file) => {
|
||||||
|
// if the file url contains sketch user
|
||||||
|
if (file.url && file.url.includes(process.env.S3_BUCKET_URL_BASE) && !file.url.includes(currentUser._id)) {
|
||||||
|
if (file.url.includes(sketch.user)) {
|
||||||
|
const fileSavePromise = moveObjectToUserInS3(file.url, currentUser._id)
|
||||||
|
.then((newUrl) => {
|
||||||
|
file.url = newUrl;
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log('Move Error:');
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
moveSketchFilesPromises.push(fileSavePromise);
|
||||||
|
} else {
|
||||||
|
const fileSavePromise = copyObjectInS3(file.url, currentUser._id)
|
||||||
|
.then((newUrl) => {
|
||||||
|
file.url = newUrl;
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log('Copy Error:');
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
moveSketchFilesPromises.push(fileSavePromise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sketchSavePromise = Promise.all(moveSketchFilesPromises).then(() => {
|
||||||
|
sketch.user = ObjectId(currentUser._id);
|
||||||
|
return sketch.save();
|
||||||
|
});
|
||||||
|
saveSketchPromises.push(sketchSavePromise);
|
||||||
|
});
|
||||||
|
return Promise.all(saveSketchPromises);
|
||||||
|
}).then(() => {
|
||||||
|
console.log('Moved and updated all sketches.');
|
||||||
|
return Collection.updateMany(
|
||||||
|
{ owner: { $in: duplicates } },
|
||||||
|
{ $set: { owner: ObjectId(currentUser.id) } }
|
||||||
|
);
|
||||||
|
}).then(() => {
|
||||||
|
console.log('Moved and updated all collections.');
|
||||||
|
return User.deleteMany({ _id: { $in: duplicates } });
|
||||||
|
}).then(() => {
|
||||||
|
console.log('Deleted other user accounts.');
|
||||||
|
currentUser.email = currentUser.email.toLowerCase();
|
||||||
|
return currentUser.save();
|
||||||
|
}).then(() => {
|
||||||
|
console.log('Migrated email to lowercase.');
|
||||||
|
// const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
|
||||||
|
const mailOptions = renderAccountConsolidation({
|
||||||
|
body: {
|
||||||
|
domain: 'https://editor.p5js.org',
|
||||||
|
username: currentUser.username,
|
||||||
|
email: currentUser.email
|
||||||
|
},
|
||||||
|
to: currentUser.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
mail.send(mailOptions, (mailErr, result) => {
|
||||||
|
console.log('Sent email.');
|
||||||
|
if (mailErr) {
|
||||||
|
return reject(mailErr);
|
||||||
|
}
|
||||||
|
return resolve(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// let duplicates = [
|
||||||
|
// "5ce3d936e0f9df0022d8330c",
|
||||||
|
// "5cff843f091745001e83c070",
|
||||||
|
// "5d246f5db489e6001eaee6e9"
|
||||||
|
// ];
|
||||||
|
// let currentUser = null;
|
||||||
|
// User.deleteMany({ _id: { $in: duplicates } }).then(() => {
|
||||||
|
// return User.findOne({ "email": "Silverstar09@hotmail.com" })
|
||||||
|
// }).then((result) => {
|
||||||
|
// currentUser = result;
|
||||||
|
// console.log('Deleted other user accounts.');
|
||||||
|
// currentUser.email = currentUser.email.toLowerCase();
|
||||||
|
// return currentUser.save();
|
||||||
|
// }).then(() => {
|
||||||
|
// const mailOptions = renderAccountConsolidation({
|
||||||
|
// body: {
|
||||||
|
// domain: 'https://editor.p5js.org',
|
||||||
|
// username: currentUser.username,
|
||||||
|
// email: currentUser.email
|
||||||
|
// },
|
||||||
|
// to: currentUser.email,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return new Promise((resolve, reject) => {
|
||||||
|
// mail.send(mailOptions, (mailErr, result) => {
|
||||||
|
// console.log('Sent email.');
|
||||||
|
// if (mailErr) {
|
||||||
|
// return reject(mailErr);
|
||||||
|
// }
|
||||||
|
// return resolve(result);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
// import s3 from '@auth0/s3';
|
||||||
|
|
||||||
|
// const client = s3.createClient({
|
||||||
|
// maxAsyncS3: 20,
|
||||||
|
// s3RetryCount: 3,
|
||||||
|
// s3RetryDelay: 1000,
|
||||||
|
// multipartUploadThreshold: 20971520, // this is the default (20 MB)
|
||||||
|
// multipartUploadSize: 15728640, // this is the default (15 MB)
|
||||||
|
// s3Options: {
|
||||||
|
// accessKeyId: `${process.env.AWS_ACCESS_KEY}`,
|
||||||
|
// secretAccessKey: `${process.env.AWS_SECRET_KEY}`,
|
||||||
|
// region: `${process.env.AWS_REGION}`
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const headParams = {
|
||||||
|
// Bucket: `${process.env.S3_BUCKET}`,
|
||||||
|
// Key: "5c9de807f6bccf0017da7927/8b9d95ae-7ddd-452a-b398-672392c4ac43.png"
|
||||||
|
// };
|
||||||
|
// client.s3.headObject(headParams, (err, data) => {
|
||||||
|
// console.log(err);
|
||||||
|
// console.log(data);
|
||||||
|
// });
|
|
@ -1,7 +1,8 @@
|
||||||
require('@babel/register');
|
require('@babel/register');
|
||||||
require('@babel/polyfill');
|
require('@babel/polyfill');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
require('dotenv').config({ path: path.resolve('.env') });
|
require('dotenv').config({ path: path.resolve('.env.production') });
|
||||||
require('./populateTotalSize');
|
require('./emailConsolidation');
|
||||||
|
// require('./populateTotalSize');
|
||||||
// require('./moveBucket');
|
// require('./moveBucket');
|
||||||
// require('./truncate');
|
// require('./truncate');
|
||||||
|
|
|
@ -141,17 +141,86 @@ userSchema.methods.findMatchingKey = function findMatchingKey(candidateKey, cb)
|
||||||
if (!foundOne) cb('Matching API key not found !', false, null);
|
if (!foundOne) cb('Matching API key not found !', false, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
userSchema.statics.findByMailOrName = function findByMailOrName(email) {
|
/**
|
||||||
|
*
|
||||||
|
* Queries User collection by email and returns one User document.
|
||||||
|
*
|
||||||
|
* @param {string|string[]} email - Email string or array of email strings
|
||||||
|
* @callback [cb] - Optional error-first callback that passes User document
|
||||||
|
* @return {Promise<Object>} - Returns Promise fulfilled by User document
|
||||||
|
*/
|
||||||
|
userSchema.statics.findByEmail = function findByEmail(email, cb) {
|
||||||
|
let query;
|
||||||
|
if (Array.isArray(email)) {
|
||||||
|
query = {
|
||||||
|
email: { $in: email }
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
query = {
|
||||||
|
email
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Email addresses should be case-insensitive unique
|
||||||
|
// In MongoDB, you must use collation in order to do a case-insensitive query
|
||||||
|
return this.findOne(query).collation({ locale: 'en', strength: 2 }).exec(cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Queries User collection by username and returns one User document.
|
||||||
|
*
|
||||||
|
* @param {string} username - Username string
|
||||||
|
* @callback [cb] - Optional error-first callback that passes User document
|
||||||
|
* @return {Promise<Object>} - Returns Promise fulfilled by User document
|
||||||
|
*/
|
||||||
|
userSchema.statics.findByUsername = function findByUsername(username, cb) {
|
||||||
const query = {
|
const query = {
|
||||||
$or: [{
|
username
|
||||||
email,
|
|
||||||
}, {
|
|
||||||
username: email,
|
|
||||||
}],
|
|
||||||
};
|
};
|
||||||
return this.findOne(query).exec();
|
return this.findOne(query, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Queries User collection using email or username with optional callback.
|
||||||
|
* This function will determine automatically whether the data passed is
|
||||||
|
* a username or email.
|
||||||
|
*
|
||||||
|
* @param {string} value - Email or username
|
||||||
|
* @callback [cb] - Optional error-first callback that passes User document
|
||||||
|
* @return {Promise<Object>} - Returns Promise fulfilled by User document
|
||||||
|
*/
|
||||||
|
userSchema.statics.findByEmailOrUsername = function findByEmailOrUsername(value, cb) {
|
||||||
|
const isEmail = value.indexOf('@') > -1;
|
||||||
|
if (isEmail) {
|
||||||
|
return this.findByEmail(value, cb);
|
||||||
|
}
|
||||||
|
return this.findByUsername(value, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Queries User collection, performing a MongoDB logical or with the email
|
||||||
|
* and username (i.e. if either one matches, will return the first document).
|
||||||
|
*
|
||||||
|
* @param {string} email
|
||||||
|
* @param {string} username
|
||||||
|
* @callback [cb] - Optional error-first callback that passes User document
|
||||||
|
* @return {Promise<Object>} - Returns Promise fulfilled by User document
|
||||||
|
*/
|
||||||
|
userSchema.statics.findByEmailAndUsername = function findByEmailAndUsername(email, username, cb) {
|
||||||
|
const query = {
|
||||||
|
$or: [
|
||||||
|
{ email },
|
||||||
|
{ username }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
return this.findOne(query).collation({ locale: 'en', strength: 2 }).exec(cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
userSchema.statics.EmailConfirmation = EmailConfirmationStates;
|
userSchema.statics.EmailConfirmation = EmailConfirmationStates;
|
||||||
|
|
||||||
|
userSchema.index({ username: 1 }, { collation: { locale: 'en', strength: 2 } });
|
||||||
|
userSchema.index({ email: 1 }, { collation: { locale: 'en', strength: 2 } });
|
||||||
|
|
||||||
export default mongoose.model('User', userSchema);
|
export default mongoose.model('User', userSchema);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import isAuthenticated from '../utils/isAuthenticated';
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
router.post('/S3/sign', isAuthenticated, AWSController.signS3);
|
router.post('/S3/sign', isAuthenticated, AWSController.signS3);
|
||||||
router.post('/S3/copy', isAuthenticated, AWSController.copyObjectInS3);
|
router.post('/S3/copy', isAuthenticated, AWSController.copyObjectInS3RequestHandler);
|
||||||
router.delete('/S3/:userId?/:objectKey', isAuthenticated, AWSController.deleteObjectFromS3);
|
router.delete('/S3/:userId?/:objectKey', isAuthenticated, AWSController.deleteObjectFromS3);
|
||||||
router.get('/S3/objects', AWSController.listObjectsInS3ForUserRequestHandler);
|
router.get('/S3/objects', AWSController.listObjectsInS3ForUserRequestHandler);
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,18 @@ app.use(corsMiddleware);
|
||||||
app.options('*', corsMiddleware);
|
app.options('*', corsMiddleware);
|
||||||
|
|
||||||
// Body parser, cookie parser, sessions, serve public assets
|
// Body parser, cookie parser, sessions, serve public assets
|
||||||
app.use('/locales', Express.static(path.resolve(__dirname, '../dist/static/locales'), { cacheControl: false }));
|
app.use(
|
||||||
|
'/locales',
|
||||||
|
Express.static(
|
||||||
|
path.resolve(__dirname, '../dist/static/locales'),
|
||||||
|
{
|
||||||
|
// Browsers must revalidate for changes to the locale files
|
||||||
|
// It doesn't actually mean "don't cache this file"
|
||||||
|
// See: https://jakearchibald.com/2016/caching-best-practices/
|
||||||
|
setHeaders: res => res.setHeader('Cache-Control', 'no-cache')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
app.use(Express.static(path.resolve(__dirname, '../dist/static'), {
|
app.use(Express.static(path.resolve(__dirname, '../dist/static'), {
|
||||||
maxAge: process.env.STATIC_MAX_AGE || (process.env.NODE_ENV === 'production' ? '1d' : '0')
|
maxAge: process.env.STATIC_MAX_AGE || (process.env.NODE_ENV === 'production' ? '1d' : '0')
|
||||||
}));
|
}));
|
||||||
|
|
78
server/views/consolidationMailLayout.js
Normal file
78
server/views/consolidationMailLayout.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
export default ({
|
||||||
|
domain,
|
||||||
|
headingText,
|
||||||
|
greetingText,
|
||||||
|
messageText,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
message2Text,
|
||||||
|
resetPasswordLink,
|
||||||
|
directLinkText,
|
||||||
|
resetPasswordText,
|
||||||
|
noteText,
|
||||||
|
meta
|
||||||
|
}) => (
|
||||||
|
`
|
||||||
|
<mjml>
|
||||||
|
<mj-head>
|
||||||
|
<mj-raw>
|
||||||
|
<meta name="keywords" content="${meta.keywords}" />
|
||||||
|
<meta name="description" content="${meta.description}" />
|
||||||
|
</mj-raw>
|
||||||
|
</mj-head>
|
||||||
|
<mj-body>
|
||||||
|
<mj-container>
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-image width="192" src="${domain}/images/p5js-square-logo.png" alt="p5.js" />
|
||||||
|
<mj-divider border-color="#ed225d"></mj-divider>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text font-size="20px" color="#333333" font-family="sans-serif">
|
||||||
|
${headingText}
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text color="#333333">
|
||||||
|
${greetingText}
|
||||||
|
</mj-text>
|
||||||
|
<mj-text color="#333333">
|
||||||
|
${messageText}
|
||||||
|
</mj-text>
|
||||||
|
<mj-text color="#333333">
|
||||||
|
<span style="font-weight:bold;">Username:</span> ${username}
|
||||||
|
</mj-text>
|
||||||
|
<mj-text color="#333333">
|
||||||
|
<span style="font-weight:bold;">Email:</span> ${email}
|
||||||
|
</mj-text>
|
||||||
|
<mj-text color="#333333">
|
||||||
|
${message2Text}
|
||||||
|
</mj-text>
|
||||||
|
<mj-button background-color="#ed225d" href="${domain}/${resetPasswordLink}">
|
||||||
|
${resetPasswordText}
|
||||||
|
</mj-button>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text color="#333333">
|
||||||
|
${directLinkText}
|
||||||
|
</mj-text>
|
||||||
|
<mj-text align="center" color="#333333"><a href="${domain}/${resetPasswordLink}">${domain}/${resetPasswordLink}</a></mj-text>
|
||||||
|
<mj-text color="#333333">
|
||||||
|
${noteText}
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
</mj-container>
|
||||||
|
</mj-body>
|
||||||
|
</mjml>
|
||||||
|
`
|
||||||
|
);
|
|
@ -1,5 +1,47 @@
|
||||||
import renderMjml from '../utils/renderMjml';
|
import renderMjml from '../utils/renderMjml';
|
||||||
import mailLayout from './mailLayout';
|
import mailLayout from './mailLayout';
|
||||||
|
import consolidationMailLayout from './consolidationMailLayout';
|
||||||
|
|
||||||
|
export const renderAccountConsolidation = (data) => {
|
||||||
|
const subject = 'p5.js Web Editor Account Consolidation';
|
||||||
|
const templateOptions = {
|
||||||
|
domain: data.body.domain,
|
||||||
|
headingText: 'Account Consolidation',
|
||||||
|
greetingText: 'Hello,',
|
||||||
|
messageText: `You're receiving this message because you previous registered for the
|
||||||
|
<a href="https://editor.p5js.org">p5.js Web Editor</a>
|
||||||
|
using the same email address multiple times. In order to fix bugs and prevent future bugs,
|
||||||
|
your accounts have been consolidated to the first account you created. You can login with
|
||||||
|
the following email and username:`,
|
||||||
|
username: data.body.username,
|
||||||
|
email: data.body.email,
|
||||||
|
message2Text: `All of your sketches and collections have been preserved and have not been modified.
|
||||||
|
If you have forgotten your password you can reset it:`,
|
||||||
|
resetPasswordLink: 'reset-password',
|
||||||
|
resetPasswordText: 'Reset Password',
|
||||||
|
directLinkText: 'Or copy and paste the following URL into your browser:',
|
||||||
|
noteText: `We are grateful for your patience and understanding. Thank you for supporting p5.js and the
|
||||||
|
p5.js Web Editor!`,
|
||||||
|
meta: {
|
||||||
|
keywords: 'p5.js, p5.js web editor, web editor, processing, code editor',
|
||||||
|
description: 'A web editor for p5.js, a JavaScript library with the goal'
|
||||||
|
+ ' of making coding accessible to artists, designers, educators, and beginners.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return MJML string
|
||||||
|
const template = consolidationMailLayout(templateOptions);
|
||||||
|
|
||||||
|
// Render MJML to HTML string
|
||||||
|
const html = renderMjml(template);
|
||||||
|
|
||||||
|
// Return options to send mail
|
||||||
|
return Object.assign(
|
||||||
|
{},
|
||||||
|
data,
|
||||||
|
{ html, subject },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const renderResetPassword = (data) => {
|
export const renderResetPassword = (data) => {
|
||||||
const subject = 'p5.js Web Editor Password Reset';
|
const subject = 'p5.js Web Editor Password Reset';
|
||||||
|
|
Loading…
Reference in a new issue