Skip to content

Commit 11fe1ff

Browse files
Merge pull request #851 from appwrite/feat-multiple-accounts-and-instances
Feat multiple accounts and instances
2 parents 81ded63 + 0860867 commit 11fe1ff

15 files changed

+1057
-610
lines changed

templates/cli/base/requests/api.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
if (parseOutput) {
2020
{%~ if methodHaveConsolePreview(method.name,service.name) %}
2121
if(console) {
22-
showConsoleLink('{{service.name}}', '{{ method.name }}',open
22+
showConsoleLink('{{service.name}}', '{{ method.name }}'
2323
{%- for parameter in method.parameters.path -%}{%- set param = (parameter.name | caseCamel | escapeKeyword) -%}{%- if param ends with 'Id' -%}, {{ param }} {%- endif -%}{%- endfor -%}
2424
);
2525
} else {

templates/cli/index.js.twig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@ const chalk = require("chalk");
1010
const { version } = require("./package.json");
1111
const { commandDescriptions, cliConfig } = require("./lib/parser");
1212
const { client } = require("./lib/commands/generic");
13+
const inquirer = require("inquirer");
1314
{% if sdk.test != "true" %}
1415
const { login, logout, whoami } = require("./lib/commands/generic");
1516
const { init } = require("./lib/commands/init");
1617
const { pull } = require("./lib/commands/pull");
1718
const { push } = require("./lib/commands/push");
19+
{% else %}
20+
const { migrate } = require("./lib/commands/generic");
1821
{% endif %}
1922
{% for service in spec.services %}
2023
const { {{ service.name | caseLower }} } = require("./lib/commands/{{ service.name | caseLower }}");
2124
{% endfor %}
2225

26+
inquirer.registerPrompt('search-list', require('inquirer-search-list'));
27+
2328
program
2429
.description(commandDescriptions['main'])
2530
.configureHelp({
@@ -29,6 +34,7 @@ program
2934
.version(version, "-v, --version")
3035
.option("--verbose", "Show complete error log")
3136
.option("--json", "Output in JSON format")
37+
.hook('preAction', migrate)
3238
.option("-f,--force", "Flag to confirm all warnings")
3339
.option("-a,--all", "Flag to push all resources")
3440
.option("--id [id...]", "Flag to pass list of ids for a giving action")

templates/cli/lib/client.js.twig

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ const { fetch, FormData, Agent } = require("undici");
44
const JSONbig = require("json-bigint")({ storeAsString: false });
55
const {{spec.title | caseUcfirst}}Exception = require("./exception.js");
66
const { globalConfig } = require("./config.js");
7+
const chalk = require("chalk");
78

89
class Client {
910
CHUNK_SIZE = 5*1024*1024; // 5MB
10-
11+
1112
constructor() {
1213
this.endpoint = '{{spec.endpoint}}';
1314
this.headers = {
@@ -144,6 +145,14 @@ class Client {
144145
} catch (error) {
145146
throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text);
146147
}
148+
149+
if (path !== '/account' && json.code === 401 && json.type === 'user_more_factors_required') {
150+
console.log(`${chalk.cyan.bold("ℹ Info")} ${chalk.cyan("Unusable account found, removing...")}`);
151+
152+
const current = globalConfig.getCurrentSession();
153+
globalConfig.setCurrentSession('');
154+
globalConfig.removeSession(current);
155+
}
147156
throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json);
148157
}
149158

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({
7070
{%- if 'multipart/form-data' in method.consumes -%},onProgress = () => {}{%- endif -%}
7171

7272
{%- if method.type == 'location' -%}, destination{%- endif -%}
73-
{% if methodHaveConsolePreview(method.name,service.name) %}, console, open{%- endif -%}
73+
{% if methodHaveConsolePreview(method.name,service.name) %}, console{%- endif -%}
7474
}) => {
7575
{%~ endblock %}
7676
let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} :
@@ -97,7 +97,6 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({
9797
{% endif %}
9898
{% if methodHaveConsolePreview(method.name,service.name) %}
9999
.option(`--console`, `Get the resource console url`)
100-
.option(`--open`, `Use with '--console' to open the using default browser`)
101100
{% endif %}
102101
{% endautoescape %}
103102
.action(actionRunner({{ service.name | caseLower }}{{ method.name | caseUcfirst }}))

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

Lines changed: 129 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,69 @@ const { Command } = require("commander");
33
const Client = require("../client");
44
const { sdkForConsole } = require("../sdks");
55
const { globalConfig, localConfig } = require("../config");
6-
const { actionRunner, success, parseBool, commandDescriptions, error, parse, drawTable } = require("../parser");
6+
const { actionRunner, success, parseBool, commandDescriptions, error, parse, log, drawTable } = require("../parser");
7+
const ID = require("../id");
78
{% if sdk.test != "true" %}
8-
const { questionsLogin, questionsListFactors, questionsMfaChallenge } = require("../questions");
9-
const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account");
9+
const { questionsLogin, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions");
10+
const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account");
1011

11-
const loginCommand = async () => {
12-
const answers = await inquirer.prompt(questionsLogin)
12+
const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1';
1313

14-
let client = await sdkForConsole(false);
14+
const loginCommand = async ({ email, password, endpoint, mfa, code }) => {
15+
const oldCurrent = globalConfig.getCurrentSession();
16+
let configEndpoint = endpoint ?? DEFAULT_ENDPOINT;
1517

16-
await accountCreateEmailPasswordSession({
17-
email: answers.email,
18-
password: answers.password,
19-
parseOutput: false,
20-
sdk: client
21-
})
18+
const answers = email && password ? { email, password } : await inquirer.prompt(questionsLogin);
19+
20+
if (answers.method === 'select') {
21+
const accountId = answers.accountId;
22+
23+
if (!globalConfig.getSessionIds().includes(accountId)) {
24+
throw Error('Session ID not found');
25+
}
26+
27+
globalConfig.setCurrentSession(accountId);
28+
success(`Current account is ${accountId}`);
29+
30+
return;
31+
}
32+
33+
const id = ID.unique();
2234

23-
client.setCookie(globalConfig.getCookie());
35+
globalConfig.addSession(id, {});
36+
globalConfig.setCurrentSession(id);
37+
globalConfig.setEndpoint(configEndpoint);
38+
globalConfig.setEmail(answers.email);
39+
40+
let client = await sdkForConsole(false);
2441

2542
let account;
2643

2744
try {
45+
await accountCreateEmailPasswordSession({
46+
email: answers.email,
47+
password: answers.password,
48+
parseOutput: false,
49+
sdk: client
50+
})
51+
52+
client.setCookie(globalConfig.getCookie());
53+
2854
account = await accountGet({
2955
sdk: client,
3056
parseOutput: false
3157
});
32-
} catch(error) {
58+
} catch (error) {
3359
if (error.response === 'user_more_factors_required') {
34-
const { factor } = await inquirer.prompt(questionsListFactors);
60+
const { factor } = mfa ? { factor: mfa } : await inquirer.prompt(questionsListFactors);
3561

3662
const challenge = await accountCreateMfaChallenge({
3763
factor,
3864
parseOutput: false,
3965
sdk: client
4066
});
4167

42-
const { otp } = await inquirer.prompt(questionsMfaChallenge);
68+
const { otp } = code ? { otp: code } : await inquirer.prompt(questionsMfaChallenge);
4369

4470
await accountUpdateMfaChallenge({
4571
challengeId: challenge.$id,
@@ -53,6 +79,8 @@ const loginCommand = async () => {
5379
parseOutput: false
5480
});
5581
} else {
82+
globalConfig.removeSession(id);
83+
globalConfig.setCurrentSession(oldCurrent);
5684
throw error;
5785
}
5886
}
@@ -88,7 +116,8 @@ const whoami = new Command("whoami")
88116
'ID': account.$id,
89117
'Name': account.name,
90118
'Email': account.email,
91-
'MFA enabled': account.mfa ? 'Yes' : 'No'
119+
'MFA enabled': account.mfa ? 'Yes' : 'No',
120+
'Endpoint': globalConfig.getEndpoint()
92121
}
93122
];
94123
if (json) {
@@ -100,20 +129,20 @@ const whoami = new Command("whoami")
100129
drawTable(data)
101130
}));
102131

103-
104132
const login = new Command("login")
105133
.description(commandDescriptions['login'])
134+
.option(`--email [email]`, `User email`)
135+
.option(`--password [password]`, `User password`)
136+
.option(`--endpoint [endpoint]`, `Appwrite endpoint for self hosted instances`)
137+
.option(`--mfa [factor]`, `Multi-factor authentication login factor: totp, email, phone or recoveryCode`)
138+
.option(`--code [code]`, `Multi-factor code`)
106139
.configureHelp({
107140
helpWidth: process.stdout.columns || 80
108141
})
109142
.action(actionRunner(loginCommand));
110143

111-
const logout = new Command("logout")
112-
.description(commandDescriptions['logout'])
113-
.configureHelp({
114-
helpWidth: process.stdout.columns || 80
115-
})
116-
.action(actionRunner(async () => {
144+
const deleteSession = async (accountId) => {
145+
try {
117146
let client = await sdkForConsole();
118147

119148
await accountDeleteSession({
@@ -122,8 +151,51 @@ const logout = new Command("logout")
122151
sdk: client
123152
})
124153

125-
globalConfig.setCookie("");
126-
success()
154+
globalConfig.removeSession(accountId);
155+
} catch (e) {
156+
error('Unable to log out, removing locally saved session information')
157+
}
158+
globalConfig.removeSession(accountId);
159+
}
160+
161+
const logout = new Command("logout")
162+
.description(commandDescriptions['logout'])
163+
.configureHelp({
164+
helpWidth: process.stdout.columns || 80
165+
})
166+
.action(actionRunner(async () => {
167+
const sessions = globalConfig.getSessions();
168+
const current = globalConfig.getCurrentSession();
169+
170+
if (current === '') {
171+
return;
172+
}
173+
if (sessions.length === 1) {
174+
await deleteSession(current);
175+
success();
176+
177+
return;
178+
}
179+
180+
const answers = await inquirer.prompt(questionsLogout);
181+
182+
if (answers.accounts) {
183+
for (let accountId of answers.accounts) {
184+
globalConfig.setCurrentSession(accountId);
185+
await deleteSession(accountId);
186+
}
187+
}
188+
189+
const leftSessions = globalConfig.getSessions();
190+
191+
if (leftSessions.length > 0 && leftSessions.filter(session => session.id === current).length !== 1) {
192+
const accountId = leftSessions[0].id;
193+
globalConfig.setCurrentSession(accountId);
194+
195+
success(`Current account is ${accountId}`);
196+
}
197+
198+
success();
127199
}));
128200
{% endif %}
129201

@@ -156,6 +228,7 @@ const client = new Command("client")
156228

157229
if (endpoint !== undefined) {
158230
try {
231+
const id = ID.unique();
159232
let url = new URL(endpoint);
160233
if (url.protocol !== "http:" && url.protocol !== "https:") {
161234
throw new Error();
@@ -170,7 +243,8 @@ const client = new Command("client")
170243
if (!response.version) {
171244
throw new Error();
172245
}
173-
246+
globalConfig.setCurrentSession(id);
247+
globalConfig.addSession(id, {});
174248
globalConfig.setEndpoint(endpoint);
175249
} catch (_) {
176250
throw new Error("Invalid endpoint or your Appwrite server is not running as expected.");
@@ -190,22 +264,45 @@ const client = new Command("client")
190264
}
191265

192266
if (reset !== undefined) {
193-
globalConfig.setEndpoint("");
194-
globalConfig.setKey("");
195-
globalConfig.setCookie("");
196-
globalConfig.setSelfSigned("");
197-
localConfig.setProject("", "");
267+
const sessions = globalConfig.getSessions();
268+
269+
for (let accountId of sessions.map(session => session.id)) {
270+
globalConfig.setCurrentSession(accountId);
271+
await deleteSession(accountId);
272+
}
198273
}
199274

200275
success()
201276
}));
202277

278+
const migrate = async () => {
279+
if (!globalConfig.has('endpoint') || !globalConfig.has('cookie')) {
280+
return;
281+
}
282+
283+
const endpoint = globalConfig.get('endpoint');
284+
const cookie = globalConfig.get('cookie');
285+
286+
const id = ID.unique();
287+
const data = {
288+
endpoint,
289+
cookie,
290+
email: 'legacy'
291+
};
292+
293+
globalConfig.addSession(id, data);
294+
globalConfig.setCurrentSession(id);
295+
globalConfig.delete('endpoint');
296+
globalConfig.delete('cookie');
297+
298+
}
203299
module.exports = {
204300
{% if sdk.test != "true" %}
205301
loginCommand,
206302
whoami,
207303
login,
208304
logout,
209305
{% endif %}
306+
migrate,
210307
client
211308
};

0 commit comments

Comments
 (0)