first commit
This commit is contained in:
Коммит
28bd196ee1
|
@ -0,0 +1 @@
|
|||
node_modules
|
|
@ -0,0 +1,213 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var path = require('path')
|
||||
var fs = require('fs')
|
||||
var mysql = require('mysql')
|
||||
var P = require('../promise.js')
|
||||
var options = require('../config')
|
||||
var log = require('../log')(options.logLevel, 'db-patcher')
|
||||
|
||||
var schemaDir = path.join(__dirname, '..', 'db', 'schema')
|
||||
var patches = {}
|
||||
var files = fs.readdirSync(schemaDir)
|
||||
files.forEach(function(filename) {
|
||||
var from, to
|
||||
var m = filename.match(/^patch-(\d+)-(\d+)\.sql$/)
|
||||
if (m) {
|
||||
from = parseInt(m[1], 10)
|
||||
to = parseInt(m[2], 10)
|
||||
patches[from] = patches[from] || {}
|
||||
patches[from][to] = fs.readFileSync(path.join(schemaDir, filename), { encoding: 'utf8'})
|
||||
}
|
||||
else {
|
||||
console.warn("Startup error: Unknown file in schema/ directory - '%s'", filename)
|
||||
process.exit(2)
|
||||
}
|
||||
})
|
||||
|
||||
// To run any patches we need to switch multipleStatements on
|
||||
options.master.multipleStatements = true
|
||||
|
||||
// when creating the database, we need to connect without a database name
|
||||
var database = options.master.database
|
||||
delete options.master.database
|
||||
|
||||
var client = mysql.createConnection(options.master)
|
||||
|
||||
createDatabase()
|
||||
.then(changeUser)
|
||||
.then(checkDbMetadataExists)
|
||||
.then(readDbPatchLevel)
|
||||
.then(patchToRequiredLevel)
|
||||
.then(closeAndReconnect)
|
||||
.done(
|
||||
function() {
|
||||
log.info('Patching complete')
|
||||
},
|
||||
function(err) {
|
||||
log.fatal(err)
|
||||
process.exit(2)
|
||||
}
|
||||
)
|
||||
|
||||
// helper functions
|
||||
function createDatabase() {
|
||||
var d = P.defer()
|
||||
log.trace( { op: 'MySql.createSchema:CreateDatabase' } )
|
||||
client.query(
|
||||
'CREATE DATABASE IF NOT EXISTS ' + database + ' CHARACTER SET utf8 COLLATE utf8_unicode_ci',
|
||||
function (err) {
|
||||
if (err) {
|
||||
log.error({ op: 'MySql.createSchema:CreateDatabase', err: err.message })
|
||||
return d.reject(err)
|
||||
}
|
||||
d.resolve()
|
||||
}
|
||||
)
|
||||
return d.promise
|
||||
}
|
||||
|
||||
function changeUser() {
|
||||
var d = P.defer()
|
||||
log.trace( { op: 'MySql.createSchema:ChangeUser' } )
|
||||
client.changeUser(
|
||||
{
|
||||
user : options.master.user,
|
||||
password : options.master.password,
|
||||
database : database
|
||||
},
|
||||
function (err) {
|
||||
if (err) {
|
||||
log.error({ op: 'MySql.createSchema:ChangeUser', err: err.message })
|
||||
return d.reject(err)
|
||||
}
|
||||
d.resolve()
|
||||
}
|
||||
)
|
||||
return d.promise
|
||||
}
|
||||
|
||||
function checkDbMetadataExists() {
|
||||
log.trace( { op: 'MySql.createSchema:CheckDbMetadataExists' } )
|
||||
var d = P.defer()
|
||||
var query = "SELECT COUNT(*) AS count FROM information_schema.TABLES WHERE table_schema = ? AND table_name = 'dbMetadata'"
|
||||
client.query(
|
||||
query,
|
||||
[ database ],
|
||||
function (err, result) {
|
||||
if (err) {
|
||||
log.trace( { op: 'MySql.createSchema:MakingTheSchema', err: err.message } )
|
||||
return d.reject(err)
|
||||
}
|
||||
d.resolve(result[0].count === 0 ? false : true)
|
||||
}
|
||||
)
|
||||
return d.promise
|
||||
}
|
||||
|
||||
function readDbPatchLevel(dbMetadataExists) {
|
||||
log.trace( { op: 'MySql.createSchema:ReadDbPatchLevel' } )
|
||||
if ( dbMetadataExists === false ) {
|
||||
// the table doesn't exist, so start at patch level 0
|
||||
return P.resolve(0)
|
||||
}
|
||||
|
||||
// find out what patch level the database is currently at
|
||||
var d = P.defer()
|
||||
var query = "SELECT value FROM dbMetadata WHERE name = ?"
|
||||
client.query(
|
||||
query,
|
||||
[ options.patchKey ],
|
||||
function(err, result) {
|
||||
if (err) {
|
||||
log.trace( { op: 'MySql.createSchema:ReadDbPatchLevel', err: err.message } )
|
||||
return d.reject(err)
|
||||
}
|
||||
// convert the patch level from a string to a number
|
||||
return d.resolve(+result[0].value)
|
||||
}
|
||||
)
|
||||
return d.promise
|
||||
}
|
||||
|
||||
function patchToRequiredLevel(currentPatchLevel) {
|
||||
log.trace( { op: 'MySql.createSchema:PatchToRequiredLevel' } )
|
||||
|
||||
// if we don't need any patches
|
||||
if ( options.patchLevel === currentPatchLevel ) {
|
||||
log.trace( { op: 'MySql.createSchema:PatchToRequiredLevel', patch: 'No patch required' } )
|
||||
return P.resolve()
|
||||
}
|
||||
|
||||
// We don't want any reverse patches to be automatically applied, so
|
||||
// just emit a warning and carry on.
|
||||
if ( options.patchLevel < currentPatchLevel ) {
|
||||
log.warn( { op: 'MySql.createSchema:PatchToRequiredLevel', err: 'Reverse patch required - must be done manually' } )
|
||||
return P.resolve()
|
||||
}
|
||||
|
||||
log.trace({
|
||||
op: 'MySql.createSchema:PatchToRequiredLevel',
|
||||
msg1: 'Patching from ' + currentPatchLevel + ' to ' + options.patchLevel
|
||||
})
|
||||
|
||||
var promise = P.resolve()
|
||||
var patchesToApply = []
|
||||
|
||||
// First, loop through all the patches we need to apply
|
||||
// to make sure they exist.
|
||||
while ( currentPatchLevel < options.patchLevel ) {
|
||||
// check that this patch exists
|
||||
if ( !patches[currentPatchLevel][currentPatchLevel+1] ) {
|
||||
log.fatal({
|
||||
op: 'MySql.createSchema:PatchToRequiredLevel',
|
||||
err: 'Patch from level ' + currentPatchLevel + ' to ' + (currentPatchLevel+1) + ' does not exist'
|
||||
});
|
||||
process.exit(2)
|
||||
}
|
||||
patchesToApply.push({
|
||||
sql : patches[currentPatchLevel][currentPatchLevel+1],
|
||||
from : currentPatchLevel,
|
||||
to : currentPatchLevel+1,
|
||||
})
|
||||
currentPatchLevel += 1
|
||||
}
|
||||
|
||||
// now apply each patch
|
||||
patchesToApply.forEach(function(patch) {
|
||||
promise = promise.then(function() {
|
||||
var d = P.defer()
|
||||
log.trace({ op: 'MySql.createSchema:PatchToRequiredLevel', msg1: 'Updating DB for patch ' + patch.from + ' to ' + patch.to })
|
||||
client.query(
|
||||
patch.sql,
|
||||
function(err) {
|
||||
if (err) return d.reject(err)
|
||||
d.resolve()
|
||||
}
|
||||
)
|
||||
return d.promise
|
||||
})
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
function closeAndReconnect() {
|
||||
var d = P.defer()
|
||||
log.trace( { op: 'MySql.createSchema:CloseAndReconnect' } )
|
||||
client.end(
|
||||
function (err) {
|
||||
if (err) {
|
||||
log.error({ op: 'MySql.createSchema:Closed', err: err.message })
|
||||
return d.reject(err)
|
||||
}
|
||||
|
||||
// create the mysql class
|
||||
log.trace( { op: 'MySql.createSchema:ResolvingWithNewClient' } )
|
||||
d.resolve('ok')
|
||||
}
|
||||
)
|
||||
return d.promise
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var restify = require('restify')
|
||||
var error = require('../error')
|
||||
var bufferize = require('../bufferize')
|
||||
var config = require('../config')
|
||||
var log = require('../log')(config.logLevel, 'db-api')
|
||||
var DB = require('../db/mysql')(log, error)
|
||||
|
||||
function startServer(db) {
|
||||
|
||||
function reply(fn) {
|
||||
return function (req, res, next) {
|
||||
log.trace(
|
||||
{
|
||||
op: 'request',
|
||||
route: req.route.name,
|
||||
id: req.params && req.params.id
|
||||
}
|
||||
)
|
||||
fn.call(db, req.params.id, req.body)
|
||||
.then(
|
||||
function (result) {
|
||||
log.info({ op: 'request.summary' })
|
||||
if (Array.isArray(result)) {
|
||||
res.send(result.map(bufferize.unbuffer))
|
||||
}
|
||||
else {
|
||||
res.send(bufferize.unbuffer(result || {}))
|
||||
}
|
||||
},
|
||||
function (err) {
|
||||
log.error({ op: 'request.summary', err: err })
|
||||
res.send(err.code || 500, err)
|
||||
}
|
||||
)
|
||||
.done(next, next)
|
||||
}
|
||||
}
|
||||
var api = restify.createServer()
|
||||
api.use(restify.bodyParser())
|
||||
api.use(bufferize.bufferizeRequest)
|
||||
|
||||
api.get('/account/:id', reply(db.account))
|
||||
api.del('/account/:id', reply(db.deleteAccount))
|
||||
api.put('/account/:id', reply(db.createAccount))
|
||||
api.get('/account/:id/devices', reply(db.accountDevices))
|
||||
api.post('/account/:id/reset', reply(db.resetAccount))
|
||||
api.post('/account/:id/verifyEmail', reply(db.verifyEmail))
|
||||
|
||||
api.get('/sessionToken/:id', reply(db.sessionToken))
|
||||
api.del('/sessionToken/:id', reply(db.deleteSessionToken))
|
||||
api.put('/sessionToken/:id', reply(db.createSessionToken))
|
||||
|
||||
api.get('/keyFetchToken/:id', reply(db.keyFetchToken))
|
||||
api.del('/keyFetchToken/:id', reply(db.deleteKeyFetchToken))
|
||||
api.put('/keyFetchToken/:id', reply(db.createKeyFetchToken))
|
||||
|
||||
api.get('/accountResetToken/:id', reply(db.accountResetToken))
|
||||
api.del('/accountResetToken/:id', reply(db.deleteAccountResetToken))
|
||||
api.put('/accountResetToken/:id', reply(db.createAccountResetToken))
|
||||
|
||||
api.get('/passwordChangeToken/:id', reply(db.passwordChangeToken))
|
||||
api.del('/passwordChangeToken/:id', reply(db.deletePasswordChangeToken))
|
||||
api.put('/passwordChangeToken/:id', reply(db.createPasswordChangeToken))
|
||||
|
||||
api.get('/passwordForgotToken/:id', reply(db.passwordForgotToken))
|
||||
api.del('/passwordForgotToken/:id', reply(db.deletePasswordForgotToken))
|
||||
api.put('/passwordForgotToken/:id', reply(db.createPasswordForgotToken))
|
||||
api.post('/passwordForgotToken/:id/update', reply(db.updatePasswordForgotToken))
|
||||
api.post('/passwordForgotToken/:id/verified', reply(db.forgotPasswordVerified))
|
||||
|
||||
api.get('/emailRecord/:id', reply(db.emailRecord))
|
||||
api.head('/emailRecord/:id', reply(db.accountExists))
|
||||
|
||||
api.get('/__heartbeat__', reply(db.ping))
|
||||
|
||||
api.listen(
|
||||
config.port,
|
||||
function () {
|
||||
log.info({ op: 'listening', port: config.port })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
DB.connect(config).done(startServer)
|
|
@ -0,0 +1,39 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/
|
||||
|
||||
function unbuffer(object) {
|
||||
var keys = Object.keys(object)
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var x = object[keys[i]]
|
||||
if (Buffer.isBuffer(x)) {
|
||||
object[keys[i]] = x.toString('hex')
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
function bufferize(object) {
|
||||
var keys = Object.keys(object)
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var x = object[keys[i]]
|
||||
if (typeof(x) === 'string' && HEX_STRING.test(x)) {
|
||||
object[keys[i]] = Buffer(x, 'hex')
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
function bufferizeRequest(req, res, next) {
|
||||
if (req.body) { req.body = bufferize(req.body) }
|
||||
if (req.params) { req.params = bufferize(req.params) }
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
unbuffer: unbuffer,
|
||||
bufferize: bufferize,
|
||||
bufferizeRequest: bufferizeRequest
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = require('rc')(
|
||||
'fxa_db',
|
||||
{
|
||||
logLevel: 'trace',
|
||||
port: 8000,
|
||||
patchKey: 'schema-patch-level',
|
||||
patchLevel: 2,
|
||||
master: {
|
||||
user: 'root',
|
||||
password: '',
|
||||
database: 'fxa',
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
connectionLimit: 10,
|
||||
waitForConnections: true,
|
||||
queueLimit: 100
|
||||
},
|
||||
slave: {
|
||||
user: 'root',
|
||||
password: '',
|
||||
database: 'fxa',
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
connectionLimit: 10,
|
||||
waitForConnections: true,
|
||||
queueLimit: 100
|
||||
}
|
||||
}
|
||||
)
|
|
@ -0,0 +1,574 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var mysql = require('mysql')
|
||||
var P = require('../promise')
|
||||
|
||||
module.exports = function (log, error) {
|
||||
|
||||
// http://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
|
||||
var LOCK_ERRNOS = [ 1205, 1206, 1213, 1689 ]
|
||||
|
||||
// make a pool of connections that we can draw from
|
||||
function MySql(options) {
|
||||
|
||||
// poolCluster will remove the pool after `removeNodeErrorCount` errors.
|
||||
// We don't ever want to remove a pool because we only have one pool
|
||||
// for writing and reading each. Connection errors are mostly out of our
|
||||
// control for automatic recovery so monitoring of 503s is critical.
|
||||
// Since `removeNodeErrorCount` is Infinity `canRetry` must be false
|
||||
// to prevent inifinite retry attempts.
|
||||
this.poolCluster = mysql.createPoolCluster(
|
||||
{
|
||||
removeNodeErrorCount: Infinity,
|
||||
canRetry: false
|
||||
}
|
||||
)
|
||||
|
||||
// Use separate pools for master and slave connections.
|
||||
this.poolCluster.add('MASTER', options.master)
|
||||
this.poolCluster.add('SLAVE', options.slave)
|
||||
this.getClusterConnection = P.promisify(this.poolCluster.getConnection, this.poolCluster)
|
||||
|
||||
|
||||
this.statInterval = setInterval(
|
||||
reportStats.bind(this),
|
||||
options.statInterval || 15000
|
||||
)
|
||||
this.statInterval.unref()
|
||||
}
|
||||
|
||||
function reportStats() {
|
||||
var nodes = Object.keys(this.poolCluster._nodes).map(
|
||||
function (name) {
|
||||
return this.poolCluster._nodes[name]
|
||||
}.bind(this)
|
||||
)
|
||||
var stats = nodes.reduce(
|
||||
function (totals, node) {
|
||||
totals.errors += node.errorCount
|
||||
totals.connections += node.pool._allConnections.length
|
||||
totals.queue += node.pool._connectionQueue.length
|
||||
totals.free += node.pool._freeConnections.length
|
||||
return totals
|
||||
},
|
||||
{
|
||||
stat: 'mysql',
|
||||
errors: 0,
|
||||
connections: 0,
|
||||
queue: 0,
|
||||
free: 0
|
||||
}
|
||||
)
|
||||
log.stat(stats)
|
||||
}
|
||||
|
||||
// this will be called from outside this file
|
||||
MySql.connect = function(options) {
|
||||
// check that the database patch level is what we expect (or one above)
|
||||
var mysql = new MySql(options)
|
||||
|
||||
return mysql.readOne("SELECT value FROM dbMetadata WHERE name = ?", options.patchKey)
|
||||
.then(
|
||||
function (result) {
|
||||
var patchLevel = +result.value
|
||||
if ( patchLevel !== options.patchLevel && patchLevel !== options.patchLevel + 1 ) {
|
||||
throw new Error('dbIncorrectPatchLevel')
|
||||
}
|
||||
log.trace({
|
||||
op: 'MySql.connect',
|
||||
patchLevel: patchLevel,
|
||||
patchLevelRequired: options.patchLevel
|
||||
})
|
||||
return mysql
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
MySql.prototype.close = function () {
|
||||
this.poolCluster.end()
|
||||
clearInterval(this.statInterval)
|
||||
return P.resolve()
|
||||
}
|
||||
|
||||
MySql.prototype.ping = function () {
|
||||
return this.getConnection('MASTER')
|
||||
.then(
|
||||
function(connection) {
|
||||
var d = P.defer()
|
||||
connection.ping(
|
||||
function (err) {
|
||||
connection.release()
|
||||
return err ? d.reject(err) : d.resolve()
|
||||
}
|
||||
)
|
||||
return d.promise
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// CREATE
|
||||
var CREATE_ACCOUNT = 'INSERT INTO accounts' +
|
||||
' (uid, normalizedEmail, email, emailCode, emailVerified, kA, wrapWrapKb,' +
|
||||
' authSalt, verifierVersion, verifyHash, verifierSetAt, createdAt)' +
|
||||
' VALUES (?, LOWER(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
|
||||
MySql.prototype.createAccount = function (uid, data) {
|
||||
data.normalizedEmail = data.email
|
||||
data.createdAt = data.verifierSetAt = Date.now()
|
||||
|
||||
return this.write(
|
||||
CREATE_ACCOUNT,
|
||||
[
|
||||
uid,
|
||||
data.normalizedEmail,
|
||||
data.email,
|
||||
data.emailCode,
|
||||
data.emailVerified,
|
||||
data.kA,
|
||||
data.wrapWrapKb,
|
||||
data.authSalt,
|
||||
data.verifierVersion,
|
||||
data.verifyHash,
|
||||
data.verifierSetAt,
|
||||
data.createdAt
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
var CREATE_SESSION_TOKEN = 'INSERT INTO sessionTokens' +
|
||||
' (tokenId, tokenData, uid, createdAt)' +
|
||||
' VALUES (?, ?, ?, ?)'
|
||||
|
||||
MySql.prototype.createSessionToken = function (tokenId, sessionToken) {
|
||||
return this.write(
|
||||
CREATE_SESSION_TOKEN,
|
||||
[
|
||||
tokenId,
|
||||
sessionToken.data,
|
||||
sessionToken.uid,
|
||||
sessionToken.createdAt
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
var CREATE_KEY_FETCH_TOKEN = 'INSERT INTO keyFetchTokens' +
|
||||
' (tokenId, authKey, uid, keyBundle, createdAt)' +
|
||||
' VALUES (?, ?, ?, ?, ?)'
|
||||
|
||||
MySql.prototype.createKeyFetchToken = function (tokenId, keyFetchToken) {
|
||||
return this.write(
|
||||
CREATE_KEY_FETCH_TOKEN,
|
||||
[
|
||||
tokenId,
|
||||
keyFetchToken.authKey,
|
||||
keyFetchToken.uid,
|
||||
keyFetchToken.keyBundle,
|
||||
keyFetchToken.createdAt
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
var CREATE_ACCOUNT_RESET_TOKEN = 'REPLACE INTO accountResetTokens' +
|
||||
' (tokenId, tokenData, uid, createdAt)' +
|
||||
' VALUES (?, ?, ?, ?)'
|
||||
|
||||
MySql.prototype.createAccountResetToken = function (tokenId, accountResetToken) {
|
||||
return this.write(
|
||||
CREATE_ACCOUNT_RESET_TOKEN,
|
||||
[
|
||||
tokenId,
|
||||
accountResetToken.data,
|
||||
accountResetToken.uid,
|
||||
accountResetToken.createdAt
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
var CREATE_PASSWORD_FORGOT_TOKEN = 'REPLACE INTO passwordForgotTokens' +
|
||||
' (tokenId, tokenData, uid, passCode, createdAt, tries)' +
|
||||
' VALUES (?, ?, ?, ?, ?, ?)'
|
||||
|
||||
MySql.prototype.createPasswordForgotToken = function (tokenId, passwordForgotToken) {
|
||||
return this.write(
|
||||
CREATE_PASSWORD_FORGOT_TOKEN,
|
||||
[
|
||||
tokenId,
|
||||
passwordForgotToken.data,
|
||||
passwordForgotToken.uid,
|
||||
passwordForgotToken.passCode,
|
||||
passwordForgotToken.createdAt,
|
||||
passwordForgotToken.tries
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
var CREATE_PASSWORD_CHANGE_TOKEN = 'REPLACE INTO passwordChangeTokens' +
|
||||
' (tokenId, tokenData, uid, createdAt)' +
|
||||
' VALUES (?, ?, ?, ?)'
|
||||
|
||||
MySql.prototype.createPasswordChangeToken = function (tokenId, passwordChangeToken) {
|
||||
return this.write(
|
||||
CREATE_PASSWORD_CHANGE_TOKEN,
|
||||
[
|
||||
tokenId,
|
||||
passwordChangeToken.data,
|
||||
passwordChangeToken.uid,
|
||||
passwordChangeToken.createdAt
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
// READ
|
||||
|
||||
var ACCOUNT_EXISTS = 'SELECT uid FROM accounts WHERE normalizedEmail = LOWER(?)'
|
||||
|
||||
MySql.prototype.accountExists = function (email) {
|
||||
return this.readOne(ACCOUNT_EXISTS, email)
|
||||
}
|
||||
|
||||
var ACCOUNT_DEVICES = 'SELECT tokenId FROM sessionTokens WHERE uid = ?'
|
||||
|
||||
MySql.prototype.accountDevices = function (uid) {
|
||||
return this.read(ACCOUNT_DEVICES, uid)
|
||||
}
|
||||
|
||||
var SESSION_TOKEN = 'SELECT t.tokenData, t.uid, t.createdAt,' +
|
||||
' a.emailVerified, a.email, a.emailCode, a.verifierSetAt' +
|
||||
' FROM sessionTokens t, accounts a' +
|
||||
' WHERE t.tokenId = ? AND t.uid = a.uid'
|
||||
|
||||
MySql.prototype.sessionToken = function (id) {
|
||||
return this.readOne(SESSION_TOKEN, id)
|
||||
}
|
||||
|
||||
var KEY_FETCH_TOKEN = 'SELECT t.authKey, t.uid, t.keyBundle, t.createdAt,' +
|
||||
' a.emailVerified, a.verifierSetAt' +
|
||||
' FROM keyFetchTokens t, accounts a' +
|
||||
' WHERE t.tokenId = ? AND t.uid = a.uid'
|
||||
|
||||
MySql.prototype.keyFetchToken = function (id) {
|
||||
return this.readOne(KEY_FETCH_TOKEN, id)
|
||||
}
|
||||
|
||||
var ACCOUNT_RESET_TOKEN = 'SELECT t.uid, t.tokenData, t.createdAt,' +
|
||||
' a.verifierSetAt' +
|
||||
' FROM accountResetTokens t, accounts a' +
|
||||
' WHERE t.tokenId = ? AND t.uid = a.uid'
|
||||
|
||||
MySql.prototype.accountResetToken = function (id) {
|
||||
return this.readOne(ACCOUNT_RESET_TOKEN, id)
|
||||
}
|
||||
|
||||
var PASSWORD_FORGOT_TOKEN = 'SELECT t.tokenData, t.uid, t.createdAt,' +
|
||||
' t.passCode, t.tries, a.email, a.verifierSetAt' +
|
||||
' FROM passwordForgotTokens t, accounts a' +
|
||||
' WHERE t.tokenId = ? AND t.uid = a.uid'
|
||||
|
||||
MySql.prototype.passwordForgotToken = function (id) {
|
||||
return this.readOne(PASSWORD_FORGOT_TOKEN, id)
|
||||
}
|
||||
|
||||
var PASSWORD_CHANGE_TOKEN = 'SELECT t.tokenData, t.uid, t.createdAt, a.verifierSetAt' +
|
||||
' FROM passwordChangeTokens t, accounts a' +
|
||||
' WHERE t.tokenId = ? AND t.uid = a.uid'
|
||||
|
||||
MySql.prototype.passwordChangeToken = function (id) {
|
||||
return this.readOne(PASSWORD_CHANGE_TOKEN, id)
|
||||
}
|
||||
|
||||
var EMAIL_RECORD = 'SELECT uid, email, normalizedEmail, emailVerified, emailCode,' +
|
||||
' kA, wrapWrapKb, verifierVersion, verifyHash, authSalt, verifierSetAt' +
|
||||
' FROM accounts' +
|
||||
' WHERE normalizedEmail = LOWER(?)'
|
||||
|
||||
MySql.prototype.emailRecord = function (email) {
|
||||
return this.readOne(EMAIL_RECORD, email)
|
||||
}
|
||||
|
||||
var ACCOUNT = 'SELECT uid, email, normalizedEmail, emailCode, emailVerified, kA,' +
|
||||
' wrapWrapKb, verifierVersion, verifyHash, authSalt, verifierSetAt, createdAt' +
|
||||
' FROM accounts WHERE uid = ?'
|
||||
|
||||
MySql.prototype.account = function (uid) {
|
||||
return this.readOne(ACCOUNT, uid)
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
|
||||
var UPDATE_PASSWORD_FORGOT_TOKEN = 'UPDATE passwordForgotTokens' +
|
||||
' SET tries = ? WHERE tokenId = ?'
|
||||
|
||||
MySql.prototype.updatePasswordForgotToken = function (tokenId, token) {
|
||||
return this.write(UPDATE_PASSWORD_FORGOT_TOKEN, [token.tries, tokenId])
|
||||
}
|
||||
|
||||
// DELETE
|
||||
|
||||
MySql.prototype.deleteAccount = function (uid) {
|
||||
return this.transaction(
|
||||
function (connection) {
|
||||
var tables = [
|
||||
'sessionTokens',
|
||||
'keyFetchTokens',
|
||||
'accountResetTokens',
|
||||
'passwordChangeTokens',
|
||||
'passwordForgotTokens',
|
||||
'accounts'
|
||||
]
|
||||
var queries = deleteFromTablesWhereUid(connection, tables, uid)
|
||||
return P.all(queries)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var DELETE_SESSION_TOKEN = 'DELETE FROM sessionTokens WHERE tokenId = ?'
|
||||
|
||||
MySql.prototype.deleteSessionToken = function (tokenId) {
|
||||
return this.write(DELETE_SESSION_TOKEN, [tokenId])
|
||||
}
|
||||
|
||||
var DELETE_KEY_FETCH_TOKEN = 'DELETE FROM keyFetchTokens WHERE tokenId = ?'
|
||||
|
||||
MySql.prototype.deleteKeyFetchToken = function (tokenId) {
|
||||
return this.write(DELETE_KEY_FETCH_TOKEN, [tokenId])
|
||||
}
|
||||
|
||||
var DELETE_ACCOUNT_RESET_TOKEN = 'DELETE FROM accountResetTokens WHERE tokenId = ?'
|
||||
|
||||
MySql.prototype.deleteAccountResetToken = function (tokenId) {
|
||||
return this.write(DELETE_ACCOUNT_RESET_TOKEN, [tokenId])
|
||||
}
|
||||
|
||||
var DELETE_PASSWORD_FORGOT_TOKEN = 'DELETE FROM passwordForgotTokens WHERE tokenId = ?'
|
||||
|
||||
MySql.prototype.deletePasswordForgotToken = function (tokenId) {
|
||||
return this.write(DELETE_PASSWORD_FORGOT_TOKEN, [tokenId])
|
||||
}
|
||||
|
||||
var DELETE_PASSWORD_CHANGE_TOKEN = 'DELETE FROM passwordChangeTokens WHERE tokenId = ?'
|
||||
|
||||
MySql.prototype.deletePasswordChangeToken = function (tokenId) {
|
||||
return this.write(DELETE_PASSWORD_CHANGE_TOKEN, [tokenId])
|
||||
}
|
||||
|
||||
// BATCH
|
||||
|
||||
var RESET_ACCOUNT = 'UPDATE accounts' +
|
||||
' SET verifyHash = ?, authSalt = ?, wrapWrapKb = ?, verifierSetAt = ?,' +
|
||||
' verifierVersion = ?' +
|
||||
' WHERE uid = ?'
|
||||
|
||||
MySql.prototype.resetAccount = function (uid, data) {
|
||||
return this.transaction(
|
||||
function (connection) {
|
||||
var tables = [
|
||||
'sessionTokens',
|
||||
'keyFetchTokens',
|
||||
'accountResetTokens',
|
||||
'passwordChangeTokens',
|
||||
'passwordForgotTokens'
|
||||
]
|
||||
var queries = deleteFromTablesWhereUid(connection, tables, uid)
|
||||
queries.push(
|
||||
query(
|
||||
connection,
|
||||
RESET_ACCOUNT,
|
||||
[
|
||||
data.verifyHash,
|
||||
data.authSalt,
|
||||
data.wrapWrapKb,
|
||||
Date.now(),
|
||||
data.verifierVersion,
|
||||
uid
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
return P.all(queries)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var VERIFY_EMAIL = 'UPDATE accounts SET emailVerified = true WHERE uid = ?'
|
||||
|
||||
MySql.prototype.verifyEmail = function (uid) {
|
||||
return this.write(VERIFY_EMAIL, [uid])
|
||||
}
|
||||
|
||||
MySql.prototype.forgotPasswordVerified = function (tokenId, accountResetToken) {
|
||||
return this.transaction(
|
||||
function (connection) {
|
||||
return P.all([
|
||||
query(
|
||||
connection,
|
||||
DELETE_PASSWORD_FORGOT_TOKEN,
|
||||
[tokenId]
|
||||
),
|
||||
query(
|
||||
connection,
|
||||
CREATE_ACCOUNT_RESET_TOKEN,
|
||||
[
|
||||
accountResetToken.tokenId,
|
||||
accountResetToken.data,
|
||||
accountResetToken.uid,
|
||||
accountResetToken.createdAt
|
||||
]
|
||||
)
|
||||
])
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
MySql.prototype.singleQuery = function (poolName, sql, params) {
|
||||
return this.getConnection(poolName)
|
||||
.then(
|
||||
function (connection) {
|
||||
return query(connection, sql, params)
|
||||
.then(
|
||||
function (result) {
|
||||
connection.release()
|
||||
return result
|
||||
},
|
||||
function (err) {
|
||||
connection.release()
|
||||
throw err
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
MySql.prototype.transaction = function (fn) {
|
||||
return retryable(
|
||||
function () {
|
||||
return this.getConnection('MASTER')
|
||||
.then(
|
||||
function (connection) {
|
||||
return query(connection, 'BEGIN')
|
||||
.then(
|
||||
function () {
|
||||
return fn(connection)
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function (result) {
|
||||
return query(connection, 'COMMIT')
|
||||
.then(function () { return result })
|
||||
}
|
||||
)
|
||||
.catch(
|
||||
function (err) {
|
||||
log.error({ op: 'MySql.transaction', err: err })
|
||||
return query(connection, 'ROLLBACK')
|
||||
.then(function () { throw err })
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function (result) {
|
||||
connection.release()
|
||||
return result
|
||||
},
|
||||
function (err) {
|
||||
connection.release()
|
||||
throw err
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}.bind(this),
|
||||
LOCK_ERRNOS
|
||||
)
|
||||
.catch(
|
||||
function (err) {
|
||||
throw error.wrap(err)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
MySql.prototype.readOne = function (sql, param) {
|
||||
return this.read(sql, param).then(firstResult)
|
||||
}
|
||||
|
||||
MySql.prototype.read = function (sql, param) {
|
||||
return this.singleQuery('SLAVE*', sql, [param])
|
||||
.catch(
|
||||
function (err) {
|
||||
log.error({ op: 'MySql.read', sql: sql, id: param, err: err })
|
||||
throw error.wrap(err)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
MySql.prototype.write = function (sql, params) {
|
||||
return this.singleQuery('MASTER', sql, params)
|
||||
.then(
|
||||
function (result) {
|
||||
log.trace({ op: 'MySql.write', sql: sql, result: result })
|
||||
return {}
|
||||
},
|
||||
function (err) {
|
||||
log.error({ op: 'MySql.write', sql: sql, err: err })
|
||||
if (err.errno === 1062) {
|
||||
err = error.duplicate()
|
||||
}
|
||||
else {
|
||||
err = error.wrap(err)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
MySql.prototype.getConnection = function (name) {
|
||||
return retryable(
|
||||
this.getClusterConnection,
|
||||
[1040, 'ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET']
|
||||
)
|
||||
}
|
||||
|
||||
function firstResult(results) {
|
||||
if (!results.length) { throw error.notFound() }
|
||||
return results[0]
|
||||
}
|
||||
|
||||
function query(connection, sql, params) {
|
||||
var d = P.defer()
|
||||
connection.query(
|
||||
sql,
|
||||
params || [],
|
||||
function (err, results) {
|
||||
if (err) { return d.reject(err) }
|
||||
d.resolve(results)
|
||||
}
|
||||
)
|
||||
return d.promise
|
||||
}
|
||||
|
||||
function deleteFromTablesWhereUid(connection, tables, uid) {
|
||||
return tables.map(
|
||||
function (table) {
|
||||
return query(connection, 'DELETE FROM ' + table + ' WHERE uid = ?', uid)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function retryable(fn, errnos) {
|
||||
function success(result) {
|
||||
return result
|
||||
}
|
||||
function failure(err) {
|
||||
log.error({ op: 'MySql.retryable', err: err })
|
||||
if (errnos.indexOf(err.errno) === -1) {
|
||||
throw err
|
||||
}
|
||||
return fn()
|
||||
}
|
||||
return fn().then(success, failure)
|
||||
}
|
||||
|
||||
return MySql
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
-- Create the 'dbMetadata' table.
|
||||
-- Note: This should be the only thing in this initial patch.
|
||||
|
||||
CREATE TABLE dbMetadata (
|
||||
name VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
value VARCHAR(255) NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO dbMetadata SET name = 'schema-patch-level', value = '1';
|
|
@ -0,0 +1,2 @@
|
|||
-- -- drop the dbMetadata table
|
||||
-- DROP TABLE dbMetadata;
|
|
@ -0,0 +1,59 @@
|
|||
-- create all tables
|
||||
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
uid BINARY(16) PRIMARY KEY,
|
||||
normalizedEmail VARCHAR(255) NOT NULL UNIQUE KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
emailCode BINARY(16) NOT NULL,
|
||||
emailVerified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
kA BINARY(32) NOT NULL,
|
||||
wrapWrapKb BINARY(32) NOT NULL,
|
||||
authSalt BINARY(32) NOT NULL,
|
||||
verifyHash BINARY(32) NOT NULL,
|
||||
verifierVersion TINYINT UNSIGNED NOT NULL,
|
||||
verifierSetAt BIGINT UNSIGNED NOT NULL,
|
||||
createdAt BIGINT UNSIGNED NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessionTokens (
|
||||
tokenId BINARY(32) PRIMARY KEY,
|
||||
tokenData BINARY(32) NOT NULL,
|
||||
uid BINARY(16) NOT NULL,
|
||||
createdAt BIGINT UNSIGNED NOT NULL,
|
||||
INDEX session_uid (uid)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS keyFetchTokens (
|
||||
tokenId BINARY(32) PRIMARY KEY,
|
||||
authKey BINARY(32) NOT NULL,
|
||||
uid BINARY(16) NOT NULL,
|
||||
keyBundle BINARY(96) NOT NULL,
|
||||
createdAt BIGINT UNSIGNED NOT NULL,
|
||||
INDEX key_uid (uid)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS accountResetTokens (
|
||||
tokenId BINARY(32) PRIMARY KEY,
|
||||
tokenData BINARY(32) NOT NULL,
|
||||
uid BINARY(16) NOT NULL UNIQUE KEY,
|
||||
createdAt BIGINT UNSIGNED NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS passwordForgotTokens (
|
||||
tokenId BINARY(32) PRIMARY KEY,
|
||||
tokenData BINARY(32) NOT NULL,
|
||||
uid BINARY(16) NOT NULL UNIQUE KEY,
|
||||
passCode BINARY(16) NOT NULL,
|
||||
createdAt BIGINT UNSIGNED NOT NULL,
|
||||
tries SMALLINT UNSIGNED NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS passwordChangeTokens (
|
||||
tokenId BINARY(32) PRIMARY KEY,
|
||||
tokenData BINARY(32) NOT NULL,
|
||||
uid BINARY(16) NOT NULL,
|
||||
createdAt BIGINT UNSIGNED NOT NULL,
|
||||
INDEX session_uid (uid)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
UPDATE dbMetadata SET value = '2' WHERE name = 'schema-patch-level';
|
|
@ -0,0 +1,10 @@
|
|||
-- -- drop tables
|
||||
|
||||
-- DROP TABLE passwordChangeTokens;
|
||||
-- DROP TABLE passwordForgotTokens;
|
||||
-- DROP TABLE accountResetTokens;
|
||||
-- DROP TABLE keyFetchTokens;
|
||||
-- DROP TABLE sessionTokens;
|
||||
-- DROP TABLE accounts;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '1' WHERE name = 'schema-patch-level';
|
|
@ -0,0 +1,51 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var inherits = require('util').inherits
|
||||
|
||||
function AppError(options) {
|
||||
this.message = options.message
|
||||
this.errno = options.errno
|
||||
this.code = options.code
|
||||
}
|
||||
inherits(AppError, Error)
|
||||
|
||||
AppError.prototype.toString = function () {
|
||||
return 'Error: ' + this.message
|
||||
}
|
||||
|
||||
AppError.duplicate = function () {
|
||||
return new AppError(
|
||||
{
|
||||
code: 409,
|
||||
error: 'Conflict',
|
||||
errno: 101,
|
||||
message: 'Record already exists'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AppError.notFound = function () {
|
||||
return new AppError(
|
||||
{
|
||||
code: 404,
|
||||
error: 'Not Found',
|
||||
errno: 116,
|
||||
message: 'Not Found'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AppError.wrap = function (err) {
|
||||
return new AppError(
|
||||
{
|
||||
code: 500,
|
||||
error: 'Internal Server Error',
|
||||
errno: err.errno,
|
||||
message: err.code
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = AppError
|
|
@ -0,0 +1,74 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var Domain = require('domain')
|
||||
var util = require('util')
|
||||
var Logger = require('bunyan')
|
||||
var unbuffer = require('./bufferize').unbuffer
|
||||
|
||||
function Overdrive(options) {
|
||||
Logger.call(this, options)
|
||||
}
|
||||
util.inherits(Overdrive, Logger)
|
||||
|
||||
Overdrive.prototype.trace = function () {
|
||||
// TODO if this is a performance burden reintroduce the level check
|
||||
// otherwise this is valuable data for debugging in the log.summary
|
||||
var arg0 = arguments[0]
|
||||
if (typeof(arg0) === 'object') {
|
||||
unbuffer(arg0)
|
||||
}
|
||||
|
||||
return Logger.prototype.trace.apply(this, arguments)
|
||||
}
|
||||
|
||||
Overdrive.prototype.event = function (name, data) {
|
||||
var e = {
|
||||
event: name,
|
||||
data: unbuffer(data)
|
||||
}
|
||||
process.stdout.write(JSON.stringify(e) + '\n')
|
||||
}
|
||||
|
||||
Overdrive.prototype.stat = function (stats) {
|
||||
stats.op = 'stat'
|
||||
this.info(stats)
|
||||
}
|
||||
|
||||
module.exports = function (level, name) {
|
||||
var logStreams = [{ stream: process.stderr, level: level }]
|
||||
name = name || 'fxa-auth-server'
|
||||
|
||||
var log = new Overdrive(
|
||||
{
|
||||
name: name,
|
||||
streams: logStreams
|
||||
}
|
||||
)
|
||||
|
||||
process.stdout.on(
|
||||
'error',
|
||||
function (err) {
|
||||
if (err.code === 'EPIPE') {
|
||||
log.emit('error', err)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Object.keys(console).forEach(
|
||||
function (key) {
|
||||
console[key] = function () {
|
||||
var json = { op: 'console', message: util.format.apply(null, arguments) }
|
||||
if(log[key]) {
|
||||
log[key](json)
|
||||
}
|
||||
else {
|
||||
log.warn(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return log
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "fxa-auth-db-server",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "tap test",
|
||||
"start": "node ./bin/db_server.js 2>&1 | bunyan -o short"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dannycoates/fxa-auth-db-server"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MPL 2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/dannycoates/fxa-auth-db-server/issues"
|
||||
},
|
||||
"homepage": "https://github.com/dannycoates/fxa-auth-db-server",
|
||||
"dependencies": {
|
||||
"restify": "2.7.0",
|
||||
"mysql": "2.1.1",
|
||||
"bluebird": "1.2.2",
|
||||
"bunyan": "0.22.3",
|
||||
"rc": "0.3.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = require('bluebird')
|
Загрузка…
Ссылка в новой задаче