1
0
Fork 0

wip(backend): Refactoring

This commit is contained in:
joostdecock 2022-11-03 19:56:06 +01:00
parent 18d8d5c590
commit b45a1c61b4
27 changed files with 369 additions and 1654 deletions

View file

@ -19,11 +19,11 @@ const config = {
db: {
url: process.env.API_DB_URL,
},
tests: {
domain: process.env.TESTDOMAIN || 'mailtrap.freesewing.dev',
},
static: process.env.API_STATIC,
storage: process.env.API_STORAGE,
hashing: {
saltRounds: 10,
},
encryption: {
key: process.env.API_ENC_KEY,
},
@ -37,6 +37,11 @@ const config = {
aws: {
ses: {
region: 'us-east-1',
from: 'FreeSewing <info@freesewing.org>',
replyTo: ['FreeSewing <info@freesewing.org>'],
feedback: 'bounce@freesewing.org',
cc: [],
bcc: ['records@freesewing.org'],
},
},
sanity: {

View file

@ -1,177 +0,0 @@
import { User, Person, Pattern, Newsletter } from '../models'
import jwt from 'jsonwebtoken'
import config from '../config'
import { ehash } from '../utils'
function AdminController() {}
AdminController.prototype.search = function (req, res) {
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, admin) => {
if (err || admin === null) return res.sendStatus(400)
if (admin.role !== 'admin') return res.sendStatus(403)
User.find({
$or: [
{ handle: { $regex: `.*${req.body.query}.*` } },
{ username: { $regex: `.*${req.body.query}.*` } },
{ ehash: ehash(req.body.query) },
],
})
.sort('username')
.exec((err, users) => {
if (err) return res.sendStatus(400)
Person.find({ handle: { $regex: `.*${req.body.query}.*` } })
.sort('handle')
.exec((err, people) => {
if (err) return res.sendStatus(400)
if (users === null && people === null) return res.sendStatus(404)
return res.send({
users: users.map((user) => user.adminProfile()),
people: people.map((person) => person.info()),
})
})
})
})
}
AdminController.prototype.setPatronStatus = function (req, res) {
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, admin) => {
if (err || admin === null) return res.sendStatus(400)
if (admin.role !== 'admin') return res.sendStatus(403)
User.findOne({ handle: req.body.handle }, (err, user) => {
if (err) return res.sendStatus(400)
if (user === null) return res.sendStatus(404)
user.patron = req.body.patron
return saveAndReturnAccount(res, user)
})
})
}
AdminController.prototype.setRole = function (req, res) {
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, admin) => {
if (err || admin === null) return res.sendStatus(400)
if (admin.role !== 'admin') return res.sendStatus(403)
User.findOne({ handle: req.body.handle }, (err, user) => {
if (err) return res.sendStatus(400)
if (user === null) return res.sendStatus(404)
user.role = req.body.role
return saveAndReturnAccount(res, user)
})
})
}
AdminController.prototype.unfreeze = function (req, res) {
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, admin) => {
if (err || admin === null) return res.sendStatus(400)
if (admin.role !== 'admin') return res.sendStatus(403)
User.findOne({ handle: req.body.handle }, (err, user) => {
if (err) return res.sendStatus(400)
if (user === null) return res.sendStatus(404)
user.status = 'active'
return saveAndReturnAccount(res, user)
})
})
}
AdminController.prototype.impersonate = function (req, res) {
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, admin) => {
if (err || admin === null) return res.sendStatus(400)
if (admin.role !== 'admin') return res.sendStatus(403)
User.findOne({ handle: req.body.handle }, (err, user) => {
if (err) return res.sendStatus(400)
if (user === null) return res.sendStatus(404)
let account = user.account()
let token = getToken(account)
let people = {}
Person.find({ user: user.handle }, (err, personList) => {
if (err) return res.sendStatus(400)
for (let person of personList) people[person.handle] = person.info()
let patterns = {}
Pattern.find({ user: user.handle }, (err, patternList) => {
if (err) return res.sendStatus(400)
for (let pattern of patternList) patterns[pattern.handle] = pattern
return user.updateLoginTime(() => res.send({ account, people, patterns, token }))
})
})
})
})
}
AdminController.prototype.patronList = function (req, res) {
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, admin) => {
if (err || admin === null) return res.sendStatus(400)
if (admin.role !== 'admin') return res.sendStatus(403)
User.find({ patron: { $gt: 0 } }, (err, patronList) => {
if (err) return res.sendStatus(500)
return res.send(patronList)
})
})
}
AdminController.prototype.subscriberList = function (req, res) {
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, admin) => {
if (err || admin === null) return res.sendStatus(400)
if (admin.role !== 'admin') return res.sendStatus(403)
User.find({ newsletter: true }, (err, subscribedUsers) => {
if (err) return res.sendStatus(500)
let subscribers = subscribedUsers.map((user) => ({
ehash: user.ehash,
email: user.email,
}))
Newsletter.find({}, (err, subs) => {
if (err) return res.sendStatus(500)
return res.send(subscribers.concat(subs))
})
})
})
}
AdminController.prototype.stats = function (req, res) {
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, admin) => {
if (err || admin === null) return res.sendStatus(400)
if (admin.role !== 'admin') return res.sendStatus(403)
User.find({ 'consent.profile': true }, (err, users) => {
if (err) return res.sendStatus(500)
Person.find({}, (err, people) => {
if (err) return res.sendStatus(500)
Pattern.find({}, (err, patterns) => {
return res.send({
users: users.length,
people: people.length,
patterns: patterns.length,
})
})
})
})
})
}
function saveAndReturnAccount(res, user) {
user.save(function (err, updatedUser) {
if (err) {
return res.sendStatus(500)
} else return res.send({ account: updatedUser.account() })
})
}
const getToken = (account) => {
return jwt.sign(
{
_id: account._id,
handle: account.handle,
role: account.role,
aud: config.jwt.audience,
iss: config.jwt.issuer,
},
config.jwt.secretOrKey
)
}
export default AdminController

View file

