1
0
Fork 0

wip(backend): Don't require password at signup

This commit is contained in:
joostdecock 2022-11-08 22:41:30 +01:00
parent 19050ce3b7
commit 73ee7cceb3
7 changed files with 159 additions and 1230 deletions

View file

@ -39,7 +39,8 @@ UserModel.prototype.read = async function (where) {
UserModel.prototype.reveal = async function (where) {
this.clear = {}
if (this.record) {
this.clear.data = JSON.parse(this.decrypt(this.record.data))
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)
}
@ -47,6 +48,18 @@ UserModel.prototype.reveal = async function (where) {
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
}
/*
* Loads a user from the database based on the where clause you pass it
* In addition prepares it for returning the account data
@ -125,8 +138,9 @@ UserModel.prototype.setExists = function () {
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')
if (!this.config.languages.includes(body.language))
return this.setResponse(400, 'unsupportedLanguage')
const ehash = hash(clean(body.email))
await this.read({ ehash })
@ -145,8 +159,10 @@ UserModel.prototype.create = async function ({ body }) {
initial: email,
username,
lusername: username,
data: this.encrypt(asJson({ settings: { language: this.language } })),
password: asJson(hashPassword(body.password)),
language: body.language,
password: asJson(hashPassword(randomString())), // We'll change this later
github: this.encrypt(''),
bio: this.encrypt(''),
}
this.record = await this.prisma.user.create({ data })
} catch (err) {
@ -300,41 +316,92 @@ UserModel.prototype.safeUpdate = async function (data) {
*/
UserModel.prototype.unsafeUpdate = async function (body) {
const data = {}
// Update consent
const notes = []
// Bio
if (typeof body.bio === 'string') data.bio = body.bio
// Consent
if ([0, 1, 2, 3].includes(body.consent)) data.consent = body.consent
// Update newsletter
// Github
if (typeof body.github === 'string') data.github = body.github.split('@').pop()
// Imperial
if ([true, false].includes(body.imperial)) data.imperial = body.imperial
// Language
if (this.config.languages.includes(body.language)) data.language = body.language
// Newsletter
if ([true, false].includes(body.newsletter)) data.newsletter = body.newsletter
// Update username
// Password
if (typeof body.password === 'string') data.password = body.password // Will be cloaked below
// Patron
if ([0, 2, 4, 8].includes(body.patron)) data.patron = body.patron
// Username
if (typeof body.username === 'string') {
if (await this.isAvailableUsername(body.username)) {
const available = await this.isLusernameAvailable(body.username)
if (available) {
data.username = body.username.trim()
data.lusername = clean(body.username)
} else {
log.info(`Rejected user name change from ${data.username} to ${body.username.trim()}`)
notes.push('usernameChangeRejected')
}
}
// Update password
if (typeof body.password === 'string') {
data.password = asJson(hashPassword(body.password))
}
// Update data
if (typeof body.data === 'object') {
data.data = { ...this.record.data }
// Now update the record
await this.safeUpdate(this.cloak(data))
// Email change requires confirmation
if (typeof body.email === 'string' && this.clear.email !== clean(body.email)) {
if (typeof body.confirmation === 'string') {
// Retrieve confirmation record
await this.Confirmation.read({ id: body.confirmation })
if (!this.Confirmation.exists) {
log.warn(err, `Could not find confirmation id ${params.id}`)
return this.setResponse(404, 'failedToFindConfirmationId')
}
if (this.Confirmation.record.type !== 'emailchange') {
log.warn(err, `Confirmation mismatch; ${params.id} is not an emailchange id`)
return this.setResponse(404, 'confirmationIdTypeMismatch')
}
const data = this.Confirmation.clear.data
if (data.email.current === this.clear.email && typeof data.email.new === 'string') {
await this.saveUpdate({
email: this.encrypt(data.email),
})
}
} else {
// Create confirmation for email change
this.confirmation = await this.Confirmation.create({
type: 'emailchange',
data: {
language: this.record.language,
email: {
current: this.clear.email,
new: body.email,
},
},
userId: this.record.id,
})
// Send confirmation email
await this.mailer.send({
template: 'emailchange',
language: this.record.language,
to: body.email,
cc: this.clear.email,
replacements: {
actionUrl: i18nUrl(this.language, `/confirm/emailchange/${this.Confirmation.record.id}`),
whyUrl: i18nUrl(this.language, `/docs/faq/email/why-emailchange`),
supportUrl: i18nUrl(this.language, `/patrons/join`),
},
})
}
}
/*
data String @default("{}")
ehash String @unique
email String
*/
try {
this.record = await this.prisma.user.update({ where, data })
} catch (err) {
log.warn(err, 'Could not update user record')
process.exit()
return this.setResponse(500, 'updateUserFailed')
}
return this.setResponse(200)
return this.setResponse(200, false, {
result: 'success',
account: this.asAccount(),
})
}
/*
@ -441,3 +508,21 @@ UserModel.prototype.loginOk = function () {
account: this.asAccount(),
})
}
/*
* Check to see if a (lowercase) username is available
* as well as making sure username is not something we
* do not allow
*/
UserModel.prototype.isLusernameAvailable = async function (lusername) {
if (lusername.length < 2) return false
if (lusername.slice(0, 5) === 'user-') return false
let found
try {
found = await this.prisma.user.findUnique({ where: { lusername } })
} catch (err) {
log.warn({ err, where }, 'Could not search for free username')
}
return true
}