1
0
Fork 0
freesewing/sites/backend/src/models/pattern.mjs

595 lines
17 KiB
JavaScript
Raw Normal View History

import { log } from '../utils/log.mjs'
import { capitalize } from '../utils/index.mjs'
import { storeImage } from '../utils/cloudflare-images.mjs'
import yaml from 'js-yaml'
import { SetModel } from './set.mjs'
export function PatternModel(tools) {
this.config = tools.config
this.prisma = tools.prisma
this.decrypt = tools.decrypt
this.encrypt = tools.encrypt
this.rbac = tools.rbac
this.encryptedFields = ['data', 'img', 'name', 'notes', 'settings']
this.clear = {} // For holding decrypted data
this.Set = new SetModel(tools)
return this
}
2023-05-26 17:23:56 +02:00
/*
* Returns a list of sets for the user making the API call
*/
PatternModel.prototype.userPatterns = async function (uid) {
if (!uid) return false
let patterns
try {
patterns = await this.prisma.pattern.findMany({
where: { userId: uid },
include: {
set: true,
cset: true,
},
})
2023-05-26 17:23:56 +02:00
} catch (err) {
log.warn(`Failed to search patterns for user ${uid}: ${err}`)
}
const list = []
for (const pattern of patterns) list.push(this.revealPattern(pattern))
return list
}
PatternModel.prototype.guardedCreate = async function ({ body, user }) {
if (!this.rbac.user(user)) return this.setResponse(403, 'insufficientAccessLevel')
2022-12-29 13:40:25 -08:00
if (Object.keys(body).length < 2) return this.setResponse(400, 'postBodyMissing')
if (!body.set && !body.cset) return this.setResponse(400, 'setOrCsetMissing')
if (typeof body.set !== 'undefined' && typeof body.set !== 'number')
return this.setResponse(400, 'setNotNumeric')
if (typeof body.cset !== 'undefined' && typeof body.cset !== 'number')
return this.setResponse(400, 'csetNotNumeric')
if (typeof body.settings !== 'object') return this.setResponse(400, 'settingsNotAnObject')
if (body.data && typeof body.data !== 'object') return this.setResponse(400, 'dataNotAnObject')
if (!body.design && !body.data?.design) return this.setResponse(400, 'designMissing')
if (typeof body.design !== 'string') return this.setResponse(400, 'designNotStringy')
// Prepare data
const data = {
design: body.design,
settings: body.settings,
}
if (data.settings.measurements) delete data.settings.measurements
if (body.set) data.setId = body.set
else if (body.cset) data.csetId = body.cset
else return this.setResponse(400, 'setOrCsetMissing')
// Data (will be encrypted, so always set _some_ value)
if (typeof body.data === 'object') data.data = body.data
else data.data = {}
// Name (will be encrypted, so always set _some_ value)
if (typeof body.name === 'string' && body.name.length > 0) data.name = body.name
else data.name = '--'
// Notes (will be encrypted, so always set _some_ value)
if (typeof body.notes === 'string' && body.notes.length > 0) data.notes = body.notes
else data.notes = '--'
// Public
if (body.public === true) data.public = true
data.userId = user.uid
// Set this one initially as we need the ID to store an image on cloudflare
data.img = this.config.avatars.pattern
// Create record
await this.unguardedCreate(data)
// Update img? (now that we have the ID)
const img =
this.config.use.cloudflareImages &&
typeof body.img === 'string' &&
(!body.test || (body.test && this.config.use.tests?.cloudflareImages))
? await storeImage({
id: `pattern-${this.record.id}`,
metadata: { user: user.uid },
b64: body.img,
})
: false
if (img) await this.unguardedUpdate(this.cloak({ img: img.url }))
else await this.read({ id: this.record.id })
return this.setResponse(201, 'created', { pattern: this.asPattern() })
}
PatternModel.prototype.unguardedCreate = async function (data) {
try {
this.record = await this.prisma.pattern.create({ data: this.cloak(data) })
} catch (err) {
log.warn(err, 'Could not create pattern')
return this.setResponse(500, 'createPatternFailed')
}
return this
}
/*
* Loads a pattern from the database based on the where clause you pass it
*
* Stores result in this.record
*/
PatternModel.prototype.read = async function (where) {
try {
this.record = await this.prisma.pattern.findUnique({
where,
include: {
set: true,
cset: true,
},
})
} catch (err) {
log.warn({ err, where }, 'Could not read pattern')
}
this.reveal()
return this.setExists()
}
/*
* Loads a pattern from the database but only if it's public
*
* Stores result in this.record
*/
PatternModel.prototype.publicRead = async function ({ params }) {
await this.read({ id: parseInt(params.id) })
if (this.record.public !== true) {
// Note that we return 404
// because we don't want to reveal that a non-public pattern exists.
return this.setResponse(404)
}
return this.setResponse(200, false, this.asPublicPattern(), true)
}
/*
* Loads a pattern from the database based on the where clause you pass it
* In addition prepares it for returning the pattern data
*
* Stores result in this.record
*/
PatternModel.prototype.guardedRead = async function ({ params, user }) {
if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
if (typeof params.id !== 'undefined' && !Number(params.id))
return this.setResponse(403, 'idNotNumeric')
await this.read({ id: parseInt(params.id) })
if (!this.record) return this.setResponse(404, 'notFound')
if (this.record.userId !== user.uid && !this.rbac.bughunter(user)) {
return this.setResponse(403, 'insufficientAccessLevel')
}
return this.setResponse(200, false, {
result: 'success',
pattern: this.asPattern(),
})
}
/*
* Clones a pattern
* In addition prepares it for returning the pattern data
*
* Stores result in this.record
*/
PatternModel.prototype.guardedClone = async function ({ params, user }) {
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
2023-05-31 18:34:37 +02:00
if (this.record.userId !== user.uid && !this.record.public && !this.rbac.support(user)) {
return this.setResponse(403, 'insufficientAccessLevel')
}
// Clone pattern
const data = this.asPattern()
delete data.id
data.name += ` (cloned from #${this.record.id})`
data.notes += ` (Note: This pattern was cloned from pattern #${this.record.id})`
await this.unguardedCreate(data)
// Update unencrypted data
this.reveal()
return this.setResponse(200, false, {
result: 'success',
pattern: this.asPattern(),
})
}
/*
* Helper method to decrypt at-rest data
*/
PatternModel.prototype.reveal = async function () {
this.clear = {}
if (this.record) {
for (const field of this.encryptedFields) {
this.clear[field] = this.decrypt(this.record[field])
}
}
// Parse JSON content
if (this.record?.cset) {
for (const lang of this.config.languages) {
const key = `tags${capitalize(lang)}`
if (this.record.cset[key]) this.record.cset[key] = JSON.parse(this.record.cset[key])
}
if (this.record.cset.measies) this.record.cset.measies = JSON.parse(this.record.cset.measies)
}
if (this.record?.set) {
this.record.set = this.Set.revealSet(this.record.set)
}
return this
}
/*
* Helper method to encrypt at-rest data
*/
PatternModel.prototype.cloak = function (data) {
for (const field of this.encryptedFields) {
if (typeof data[field] !== 'undefined') {
data[field] = this.encrypt(data[field])
}
}
return data
}
/*
* Checks this.record and sets a boolean to indicate whether
* the pattern exists or not
*
* Stores result in this.exists
*/
PatternModel.prototype.setExists = function () {
this.exists = this.record ? true : false
return this
}
/*
* Updates the pattern data - Used when we create the data ourselves
* so we know it's safe
*/
PatternModel.prototype.unguardedUpdate = async function (data) {
try {
this.record = await this.prisma.pattern.update({
where: { id: this.record.id },
data,
include: {
set: true,
cset: true,
},
})
} catch (err) {
log.warn(err, 'Could not update pattern record')
process.exit()
return this.setResponse(500, 'updatePatternFailed')
}
await this.reveal()
return this.setResponse(200)
}
/*
* Updates the pattern data - Used when we pass through user-provided data
* so we can't be certain it's safe
*/
PatternModel.prototype.guardedUpdate = async function ({ params, body, user }) {
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && !this.rbac.admin(user)) {
return this.setResponse(403, 'insufficientAccessLevel')
}
const data = {}
// Name
if (typeof body.name === 'string') data.name = body.name
// Notes
if (typeof body.notes === 'string') data.notes = body.notes
// Public
if (body.public === true || body.public === false) data.public = body.public
// Data
if (typeof body.data === 'object') data.data = body.data
// Settings
if (typeof body.settings === 'object') data.settings = body.settings
// Image (img)
if (typeof body.img === 'string') {
const img = await storeImage({
id: `pattern-${this.record.id}`,
metadata: { user: this.user.uid },
b64: body.img,
})
data.img = img.url
}
// Now update the record
await this.unguardedUpdate(this.cloak(data))
return this.setResponse(200, false, { pattern: this.asPattern() })
}
/*
* Removes the pattern - No questions asked
*/
PatternModel.prototype.unguardedDelete = async function () {
2023-05-26 17:23:56 +02:00
await this.prisma.pattern.delete({ where: { id: this.record.id } })
this.record = null
this.clear = null
return this.setExists()
}
/*
* Removes the pattern - Checks permissions
*/
PatternModel.prototype.guardedDelete = async function ({ params, user }) {
2023-05-26 17:23:56 +02:00
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
await this.read({ id: parseInt(params.id) })
if (this.record.userId !== user.uid && !this.rbac.admin(user)) {
return this.setResponse(403, 'insufficientAccessLevel')
}
await this.unguardedDelete()
return this.setResponse(204, false)
}
/*
* Returns record data
*/
PatternModel.prototype.asPattern = function () {
return {
...this.record,
...this.clear,
}
}
2023-05-26 17:23:56 +02:00
/*
* Helper method to decrypt data from a non-instantiated pattern
*/
PatternModel.prototype.revealPattern = function (pattern) {
const clear = {}
for (const field of this.encryptedFields) {
try {
clear[field] = this.decrypt(pattern[field])
} catch (err) {
//console.log(err)
}
}
if (pattern.set) delete pattern.set.measies
if (pattern.cset) delete pattern.cset.measies
2023-05-26 17:23:56 +02:00
return { ...pattern, ...clear }
}
/*
* Helper method to set the response code, result, and body
*
* Will be used by this.sendResponse()
*/
PatternModel.prototype.setResponse = function (
status = 200,
error = false,
data = {},
rawData = false
) {
this.response = {
status,
body: rawData
? data
: {
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()
}
/*
* Helper method to send response
*/
PatternModel.prototype.sendResponse = async function (res) {
return res.status(this.response.status).send(this.response.body)
}
/*
* Helper method to send response as YAML
*/
PatternModel.prototype.sendYamlResponse = async function (res) {
return res.status(this.response.status).type('yaml').send(yaml.dump(this.response.body))
}
/*
* Returns record data fit for public publishing
*/
PatternModel.prototype.asPublicPattern = function () {
const data = {
author: 'FreeSewing.org',
type: 'pattern',
...this.asPattern(),
}
delete data.userId
delete data.public
return data
}
2023-08-09 20:40:38 +02:00
const migratePattern = (v2, userId) => ({
createdAt: new Date(v2.created ? v2.created : v2.createdAt),
data: { version: v2.data.version, notes: ['Migrated from version 2'] },
design: v2.design || v2.data.design,
name: v2.name || '--',
notes: v2.notes ? v2.notes + '\n\nMigrated from v2' : 'Migrated from v2',
settings: v2.data.settings,
userId,
})
const v2lut = {
'size 28, with breasts': 1,
'size 30, with breasts': 2,
'size 32, with breasts': 3,
'size 34, with breasts': 4,
'size 36, with breasts': 5,
'size 38, with breasts': 6,
'size 40, with breasts': 7,
'size 42, with breasts': 8,
'size 44, with breasts': 9,
'size 46, with breasts': 10,
'size-28-b': 1,
'size-30-b': 2,
'size-32-b': 3,
'size-34-b': 4,
'size-36-b': 5,
'size-38-b': 6,
'size-40-b': 7,
'size-42-b': 8,
'size-44-b': 9,
'size-46-b': 10,
'size-28-with-breasts': 1,
'size-30-with-breasts': 2,
'size-32-with-breasts': 3,
'size-34-with-breasts': 4,
'size-36-with-breasts': 5,
'size-38-with-breasts': 6,
'size-40-with-breasts': 7,
'size-42-with-breasts': 8,
'size-44-with-breasts': 9,
'size-46-with-breasts': 10,
'größe 28, mit brüsten': 1,
'größe 30, mit brüsten': 2,
'größe 32, mit brüsten': 3,
'größe 34, mit brüsten': 4,
'größe 36, mit brüsten': 5,
'größe 38, mit brüsten': 6,
'größe 40, mit brüsten': 7,
'größe 42, mit brüsten': 8,
'größe 44, mit brüsten': 9,
'größe 46, mit brüsten': 10,
'taille 28, avec des seins': 1,
'taille 30, avec des seins': 2,
'taille 32, avec des seins': 3,
'taille 34, avec des seins': 4,
'taille 36, avec des seins': 5,
'taille 38, avec des seins': 6,
'taille 40, avec des seins': 7,
'taille 42, avec des seins': 8,
'taille 44, avec des seins': 9,
'taille 46, avec des seins': 10,
'tamaño 28, con pechos': 1,
'tamaño 30, con pechos': 2,
'tamaño 32, con pechos': 3,
'tamaño 34, con pechos': 4,
'tamaño 36, con pechos': 5,
'tamaño 38, con pechos': 6,
'tamaño 40, con pechos': 7,
'tamaño 42, con pechos': 8,
'tamaño 44, con pechos': 9,
'tamaño 46, con pechos': 10,
'size 32, without breasts': 11,
'size 34, without breasts': 12,
'size 36, without breasts': 13,
'size 38, without breasts': 14,
'size 40, without breasts': 15,
'size 42, without breasts': 16,
'size 44, without breasts': 17,
'size 46, without breasts': 18,
'size 48, without breasts': 19,
'size 50, without breasts': 20,
'taille 32, sans seins': 11,
'taille 34, sans seins': 12,
'taille 36, sans seins': 13,
'taille 38, sans seins': 14,
'taille 40, sans seins': 15,
'taille 42, sans seins': 16,
'taille 44, sans seins': 17,
'taille 46, sans seins': 18,
'taille 48, sans seins': 19,
'taille 50, sans seins': 20,
'size-32-a': 11,
'size-34-a': 12,
'size-36-a': 13,
'size-38-a': 14,
'size-40-a': 15,
'size-42-a': 16,
'size-44-a': 17,
'size-46-a': 18,
'size-48-a': 19,
'size-50-a': 20,
'maat 32, zonder borsten': 11,
'maat 34, zonder borsten': 12,
'maat 36, zonder borsten': 13,
'maat 38, zonder borsten': 14,
'maat 40, zonder borsten': 15,
'maat 42, zonder borsten': 16,
'maat 44, zonder borsten': 17,
'maat 46, zonder borsten': 18,
'maat 48, zonder borsten': 19,
'maat 50, zonder borsten': 20,
'größe 32, ohne brüste': 11,
'größe 34, ohne brüste': 12,
'größe 36, ohne brüste': 13,
'größe 38, ohne brüste': 14,
'größe 40, ohne brüste': 15,
'größe 42, ohne brüste': 16,
'größe 44, ohne brüste': 17,
'größe 46, ohne brüste': 18,
'größe 48, ohne brüste': 19,
'größe 50, ohne brüste': 20,
'tamaño 32, sin pechos': 11,
'tamaño 34, sin pechos': 12,
'tamaño 36, sin pechos': 13,
'tamaño 38, sin pechos': 14,
'tamaño 40, sin pechos': 15,
'tamaño 42, sin pechos': 16,
'tamaño 44, sin pechos': 17,
'tamaño 46, sin pechos': 18,
'tamaño 48, sin pechos': 19,
'tamaño 50, sin pechos': 20,
}
/*
* This is a special route not available for API users
*/
PatternModel.prototype.import = async function (v2user, lut, userId) {
for (const [handle, pattern] of Object.entries(v2user.patterns)) {
let skip = false
const data = { ...migratePattern(pattern, userId), userId }
if (lut[pattern.person]) data.setId = lut[pattern.person]
else if (v2lut[pattern.person]) data.csetId = v2lut[pattern.person]
else if (pattern.person.length !== 5 && !['any', 'original'].includes(pattern.person)) {
console.log(`Cannot find ${pattern.person}`, pattern, { lut, v2lut })
process.exit()
}
if (!data.design || ['theo', 'ursula', 'unice'].includes(data.design)) skip = true
if (!skip) {
// V2 does not support images for patterns
data.img = 'default-avatar'
const cloaked = await this.cloak(data)
try {
this.record = await this.prisma.pattern.create({ data: cloaked })
} catch (err) {
log.warn(err, 'Could not create pattern')
console.log(data)
}
}
}
}