@ -1,225 +0,0 @@
import { User, Person, Pattern, Confirmation } from '../models'
import {
createUrl,
getHash,
getToken,
getHandle,
createHandle,
imageType,
saveAvatarFromBase64,
} from '../utils'
import config from '../config'
import queryString from 'query-string'
import axios from 'axios'
/** This is essentially part of the user controller, but
* it seemed best to keep all this authentication stuff
* somewhat apart
*/
function AuthController() {}
AuthController.prototype.initOauth = function (req, res) {
if (!req.body) return res.sendStatus(400)
let confirmation = new Confirmation({
type: 'oauth',
data: {
language: req.body.language,
provider: req.body.provider,
},
})
confirmation.save(function (err) {
if (err) return res.sendStatus(500)
return res.send({ state: confirmation._id })
})
}
AuthController.prototype.loginOauth = function (req, res) {
if (!req.body) return res.sendStatus(400)
Confirmation.findById(req.body.confirmation, (err, confirmation) => {
if (err) return res.sendStatus(400)
if (confirmation === null) return res.sendStatus(401)
if (String(confirmation.data.validation) !== String(req.body.validation))
return res.sendStatus(401)
let signup = confirmation.data.signup
User.findOne({ handle: confirmation.data.handle }, (err, user) => {
if (err) return res.sendStatus(400)
if (user === null) return res.sendStatus(401)
if (user.status !== 'active') return res.sendStatus(403)
let account = user.account()
let token = getToken(account)
let people = {}
Person.find({ user: user.handle }, (err, personList) => {
if (err) return res.sendStatus(400)
for (let person of personList) people[person.handle] = person
let patterns = {}
Pattern.find({ user: user.handle }, (err, patternList) => {
if (err) return res.sendStatus(400)
for (let pattern of patternList) patterns[pattern.handle] = pattern
confirmation.remove((err) => {
if (err !== null) return res.sendStatus(500)
user.updateLoginTime(() => res.send({ account, people, token, signup }))
})
})
})
})
})
}
AuthController.prototype.providerCallback = function (req, res) {
let language, token, email, avatarUri, username
let provider = req.params.provider
let conf = config.oauth[provider]
let signup = false
// Verify state
Confirmation.findById(req.query.state, (err, confirmation) => {
if (err) return res.sendStatus(400)
if (confirmation === null) return res.sendStatus(401)
if (String(confirmation._id) !== String(req.query.state)) return res.sendStatus(401)
if (confirmation.data.provider !== provider) return res.sendStatus(401)
language = confirmation.data.language
// Get access token
const go = axios.create({ baseURL: '', timeout: 5000 })
go.post(conf.tokenUri, {
client_id: conf.clientId,
client_secret: conf.clientSecret,
code: req.query.code,
accept: 'json',
grant_type: 'authorization_code',
redirect_uri: config.api + '/oauth/callback/from/' + provider,
})
.then((result) => {
if (result.status !== 200) return res.sendStatus(401)
if (provider === 'github') token = queryString.parse(result.data).access_token
else if (provider === 'google') token = result.data.access_token
// Contact API for user info
const headers = (token) => ({ headers: { Authorization: 'Bearer ' + token } })
go.get(conf.dataUri, headers(token))
.then(async (result) => {
if (provider === 'github') {
;(email = await getGithubEmail(result.data.email, go, conf.emailUri, headers(token))),
(avatarUri = result.data.avatar_url)
username = result.data.login
} else if (provider === 'google') {
for (let address of result.data.emailAddresses) {
if (address.metadata.primary === true) email = address.value
}
for (let photo of result.data.photos) {
if (photo.metadata.primary === true) avatarUri = photo.url
}
for (let name of result.data.names) {
if (name.metadata.primary === true) username = name.displayName
}
}
User.findOne({ ehash: getHash(email) }, (err, user) => {
if (err) return res.sendStatus(400)
if (user === null) {
// New user: signup
signup = true
let handle = getHandle()
go.get(avatarUri, { responseType: 'arraybuffer' }).then((avatar) => {
let type = imageType(avatar.headers['content-type'])
saveAvatarFromBase64(
new Buffer(avatar.data, 'binary').toString('base64'),
handle,
type
)
let userData = {
picture: handle + '.' + type,
email: email,
initial: email,
ehash: getHash(email),
handle,
username: username,
settings: { language: language },
social: {
github: '',
twitter: '',
instagram: '',
},
time: {
created: new Date(),
login: new Date(),
},
}
if (provider === 'github') {
userData.social.github = result.data.login
userData.bio = result.data.bio
}
let user = new User(userData)
user.save(function (err) {
if (err) return res.sendStatus(500)
let validation = createHandle(20)
confirmation.data.handle = user.handle
confirmation.data.validation = validation
confirmation.data.signup = true
confirmation.save(function (err) {
if (err) return res.sendStatus(500)
return res.redirect(
createUrl(
language,
signup
? '/confirm/signup/' + req.query.state + '/'
: '/login/callback/' + confirmation._id + '/' + validation
)
)
})
})
})
} else {
// Existing user
if (provider === 'github') {
if (user.bio === '') user.bio = result.data.bio
user.social.github = result.data.login
}
user.save(function (err) {
let validation = createHandle(20)
confirmation.data.handle = user.handle
confirmation.data.validation = validation
confirmation.data.signup = false
confirmation.save(function (err) {
if (err) return res.sendStatus(500)
return res.redirect(
// Watch out for pending users
createUrl(
language,
user.status === 'pending'
? '/confirm/signup/' + req.query.state + '/'
: '/login/callback/' + confirmation._id + '/' + validation
)
)
})
})
}
})
})
.catch((err) => {
console.log('api token error', err)
res.sendStatus(401)
})
})
.catch((err) => {
console.log('post token error', err)
res.sendStatus(401)
})
})
}
/*
* Github does not always return the email address
* See https://github.com/freesewing/backend/issues/162
*/
const getGithubEmail = async (email, client, uri, headers) => {
if (email === null) {
return client.get(uri, headers).then((result) => {
for (let e of result.data) {
if (e.primary) return e.email
}
})
} else return email
}
export default AuthController

View file

@ -1,103 +0,0 @@
import axios from 'axios'
import config from '../config'
function GithubController() {}
// Create a gist
GithubController.prototype.createGist = function (req, res) {
if (!req.body.data) return res.sendStatus(400)
let client = GithubClient()
client
.post('/gists', {
public: true,
description: `An open source sewing pattern from freesewing.org`,
files: {
'pattern.yaml': { content: req.body.data },
},
})
.then((result) => {
let id = result.data.id
client
.post(`/gists/${id}/comments`, {
body: `👉 https://freesewing.org/recreate/gist/${id} 👀`,
})
.then((result) => res.send({ id }))
.catch((err) => res.sendStatus(500))
})
.catch((err) => res.sendStatus(500))
}
GithubController.prototype.createIssue = function (req, res) {
if (!req.body.data) return res.sendStatus(400)
if (!req.body.design) return res.sendStatus(400)
let client = GithubClient()
client
.post('/gists', {
public: true,
description: `A FreeSewing crash report`,
files: {
'pattern.yaml': { content: req.body.data },
'settings.yaml': { content: req.body.patternProps.settings },
'events.yaml': { content: req.body.patternProps.events },
'errors.md': { content: req.body.traces },
'parts.json': { content: req.body.patternProps.parts },
},
})
.then((gist) => {
client
.post('/repos/freesewing/freesewing/issues', {
title: `Error while drafting ${req.body.design}`,
body: `An error occured while drafting ${req.body.design} and a [crash report](https://gist.github.com/${gist.data.id}) was generated.`,
labels: [`:package: ${req.body.design}`, ':robot: robot'],
})
.then((issue) => {
let notify =
typeof config.github.notify.specific[req.body.design] === 'undefined'
? config.github.notify.dflt
: config.github.notify.specific[req.body.design]
let id = issue.data.number
let path = `/recreate/gist/${gist.data.id}`
let body = 'Ping '
for (const user of notify) body += `@${user} `
if (req.body.userGithub) body += `@${req.body.userGithub} `
body += ' 👋 \nRecreate this:\n\n'
body +=
`- **Lab**: 👉 https://lab.freesewing.dev/v/next/` +
`${req.body.design}?from=github&preload=${gist.data.id}`
body += '\n\n'
body += `- **Production**: 👉 https://freesewing.org${path}`
body += '\n\n'
if (req.body.userHandle) body += `(user handle: ${req.body.userHandle})`
client
.post(`/repos/freesewing/freesewing/issues/${id}/comments`, { body })
.then((result) => res.send({ id }))
.catch((err) => {
console.log(err)
res.sendStatus(500)
})
})
.catch((err) => {
console.log(err)
res.sendStatus(500)
})
})
.catch((err) => {
console.log(err)
res.sendStatus(500)
})
}
const GithubClient = () =>
axios.create({
baseURL: config.github.api,
timeout: 5000,
auth: {
username: config.github.bot.user,
password: config.github.token,
},
headers: {
Accept: 'application/vnd.github.v3+json',
},
})
export default GithubController

