wip(backend): Refactoring
This commit is contained in:
parent
18d8d5c590
commit
b45a1c61b4
27 changed files with 369 additions and 1654 deletions
|
@ -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: {
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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/', '<== 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
69
sites/backend/src/models/confirmation.mjs
Normal file
69
sites/backend/src/models/confirmation.mjs
Normal 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)
|
||||
}
|
149
sites/backend/src/models/user.mjs
Normal file
149
sites/backend/src/models/user.mjs
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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'))
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
*/
|
||||
})
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue