chore: Re-structure workspaces, enforce build order
These are some changes in the way the monorepo is structured, that are aimed at making it easier to get started. There are two important changes: **Multiple workspaces** We had a yarn workspaces setup at `packages/*`. But our monorepo has grown to 92 packages which can be overwhelming for people not familiar with the package names. To remedy this, I've split it into 4 different workspaces: - `designs/*`: Holds FreeSewing designs (think patterns) - `plugins/*`: Holds FreeSewing plugins - `packages/*`: Holds other packages published on NPM - `sites/*`: Holds software that is not published as an NPM package, such as our various websites and backend API This should make it easier to find things, and to answer questions like *where do I find the code for the plugins*. **Updated reconfigure script to handle build order** One problem when bootstrapping the repo is inter-dependencies between packages. For example, building a pattern will only work once `plugin-bundle` is built. Which will only work once all plugins in the bundle or built. And that will only work when `core` is built, and so on. This can be frustrating for new users as `yarn buildall` will fail. And it gets overlooked by seasoned devs because they're likely to have every package built in their repo so this issue doesn't concern them. To remedy this, we now have a `config/build-order.mjs` file and the updated `/scripts/reconfigure.mjs` script will enforce the build order so that things *just work*.
This commit is contained in:
parent
895f993a70
commit
e4035b2509
1581 changed files with 2118 additions and 1868 deletions
215
sites/backend/src/utils/email/index.js
Normal file
215
sites/backend/src/utils/email/index.js
Normal file
|
@ -0,0 +1,215 @@
|
|||
import config from '../../config'
|
||||
import { strings as i18n } from '@freesewing/i18n'
|
||||
import templates from '../../templates'
|
||||
import { createUrl } from '../'
|
||||
import sendEmailWith from './relays'
|
||||
|
||||
const deliver = sendEmailWith(config.sendEmailWith)
|
||||
const email = {}
|
||||
|
||||
const loadTemplate = (type, format, language='en') => {
|
||||
let template = templates.header[format] + templates[type][format] + templates.footer[format]
|
||||
let toTranslate = templates[type].i18n.concat(templates.footer.i18n)
|
||||
let from = []
|
||||
let to = []
|
||||
for (let key of toTranslate) {
|
||||
from.push(`__${key}__`)
|
||||
to.push(i18n[language]['email.' + key] || key)
|
||||
}
|
||||
for (let i = 0; i < from.length; i++) template = template.replace(from[i], to[i])
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
const replace = (text, from, to) => {
|
||||
for (const id in from) text = text.split(from[id]).join(to[id] || from[id])
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
email.signup = (recipient, language, id) => {
|
||||
let html = loadTemplate('signup', 'html', language)
|
||||
let text = loadTemplate('signup', 'text', language)
|
||||
let from = ['__signupActionLink__', '__headerOpeningLine__', '__hiddenIntro__', '__footerWhy__']
|
||||
let link = createUrl(language, `/confirm/signup/${id}`)
|
||||
let to = [
|
||||
link,
|
||||
i18n[language]['email.signupHeaderOpeningLine'],
|
||||
i18n[language]['email.signupHiddenIntro'],
|
||||
i18n[language]['email.signupWhy']
|
||||
]
|
||||
html = replace(html, from, to)
|
||||
text = replace(text, from, to)
|
||||
let options = {
|
||||
from: `"${i18n[language]['email.joostFromFreesewing']}" <info@freesewing.org>`,
|
||||
to: recipient,
|
||||
subject: i18n[language]['email.signupSubject'],
|
||||
headers: {
|
||||
'X-Freesewing-Confirmation-ID': '' + id
|
||||
},
|
||||
text,
|
||||
html
|
||||
}
|
||||
deliver(options, (error, info) => {
|
||||
if (error) return console.log(error)
|
||||
console.log('Message sent', info)
|
||||
})
|
||||
}
|
||||
|
||||
email.emailchange = (newAddress, currentAddress, language, id) => {
|
||||
let html = loadTemplate('emailchange', 'html', language)
|
||||
let text = loadTemplate('emailchange', 'text', language)
|
||||
let from = [
|
||||
'__emailchangeActionLink__',
|
||||
'__emailchangeActionText__',
|
||||
'__emailchangeTitle__',
|
||||
'__emailchangeCopy1__',
|
||||
'__headerOpeningLine__',
|
||||
'__hiddenIntro__',
|
||||
'__footerWhy__',
|
||||
'__questionsJustReply__',
|
||||
'__signature__'
|
||||
]
|
||||
let to = [
|
||||
createUrl(language, `/confirm/email/${id}`),
|
||||
i18n[language]['email.emailchangeActionText'],
|
||||
i18n[language]['email.emailchangeTitle'],
|
||||
i18n[language]['email.emailchangeCopy1'],
|
||||
i18n[language]['email.emailchangeHeaderOpeningLine'],
|
||||
i18n[language]['email.emailchangeHiddenIntro'],
|
||||
i18n[language]['email.emailchangeWhy'],
|
||||
i18n[language]['email.questionsJustReply'],
|
||||
i18n[language]['email.signature'],
|
||||
]
|
||||
html = replace(html, from, to)
|
||||
text = replace(text, from, to)
|
||||
|
||||
let options = {
|
||||
from: `"${i18n[language]['email.joostFromFreesewing']}" <info@freesewing.org>`,
|
||||
to: newAddress,
|
||||
cc: currentAddress,
|
||||
subject: i18n[language]['email.emailchangeSubject'],
|
||||
headers: {
|
||||
'X-Freesewing-Confirmation-ID': '' + id
|
||||
},
|
||||
text,
|
||||
html
|
||||
}
|
||||
deliver(options, (error, info) => {
|
||||
if (error) return console.log(error)
|
||||
console.log('Message sent', info)
|
||||
})
|
||||
}
|
||||
|
||||
email.passwordreset = (recipient, language, id) => {
|
||||
let html = loadTemplate('passwordreset', 'html', language)
|
||||
let text = loadTemplate('passwordreset', 'text', language)
|
||||
let from = [
|
||||
'__passwordresetActionLink__',
|
||||
'__headerOpeningLine__',
|
||||
'__hiddenIntro__',
|
||||
'__footerWhy__'
|
||||
]
|
||||
let to = [
|
||||
createUrl(language, `/confirm/reset/${id}`),
|
||||
i18n[language]['email.passwordresetHeaderOpeningLine'],
|
||||
i18n[language]['email.passwordresetHiddenIntro'],
|
||||
i18n[language]['email.passwordresetWhy']
|
||||
]
|
||||
html = replace(html, from, to)
|
||||
text = replace(text, from, to)
|
||||
|
||||
let options = {
|
||||
from: `"${i18n[language]['email.joostFromFreesewing']}" <info@freesewing.org>`,
|
||||
to: recipient,
|
||||
subject: i18n[language]['email.passwordresetSubject'],
|
||||
headers: {
|
||||
'X-Freesewing-Confirmation-ID': '' + id
|
||||
},
|
||||
text,
|
||||
html
|
||||
}
|
||||
deliver(options, (error, info) => {
|
||||
if (error) return console.log(error)
|
||||
console.log('Message sent', info)
|
||||
})
|
||||
}
|
||||
|
||||
email.goodbye = async (recipient, language) => {
|
||||
let html = loadTemplate('goodbye', 'html', language)
|
||||
let text = loadTemplate('goodbye', 'text', language)
|
||||
let from = ['__headerOpeningLine__', '__hiddenIntro__', '__footerWhy__']
|
||||
let to = [
|
||||
i18n[language]['email.goodbyeHeaderOpeningLine'],
|
||||
i18n[language]['email.goodbyeHiddenIntro'],
|
||||
i18n[language]['email.goodbyeWhy']
|
||||
]
|
||||
html = replace(html, from, to)
|
||||
text = replace(text, from, to)
|
||||
|
||||
let options = {
|
||||
from: `"${i18n[language]['email.joostFromFreesewing']}" <info@freesewing.org>`,
|
||||
to: recipient,
|
||||
subject: i18n[language]['email.goodbyeSubject'],
|
||||
text,
|
||||
html
|
||||
}
|
||||
deliver(options, (error, info) => {
|
||||
if (error) return console.log(error)
|
||||
console.log('Message sent', info)
|
||||
})
|
||||
}
|
||||
|
||||
email.subscribe = async (recipient, token) => {
|
||||
let html = loadTemplate('newsletterSubscribe', 'html', 'en')
|
||||
let text = loadTemplate('newsletterSubscribe', 'text', 'en')
|
||||
let from = ['__hiddenIntro__', '__headerOpeningLine__', '__newsletterConfirmationLink__', '__footerWhy__']
|
||||
let to = [
|
||||
'Confirm your subscription to the FreeSewing newsletter',
|
||||
'Please confirm it was you who requested this',
|
||||
`https://backend.freesewing.org/newsletter/confirm/${token}`,
|
||||
`You received this email because somebody tried to subscribe ${recipient} to the FreeSewing newsletter`
|
||||
]
|
||||
html = replace(html, from, to)
|
||||
text = replace(text, from, to)
|
||||
|
||||
let options = {
|
||||
from: `"FreeSewing" <newsletter@freesewing.org>`,
|
||||
to: recipient,
|
||||
subject: 'Confirm your subscription to the FreeSewing newsletter',
|
||||
text,
|
||||
html
|
||||
}
|
||||
deliver(options, (error, info) => {
|
||||
if (error) return console.log(error)
|
||||
console.log('Message sent', info)
|
||||
})
|
||||
}
|
||||
|
||||
email.newsletterWelcome = async (recipient, ehash) => {
|
||||
let html = loadTemplate('newsletterWelcome', 'html', 'en')
|
||||
let text = loadTemplate('newsletterWelcome', 'text', 'en')
|
||||
let from = ['__hiddenIntro__', '__headerOpeningLine__', '__newsletterUnsubscribeLink__', '__footerWhy__']
|
||||
let to = [
|
||||
'No action required; This is just an FYI',
|
||||
"You're in. Now what?",
|
||||
`https://backend.freesewing.org/newsletter/unsubscribe/${ehash}`,
|
||||
`You received this email because you subscribed to the FreeSewing newsletter`
|
||||
]
|
||||
html = replace(html, from, to)
|
||||
text = replace(text, from, to)
|
||||
|
||||
let options = {
|
||||
from: `"FreeSewing" <newsletter@freesewing.org>`,
|
||||
to: recipient,
|
||||
subject: 'Welcome to the FreeSewing newsletter',
|
||||
text,
|
||||
html
|
||||
}
|
||||
deliver(options, (error, info) => {
|
||||
if (error) return console.log(error)
|
||||
console.log('Message sent', info)
|
||||
})
|
||||
}
|
||||
|
||||
export default email
|
12
sites/backend/src/utils/email/relays.js
Normal file
12
sites/backend/src/utils/email/relays.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import smtp from './smtp'
|
||||
//import sendgrid from './sendgrid'
|
||||
|
||||
const sendEmailWith = type => {
|
||||
const types = {
|
||||
smtp
|
||||
//sendgrid,
|
||||
}
|
||||
return types[type]
|
||||
}
|
||||
|
||||
export default sendEmailWith
|
19
sites/backend/src/utils/email/sendgrid.js
Normal file
19
sites/backend/src/utils/email/sendgrid.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import sendgrid from '@sendgrid/mail'
|
||||
import config from '../../config'
|
||||
|
||||
sendgrid.setApiKey(config.sendgrid)
|
||||
|
||||
const deliver = (data, callback) => {
|
||||
sendgrid.send(data).then(result => {
|
||||
// FIXME: This is obviously nonsense
|
||||
if (result[0].statusCode === 202)
|
||||
callback(false, {
|
||||
from: data.from,
|
||||
to: data.to,
|
||||
subject: data.subject
|
||||
})
|
||||
else callback(true, 'Sending via SendGridfailed')
|
||||
})
|
||||
}
|
||||
|
||||
export default deliver
|
18
sites/backend/src/utils/email/smtp.js
Normal file
18
sites/backend/src/utils/email/smtp.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import nodemailer from 'nodemailer'
|
||||
import config from '../../config'
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: config.smtp.host,
|
||||
port: config.smtp.port,
|
||||
secure: false, // Only needed or SSL, not for TLS
|
||||
auth: {
|
||||
user: config.smtp.user,
|
||||
pass: config.smtp.pass
|
||||
}
|
||||
})
|
||||
|
||||
const deliver = (data, callback) => {
|
||||
transporter.sendMail(data, callback)
|
||||
}
|
||||
|
||||
export default deliver
|
140
sites/backend/src/utils/index.js
Normal file
140
sites/backend/src/utils/index.js
Normal file
|
@ -0,0 +1,140 @@
|
|||
import { User, Person, Pattern } from '../models'
|
||||
import crypto from 'crypto'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import mailer from './email'
|
||||
import logger from './log'
|
||||
import config from '../config'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import sharp from 'sharp'
|
||||
import avatar from '../templates/avatar'
|
||||
|
||||
export const email = mailer
|
||||
export const log = logger
|
||||
|
||||
export const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1)
|
||||
|
||||
export const createUrl = (language, path) => {
|
||||
// Handle development mode
|
||||
if (config.api.indexOf('localhost') !== -1) return 'http://localhost:8000' + path
|
||||
else return config.website.scheme + '://' + language + '.' + config.website.domain + path
|
||||
}
|
||||
|
||||
export const getHash = email => {
|
||||
let hash = crypto.createHash('sha256')
|
||||
hash.update(clean(email))
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
export const clean = email => email.toLowerCase().trim()
|
||||
|
||||
export const getToken = account => {
|
||||
return jwt.sign(
|
||||
{
|
||||
_id: account._id,
|
||||
handle: account.handle,
|
||||
aud: config.jwt.audience,
|
||||
iss: config.jwt.issuer
|
||||
},
|
||||
config.jwt.secretOrKey
|
||||
)
|
||||
}
|
||||
|
||||
export const getHandle = type => {
|
||||
let go, handle, exists
|
||||
if (type === 'person') go = Person
|
||||
else if (type === 'pattern') go = Pattern
|
||||
else go = User
|
||||
do {
|
||||
exists = false
|
||||
handle = createHandle()
|
||||
go.findOne({ handle: handle }, (err, result) => {
|
||||
if (result !== null) exists = true
|
||||
})
|
||||
} while (exists !== false)
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
export const createHandle = (length = 5) => {
|
||||
let handle = ''
|
||||
let possible = 'abcdefghijklmnopqrstuvwxyz'
|
||||
for (let i = 0; i < length; i++)
|
||||
handle += possible.charAt(Math.floor(Math.random() * possible.length))
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
export const imageType = contentType => {
|
||||
if (contentType === 'image/png') return 'png'
|
||||
if (contentType === 'image/jpeg') return 'jpg'
|
||||
if (contentType === 'image/gif') return 'gif'
|
||||
if (contentType === 'image/bmp') return 'bmp'
|
||||
if (contentType === 'image/webp') return 'webp'
|
||||
}
|
||||
|
||||
export const saveAvatarFromBase64 = (data, handle, type) => {
|
||||
fs.mkdir(userStoragePath(handle), { recursive: true }, err => {
|
||||
if (err) log.error('mkdirFailed', err)
|
||||
let imgBuffer = Buffer.from(data, 'base64')
|
||||
for (let size of Object.keys(config.avatar.sizes)) {
|
||||
sharp(imgBuffer)
|
||||
.resize(config.avatar.sizes[size], config.avatar.sizes[size])
|
||||
.toFile(avatarPath(size, handle, type), (err, info) => {
|
||||
if (err) log.error('avatarNotSaved', err)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const avatarPath = (size, handle, ext, type = 'user') => {
|
||||
let dir = userStoragePath(handle)
|
||||
if (size === 'l') return path.join(dir, handle + '.' + ext)
|
||||
else return path.join(dir, size + '-' + handle + '.' + ext)
|
||||
}
|
||||
|
||||
export const randomColor = () => (0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6)
|
||||
|
||||
export const randomAvatar = () =>
|
||||
avatar.replace('000000', randomColor()).replace('FFFFFF', randomColor())
|
||||
|
||||
export const ehash = email => {
|
||||
let hash = crypto.createHash('sha256')
|
||||
hash.update(clean(email))
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
export const newHandle = (length = 5) => {
|
||||
let handle = ''
|
||||
let possible = 'abcdefghijklmnopqrstuvwxyz'
|
||||
for (let i = 0; i < length; i++)
|
||||
handle += possible.charAt(Math.floor(Math.random() * possible.length))
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
export const uniqueHandle = () => {
|
||||
let handle, exists
|
||||
do {
|
||||
exists = false
|
||||
handle = newHandle()
|
||||
User.findOne({ handle: handle }, (err, user) => {
|
||||
if (user !== null) exists = true
|
||||
})
|
||||
} while (exists !== false)
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
export const userStoragePath = handle =>
|
||||
path.join(config.storage, 'users', handle.substring(0, 1), handle)
|
||||
|
||||
export const createAvatar = handle => {
|
||||
let dir = userStoragePath(handle)
|
||||
fs.mkdir(dir, { recursive: true }, err => {
|
||||
if (err) console.log('mkdirFailed', dir, err)
|
||||
fs.writeFile(path.join(dir, handle) + '.svg', randomAvatar(), err => {
|
||||
if (err) console.log('writeFileFailed', dir, err)
|
||||
})
|
||||
})
|
||||
}
|
45
sites/backend/src/utils/log/index.js
Normal file
45
sites/backend/src/utils/log/index.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import dateFormat from 'dateformat'
|
||||
|
||||
// FIXME: This needs work
|
||||
|
||||
const now = () => dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss')
|
||||
|
||||
const logWorthy = (msg, data) => {
|
||||
let d = { at: now() }
|
||||
switch (msg) {
|
||||
case 'ping':
|
||||
case 'login':
|
||||
case 'wrongPassword':
|
||||
case 'passwordSet':
|
||||
case 'dataExport':
|
||||
d.user = data.user.handle
|
||||
d.from = data.req.ip
|
||||
d.with = data.req.headers['user-agent']
|
||||
break
|
||||
case 'signupRequest':
|
||||
d.email = data.email
|
||||
d.confirmation = data.confirmation
|
||||
break
|
||||
case 'accountRemovalFailed':
|
||||
d.err = data.err
|
||||
d.user = data.user.handle
|
||||
d.from = data.req.ip
|
||||
d.with = data.req.headers['user-agent']
|
||||
break
|
||||
default:
|
||||
d.data = data
|
||||
break
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
const log = (type, msg, data) => {
|
||||
console.log(type, msg, logWorthy(msg, data))
|
||||
}
|
||||
|
||||
log.info = (msg, data) => log('info', msg, data)
|
||||
log.warning = (msg, data) => log('warning', msg, data)
|
||||
log.error = (msg, data) => log('error', msg, data)
|
||||
|
||||
export default log
|
Loading…
Add table
Add a link
Reference in a new issue