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 { model Person {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
img String @default("https://freesewing.org/avatar.svg")
name String @default("") name String @default("")
notes String @default("") notes String @default("")
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])

View file

@ -18,21 +18,30 @@ const envToBool = (input = 'no') => {
// Construct config object // Construct config object
const config = { 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, api,
port, apikeys: {
website: { levels: [0, 1, 2, 3, 4, 5, 6, 7, 8],
domain: process.env.BACKEND_WEBSITE_DOMAIN || 'freesewing.org', expiryMaxSeconds: 365 * 24 * 3600,
scheme: process.env.BACKEND_WEBSITE_SCHEME || 'https',
}, },
db: { db: {
url: process.env.BACKEND_DB_URL, 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: { encryption: {
key: process.env.BACKEND_ENC_KEY, key: process.env.BACKEND_ENC_KEY,
}, },
@ -42,10 +51,9 @@ const config = {
audience: process.env.BACKEND_JWT_ISSUER || 'freesewing.org', audience: process.env.BACKEND_JWT_ISSUER || 'freesewing.org',
expiresIn: process.env.BACKEND_JWT_EXPIRY || '7d', expiresIn: process.env.BACKEND_JWT_EXPIRY || '7d',
}, },
apikeys: { languages: ['en', 'de', 'es', 'fr', 'nl'],
levels: [0, 1, 2, 3, 4, 5, 6, 7, 8], measies: measurements,
expiryMaxSeconds: 365 * 24 * 3600, port,
},
roles: { roles: {
levels: { levels: {
user: 4, user: 4,
@ -55,49 +63,21 @@ const config = {
}, },
base: 'user', base: 'user',
}, },
languages: ['en', 'de', 'es', 'fr', 'nl'], website: {
measies: measurements, domain: process.env.BACKEND_WEBSITE_DOMAIN || 'freesewing.org',
aws: { scheme: process.env.BACKEND_WEBSITE_SCHEME || 'https',
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>'],
},
}, },
sanity: { oauth: {},
use: process.env.BACKEND_USE_SANITY || false, github: {},
project: process.env.SANITY_PROJECT, }
dataset: process.env.SANITY_DATASET || 'production',
token: process.env.SANITY_TOKEN, /*
version: process.env.SANITY_VERSION || 'v2022-10-31', * Config behind feature flags
api: `https://${process.env.SANITY_PROJECT || 'missing-project-id'}.api.sanity.io/${ */
process.env.SANITY_VERSION || 'v2022-10-31'
}`, // Github config
}, if (config.use.github)
oauth: { config.github = {
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: {
token: process.env.BACKEND_GITHUB_TOKEN, token: process.env.BACKEND_GITHUB_TOKEN,
api: 'https://api.github.com', api: 'https://api.github.com',
bot: { bot: {
@ -125,11 +105,64 @@ const config = {
}, },
dflt: [process.env.BACKEND_GITHUB_NOTIFY_DEFAULT_USER || 'joostdecock'], dflt: [process.env.BACKEND_GITHUB_NOTIFY_DEFAULT_USER || 'joostdecock'],
}, },
}, }
}
// Stand-alone config // Unit test config
export const sanity = config.sanity 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 export const website = config.website
const vars = { const vars = {

View file

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

View file

@ -1,7 +1,7 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import { log } from '../utils/log.mjs' import { log } from '../utils/log.mjs'
import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.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 { clean, asJson, i18nUrl } from '../utils/index.mjs'
import { ConfirmationModel } from './confirmation.mjs' import { ConfirmationModel } from './confirmation.mjs'
@ -346,7 +346,7 @@ UserModel.prototype.unsafeUpdate = async function (body) {
} }
// Image (img) // Image (img)
if (typeof body.img === 'string') { 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 data.img = img.url
} }

View file

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