FoseFx/twitch-chatbot-boilerplate-core

View on GitHub
src/core/server/auth.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import fetch from 'node-fetch';
import { Response as FetchResponse } from 'node-fetch';
import { Request, Response, RequestHandler, NextFunction } from 'express';
import {
  StartServerOptions,
  TokenResponse,
  AuthData,
  BasicProfile,
} from './server.types';
import { writeToDisk, finishSetup } from '../setup';
import { ensureFetchIsOk } from './util';

/**
 * Converts array of scope strings to a list usable in URLs
 * @internal
 */
export function scopesToString(scopes: string[]): string {
  let string = '';

  for (const scope of scopes) {
    string += scope + ' ';
  }
  return string.substr(0, string.length - 1); // remove last " "
}

/**
 * Generates the Twitch-Authwall-URL
 * @internal
 * */
export function getOAuthUrl(
  opts: StartServerOptions,
  scopes: string[],
  callbackURI: string,
): string {
  const claims = {
    id_token: { email: null },
    userinfo: {},
  };

  const redirectURL =
    `https://id.twitch.tv/oauth2/authorize` +
    `?client_id=${opts.clientId}` +
    `&redirect_uri=${callbackURI}` +
    `&response_type=code` +
    `&scope=${_this.scopesToString(scopes)}` +
    `&claims=${JSON.stringify(claims)}` +
    `&force_verify=true`;
  return redirectURL;
}

/**
 * The RequestHandler that handles /setup/callback
 * @internal
 * */
export function setupCallback(options: StartServerOptions): RequestHandler {
  return function (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<void> {
    const { code } = req.query; // authorization code

    return _this
      .obtainAccessToken(
        options,
        code as string,
        `${options.host}/setup/callback`,
      )
      .then((token) => finishSetup(options, token))
      .then(() => res.render('ok'))
      .catch((e) => next(e));
  };
}

/** @internal */
export function obtainAccessToken(
  opts: StartServerOptions,
  code: string,
  redirectURI: string,
): Promise<TokenResponse> {
  const reqURL =
    `https://id.twitch.tv/oauth2/token` +
    `?client_id=${opts.clientId}` +
    `&client_secret=${opts.clientSecret}` +
    `&code=${code}` +
    `&grant_type=authorization_code` +
    `&redirect_uri=${redirectURI}`;

  return fetch(reqURL, {
    method: 'post',
  })
    .then(ensureFetchIsOk)
    .then((resp: FetchResponse) => resp.json());
}

/** @internal */
export async function refreshAccessToken(
  options: StartServerOptions,
  authData: AuthData,
  write: boolean,
): Promise<AuthData> {
  const refreshURL =
    `https://id.twitch.tv/oauth2/token` +
    `?grant_type=refresh_token` +
    `&refresh_token=${encodeURIComponent(authData.refresh_token)}` +
    `&client_id=${options.clientId}` +
    `&client_secret=${options.clientSecret}`;

  const resp = await fetch(refreshURL, {
    method: 'POST',
  });
  if (!resp.ok) {
    throw new Error(
      'Refresh request was rejected: ' +
        resp.status +
        ': ' +
        (await resp.text()),
    );
  }

  const json = (await resp.json()) as TokenResponse;
  if (write) {
    writeToDisk(json);
  }
  return json;
}

/** @internal */
export function getBasicProfileInfo(
  options: StartServerOptions,
  authData: AuthData,
): Promise<BasicProfile> {
  return fetch(`https://api.twitch.tv/helix/users`, {
    method: 'get',
    headers: {
      'Client-Id': options.clientId,
      Authorization: 'Bearer ' + authData.access_token,
    },
  })
    .then(ensureFetchIsOk)
    .then((resp) => resp.json())
    .then((json) => json.data[0]);
}

/** @internal */
export const _this = {
  obtainAccessToken,
  scopesToString,
  getOAuthUrl,
  setupCallback,
  refreshAccessToken,
};