mod_user__user.js

/**
## /user

The _user module exports the user method to route User API requests.

- add
- admin
- cookie
- delete
- key
- list
- log
- login
- register
- token
- update
- verify

@requires module:/utils/reqHost

@module /user
*/

import reqHost from '../utils/reqHost.js';

import add from './add.js';
import admin from './admin.js';
import cookie from './cookie.js';
import deleteMethod from './delete.js'; // renamed because delete is a reserved word
import key from './key.js';
import list from './list.js';
import log from './log.js';
import login from './login.js';
import register from './register.js';
import token from './token.js';
import update from './update.js';
import verify from './verify.js';

const methods = {
  add,
  admin,
  cookie,
  delete: deleteMethod, // use the renamed import
  key,
  list,
  log,
  login,
  register,
  token,
  update,
  verify,
};

const previousAddress = {};

/**
@function user
@async

@description
The Mapp API uses the user method to lookup and route User API requests.

The route method assigns the host param from /utils/reqHost before the request and response arguments are passed a User API method identified by the method param.

The method request parameter must be an own member of the methods object, eg. `admin`, `register`, `verify`, `add`, `delete`, `update`, `list`, `log`, `key`, `token`, `cookie`, or `login`.

Requests to the user module are debounced by 5 seconds preventing registration, login, etc in quick succession from the same IP address.

@param {Object} req HTTP request.
@param {Object} res HTTP response.
@param {Object} req.params Request parameter.
@param {string} req.params.method Method request parameter.
*/
export default async function user(req, res) {
  if (!Object.hasOwn(methods, req.params.method)) {
    return res.send(`Failed to evaluate 'method' param.`);
  }

  if (req.body) {
    debounceRequest(req, res);

    if (res.finished) return;
  }

  req.params.host = reqHost(req);

  const method = await methods[req.params.method](req, res);

  if (method instanceof Error) {
    req.params.msg = method.message;
    methods.login(req, res);
  }
}

/**
@function debounceRequest

@description
The remote_address determined from the request header is stored in the previousAddress module variable. Requests from the same address within 30 seconds will be bounced.

@param {req} req HTTP request.
@param {res} res HTTP response.
@property {Object} req.params HTTP request parameter.
@property {Object} req.header HTTP request header.
*/
function debounceRequest(req, res) {
  // Admin user requests should not be debounced.
  if (req.params.user?.admin) return;

  if (!req.headers['x-forwarded-for']) {
    req.params.remote_address = 'unknown';
  } else {
    req.params.remote_address = /^[A-Za-z0-9.,_-\s]*$/.test(
      req.headers['x-forwarded-for'],
    )
      ? req.headers['x-forwarded-for']
      : 'invalid';
  }

  // The remote_address has been previously used
  if (
    Object.hasOwn(previousAddress, req.params.remote_address) &&
    // within 5 seconds or less.
    new Date() - previousAddress[req.params.remote_address] < 5000
  ) {
    res
      .status(429)
      .send(`Address ${req.params.remote_address} temporarily locked.`);

    return;
  }

  // Log the remote_address with the current datetime.
  previousAddress[req.params.remote_address] = new Date();
}