1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const tslib_1 = require("tslib");
|
4 | const sdk_1 = require("@cto.ai/sdk");
|
5 | const command_1 = tslib_1.__importStar(require("@oclif/command"));
|
6 | exports.flags = command_1.flags;
|
7 | const debug_1 = tslib_1.__importDefault(require("debug"));
|
8 | const jsonwebtoken_1 = tslib_1.__importDefault(require("jsonwebtoken"));
|
9 | const services_1 = require("./services");
|
10 | const utils_1 = require("./utils");
|
11 | const env_1 = require("./constants/env");
|
12 | const CustomErrors_1 = require("./errors/CustomErrors");
|
13 | const debug = debug_1.default('ops:BaseCommand');
|
14 | class 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 |
|
21 | const { exp: refreshTokenExp } = jsonwebtoken_1.default.decode(refreshToken);
|
22 | const clockTimestamp = Math.floor(Date.now() / 1000);
|
23 | |
24 |
|
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 |
|
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 |
|
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 |
|
102 | const sessionState = this.state.config
|
103 | ? this.state.config.tokens
|
104 | ? this.state.config.tokens.sessionState
|
105 | : null
|
106 | : null;
|
107 |
|
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 | }
|
193 | exports.default = CTOCommand;
|