wip(backend): Don't require password at signup
This commit is contained in:
parent
19050ce3b7
commit
73ee7cceb3
7 changed files with 159 additions and 1230 deletions
|
@ -39,27 +39,29 @@ model Subscriber {
|
|||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
apikeys Apikey[]
|
||||
bio String @default("")
|
||||
confirmations Confirmation[]
|
||||
consent Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
data String @default("{}")
|
||||
ehash String @unique
|
||||
email String
|
||||
github String @default("")
|
||||
ihash String
|
||||
initial String
|
||||
imperial Boolean @default(false)
|
||||
language String @default("en")
|
||||
lastLogin DateTime?
|
||||
lusername String @unique
|
||||
newsletter Boolean @default(false)
|
||||
password String
|
||||
patron Int @default(0)
|
||||
apikeys Apikey[]
|
||||
confirmations Confirmation[]
|
||||
people Person[]
|
||||
patterns Pattern[]
|
||||
people Person[]
|
||||
role String @default("user")
|
||||
status Int @default(0)
|
||||
updatedAt DateTime? @updatedAt
|
||||
username String
|
||||
lusername String @unique
|
||||
|
||||
@@index([ihash])
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
|
@ -1,745 +0,0 @@
|
|||
//import { log, email, ehash, newHandle, uniqueHandle } from '../utils'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import axios from 'axios'
|
||||
//import path from 'path'
|
||||
//import fs from 'fs'
|
||||
import Zip from 'jszip'
|
||||
//import rimraf from 'rimraf'
|
||||
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 { 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
|
||||
* So here's a bunch of helper methods that expect a user object
|
||||
* as input
|
||||
*/
|
||||
const asAccount = (user, decrypt) => ({
|
||||
id: user.id,
|
||||
consent: user.consent,
|
||||
createdAt: user.createdAt,
|
||||
data: user.data,
|
||||
email: decrypt(user.email),
|
||||
initial: decrypt(user.initial),
|
||||
lastLogin: user.lastLogin,
|
||||
newsletter: user.newsletter,
|
||||
patron: user.patron,
|
||||
role: user.role,
|
||||
status: user.status,
|
||||
updatedAt: user.updatedAt,
|
||||
username: user.username,
|
||||
lusername: user.lusername,
|
||||
})
|
||||
|
||||
const getToken = (user, config) =>
|
||||
jwt.sign(
|
||||
{
|
||||
_id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
status: user.status,
|
||||
aud: config.jwt.audience,
|
||||
iss: config.jwt.issuer,
|
||||
},
|
||||
config.jwt.secretOrKey,
|
||||
{ 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'
|
||||
|
||||
export function UserController() {}
|
||||
|
||||
/*
|
||||
* Signup
|
||||
*
|
||||
* This is the endpoint that handles account signups
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
UserController.prototype.signup = async (req, res, tools) => {
|
||||
const User = new UserModel(tools)
|
||||
await User.create(req.body)
|
||||
|
||||
return User.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Confirm account (after signup)
|
||||
*
|
||||
* This is the endpoint that fully unlocks the account if the user gives their consent
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
UserController.prototype.confirm = async (req, res, tools) => {
|
||||
if (!req.params.id) return res.status(404).send({ error: 'missingConfirmationId', result })
|
||||
if (Object.keys(req.body) < 1) return res.status(400).json({ error: 'postBodyMissing', result })
|
||||
if (!req.body.consent || req.body.consent < 1)
|
||||
return res.status(400).send({ error: 'consentRequired', result })
|
||||
|
||||
// Destructure what we need from tools
|
||||
const { prisma, config, decrypt } = tools
|
||||
|
||||
// Retrieve confirmation record
|
||||
let confirmation
|
||||
try {
|
||||
confirmation = await prisma.confirmation.findUnique({
|
||||
where: {
|
||||
id: req.params.id,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
log.warn(err, `Could not lookup confirmation id ${req.params.id}`)
|
||||
return res.status(404).send({ error: 'failedToRetrieveConfirmationId', result })
|
||||
}
|
||||
if (!confirmation) {
|
||||
log.warn(err, `Could not find confirmation id ${req.params.id}`)
|
||||
return res.status(404).send({ error: 'failedToFindConfirmationId', result })
|
||||
}
|
||||
if (confirmation.type !== 'signup') {
|
||||
log.warn(err, `Confirmation mismatch; ${req.params.id} is not a signup id`)
|
||||
return res.status(404).send({ error: 'confirmationIdTypeMismatch', result })
|
||||
}
|
||||
const data = decrypt(confirmation.data)
|
||||
|
||||
// Retrieve user account
|
||||
let account
|
||||
try {
|
||||
account = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: data.id,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
log.warn(err, `Could not lookup user id ${data.id} from confirmation data`)
|
||||
return res.status(404).send({ error: 'failedToRetrieveUserIdFromConfirmationData', result })
|
||||
}
|
||||
if (!account) {
|
||||
log.warn(err, `Could not find user id ${data.id} from confirmation data`)
|
||||
return res.status(404).send({ error: 'failedToLoadUserFromConfirmationData', result })
|
||||
}
|
||||
|
||||
// Update user consent and status
|
||||
let updateUser
|
||||
try {
|
||||
updateUser = await prisma.user.update({
|
||||
where: {
|
||||
id: account.id,
|
||||
},
|
||||
data: {
|
||||
status: 1,
|
||||
consent: req.body.consent,
|
||||
lastLogin: new Date(),
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
log.warn(err, `Could not update user id ${data.id} after confirmation`)
|
||||
return res.status(404).send({ error: 'failedToUpdateUserAfterConfirmation', result })
|
||||
}
|
||||
|
||||
// Account is now active, let's return a passwordless login
|
||||
return res.status(200).send({
|
||||
result: 'success',
|
||||
token: getToken(account, config),
|
||||
account: asAccount({ ...account, status: 1, consent: req.body.consent }, decrypt),
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Login (with username and password)
|
||||
*
|
||||
* This is the endpoint that provides traditional username/password login
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
UserController.prototype.login = async function (req, res, tools) {
|
||||
if (Object.keys(req.body) < 1) return res.status(400).json({ error: 'postBodyMissing', result })
|
||||
if (!req.body.username) return res.status(400).json({ error: 'usernameMissing', result })
|
||||
if (!req.body.password) return res.status(400).json({ error: 'passwordMissing', result })
|
||||
|
||||
// Destructure what we need from tools
|
||||
const { prisma, config, decrypt } = tools
|
||||
|
||||
// Retrieve user account
|
||||
let account
|
||||
try {
|
||||
account = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ lusername: { equals: clean(req.body.username) } },
|
||||
{ ehash: { equals: hash(clean(req.body.username)) } },
|
||||
{ id: { equals: parseInt(req.body.username) || -1 } },
|
||||
],
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
log.warn(err, `Error while trying to find username: ${req.body.username}`)
|
||||
return res.status(401).send({ error: 'loginFailed', result })
|
||||
}
|
||||
if (!account) {
|
||||
log.warn(`Login attempt for non-existing user: ${req.body.username} from ${req.ip}`)
|
||||
return res.status(401).send({ error: 'loginFailed', result })
|
||||
}
|
||||
|
||||
// Account found, check password
|
||||
const [valid, updatedPasswordField] = verifyPassword(req.body.password, account.password)
|
||||
if (!valid) {
|
||||
log.warn(`Wrong password for existing user: ${req.body.username} from ${req.ip}`)
|
||||
return res.status(401).send({ error: 'loginFailed', result })
|
||||
}
|
||||
|
||||
// Login success
|
||||
log.info(`Login by user ${account.id} (${account.username})`)
|
||||
if (updatedPasswordField) {
|
||||
// Update the password field with a v3 hash
|
||||
let updateUser
|
||||
try {
|
||||
updateUser = await prisma.user.update({
|
||||
where: {
|
||||
id: account.id,
|
||||
},
|
||||
data: {
|
||||
password: updatedPasswordField,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
log.warn(
|
||||
err,
|
||||
`Could not update password field with v3 hash for user id ${account.id} (${account.username})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
result: 'success',
|
||||
token: getToken(account, config),
|
||||
account: asAccount({ ...account }, decrypt),
|
||||
})
|
||||
}
|
||||
|
||||
UserController.prototype.readAccount = async (req, res, tools) => {
|
||||
if (!req.user?._id) return res.status(400).send({ error: 'bearerMissing', result })
|
||||
|
||||
// Destructure what we need from tools
|
||||
const { prisma, decrypt } = tools
|
||||
|
||||
// Retrieve user account
|
||||
let account
|
||||
try {
|
||||
account = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: req.user._id,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
log.warn(err, `Could not lookup user id ${req.user._id} from token data`)
|
||||
return res.status(404).send({ error: 'failedToRetrieveUserIdFromTokenData', result })
|
||||
}
|
||||
if (!account) {
|
||||
log.warn(err, `Could not find user id ${req.user._id} from token data`)
|
||||
return res.status(404).send({ error: 'failedToLoadUserFromTokenData', result })
|
||||
}
|
||||
|
||||
// Return account data
|
||||
return res.status(200).send({
|
||||
result: 'success',
|
||||
account: asAccount({ ...account }, decrypt),
|
||||
})
|
||||
}
|
||||
|
||||
UserController.prototype.update = async (req, res, tools) => {
|
||||
if (!req.user?._id) return res.status(400).send({ error: 'bearerMissing', result })
|
||||
|
||||
// Destructure what we need from tools
|
||||
const { prisma, decrypt } = tools
|
||||
|
||||
// Retrieve user account
|
||||
let account
|
||||
try {
|
||||
account = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: req.user._id,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
log.warn(err, `Could not lookup user id ${req.user._id} from token data`)
|
||||
return res.status(404).send({ error: 'failedToRetrieveUserIdFromTokenData', result })
|
||||
}
|
||||
if (!account) {
|
||||
log.warn(err, `Could not find user id ${req.user._id} from token data`)
|
||||
return res.status(404).send({ error: 'failedToLoadUserFromTokenData', result })
|
||||
}
|
||||
|
||||
// Account loaded - Handle various updates
|
||||
const data = {}
|
||||
// Username
|
||||
if (req.body.username) {
|
||||
if (!isUsernameAvailable(req.body.username, prisma)) {
|
||||
return res.status(400).send({ error: 'usernameTaken', result })
|
||||
}
|
||||
data.username = req.body.username
|
||||
data.lusername = data.username.toLowerCase()
|
||||
}
|
||||
// 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)
|
||||
|
||||
// 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) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (confirmation === null) return res.sendStatus(401)
|
||||
if (confirmation.data.email.new === req.body.email) {
|
||||
user.ehash = ehash(req.body.email)
|
||||
user.email = req.body.email
|
||||
return saveAndReturnAccount(res, user)
|
||||
} else return res.sendStatus(400)
|
||||
})
|
||||
} else {
|
||||
let confirmation = new Confirmation({
|
||||
type: 'emailchange',
|
||||
data: {
|
||||
handle: user.handle,
|
||||
language: user.settings.language,
|
||||
email: {
|
||||
new: req.body.email,
|
||||
current: user.email,
|
||||
},
|
||||
},
|
||||
})
|
||||
confirmation.save(function (err) {
|
||||
if (err) return res.sendStatus(500)
|
||||
log.info('emailchangeRequest', {
|
||||
newEmail: req.body.email,
|
||||
confirmation: confirmation._id,
|
||||
})
|
||||
email.emailchange(req.body.email, user.email, user.settings.language, confirmation._id)
|
||||
return saveAndReturnAccount(res, user)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
*/
|
||||
return res.status(200).send({})
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
// For people who have forgotten their password, or password-less logins
|
||||
UserController.prototype.confirmationLogin = function (req, res) {
|
||||
if (!req.body || !req.body.id) return res.sendStatus(400)
|
||||
Confirmation.findById(req.body.id, (err, confirmation) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (confirmation === null) return res.sendStatus(401)
|
||||
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)
|
||||
else {
|
||||
log.info('confirmationLogin', { user, req })
|
||||
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 }))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// CRUD basics
|
||||
|
||||
// Note that the user is already crearted (in signup)
|
||||
// we just need to active the account
|
||||
UserController.prototype.create = (req, res) => {
|
||||
if (!req.body) return res.sendStatus(400)
|
||||
if (!req.body.consent || !req.body.consent.profile) return res.status(400).send('consentRequired')
|
||||
Confirmation.findById(req.body.id, (err, confirmation) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (confirmation === null) return res.sendStatus(401)
|
||||
User.findOne({ handle: confirmation.data.handle }, (err, user) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (user === null) return res.sendStatus(401)
|
||||
user.status = 'active'
|
||||
user.consent = req.body.consent
|
||||
user.time.login = new Date()
|
||||
log.info('accountActivated', { handle: user.handle })
|
||||
let account = user.account()
|
||||
let token = getToken(account)
|
||||
user.save(function (err) {
|
||||
if (err) return res.sendStatus(400)
|
||||
Confirmation.findByIdAndDelete(req.body.id, (err, confirmation) => {
|
||||
return res.send({ account, people: {}, patterns: {}, token })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
UserController.prototype.readProfile = (req, res) => {
|
||||
User.findOne({ username: req.params.username }, (err, user) => {
|
||||
if (err) return res.sendStatus(404)
|
||||
if (user === null) return res.sendStatus(404)
|
||||
else res.send(user.profile())
|
||||
})
|
||||
}
|
||||
|
||||
function saveAndReturnAccount(res, user) {
|
||||
user.save(function (err, updatedUser) {
|
||||
if (err) {
|
||||
log.error('accountUpdateFailed', err)
|
||||
return res.sendStatus(500)
|
||||
} else return res.send({ account: updatedUser.account() })
|
||||
})
|
||||
}
|
||||
|
||||
function temporaryStoragePath(dir) {
|
||||
return path.join(config.storage, 'tmp', dir)
|
||||
}
|
||||
|
||||
UserController.prototype.isUsernameAvailable = (req, res) => {
|
||||
if (!req.user._id) return res.sendStatus(400)
|
||||
let username = req.body.username.toLowerCase().trim()
|
||||
if (username === '') return res.sendStatus(400)
|
||||
User.findOne({ username: username }, (err, user) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (user === null) return res.sendStatus(200)
|
||||
if (user._id + '' === req.user._id) return res.sendStatus(200)
|
||||
else return res.sendStatus(400)
|
||||
})
|
||||
}
|
||||
|
||||
// // Re-send activation email
|
||||
UserController.prototype.resend = (req, res) => {
|
||||
if (!req.body) return res.sendStatus(400)
|
||||
if (!req.body.email) return res.status(400).send('emailMissing')
|
||||
if (!req.body.language) return res.status(400).send('languageMissing')
|
||||
User.findOne(
|
||||
{
|
||||
ehash: ehash(req.body.email),
|
||||
},
|
||||
(err, user) => {
|
||||
if (err) return res.sendStatus(500)
|
||||
if (user === null) return res.status(404).send('noSuchUser')
|
||||
else {
|
||||
let confirmation = new Confirmation({
|
||||
type: 'signup',
|
||||
data: {
|
||||
language: req.body.language,
|
||||
email: user.email,
|
||||
handle: user.handle,
|
||||
},
|
||||
})
|
||||
confirmation.save(function (err) {
|
||||
if (err) return res.sendStatus(500)
|
||||
log.info('resendActivationRequest', {
|
||||
email: req.body.email,
|
||||
confirmation: confirmation._id,
|
||||
})
|
||||
email.signup(req.body.email, req.body.language, confirmation._id)
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
UserController.prototype.resetPassword = (req, res) => {
|
||||
if (!req.body) return res.sendStatus(400)
|
||||
User.findOne(
|
||||
{
|
||||
$or: [
|
||||
{ username: req.body.username.toLowerCase().trim() },
|
||||
{ ehash: ehash(req.body.username) },
|
||||
],
|
||||
},
|
||||
(err, user) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
if (user === null) return res.sendStatus(401)
|
||||
let confirmation = new Confirmation({
|
||||
type: 'passwordreset',
|
||||
data: {
|
||||
handle: user.handle,
|
||||
},
|
||||
})
|
||||
confirmation.save(function (err) {
|
||||
if (err) return res.sendStatus(500)
|
||||
log.info('passwordresetRequest', { user: user.handle, confirmation: confirmation._id })
|
||||
email.passwordreset(user.email, user.settings.language, confirmation._id)
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
UserController.prototype.setPassword = (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)
|
||||
User.findOne({ handle: req.body.handle }, (err, user) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (user === null) return res.sendStatus(401)
|
||||
if (confirmation.type === 'passwordreset' && confirmation.data.handle === user.handle) {
|
||||
user.password = req.body.password
|
||||
user.save(function (err) {
|
||||
log.info('passwordSet', { user, req })
|
||||
let account = user.account()
|
||||
let token = getToken(account)
|
||||
return user.updateLoginTime(() => res.send({ account, token }))
|
||||
})
|
||||
} else return res.sendStatus(401)
|
||||
})
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
UserController.prototype.confirmChangedEmail = (req, res) => {
|
||||
if (!req.body || !req.body.id || !req.user._id) return res.sendStatus(400)
|
||||
Confirmation.findById(req.body.id, (err, confirmation) => {
|
||||
if (err || confirmation === null) return res.sendStatus(401)
|
||||
User.findById(req.user._id, async (err, user) => {
|
||||
if (err || confirmation.data.handle !== user.handle) return res.sendStatus(401)
|
||||
user.ehash = ehash(confirmation.data.email.new)
|
||||
user.email = confirmation.data.email.new
|
||||
return saveAndReturnAccount(res, user)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// // Other
|
||||
UserController.prototype.patronList = (req, res) => {
|
||||
User.find({ patron: { $gte: 2 } })
|
||||
.sort('username')
|
||||
.exec((err, users) => {
|
||||
if (err || users === null) return res.sendStatus(400)
|
||||
let patrons = {
|
||||
2: [],
|
||||
4: [],
|
||||
8: [],
|
||||
}
|
||||
for (let key of Object.keys(users)) {
|
||||
let user = users[key].profile()
|
||||
patrons[user.patron].push({
|
||||
handle: user.handle,
|
||||
username: user.username,
|
||||
bio: user.bio,
|
||||
picture: user.picture,
|
||||
social: user.social,
|
||||
pictureUris: user.pictureUris,
|
||||
})
|
||||
}
|
||||
return res.send(patrons)
|
||||
})
|
||||
}
|
||||
|
||||
UserController.prototype.export = (req, res) => {
|
||||
if (!req.user._id) return res.sendStatus(400)
|
||||
User.findById(req.user._id, (err, user) => {
|
||||
if (user === null) return res.sendStatus(400)
|
||||
let dir = createTempDir()
|
||||
if (!dir) return res.sendStatus(500)
|
||||
let zip = new Zip()
|
||||
zip.file('account.json', asJson(user.export(), null, 2))
|
||||
loadAvatar(user).then((avatar) => {
|
||||
if (avatar) zip.file(user.picture, data)
|
||||
zip
|
||||
.generateAsync({
|
||||
type: 'uint8array',
|
||||
comment: 'freesewing.org',
|
||||
streamFiles: true,
|
||||
})
|
||||
.then(function (data) {
|
||||
let file = path.join(dir, 'export.zip')
|
||||
fs.writeFile(file, data, (err) => {
|
||||
log.info('dataExport', { user, req })
|
||||
return res.send({ export: uri(file) })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const loadAvatar = async (user) => {
|
||||
if (user.picture)
|
||||
await fs.readFile(path.join(user.storagePath(), user.picture), (err, data) => data)
|
||||
else return false
|
||||
}
|
||||
|
||||
// restrict processing of data, aka freeze account
|
||||
UserController.prototype.restrict = (req, res) => {
|
||||
if (!req.user._id) return res.sendStatus(400)
|
||||
User.findById(req.user._id, (err, user) => {
|
||||
if (user === null) return res.sendStatus(400)
|
||||
user.status = 'frozen'
|
||||
user.save(function (err) {
|
||||
if (err) {
|
||||
log.error('accountFreezeFailed', user)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Remove account
|
||||
UserController.prototype.remove = (req, res) => {
|
||||
if (!req.user._id) return res.sendStatus(400)
|
||||
User.findById(req.user._id, (err, user) => {
|
||||
if (user === null) return res.sendStatus(400)
|
||||
rimraf(user.storagePath(), (err) => {
|
||||
if (err) {
|
||||
console.log('rimraf', err)
|
||||
log.error('accountRemovalFailed', { err, user, req })
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
user.remove((err, usr) => {
|
||||
if (err !== null) {
|
||||
log.error('accountRemovalFailed', { err, user, req })
|
||||
return res.sendStatus(500)
|
||||
} else return res.sendStatus(200)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
const createTempDir = () => {
|
||||
let path = temporaryStoragePath(newHandle(10))
|
||||
fs.mkdir(path, { recursive: true }, (err) => {
|
||||
if (err) {
|
||||
log.error('mkdirFailed', err)
|
||||
path = false
|
||||
}
|
||||
})
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
const uri = (path) => config.static + path.substring(config.storage.length)
|
||||
|
||||
*/
|
|
@ -1,26 +1,5 @@
|
|||
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 set from 'lodash.set'
|
||||
import { UserModel } from '../models/user.mjs'
|
||||
|
||||
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'
|
||||
|
||||
export function UserController() {}
|
||||
|
||||
/*
|
||||
|
@ -62,6 +41,11 @@ UserController.prototype.login = async function (req, res, tools) {
|
|||
return User.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the account of the authenticated user (with JWT)
|
||||
*
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
UserController.prototype.whoami = async (req, res, tools) => {
|
||||
const User = new UserModel(tools)
|
||||
await User.readAsAccount({ id: req.user.uid })
|
||||
|
@ -69,433 +53,15 @@ UserController.prototype.whoami = async (req, res, tools) => {
|
|||
return User.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Updates the account of the authenticated user
|
||||
*
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
UserController.prototype.update = async (req, res, tools) => {
|
||||
const User = new UserModel(tools)
|
||||
await User.read({ id: req.user.uid })
|
||||
await User.unsafeUpdate(req.body)
|
||||
|
||||
// Commit
|
||||
//await User.update({ id: req.user.uid }, req.body)
|
||||
// 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) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (confirmation === null) return res.sendStatus(401)
|
||||
if (confirmation.data.email.new === req.body.email) {
|
||||
user.ehash = ehash(req.body.email)
|
||||
user.email = req.body.email
|
||||
return saveAndReturnAccount(res, user)
|
||||
} else return res.sendStatus(400)
|
||||
})
|
||||
} else {
|
||||
let confirmation = new Confirmation({
|
||||
type: 'emailchange',
|
||||
data: {
|
||||
handle: user.handle,
|
||||
language: user.settings.language,
|
||||
email: {
|
||||
new: req.body.email,
|
||||
current: user.email,
|
||||
},
|
||||
},
|
||||
})
|
||||
confirmation.save(function (err) {
|
||||
if (err) return res.sendStatus(500)
|
||||
log.info('emailchangeRequest', {
|
||||
newEmail: req.body.email,
|
||||
confirmation: confirmation._id,
|
||||
})
|
||||
email.emailchange(req.body.email, user.email, user.settings.language, confirmation._id)
|
||||
return saveAndReturnAccount(res, user)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
*/
|
||||
return res.status(200).send({})
|
||||
return User.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
// For people who have forgotten their password, or password-less logins
|
||||
UserController.prototype.confirmationLogin = function (req, res) {
|
||||
if (!req.body || !req.body.id) return res.sendStatus(400)
|
||||
Confirmation.findById(req.body.id, (err, confirmation) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (confirmation === null) return res.sendStatus(401)
|
||||
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)
|
||||
else {
|
||||
log.info('confirmationLogin', { user, req })
|
||||
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 }))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// CRUD basics
|
||||
|
||||
// Note that the user is already crearted (in signup)
|
||||
// we just need to active the account
|
||||
UserController.prototype.create = (req, res) => {
|
||||
if (!req.body) return res.sendStatus(400)
|
||||
if (!req.body.consent || !req.body.consent.profile) return res.status(400).send('consentRequired')
|
||||
Confirmation.findById(req.body.id, (err, confirmation) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (confirmation === null) return res.sendStatus(401)
|
||||
User.findOne({ handle: confirmation.data.handle }, (err, user) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (user === null) return res.sendStatus(401)
|
||||
user.status = 'active'
|
||||
user.consent = req.body.consent
|
||||
user.time.login = new Date()
|
||||
log.info('accountActivated', { handle: user.handle })
|
||||
let account = user.account()
|
||||
let token = getToken(account)
|
||||
user.save(function (err) {
|
||||
if (err) return res.sendStatus(400)
|
||||
Confirmation.findByIdAndDelete(req.body.id, (err, confirmation) => {
|
||||
return res.send({ account, people: {}, patterns: {}, token })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
UserController.prototype.readProfile = (req, res) => {
|
||||
User.findOne({ username: req.params.username }, (err, user) => {
|
||||
if (err) return res.sendStatus(404)
|
||||
if (user === null) return res.sendStatus(404)
|
||||
else res.send(user.profile())
|
||||
})
|
||||
}
|
||||
|
||||
function saveAndReturnAccount(res, user) {
|
||||
user.save(function (err, updatedUser) {
|
||||
if (err) {
|
||||
log.error('accountUpdateFailed', err)
|
||||
return res.sendStatus(500)
|
||||
} else return res.send({ account: updatedUser.account() })
|
||||
})
|
||||
}
|
||||
|
||||
function temporaryStoragePath(dir) {
|
||||
return path.join(config.storage, 'tmp', dir)
|
||||
}
|
||||
|
||||
UserController.prototype.isUsernameAvailable = (req, res) => {
|
||||
if (!req.user._id) return res.sendStatus(400)
|
||||
let username = req.body.username.toLowerCase().trim()
|
||||
if (username === '') return res.sendStatus(400)
|
||||
User.findOne({ username: username }, (err, user) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (user === null) return res.sendStatus(200)
|
||||
if (user._id + '' === req.user._id) return res.sendStatus(200)
|
||||
else return res.sendStatus(400)
|
||||
})
|
||||
}
|
||||
|
||||
// // Re-send activation email
|
||||
UserController.prototype.resend = (req, res) => {
|
||||
if (!req.body) return res.sendStatus(400)
|
||||
if (!req.body.email) return res.status(400).send('emailMissing')
|
||||
if (!req.body.language) return res.status(400).send('languageMissing')
|
||||
User.findOne(
|
||||
{
|
||||
ehash: ehash(req.body.email),
|
||||
},
|
||||
(err, user) => {
|
||||
if (err) return res.sendStatus(500)
|
||||
if (user === null) return res.status(404).send('noSuchUser')
|
||||
else {
|
||||
let confirmation = new Confirmation({
|
||||
type: 'signup',
|
||||
data: {
|
||||
language: req.body.language,
|
||||
email: user.email,
|
||||
handle: user.handle,
|
||||
},
|
||||
})
|
||||
confirmation.save(function (err) {
|
||||
if (err) return res.sendStatus(500)
|
||||
log.info('resendActivationRequest', {
|
||||
email: req.body.email,
|
||||
confirmation: confirmation._id,
|
||||
})
|
||||
email.signup(req.body.email, req.body.language, confirmation._id)
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
UserController.prototype.resetPassword = (req, res) => {
|
||||
if (!req.body) return res.sendStatus(400)
|
||||
User.findOne(
|
||||
{
|
||||
$or: [
|
||||
{ username: req.body.username.toLowerCase().trim() },
|
||||
{ ehash: ehash(req.body.username) },
|
||||
],
|
||||
},
|
||||
(err, user) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
if (user === null) return res.sendStatus(401)
|
||||
let confirmation = new Confirmation({
|
||||
type: 'passwordreset',
|
||||
data: {
|
||||
handle: user.handle,
|
||||
},
|
||||
})
|
||||
confirmation.save(function (err) {
|
||||
if (err) return res.sendStatus(500)
|
||||
log.info('passwordresetRequest', { user: user.handle, confirmation: confirmation._id })
|
||||
email.passwordreset(user.email, user.settings.language, confirmation._id)
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
UserController.prototype.setPassword = (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)
|
||||
User.findOne({ handle: req.body.handle }, (err, user) => {
|
||||
if (err) return res.sendStatus(400)
|
||||
if (user === null) return res.sendStatus(401)
|
||||
if (confirmation.type === 'passwordreset' && confirmation.data.handle === user.handle) {
|
||||
user.password = req.body.password
|
||||
user.save(function (err) {
|
||||
log.info('passwordSet', { user, req })
|
||||
let account = user.account()
|
||||
let token = getToken(account)
|
||||
return user.updateLoginTime(() => res.send({ account, token }))
|
||||
})
|
||||
} else return res.sendStatus(401)
|
||||
})
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
UserController.prototype.confirmChangedEmail = (req, res) => {
|
||||
if (!req.body || !req.body.id || !req.user._id) return res.sendStatus(400)
|
||||
Confirmation.findById(req.body.id, (err, confirmation) => {
|
||||
if (err || confirmation === null) return res.sendStatus(401)
|
||||
User.findById(req.user._id, async (err, user) => {
|
||||
if (err || confirmation.data.handle !== user.handle) return res.sendStatus(401)
|
||||
user.ehash = ehash(confirmation.data.email.new)
|
||||
user.email = confirmation.data.email.new
|
||||
return saveAndReturnAccount(res, user)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// // Other
|
||||
UserController.prototype.patronList = (req, res) => {
|
||||
User.find({ patron: { $gte: 2 } })
|
||||
.sort('username')
|
||||
.exec((err, users) => {
|
||||
if (err || users === null) return res.sendStatus(400)
|
||||
let patrons = {
|
||||
2: [],
|
||||
4: [],
|
||||
8: [],
|
||||
}
|
||||
for (let key of Object.keys(users)) {
|
||||
let user = users[key].profile()
|
||||
patrons[user.patron].push({
|
||||
handle: user.handle,
|
||||
username: user.username,
|
||||
bio: user.bio,
|
||||
picture: user.picture,
|
||||
social: user.social,
|
||||
pictureUris: user.pictureUris,
|
||||
})
|
||||
}
|
||||
return res.send(patrons)
|
||||
})
|
||||
}
|
||||
|
||||
UserController.prototype.export = (req, res) => {
|
||||
if (!req.user._id) return res.sendStatus(400)
|
||||
User.findById(req.user._id, (err, user) => {
|
||||
if (user === null) return res.sendStatus(400)
|
||||
let dir = createTempDir()
|
||||
if (!dir) return res.sendStatus(500)
|
||||
let zip = new Zip()
|
||||
zip.file('account.json', asJson(user.export(), null, 2))
|
||||
loadAvatar(user).then((avatar) => {
|
||||
if (avatar) zip.file(user.picture, data)
|
||||
zip
|
||||
.generateAsync({
|
||||
type: 'uint8array',
|
||||
comment: 'freesewing.org',
|
||||
streamFiles: true,
|
||||
})
|
||||
.then(function (data) {
|
||||
let file = path.join(dir, 'export.zip')
|
||||
fs.writeFile(file, data, (err) => {
|
||||
log.info('dataExport', { user, req })
|
||||
return res.send({ export: uri(file) })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const loadAvatar = async (user) => {
|
||||
if (user.picture)
|
||||
await fs.readFile(path.join(user.storagePath(), user.picture), (err, data) => data)
|
||||
else return false
|
||||
}
|
||||
|
||||
// restrict processing of data, aka freeze account
|
||||
UserController.prototype.restrict = (req, res) => {
|
||||
if (!req.user._id) return res.sendStatus(400)
|
||||
User.findById(req.user._id, (err, user) => {
|
||||
if (user === null) return res.sendStatus(400)
|
||||
user.status = 'frozen'
|
||||
user.save(function (err) {
|
||||
if (err) {
|
||||
log.error('accountFreezeFailed', user)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Remove account
|
||||
UserController.prototype.remove = (req, res) => {
|
||||
if (!req.user._id) return res.sendStatus(400)
|
||||
User.findById(req.user._id, (err, user) => {
|
||||
if (user === null) return res.sendStatus(400)
|
||||
rimraf(user.storagePath(), (err) => {
|
||||
if (err) {
|
||||
console.log('rimraf', err)
|
||||
log.error('accountRemovalFailed', { err, user, req })
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
user.remove((err, usr) => {
|
||||
if (err !== null) {
|
||||
log.error('accountRemovalFailed', { err, user, req })
|
||||
return res.sendStatus(500)
|
||||
} else return res.sendStatus(200)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
const createTempDir = () => {
|
||||
let path = temporaryStoragePath(newHandle(10))
|
||||
fs.mkdir(path, { recursive: true }, (err) => {
|
||||
if (err) {
|
||||
log.error('mkdirFailed', err)
|
||||
path = false
|
||||
}
|
||||
})
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
const uri = (path) => config.static + path.substring(config.storage.length)
|
||||
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ export const setup = async function (config, store, chai) {
|
|||
.request(config.api)
|
||||
.post('/signup')
|
||||
.send({
|
||||
...store.account,
|
||||
email: store.account.email,
|
||||
language: store.account.language,
|
||||
unittest: true,
|
||||
})
|
||||
.end((err, res) => {
|
||||
|
@ -65,6 +66,28 @@ export const setup = async function (config, store, chai) {
|
|||
done()
|
||||
})
|
||||
})
|
||||
|
||||
step(`${store.icon('user')} Should set the initial password`, (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.put('/account/jwt')
|
||||
.set('Authorization', 'Bearer ' + store.account.token)
|
||||
.send({
|
||||
password: store.account.password,
|
||||
})
|
||||
.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(store.account.email)
|
||||
expect(res.body.account.username).to.equal(store.account.username)
|
||||
expect(res.body.account.lusername).to.equal(store.account.username.toLowerCase())
|
||||
expect(typeof res.body.account.id).to.equal(`number`)
|
||||
store.token = res.body.token
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ export const userTests = async (config, store, chai) => {
|
|||
|
||||
const fields = {
|
||||
email: 'test@freesewing.dev',
|
||||
password: 'test',
|
||||
language: 'fr',
|
||||
}
|
||||
Object.keys(fields).map((key) => {
|
||||
|
@ -193,7 +192,6 @@ export const userTests = async (config, store, chai) => {
|
|||
done()
|
||||
})
|
||||
})
|
||||
|
||||
step(`${store.icon('user', 'jwt')} Should load account (jwt)`, (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue