homebridge-config-ui-x
Advanced tools
Comparing version 2.3.1 to 2.3.2
@@ -55,3 +55,5 @@ "use strict"; | ||
console.error(err); | ||
res.status(err.status || 500); | ||
if (res.statusCode === 200) { | ||
res.status(500); | ||
} | ||
res.json({ | ||
@@ -58,0 +60,0 @@ error: err, |
@@ -11,14 +11,10 @@ "use strict"; | ||
this.passport.use(new passport_http_1.BasicStrategy((username, password, callback) => { | ||
users_1.users.findByUsername(username, (err, user) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
return users_1.users.login(username, password) | ||
.then((user) => { | ||
if (!user) { | ||
return callback(null, false); | ||
} | ||
if (user.hashedPassword !== users_1.users.hashPassword(password, username)) { | ||
return callback(null, false); | ||
} | ||
return callback(null, user); | ||
}); | ||
}) | ||
.catch((err) => callback(err, false)); | ||
})); | ||
@@ -29,8 +25,10 @@ this.passport.serializeUser((user, callback) => { | ||
this.passport.deserializeUser((id, callback) => { | ||
users_1.users.findById(id, (err, user) => { | ||
if (err) { | ||
return callback(err); | ||
return users_1.users.findById(id) | ||
.then((user) => { | ||
if (!user) { | ||
return callback(null, false); | ||
} | ||
callback(null, user); | ||
}); | ||
return callback(null, user); | ||
}) | ||
.catch((err) => callback(err, false)); | ||
}); | ||
@@ -58,14 +56,22 @@ this.init = [ | ||
noAuthHandler(req, res, next) { | ||
req.user = users_1.users.getUsers()[0]; | ||
next(); | ||
return users_1.users.getUsers() | ||
.then((authfile) => { | ||
req.user = authfile[0]; | ||
return next(); | ||
}) | ||
.catch(next); | ||
} | ||
formAuthHandler(req, res, next) { | ||
if (req.headers['x-jwt']) { | ||
users_1.users.verifyJwt(req.headers['x-jwt'], (err, user) => { | ||
if (err) { | ||
return users_1.users.verifyJwt(req.headers['x-jwt']) | ||
.then((user) => { | ||
if (user) { | ||
req.user = user; | ||
return next(); | ||
} | ||
else { | ||
return res.sendStatus(401); | ||
} | ||
req.user = user; | ||
next(); | ||
}); | ||
}) | ||
.catch(() => res.sendStatus(401)); | ||
} | ||
@@ -72,0 +78,0 @@ else { |
@@ -23,4 +23,2 @@ "use strict"; | ||
hb_1.hb.init(log, config); | ||
// ensure auth.json is setup correctly | ||
users_1.users.setupAuthFile(); | ||
// bootstrap the server | ||
@@ -31,2 +29,4 @@ this.bootstrap(); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
// ensure auth.json is setup correctly | ||
yield users_1.users.setupAuthFile(); | ||
// dynamically load modules so app is only loaded if plugin is enabled | ||
@@ -33,0 +33,0 @@ const { ExpressServer } = yield Promise.resolve().then(() => require('./app')); |
@@ -11,16 +11,15 @@ "use strict"; | ||
login(req, res, next) { | ||
users_1.users.findByUsername(req.body.username, (err, user) => { | ||
if (err) { | ||
return res.sendStatus(403); | ||
} | ||
return users_1.users.login(req.body.username, req.body.password) | ||
.then((user) => { | ||
if (!user) { | ||
return res.sendStatus(403); | ||
} | ||
if (user.hashedPassword !== users_1.users.hashPassword(req.body.password, req.body.username)) { | ||
return res.sendStatus(403); | ||
} | ||
return res.json({ | ||
token: users_1.users.getJwt(user) | ||
return users_1.users.getJwt(user) | ||
.then((token) => { | ||
return res.json({ | ||
token: token | ||
}); | ||
}); | ||
}); | ||
}) | ||
.catch(next); | ||
} | ||
@@ -27,0 +26,0 @@ } |
@@ -61,5 +61,9 @@ "use strict"; | ||
getToken(req, res, next) { | ||
return res.json({ | ||
token: users_1.users.getJwt(req.user) | ||
}); | ||
return users_1.users.getJwt(req.user) | ||
.then((token) => { | ||
return res.json({ | ||
token: token | ||
}); | ||
}) | ||
.catch(next); | ||
} | ||
@@ -66,0 +70,0 @@ } |
@@ -14,26 +14,40 @@ "use strict"; | ||
getUser(req, res, next) { | ||
let authfile = users_1.users.getUsers(); | ||
// remove user passwords before sending to client | ||
authfile = authfile.map((user) => { | ||
delete user.password; | ||
return user; | ||
}); | ||
res.json(authfile); | ||
return users_1.users.getUsers() | ||
.then((authfile) => { | ||
// remove sensitive data before sending user list to client | ||
authfile = authfile.map((user) => { | ||
delete user.hashedPassword; | ||
return user; | ||
}); | ||
return res.json(authfile); | ||
}) | ||
.catch(next); | ||
} | ||
createUser(req, res, next) { | ||
const authfile = users_1.users.getUsers(); | ||
// check to see if user already exists | ||
if (authfile.find(x => x.username === req.body.username)) { | ||
return res.sendStatus(409); | ||
} | ||
users_1.users.addUser(req.body); | ||
res.json({ ok: true }); | ||
return users_1.users.findByUsername(req.body.username) | ||
.then((user) => { | ||
if (user) { | ||
return res.sendStatus(409); | ||
} | ||
return users_1.users.addUser(req.body) | ||
.then(() => { | ||
return res.json({ ok: true }); | ||
}); | ||
}) | ||
.catch(next); | ||
} | ||
deleteUser(req, res, next) { | ||
users_1.users.deleteUser(req.params.id); | ||
res.json({ ok: true }); | ||
return users_1.users.deleteUser(req.params.id) | ||
.then(() => { | ||
return res.json({ ok: true }); | ||
}) | ||
.catch(next); | ||
} | ||
updateUser(req, res, next) { | ||
users_1.users.updateUser(parseInt(req.params.id, 10), req.body); | ||
res.json({ ok: true }); | ||
return users_1.users.updateUser(parseInt(req.params.id, 10), req.body) | ||
.then(() => { | ||
return res.json({ ok: true }); | ||
}) | ||
.catch(next); | ||
} | ||
@@ -40,0 +54,0 @@ } |
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const fs = require("fs-extra"); | ||
const crypto = require("crypto"); | ||
const jwt = require("jsonwebtoken"); | ||
const cryptoNode = require("crypto"); | ||
const Bluebird = require("bluebird"); | ||
const jsonwebtoken = require("jsonwebtoken"); | ||
const crypto = Bluebird.promisifyAll(cryptoNode); | ||
const jwt = Bluebird.promisifyAll(jsonwebtoken); | ||
const hb_1 = require("./hb"); | ||
class Users { | ||
findById(id, callback) { | ||
const authfile = this.getUsers(); | ||
const user = authfile.find(x => x.id === id); | ||
if (user) { | ||
callback(null, user); | ||
} | ||
else { | ||
callback(new Error('User ' + id + ' does not exist')); | ||
} | ||
getUsers() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const allUsers = yield fs.readJson(hb_1.hb.authPath); | ||
return allUsers; | ||
}); | ||
} | ||
findByUsername(username, callback) { | ||
const authfile = this.getUsers(); | ||
const user = authfile.find(x => x.username === username); | ||
if (user) { | ||
callback(null, user); | ||
} | ||
else { | ||
callback(null, null); | ||
} | ||
findById(id) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const authfile = yield this.getUsers(); | ||
const user = authfile.find(x => x.id === id); | ||
return user; | ||
}); | ||
} | ||
getUsers() { | ||
return fs.readJsonSync(hb_1.hb.authPath); | ||
findByUsername(username) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const authfile = yield this.getUsers(); | ||
const user = authfile.find(x => x.username === username); | ||
return user; | ||
}); | ||
} | ||
hashPassword(password, salt) { | ||
// pbkdf2 iterations have been kept low so we don't lock up homebridge when a user logs in on low powered devices | ||
// we're using the username as the salt for the sake of keeping the module portable | ||
const derivedKey = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512'); | ||
return derivedKey.toString('hex'); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const derivedKey = yield crypto.pbkdf2Async(password, salt, 1000, 64, 'sha512'); | ||
return derivedKey.toString('hex'); | ||
}); | ||
} | ||
genSalt() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const salt = yield crypto.randomBytesAsync(32); | ||
return salt.toString('hex'); | ||
}); | ||
} | ||
login(username, password) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const user = yield this.findByUsername(username); | ||
if (!user) { | ||
return null; | ||
} | ||
// using username as salt if user.salt not set to maintain backwards compatibility with older versions. | ||
const hashedPassword = yield this.hashPassword(password, user.salt || user.username); | ||
if (hashedPassword === user.hashedPassword) { | ||
return user; | ||
} | ||
else { | ||
return null; | ||
} | ||
}); | ||
} | ||
addUser(user) { | ||
const authfile = this.getUsers(); | ||
// user object | ||
const newuser = { | ||
id: authfile.length ? Math.max.apply(Math, authfile.map(x => x.id)) + 1 : 1, | ||
username: user.username, | ||
name: user.name, | ||
hashedPassword: this.hashPassword(user.password, user.username), | ||
admin: user.admin | ||
}; | ||
// add the user to the authfile | ||
authfile.push(newuser); | ||
// update the auth.json | ||
fs.writeFileSync(hb_1.hb.authPath, JSON.stringify(authfile, null, 4)); | ||
hb_1.hb.log(`Added new user: ${user.username}`); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const authfile = yield this.getUsers(); | ||
const salt = yield this.genSalt(); | ||
// user object | ||
const newUser = { | ||
id: authfile.length ? Math.max.apply(Math, authfile.map(x => x.id)) + 1 : 1, | ||
username: user.username, | ||
name: user.name, | ||
hashedPassword: yield this.hashPassword(user.password, salt), | ||
salt: salt, | ||
admin: user.admin | ||
}; | ||
// add the user to the authfile | ||
authfile.push(newUser); | ||
// update the auth.json | ||
yield fs.writeJson(hb_1.hb.authPath, authfile, { spaces: 4 }); | ||
hb_1.hb.log(`Added new user: ${user.username}`); | ||
}); | ||
} | ||
updateUser(userId, update) { | ||
const authfile = this.getUsers(); | ||
const user = authfile.find(x => x.id === userId); | ||
if (!user) { | ||
throw new Error('User not gound'); | ||
} | ||
user.name = update.name || user.name; | ||
user.admin = update.admin || user.admin; | ||
if (update.password) { | ||
user.hashedPassword = this.hashPassword(update.password, user.username); | ||
} | ||
// update the auth.json | ||
fs.writeFileSync(hb_1.hb.authPath, JSON.stringify(authfile, null, 4)); | ||
hb_1.hb.log(`Updated user: ${user.username}`); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const authfile = yield this.getUsers(); | ||
const user = authfile.find(x => x.id === userId); | ||
if (!user) { | ||
throw new Error('User Not Found'); | ||
} | ||
user.name = update.name || user.name; | ||
user.admin = update.admin || user.admin; | ||
if (update.password) { | ||
const salt = yield this.genSalt(); | ||
user.hashedPassword = yield this.hashPassword(update.password, salt); | ||
user.salt = salt; | ||
} | ||
// update the auth.json | ||
yield fs.writeJson(hb_1.hb.authPath, authfile, { spaces: 4 }); | ||
hb_1.hb.log(`Updated user: ${user.username}`); | ||
}); | ||
} | ||
deleteUser(id) { | ||
const authfile = this.getUsers(); | ||
const index = authfile.findIndex(x => x.id === parseInt(id, 10)); | ||
if (index < 0) { | ||
throw new Error('User not found'); | ||
} | ||
authfile.splice(index, 1); | ||
// update the auth.json | ||
fs.writeFileSync(hb_1.hb.authPath, JSON.stringify(authfile, null, 4)); | ||
hb_1.hb.log(`Deleted user with ID ${id}`); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const authfile = yield this.getUsers(); | ||
const index = authfile.findIndex(x => x.id === parseInt(id, 10)); | ||
if (index < 0) { | ||
throw new Error('User not found'); | ||
} | ||
authfile.splice(index, 1); | ||
// update the auth.json | ||
yield fs.writeJson(hb_1.hb.authPath, authfile, { spaces: 4 }); | ||
hb_1.hb.log(`Deleted user with ID ${id}`); | ||
}); | ||
} | ||
getJwt(user) { | ||
return jwt.sign({ | ||
username: user.username, | ||
name: user.name, | ||
admin: user.admin | ||
}, user.hashedPassword, { expiresIn: '8h' }); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return jwt.signAsync({ | ||
username: user.username, | ||
name: user.name, | ||
admin: user.admin | ||
}, user.hashedPassword, { expiresIn: '8h' }); | ||
}); | ||
} | ||
verifyJwt(token, callback) { | ||
const authfile = this.getUsers(); | ||
const decoded = jwt.decode(token); | ||
if (!decoded) { | ||
return callback(new Error('User Not Found')); | ||
} | ||
const user = authfile.find(x => x.username === decoded.username); | ||
if (user) { | ||
jwt.verify(token, user.hashedPassword, (err, res) => { | ||
return callback(err, user); | ||
}); | ||
} | ||
else { | ||
return callback(new Error('User Not Found')); | ||
} | ||
} | ||
updateOldPasswords() { | ||
let authfile = this.getUsers(); | ||
authfile = authfile.map((user) => { | ||
if (user.password && !user.hashedPassword) { | ||
user.hashedPassword = this.hashPassword(user.password, user.username); | ||
delete user.password; | ||
return user; | ||
verifyJwt(token) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const decoded = jwt.decode(token); | ||
if (!decoded) { | ||
return null; | ||
} | ||
const user = yield this.findByUsername(decoded.username); | ||
if (user) { | ||
try { | ||
yield jwt.verifyAsync(token, user.hashedPassword); | ||
return user; | ||
} | ||
catch (e) { | ||
hb_1.hb.log(`Invalid token sent by ${user.username}: ${e.message}`); | ||
return null; | ||
} | ||
} | ||
else { | ||
return user; | ||
return null; | ||
} | ||
}); | ||
fs.writeFileSync(hb_1.hb.authPath, JSON.stringify(authfile, null, 4)); | ||
} | ||
updateOldPasswords() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
let authfile = yield this.getUsers(); | ||
authfile = yield Bluebird.map(authfile, (user) => __awaiter(this, void 0, void 0, function* () { | ||
if (user.password && !user.hashedPassword) { | ||
const salt = yield this.genSalt(); | ||
user.hashedPassword = yield this.hashPassword(user.password, salt); | ||
user.salt = salt; | ||
delete user.password; | ||
hb_1.hb.log(`Hashed password for "${user.username}" in auth.json`); | ||
return user; | ||
} | ||
else { | ||
return user; | ||
} | ||
})); | ||
// update the auth.json | ||
yield fs.writeJson(hb_1.hb.authPath, authfile, { spaces: 4 }); | ||
}); | ||
} | ||
setupDefaultUser() { | ||
return this.addUser({ | ||
'username': 'admin', | ||
'password': 'admin', | ||
'name': 'Administrator', | ||
'admin': true | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return this.addUser({ | ||
'username': 'admin', | ||
'password': 'admin', | ||
'name': 'Administrator', | ||
'admin': true | ||
}); | ||
}); | ||
} | ||
setupAuthFile() { | ||
if (!fs.existsSync(hb_1.hb.authPath)) { | ||
fs.writeFileSync(hb_1.hb.authPath, '[]'); | ||
} | ||
const authfile = this.getUsers(); | ||
// if there are no admin users, add the default user | ||
if (!authfile.find(x => x.admin === true || x.username === 'admin')) { | ||
this.setupDefaultUser(); | ||
} | ||
// update older auth.json files from plain text to hashed passwords | ||
if (authfile.find(x => x.password && !x.hashedPassword)) { | ||
this.updateOldPasswords(); | ||
} | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!(yield fs.pathExists(hb_1.hb.authPath))) { | ||
yield fs.writeJson(hb_1.hb.authPath, []); | ||
} | ||
const authfile = yield this.getUsers(); | ||
// if there are no admin users, add the default user | ||
if (!authfile.find(x => x.admin === true || x.username === 'admin')) { | ||
yield this.setupDefaultUser(); | ||
} | ||
// update older auth.json files from plain text to hashed passwords | ||
if (authfile.find(x => x.password && !x.hashedPassword)) { | ||
yield this.updateOldPasswords(); | ||
} | ||
}); | ||
} | ||
@@ -138,0 +196,0 @@ } |
@@ -46,15 +46,20 @@ "use strict"; | ||
if (params.token) { | ||
users_1.users.verifyJwt(params.token, (err) => { | ||
if (err) { | ||
return users_1.users.verifyJwt(params.token) | ||
.then((user) => { | ||
if (user) { | ||
return callback(true); | ||
} | ||
else { | ||
// invalid token - reject the websocket connection | ||
hb_1.hb.log('Unauthorised WebSocket Connection Closed'); | ||
callback(false); | ||
return callback(false); | ||
} | ||
}); | ||
}) | ||
.catch(() => callback(false)); | ||
} | ||
else { | ||
// no token provided - reject the websocket connection | ||
callback(false); | ||
hb_1.hb.log('Unauthorised WebSocket Connection Closed'); | ||
return callback(false); | ||
} | ||
callback(true); | ||
} | ||
@@ -61,0 +66,0 @@ subscribeHandler(ws, req, msg) { |
{ | ||
"name": "homebridge-config-ui-x", | ||
"version": "2.3.1", | ||
"version": "2.3.2", | ||
"description": "Configuration UI plugin for Homebridge", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -75,3 +75,7 @@ import * as path from 'path'; | ||
console.error(err); | ||
res.status(err.status || 500); | ||
if (res.statusCode === 200) { | ||
res.status(500); | ||
} | ||
res.json({ | ||
@@ -78,0 +82,0 @@ error: err, |
@@ -18,17 +18,11 @@ import { Passport } from 'passport'; | ||
this.passport.use(new BasicStrategy((username, password, callback) => { | ||
users.findByUsername(username, (err, user) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
return users.login(username, password) | ||
.then((user) => { | ||
if (!user) { | ||
return callback(null, false); | ||
} | ||
if (!user) { | ||
return callback(null, false); | ||
} | ||
if (user.hashedPassword !== users.hashPassword(password, username)) { | ||
return callback(null, false); | ||
} | ||
return callback(null, user); | ||
}); | ||
return callback(null, user); | ||
}) | ||
.catch((err) => callback(err, false)); | ||
})); | ||
@@ -41,9 +35,11 @@ | ||
this.passport.deserializeUser((id, callback) => { | ||
users.findById(id, (err, user) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
return users.findById(id) | ||
.then((user) => { | ||
if (!user) { | ||
return callback(null, false); | ||
} | ||
callback(null, user); | ||
}); | ||
return callback(null, user); | ||
}) | ||
.catch((err) => callback(err, false)); | ||
}); | ||
@@ -72,4 +68,8 @@ | ||
noAuthHandler (req: Request, res: Response, next: NextFunction) { | ||
req.user = users.getUsers()[0]; | ||
next(); | ||
return users.getUsers() | ||
.then((authfile) => { | ||
req.user = authfile[0]; | ||
return next(); | ||
}) | ||
.catch(next); | ||
} | ||
@@ -79,9 +79,12 @@ | ||
if (req.headers['x-jwt']) { | ||
users.verifyJwt(req.headers['x-jwt'], (err, user) => { | ||
if (err) { | ||
return res.sendStatus(401); | ||
} | ||
req.user = user; | ||
next(); | ||
}); | ||
return users.verifyJwt(req.headers['x-jwt']) | ||
.then((user) => { | ||
if (user) { | ||
req.user = user; | ||
return next(); | ||
} else { | ||
return res.sendStatus(401); | ||
} | ||
}) | ||
.catch(() => res.sendStatus(401)); | ||
} else { | ||
@@ -88,0 +91,0 @@ return res.sendStatus(401); |
@@ -20,5 +20,2 @@ import 'source-map-support/register'; | ||
// ensure auth.json is setup correctly | ||
users.setupAuthFile(); | ||
// bootstrap the server | ||
@@ -29,2 +26,5 @@ this.bootstrap(); | ||
async bootstrap () { | ||
// ensure auth.json is setup correctly | ||
await users.setupAuthFile(); | ||
// dynamically load modules so app is only loaded if plugin is enabled | ||
@@ -31,0 +31,0 @@ const { ExpressServer } = await import('./app'); |
@@ -15,20 +15,17 @@ import { Router, Request, Response, NextFunction } from 'express'; | ||
login (req: Request, res: Response, next: NextFunction) { | ||
users.findByUsername(req.body.username, (err, user) => { | ||
if (err) { | ||
return res.sendStatus(403); | ||
} | ||
return users.login(req.body.username, req.body.password) | ||
.then((user) => { | ||
if (!user) { | ||
return res.sendStatus(403); | ||
} | ||
if (!user) { | ||
return res.sendStatus(403); | ||
} | ||
if (user.hashedPassword !== users.hashPassword(req.body.password, req.body.username)) { | ||
return res.sendStatus(403); | ||
} | ||
return res.json({ | ||
token: users.getJwt(user) | ||
}); | ||
}); | ||
return users.getJwt(user) | ||
.then((token) => { | ||
return res.json({ | ||
token: token | ||
}); | ||
}); | ||
}) | ||
.catch(next); | ||
} | ||
} |
@@ -72,6 +72,10 @@ import * as qr from 'qr-image'; | ||
getToken (req: Request, res: Response, next: NextFunction) { | ||
return res.json({ | ||
token: users.getJwt(req.user) | ||
}); | ||
return users.getJwt(req.user) | ||
.then((token) => { | ||
return res.json({ | ||
token: token | ||
}); | ||
}) | ||
.catch(next); | ||
} | ||
} |
@@ -18,35 +18,46 @@ import { Router, Request, Response, NextFunction } from 'express'; | ||
getUser (req: Request, res: Response, next: NextFunction) { | ||
let authfile = users.getUsers(); | ||
return users.getUsers() | ||
.then((authfile) => { | ||
// remove sensitive data before sending user list to client | ||
authfile = authfile.map((user) => { | ||
delete user.hashedPassword; | ||
return user; | ||
}); | ||
// remove user passwords before sending to client | ||
authfile = authfile.map((user) => { | ||
delete user.password; | ||
return user; | ||
}); | ||
res.json(authfile); | ||
return res.json(authfile); | ||
}) | ||
.catch(next); | ||
} | ||
createUser (req: Request, res: Response, next: NextFunction) { | ||
const authfile = users.getUsers(); | ||
// check to see if user already exists | ||
if (authfile.find(x => x.username === req.body.username)) { | ||
return res.sendStatus(409); | ||
} | ||
return users.findByUsername(req.body.username) | ||
.then((user) => { | ||
if (user) { | ||
return res.sendStatus(409); | ||
} | ||
users.addUser(req.body); | ||
res.json({ ok: true }); | ||
return users.addUser(req.body) | ||
.then(() => { | ||
return res.json({ ok: true }); | ||
}); | ||
}) | ||
.catch(next); | ||
} | ||
deleteUser (req: Request, res: Response, next: NextFunction) { | ||
users.deleteUser(req.params.id); | ||
res.json({ ok: true }); | ||
return users.deleteUser(req.params.id) | ||
.then(() => { | ||
return res.json({ ok: true }); | ||
}) | ||
.catch(next); | ||
} | ||
updateUser (req: Request, res: Response, next: NextFunction) { | ||
users.updateUser(parseInt(req.params.id, 10), req.body); | ||
res.json({ ok: true }); | ||
return users.updateUser(parseInt(req.params.id, 10), req.body) | ||
.then(() => { | ||
return res.json({ ok: true }); | ||
}) | ||
.catch(next); | ||
} | ||
} |
162
src/users.ts
import * as fs from 'fs-extra'; | ||
import * as crypto from 'crypto'; | ||
import * as jwt from 'jsonwebtoken'; | ||
import * as cryptoNode from 'crypto'; | ||
import * as Bluebird from 'bluebird'; | ||
import * as jsonwebtoken from 'jsonwebtoken'; | ||
const crypto = Bluebird.promisifyAll(cryptoNode); | ||
const jwt = Bluebird.promisifyAll(jsonwebtoken); | ||
import { hb } from './hb'; | ||
interface User { | ||
id: number; | ||
name: string; | ||
username: string; | ||
admin: boolean; | ||
hashedPassword: string; | ||
salt: string; | ||
password?: string; | ||
} | ||
class Users { | ||
findById (id, callback) { | ||
const authfile = this.getUsers(); | ||
async getUsers () { | ||
const allUsers: User[] = await fs.readJson(hb.authPath); | ||
return allUsers; | ||
} | ||
async findById (id: number) { | ||
const authfile = await this.getUsers(); | ||
const user = authfile.find(x => x.id === id); | ||
return user; | ||
} | ||
if (user) { | ||
callback(null, user); | ||
} else { | ||
callback(new Error('User ' + id + ' does not exist')); | ||
} | ||
async findByUsername (username: string) { | ||
const authfile = await this.getUsers(); | ||
const user = authfile.find(x => x.username === username); | ||
return user; | ||
} | ||
findByUsername (username, callback) { | ||
const authfile = this.getUsers(); | ||
async hashPassword (password: string, salt: string) { | ||
const derivedKey = await crypto.pbkdf2Async(password, salt, 1000, 64, 'sha512'); | ||
return derivedKey.toString('hex'); | ||
} | ||
const user = authfile.find(x => x.username === username); | ||
async genSalt () { | ||
const salt = await crypto.randomBytesAsync(32); | ||
return salt.toString('hex'); | ||
} | ||
if (user) { | ||
callback(null, user); | ||
async login (username: string, password: string) { | ||
const user = await this.findByUsername(username); | ||
if (!user) { | ||
return null; | ||
} | ||
// using username as salt if user.salt not set to maintain backwards compatibility with older versions. | ||
const hashedPassword = await this.hashPassword(password, user.salt || user.username); | ||
if (hashedPassword === user.hashedPassword) { | ||
return user; | ||
} else { | ||
callback(null, null); | ||
return null; | ||
} | ||
} | ||
getUsers () { | ||
return fs.readJsonSync(hb.authPath); | ||
} | ||
async addUser (user) { | ||
const authfile = await this.getUsers(); | ||
hashPassword (password, salt) { | ||
// pbkdf2 iterations have been kept low so we don't lock up homebridge when a user logs in on low powered devices | ||
// we're using the username as the salt for the sake of keeping the module portable | ||
const derivedKey = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512'); | ||
return derivedKey.toString('hex'); | ||
} | ||
const salt = await this.genSalt(); | ||
addUser (user) { | ||
const authfile = this.getUsers(); | ||
// user object | ||
const newuser = { | ||
const newUser: User = { | ||
id: authfile.length ? Math.max.apply(Math, authfile.map(x => x.id)) + 1 : 1, | ||
username: user.username, | ||
name: user.name, | ||
hashedPassword: this.hashPassword(user.password, user.username), | ||
hashedPassword: await this.hashPassword(user.password, salt), | ||
salt: salt, | ||
admin: user.admin | ||
@@ -56,6 +82,6 @@ }; | ||
// add the user to the authfile | ||
authfile.push(newuser); | ||
authfile.push(newUser); | ||
// update the auth.json | ||
fs.writeFileSync(hb.authPath, JSON.stringify(authfile, null, 4)); | ||
await fs.writeJson(hb.authPath, authfile, {spaces: 4}); | ||
@@ -65,4 +91,4 @@ hb.log(`Added new user: ${user.username}`); | ||
updateUser (userId, update) { | ||
const authfile = this.getUsers(); | ||
async updateUser (userId, update) { | ||
const authfile = await this.getUsers(); | ||
@@ -72,3 +98,3 @@ const user = authfile.find(x => x.id === userId); | ||
if (!user) { | ||
throw new Error('User not gound'); | ||
throw new Error('User Not Found'); | ||
} | ||
@@ -80,7 +106,10 @@ | ||
if (update.password) { | ||
user.hashedPassword = this.hashPassword(update.password, user.username); | ||
const salt = await this.genSalt(); | ||
user.hashedPassword = await this.hashPassword(update.password, salt); | ||
user.salt = salt; | ||
} | ||
// update the auth.json | ||
fs.writeFileSync(hb.authPath, JSON.stringify(authfile, null, 4)); | ||
await fs.writeJson(hb.authPath, authfile, { spaces: 4 }); | ||
@@ -90,6 +119,7 @@ hb.log(`Updated user: ${user.username}`); | ||
deleteUser (id) { | ||
const authfile = this.getUsers(); | ||
async deleteUser (id) { | ||
const authfile = await this.getUsers(); | ||
const index = authfile.findIndex(x => x.id === parseInt(id, 10)); | ||
if (index < 0) { | ||
@@ -102,3 +132,3 @@ throw new Error('User not found'); | ||
// update the auth.json | ||
fs.writeFileSync(hb.authPath, JSON.stringify(authfile, null, 4)); | ||
await fs.writeJson(hb.authPath, authfile, { spaces: 4 }); | ||
@@ -108,4 +138,4 @@ hb.log(`Deleted user with ID ${id}`); | ||
getJwt (user) { | ||
return jwt.sign({ | ||
async getJwt (user) { | ||
return jwt.signAsync({ | ||
username: user.username, | ||
@@ -117,28 +147,37 @@ name: user.name, | ||
verifyJwt (token, callback) { | ||
const authfile = this.getUsers(); | ||
async verifyJwt (token) { | ||
const decoded = jwt.decode(token); | ||
if (!decoded) { | ||
return callback(new Error('User Not Found')); | ||
return null; | ||
} | ||
const user = authfile.find(x => x.username === decoded.username); | ||
const user = await this.findByUsername(decoded.username); | ||
if (user) { | ||
jwt.verify(token, user.hashedPassword, (err, res) => { | ||
return callback(err, user); | ||
}); | ||
try { | ||
await jwt.verifyAsync(token, user.hashedPassword); | ||
return user; | ||
} catch (e) { | ||
hb.log(`Invalid token sent by ${user.username}: ${e.message}`); | ||
return null; | ||
} | ||
} else { | ||
return callback(new Error('User Not Found')); | ||
return null; | ||
} | ||
} | ||
updateOldPasswords () { | ||
let authfile = this.getUsers(); | ||
async updateOldPasswords () { | ||
let authfile = await this.getUsers(); | ||
authfile = authfile.map((user) => { | ||
authfile = await Bluebird.map(authfile, async (user) => { | ||
if (user.password && !user.hashedPassword) { | ||
user.hashedPassword = this.hashPassword(user.password, user.username); | ||
const salt = await this.genSalt(); | ||
user.hashedPassword = await this.hashPassword(user.password, salt); | ||
user.salt = salt; | ||
delete user.password; | ||
hb.log(`Hashed password for "${user.username}" in auth.json`); | ||
return user; | ||
@@ -150,6 +189,7 @@ } else { | ||
fs.writeFileSync(hb.authPath, JSON.stringify(authfile, null, 4)); | ||
// update the auth.json | ||
await fs.writeJson(hb.authPath, authfile, { spaces: 4 }); | ||
} | ||
setupDefaultUser () { | ||
async setupDefaultUser () { | ||
return this.addUser({ | ||
@@ -163,12 +203,12 @@ 'username': 'admin', | ||
setupAuthFile () { | ||
if (!fs.existsSync(hb.authPath)) { | ||
fs.writeFileSync(hb.authPath, '[]'); | ||
async setupAuthFile () { | ||
if (!await fs.pathExists(hb.authPath)) { | ||
await fs.writeJson(hb.authPath, []); | ||
} | ||
const authfile = this.getUsers(); | ||
const authfile = await this.getUsers(); | ||
// if there are no admin users, add the default user | ||
if (!authfile.find(x => x.admin === true || x.username === 'admin')) { | ||
this.setupDefaultUser(); | ||
await this.setupDefaultUser(); | ||
} | ||
@@ -178,3 +218,3 @@ | ||
if (authfile.find(x => x.password && !x.hashedPassword)) { | ||
this.updateOldPasswords(); | ||
await this.updateOldPasswords(); | ||
} | ||
@@ -181,0 +221,0 @@ } |
@@ -53,14 +53,18 @@ import * as url from 'url'; | ||
if (params.token) { | ||
users.verifyJwt(params.token, (err) => { | ||
if (err) { | ||
// invalid token - reject the websocket connection | ||
hb.log('Unauthorised WebSocket Connection Closed'); | ||
callback(false); | ||
} | ||
}); | ||
return users.verifyJwt(params.token) | ||
.then((user) => { | ||
if (user) { | ||
return callback(true); | ||
} else { | ||
// invalid token - reject the websocket connection | ||
hb.log('Unauthorised WebSocket Connection Closed'); | ||
return callback(false); | ||
} | ||
}) | ||
.catch(() => callback(false)); | ||
} else { | ||
// no token provided - reject the websocket connection | ||
callback(false); | ||
hb.log('Unauthorised WebSocket Connection Closed'); | ||
return callback(false); | ||
} | ||
callback(true); | ||
} | ||
@@ -67,0 +71,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
5123524
4548