UNPKG

@cto.ai/ops

Version:

💻 CTO.ai Ops - The CLI built for Teams 🚀

172 lines (171 loc) 7.52 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const sdk_1 = require("@cto.ai/sdk"); const command_1 = tslib_1.__importStar(require("@oclif/command")); exports.flags = command_1.flags; const debug_1 = tslib_1.__importDefault(require("debug")); const jsonwebtoken_1 = tslib_1.__importDefault(require("jsonwebtoken")); const axios_1 = tslib_1.__importDefault(require("axios")); const services_1 = require("./services"); const utils_1 = require("./utils"); const env_1 = require("./constants/env"); const CustomErrors_1 = require("./errors/CustomErrors"); const debug = debug_1.default('ops:BaseCommand'); class CTOCommand extends command_1.default { constructor(argv, config, services = services_1.defaultServicesList) { super(argv, config); this.ux = sdk_1.ux; this.isTokenValid = (tokens) => { const { refreshToken } = tokens; const { exp: refreshTokenExp } = jsonwebtoken_1.default.decode(refreshToken); const clockTimestamp = Math.floor(Date.now() / 1000); /* * Note: when the token is an offline token, refreshTokenExp will be equal to 0. We are not issuing offline tokens at the moment, but if we do, we need to add the extra condition that refreshTokenExp !== 0 */ return clockTimestamp < refreshTokenExp; }; this.checkAndRefreshAccessToken = async (tokens) => { debug('checking for valid access token'); try { const { refreshToken } = tokens; if (!this.isTokenValid(tokens)) throw new CustomErrors_1.TokenExpiredError(); /** * The following code updates the access token every time a command is run */ const oldConfig = await this.readConfig(); const newTokens = await this.services.keycloakService.refreshAccessToken(oldConfig, refreshToken); this.accessToken = newTokens.accessToken; await this.writeConfig(oldConfig, { tokens: newTokens }); const config = await this.readConfig(); this.state.config = config; return config; } catch (error) { debug('%O', error); await this.clearConfig(); throw new CustomErrors_1.TokenExpiredError(); } }; this.fetchUserInfo = async ({ tokens }) => { if (!tokens) { this.ux.spinner.stop(`failed`); this.log('missing parameter'); process.exit(); } const { accessToken, idToken } = tokens; if (!accessToken || !idToken) { this.ux.spinner.stop(`❗️\n`); this.log(`🤔 Sorry, we couldn’t find an account with that email or password.\nForgot your password? Run ${this.ux.colors.bold('ops account:reset')}.\n`); process.exit(); } const { sub, preferred_username, email } = jsonwebtoken_1.default.decode(idToken); const me = { id: sub, username: preferred_username, email, }; const { data: teams } = await this.services.api .find('/private/teams', { query: { userId: sub, }, headers: { Authorization: accessToken }, }) .catch(err => { debug('%O', err); throw new CustomErrors_1.APIError(err); }); if (!teams) { throw new CustomErrors_1.APIError('According to the API, this user does not belong to any teams.'); } const meResponse = { me, teams, }; return { meResponse, tokens }; }; this.writeConfig = async (oldConfigObj = {}, newConfigObj) => { return utils_1.writeConfig(oldConfigObj, newConfigObj, this.config.configDir); }; this.readConfig = async () => { return utils_1.readConfig(this.config.configDir); }; this.clearConfig = async () => { return utils_1.clearConfig(this.config.configDir); }; this.invalidateKeycloakSession = async () => { // Obtains the session state if exists const sessionState = this.state.config ? this.state.config.tokens ? this.state.config.tokens.sessionState : null : null; // If session state exists, invalidate it if (sessionState) await axios_1.default.get(this.services.keycloakService.buildInvalidateSessionUrl(), { headers: this.services.keycloakService.buildInvalidateSessionHeaders(sessionState, this.accessToken), }); }; this.initConfig = async (tokens) => { await this.clearConfig(); const signinFlowPipeline = utils_1.asyncPipe(this.fetchUserInfo, utils_1.formatConfigObject, this.writeConfig, this.readConfig); const config = await signinFlowPipeline({ tokens }); return config; }; this.services = Object.assign(services_1.defaultServicesList, services); } async init() { try { debug('initiating base command'); const config = await this.readConfig(); const { user, tokens, team } = config; if (tokens) { this.accessToken = tokens.accessToken; } this.user = user; this.team = team; this.state = { config }; } catch (err) { this.config.runHook('error', { err, accessToken: this.accessToken }); } } async isLoggedIn() { debug('checking if user is logged in'); const config = await this.readConfig(); const { tokens } = config; if (!this.user || !this.team || !this.accessToken || !tokens || !tokens.accessToken || !tokens.refreshToken || !tokens.idToken) { this.log(''); this.log('✋ Sorry you need to be logged in to do that.'); this.log(`🎳 You can sign up with ${this.ux.colors.green('$')} ${this.ux.colors.callOutCyan('ops account:signup')}`); this.log(''); this.log('❔ Please reach out to us with questions anytime!'); this.log(`⌚️ We are typically available ${this.ux.colors.white('Monday-Friday 9am-5pm PT')}.`); this.log(`📬 You can always reach us by ${this.ux.url('email', `mailto:${env_1.INTERCOM_EMAIL}`)} ${this.ux.colors.dim(`(${env_1.INTERCOM_EMAIL})`)}.\n`); this.log("🖖 We'll get back to you as soon as we possibly can."); this.log(''); process.exit(); } return this.checkAndRefreshAccessToken(tokens); } async validateUniqueField(query, accessToken) { const response = await this.services.api .find('/private/validate', { query, headers: { Authorization: accessToken }, }) .catch(err => { throw new CustomErrors_1.APIError(err); }); return response.data; } } exports.default = CTOCommand;