UNPKG

8.27 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const sdk_1 = require("@cto.ai/sdk");
5const command_1 = tslib_1.__importStar(require("@oclif/command"));
6exports.flags = command_1.flags;
7const debug_1 = tslib_1.__importDefault(require("debug"));
8const jsonwebtoken_1 = tslib_1.__importDefault(require("jsonwebtoken"));
9const services_1 = require("./services");
10const utils_1 = require("./utils");
11const env_1 = require("./constants/env");
12const CustomErrors_1 = require("./errors/CustomErrors");
13const debug = debug_1.default('ops:BaseCommand');
14class CTOCommand extends command_1.default {
15 constructor(argv, config, services = services_1.defaultServicesList) {
16 super(argv, config);
17 this.ux = sdk_1.ux;
18 this.isTokenValid = (tokens) => {
19 const { refreshToken } = tokens;
20 // NOTE: This assumes that `idtoken` is a valid JWT
21 const { exp: refreshTokenExp } = jsonwebtoken_1.default.decode(refreshToken);
22 const clockTimestamp = Math.floor(Date.now() / 1000);
23 /*
24 * 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
25 */
26 return clockTimestamp < refreshTokenExp;
27 };
28 this.checkAndRefreshAccessToken = async (tokens) => {
29 debug('checking for valid access token');
30 try {
31 const { refreshToken } = tokens;
32 if (!this.isTokenValid(tokens))
33 throw new CustomErrors_1.TokenExpiredError();
34 /**
35 * The following code updates the access token every time a command is run
36 */
37 const oldConfig = await this.readConfig();
38 const newTokens = await this.services.keycloakService.refreshAccessToken(oldConfig, refreshToken);
39 this.accessToken = newTokens.accessToken;
40 await this.writeConfig(oldConfig, { tokens: newTokens });
41 const config = await this.readConfig();
42 this.state.config = config;
43 config.version = this.config.version;
44 return config;
45 }
46 catch (error) {
47 debug('%O', error);
48 await this.clearConfig();
49 throw new CustomErrors_1.TokenExpiredError();
50 }
51 };
52 this.fetchUserInfo = async ({ tokens }) => {
53 if (!tokens) {
54 this.ux.spinner.stop(`failed`);
55 this.log('missing parameter');
56 process.exit();
57 }
58 const { accessToken, idToken } = tokens;
59 if (!accessToken || !idToken) {
60 this.ux.spinner.stop(`❗️\n`);
61 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`);
62 process.exit();
63 }
64 // NOTE: This assumes that `idtoken` is a valid JWT
65 const { sub, preferred_username, email } = jsonwebtoken_1.default.decode(idToken);
66 const me = {
67 id: sub,
68 username: preferred_username,
69 email,
70 };
71 const { data: teams } = await this.services.api
72 .find('/private/teams', {
73 query: {
74 userId: sub,
75 },
76 headers: { Authorization: accessToken },
77 })
78 .catch(err => {
79 debug('%O', err);
80 throw new CustomErrors_1.APIError(err);
81 });
82 if (!teams) {
83 throw new CustomErrors_1.APIError('According to the API, this user does not belong to any teams.');
84 }
85 const meResponse = {
86 me,
87 teams,
88 };
89 return { meResponse, tokens };
90 };
91 this.writeConfig = async (oldConfigObj = {}, newConfigObj) => {
92 return utils_1.writeConfig(oldConfigObj, newConfigObj, this.config.configDir);
93 };
94 this.readConfig = async () => {
95 return utils_1.readConfig(this.config.configDir);
96 };
97 this.clearConfig = async () => {
98 return utils_1.clearConfig(this.config.configDir);
99 };
100 this.invalidateKeycloakSession = async () => {
101 // Obtains the session state if exists
102 const sessionState = this.state.config
103 ? this.state.config.tokens
104 ? this.state.config.tokens.sessionState
105 : null
106 : null;
107 // If session state exists, invalidate it
108 if (sessionState) {
109 const { accessToken, refreshToken } = this.state.config.tokens;
110 this.services.keycloakService
111 .InvalidateSession(accessToken, refreshToken)
112 .catch(err => {
113 debug('error signing out', err);
114 });
115 }
116 };
117 this.initConfig = async (tokens) => {
118 await this.clearConfig();
119 const signinFlowPipeline = utils_1.asyncPipe(this.fetchUserInfo, utils_1.formatConfigObject, this.writeConfig, this.readConfig);
120 const config = await signinFlowPipeline({ tokens });
121 return config;
122 };
123 this.services = Object.assign(services_1.defaultServicesList, services);
124 }
125 async init() {
126 try {
127 debug('initiating base command');
128 const config = await this.readConfig();
129 const { user, tokens, team } = config;
130 if (tokens) {
131 this.accessToken = tokens.accessToken;
132 }
133 this.user = user;
134 this.team = team;
135 this.state = { config };
136 }
137 catch (err) {
138 this.config.runHook('error', { err, accessToken: this.accessToken });
139 }
140 }
141 async isLoggedIn() {
142 debug('checking if user is logged in');
143 const config = await this.readConfig();
144 const { tokens } = config;
145 if (!this.user ||
146 !this.team ||
147 !this.accessToken ||
148 !tokens ||
149 !tokens.accessToken ||
150 !tokens.refreshToken ||
151 !tokens.idToken) {
152 this.log('');
153 this.log('✋ Sorry you need to be logged in to do that.');
154 this.log(`🎳 You can sign up with ${this.ux.colors.green('$')} ${this.ux.colors.callOutCyan('ops account:signup')}`);
155 this.log('');
156 this.log('❔ Please reach out to us with questions anytime!');
157 this.log(`⌚️ We are typically available ${this.ux.colors.white('Monday-Friday 9am-5pm PT')}.`);
158 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`);
159 this.log("🖖 We'll get back to you as soon as we possibly can.");
160 this.log('');
161 process.exit();
162 }
163 return this.checkAndRefreshAccessToken(tokens);
164 }
165 async validateUniqueField(query, accessToken) {
166 const response = await this.services.api
167 .find('/private/validate', {
168 query,
169 headers: { Authorization: accessToken },
170 })
171 .catch(err => {
172 throw new CustomErrors_1.APIError(err);
173 });
174 return response.data;
175 }
176 async pickFromList(items, question) {
177 switch (items.length) {
178 case 0:
179 throw new CustomErrors_1.EmptyListError();
180 case 1:
181 return items[0];
182 default:
183 const answers = await this.ux.prompt({
184 type: 'list',
185 name: 'listSelection',
186 message: `${question} ${this.ux.colors.reset.green('→')}`,
187 choices: items,
188 });
189 return answers.listSelection;
190 }
191 }
192}
193exports.default = CTOCommand;