"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 database_1 = require("../../database");
const password = require("password-hash-and-salt");
const Boom = require("boom");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const assert = require("assert");
const storyboard_1 = require("storyboard");
const cluster = require("cluster");
const docURI = require('docuri');
const URI = docURI.route('users/:username');
// declare namespace Express {
//   interface Request {
//     user?: UserProps;
//     isAuthenticated: () => boolean;
//   }
// }
class Authenticator {
    /**
     * @param instanceID the current process instance ID, generated on start
     * @param secret the JWT secret
     */
    constructor(instanceID, secret) {
        this.instanceID = instanceID;
        this.secret = secret;
        this.passwords = {};
    }
    /**
     * Hash a password using password-hash-and-salt
     * @param the password to hash
     */
    hashPassword(pass) {
        return new Promise((resolve, reject) => {
            password(pass).hash((err, hash) => {
                if (err)
                    return reject(err);
                resolve(hash);
            });
        });
    }
    /**
     * Generate a random password in Base64 using crypto
     * @param length the number of bytes to generate
     */
    generatePassword(length = 32) {
        return new Promise((resolve, reject) => {
            crypto.randomBytes(length, (err, data) => {
                if (err)
                    return reject(err);
                resolve(data.toString('hex'));
            });
        });
    }
    /**
     * Verify using password-hash-and-salt the password
     * @param pass the current password to verify
     * @param hash the hash generated by hashPassword()
     */
    verifyPassword(pass, hash) {
        return new Promise((resolve, reject) => {
            password(pass).verifyAgainst(hash, (err, verified) => {
                if (err)
                    return reject(err);
                resolve(verified);
            });
        });
    }
    /**
     * Create a database _user directly by PUT the CouchDB instance
     * @param user the DatabaseUser to create
     */
    createDatabaseUser(user) {
        return __awaiter(this, void 0, void 0, function* () {
            const _users = new database_1.default('_users');
            try {
                const current = yield _users.get(user._id);
                yield _users.remove(current._id, current._rev);
            }
            catch (err) {
            }
            const doc = yield _users.put(user);
        });
    }
    /**
     * Validate the session token and return the username. If it isn't valid, then
     * throw an error...
     * @param token the JWT token to validate
     * @return the username
     */
    verifySession(token) {
        try {
            const session = jwt.verify(token, this.secret, {
                issuer: `compactd#${this.instanceID}`
            });
            if (!session.ok || !session.user) {
                throw new Error('');
            }
            return session.user;
        }
        catch (err) {
            throw Boom.create(400, 'Invalid or expired token');
        }
    }
    /**
     * Returns a database user object, fit for clusters
     * It appends the cluster id/ master to username and id
     * @param user the orginal username
     * @param pass the databse password
     */
    getDatabaseUser(user, pass) {
        if (cluster.isMaster) {
            return {
                _id: 'org.couchdb.user:m_' + user,
                name: 'm_' + user,
                roles: ['end_user'],
                type: 'user',
                password: pass
            };
        }
        else {
            return {
                _id: `org.couchdb.user:c${cluster.worker.id}_${user}`,
                name: `c${cluster.worker.id}_${user}`,
                roles: ['end_user'],
                type: 'user',
                password: pass
            };
        }
    }
    /**
     * Creates a signed session token, create a database user with random password
     * and store the db user password into the password store.
     * @param username the username to create session for
     * @param pass the user password to verify
     * @return the signed token
     */
    createSession(username, pass) {
        return __awaiter(this, void 0, void 0, function* () {
            const appUsers = new database_1.default('app_users');
            try {
                const user = yield appUsers.get(`users/${username}`);
                if (yield this.verifyPassword(pass, user.password)) {
                    const pwd = yield this.generatePassword();
                    const dbUser = this.getDatabaseUser(username, pwd);
                    yield this.createDatabaseUser(dbUser);
                    this.passwords[username] = pwd;
                    setTimeout(() => {
                        delete this.passwords[username];
                    }, 1000 * 60 * 60 * 24 * 7);
                    const token = jwt.sign({ user: username, ok: true }, this.secret, {
                        expiresIn: '7d',
                        issuer: `compactd#${this.instanceID}`
                    });
                    return token;
                }
                else {
                    throw new Error();
                }
            }
            catch (err) {
                storyboard_1.mainStory.error('authenticator', err.message, { attach: err });
                throw Boom.create(400, 'Invalid credentials');
            }
        });
    }
    /**
     * Create a new `app_user`
     * @param props the user props including username and password
     */
    registerUser(props) {
        return __awaiter(this, void 0, void 0, function* () {
            yield Promise.resolve();
            if (!/^[a-z0-9_]{4,16}$/i.test(props.username)) {
                throw Boom.create(400, 'Username may only contain between 4 and 16 alphanumeric characters');
            }
            if (!/^.{4,}$/.test(props.password)) {
                throw Boom.create(400, 'Password needs to be at least 4 characters long');
            }
            try {
                const pass = yield this.hashPassword(props.password);
                const appUsers = new database_1.default('app_users');
                yield appUsers.put({
                    _id: URI(props),
                    username: props.username,
                    password: pass,
                    role: props.role
                });
            }
            catch (err) {
                storyboard_1.mainStory.error('authenticator', err.message, { attach: err });
                throw Boom.create(500, 'Unexpected internal error');
            }
        });
    }
    /**
     * Create a function that will only call next if use token is valid
     */
    requireAuthentication() {
        return (req, res, next) => {
            if (process.env.WALTZ_AUTH)
                return next();
            if (!req.header('Authorization')) {
                return res.status(401).send({ error: 'You shall not pass' });
            }
            const [bearer, token] = req.header('Authorization').split(' ');
            if (bearer !== 'Bearer' || !token) {
                return res.status(401).send({
                    error: 'Not authenticated'
                });
            }
            try {
                const user = this.verifySession(token);
                const unused = user.toLowerCase(); // Check is user isnt undefined, otherwise throw error
                next();
            }
            catch (err) {
                if (err.isBoom) {
                    return res
                        .status(err.output.statusCode)
                        .send({ error: err.message });
                }
                res.status(500).send({ error: 'Unexpected error while verifying session token' });
            }
        };
    }
    /**
     * Create a session upon reaching this endpoint
     */
    signinUser() {
        return (req, res, next) => {
            this.createSession(req.body.username, req.body.password).then((token) => {
                return res.status(201).send({
                    ok: true,
                    token
                });
            }).catch((err) => {
                if (err.isBoom) {
                    const boom = err;
                    res.status(boom.output.statusCode).send({
                        error: boom.message
                    });
                }
            });
        };
    }
    signupUser() {
        return (req, res, next) => {
            assert.equal(req.method.toLowerCase(), 'post');
            this.registerUser(Object.assign({}, req.body, {
                role: 'end_user'
            })).then(() => {
                res.status(201).send({
                    ok: true,
                    message: 'User created'
                });
            }).catch((err) => {
                if (err.isBoom) {
                    return res
                        .status(err.output.statusCode)
                        .send({ error: err.message });
                }
                res.status(500).send({ error: 'Unexpected error while creating user' });
            });
        };
    }
    proxyRequestDecorator() {
        return (proxyReqOpts, srcReq) => __awaiter(this, void 0, void 0, function* () {
            if (process.env.ADMIN_PARTY) {
                const auth = new Buffer(database_1.default.credentials).toString('base64');
                proxyReqOpts.headers.authorization = `Basic ${auth}`;
                return proxyReqOpts;
            }
            if (!proxyReqOpts.headers.authorization) {
                throw new Error('Please provide a token');
            }
            const [bearer, token] = proxyReqOpts.headers.authorization.split(' ');
            let user = this.verifySession(token);
            if (!this.passwords[user]) {
                const pwd = yield this.generatePassword();
                yield this.createDatabaseUser(this.getDatabaseUser(user, pwd));
                this.passwords[user] = pwd;
            }
            const pass = this.passwords[user];
            if (cluster.isWorker) {
                user = `c${cluster.worker.id}_${user}`;
            }
            else {
                user = `m_${user}`;
            }
            const auth = new Buffer(`${user}:${pass}`)
                .toString('base64');
            proxyReqOpts.headers.authorization = `Basic ${auth}`;
            return proxyReqOpts;
        });
    }
}
exports.default = Authenticator;
//# sourceMappingURL=Authenticator.js.map