ilp-core
Version:
ILP core module managing ledger abstraction
284 lines (219 loc) • 8.01 kB
JavaScript
'use strict'
module.exports = UserFactory
const _ = require('lodash')
const Container = require('constitute').Container
const Model = require('five-bells-shared').Model
const PersistentModelMixin = require('five-bells-shared').PersistentModelMixin
const Database = require('../lib/db')
const Validator = require('five-bells-shared/lib/validator')
const Ledger = require('../lib/ledger')
const Config = require('../lib/config')
const Utils = require('../lib/utils')
const Sequelize = require('sequelize')
const InviteFactory = require('./invite')
const ServerError = require('../errors/server-error')
const InvalidBodyError = require('../errors/invalid-body-error')
const EmailTakenError = require('../errors/email-taken-error')
UserFactory.constitute = [Database, Validator, Container, Ledger, Config, Utils]
function UserFactory (sequelize, validator, container, ledger, config, utils) {
class User extends Model {
static convertFromExternal (data) {
return data
}
static convertToExternal (data) {
delete data.password
delete data.created_at
delete data.updated_at
if (data.profile_picture && data.profile_picture.indexOf('://') === -1) {
data.profile_picture = config.data.getIn(['server', 'base_uri']) +
'/users/' + data.username + '/profilepic'
}
return data
}
static convertFromPersistent (data) {
data = _.omit(data, _.isNull)
data.identifier = utils.getWebfingerAddress(data.username)
if (data.username === config.data.getIn(['ledger', 'admin', 'user'])) {
data.isAdmin = true
}
return data
}
static convertToPersistent (data) {
return data
}
static createBodyParser () {
const Self = this
return function * (next) {
const json = this.body
const validationResult = Self.validateExternal(json)
if (validationResult.valid !== true) {
const message = validationResult.schema
? 'Body did not match schema ' + validationResult.schema
: 'Body did not pass validation'
throw new InvalidBodyError(message, validationResult.errors)
}
const model = new Self()
model.setDataExternal(json)
this.body = model
yield next
}
}
static * getAvailableUsername (username) {
const user = yield User.findOne({ where: { username } })
return user ? username + Math.floor((Math.random() * 1000) + 1) : username
}
static getVerificationCode (email) {
return config.generateSecret('verify' + email).toString('hex')
}
static getVerificationLink (username, email) {
return config.data.get(['client_host']) + '/verify/' + username + '/' + User.getVerificationCode(email)
}
static * setupAdminAccount () {
const username = config.data.getIn(['ledger', 'admin', 'user'])
let dbUser = yield this.findOne({ where: { username } })
if (!dbUser) {
// Create the admin account
dbUser = new this()
dbUser.username = username
dbUser = this.fromDatabaseModel(yield dbUser.save())
dbUser.new = true
}
// Setup ledger admin account
const ledgerAccount = yield ledger.setupAdminAccount()
return yield dbUser.appendLedgerAccount(ledgerAccount)
}
static * setupConnectorAccount () {
const ledgers = JSON.parse(config.data.getIn(['connector', 'ledgers']))
const prefix = config.data.getIn(['ledger', 'prefix'])
if (!ledgers || !ledgers[prefix]) return
const options = ledgers[prefix].options
// backwards compatibility (dec, 2016)
const derivedUsername = options.account.split('accounts/')[1]
const username = options.username || derivedUsername
const password = options.password
let dbUser = yield this.findOne({ where: { username } })
// Create the db connector account
if (!dbUser) {
dbUser = new this()
dbUser.username = username
dbUser = this.fromDatabaseModel(yield dbUser.save())
// Used in app.js for the initial funding
dbUser.new = true
}
let ledgerAccount
// Create the ledger connector account
try {
// Does the account already exist?
ledgerAccount = yield ledger.getAccount({ username, password })
} catch (err) {
// TODO does account not exist or is this a different exception?
// Create the account
ledgerAccount = yield ledger.createAccount({ username, password })
}
return yield dbUser.appendLedgerAccount(ledgerAccount)
}
* changeEmail (email, verified) {
if (this.email === email) return this
this.email = email
this.email_verified = verified || false
const validationResult = User.validateExternal({
email: email
})
// Invalid email
if (validationResult.valid !== true) {
const message = validationResult.schema
? 'Body did not match schema ' + validationResult.schema
: 'Body did not pass validation'
throw new InvalidBodyError(message)
}
try {
// TODO verification
yield this.save()
} catch (e) {
// Email is already taken by someone else
if (e.name === 'SequelizeUniqueConstraintError') {
throw new EmailTakenError('Email is already taken')
}
// Something else went wrong
throw new ServerError('Failed to change the user email')
}
return this
}
* appendLedgerAccount (ledgerUser) {
if (!ledgerUser) {
ledgerUser = yield ledger.getAccount(this, true)
}
this.balance = Math.round(ledgerUser.balance * 100) / 100
this.minimum_allowed_balance = ledgerUser.minimum_allowed_balance
return this
}
generateForgotPasswordCode (date) {
date = date || Math.floor(Date.now() / 1000)
return date + '.' + config.generateSecret(+date + this.id + this.updated_at.toString()).toString('hex')
}
generateForgotPasswordLink () {
return config.data.get(['client_host']) + '/change-password/' + this.username + '/' + this.generateForgotPasswordCode()
}
verifyForgotPasswordCode (code) {
if (!code) throw new InvalidBodyError('Missing code')
const parts = code.split('.')
const date = parts[0]
const hash = parts[1]
if (!date || !hash) throw new InvalidBodyError('Invalid code')
const currentDate = Math.floor(Date.now() / 1000)
// Code is only valid for an hour
console.log('CURRENT:', currentDate, date)
if (currentDate > date + 3600) {
// TODO should this be an invalid body error?
throw new InvalidBodyError('The code has been expired')
}
if (code !== this.generateForgotPasswordCode(date)) {
throw new InvalidBodyError('The code is invalid or has already been used')
}
}
}
User.validateExternal = validator.create('User')
PersistentModelMixin(User, sequelize, {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
username: {
type: Sequelize.STRING,
unique: true
},
email: {
type: Sequelize.STRING,
unique: true
},
email_verified: {
type: Sequelize.BOOLEAN
},
github_id: {
type: Sequelize.INTEGER,
unique: true
},
destination: Sequelize.STRING,
profile_picture: Sequelize.STRING,
name: {
type: Sequelize.STRING
},
phone: Sequelize.STRING,
address1: Sequelize.STRING,
address2: Sequelize.STRING,
city: Sequelize.STRING,
region: Sequelize.STRING,
country: Sequelize.STRING,
zip_code: Sequelize.STRING
})
container.schedulePostConstructor((Invite) => {
User.DbModel.belongsTo(Invite.DbModel, {
foreignKey: {
name: 'invite_code'
},
constraints: false
})
}, [ InviteFactory ])
return User
}