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 axios_1 = tslib_1.__importDefault(require("axios"));
|
10 | const utils_1 = require("./utils");
|
11 | const env_1 = require("./constants/env");
|
12 | const Feathers_1 = require("./services/Feathers");
|
13 | const Analytics_1 = require("./services/Analytics");
|
14 | const CustomErrors_1 = require("./errors/CustomErrors");
|
15 | const Publish_1 = require("./services/Publish");
|
16 | const BuildSteps_1 = require("./services/BuildSteps");
|
17 | const Image_1 = require("./services/Image");
|
18 | const Workflow_1 = require("./services/Workflow");
|
19 | const Op_1 = require("./services/Op");
|
20 | const Keycloak_1 = require("./services/Keycloak");
|
21 | const RegistryAuth_1 = require("./services/RegistryAuth");
|
22 | const debug = debug_1.default('ops:BaseCommand');
|
23 | class CTOCommand extends command_1.default {
|
24 | constructor(argv, config, services = {
|
25 | api: new Feathers_1.FeathersClient(),
|
26 | publishService: new Publish_1.Publish(),
|
27 | buildStepService: new BuildSteps_1.BuildSteps(),
|
28 | imageService: new Image_1.ImageService(),
|
29 | analytics: new Analytics_1.AnalyticsService(env_1.OPS_SEGMENT_KEY),
|
30 | workflowService: new Workflow_1.WorkflowService(),
|
31 | opService: new Op_1.OpService(),
|
32 | keycloakService: new Keycloak_1.KeycloakService(),
|
33 | registryAuthService: new RegistryAuth_1.RegistryAuthService(),
|
34 | }) {
|
35 | super(argv, config);
|
36 | this.ux = sdk_1.ux;
|
37 | this.isTokenValid = (tokens) => {
|
38 | const { refreshToken } = tokens;
|
39 | const { exp: refreshTokenExp } = jsonwebtoken_1.default.decode(refreshToken);
|
40 | const clockTimestamp = Math.floor(Date.now() / 1000);
|
41 | |
42 |
|
43 |
|
44 | return clockTimestamp < refreshTokenExp;
|
45 | };
|
46 | this.checkAndRefreshAccessToken = async (tokens) => {
|
47 | debug('checking for valid access token');
|
48 | try {
|
49 | const { refreshToken } = tokens;
|
50 | if (!this.isTokenValid(tokens))
|
51 | throw new CustomErrors_1.TokenExpiredError();
|
52 | |
53 |
|
54 |
|
55 | const oldConfig = await this.readConfig();
|
56 | const newTokens = await this.services.keycloakService.refreshAccessToken(oldConfig, refreshToken);
|
57 | this.accessToken = newTokens.accessToken;
|
58 | await this.writeConfig(oldConfig, { tokens: newTokens });
|
59 | const config = await this.readConfig();
|
60 | this.state.config = config;
|
61 | return config;
|
62 | }
|
63 | catch (error) {
|
64 | debug('%O', error);
|
65 | await this.clearConfig();
|
66 | throw new CustomErrors_1.TokenExpiredError();
|
67 | }
|
68 | };
|
69 | this.fetchUserInfo = async ({ tokens }) => {
|
70 | if (!tokens) {
|
71 | this.ux.spinner.stop(`failed`);
|
72 | this.log('missing parameter');
|
73 | process.exit();
|
74 | }
|
75 | const { accessToken, idToken } = tokens;
|
76 | if (!accessToken || !idToken) {
|
77 | this.ux.spinner.stop(`❗️\n`);
|
78 | 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`);
|
79 | process.exit();
|
80 | }
|
81 | const { sub, preferred_username, email } = jsonwebtoken_1.default.decode(idToken);
|
82 | const me = {
|
83 | id: sub,
|
84 | username: preferred_username,
|
85 | email,
|
86 | };
|
87 | const { data: teams } = await this.services.api
|
88 | .find('teams', {
|
89 | query: {
|
90 | userId: sub,
|
91 | },
|
92 | headers: { Authorization: accessToken },
|
93 | })
|
94 | .catch(err => {
|
95 | debug('%O', err);
|
96 | throw new CustomErrors_1.APIError(err);
|
97 | });
|
98 | if (!teams) {
|
99 | throw new CustomErrors_1.APIError('According to the API, this user does not belong to any teams.');
|
100 | }
|
101 | const meResponse = {
|
102 | me,
|
103 | teams,
|
104 | };
|
105 | return { meResponse, tokens };
|
106 | };
|
107 | this.writeConfig = async (oldConfigObj = {}, newConfigObj) => {
|
108 | return utils_1.writeConfig(oldConfigObj, newConfigObj, this.config.configDir);
|
109 | };
|
110 | this.readConfig = async () => {
|
111 | return utils_1.readConfig(this.config.configDir);
|
112 | };
|
113 | this.clearConfig = async () => {
|
114 | return utils_1.clearConfig(this.config.configDir);
|
115 | };
|
116 | this.invalidateKeycloakSession = async () => {
|
117 |
|
118 | const sessionState = this.state.config
|
119 | ? this.state.config.tokens
|
120 | ? this.state.config.tokens.sessionState
|
121 | : null
|
122 | : null;
|
123 |
|
124 | if (sessionState)
|
125 | await axios_1.default.get(this.services.keycloakService.buildInvalidateSessionUrl(), {
|
126 | headers: this.services.keycloakService.buildInvalidateSessionHeaders(sessionState, this.accessToken),
|
127 | });
|
128 | };
|
129 | this.initConfig = async (tokens) => {
|
130 | await this.clearConfig();
|
131 | const signinFlowPipeline = utils_1.asyncPipe(this.fetchUserInfo, utils_1.formatConfigObject, this.writeConfig, this.readConfig);
|
132 | const config = await signinFlowPipeline({ tokens });
|
133 | return config;
|
134 | };
|
135 | this.services = services;
|
136 | this.services.api = services.api || new Feathers_1.FeathersClient();
|
137 | this.services.publishService = services.publishService || new Publish_1.Publish();
|
138 | this.services.buildStepService =
|
139 | services.buildStepService || new BuildSteps_1.BuildSteps();
|
140 | this.services.imageService = services.imageService || new Image_1.ImageService();
|
141 | this.services.analytics =
|
142 | services.analytics || new Analytics_1.AnalyticsService(env_1.OPS_SEGMENT_KEY);
|
143 | this.services.workflowService =
|
144 | services.workflowService || new Workflow_1.WorkflowService();
|
145 | this.services.opService = services.opService || new Op_1.OpService();
|
146 | this.services.keycloakService =
|
147 | services.keycloakService || new Keycloak_1.KeycloakService();
|
148 | this.services.registryAuthService =
|
149 | services.registryAuthService || new RegistryAuth_1.RegistryAuthService();
|
150 | }
|
151 | async init() {
|
152 | try {
|
153 | debug('initiating base command');
|
154 | const config = await this.readConfig();
|
155 | const { user, tokens, team } = config;
|
156 | if (tokens) {
|
157 | this.accessToken = tokens.accessToken;
|
158 | }
|
159 | this.user = user;
|
160 | this.team = team;
|
161 | this.state = { config };
|
162 | }
|
163 | catch (err) {
|
164 | this.config.runHook('error', { err, accessToken: this.accessToken });
|
165 | }
|
166 | }
|
167 | async isLoggedIn() {
|
168 | debug('checking if user is logged in');
|
169 | const config = await this.readConfig();
|
170 | const { tokens } = config;
|
171 | if (!this.user ||
|
172 | !this.team ||
|
173 | !this.accessToken ||
|
174 | !tokens ||
|
175 | !tokens.accessToken ||
|
176 | !tokens.refreshToken ||
|
177 | !tokens.idToken) {
|
178 | this.log('');
|
179 | this.log('✋ Sorry you need to be logged in to do that.');
|
180 | this.log(`🎳 You can sign up with ${this.ux.colors.green('$')} ${this.ux.colors.callOutCyan('ops account:signup')}`);
|
181 | this.log('');
|
182 | this.log('❔ Please reach out to us with questions anytime!');
|
183 | this.log(`⌚️ We are typically available ${this.ux.colors.white('Monday-Friday 9am-5pm PT')}.`);
|
184 | 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`);
|
185 | this.log("🖖 We'll get back to you as soon as we possibly can.");
|
186 | this.log('');
|
187 | process.exit();
|
188 | }
|
189 | return this.checkAndRefreshAccessToken(tokens);
|
190 | }
|
191 | async validateUniqueField(query) {
|
192 | const response = await this.services.api
|
193 | .find('validate', {
|
194 | query,
|
195 | })
|
196 | .catch(err => {
|
197 | throw new CustomErrors_1.APIError(err);
|
198 | });
|
199 | return response.data;
|
200 | }
|
201 | }
|
202 | exports.default = CTOCommand;
|