View file

@ -1,93 +0,0 @@
import { Newsletter, Confirmation, User } from '../models'
import { log, email, ehash } from '../utils'
import path from 'path'
const bail = (res, page = 'index') =>
res.sendFile(path.resolve(__dirname, '..', 'landing', `${page}.html`))
function NewsletterController() {}
NewsletterController.prototype.subscribe = function (req, res, subscribe = true) {
if (!req.body || !req.body.email) return res.sendStatus(400)
let confirmation = new Confirmation({
type: 'newsletter',
data: { email: req.body.email },
})
confirmation.save(function (err) {
if (err) return res.sendStatus(500)
log.info('newsletterSubscription', {
email: req.body.email,
confirmation: confirmation._id,
})
email.subscribe(req.body.email, confirmation._id)
return res.send({ status: 'subscribed' })
})
}
NewsletterController.prototype.confirm = function (req, res, subscribe = true) {
if (!req.params.token) return bail(res, 'invalid')
Confirmation.findById(req.params.token, (err, confirmation) => {
if (err) return bail(res)
if (confirmation === null) return bail(res)
Newsletter.findOne(
{
ehash: ehash(confirmation.data.email),
},
(err, reader) => {
if (err) return bail(res)
// Already exists?
if (reader !== null) return bail(res, 'already-subscribed')
let hash = ehash(confirmation.data.email)
let sub = new Newsletter({
email: confirmation.data.email,
ehash: hash,
time: {
created: new Date(),
},
})
sub.save(function (err) {
if (err) {
log.error('newsletterSubscriptionFailed', sub)
console.log(err)
return res.sendStatus(500)
} else {
console.log(`Subscribed ${reader.email} to the newsletter`)
email.newsletterWelcome(confirmation.data.email, hash)
return bail(res, 'subscribe')
}
})
}
)
})
}
NewsletterController.prototype.unsubscribe = function (req, res) {
if (!req.params.ehash) return bail(res, 'invalid')
Newsletter.findOne({ ehash: req.params.ehash }, (err, reader) => {
if (reader) {
Newsletter.deleteOne({ id: reader.id }, (err, result) => {
if (!err) {
console.log(`Unsubscribed ${reader.email} from the newsletter`)
return bail(res, 'unsubscribe')
} else return bail(res, 'oops')
})
} else {
User.findOne({ ehash: req.params.ehash }, (err, user) => {
if (user) {
user.newsletter = false
user.save(function (err, updatedUser) {
if (err) {
log.error('accountUpdateFailed', err)
return res.sendStatus(500)
} else return bail(res, 'unsubscribe')
})
} else return bail(res, 'oops')
})
}
})
}
export default NewsletterController

View file

@ -1,225 +0,0 @@
import config from '../config'
import { capitalize } from '../utils'
import sharp from 'sharp'
import fs from 'fs'
import path from 'path'
import axios from 'axios'
import remark from 'remark'
import remarkParse from 'remark-parse'
import remarkFrontmatter from 'remark-frontmatter'
import toString from 'mdast-util-to-string'
import { Buffer } from 'buffer'
import yaml from 'yaml'
// Sites for which we generate images
const sites = ['dev', 'org']
// Langauges for which we generate images
const languages = ['en', 'fr', 'de', 'es', 'nl']
// Load template once at startup
const template = fs.readFileSync(path.resolve(...config.og.template), 'utf-8')
/* Helper method to extract intro from strapi markdown */
const introFromStrapiMarkdown = async (md, slug) => {
const tree = await remark().use(remarkParse).parse(md)
if (tree.children[0].type !== 'paragraph')
console.log('Markdown does not start with paragraph', slug)
return toString(tree.children[0])
}
/* Helper method to extract title from markdown frontmatter */
const titleAndIntroFromLocalMarkdown = async (md, slug) => {
const tree = await remark().use(remarkParse).use(remarkFrontmatter, ['yaml']).parse(md)
if (tree.children[0].type !== 'yaml')
console.log('Markdown does not start with frontmatter', slug)
else
return {
title: titleAsLines(yaml.parse(tree.children[0].value).title),
intro: introAsLines(toString(tree.children.slice(1, 2))),
}
return false
}
/* Helper method to load dev blog post */
const loadDevBlogPost = async (slug) => {
const result = await axios.get(
`${config.strapi}/blogposts?_locale=en&dev_eq=true&slug_eq=${slug}`
)
if (result.data)
return {
title: titleAsLines(result.data[0].title),
intro: introAsLines(await introFromStrapiMarkdown(result.data[0].body, slug)),
sub: [
result.data[0].author.displayname,
new Date(result.data[0].published_at).toString().split(' ').slice(0, 4).join(' '),
],
lead: 'Developer Blog',
}
return false
}
/* Helper method to load markdown file from disk */
const loadMarkdownFile = async (page, site, lang) =>
fs.promises
.readFile(path.resolve('..', '..', 'markdown', site, ...page.split('/'), `${lang}.md`), 'utf-8')
.then(async (md) =>
md
? {
...(await titleAndIntroFromLocalMarkdown(md, page)),
sub: ['freesewing.dev/', page],
lead: capitalize(page.split('/').shift()),
}
: false
)
/* Find longest possible place to split a string */
const splitLine = (line, chars) => {
const words = line.split(' ')
if (words[0].length > chars) {
// Force a word break
return [line.slice(0, chars - 1) + '-', line.slice(chars - 1)]
}
// Glue chunks together until it's too long
let firstLine = ''
let max = false
for (const word of words) {
if (!max && `${firstLine}${word}`.length <= chars) firstLine += `${word} `
else max = true
}
return [firstLine, words.join(' ').slice(firstLine.length)]
}
/* Divide title into lines to fit on image */
const titleAsLines = (title) => {
// Does it fit on one line?
if (title.length <= config.og.chars.title_1) return [title]
// Does it fit on two lines?
let lines = splitLine(title, config.og.chars.title_1)
if (lines[1].length <= config.og.chars.title_2) return lines
// Three lines it is
return [lines[0], ...splitLine(lines[1], config.og.chars.title_2)]
}
/* Divive intro into lines to fit on image */
const introAsLines = (intro) => {
// Does it fit on one line?
if (intro.length <= config.og.chars.intro) return [intro]
// Two lines it is
return splitLine(intro, config.og.chars.intro)
}
// Get title and intro
const getMetaData = {
dev: async (page) => {
const chunks = page.split('/')
// Home page
if (chunks.length === 1 && chunks[0] === '')
return {
title: ['FreeSewing.dev'],
intro: introAsLines(
'FreeSewing API documentation and tutorials for developers and contributors'
),
sub: ['Also featuring', ' our developers blog'],
lead: '.dev',
}
// Blog index page
if (chunks.length === 1 && chunks[0] === 'blog')
return {
title: titleAsLines('FreeSewing Developer Blog'),
intro: introAsLines(
'Contains no sewing news whatsover. Only posts for (aspiring) developers :)'
),
sub: ['freesewing.dev', '/blog'],
lead: 'Developer Blog',
}
// Blog post
if (chunks.length === 2 && chunks[0] === 'blog') {
return await loadDevBlogPost(chunks[1])
}
// Other (MDX) page
const md = await loadMarkdownFile(page, 'dev', 'en')
// Return markdown info or default generic data
return md
? md
: {
title: titleAsLines('FreeSewing.dev'),
intro: introAsLines(
'Documentation, guides, and howtos for contributors and developers alike'
),
sub: ['https://freesewing.dev/', '&lt;== Check it out'],
lead: 'freesewing.dev',
}
},
org: async (page, site, lang) => ({}),
}
/* Hide unused placeholders */
const hidePlaceholders = (list) => {
let svg = template
for (const i of list) {
svg = svg.replace(`${i}title_1`, '').replace(`${i}title_2`, '').replace(`${i}title_3`, '')
}
return svg
}
/* Place text in SVG template */
const decorateSvg = (data) => {
let svg
// Single title line
if (data.title.length === 1) {
svg = hidePlaceholders([2, 3]).replace(`1title_1`, data.title[0])
}
// Double title line
else if (data.title.length === 2) {
svg = hidePlaceholders([1, 3])
.replace(`2title_1`, data.title[0])
.replace(`2title_2`, data.title[1])
}
// Triple title line
else if (data.title.length === 3) {
svg = hidePlaceholders([1, 2])
.replace(`3title_1`, data.title[0])
.replace(`3title_2`, data.title[1])
.replace(`3title_3`, data.title[2])
}
return svg
.replace('sub_1', data.sub[0] || '')
.replace('sub_2', data.sub[1] || '')
.replace(`intro_1`, data.intro[0] || '')
.replace(`intro_2`, data.intro[1] || '')
.replace('lead_1', data.lead || '')
}
/* This generates open graph images */
function OgController() {}
OgController.prototype.image = async function (req, res) {
// Extract path parameters
const { lang = 'en', site = 'dev' } = req.params
const page = req.params['0']
if (sites.indexOf(site) === -1) return res.send({ error: 'sorry' })
if (languages.indexOf(lang) === -1) return res.send({ error: 'sorry' })
// Load meta data
const data = await getMetaData[site](page, site, lang)
// Inject into SVG
const svg = decorateSvg(data)
// Turn into PNG
sharp(Buffer.from(svg, 'utf-8'))
.resize({ width: 1200 })
.toBuffer((err, data, info) => {
if (err) console.log(err)
return res.type('png').send(data)
})
}
export default OgController

