Skip to content

Commit 075e353

Browse files
committed
bring back legacy client
1 parent 61a1760 commit 075e353

File tree

2 files changed

+342
-8
lines changed

2 files changed

+342
-8
lines changed

lib/client.ts

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
import os from "os";
2+
import { fetch, FormData, Agent } from "undici";
3+
import JSONbig from "json-bigint";
4+
import AppwriteException from "./exception.js";
5+
import { globalConfig } from "./config.js";
6+
import chalk from "chalk";
7+
import type {
8+
Headers,
9+
RequestParams,
10+
ResponseType,
11+
FileUpload,
12+
} from "./types.js";
13+
14+
const JSONBigInt = JSONbig({ storeAsString: false });
15+
16+
class Client {
17+
private endpoint: string;
18+
private headers: Headers;
19+
private selfSigned: boolean;
20+
21+
constructor() {
22+
this.endpoint = "https://cloud.appwrite.io/v1";
23+
this.selfSigned = false;
24+
this.headers = {
25+
"content-type": "",
26+
"x-sdk-name": "Command Line",
27+
"x-sdk-platform": "console",
28+
"x-sdk-language": "cli",
29+
"x-sdk-version": "13.0.0-rc.1",
30+
"user-agent": `AppwriteCLI/13.0.0-rc.1 (${os.type()} ${os.version()}; ${os.arch()})`,
31+
"X-Appwrite-Response-Format": "1.8.0",
32+
};
33+
}
34+
35+
/**
36+
* Set Cookie
37+
*
38+
* Your cookie
39+
*
40+
* @param {string} cookie
41+
*
42+
* @return self
43+
*/
44+
setCookie(cookie: string): this {
45+
this.addHeader("cookie", cookie);
46+
return this;
47+
}
48+
49+
/**
50+
* Set Project
51+
*
52+
* Your project ID
53+
*
54+
* @param {string} project
55+
*
56+
* @return self
57+
*/
58+
setProject(project: string): this {
59+
this.addHeader("X-Appwrite-Project", project);
60+
return this;
61+
}
62+
63+
/**
64+
* Set Key
65+
*
66+
* Your secret API key
67+
*
68+
* @param {string} key
69+
*
70+
* @return self
71+
*/
72+
setKey(key: string): this {
73+
this.addHeader("X-Appwrite-Key", key);
74+
return this;
75+
}
76+
77+
/**
78+
* Set JWT
79+
*
80+
* Your secret JSON Web Token
81+
*
82+
* @param {string} jwt
83+
*
84+
* @return self
85+
*/
86+
setJWT(jwt: string): this {
87+
this.addHeader("X-Appwrite-JWT", jwt);
88+
return this;
89+
}
90+
91+
/**
92+
* Set Locale
93+
*
94+
* @param {string} locale
95+
*
96+
* @return self
97+
*/
98+
setLocale(locale: string): this {
99+
this.addHeader("X-Appwrite-Locale", locale);
100+
return this;
101+
}
102+
103+
/**
104+
* Set Mode
105+
*
106+
* @param {string} mode
107+
*
108+
* @return self
109+
*/
110+
setMode(mode: string): this {
111+
this.addHeader("X-Appwrite-Mode", mode);
112+
return this;
113+
}
114+
115+
/**
116+
* Set self signed.
117+
*
118+
* @param {bool} status
119+
*
120+
* @return this
121+
*/
122+
setSelfSigned(status: boolean): this {
123+
this.selfSigned = status;
124+
return this;
125+
}
126+
127+
/**
128+
* Set endpoint.
129+
*
130+
* @param {string} endpoint
131+
*
132+
* @return this
133+
*/
134+
setEndpoint(endpoint: string): this {
135+
if (!endpoint.startsWith("http://") && !endpoint.startsWith("https://")) {
136+
throw new AppwriteException("Invalid endpoint URL: " + endpoint);
137+
}
138+
139+
this.endpoint = endpoint;
140+
return this;
141+
}
142+
143+
/**
144+
* @param {string} key
145+
* @param {string} value
146+
*/
147+
addHeader(key: string, value: string): this {
148+
this.headers[key.toLowerCase()] = value;
149+
return this;
150+
}
151+
152+
async call<T = unknown>(
153+
method: string,
154+
path: string = "",
155+
headers: Headers = {},
156+
params: RequestParams = {},
157+
responseType: ResponseType = "json",
158+
): Promise<T> {
159+
const mergedHeaders: Headers = { ...this.headers, ...headers };
160+
const url = new URL(this.endpoint + path);
161+
162+
let body: FormData | string | undefined = undefined;
163+
164+
if (method.toUpperCase() === "GET") {
165+
url.search = new URLSearchParams(
166+
Client.flatten(params) as Record<string, string>,
167+
).toString();
168+
} else if (
169+
mergedHeaders["content-type"]
170+
?.toLowerCase()
171+
.startsWith("multipart/form-data")
172+
) {
173+
delete mergedHeaders["content-type"];
174+
const formData = new FormData();
175+
176+
const flatParams = Client.flatten(params);
177+
178+
for (const [key, value] of Object.entries(flatParams)) {
179+
if (
180+
value &&
181+
typeof value === "object" &&
182+
"type" in value &&
183+
value.type === "file"
184+
) {
185+
const fileUpload = value as FileUpload;
186+
formData.append(key, fileUpload.file as any, fileUpload.filename);
187+
} else {
188+
formData.append(key, value as string);
189+
}
190+
}
191+
192+
body = formData;
193+
} else {
194+
body = JSONBigInt.stringify(params);
195+
}
196+
197+
let response: Awaited<ReturnType<typeof fetch>> | undefined = undefined;
198+
try {
199+
response = await fetch(url.toString(), {
200+
method: method.toUpperCase(),
201+
headers: mergedHeaders,
202+
body,
203+
dispatcher: new Agent({
204+
connect: {
205+
rejectUnauthorized: !this.selfSigned,
206+
},
207+
}),
208+
});
209+
} catch (error) {
210+
throw new AppwriteException((error as Error).message);
211+
}
212+
213+
if (response.status >= 400) {
214+
const text = await response.text();
215+
let json: { message?: string; code?: number; type?: string } | undefined =
216+
undefined;
217+
try {
218+
json = JSON.parse(text);
219+
} catch (error) {
220+
throw new AppwriteException(text, response.status, "", text);
221+
}
222+
223+
if (
224+
path !== "/account" &&
225+
json.code === 401 &&
226+
json.type === "user_more_factors_required"
227+
) {
228+
console.log(
229+
`${chalk.cyan.bold("ℹ Info")} ${chalk.cyan("Unusable account found, removing...")}`,
230+
);
231+
232+
const current = globalConfig.getCurrentSession();
233+
globalConfig.setCurrentSession("");
234+
globalConfig.removeSession(current);
235+
}
236+
throw new AppwriteException(
237+
json.message || text,
238+
json.code,
239+
json.type,
240+
text,
241+
);
242+
}
243+
244+
if (responseType === "arraybuffer") {
245+
const data = await response.arrayBuffer();
246+
return data as T;
247+
}
248+
249+
const cookies = response.headers.getSetCookie();
250+
if (cookies && cookies.length > 0) {
251+
for (const cookie of cookies) {
252+
if (cookie.startsWith("a_session_console=")) {
253+
globalConfig.setCookie(cookie);
254+
}
255+
}
256+
}
257+
258+
const text = await response.text();
259+
let json: T | undefined = undefined;
260+
try {
261+
json = JSONBigInt.parse(text);
262+
} catch (error) {
263+
return text as T;
264+
}
265+
return json as T;
266+
}
267+
268+
static flatten(
269+
data: RequestParams,
270+
prefix: string = "",
271+
): Record<string, unknown> {
272+
let output: Record<string, unknown> = {};
273+
274+
for (const key in data) {
275+
const value = data[key];
276+
const finalKey = prefix ? prefix + "[" + key + "]" : key;
277+
278+
if (Array.isArray(value)) {
279+
output = {
280+
...output,
281+
...Client.flatten(value as unknown as RequestParams, finalKey),
282+
};
283+
} else {
284+
output[finalKey] = value;
285+
}
286+
}
287+
288+
return output;
289+
}
290+
}
291+
292+
export default Client;

