"use strict";
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.accounts = void 0;
exports.expandUsername = expandUsername;
exports.ctxBelongsTo = ctxBelongsTo;
exports.getUsernames = getUsernames;
exports.getAccount = getAccount;
exports.saveSrpInfo = saveSrpInfo;
exports.createAdmin = createAdmin;
exports.updateAccount = updateAccount;
exports.normalizeUsername = normalizeUsername;
exports.renameAccount = renameAccount;
exports.addAccount = addAccount;
exports.delAccount = delAccount;
exports.getFromAccount = getFromAccount;
exports.accountHasPassword = accountHasPassword;
exports.accountHasLoginMethod = accountHasLoginMethod;
exports.accountCanLogin = accountCanLogin;
exports.accountIsDisabled = accountIsDisabled;
exports.accountCanLoginAdmin = accountCanLoginAdmin;
exports.accountCanChangePassword = accountCanChangePassword;
const lodash_1 = __importDefault(require("lodash"));
const misc_1 = require("./misc");
const config_1 = require("./config");
const tssrp6a_1 = require("tssrp6a");
const events_1 = __importDefault(require("./events"));
const auth_1 = require("./auth");
// provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
function expandUsername(who) {
    const ret = new Set();
    const q = [who];
    for (const u of q) {
        const a = getAccount(u);
        if (!a || a.disabled)
            continue;
        ret.add(u);
        if (a.belongs)
            q.push(...a.belongs);
    }
    return ret;
}
// check if current username or any ancestor match the provided usernames
function ctxBelongsTo(ctx, usernames) {
    const s = ctx.state.usernames ||= expandUsername((0, auth_1.getCurrentUsername)(ctx));
    return usernames.some(u => s.has(u)); // cache ancestors' usernames inside context state
}
function getUsernames() {
    return Object.keys(exports.accounts.get());
}
function getAccount(username, normalize = true) {
    if (normalize)
        username = normalizeUsername(username);
    return username ? exports.accounts.get()[username] : undefined;
}
function saveSrpInfo(account, salt, verifier) {
    account.srp = String(salt) + '|' + String(verifier);
}
const createAdminConfig = (0, config_1.defineConfig)('create-admin', '');
createAdminConfig.sub(v => {
    if (!v)
        return;
    createAdminConfig.set('');
    // we can't createAdmin right away, as its changes will be lost after return, when our caller (setConfig) applies undefined properties. setTimeout is good enough, as the process is sync.
    setTimeout(() => createAdmin(v));
});
async function createAdmin(password, username = 'admin') {
    const acc = await addAccount(username, { admin: true, password }, true);
    console.log(acc ? "Account admin set" : "Something went wrong");
}
const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters());
async function updateAccount(account, change) {
    const jsonWas = JSON.stringify(account);
    const { username: usernameWas } = account;
    if (typeof change === 'function')
        await change?.(account);
    else {
        const u = normalizeUsername(change.username || '');
        if (u && u !== usernameWas && getAccount(u))
            throw "username already exists";
        Object.assign(account, lodash_1.default.mapValues(change, x => x || undefined));
    }
    for (const [k, v] of (0, misc_1.typedEntries)(account))
        if (!v)
            delete account[k]; // we consider all account fields, when falsy, as equivalent to be missing (so, default value applies)
    const { username, password } = account;
    if (password) {
        console.debug('Hashing password for', username);
        delete account.password;
        const res = await (0, tssrp6a_1.createVerifierAndSalt)(srp6aNimbusRoutines, username, password);
        saveSrpInfo(account, res.s, res.v);
    }
    if (account.belongs) {
        account.belongs = (0, misc_1.wantArray)(account.belongs);
        lodash_1.default.remove(account.belongs, b => {
            if (exports.accounts.get().hasOwnProperty(b))
                return;
            console.error(`Account ${username} belongs to non-existing ${b}`);
            return true;
        });
        if (!account.belongs.length)
            delete account.belongs;
    }
    account.expire &&= new Date(account.expire);
    if (username !== usernameWas)
        renameAccount(usernameWas, username);
    if (jsonWas !== JSON.stringify(account)) // this test will miss the 'username' field, because hidden, but renameAccount is already calling saveAccountsASAP
        saveAccountsAsap();
}
const saveAccountsAsap = config_1.saveConfigAsap;
exports.accounts = (0, config_1.defineConfig)('accounts', {});
exports.accounts.sub(lodash_1.default.debounce(obj => {
    // consider some validation here, in case of manual edit of the config
    lodash_1.default.each(obj, (rec, k) => {
        const norm = normalizeUsername(k);
        if (rec?.username !== norm) {
            if (!rec) // an empty object in yaml is parsed as null
                rec = obj[norm] = { username: norm };
            else if ((0, misc_1.objRenameKey)(obj, k, norm))
                saveAccountsAsap();
            (0, misc_1.setHidden)(rec, { username: norm });
        }
        void updateAccount(rec, {}); // work fields
        removeLoops(norm);
    });
    function removeLoops(normalizedUsername, visiting = new Set()) {
        if (visiting.has(normalizedUsername))
            return;
        visiting.add(normalizedUsername);
        const account = obj[normalizedUsername];
        const removed = lodash_1.default.remove(account.belongs, parent => {
            const k = normalizeUsername(parent);
            return obj[k] && visiting.has(k);
        });
        if (removed.length)
            saveAccountsAsap();
        if (account?.belongs?.length) {
            for (const parent of account.belongs) {
                const k = normalizeUsername(parent);
                if (obj[k])
                    removeLoops(k, visiting);
            }
            if (!account.belongs.length)
                delete account.belongs;
        }
        visiting.delete(normalizedUsername);
    }
})); // don't trigger in the middle of a series of deletion, as we may have an inconsistent state
function normalizeUsername(username) {
    return username.toLocaleLowerCase();
}
function renameAccount(from, to) {
    from = normalizeUsername(from);
    const as = exports.accounts.get();
    to = normalizeUsername(to);
    if (!to || !as[from] || as[to])
        return false;
    if (to === from)
        return true;
    (0, misc_1.objRenameKey)(as, from, to);
    (0, misc_1.setHidden)(as[to], { username: to });
    // update references
    for (const a of Object.values(as)) {
        const idx = a.belongs?.indexOf(from);
        if (idx !== undefined && idx >= 0)
            a.belongs[idx] = to;
    }
    exports.accounts.set(as);
    events_1.default.emit('accountRenamed', { from, to }); // everybody, take care of your stuff
    saveAccountsAsap();
    return true;
}
function addAccount(username, props, updateExisting = false) {
    username = normalizeUsername(username);
    if (!username)
        return;
    let account = getAccount(username, false);
    if (account && !updateExisting)
        return;
    account = (0, misc_1.setHidden)(account || {}, { username }); // hidden so that stringification won't include it
    Object.assign(account, lodash_1.default.pickBy(props, Boolean));
    exports.accounts.set(was => Object.assign(was, { [username]: account }));
    return updateAccount(account, account).then(() => account);
}
function delAccount(username) {
    if (!getAccount(username))
        return false;
    exports.accounts.set(was => lodash_1.default.omit(was, normalizeUsername(username)));
    saveAccountsAsap();
    return true;
}
// get some property from account, searching in its groups if necessary. Search is breadth-first, and this determines priority of inheritance.
function getFromAccount(account, getter) {
    const search = [account];
    for (const accountOrUsername of search) {
        const a = typeof accountOrUsername === 'string' ? getAccount(accountOrUsername) : accountOrUsername;
        if (!a)
            continue;
        const res = getter(a);
        if (res !== undefined)
            return res;
        if (a.belongs)
            search.push(...a.belongs);
    }
}
function accountHasPassword(account) {
    return Boolean(account.password || account.srp);
}
function accountHasLoginMethod(account) {
    return Boolean(accountHasPassword(account) || account.plugin?.auth || account.auto_login_net);
}
function accountCanLogin(account) {
    return accountHasLoginMethod(account) && !accountIsDisabled(account);
}
function accountIsDisabled(account) {
    return Boolean(account.disabled
        || account.expire < Date.now()
        || account.belongs?.length // don't every() on empty array, as it returns true
            && account.belongs.map(u => getAccount(u, false)).every(a => a && accountIsDisabled(a)));
}
function accountCanLoginAdmin(account) {
    return accountCanLogin(account) && getFromAccount(account, a => a.admin) || false;
}
function accountCanChangePassword(account) {
    return account && !getFromAccount(account, a => a.disable_password_change);
}