View file

@ -1,127 +0,0 @@
import { User, Pattern } from '../models'
import { log } from '../utils'
function PatternController() {}
// CRUD basics
PatternController.prototype.create = (req, res) => {
if (!req.body) return res.sendStatus(400)
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, user) => {
if (err || user === null) return res.sendStatus(403)
let handle = uniqueHandle()
let pattern = new Pattern({
handle,
user: user.handle,
person: req.body.person,
name: req.body.name,
notes: req.body.notes,
data: req.body.data,
created: new Date(),
})
pattern.save(function (err) {
if (err) {
log.error('patternCreationFailed', user)
console.log(err)
return res.sendStatus(500)
}
log.info('patternCreated', { handle: pattern.handle })
return res.send(pattern.anonymize())
})
})
}
PatternController.prototype.read = (req, res) => {
Pattern.findOne({ handle: req.params.handle }, (err, pattern) => {
if (err) return res.sendStatus(400)
if (pattern === null) return res.sendStatus(404)
return res.send(pattern.anonymize())
})
}
PatternController.prototype.update = (req, res) => {
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, async (err, user) => {
if (err || user === null) return res.sendStatus(400)
Pattern.findOne({ handle: req.params.handle }, (err, pattern) => {
if (err || pattern === null) return res.sendStatus(400)
if (typeof req.body.name === 'string') pattern.name = req.body.name
if (typeof req.body.notes === 'string') pattern.notes = req.body.notes
return saveAndReturnPattern(res, pattern)
})
})
}
PatternController.prototype.delete = (req, res) => {
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, async (err, user) => {
if (err || user === null) return res.sendStatus(400)
Pattern.deleteOne({ handle: req.params.handle, user: user.handle }, (err) => {
if (err) return res.sendStatus(400)
else return res.sendStatus(204)
})
})
}
// Delete multiple
PatternController.prototype.deleteMultiple = function (req, res) {
if (!req.body) return res.sendStatus(400)
if (!req.body.patterns) return res.sendStatus(400)
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, user) => {
if (err || user === null) return res.sendStatus(400)
let patterns = req.body.patterns
if (patterns.length < 1) return res.sendStatus(400)
let handles = []
for (let handle of patterns) handles.push({ handle })
Pattern.deleteMany(
{
user: user.handle,
$or: handles,
},
(err) => {
if (err) return res.sendStatus(500)
const patterns = {}
Patterns.find({ user: user.handle }, (err, patternList) => {
if (err) return res.sendStatus(400)
for (let pattern of patternList) patterns[pattern.handle] = pattern
res.send({ patterns })
})
}
)
})
}
function saveAndReturnPattern(res, pattern) {
pattern.save(function (err, updatedPattern) {
if (err) {
log.error('patternUpdateFailed', updatedPattern)
return res.sendStatus(500)
}
return res.send(updatedPattern.info())
})
}
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
}
const uniqueHandle = () => {
let handle, exists
do {
exists = false
handle = newHandle()
Pattern.findOne({ handle: handle }, (err, pattern) => {
if (pattern !== null) exists = true
})
} while (exists !== false)
return handle
}
export default PatternController

View file

@ -1,115 +0,0 @@
import { User, Person } from '../models'
import { log } from '../utils'
function PersonController() {}
// CRUD basics
PersonController.prototype.create = function (req, res) {
if (!req.body) return res.sendStatus(400)
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, user) => {
if (err || user === null) return res.sendStatus(400)
let handle = uniqueHandle()
let person = new Person({
handle,
user: user.handle,
name: req.body.name,
units: req.body.units,
breasts: req.body.breasts,
picture: handle + '.svg',
created: new Date(),
})
person.createAvatar()
person.save(function (err) {
if (err) return res.sendStatus(400)
log.info('personCreated', { handle: handle })
return res.send({ person: person.info() })
})
})
}
PersonController.prototype.read = function (req, res) {
if (!req.body) return res.sendStatus(400)
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, (err, user) => {
if (err || user === null) return res.sendStatus(400)
Person.findOne({ handle: req.params.handle }, (err, person) => {
if (err) return res.sendStatus(400)
if (person === null) return res.sendStatus(404)
return res.send({ person: person.info() })
})
})
}
PersonController.prototype.update = (req, res) => {
var async = 0
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, async (err, user) => {
if (err || user === null) return res.sendStatus(400)
Person.findOne({ handle: req.params.handle }, (err, person) => {
if (err || person === null) return res.sendStatus(400)
let data = req.body
if (typeof data.name === 'string') person.name = data.name
if (typeof data.notes === 'string') person.notes = data.notes
if (typeof data.units === 'string') person.units = data.units
if (typeof data.breasts === 'string') person.breasts = data.breasts === 'true' ? true : false
if (typeof data.measurements !== 'undefined')
person.measurements = {
...person.measurements,
...data.measurements,
}
if (typeof data.picture !== 'undefined') person.saveAvatar(data.picture)
return saveAndReturnPerson(res, person)
})
})
}
PersonController.prototype.delete = (req, res) => {
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, async (err, user) => {
if (err || user === null) return res.sendStatus(400)
Person.deleteOne({ handle: req.params.handle, user: user.handle }, (err) => {
if (err) return res.sendStatus(400)
else return res.sendStatus(204)
})
})
}
// Clone
PersonController.prototype.clone = function (req, res) {}
function saveAndReturnPerson(res, person) {
person.save(function (err, updatedPerson) {
if (err) {
log.error('personUpdateFailed', updatedPerson)
return res.sendStatus(500)
}
return res.send({ person: updatedPerson.info() })
})
}
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
}
const uniqueHandle = () => {
let handle, exists
do {
exists = false
handle = newHandle()
Person.findOne({ handle: handle }, (err, person) => {
if (person !== null) exists = true
})
} while (exists !== false)
return handle
}
export default PersonController

