/*
 * Copyright (C) Sapphirecode - All Rights Reserved
 * This file is part of Auth-Server-Helper which is released under MIT.
 * See file 'LICENSE' for full license details.
 * Created by Timo Hocker <timo@scode.ovh>, March 2020
 */

// @ts-nocheck
/* eslint-disable no-magic-numbers */

'use strict';

const password_helper = require ('@scode/password-helper');
const crypto = require ('@scode/crypto-helper');
const consts = require ('@scode/consts');

const me = {};

/**
 * initializes the module
 *
 * @param {Function<Promise|object>} get_user
 * function that returns {id:number, password:string, salt:string}
 * for a given user identifier
 * @param {Array<RegExp>} ignore_paths array of regex to skip auth
 * @returns {Function} request handler
 */
function init (get_user, ignore_paths = []) {
  me.get_user = get_user;
  me.session_timeout_milliseconds = 300000;
  me.ignore_paths = ignore_paths;
  me.jwt_secret = crypto.create_salt ();
  me.app_id = crypto.create_salt ();
  return request_handler;
}

/**
 * tries to authenticate a user
 *
 * @param {string} user name or email of the given user
 * @param {string} password hashed password
 * @returns {Promise<string>} session key if successful
 */
async function authenticate (user, password) {
  const user_entry
    = await new Promise ((res) => res (me.get_user (user)));

  if (!user_entry)
    return null;

  if (!await password_helper.verify (user_entry.password, password))
    return null;

  const session_key = crypto.sign_object (
    { id: user_entry.id },
    me.jwt_secret
  );

  return session_key;
}

/**
 * gets the correct salt for a given user
 *
 * @param {string} user user name or email to query
 */
async function salt (user) {
  const user_entry
  = await new Promise ((res) => res (me.get_user (user)));
  if (!user_entry)
    return null;

  return user_entry.salt;
}

/**
 * block if no auth header found
 *
 * @param {string} session session key
 * @param {string} user user name
 * @param {any} res response object
 * @returns {boolean} true if handler blocked request
 */
function request_handler_block (session, user, res) {
  if (typeof session === 'undefined' && typeof user === 'undefined') {
    res.status (consts.http.status_unauthorized);
    res.end ();
    return true;
  }
  return false;
}

/**
 * handle authentication
 *
 * @param {string} session session key
 * @param {string} user user name
 * @param {string} key user hash
 * @param {any} res response object
 * @param {any} next next handler
 * @returns {Promise<boolean>} true if handler authenticated
 */
async function request_handler_authenticate (session, user, key, res, next) {
  if (typeof session === 'undefined' && typeof user !== 'undefined') {
    if (typeof key === 'undefined') {
      const user_salt = await salt (user);
      res.status (
        user_salt === null
          ? consts.http.status_forbidden
          : consts.http.status_ok
      );
      res.end (user_salt);
      return true;
    }

    const session_key = await authenticate (user, key);

    res.status (
      session_key === null
        ? consts.http.status_forbidden
        : consts.http.status_ok
    )
      .cookie (me.app_id, session_key, { maxAge: 900000, httpOnly: true })
      .end (session_key);
    return true;
  }
  try {
    const jwt = crypto.verify_signature (
      session,
      me.jwt_secret,
      me.session_timeout_milliseconds
    );
    res.locals.user_id = jwt.id;
    const new_user_token = crypto.sign_object (
      { id: jwt.id },
      me.jwt_secret
    );
    res.cookie (
      me.app_id,
      new_user_token,
      { maxAge: 900000, httpOnly: true }
    )
      .header ('session', new_user_token);
    next ();
    return true;
  }
  catch (err) {
    return false;
  }
}

/**
 * check if a filter matches a request
 *
 * @param {any} req request
 * @param {any} filter filter
 * @returns {boolean} true if filter matches
 */
function filter_matches (req, filter) {
  if (filter instanceof RegExp && filter.test (req.url))
    return true;
  return req.method === filter.method
    && filter.regex
    && filter.regex.test (req.url);
}

/**
 * handles http requests
 *
 * @param {any} req request
 * @param {any} res response
 * @param {any} next next handler
 */
async function request_handler (req, res, next) {
  if (Array.isArray (me.ignore_paths)) {
    for (const ignore of me.ignore_paths) {
      if (filter_matches (req, ignore)) {
        next ();
        return;
      }
    }
  }

  const { user, key, session: header_session } = req.headers;
  const cookie_session = typeof req.cookies === 'undefined'
    ? null
    : req.cookies[me.app_id];

  const session = cookie_session || header_session;

  if (request_handler_block (session, user, res))
    return;
  if (await request_handler_authenticate (session, user, key, res, next))
    return;

  res.status (consts.http.status_forbidden);
  res.end ();
}

module.exports = init;