@cto.ai/ops
Version:
💻 CTO.ai - The CLI built for Teams 🚀
196 lines (195 loc) • 8.44 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.flags = exports.default = void 0;
const tslib_1 = require("tslib");
const sdk = require("@cto.ai/cli-sdk");
const { ux: UX } = sdk;
const command_1 = tslib_1.__importStar(require("@oclif/command"));
Object.defineProperty(exports, "flags", { enumerable: true, get: function () { return command_1.flags; } });
const debug_1 = tslib_1.__importDefault(require("debug"));
const jsonwebtoken_1 = tslib_1.__importDefault(require("jsonwebtoken"));
const services_1 = require("./services");
const utils_1 = require("./utils");
const env_1 = require("./constants/env");
const CustomErrors_1 = require("./errors/CustomErrors");
const debug = (0, debug_1.default)('ops:BaseCommand');
class CTOCommand extends command_1.default {
constructor(argv, config, services = services_1.defaultServicesList) {
super(argv, config);
this.ux = UX;
this.isTokenValid = (tokens) => {
const { refreshToken } = tokens;
// NOTE: This assumes that `idtoken` is a valid JWT
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;
config.version = this.config.version;
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();
}
// NOTE: This assumes that `idtoken` is a valid JWT
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 (0, utils_1.writeConfig)(oldConfigObj, newConfigObj, this.config.configDir);
};
this.readConfig = async () => {
return (0, utils_1.readConfig)(this.config.configDir);
};
this.clearConfig = async () => {
return (0, 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) {
const { accessToken, refreshToken } = this.state.config.tokens;
this.services.keycloakService
.InvalidateSession(accessToken, refreshToken)
.catch(err => {
debug('error signing out', err);
});
}
};
this.initConfig = async (tokens) => {
await this.clearConfig();
const signinFlowPipeline = (0, 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;
}
async pickFromList(items, question) {
switch (items.length) {
case 0:
throw new CustomErrors_1.EmptyListError();
case 1:
return items[0];
default:
const answers = await this.ux.prompt({
type: 'list',
name: 'listSelection',
message: `${question} ${this.ux.colors.reset.green('→')}`,
choices: items,
});
return answers.listSelection;
}
}
}
exports.default = CTOCommand;