"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 assert = require("assert");
const crypto = require("crypto");
const moment = require("moment");
const nodemailer = require("nodemailer");
const AuthProvider_1 = require("../AuthProvider");
const errors = require("../../errors");
const shared_1 = require("../../shared");
const PasswordRealm_1 = require("./PasswordRealm");
const emails_1 = require("./emails");
class PasswordAuthProvider extends AuthProvider_1.AuthProvider {
    constructor(config = {}) {
        super();
        this.name = 'password';
        this.autoCreateAdminUser = config.autoCreateAdminUser || false;
        this.saltLength = config.saltLength || 32;
        this.iterations = config.iterations || 10000;
        this.keyLength = config.keyLength || 512;
        this.digest = config.digest || "sha512";
        this.config = config;
    }
    start() {
        const _super = name => super[name];
        return __awaiter(this, void 0, void 0, function* () {
            yield _super("start").call(this);
            this.passwordRealm = yield this.service.server.openRealm(PasswordRealm_1.PasswordRealm);
            if (this.autoCreateAdminUser) {
                try {
                    const username = "realm-admin";
                    const foundUser = this.service.getUserByProviderId(this.name, username);
                    if (!foundUser) {
                        const password = process.env.ROS_DEFAULT_PASSWORD || "";
                        yield this.attemptToRegister(username, password, true);
                        this.service.logger.info(`Autocreated admin user: ${username}`);
                    }
                }
                catch (err) {
                    this.service.logger.error(`Failed to autocreate admin user: ${err}`);
                }
            }
            if (this.config.emailHandler) {
                this.emailHandler = this.config.emailHandler;
            }
            else if (this.config.emailHandlerConfig) {
                try {
                    this.emailHandler = new BasicEmailHandler(this.config.emailHandlerConfig, this.service.logger);
                }
                catch (err) {
                    this.service.logger.error(`Failed to create emailHandler for config: ${JSON.stringify(this.config.emailHandlerConfig)}. Error: ${err}`);
                }
            }
        });
    }
    stop() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.passwordRealm) {
                this.passwordRealm.close();
            }
        });
    }
    authenticateOrCreateUser(body) {
        return __awaiter(this, void 0, void 0, function* () {
            let username;
            let password;
            let register;
            if (body["user_info"]) {
                username = body["data"];
                password = body["user_info"]["password"];
                register = body["user_info"]["register"];
            }
            else {
                username = body["username"];
                password = body["password"];
                register = body["register"];
            }
            if (!username) {
                throw new errors.realm.MissingParameters('username');
            }
            if (password === undefined || password === null) {
                throw new errors.realm.MissingParameters('password');
            }
            if (register === undefined) {
                const foundUser = this.service.getUserByProviderId('password', username);
                register = !foundUser;
            }
            if (register) {
                return this.attemptToRegister(username, password, false);
            }
            return this.attemptToLogin(username, password);
        });
    }
    update(user, data) {
        return __awaiter(this, void 0, void 0, function* () {
            const newPassword = data.new_password;
            if (!newPassword) {
                throw new errors.realm.MissingParameters('new_password');
            }
            yield this.updatePassword(newPassword, user.userId);
            return {};
        });
    }
    updateProviderAccount(providerId, user, data, isAuthenticated, userAgent, remoteIp) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.emailHandler) {
                throw new errors.realm.NotSupported({
                    detail: 'The Password provider is not configured with an emailHandler.'
                });
            }
            switch (data.action) {
                case 'reset_password':
                    if (!this.emailHandler.resetPassword) {
                        throw new errors.realm.NotSupported({
                            detail: "The Password provider's emailHandler doesn't implement resetPassword."
                        });
                    }
                    const token = crypto.randomBytes(32).toString('hex');
                    if (user) {
                        this.passwordRealm.write(() => {
                            this.passwordRealm.create('PasswordResetRequest', {
                                token,
                                expires: moment().add(12, 'hour').toDate(),
                                userId: user.userId
                            });
                        });
                        this.emailHandler.resetPassword(providerId, token, userAgent, remoteIp)
                            .catch((err) => {
                            this.service.logger.error('Could not send reset email:', err);
                        });
                    }
                    return {};
                case 'complete_reset':
                    if (!this.emailHandler.resetPassword) {
                        throw new errors.realm.NotSupported({
                            detail: "The Password provider's emailHandler doesn't implement resetPassword."
                        });
                    }
                    const missingParams = [];
                    if (data.new_password === undefined || data.new_password === null) {
                        missingParams.push('new_password');
                    }
                    if (!data.token) {
                        missingParams.push('token');
                    }
                    if (missingParams.length > 0) {
                        throw new errors.realm.MissingParameters(...missingParams);
                    }
                    const existing = this.passwordRealm.objectForPrimaryKey('PasswordResetRequest', data.token);
                    if (!existing || existing.consumed || existing.expires < new Date()) {
                        throw new errors.realm.AccessDenied({
                            detail: 'The password reset token is invalid or has expired.'
                        });
                    }
                    this.passwordRealm.write(() => {
                        existing.consumed = new Date();
                    });
                    yield this.updatePassword(data.new_password, existing.userId);
                    return {};
                case 'request_email_confirmation':
                    if (!this.emailHandler.confirmEmail) {
                        throw new errors.realm.NotSupported({
                            detail: "The Password provider's emailHandler doesn't implement confirmEmail."
                        });
                    }
                    if (user) {
                        this.requestEmailConfirmation(user, providerId);
                    }
                    return {};
                case 'confirm_email':
                    if (!this.emailHandler.confirmEmail) {
                        throw new errors.realm.NotSupported({
                            detail: "The Password provider's emailHandler doesn't implement confirmEmail."
                        });
                    }
                    if (!data.token) {
                        throw new errors.realm.MissingParameters('token');
                    }
                    const confirmationRequest = this.passwordRealm.objectForPrimaryKey('EmailConfirmationRequest', data.token);
                    if (!confirmationRequest || confirmationRequest.consumed || confirmationRequest.expires < new Date()) {
                        throw new errors.realm.AccessDenied({
                            detail: 'The email confirmation token is invalid or has expired.'
                        });
                    }
                    const passwordHash = this.passwordRealm.objectForPrimaryKey('PasswordSaltHash', confirmationRequest.userId);
                    this.passwordRealm.write(() => {
                        passwordHash.isEmailConfirmed = true;
                        confirmationRequest.consumed = new Date();
                    });
                    this.service.updateUserMetadata(confirmationRequest.userId, {
                        isEmailConfirmed: 'true'
                    });
                    return {};
                default:
                    throw new errors.realm.InvalidParameters({
                        name: 'action',
                        reason: `The action ${data.action} is not supported.`
                    });
            }
        });
    }
    deleteUser(userId) {
        return __awaiter(this, void 0, void 0, function* () {
            let userDeleted = false;
            this.passwordRealm.write(() => {
                const foundPassword = this.passwordRealm.objectForPrimaryKey('PasswordSaltHash', userId);
                if (foundPassword) {
                    userDeleted = true;
                    this.passwordRealm.delete(foundPassword);
                }
            });
            return userDeleted;
        });
    }
    updatePassword(newPassword, userId) {
        return __awaiter(this, void 0, void 0, function* () {
            const salt = yield this.createSalt(this.saltLength);
            const hash = yield this.hashPassword(newPassword, salt, this.iterations, this.keyLength, this.digest);
            this.passwordRealm.write(() => {
                const existingHash = this.passwordRealm.objectForPrimaryKey('PasswordSaltHash', userId);
                this.passwordRealm.create('PasswordSaltHash', {
                    salt: salt.toString("base64"),
                    iterations: this.iterations,
                    keyLength: this.keyLength,
                    digest: this.digest,
                    hash: hash.toString("base64"),
                    userId: userId,
                    isEmailConfirmed: (existingHash && existingHash.isEmailConfirmed) || false
                }, true);
            });
        });
    }
    requestEmailConfirmation(user, email) {
        const token = crypto.randomBytes(32).toString('hex');
        this.passwordRealm.write(() => {
            this.passwordRealm.create('EmailConfirmationRequest', {
                token,
                expires: moment().add(1, 'month').toDate(),
                userId: user.userId,
                email
            });
        });
        this.emailHandler.confirmEmail(email, token)
            .catch((err) => {
            this.service.logger.error('Could not send confirmation email:', err);
        });
    }
    hashPassword(clearTextPassword, salt, iterations, keyLength, digest) {
        return __awaiter(this, void 0, void 0, function* () {
            return shared_1.Promisify(crypto.pbkdf2.bind(crypto), clearTextPassword, salt, iterations, keyLength, digest);
        });
    }
    createSalt(saltLength) {
        return __awaiter(this, void 0, void 0, function* () {
            return shared_1.Promisify(crypto.randomBytes.bind(crypto), saltLength);
        });
    }
    comparePassword(candidatePassword, salt, iterations, keyLength, digest, hash) {
        return __awaiter(this, void 0, void 0, function* () {
            const hashedCandidatePassword = yield this.hashPassword(candidatePassword, salt, iterations, keyLength, digest);
            return hashedCandidatePassword.equals(hash);
        });
    }
    attemptToRegister(username, password, isAdmin) {
        return __awaiter(this, void 0, void 0, function* () {
            const foundUser = this.service.getUserByProviderId(this.name, username);
            if (foundUser) {
                throw new errors.realm.InvalidCredentials();
            }
            const user = yield this.service.createOrUpdateUser(username, this.name, isAdmin);
            yield this.updatePassword(password, user.userId);
            if (this.emailHandler && this.emailHandler.confirmEmail) {
                this.requestEmailConfirmation(user, username);
            }
            return user;
        });
    }
    attemptToLogin(username, password) {
        return __awaiter(this, void 0, void 0, function* () {
            const foundUser = this.service.getUserByProviderId(this.name, username);
            if (!foundUser) {
                const salt = yield this.createSalt(this.saltLength);
                const hash = yield this.hashPassword(password, salt, this.iterations, this.keyLength, this.digest);
                throw new errors.realm.InvalidCredentials();
            }
            const foundPassword = this.passwordRealm.objectForPrimaryKey('PasswordSaltHash', foundUser.userId);
            if (!foundPassword) {
                const salt = yield this.createSalt(this.saltLength);
                const hash = yield this.hashPassword(password, salt, this.iterations, this.keyLength, this.digest);
                throw new errors.realm.InvalidCredentials();
            }
            const passwordMatch = yield this.comparePassword(password, new Buffer(foundPassword.salt, "base64"), foundPassword.iterations, foundPassword.keyLength, foundPassword.digest, new Buffer(foundPassword.hash, "base64"));
            if (!passwordMatch) {
                throw new errors.realm.InvalidCredentials();
            }
            if (foundPassword.isEmailConfirmed !== foundUser.isEmailConfirmed()) {
                this.service.updateUserMetadata(foundUser, {
                    isEmailConfirmed: foundPassword.isEmailConfirmed ? 'true' : 'false'
                });
            }
            return foundUser;
        });
    }
}
exports.PasswordAuthProvider = PasswordAuthProvider;
class BasicEmailHandler {
    constructor(config, logger) {
        this.transporter = nodemailer.createTransport(config.connectionString);
        this.from = config.from;
        this.logger = logger;
        this.resetActionConfig = config.resetActionConfig || {
            subject: BasicEmailHandler.defaultResetSubject,
            textTemplate: BasicEmailHandler.defaultResetTextTemplate,
            htmlTemplate: BasicEmailHandler.defaultResetHtmlTemplate
        };
        this.confirmActionConfig = config.confirmActionConfig || {
            subject: BasicEmailHandler.defaultConfirmSubject,
            textTemplate: BasicEmailHandler.defaultConfirmTextTemplate,
            htmlTemplate: BasicEmailHandler.defaultConfirmHtmlTemplate
        };
        this.transporter.verify().catch((err) => {
            this.logger.error(`Email configuration for the Password provider is invalid: ${err}`);
        });
        const trimRegex = /\/+$/g;
        this.baseUrl = config.baseUrl.replace(trimRegex, '');
        this.validate();
    }
    resetPassword(email, token, userAgent, remoteIp) {
        return __awaiter(this, void 0, void 0, function* () {
            const templateVars = { TOKEN: token, IP: remoteIp, BASE_URL: this.baseUrl || '[missing baseUrl]' };
            const message = {
                from: this.from,
                to: email,
                subject: this.resetActionConfig.subject,
                text: this.renderEmail(this.resetActionConfig.textTemplate, templateVars),
                html: this.renderEmail(this.resetActionConfig.htmlTemplate, templateVars),
            };
            const response = yield this.transporter.sendMail(message);
            this.logger.detail(`Password reset email sent for ${email}: ${JSON.stringify(response)}`);
        });
    }
    confirmEmail(email, token) {
        return __awaiter(this, void 0, void 0, function* () {
            const templateVars = { TOKEN: token, BASE_URL: this.baseUrl || '[missing baseUrl]' };
            const message = {
                from: this.from,
                to: email,
                subject: this.confirmActionConfig.subject,
                text: this.renderEmail(this.confirmActionConfig.textTemplate, templateVars),
                html: this.renderEmail(this.confirmActionConfig.htmlTemplate, templateVars),
            };
            const response = yield this.transporter.sendMail(message);
            this.logger.detail(`Confirm email sent for ${email}: ${JSON.stringify(response)}`);
        });
    }
    renderEmail(template, vars) {
        let result = template;
        for (const key in vars) {
            const pattern = new RegExp(`%${key}%`, 'g');
            result = result.replace(pattern, vars[key]);
        }
        return result;
    }
    validate() {
        if (!this.baseUrl) {
            const key = '%BASE_URL%';
            assert(this.resetActionConfig.textTemplate.indexOf(key) === -1, `Reset password text template uses ${key} but baseUrl is not configured`);
            assert(this.resetActionConfig.htmlTemplate.indexOf(key) === -1, `Reset password html template uses ${key} but baseUrl is not configured`);
            assert(this.confirmActionConfig.textTemplate.indexOf(key) === -1, `Confirm email text template uses ${key} but baseUrl is not configured`);
            assert(this.confirmActionConfig.htmlTemplate.indexOf(key) === -1, `Confirm email html template uses ${key} but baseUrl is not configured`);
        }
    }
}
BasicEmailHandler.defaultResetSubject = 'Password reset requested';
BasicEmailHandler.defaultResetTextTemplate = emails_1.emails.resetPassword.text;
BasicEmailHandler.defaultResetHtmlTemplate = emails_1.emails.resetPassword.html;
BasicEmailHandler.defaultConfirmSubject = 'Confirm your email';
BasicEmailHandler.defaultConfirmTextTemplate = emails_1.emails.confirmEmail.text;
BasicEmailHandler.defaultConfirmHtmlTemplate = emails_1.emails.confirmEmail.html;
//# sourceMappingURL=PasswordAuthProvider.js.map