mediumroast_api
Version:
Mediumroast for Git(Hub) SDK covering all categories of function.
259 lines (231 loc) • 8.79 kB
JavaScript
/**
* @fileoverview This file contains the code to authorize the user to the GitHub API
* @license Apache-2.0
* @version 3.0.0
*
* @author Michael Hay <michael.hay@mediumroast.io>
* @file authorize.js
* @copyright 2025 Mediumroast, Inc. All rights reserved.
*
* @class GitHubAuth
* @classdesc This class is used to authorize the user to the GitHub API
*
* @requires open
* @requires octoDevAuth
* @requires chalk
* @requires cli-table3
*
* @exports GitHubAuth
*
* @example
* import {GitHubAuth} from './api/authorize.js'
* const github = new GitHubAuth(env, environ, configFile)
* const githubToken = github.verifyAccessToken()
*
*/
import open from 'open';
import * as octoDevAuth from '@octokit/auth-oauth-device';
import chalk from 'chalk';
import Table from 'cli-table3';
class GitHubAuth {
/**
* @constructor
* @param {Object} env - The environment object
* @param {Object} environ - The environmentals object
* @param {String} configFile - The configuration file path
* @param {Boolean} configExists - Whether the configuration file exists
*/
constructor(env, environ, configFile, configExists) {
this.env = env;
this.clientType = 'github-app';
this.configFile = configFile;
this.configExists = configExists;
this.environ = environ;
this.config = configExists ? environ.readConfig(configFile) : null;
}
/**
* Verifies if the GitHub section exists in the configuration
* @returns {Boolean} True if the GitHub section exists, otherwise false
*/
verifyGitHubSection() {
if (!this.config) {
return false;
}
return this.config.hasSection('GitHub');
}
/**
* Gets a value from the configuration file
* @private
* @param {String} section - The section name in the config file
* @param {String} option - The option name in the section
* @returns {String|null} The value or null if not found
*/
getFromConfig(section, option) {
if (!this.config) return null;
return this.config.hasKey(section, option) ?
this.config.get(section, option) : null;
}
/**
* Gets the access token from the configuration file
* @returns {String|null} The access token or null if not found
*/
getAccessTokenFromConfig() {
return this.getFromConfig('GitHub', 'token');
}
/**
* Gets the authentication type from the configuration file
* @returns {String|null} The authentication type or null if not found
*/
getAuthTypeFromConfig() {
return this.getFromConfig('GitHub', 'authType');
}
/**
* Checks if a GitHub token is valid and not expired
* @async
* @param {String} token - The GitHub token to check
* @returns {Array} [isValid, statusObject, userData]
*/
async checkTokenExpiration(token) {
try {
const response = await fetch('https://api.github.com/user', {
method: 'GET',
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github.v3+json'
}
});
if (!response.ok) {
return [false, {status_code: response.status, status_msg: response.statusText}, null];
}
const data = await response.json();
return [true, {status_code: 200, status_msg: response.statusText}, data];
} catch (error) {
return [false, {status_code: 500, status_msg: error.message}, null];
}
}
/**
* Gets an access token using the GitHub device flow
* @async
* @returns {Object} The access token object
*/
async getAccessTokenDeviceFlow() {
// Set the clientId depending on if the config file exists
const clientId = this.configExists ? this.env.clientId : this.env.GitHub.clientId;
let deviceCode;
const deviceauth = octoDevAuth.createOAuthDeviceAuth({
clientType: this.clientType,
clientId: clientId,
onVerification(verifier) {
deviceCode = verifier.device_code;
// eslint-disable-next-line no-console
console.log(
chalk.blue.bold('If supported opening your browser to the Authorization website.\nIf your browser doesn\'t open, please copy and paste the Authorization website URL into your browser\'s address bar.\n')
);
const authWebsitePrefix = 'Authorization website:';
const authCodePrefix = 'Authorization code:';
const authWebsite = chalk.bold.red(verifier.verification_uri);
const authCode = chalk.bold.red(verifier.user_code);
const table = new Table({
rows: [
[authWebsitePrefix, authWebsite],
[authCodePrefix, authCode]
]
});
// Use table if available, fallback to plain text
const tableString = table.toString();
if (tableString !== '') {
// eslint-disable-next-line no-console
console.log(tableString);
} else {
// eslint-disable-next-line no-console
console.log(`\t${authWebsitePrefix} ${authWebsite}`);
// eslint-disable-next-line no-console
console.log(`\t${authCodePrefix} ${authCode}`);
}
// eslint-disable-next-line no-console
console.log('\nCopy and paste the Authorization code into correct field on the Authorization website. Once authorized setup will continue.\n');
open(verifier.verification_uri);
}
});
// Call GitHub to obtain the token
const accessToken = await deviceauth({type: 'oauth'});
accessToken.deviceCode = deviceCode;
return accessToken;
}
/**
* Verifies if the access token is valid and gets a new one if needed
* @async
* @param {Boolean} saveToConfig - Whether to save to the configuration file, default is true
* @returns {Array} [success, statusObject, tokenData]
*/
async verifyAccessToken(saveToConfig = true) {
// Check if config exists and has GitHub section
if (this.configExists && !this.verifyGitHubSection()) {
return [
false,
{status_code: 500, status_msg: 'The GitHub section is not available in the configuration file'},
null
];
}
// Get authorization details
let accessToken;
let authType = 'deviceFlow'; // Default
if (this.configExists) {
accessToken = this.getAccessTokenFromConfig();
authType = this.getAuthTypeFromConfig() || authType;
}
// Check token validity
const validToken = this.configExists ?
await this.checkTokenExpiration(accessToken) :
[false, {status_code: 500, status_msg: 'The configuration file isn\'t present'}, null];
// If token is valid, return it
if (validToken[0] && this.configExists) {
return [
true,
{status_code: 200, status_msg: validToken[1].status_msg},
{token: accessToken, authType: authType}
];
}
// Token is invalid or missing, handle based on auth type
if (authType === 'pat') {
// PAT is invalid, caller must handle
return [
false,
{
status_code: 500,
status_msg: `The Personal Access Token appears to be invalid and was rejected with an error message [${validToken[1].status_msg}].\n\tPlease obtain a new PAT and update the GitHub token setting in the configuration file [${this.configFile}].`
},
null
];
} else if (authType === 'deviceFlow') {
// Get new token via device flow
const tokenData = await this.getAccessTokenDeviceFlow();
// Update config if it exists and saveToConfig is true
if (this.configExists && this.config && saveToConfig) {
let tmpConfig = this.environ.updateConfigSetting(this.config, 'GitHub', 'token', tokenData.token);
tmpConfig = this.environ.updateConfigSetting(tmpConfig[1], 'GitHub', 'authType', authType);
tmpConfig = this.environ.updateConfigSetting(tmpConfig[1], 'GitHub', 'deviceCode', tokenData.deviceCode);
// Save updates
this.config = tmpConfig[1];
if (saveToConfig) {
await this.config.write(this.configFile);
}
}
return [
true,
{
status_code: 200,
status_msg: `The access token has been successfully updated and saved to the configuration file [${this.configFile}]`
},
{token: tokenData.token, authType: authType, deviceCode: tokenData.deviceCode}
];
}
// Fallback for unexpected auth type
return [
false,
{status_code: 500, status_msg: `Unsupported authentication type: ${authType}`},
null
];
}
}
export {GitHubAuth};