Skip to content

Commit d9fa0bb

Browse files
committed
feat(cli): Multiple accounts in CLI
1 parent 151f59b commit d9fa0bb

File tree

4 files changed

+264
-58
lines changed

4 files changed

+264
-58
lines changed

templates/cli/lib/commands/generic.js.twig

Lines changed: 126 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,82 @@
11
const inquirer = require("inquirer");
22
const { Command } = require("commander");
33
const Client = require("../client");
4-
const { sdkForConsole } = require("../sdks");
4+
const { sdkForConsole, questionGetEndpoint } = require("../sdks");
55
const { globalConfig, localConfig } = require("../config");
66
const { actionRunner, success, parseBool, commandDescriptions, error, parse, drawTable } = require("../parser");
77
{% if sdk.test != "true" %}
88
const { questionsLogin, questionsListFactors, questionsMfaChallenge } = require("../questions");
99
const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account");
10+
const ID = require("../id");
11+
12+
const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1';
13+
14+
const loginCommand = async ({ selfHosted }) => {
15+
const answers = await inquirer.prompt(questionsLogin);
16+
const oldCurrent = globalConfig.getCurrentLogin();
17+
const id = ID.unique();
18+
19+
globalConfig.setCurrentLogin(id);
20+
globalConfig.addLogin(id, {});
21+
globalConfig.setEmail(answers.email);
22+
globalConfig.setEndpoint(DEFAULT_ENDPOINT);
23+
24+
if (selfHosted) {
25+
const selfHostedAnswers = await inquirer.prompt(questionGetEndpoint);
26+
27+
globalConfig.setEndpoint(selfHostedAnswers.endpoint);
28+
}
29+
30+
let client = await sdkForConsole(false);
31+
32+
let account;
33+
34+
try {
35+
await accountCreateEmailPasswordSession({
36+
email: answers.email,
37+
password: answers.password,
38+
parseOutput: false,
39+
sdk: client
40+
})
41+
42+
client.setCookie(globalConfig.getCookie());
43+
44+
account = await accountGet({
45+
sdk: client,
46+
parseOutput: false
47+
});
48+
} catch (error) {
49+
if (error.response === 'user_more_factors_required') {
50+
const { factor } = await inquirer.prompt(questionsListFactors);
51+
52+
const challenge = await accountCreateMfaChallenge({
53+
factor,
54+
parseOutput: false,
55+
sdk: client
56+
});
57+
58+
const { otp } = await inquirer.prompt(questionsMfaChallenge);
59+
60+
await accountUpdateMfaChallenge({
61+
challengeId: challenge.$id,
62+
otp,
63+
parseOutput: false,
64+
sdk: client
65+
});
66+
67+
account = await accountGet({
68+
sdk: client,
69+
parseOutput: false
70+
});
71+
} else {
72+
globalConfig.removeLogin(id);
73+
globalConfig.setCurrentLogin(oldCurrent);
74+
throw error;
75+
}
76+
}
77+
78+
success("Signed in as user with ID: " + account.$id);
79+
};
1080

1181
const whoami = new Command("whoami")
1282
.description(commandDescriptions['whoami'])
@@ -49,61 +119,74 @@ const whoami = new Command("whoami")
49119

50120
const login = new Command("login")
51121
.description(commandDescriptions['login'])
122+
.option(`-sa, --self-hosted`, `Flag for enabling custom endpoint for self hosted instances`)
52123
.configureHelp({
53124
helpWidth: process.stdout.columns || 80
54125
})
126+
.action(actionRunner(loginCommand));
127+
128+
login
129+
.command('list')
130+
.description("List available logged accounts.")
55131
.action(actionRunner(async () => {
56-
const answers = await inquirer.prompt(questionsLogin)
132+
const logins = globalConfig.getLogins();
133+
const current = globalConfig.getCurrentLogin();
57134

58-
let client = await sdkForConsole(false);
135+
const data = [...logins.map((login => {
136+
return {
137+
'Current': login.id === current ? '*' : '',
138+
'ID': login.id,
139+
'Endpoint': login.endpoint,
140+
'Email': login.email
141+
};
142+
}))];
59143

60-
await accountCreateEmailPasswordSession({
61-
email: answers.email,
62-
password: answers.password,
63-
parseOutput: false,
64-
sdk: client
65-
})
144+
drawTable(data);
66145

67-
client.setCookie(globalConfig.getCookie());
146+
}));
68147