lib/commands/generic.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
questionsMFAChallenge,
2424
} from "../questions.js";
2525
import { Account, Client as ConsoleClient } from "@appwrite.io/console";
26+
import ClientLegacy from "../client.js";
2627

2728
const DEFAULT_ENDPOINT = "https://cloud.appwrite.io/v1";
2829

@@ -83,22 +84,46 @@ export const loginCommand = async ({
8384
globalConfig.setEndpoint(configEndpoint);
8485
globalConfig.setEmail(answers.email);
8586

87+
// Use legacy client for login to extract cookies from response
88+
const legacyClient = new ClientLegacy();
89+
legacyClient.setEndpoint(configEndpoint);
90+
legacyClient.setProject("console");
91+
92+
if (globalConfig.getSelfSigned()) {
93+
legacyClient.setSelfSigned(true);
94+
}
95+
8696
let client = await sdkForConsole(false);
8797
let accountClient = new Account(client);
8898

8999
let account;
90100

91101
try {
92-
await accountClient.createEmailPasswordSession({
93-
email: answers.email,
94-
password: answers.password,
95-
});
102+
await legacyClient.call(
103+
"POST",
104+
"/account/sessions/email",
105+
{
106+
"content-type": "application/json",
107+
},
108+
{
109+
email: answers.email,
110+
password: answers.password,
111+
},
112+
);
113+
114+
const savedCookie = globalConfig.getCookie();
96115

97-
client.setCookie(globalConfig.getCookie());
116+
if (savedCookie) {
117+
client.setCookie(savedCookie);
118+
}
98119

120+
accountClient = new Account(client);
99121
account = await accountClient.get();
100122
} catch (err: any) {
101-
if (err.response === "user_more_factors_required") {
123+
if (
124+
err.type === "user_more_factors_required" ||
125+
err.response === "user_more_factors_required"
126+
) {
102127
const { factor } = mfa
103128
? { factor: mfa }
104129
: await inquirer.prompt(questionsListFactors);
@@ -109,15 +134,32 @@ export const loginCommand = async ({
109134
? { otp: code }
110135
: await inquirer.prompt(questionsMFAChallenge);
111136

112-
await accountClient.updateMfaChallenge(challenge.$id, otp);
137+
await legacyClient.call(
138+
"PUT",
139+
"/account/sessions/mfa/challenge",
140+
{
141+
"content-type": "application/json",
142+
},
143+
{
144+
challengeId: challenge.$id,
145+
otp: otp,
146+
},
147+
);
148+
149+
const savedCookie = globalConfig.getCookie();
150+
if (savedCookie) {
151+
client.setCookie(savedCookie);
152+
}
113153

154+
accountClient = new Account(client);
114155
account = await accountClient.get();
115156
} else {
116157
globalConfig.removeSession(id);
117158
globalConfig.setCurrentSession(oldCurrent);
118159
if (
119160
endpoint !== DEFAULT_ENDPOINT &&
120-
err.response === "user_invalid_credentials"
161+
(err.type === "user_invalid_credentials" ||
162+
err.response === "user_invalid_credentials")
121163
) {
122164
log("Use the --endpoint option for self-hosted instances");
123165
}

0 commit comments

Comments
 (0)