View file

@ -1,129 +0,0 @@
import axios from 'axios'
import config from '../config'
import asBuffer from 'data-uri-to-buffer'
import FormData from 'form-data'
import fs from 'fs'
const getToken = async () => {
let result
try {
result = await axios.post(
`${config.strapi.protocol}://${config.strapi.host}:${config.strapi.port}/auth/local`,
{
identifier: config.strapi.username,
password: config.strapi.password,
}
)
} catch (err) {
console.log('ERROR: Failed to load strapi token')
return false
}
return result.data.jwt
}
const withToken = (token) => ({
headers: {
Authorization: `Bearer ${token}`,
},
})
const ext = (type) => {
switch (type.toLowerCase()) {
case 'image/jpg':
case 'image/jpeg':
return 'jpg'
break
case 'image/png':
return 'png'
break
case 'image/webp':
return 'webp'
break
default:
return false
}
}
const api = (path) =>
`${config.strapi.protocol}://${config.strapi.host}:${config.strapi.port}${path}`
// Uploads a picture to Strapi
const uploadPicture = async (img, name, token) => {
const form = new FormData()
const buff = asBuffer(img)
const extention = ext(buff.type)
if (!extention) return [false, { error: `Filetype ${buff.type} is not supported` }]
// I hate you strapi, because this hack is the only way I can get your shitty upload to work
const filename = `${config.strapi.tmp}/viaBackend.${extention}`
const file = fs.createReadStream(filename)
form.append('files', file)
form.append(
'fileInfo',
JSON.stringify({
alternativeText: `The picture/avatar for maker ${name}`,
caption: `Maker: ${name}`,
})
)
let result
try {
result = await axios.post(api('/upload'), form, {
headers: {
...form.getHeaders(),
Authorization: `Bearer ${token}`,
},
})
} catch (err) {
console.log('ERROR: Failed to upload picture')
return [false, { error: 'Upload failed' }]
}
return [true, result.data]
}
const validRequest = (body) =>
body &&
body.displayname &&
body.about &&
body.picture &&
typeof body.displayname === 'string' &&
typeof body.about === 'string' &&
typeof body.picture === 'string'
// Creates a maker or author in Strapi
const createPerson = async (type, data, token) => {
let result
try {
result = await axios.post(api(`/${type}s`), data, withToken(token))
} catch (err) {
console.log('ERROR: Failed to create', type)
return [false, { error: 'Creation failed' }]
}
return [true, result.data]
}
function StrapiController() {}
StrapiController.prototype.addPerson = async function (req, res, type) {
if (!validRequest(req.body)) return res.sendStatus(400)
const token = await getToken()
const [upload, picture] = await uploadPicture(req.body.picture, req.body.displayname, token)
if (!upload) return res.status(400).send(picture)
const [create, person] = await createPerson(
type,
{
picture: picture[0].id,
displayname: req.body.displayname,
about: req.body.about,
},
token
)
if (!create) return res.status(400).send(person)
return res.send(person)
}
export default StrapiController

View file