69-
let account;
148+
login
149+
.command('change')
150+
.description("Change the current account")
151+
.option(`-a, --accountId <accountId>`, `Login ID`)
152+
.action(actionRunner(async ({ accountId }) => {
153+
const loginIds = globalConfig.getLoginIds();
70154

71-
try {
72-
account = await accountGet({
73-
sdk: client,
74-
parseOutput: false
75-
});
76-
} catch (error) {
77-
if (error.response === 'user_more_factors_required') {
78-
const { factor } = await inquirer.prompt(questionsListFactors);
79-
80-
const challenge = await accountCreateMfaChallenge({
81-
factor,
82-
parseOutput: false,
83-
sdk: client
84-
});
85-
86-
const { otp } = await inquirer.prompt(questionsMfaChallenge);
87-
88-
await accountUpdateMfaChallenge({
89-
challengeId: challenge.$id,
90-
otp,
91-
parseOutput: false,
92-
sdk: client
93-
});
94-
95-
account = await accountGet({
96-
sdk: client,
97-
parseOutput: false
98-
});
99-
} else {
100-
throw error;
101-
}
155+
if (!loginIds.includes(accountId)) {
156+
throw Error('Login ID not found');
102157
}
103158

104-
success("Signed in as user with ID: " + account.$id);
159+
globalConfig.setCurrentLogin(accountId);
160+
success(`Current account is ${accountId}`);
105161
}));
106162

163+
login
164+
.command('migrate')
165+
.description("Migrate existing login to new scheme")
166+
.action(actionRunner(async ({ accountId }) => {
167+
const endpoint = globalConfig.getEndpoint();
168+
const cookie = globalConfig.getCookie();
169+
170+
if (endpoint === '' || cookie === '') {
171+
throw Error(`Couldn't find any existing account credentials`)
172+
}
173+
174+
const id = ID.unique();
175+
const data = {
176+
endpoint,
177+
cookie,
178+
email: 'legacy'
179+
};
180+
181+
globalConfig.addLogin(id, data);
182+
globalConfig.setCurrentLogin(id);
183+
globalConfig.delete('endpoint');
184+
globalConfig.delete('cookie');
185+
186+
success(`Account was migrated and it's the current account`);
187+
}));
188+
189+
107190
const logout = new Command("logout")
108191
.description(commandDescriptions['logout'])
109192
.configureHelp({

templates/cli/lib/config.js.twig

Lines changed: 110 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -341,14 +341,18 @@ class Local extends Config {
341341
class Global extends Config {
342342
static CONFIG_FILE_PATH = ".{{ spec.title|caseLower }}/prefs.json";
343343

344+
static PREFERENCE_CURRENT = "current";
344345
static PREFERENCE_ENDPOINT = "endpoint";
346+
static PREFERENCE_EMAIL = "email";
345347
static PREFERENCE_SELF_SIGNED = "selfSigned";
346348
static PREFERENCE_COOKIE = "cookie";
347349
static PREFERENCE_PROJECT = "project";
348350
static PREFERENCE_KEY = "key";
349351
static PREFERENCE_LOCALE = "locale";
350352
static PREFERENCE_MODE = "mode";
351353

354+
static IGNORE_ATTRIBUTES = [Global.PREFERENCE_CURRENT, Global.PREFERENCE_SELF_SIGNED, Global.PREFERENCE_ENDPOINT, Global.PREFERENCE_COOKIE, Global.PREFERENCE_PROJECT, Global.PREFERENCE_KEY, Global.PREFERENCE_LOCALE, Global.PREFERENCE_MODE];
355+
352356
static MODE_ADMIN = "admin";
353357
static MODE_DEFAULT = "default";
354358

@@ -359,59 +363,151 @@ class Global extends Config {
359363
super(`${homeDir}/${path}`);
360364
}
361365

366+
getCurrentLogin() {
367+
if (!this.has(Global.PREFERENCE_CURRENT)) {
368+
return "";
369+
}
370+
return this.get(Global.PREFERENCE_CURRENT);
371+
}
372+
373+
setCurrentLogin(endpoint) {
374+
this.set(Global.PREFERENCE_CURRENT, endpoint);
375+
}
376+
377+
getLoginIds() {
378+
return Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key));
379+
}
380+
381+
getLogins() {
382+
const logins = Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key))
383+
384+
return logins.map((login) => {
385+
386+
return {
387+
id: login,
388+
endpoint: this.data[login][Global.PREFERENCE_ENDPOINT],
389+
email: this.data[login][Global.PREFERENCE_EMAIL]
390+
}
391+
})
392+
}
393+
394+
addLogin(login, data) {
395+
this.set(login, data);
396+
}
397+
398+
removeLogin(login, data) {
399+
this.delete(login);
400+
}
401+
402+
getEmail() {
403+
if (!this.hasFrom(Global.PREFERENCE_EMAIL)) {
404+
return "";
405+
}
406+
407+
return this.getFrom(Global.PREFERENCE_EMAIL);
408+
}
409+
410+
setEmail(email) {
411+
this.setTo(Global.PREFERENCE_EMAIL, email);
412+
}
413+
362414
getEndpoint() {
363-
if (!this.has(Global.PREFERENCE_ENDPOINT)) {
415+
if (!this.hasFrom(Global.PREFERENCE_ENDPOINT)) {
364416
return "";
365417
}
366-
return this.get(Global.PREFERENCE_ENDPOINT);
418+
419+
return this.getFrom(Global.PREFERENCE_ENDPOINT);
367420
}
368421

369422
setEndpoint(endpoint) {
370-
this.set(Global.PREFERENCE_ENDPOINT, endpoint);
423+
this.setTo(Global.PREFERENCE_ENDPOINT, endpoint);
371424
}
372425

373426
getSelfSigned() {
374-
if (!this.has(Global.PREFERENCE_SELF_SIGNED)) {
427+
if (!this.hasFrom(Global.PREFERENCE_SELF_SIGNED)) {
375428
return false;
376429
}
377-
return this.get(Global.PREFERENCE_SELF_SIGNED);
430+
return this.getFrom(Global.PREFERENCE_SELF_SIGNED);
378431
}
379432

380433
setSelfSigned(selfSigned) {
381-
this.set(Global.PREFERENCE_SELF_SIGNED, selfSigned);
434+
this.setTo(Global.PREFERENCE_SELF_SIGNED, selfSigned);
382435
}
383436

384437
getCookie() {
385-
if (!this.has(Global.PREFERENCE_COOKIE)) {
438+
if (!this.hasFrom(Global.PREFERENCE_COOKIE)) {
386439
return "";
387440
}
388-
return this.get(Global.PREFERENCE_COOKIE);
441+
return this.getFrom(Global.PREFERENCE_COOKIE);
389442
}
390443

391444
setCookie(cookie) {
392-
this.set(Global.PREFERENCE_COOKIE, cookie);
445+
this.setTo(Global.PREFERENCE_COOKIE, cookie);
393446
}
394447

395448
getProject() {
396-
if (!this.has(Global.PREFERENCE_PROJECT)) {
449+
if (!this.hasFrom(Global.PREFERENCE_PROJECT)) {
397450
return "";
398451
}
399-
return this.get(Global.PREFERENCE_PROJECT);
452+
return this.getFrom(Global.PREFERENCE_PROJECT);
400453
}
401454

402455
setProject(project) {
403-
this.set(Global.PREFERENCE_PROJECT, project);
456+
this.setTo(Global.PREFERENCE_PROJECT, project);
404457
}
405458

406459
getKey() {
407-
if (!this.has(Global.PREFERENCE_KEY)) {
460+
if (!this.hasFrom(Global.PREFERENCE_KEY)) {
408461
return "";
409462
}
410463
return this.get(Global.PREFERENCE_KEY);
411464
}
412465

413466
setKey(key) {
414-
this.set(Global.PREFERENCE_KEY, key);
467+
this.setTo(Global.PREFERENCE_KEY, key);
468+
}
469+
470+
hasFrom(key) {
471+
try {
472+
const current = this.getCurrentLogin();
473+
474+
if (current) {
475+
const config = this.get(current);
476+
477+
return config[key] !== undefined;
478+
}
479+
} catch {
480+
return this.has(key);
481+
}
482+
}
483+
484+
getFrom(key) {
485+
try {
486+
const current = this.getCurrentLogin();
487+
488+
if (current) {
489+
const config = this.get(current);
490+
491+
return config[key];
492+
}
493+
} catch {
494+
return this.get(key);
495+
}
496+
}
497+
498+
setTo(key, value) {
499+
try {
500+
const current = this.getCurrentLogin();
501+
502+
if (current) {
503+
const config = this.get(current);
504+
505+
config[key] = value;
506+
this.write();
507+
}
508+
} catch {
509+
this.set(key, value);
510+
}
415511
}
416512
}
417513

0 commit comments

Comments
 (0)