1
0
Fork 0

wip(backend): Person creation

This commit is contained in:
joostdecock 2022-11-12 20:05:16 +01:00
parent 41c35b2608
commit 2e938ac29f
5 changed files with 157 additions and 108 deletions

View file

@ -82,6 +82,7 @@ model Pattern {
model Person {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
img String @default("https://freesewing.org/avatar.svg")
name String @default("")
notes String @default("")
user User @relation(fields: [userId], references: [id])

View file

@ -18,21 +18,30 @@ const envToBool = (input = 'no') => {
// Construct config object
const config = {
// Feature flags
use: {
github: envToBool(process.env.BACKEND_ENABLE_GITHUB),
oauth: {
github: envToBool(process.env.BACKEND_ENABLE_OAUTH_GITHUB),
google: envToBool(process.env.BACKEND_ENABLE_OAUTH_GOOGLE),
},
sanity: envToBool(process.env.BACKEND_ENABLE_SANITY),
ses: envToBool(process.env.BACKEND_ENABLE_AWS_SES),
tests: {
base: envToBool(process.env.BACKEND_ENABLE_TESTS),
email: envToBool(process.env.BACKEND_ENABLE_TESTS_EMAIL),
sanity: envToBool(process.env.BACKEND_ENABLE_TESTS_SANITY),
},
},
// Config
api,
port,
website: {
domain: process.env.BACKEND_WEBSITE_DOMAIN || 'freesewing.org',
scheme: process.env.BACKEND_WEBSITE_SCHEME || 'https',
apikeys: {
levels: [0, 1, 2, 3, 4, 5, 6, 7, 8],
expiryMaxSeconds: 365 * 24 * 3600,
},
db: {
url: process.env.BACKEND_DB_URL,
},
tests: {
allow: envToBool(process.env.BACKEND_TEST_ALLOW),
domain: process.env.BACKEND_TEST_DOMAIN || 'freesewing.dev',
sendEmail: envToBool(process.env.BACKEND_TEST_SEND_EMAIL),
includeSanity: envToBool(process.env.BACKEND_TEST_SANITY),
},
encryption: {
key: process.env.BACKEND_ENC_KEY,
},
@ -42,10 +51,9 @@ const config = {
audience: process.env.BACKEND_JWT_ISSUER || 'freesewing.org',
expiresIn: process.env.BACKEND_JWT_EXPIRY || '7d',
},
apikeys: {
levels: [0, 1, 2, 3, 4, 5, 6, 7, 8],
expiryMaxSeconds: 365 * 24 * 3600,
},
languages: ['en', 'de', 'es', 'fr', 'nl'],
measies: measurements,
port,
roles: {
levels: {
user: 4,
@ -55,49 +63,21 @@ const config = {
},
base: 'user',
},
languages: ['en', 'de', 'es', 'fr', 'nl'],
measies: measurements,
aws: {
ses: {
region: process.env.BACKEND_AWS_SES_REGION || 'us-east-1',
from: process.env.BACKEND_AWS_SES_FROM || 'FreeSewing <info@freesewing.org>',
replyTo: process.env.BACKEND_AWS_SES_REPLY_TO
? JSON.parse(process.env.BACKEND_AWS_SES_REPLY_TO)
: ['FreeSewing <info@freesewing.org>'],
feedback: process.env.BACKEND_AWS_SES_FEEDBACK,
cc: process.env.BACKEND_AWS_SES_CC ? JSON.parse(process.env.BACKEND_AWS_SES_CC) : [],
bcc: process.env.BACKEND_AWS_SES_BCC
? JSON.parse(process.env.BACKEND_AWS_SES_BCC)
: ['FreeSewing records <records@freesewing.org>'],
},
website: {
domain: process.env.BACKEND_WEBSITE_DOMAIN || 'freesewing.org',
scheme: process.env.BACKEND_WEBSITE_SCHEME || 'https',
},
sanity: {
use: process.env.BACKEND_USE_SANITY || false,
project: process.env.SANITY_PROJECT,
dataset: process.env.SANITY_DATASET || 'production',
token: process.env.SANITY_TOKEN,
version: process.env.SANITY_VERSION || 'v2022-10-31',
api: `https://${process.env.SANITY_PROJECT || 'missing-project-id'}.api.sanity.io/${
process.env.SANITY_VERSION || 'v2022-10-31'
}`,
},
oauth: {
github: {
clientId: process.env.BACKEND_OAUTH_GITHUB_CLIENT_ID,
clientSecret: process.env.BACKEND_OAUTH_GITHUB_CLIENT_SECRET,
tokenUri: 'https://github.com/login/oauth/access_token',
dataUri: 'https://api.github.com/user',
emailUri: 'https://api.github.com/user/emails',
},
google: {
clientId: process.env.BACKEND_OAUTH_GOOGLE_CLIENT_ID,
clientSecret: process.env.BACKEND_OAUTH_GOOGLE_CLIENT_SECRET,
tokenUri: 'https://oauth2.googleapis.com/token',
dataUri:
'https://people.googleapis.com/v1/people/me?personFields=emailAddresses,names,photos',
},
},
github: {
oauth: {},
github: {},
}
/*
* Config behind feature flags
*/
// Github config
if (config.use.github)
config.github = {
token: process.env.BACKEND_GITHUB_TOKEN,
api: 'https://api.github.com',
bot: {
@ -125,11 +105,64 @@ const config = {
},
dflt: [process.env.BACKEND_GITHUB_NOTIFY_DEFAULT_USER || 'joostdecock'],
},
},
}
}
// Stand-alone config
export const sanity = config.sanity
// Unit test config
if (config.use.tests.base)
config.tests = {
domain: process.env.BACKEND_TEST_DOMAIN || 'freesewing.dev',
}
// Sanity config
if (config.use.sanity)
config.sanity = {
project: process.env.SANITY_PROJECT,
dataset: process.env.SANITY_DATASET || 'production',
token: process.env.SANITY_TOKEN,
version: process.env.SANITY_VERSION || 'v2022-10-31',
api: `https://${process.env.SANITY_PROJECT || 'missing-project-id'}.api.sanity.io/${
process.env.SANITY_VERSION || 'v2022-10-31'
}`,
}
// AWS SES config (for sending out emails)
if (config.use.ses)
config.aws = {
ses: {
region: process.env.BACKEND_AWS_SES_REGION || 'us-east-1',
from: process.env.BACKEND_AWS_SES_FROM || 'FreeSewing <info@freesewing.org>',
replyTo: process.env.BACKEND_AWS_SES_REPLY_TO
? JSON.parse(process.env.BACKEND_AWS_SES_REPLY_TO)
: ['FreeSewing <info@freesewing.org>'],
feedback: process.env.BACKEND_AWS_SES_FEEDBACK,
cc: process.env.BACKEND_AWS_SES_CC ? JSON.parse(process.env.BACKEND_AWS_SES_CC) : [],
bcc: process.env.BACKEND_AWS_SES_BCC
? JSON.parse(process.env.BACKEND_AWS_SES_BCC)
: ['FreeSewing records <records@freesewing.org>'],
},
}
// Oauth config for Github as a provider
if (config.use.oauth?.github)
config.oauth.github = {
clientId: process.env.BACKEND_OAUTH_GITHUB_CLIENT_ID,
clientSecret: process.env.BACKEND_OAUTH_GITHUB_CLIENT_SECRET,
tokenUri: 'https://github.com/login/oauth/access_token',
dataUri: 'https://api.github.com/user',
emailUri: 'https://api.github.com/user/emails',
}
// Oauth config for Google as a provider
if (config.use.oauth?.google)
config.oauth.google = {
clientId: process.env.BACKEND_OAUTH_GOOGLE_CLIENT_ID,
clientSecret: process.env.BACKEND_OAUTH_GOOGLE_CLIENT_SECRET,
tokenUri: 'https://oauth2.googleapis.com/token',
dataUri: 'https://people.googleapis.com/v1/people/me?personFields=emailAddresses,names,photos',
}
// Exporting this stand-alone config
export const sanity = config.sanity || {}
export const website = config.website
const vars = {

View file

@ -6,6 +6,7 @@ export function PersonModel(tools) {
this.prisma = tools.prisma
this.decrypt = tools.decrypt
this.encrypt = tools.encrypt
this.encryptedFields = ['measies', 'img', 'name', 'notes']
this.clear = {} // For holding decrypted data
return this
@ -20,23 +21,29 @@ PersonModel.prototype.create = async function ({ body, user }) {
const data = { name: body.name }
if (body.notes || typeof body.notes === 'string') data.notes = body.notes
if (body.public === true) data.public = true
if (body.measies) data.measies = this.encrypt(this.sanitizeMeasurements(body.measies))
if (body.measies) data.measies = this.sanitizeMeasurements(body.measies)
data.userId = user.uid
// Create record
try {
this.record = await this.prisma.person.create({ data })
this.record = await this.prisma.person.create({ data: this.cloak(data) })
} catch (err) {
log.warn(err, 'Could not create person')
return this.setResponse(500, 'createPersonFailed')
}
return this.setResponse(201, 'created', {
person: {
...this.record,
measies: this.decrypt(this.record.measies),
},
})
// Update img? (now that we have the ID)
const img =
this.config.use.sanity &&
typeof body.img === 'string' &&
(!body.unittest || (body.unittest && this.config.use.tests?.sanity))
? await setPersonAvatar(this.record.id, body.img)
: false
if (img) await this.safeUpdate(this.cloak({ img: img.url }))
else await this.read({ id: this.record.id })
return this.setResponse(201, 'created', { person: this.asPerson() })
}
/*
@ -59,29 +66,30 @@ PersonModel.prototype.read = async function (where) {
/*
* Helper method to decrypt at-rest data
*/
//PersonModel.prototype.reveal = async function (where) {
// this.clear = {}
// if (this.record) {
// this.clear.bio = this.decrypt(this.record.bio)
// this.clear.github = this.decrypt(this.record.github)
// this.clear.email = this.decrypt(this.record.email)
// this.clear.initial = this.decrypt(this.record.initial)
// }
//
// return this
//}
PersonModel.prototype.reveal = async function () {
this.clear = {}
if (this.record) {
for (const field of this.encryptedFields) {
// Default avatar is not encrypted
if (field === 'img' && this.record.img.slice(0, 4) === 'http')
this.clear.img = this.record.img
else this.clear[field] = this.decrypt(this.record[field])
}
} else console.log('no record')
return this
}
/*
* Helper method to encrypt at-rest data
*/
//UserModel.prototype.cloak = function (data) {
// for (const field of ['bio', 'github', 'email']) {
// if (typeof data[field] !== 'undefined') data[field] = this.encrypt(data[field])
// }
// if (typeof data.password === 'string') data.password = asJson(hashPassword(data.password))
//
// return data
//}
PersonModel.prototype.cloak = function (data) {
for (const field of this.encryptedFields) {
if (typeof data[field] !== 'undefined') data[field] = this.encrypt(data[field])
}
return data
}
/*
* Loads a user from the database based on the where clause you pass it
@ -139,24 +147,24 @@ PersonModel.prototype.setExists = function () {
}
/*
* Updates the user data - Used when we create the data ourselves
* Updates the person data - Used when we create the data ourselves
* so we know it's safe
*/
//UserModel.prototype.safeUpdate = 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, 'updateUserFailed')
// }
// await this.reveal()
//
// return this.setResponse(200)
//}
PersonModel.prototype.safeUpdate = async function (data) {
try {
this.record = await this.prisma.person.update({
where: { id: this.record.id },
data,
})
} catch (err) {
log.warn(err, 'Could not update person record')
process.exit()
return this.setResponse(500, 'updatePersonFailed')
}
await this.reveal()
return this.setResponse(200)
}
/*
* Updates the user data - Used when we pass through user-provided data
@ -263,7 +271,10 @@ PersonModel.prototype.setExists = function () {
* Returns record data
*/
PersonModel.prototype.asPerson = function () {
return this.reveal()
return {
...this.record,
...this.clear,
}
}
/*

View file

@ -1,7 +1,7 @@
import jwt from 'jsonwebtoken'
import { log } from '../utils/log.mjs'
import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs'
import { setPersonAvatar } from '../utils/sanity.mjs'
import { setUserAvatar } from '../utils/sanity.mjs'
import { clean, asJson, i18nUrl } from '../utils/index.mjs'
import { ConfirmationModel } from './confirmation.mjs'
@ -346,7 +346,7 @@ UserModel.prototype.unsafeUpdate = async function (body) {
}
// Image (img)
if (typeof body.img === 'string') {
const img = await setPersonAvatar(this.record.id, body.img)
const img = await setUserAvatar(this.record.id, body.img)
data.img = img.url
}

View file

@ -1,3 +1,4 @@
import { cat } from './cat.mjs'
/*
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
@ -20,6 +21,7 @@ export const personTests = async (chai, config, expect, store) => {
neck: 420,
},
public: true,
unittest: true,
},
key: {
name: 'Sorcha',
@ -29,6 +31,8 @@ export const personTests = async (chai, config, expect, store) => {
neck: 360,
},
public: false,
img: cat,
unittest: true,
},
}
@ -49,12 +53,12 @@ export const personTests = async (chai, config, expect, store) => {
)
.send(data[auth])
.end((err, res) => {
console.log(res.body)
expect(err === null).to.equal(true)
expect(res.status).to.equal(201)
expect(res.body.result).to.equal(`success`)
for (const [key, val] of Object.entries(data[auth])) {
expect(res.body.person[key]).to.equal(val)
if (!['measies', 'img', 'unittest'].includes(key))
expect(res.body.person[key]).to.equal(val)
}
done()
})