@ -10,6 +10,8 @@ import { clean, asJson } from '../utils/index.mjs'
import { getUserAvatar } from '../utils/sanity.mjs'
import { log } from '../utils/log.mjs'
import { emailTemplate } from '../utils/email.mjs'
import set from 'lodash.set'
import { UserModel } from '../models/user.mjs'
/*
* Prisma is not an ORM and we can't attach methods to the model
@ -47,6 +49,17 @@ const getToken = (user, config) =>
{ expiresIn: config.jwt.expiresIn }
)
const isUsernameAvailable = async (username, prisma) => {
const user = await prisme.user.findUnique({
where: {
lusername: username.toLowerCase(),
},
})
if (user === null) return true
return false
}
// We'll send this result unless it goes ok
const result = 'error'
@ -59,99 +72,10 @@ export function UserController() {}
* See: https://freesewing.dev/reference/backend/api
*/
UserController.prototype.signup = async (req, res, tools) => {
if (Object.keys(req.body) < 1) return res.status(400).json({ error: 'postBodyMissing', result })
if (!req.body.email) return res.status(400).json({ error: 'emailMissing', result })
if (!req.body.password) return res.status(400).json({ error: 'passwordMissing', result })
if (!req.body.language) return res.status(400).json({ error: 'languageMissing', result })
const User = new UserModel(tools)
await User.create(req.body)
// Destructure what we need from tools
const { prisma, config, encrypt, email } = tools
// Requests looks ok - does the user exist?
const emailhash = hash(clean(req.body.email))
if (await prisma.user.findUnique({ where: { ehash: emailhash } })) {
return res.status(400).json({ error: 'emailExists', result })
}
// It does not. Creating user entry
let record
try {
const username = clean(randomString()) // Temporary username,
record = await prisma.user.create({
data: {
data: asJson({ settings: { language: req.body.language } }),
ehash: emailhash,
email: encrypt(clean(req.body.email)),
ihash: emailhash,
initial: encrypt(clean(req.body.email)),
password: asJson(hashPassword(req.body.password)),
username,
lusername: username.toLowerCase(),
},
})
} catch (err) {
log.warn(err, 'Could not create user record')
return res.status(500).send({ error: 'createAccountFailed', result })
}
// Now set username to user-ID
let updated
try {
updated = await prisma.user.update({
where: { id: record.id },
data: {
username: `user-${record.id}`,
lusername: `user-${record.id}`,
},
})
} catch (err) {
log.warn(err, 'Could not update username on created user record')
return res.status(500).send({ result: 'error', error: 'updateCreatedAccountUsernameFailed' })
}
log.info({ user: updated.id }, 'Account created')
// Create confirmation
let confirmation
try {
confirmation = await prisma.confirmation.create({
data: {
type: 'signup',
data: encrypt({
language: req.body.language,
email: req.body.email,
id: record.id,
ehash: emailhash,
}),
},
})
} catch (err) {
log.warn(err, 'Unable to create confirmation at signup')
return res.status(500).send({ result: 'error', error: 'updateCreatedAccountUsernameFailed' })
}
// Send out signup email
let sent
try {
sent = await email.send(
req.body.to,
...emailTemplate.signup(req.body.email, req.body.language, confirmation.id)
)
} catch (err) {
log.warn(err, 'Unable to send email')
return res.status(500).send({ error: 'failedToSendSignupEmail', result })
}
if (req.body.unittest && req.body.email.split('@').pop() === 'mailtrap.freesewing.dev') {
// Unit test, return confirmation code in response
return res.status(201).send({
result: 'success',
status: 'created',
email: req.body.email,
confirmation: confirmation.id,
})
}
return result
? res.status(201).send({ result: 'success', status: 'created', email: req.body.email })
: res.status(500).send({ error: 'unableToSendSignupEmail', result })
return User.sendResponse(res)
}
/*
@ -359,64 +283,105 @@ UserController.prototype.update = async (req, res, tools) => {
}
// Account loaded - Handle various updates
const data = req.body
if (typeof data.avatar !== 'undefined') {
// Catch people submitting without uploading an avatar
if (data.avatar) user.saveAvatar(data.avatar)
return saveAndReturnAccount(res, user)
const data = {}
// Username
if (req.body.username) {
if (!isUsernameAvailable(req.body.username, prisma)) {
return res.status(400).send({ error: 'usernameTaken', result })
}
/*
var async = 0
if (!req.user._id) return res.sendStatus(400)
User.findById(req.user._id, async (err, user) => {
if (err || user === null) {
return res.sendStatus(400)
data.username = req.body.username
data.lusername = data.username.toLowerCase()
}
let data = req.body
// Newsletter
if (req.body.newsletter === false) data.newsletter = false
if (req.body.newsletter === true) data.newsletter = true
// Consent
if (typeof req.body.consent !== 'undefined') data.consent = req.body.consent
// Bio
if (typeof req.body.bio === 'string') userData.bio = req.body.bio
// Password
if (typeof req.body.password === 'string')
userData.password = asJson(hashPassword(req.body.password))
// Data
const userData = JSON.parse(account.data)
const uhash = hash(account.data)
if (typeof req.body.language === 'string') set(userData, 'settings.language', req.body.language)
if (typeof req.body.units === 'string') set(userData, 'settings.units', req.body.units)
if (typeof req.body.github === 'string') set(userData, 'settings.social.github', req.body.github)
if (typeof req.body.twitter === 'string')
set(userData, 'settings.social.twitter', req.body.twitter)
if (typeof req.body.instagram === 'string')
set(userData, 'settings.social.instagram', req.body.instagram)
// Did data change?
if (uhash !== hash(userData)) data.data = JSON.stringify(userData)
if (typeof data.settings !== 'undefined') {
user.settings = {
...user.settings,
...data.settings,
}
return saveAndReturnAccount(res, user)
} else if (data.newsletter === true || data.newsletter === false) {
user.newsletter = data.newsletter
if (data.newsletter === true) email.newsletterWelcome(user.email, user.ehash)
return saveAndReturnAccount(res, user)
} else if (typeof data.bio === 'string') {
user.bio = data.bio
return saveAndReturnAccount(res, user)
} else if (typeof data.social === 'object') {
if (typeof data.social.github === 'string') user.social.github = data.social.github
if (typeof data.social.twitter === 'string') user.social.twitter = data.social.twitter
if (typeof data.social.instagram === 'string') user.social.instagram = data.social.instagram
return saveAndReturnAccount(res, user)
} else if (typeof data.consent === 'object') {
user.consent = {
...user.consent,
...data.consent,
}
return saveAndReturnAccount(res, user)
} else if (typeof data.avatar !== 'undefined') {
// Catch people submitting without uploading an avatar
if (data.avatar) user.saveAvatar(data.avatar)
return saveAndReturnAccount(res, user)
} else if (typeof data.password === 'string') {
user.password = data.password
return saveAndReturnAccount(res, user)
} else if (typeof data.username === 'string') {
User.findOne({ username: data.username }, (err, userExists) => {
if (userExists !== null && data.username !== user.username)
return res.status(400).send('usernameTaken')
else {
user.username = data.username
return saveAndReturnAccount(res, user)
}
// Commit
prisma.user.update({
where: { id: account.id },
data,
})
}
// Email change requires confirmation
if (typeof req.body.email === 'string') {
const currentEmail = decrypt(account.email)
if (req.body.email !== currentEmail) {
if (req.body.confirmation) {
// Find confirmation
let confirmation
try {
prisma.confirmation.findUnique({
where: { id: req.body.confirmation },
})
} catch (err) {
log.warn(err, `Failed to find confirmation for email change`)
return res.status(500).send({ error: 'failedToFindEmailChangeConfirmation', result })
}
if (!confirmation) {
log.warn(err, `Missing confirmation for email change`)
return res.status(400).send({ error: 'missingEmailChangeConfirmation', result })
}
} else {
// Create confirmation
let confirmation
try {
confirmation = prisma.confirmation.create({
data: {
type: 'emailchange',
data: encrypt({
language: userData.settings.language || 'en',
email: {
new: req.body.email,
current: currentEmail,
},
}),
},
})
} catch (err) {
log.warn(err, `Failed to create confirmation for email change`)
return res.status(500).send({ error: 'failedToCreateEmailChangeConfirmation', result })
}
// Send out confirmation email
let sent
try {
sent = await email.send(
req.body.email,
currentEmail,
...emailTemplate.emailchange(
req.body.email,
currentEmail,
userData.settings.language,
confirmation.id
)
)
} catch (err) {
log.warn(err, 'Unable to send email')
return res.status(500).send({ error: 'failedToSendEmailChangeConfirmationEmail', result })
}
}
}
}
// Now handle the
/*
else if (typeof data.email === 'string' && data.email !== user.email) {
if (typeof data.confirmation === 'string') {
Confirmation.findById(req.body.confirmation, (err, confirmation) => {

View file

@ -1,46 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>You are already subscribed</title>
<link
href="https://fonts.googleapis.com/css?family=Raleway:300,500&display=swap"
rel="stylesheet"
/>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
color: #f8f9fa;
line-height: 1.25;
font-size: 24px;
background: #212529;
}
h1 {
font-family: 'Raleway', sans-serif;
font-size: 3rem;
font-weight: 700;
margin: 0 0 2rem;
}
img {
width: 166px;
position: absolute;
bottom: 20px;
left: calc(50% - 83px);
}
div.msg {
text-align: left;
max-width: 36ch;
margin: 6rem auto;
}
</style>
</head>
<body>
<div class="msg">
<h1>Love the enthusiasm</h1>
<p>But you were already subscribed</p>
</div>
<img src="https://freesewing.org/avatar.svg" />
</body>
</html>

View file

@ -1,52 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Oops</title>
<link
href="https://fonts.googleapis.com/css?family=Raleway:300,500&display=swap"
rel="stylesheet"
/>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
color: #f8f9fa;
line-height: 1.25;
font-size: 24px;
background: #212529;
}
h1 {
font-family: 'Raleway', sans-serif;
font-size: 3rem;
font-weight: 700;
margin: 0 0 2rem;
}
img {
width: 166px;
position: absolute;
bottom: 20px;
left: calc(50% - 83px);
}
div.msg {
text-align: left;
max-width: 36ch;
margin: 6rem auto;
}
a,
a:visited,
a:active {
color: #d0bfff !important;
text-decoration: none;
}
</style>
</head>
<body>
<div class="msg">
<h1>Oops</h1>
<p>That did not go as planned</p>
</div>
<img src="https://freesewing.org/avatar.svg" />
</body>
</html>

View file

@ -1,46 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>You are subscribed</title>
<link
href="https://fonts.googleapis.com/css?family=Raleway:300,500&display=swap"
rel="stylesheet"
/>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
color: #f8f9fa;
line-height: 1.25;
font-size: 24px;
background: #212529;
}
h1 {
font-family: 'Raleway', sans-serif;
font-size: 3rem;
font-weight: 700;
margin: 0 0 2rem;
}
img {
width: 166px;
position: absolute;
bottom: 20px;
left: calc(50% - 83px);
}
div.msg {
text-align: left;
max-width: 36ch;
margin: 6rem auto;
}
</style>
</head>
<body>
<div class="msg">
<h1>Done</h1>
<p>You are now subscribed to the FreeSewing newsletter</p>
</div>
<img src="https://freesewing.org/avatar.svg" />
</body>
</html>

View file

@ -1,46 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>You are unsubscribed</title>
<link
href="https://fonts.googleapis.com/css?family=Raleway:300,500&display=swap"
rel="stylesheet"
/>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
color: #f8f9fa;
line-height: 1.25;
font-size: 24px;
background: #212529;
}
h1 {
font-family: 'Raleway', sans-serif;
font-size: 3rem;
font-weight: 700;
margin: 0 0 2rem;
}
img {
width: 166px;
position: absolute;
bottom: 20px;
left: calc(50% - 83px);
}
div.msg {
text-align: left;
max-width: 36ch;
margin: 6rem auto;
}
</style>
</head>
<body>
<div class="msg">
<h1>Gone</h1>
<p>You are no longer subscribed to the FreeSewing newsletter</p>
</div>
<img src="https://freesewing.org/avatar.svg" />
</body>
</html>

View file

@ -0,0 +1,69 @@
import { log } from '../utils/log.mjs'
import { hash } from '../utils/crypto.mjs'
export function ConfirmationModel(tools) {
this.config = tools.config
this.prisma = tools.prisma
return this
}
ConfirmationModel.prototype.load = async function (where) {
this.record = await this.prisma.confirmation.findUnique({ where })
return this.setExists()
}
ConfirmationModel.prototype.setExists = function () {
this.exists = this.record ? true : false
return this
}
ConfirmationModel.prototype.setResponse = function (status = 200, error = false) {
this.response = {
status,
body: {
result: 'success',
},
}
if (status > 201) {
this.response.body.error = error
this.response.body.result = 'error'
this.error = true
} else this.error = false
return this.setExists()
}
ConfirmationModel.prototype.setResponse = function (
status = 200,
result = 'success',
error = false
) {
this.response = {
status: this.status,
body: {
error: this.error,
result: this.result,
},
}
if (error) {
this.response.body.error = error
this.error = true
} else this.error = false
return this.setExists()
}
ConfirmationModel.prototype.create = async function (data = {}) {
try {
this.record = await this.prisma.confirmation.create({ data })
} catch (err) {
log.warn(err, 'Could not create confirmation record')
return this.setResponse(500, 'createConfirmationFailed')
}
log.info({ confirmation: this.record.id }, 'Confirmation created')
return this.setResponse(201)
}

View file

@ -0,0 +1,149 @@
//import jwt from 'jsonwebtoken'
//import axios from 'axios'
//import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs'
//import { clean, asJson } from '../utils/index.mjs'
//import { getUserAvatar } from '../utils/sanity.mjs'
import { log } from '../utils/log.mjs'
import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs'
import { clean, asJson } from '../utils/index.mjs'
import { ConfirmationModel } from './confirmation.mjs'
import { emailTemplate } from '../utils/email.mjs'
// import { emailTemplate } from '../utils/email.mjs'
//import set from 'lodash.set'
export function UserModel(tools) {
this.config = tools.config
this.prisma = tools.prisma
this.decrypt = tools.decrypt
this.encrypt = tools.encrypt
this.mailer = tools.email
this.Confirmation = new ConfirmationModel(tools)
return this
}
UserModel.prototype.load = async function (where) {
this.record = await this.prisma.user.findUnique({ where })
if (this.record?.email) this.email = this.decrypt(this.record.email)
return this.setExists()
}
UserModel.prototype.setExists = function () {
this.exists = this.record ? true : false
return this
}
UserModel.prototype.setResponse = function (status = 200, error = false, data = {}) {
this.response = {
status,
body: {
result: 'success',
...data,
},
}
if (status > 201) {
this.response.body.error = error
this.response.body.result = 'error'
this.error = true
} else this.error = false
return this.setExists()
}
UserModel.prototype.create = async function (body) {
if (Object.keys(body) < 1) return this.setResponse(400, 'postBodyMissing')
if (!body.email) return this.setResponse(400, 'emailMissing')
if (!body.password) return this.setResponse(400, 'passwordMissing')
if (!body.language) return this.setResponse(400, 'languageMissing')
const ehash = hash(clean(body.email))
await this.load({ ehash })
if (this.exists) return this.setResponse(400, 'emailExists')
try {
this.email = clean(body.email)
this.language = body.language
const email = this.encrypt(this.email)
const username = clean(randomString()) // Temporary username
this.record = await this.prisma.user.create({
data: {
ehash,
ihash: ehash,
email,
initial: email,
username,
lusername: username,
data: asJson({ settings: { language: this.language } }),
password: asJson(hashPassword(body.password)),
},
})
} catch (err) {
log.warn(err, 'Could not create user record')
return this.setResponse(500, 'createAccountFailed')
}
// Update username
try {
await this.update({
username: `user-${this.record.id}`,
lusername: `user-${this.record.id}`,
})
} catch (err) {
log.warn(err, 'Could not update username after user creation')
return this.setResponse(500, 'error', 'usernameUpdateAfterUserCreationFailed')
}
log.info({ user: this.record.id }, 'Account created')
// Create confirmation
this.confirmation = await this.Confirmation.create({
type: 'signup',
data: this.encrypt({
language: this.language,
email: this.email,
id: this.record.id,
ehash: ehash,
}),
})
// Send signup email
await this.sendSignupEmail()
return body.unittest && this.email.split('@').pop() === this.config.tests.domain
? this.setResponse(201, false, { email: this.email, confirmation: this.confirmation.record.id })
: this.setResponse(201, false, { email: this.email })
}
UserModel.prototype.sendSignupEmail = async function () {
try {
this.confirmationSent = await this.mailer.send(
this.email,
...emailTemplate.signup(this.email, this.language, this.confirmation)
)
} catch (err) {
log.warn(err, 'Unable to send signup email')
return this.setResponse(500, 'error', 'unableToSendSignupEmail')
}
return this.setResponse(200)
}
UserModel.prototype.update = async function (data) {
try {
this.record = await this.prisma.user.update({
where: { id: this.record.id },
data,
})
} catch (err) {
log.warn(err, 'Could not update user record')
process.exit()
return this.setResponse(500, 'error', 'updateUserFailed')
}
return this.setResponse(200)
}
UserModel.prototype.sendResponse = async function (res) {
return res.status(this.response.status).send(this.response.body)
}

View file

@ -1,23 +0,0 @@
import Controller from '../controllers/admin'
const Admin = new Controller()
export default (app, passport) => {
// Users
app.post('/admin/search', passport.authenticate('jwt', { session: false }), Admin.search)
app.put('/admin/patron', passport.authenticate('jwt', { session: false }), Admin.setPatronStatus)
app.put('/admin/role', passport.authenticate('jwt', { session: false }), Admin.setRole)
app.post(
'/admin/impersonate',
passport.authenticate('jwt', { session: false }),
Admin.impersonate
)
app.put('/admin/unfreeze', passport.authenticate('jwt', { session: false }), Admin.unfreeze)
app.get('/admin/patrons', passport.authenticate('jwt', { session: false }), Admin.patronList)
app.get(
'/admin/subscribers',
passport.authenticate('jwt', { session: false }),
Admin.subscriberList
)
app.get('/admin/stats', passport.authenticate('jwt', { session: false }), Admin.stats)
}

View file

@ -1,8 +0,0 @@
import Controller from '../controllers/github'
const Github = new Controller()
export default (app, passport) => {
app.post('/github/issue', Github.createIssue)
app.post('/github/gist', Github.createGist)
}

View file

@ -1,17 +1,5 @@
//import pattern from './pattern'
//import person from './person'
import { userRoutes } from './user.mjs'
//import oauth from './auth.mjs'
//import github from './github'
//import admin from './admin'
//import newsletter from './newsletter'
export const routes = {
userRoutes,
// pattern,
// person,
// oauth,
// github,
// admin,
// newsletter,
}

View file

@ -1,12 +0,0 @@
import Controller from '../controllers/newsletter'
const Nws = new Controller()
export default (app, passport) => {
// Email subscribe
app.post('/newsletter/subscribe', (req, res) => Nws.subscribe(req, res, true))
// Email unsubscribe
app.post('/newsletter/unsubscribe', (req, res) => Nws.subscribe(req, res, false))
app.get('/newsletter/confirm/:token', (req, res) => Nws.confirm(req, res))
app.get('/newsletter/unsubscribe/:ehash', (req, res) => Nws.unsubscribe(req, res))
}

View file

@ -1,10 +0,0 @@
import Controller from '../controllers/oauth.mjs'
const OAuth = new Controller()
export default (app, passport) => {
// Oauth
app.post('/oauth/init', OAuth.initOAuth)
app.get('/oauth/callback/from/:provider', OAuth.providerCallback)
app.post('/oauth/login', OAuth.loginOAuth)
}

View file

@ -1,9 +0,0 @@
import Controller from '../controllers/og'
// Note: Og = Open graph. See https://ogp.me/
const Og = new Controller()
export default (app, passport) => {
// Load open graph image (requires no authentication)
app.get('/og-img/:lang/:site/*', Og.image)
}

View file

@ -1,10 +0,0 @@
import Controller from '../controllers/pattern'
const Pattern = new Controller()
export default (app, passport) => {
app.get('/patterns/:handle', Pattern.read) // Anomymous read
app.post('/patterns', passport.authenticate('jwt', { session: false }), Pattern.create) // Create
app.put('/patterns/:handle', passport.authenticate('jwt', { session: false }), Pattern.update) // Update
app.delete('/patterns/:handle', passport.authenticate('jwt', { session: false }), Pattern.delete) // Delete
}

View file

@ -1,10 +0,0 @@
import Controller from '../controllers/person'
const Person = new Controller()
export default (app, passport) => {
app.post('/people', passport.authenticate('jwt', { session: false }), Person.create) // Create
app.get('/people/:handle', passport.authenticate('jwt', { session: false }), Person.read) // Read
app.put('/people/:handle', passport.authenticate('jwt', { session: false }), Person.update) // Update
app.delete('/people/:handle', passport.authenticate('jwt', { session: false }), Person.delete) // Delete
}

View file

@ -1,9 +0,0 @@
import Controller from '../controllers/strapi'
const Strapi = new Controller()
export default (app, passport) => {
// Email subscribe
app.post('/strapi/maker', (req, res) => Strapi.addPerson(req, res, 'maker'))
app.post('/strapi/author', (req, res) => Strapi.addPerson(req, res, 'author'))
}

View file

@ -7,20 +7,15 @@ import fr from '../../../../packages/i18n/dist/en/email.mjs'
import es from '../../../../packages/i18n/dist/en/email.mjs'
import de from '../../../../packages/i18n/dist/en/email.mjs'
import { i18nUrl } from './index.mjs'
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'
const i18n = { en, nl, fr, es, de }
export const emailTemplate = {
signup: (to, language, uuid) => [
i18n[language].signupTitle,
templates.signup(
i18n[language],
to,
i18nUrl(language, `/confirm/signup/${uuid}`)
)],
emailChange: (to, cc, language, uuid) => [
]
templates.signup(i18n[language], to, i18nUrl(language, `/confirm/signup/${uuid}`)),
],
}
emailTemplate.emailchange = (newAddress, currentAddress, language, id) => {
@ -189,7 +184,6 @@ emailTemplate.newsletterWelcome = async (recipient, ehash) => {
})
}
/*
* Exporting this closure that makes sure we have access to the
* instantiated config
@ -197,7 +191,7 @@ emailTemplate.newsletterWelcome = async (recipient, ehash) => {
export const mailer = (config) => ({
email: {
send: (...params) => sendEmailViaAwsSes(config, ...params),
}
},
})
/*
@ -223,15 +217,14 @@ async function sendEmailViaAwsSes(config, to, subject, text) {
},
Destination: {
ToAddresses: [to],
BccAddresses: [ 'tracking@freesewing.org' ],
CcAddresses: config.aws.ses.cc || [],
BccAddresses: config.aws.ses.bcc || [],
},
FeedbackForwardingEmailAddress: 'bounce@freesewing.org',
FromEmailAddress: 'info@freesewing.org',
ReplyToAddresses: [ 'info@freesewing.org' ],
FeedbackForwardingEmailAddress: config.aws.ses.feedback,
FromEmailAddress: config.aws.ses.from,
ReplyToAddresses: config.aws.ses.replyTo || [],
})
const result = await client.send(command)
return (result['$metadata']?.httpStatusCode === 200)
return result['$metadata']?.httpStatusCode === 200
}

View file

@ -260,7 +260,7 @@ describe(`${user} Signup flow and authentication`, () => {
})
describe(`${user} Account management`, () => {
/*
/* TODO: Need to do this once we have a UI to kick the tires
step(`${user} Should update the account avatar`, (done) => {
chai
.request(config.api)
@ -278,6 +278,27 @@ describe(`${user} Account management`, () => {
done()
})
})
it(`${user} Should update the account username`, (done) => {
chai
.request(config.api)
.put('/account')
.set('Authorization', 'Bearer ' + store.token)
.send({
username: data..username + '_updated',
})
.end((err, res) => {
expect(res.status).to.equal(200)
expect(res.type).to.equal('application/json')
expect(res.charset).to.equal('utf-8')
expect(res.body.result).to.equal(`success`)
expect(res.body.account.email).to.equal(data.email)
expect(res.body.account.username).to.equal(store.username+'_updated')
expect(res.body.account.lusername).to.equal(store.username.toLowerCase()+'_updated')
expect(typeof res.body.account.id).to.equal(`number`)
done()
})
})
*/
})