UNPKG

30.9 kBJavaScriptView Raw
1/**
2 * Copyright (c) 2018, Kinvey, Inc. All rights reserved.
3 *
4 * This software is licensed to you under the Kinvey terms of service located at
5 * http://www.kinvey.com/terms-of-use. By downloading, accessing and/or using this
6 * software, you hereby accept such terms of service (and any agreement referenced
7 * therein) and agree that you have read, understand and agree to be bound by such
8 * terms of service and are of legal age to agree to such terms with Kinvey.
9 *
10 * This software contains valuable confidential and proprietary information of
11 * KINVEY, INC and is subject to applicable licensing agreements.
12 * Unauthorized reproduction, transmission or distribution of this file and its
13 * contents is a violation of applicable laws.
14 */
15
16const async = require('async');
17const chalk = require('chalk');
18
19const {
20 AllCommandsNotRequiringAuth,
21 AuthOptionsNames,
22 CommandRequirement,
23 EnvironmentVariables,
24 Errors,
25 CommonOptionsNames,
26 LogLevel,
27 Namespace,
28 OutputFormat,
29 StderrLogLevels
30} = require('./Constants');
31const Authentication = require('./Authentication');
32const KinveyError = require('./KinveyError');
33const Request = require('./Request');
34const { formatHost, getCommandNameFromOptions, isEmpty, isNullOrUndefined, isValidEmail } = require('./Utils');
35
36const cmdInit = require('./init/init');
37const profilesRouter = require('./profile/profilesRouter');
38const flexRouter = require('./flex/flexRouter');
39const organizationsRouter = require('./organization/organizationsRouter');
40const applicationsRouter = require('./application/applicationsRouter');
41const servicesRouter = require('./service/servicesRouter');
42const environmentsRouter = require('./environment/environmentsRouter');
43const collectionsRouter = require('./collection/collectionsRouter');
44const sitesRouter = require('./website/sitesRouter');
45
46const InitController = require('./init/InitController');
47const ProfilesController = require('./profile/ProfilesController');
48const FlexController = require('./flex/FlexController');
49const OrganizationsController = require('./organization/OrganizationsController');
50const ApplicationsController = require('./application/ApplicationsController');
51const ServicesController = require('./service/ServicesController');
52const EnvironmentsController = require('./environment/EnvironmentsController');
53const CollectionsController = require('./collection/CollectionsController');
54const SitesController = require('./website/SitesController');
55
56const ServicesService = require('./service/ServicesService');
57const OrganizationsService = require('./organization/OrganizationsService');
58const ApplicationsService = require('./application/ApplicationsService');
59const EnvironmentsService = require('./environment/EnvironmentsService');
60const CollectionsService = require('./collection/CollectionsService');
61const BusinessLogicService = require('./BusinessLogicService');
62const PushService = require('./PushService');
63const RolesService = require('./RolesService');
64const GroupsService = require('./GroupsService');
65const SitesService = require('./website/SitesService');
66const SiteEnvsService = require('./website/SiteEnvsService');
67
68const AppFileExporter = require('./exporter/AppFileExporter');
69const EnvFileExporter = require('./exporter/EnvFileExporterV1');
70const OrgFileExporter = require('./exporter/OrgFileExporter');
71const ServiceExporter = require('./exporter/ServiceFileExporter');
72
73const AppFileProcessor = require('./AppFileProcessor');
74const EnvFileProcessor = require('./EnvFileProcessor');
75const OrgFileProcessor = require('./OrgFileProcessor');
76const ServiceFileProcessor = require('./ServiceFileProcessor');
77const ConfigFileProcessor = require('./ConfigFileProcessor');
78
79/**
80 * Serves as mediator between modules. Responsible for routing and controllers initializing.
81 */
82class CLIManager {
83 constructor({ setup, config, logger, notifier, cliVersion, prompter, commandsManager }) {
84 this._setup = setup;
85 this._authentication = new Authentication(this);
86 this._currentProfileName = null;
87 this._isOneTimeSession = false;
88 this._requiresAuth = true;
89 this._outputFormat = OutputFormat.HUMAN_READABLE;
90 this._baasHost = '';
91 this._logger = logger;
92 this._notifier = notifier;
93 this._cliVersion = cliVersion;
94 this._commandsManager = commandsManager;
95 this.config = config;
96 this.prompter = prompter;
97
98 this._registerControllers();
99 }
100
101 _registerControllers() {
102 this._controllers = {};
103 this._controllers.init = new InitController({ cliManager: this });
104 const applicationsService = new ApplicationsService(this);
105 const organizationsService = new OrganizationsService(this);
106 const collectionsService = new CollectionsService(this);
107 const environmentsService = new EnvironmentsService(this);
108 const servicesService = new ServicesService(this);
109 const rolesService = new RolesService(this);
110 const groupsService = new GroupsService(this);
111 const businessLogicService = new BusinessLogicService(this);
112 const pushService = new PushService(this);
113 const sitesService = new SitesService(this);
114 const siteEnvsService = new SiteEnvsService(this);
115
116 const envExporter = new EnvFileExporter({
117 cliManager: this,
118 applicationsService,
119 collectionsService,
120 businessLogicService,
121 rolesService,
122 groupsService,
123 pushService,
124 servicesService
125 });
126 const serviceExporter = new ServiceExporter({ cliManager: this, servicesService });
127 const appExporter = new AppFileExporter({ cliManager: this, applicationsService, servicesService, envExporter, serviceExporter });
128 const orgExporter = new OrgFileExporter({ cliManager: this, organizationsService, applicationsService, servicesService, appExporter, serviceExporter });
129
130 this._controllers[Namespace.FLEX] = new FlexController({ cliManager: this, applicationsService, organizationsService, servicesService });
131 this._controllers[Namespace.PROFILE] = new ProfilesController({ cliManager: this });
132 this._controllers[Namespace.ORG] = new OrganizationsController({ cliManager: this, organizationsService, exporter: orgExporter });
133 this._controllers[Namespace.APP] = new ApplicationsController({ cliManager: this, exporter: appExporter, applicationsService, organizationsService });
134 this._controllers[Namespace.ENV] = new EnvironmentsController({ cliManager: this, exporter: envExporter, environmentsService });
135 this._controllers[Namespace.SERVICE] = new ServicesController({ cliManager: this, exporter: serviceExporter, applicationsService, organizationsService });
136 this._controllers[Namespace.COLL] = new CollectionsController({ cliManager: this, collectionsService, environmentsService });
137 this._controllers[Namespace.SITE] = new SitesController({ cliManager: this, sitesService, siteEnvsService, organizationsService, applicationsService });
138
139 const envFileProcessor = new EnvFileProcessor({
140 cliManager: this,
141 applicationsService,
142 environmentsService,
143 collectionsService,
144 businessLogicService,
145 rolesService,
146 groupsService,
147 pushService,
148 servicesService
149 });
150
151 const serviceFileProcessor = new ServiceFileProcessor({ cliManager: this, servicesService });
152
153 const appFileProcessor = new AppFileProcessor({ cliManager: this, applicationsService, environmentsService, servicesService, envFileProcessor, serviceFileProcessor });
154 const orgFileProcessor = new OrgFileProcessor({ cliManager: this, organizationsService, servicesService, applicationsService, serviceFileProcessor, appFileProcessor });
155
156 this._configFileProcessor = new ConfigFileProcessor({ appFileProcessor, envFileProcessor, serviceFileProcessor, orgFileProcessor });
157 }
158
159 getController(name) {
160 const ctrl = this._controllers[name];
161 if (isNullOrUndefined(ctrl)) {
162 throw new Error(`Controller not found: ${name}.`);
163 }
164
165 return ctrl;
166 }
167
168 _processCommonOptions(options) {
169 if (options[CommonOptionsNames.NO_COLOR]) {
170 this._logger.stripColors = true;
171 chalk.level = 0;
172 }
173
174 if (options[CommonOptionsNames.OUTPUT]) {
175 this._outputFormat = options[CommonOptionsNames.OUTPUT];
176 }
177
178 if (options[CommonOptionsNames.SILENT] && options[CommonOptionsNames.VERBOSE]) {
179 const errMsg = `Mutually exclusive options: '${CommonOptionsNames.SILENT}' and '${CommonOptionsNames.VERBOSE}'.`;
180 throw new KinveyError(null, errMsg);
181 }
182
183 if (options[CommonOptionsNames.SILENT]) {
184 this._logger.level = LogLevel.SILENT;
185 }
186
187 if (options[CommonOptionsNames.VERBOSE]) {
188 this._logger.level = LogLevel.DEBUG;
189 }
190
191 if (!options[CommonOptionsNames.SUPPRESS_VERSION_CHECK]) {
192 this.log(LogLevel.DEBUG, 'Checking for package updates');
193 this._notifier.notify({ defer: false });
194 }
195 }
196
197 static _authIsRequired(cmd) {
198 if (AllCommandsNotRequiringAuth.includes(cmd)) {
199 return false;
200 }
201
202 return true;
203 }
204
205 _setIsOneTimeSession(options) {
206 const commandName = getCommandNameFromOptions(options);
207 if (AllCommandsNotRequiringAuth.includes(commandName)) {
208 this._isOneTimeSession = false;
209 return;
210 }
211
212 if (!isNullOrUndefined(options[AuthOptionsNames.PROFILE])) {
213 this._isOneTimeSession = false;
214 return;
215 }
216
217 this._isOneTimeSession = !isNullOrUndefined(options[AuthOptionsNames.EMAIL])
218 && !isNullOrUndefined(options[AuthOptionsNames.PASSWORD]);
219 }
220
221 isOneTimeSession() {
222 return this._isOneTimeSession;
223 }
224
225 static _clearAuthOptions(options) {
226 const commandName = getCommandNameFromOptions(options);
227 const disregardAllAuthOptions = commandName === 'init' || commandName === `${Namespace.PROFILE} login`;
228
229 if (disregardAllAuthOptions) {
230 Object.keys(AuthOptionsNames).forEach((key) => {
231 const propName = AuthOptionsNames[key];
232 options[propName] = null;
233 });
234
235 return;
236 }
237
238 if (commandName === 'profile create') {
239 options[AuthOptionsNames.PROFILE] = null;
240 }
241 }
242
243 /**
244 * Processes auth-related options before command execution. Determines if command requires auth or if some options
245 * must be cleared. Decides if it is a one-time session.
246 * @param options
247 * @private
248 */
249 _processAuthOptions(options) {
250 const commandName = getCommandNameFromOptions(options);
251 this._requiresAuth = CLIManager._authIsRequired(commandName);
252
253 CLIManager._clearAuthOptions(options);
254
255 this._setIsOneTimeSession(options);
256
257 if (!isNullOrUndefined(options[AuthOptionsNames.BAAS_HOST])) {
258 this._baasHost = options[AuthOptionsNames.BAAS_HOST];
259 }
260
261 if (!isNullOrUndefined(options[AuthOptionsNames.HOST])) {
262 options[AuthOptionsNames.HOST] = formatHost(options[AuthOptionsNames.HOST]);
263 }
264 }
265
266 /**
267 * Tries to set the current user from a profile name or from the active profile. If there are no saved profiles,
268 * it doesn't set it. If no name is provided, no active profile is set and there is only one saved profile,
269 * then it sets the user to this profile.
270 * @param {String} [profileName] Name of already saved profile. Throws if not found.
271 * @private
272 */
273 _setCurrentUserFromProfile(profileName) {
274 if (this._isOneTimeSession || !this._setup.hasProfiles()) {
275 return;
276 }
277
278 let profile;
279 if (!isNullOrUndefined(profileName)) {
280 profile = this._setup.findProfileByName(profileName);
281 } else if (this._setup.hasActiveProfile()) {
282 profile = this._setup.getActiveProfile();
283 } else {
284 // profile is not provided explicitly, active profile is not set
285 const allProfiles = this._setup.getProfiles();
286 const isSingleProfile = Object.keys(allProfiles).length === 1;
287 if (isSingleProfile) {
288 const singleProfileName = Object.keys(allProfiles)[0];
289 profile = this._setup.findProfileByName(singleProfileName);
290 }
291 }
292
293 if (!profile && profileName) {
294 throw new KinveyError(Errors.ProfileNotFound);
295 }
296
297 if (profile) {
298 this._currentProfileName = profile.name;
299 this.log(LogLevel.DEBUG, `Using profile '${this._currentProfileName}'`);
300 this._authentication.setCurrentUser(profile);
301 }
302 }
303
304 _loadGlobalSetup() {
305 if (this._isOneTimeSession) {
306 return;
307 }
308
309 const err = this._setup.load();
310 const canContinue = !err || (err instanceof Error && err.code === 'ENOENT');
311 if (!canContinue) {
312 this.log(LogLevel.DEBUG, 'Failed to load global settings: unable to load config file.');
313 throw err;
314 }
315 }
316
317 _initCommandsManager(args) {
318 // for testing purposes
319 if (!isNullOrUndefined(args)) {
320 this._commandsManager(args);
321 }
322
323 this._commandsManager
324 .usage('kinvey <command> [args] [options]')
325 .env(EnvironmentVariables.PREFIX) // populate options with env variables
326 .option(
327 AuthOptionsNames.BAAS_HOST, {
328 global: true,
329 describe: 'Custom BAAS host',
330 type: 'string',
331 hidden: true
332 }
333 )
334 .option(
335 AuthOptionsNames.EMAIL, {
336 global: true,
337 describe: 'E-mail address of your Kinvey account',
338 type: 'string'
339 }
340 )
341 .option(
342 AuthOptionsNames.PASSWORD, {
343 global: true,
344 describe: 'Password of your Kinvey account',
345 type: 'string'
346 }
347 )
348 .option(
349 AuthOptionsNames.TWO_FACTOR_AUTH_TOKEN, {
350 global: true,
351 alias: '2Fa',
352 describe: 'Two-factor authentication token',
353 type: 'string'
354 }
355 )
356 .option(
357 AuthOptionsNames.HOST, {
358 global: true,
359 alias: 'instanceId',
360 describe: 'Instance ID',
361 type: 'string'
362 }
363 )
364 .option(
365 AuthOptionsNames.PROFILE, {
366 global: true,
367 describe: 'Profile to use',
368 type: 'string'
369 }
370 )
371 .option(
372 CommonOptionsNames.OUTPUT, {
373 global: true,
374 describe: 'Output format',
375 type: 'string',
376 choices: [OutputFormat.JSON]
377 }
378 )
379 .option(
380 CommonOptionsNames.SILENT, {
381 global: true,
382 describe: 'Do not output anything',
383 type: 'boolean'
384 }
385 )
386 .option(
387 CommonOptionsNames.SUPPRESS_VERSION_CHECK, {
388 global: true,
389 alias: 'suppressVersionCheck',
390 describe: 'Do not check for package updates',
391 type: 'boolean'
392 }
393 )
394 .option(CommonOptionsNames.VERBOSE, {
395 global: true,
396 describe: 'Output debug messages',
397 type: 'boolean'
398 })
399 .option(CommonOptionsNames.NO_COLOR, {
400 global: true,
401 alias: 'noColor',
402 describe: 'Disable colors',
403 type: 'boolean'
404 })
405 .check((argv) => {
406 // FIXME: Move to a middleware when yargs provides middleware support (https://github.com/yargs/yargs/pull/881)
407 this._processCommonOptions(argv);
408 this._processAuthOptions(argv);
409
410 this._loadGlobalSetup();
411
412 if (!this._isOneTimeSession && this._requiresAuth) {
413 this._setCurrentUserFromProfile(argv[AuthOptionsNames.PROFILE]);
414 }
415
416 if (this._requiresAuth && !this._isOneTimeSession && !this._authentication.hasCurrentUser()) {
417 throw new Error('You must be authenticated.');
418 }
419
420 return true;
421 })
422 .command(cmdInit(this))
423 .command(Namespace.PROFILE, `Manage profiles. Run 'kinvey ${Namespace.PROFILE} -h' for details.`, profilesRouter(this))
424 .command(Namespace.ORG, `Manage organizations. Run 'kinvey ${Namespace.ORG} -h' for details.`, organizationsRouter(this))
425 .command(Namespace.APP, `Manage applications. Run 'kinvey ${Namespace.APP} -h' for details.`, applicationsRouter(this))
426 .command(Namespace.ENV, `Manage environments. Run 'kinvey ${Namespace.ENV} -h' for details.`, environmentsRouter(this))
427 .command(Namespace.COLL, `Manage collections. Run 'kinvey ${Namespace.COLL} -h' for details.`, collectionsRouter(this))
428 .command(Namespace.SERVICE, `Manage services. Run 'kinvey ${Namespace.SERVICE} -h' for details.`, servicesRouter(this))
429 .command(Namespace.FLEX, `Deploy and manage flex services. Run 'kinvey ${Namespace.FLEX} -h' for details.`, flexRouter(this))
430 .command(Namespace.SITE, `Manage websites. Run 'kinvey ${Namespace.SITE} -h' for details.`, sitesRouter(this))
431 .demand(1, '')
432 .strict(true)
433 .help('h')
434 .alias('h', 'help')
435 .showHelpOnFail(true);
436
437 this._commandsManager.argv;
438 }
439
440 /**
441 * Executes any command handler. Ensures that results from various command executions are handled in a similar
442 * fashion. If there are any requirements, they are executed first.
443 * @param {Object} ctrl The controller containing command handler.
444 * @param {String} commandHandler Name of the command handler.
445 * @param {Array} requirements Requirements that need to be fulfilled before actually handling the command.
446 * @param {Object} options Command-line positional arguments, options and flags.
447 */
448 executeCommandHandler(ctrl, commandHandler, requirements = [], options) {
449 async.series([
450 (next) => {
451 const requiresProfile = requirements.includes(CommandRequirement.PROFILE_AVAILABLE);
452 if (!requiresProfile) {
453 return setImmediate(next);
454 }
455
456 if (!this.getCurrentProfileName()) {
457 return setImmediate(() => { next(new KinveyError(Errors.ProfileRequired)); });
458 }
459
460 return setImmediate(next);
461 },
462 (next) => {
463 const requiresAuth = requirements.includes(CommandRequirement.AUTH);
464 if (!requiresAuth) {
465 return setImmediate(next);
466 }
467
468 this.setCurrentUserFromOptions(options, next);
469 },
470 (next) => {
471 ctrl[commandHandler].apply(ctrl, [options, next]);
472 }
473 ], (err, results) => {
474 if (err) {
475 return this.processCommandResult(err);
476 }
477
478 const result = results.pop();
479 this.processCommandResult(null, result);
480 });
481 }
482
483 /**
484 * Applies command definitions to commands manager.
485 * @param {Object[]} commandDefinitions
486 * @param {String} commandDefinitions[].command Official command name.
487 * @param {String} commandDefinitions[].desc Official command description.
488 * @param {Function} commandDefinitions[].builder Builder function that runs before options reach command handler.
489 * @param {String} commandDefinitions[].handlerName Name of command handler.
490 * @param {Constants.CommandRequirement[]} commandDefinitions[].requirements Requirements that need to be fulfilled
491 * before actually handling the command.
492 * @param commandsManager Currently, this is yargs.
493 * @param {Object} ctrl Controller responsible for command handling.
494 */
495 applyCommandDefinitions(commandDefinitions, commandsManager, ctrl) {
496 if (isEmpty(commandDefinitions) || !Array.isArray(commandDefinitions)) {
497 throw new Error('Command definitions are either empty or not an array.');
498 }
499
500 commandDefinitions.forEach((cmdDef) => {
501 const handlerName = cmdDef.handlerName;
502 if (typeof ctrl[handlerName] !== 'function') {
503 throw new Error(`${ctrl.constructor.name} does not contain a function called '${handlerName}'.`);
504 }
505
506 const modifiedDef = {
507 command: cmdDef.command,
508 desc: cmdDef.desc,
509 handler: (options) => {
510 // wrap command-specific handler in CLIManager's general handler
511 this.executeCommandHandler(ctrl, handlerName, cmdDef.requirements, options);
512 }
513 };
514
515 const ensureErrorOnUndeclaredArgs = (commandsManager) => { commandsManager.demandCommand(0, 0); };
516
517 if (cmdDef.builder) {
518 const originalBuilder = cmdDef.builder;
519 modifiedDef.builder = (commandsManager) => {
520 ensureErrorOnUndeclaredArgs(commandsManager);
521 originalBuilder(commandsManager, this);
522 };
523 } else {
524 modifiedDef.builder = ensureErrorOnUndeclaredArgs;
525 }
526
527 // apply command to commandsManager - basically, expose it to the world
528 commandsManager.command(modifiedDef);
529 });
530 }
531
532 /**
533 * Makes sure that if profile exists, token will be invalidated. If an error occurs, it logs but doesn't pass it to
534 * callback.
535 * @param {String} profileName
536 * @param done
537 * @returns {*}
538 * @private
539 */
540 _logoutUserFromProfile(profileName, done) {
541 const profile = this._setup.findProfileByName(profileName);
542 if (isNullOrUndefined(profile)) {
543 return setImmediate(done);
544 }
545
546 this._authentication.setCurrentUser(profile);
547 this._authentication.logout((err) => {
548 if (err) {
549 this.log(LogLevel.DEBUG, `Failed to invalidate token for profile with name '${profileName}'.`);
550 }
551
552 done();
553 });
554 }
555
556 /**
557 * Entry point of CLI manager to start processing commands.
558 * @param args
559 */
560 init(args) {
561 this._initCommandsManager(args);
562 }
563
564 /**
565 * Sends request. Returns the request object if additional control is needed (e.g. stop the request).
566 * @param {Object} options
567 * @param {String} options.endpoint Relative URL (e.g. 'v2/apps')
568 * @param {String} [options.host] Base URL (e.g. 'https://manage.kinvey.com/')
569 * @param {Boolean} [options.isBaas] If true, then this is a baas request and not a metadata one.
570 * @param {String} [options.method] HTTP method
571 * @param {Object} [options.data]
572 * @param {Object} [options.formData]
573 * @param {Boolean} [options.skipAuth] If true, doesn't set auth header.
574 * @param done
575 * @returns {*}
576 */
577 sendRequest(options = {}, done) {
578 if (isNullOrUndefined(options.host)) {
579 options.host = this.config.host;
580 }
581
582 if (options.isBaas && this._baasHost) {
583 options.baasHost = this._baasHost;
584 }
585
586 if (isNullOrUndefined(options.timeout)) {
587 options.timeout = this.config.timeout;
588 }
589
590 options.cliVersion = this._cliVersion;
591
592 const reqObj = new Request(this._authentication.getCurrentUser(), options);
593
594 this.log(LogLevel.DEBUG, 'Request: %s %s', reqObj.options.method, reqObj.options.url);
595
596 return reqObj.send((err, res) => {
597 const status = res.statusCode || '';
598 this.log(LogLevel.DEBUG, 'Response: %s %s %s', reqObj.options.method, reqObj.options.url, status);
599 done(err, res.body);
600 });
601 }
602
603 /**
604 * Log using the logger module.
605 * @param {Constants.LogLevel} logLevel
606 * @param args
607 */
608 log(logLevel, ...args) {
609 if (!StderrLogLevels.includes(logLevel)) {
610 return;
611 }
612
613 this._logger[logLevel](...args);
614 }
615
616 /**
617 * Logs out current user if it is a one-time session.
618 * @param done
619 * @returns {*}
620 * @private
621 */
622 _clearSession(done) {
623 if (!this._isOneTimeSession) {
624 return setImmediate(() => { done(); });
625 }
626
627
628 this._authentication.logout(done);
629 }
630
631 /**
632 * Handles any result from command execution.
633 * @param {Error} [err]
634 * @param {CommandResult} [result]
635 */
636 processCommandResult(err, result) {
637 this._clearSession(() => {
638 if (err) {
639 this.log(LogLevel.ERROR, err);
640
641 // suggest command before exiting
642 const isAuthTokenError = err.name === 'InvalidCredentials' && err.message === 'Authorization token invalid or expired.';
643 if (isAuthTokenError) {
644 this.log(LogLevel.INFO, `Run 'kinvey ${Namespace.PROFILE} login' to reauthenticate.`);
645 }
646
647 return process.exit(1);
648 }
649
650 const output = result.getFormattedResult(this._outputFormat);
651 this._logger.log(LogLevel.DATA, output);
652 });
653 }
654
655 /**
656 * Sets the current user if not already set using info from options.
657 * @param {Object} options
658 * @param done
659 * @returns {*}
660 */
661 setCurrentUserFromOptions(options, done) {
662 if (this._authentication.hasCurrentUser()) {
663 return setImmediate(() => { done(); });
664 }
665
666 this.login(
667 options[AuthOptionsNames.EMAIL],
668 options[AuthOptionsNames.PASSWORD],
669 options[AuthOptionsNames.TWO_FACTOR_AUTH_TOKEN],
670 options[AuthOptionsNames.HOST],
671 done
672 );
673 }
674
675 /**
676 * Issues a login request. If it is a success, sets the current user.
677 * @param email
678 * @param password
679 * @param [MFAToken]
680 * @param [host]
681 * @param done
682 */
683 login(email, password, MFAToken, host = this.config.host, done) {
684 const emailIsValid = isValidEmail(email);
685 if (!emailIsValid) {
686 return setImmediate(() => { done(new KinveyError(Errors.InvalidEmail)); });
687 }
688
689 this.log(LogLevel.DEBUG, `Logging in user: ${email}`);
690 this._authentication.login(email, password, MFAToken, host, done);
691 }
692
693 /**
694 * Creates a new profile. Changes are persisted in the setup file.
695 * @param profileName
696 * @param email
697 * @param password
698 * @param [MFAToken]
699 * @param [host]
700 * @param done
701 * @returns {*}
702 */
703 createProfile(profileName, email, password, MFAToken, host = this.config.host, done) {
704 if (this.profileExists(profileName)) {
705 this.log(LogLevel.DEBUG, `Overriding profile with name '${profileName}'.`);
706 }
707
708 if (isNullOrUndefined(profileName)) {
709 return done(new Error('Profile name is not set.'));
710 }
711
712 let token;
713
714 async.series([
715 (next) => {
716 this.login(email, password, MFAToken, host, (err, data) => {
717 if (err) {
718 return next(err);
719 }
720
721 token = data.token;
722 next();
723 });
724 },
725 (next) => {
726 const newUser = this._authentication.getCurrentUser();
727 this._logoutUserFromProfile(profileName, () => {
728 this._authentication.setCurrentUser(newUser);
729 next();
730 });
731 },
732 (next) => {
733 // save profile
734 this._setup.addProfile(profileName, email, token, host);
735 this._setup.save(next);
736 }
737 ], done);
738 }
739
740 reAuthenticateProfile(profileName, password, mfaToken, done) {
741 const profile = this.findProfile(profileName);
742 if (isNullOrUndefined(profile)) {
743 return setImmediate(() => done(new KinveyError(Errors.ProfileNotFound)));
744 }
745
746 this.log(LogLevel.DEBUG, `Re-authenticating profile with name '${profileName}'.`);
747
748 async.waterfall([
749 (next) => {
750 this.login(profile.email, password, mfaToken, profile.host, (err, data) => {
751 if (err) {
752 return next(err);
753 }
754
755 const token = data.token;
756 next(null, token);
757 });
758 },
759 (token, next) => {
760 this._setup.setProfileToken(profileName, token);
761 this._setup.save(next);
762 }
763 ], done);
764 }
765
766 /**
767 * Returns all saved profiles.
768 * @returns {*}
769 */
770 getProfiles() {
771 return this._setup.getProfiles();
772 }
773
774 /**
775 * Searches for a profile by name. Returns null if not found.
776 * @param name
777 * @returns {*}
778 */
779 findProfile(name) {
780 return this._setup.findProfileByName(name);
781 }
782
783 /**
784 * Checks if profile name already exists.
785 * @param {String} name
786 * @returns {boolean}
787 */
788 profileExists(name) {
789 return isNullOrUndefined(this._setup.findProfileByName(name)) === false;
790 }
791
792 /**
793 * Deletes a profile by name. Changes are persisted. Doesn't return an error if profile is not found.
794 * @param name
795 * @param done
796 */
797 deleteProfile(name, done) {
798 async.series([
799 (next) => {
800 this._logoutUserFromProfile(name, next);
801 },
802 (next) => {
803 this._setup.deleteProfile(name);
804 this._setup.save(next);
805 }
806 ], done);
807 }
808
809 getActiveProfile() {
810 return this._setup.getActiveProfile();
811 }
812
813 setActiveProfile(name, done) {
814 if (!this.profileExists(name)) {
815 return done(new KinveyError(Errors.ProfileNotFound));
816 }
817
818 this._setup.setActiveProfile(name);
819 this._setup.save(done);
820 }
821
822 getCurrentProfileName() {
823 return this._currentProfileName;
824 }
825
826 /**
827 * Gets specific active item for the current profile. Returns null if no such active item or no current profile.
828 * @param {Constants.ActiveItemType} itemType
829 * @returns {*}
830 */
831 getActiveItem(itemType) {
832 const profileName = this.getCurrentProfileName();
833 if (isNullOrUndefined(profileName)) {
834 return null;
835 }
836
837 return this._setup.getActiveItemProfileLevel(itemType, profileName);
838 }
839
840 getActiveItemId(itemType) {
841 const activeItem = this.getActiveItem(itemType);
842 if (!activeItem || isNullOrUndefined(activeItem.id)) {
843 return null;
844 }
845
846 return activeItem.id;
847 }
848
849 /**
850 * Sets specific active item for the current profile.
851 * @param {Constants.ActiveItemType} itemType
852 * @param {Object} item
853 * @param done
854 */
855 setActiveItem(itemType, item, done) {
856 const profileName = this.getCurrentProfileName();
857 this._setup.setActiveItemProfileLevel(itemType, item, profileName);
858 this._setup.save(done);
859 }
860
861 /**
862 * Removes an active item if the already removed id equals the active one.
863 * @param {Constants.ActiveItemType} itemType
864 * @param {String} removedId
865 * @param done
866 * @returns {*}
867 */
868 removeActiveItem(itemType, removedId, done) {
869 if (this.isOneTimeSession()) {
870 return setImmediate(done);
871 }
872
873 const currentActiveItem = this.getActiveItem(itemType);
874 if (isNullOrUndefined(currentActiveItem) || isEmpty(currentActiveItem)) {
875 return setImmediate(done);
876 }
877
878 const activeId = currentActiveItem.id;
879 if (removedId !== activeId) {
880 return setImmediate(done);
881 }
882
883 this.setActiveItem(itemType, { id: null }, (err) => {
884 if (err) {
885 this.log(LogLevel.WARN, `Failed to remove active ${itemType}: ${activeId}. ${err}`);
886 } else {
887 this.log(LogLevel.DEBUG, `Removed active ${itemType}: ${activeId}.`);
888 }
889
890 done(err);
891 });
892 }
893
894 processConfigFile(options, done) {
895 this._configFileProcessor.process(options, done);
896 }
897}
898
899module.exports = CLIManager;