Skip to content

Commit eee5498

Browse files
Add arg based login
1 parent 6080028 commit eee5498

File tree

3 files changed

+166
-47
lines changed

3 files changed

+166
-47
lines changed

bin/index.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ async function main() {
2626
program
2727
.command('login')
2828
.description('Login to Puter account')
29-
.option('-s, --save', 'Save authentication token in .env file', '')
30-
.action(async () => {
31-
await login();
29+
.option('-s, --save', 'Save authentication token in .env file')
30+
.option('--web', 'Use browser-based login (default)')
31+
.option('--with-credentials', 'Use username/password login')
32+
.option('--host <url>', 'Puter host URL', 'https://puter.com')
33+
.action(async (options) => {
34+
await login(options);
3235
process.exit(0);
3336
});
3437

src/commands/auth.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ const config = new Conf({ projectName: PROJECT_NAME });
88

99
/**
1010
* Login user
11+
* @param {Object} options - Login options
12+
* @param {boolean} options.save - Save token to .env file
13+
* @param {boolean} options.web - Use browser-based login (default)
14+
* @param {boolean} options.withCredentials - Use username/password login
15+
* @param {string} options.host - Puter host URL
1116
* @returns void
1217
*/
13-
export async function login() {
14-
const profileAPI = getProfileModule();;
15-
await profileAPI.switchProfileWizard();
18+
export async function login(options = {}) {
19+
const profileAPI = getProfileModule();
20+
await profileAPI.switchProfileWizard(options);
1621
}
1722

1823
/**

src/modules/ProfileModule.js

Lines changed: 152 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Conf from 'conf';
44
import chalk from 'chalk';
55
import ora from 'ora';
66
import {getAuthToken} from "@heyputer/puter.js/src/init.cjs";
7+
import { puter } from "@heyputer/puter.js";
78

89
// project
910
import { BASE_URL, NULL_UUID, PROJECT_NAME, getHeaders, reconfigureURLs } from '../commons.js'
@@ -109,10 +110,9 @@ class ProfileModule {
109110
async switchProfileWizard(args = {}) {
110111
const profiles = this.getProfiles();
111112
if (profiles.length < 1) {
112-
return this.addProfileWizard();
113+
return this.addProfileWizard(args);
113114
}
114115

115-
// console.log('doing this branch');
116116
const answer = await inquirer.prompt([
117117
{
118118
name: 'profile',
@@ -134,77 +134,188 @@ class ProfileModule {
134134
]);
135135

136136
if (answer.profile === 'new') {
137-
return await this.addProfileWizard();
137+
return await this.addProfileWizard(args);
138138
}
139139

140140
this.selectProfile(answer.profile);
141141
}
142142

143143
async addProfileWizard(args = {}) {
144+
const host = args.host || 'https://puter.com';
145+
146+
if (args.withCredentials) {
147+
return await this.credentialLogin({ ...args, host });
148+
}
149+
150+
// Browser-based login (default)
151+
return await this.browserLogin({ ...args, host });
152+
}
153+
154+
async browserLogin(args) {
155+
const { host, save } = args;
156+
const TIMEOUT_MS = 60000; // 1 minute timeout
157+
let spinner;
158+
159+
try {
160+
spinner = ora('Opening browser for login...').start();
161+
162+
const timeoutPromise = new Promise((_, reject) => {
163+
setTimeout(() => reject(new Error('Login timed out after 60 seconds')), TIMEOUT_MS);
164+
});
165+
166+
const authToken = await Promise.race([
167+
getAuthToken(),
168+
timeoutPromise
169+
]);
170+
171+
if (!authToken) {
172+
spinner.fail(chalk.red('Login failed or was cancelled.'));
173+
return;
174+
}
175+
176+
spinner.text = 'Fetching user info...';
177+
178+
// Set token and fetch user info
179+
puter.setAuthToken(authToken);
180+
const userInfo = await puter.auth.getUser();
181+
182+
const profileUUID = crypto.randomUUID();
183+
const profile = {
184+
host,
185+
username: userInfo.username,
186+
cwd: `/${userInfo.username}`,
187+
token: authToken,
188+
uuid: profileUUID,
189+
};
190+
191+
this.addProfile(profile);
192+
this.selectProfile(profile);
193+
spinner.succeed(chalk.green(`Successfully logged in as ${userInfo.username}!`));
194+
195+
// Handle --save option
196+
this.saveTokenToEnv(authToken, save);
197+
} catch (error) {
198+
if (spinner) {
199+
spinner.fail(chalk.red(`Failed to login: ${error.message}`));
200+
} else {
201+
console.error(chalk.red(`Failed to login: ${error.message}`));
202+
}
203+
}
204+
}
205+
206+
async credentialLogin(args) {
207+
const { host, save } = args;
208+
144209
const answers = await inquirer.prompt([
145-
{
146-
type: 'input',
147-
name: 'host',
148-
message: 'Host (leave blank for puter.com):',
149-
default: 'https://puter.com',
150-
validate: input => input.length >= 1 || 'Host is required'
151-
},
152210
{
153211
type: 'input',
154212
name: 'username',
155213
message: 'Username:',
156214
validate: input => input.length >= 1 || 'Username is required'
157215
},
216+
{
217+
type: 'password',
218+
name: 'password',
219+
message: 'Password:',
220+
mask: '*',
221+
validate: input => input.length >= 1 || 'Password is required'
222+
}
158223
]);
159224

160225
let spinner;
161226
try {
162227
spinner = ora('Logging in to Puter...').start();
163-
const authToken = await getAuthToken();
164228

165-
if (authToken) {
166-
const profileUUID = crypto.randomUUID();
167-
const profile = {
168-
host: answers.host,
229+
const apiHost = toApiSubdomain(host);
230+
const response = await fetch(`${apiHost}/login`, {
231+
method: 'POST',
232+
headers: getHeaders(),
233+
body: JSON.stringify({
169234
username: answers.username,
170-
cwd: `/${answers.username}`,
171-
token: authToken,
172-
uuid: profileUUID,
173-
};
174-
this.addProfile(profile);
175-
this.selectProfile(profile);
176-
if (spinner) {
177-
spinner.succeed(chalk.green('Successfully logged in to Puter!'));
178-
}
179-
// Save token
180-
if (args.save) {
181-
const localEnvFile = '.env';
182-
try {
183-
// Check if the file exists, if so then append the api key to the EOF.
184-
if (fs.existsSync(localEnvFile)) {
185-
console.log(chalk.yellow(`File "${localEnvFile}" already exists... Adding token.`));
186-
fs.appendFileSync(localEnvFile, `\nPUTER_API_KEY="${authToken}"`, 'utf8');
187-
} else {
188-
console.log(chalk.cyan(`Saving token to ${chalk.green(localEnvFile)} file.`));
189-
fs.writeFileSync(localEnvFile, `PUTER_API_KEY="${authToken}"`, 'utf8');
190-
}
191-
} catch (error) {
192-
console.error(chalk.red(`Cannot save token to .env file. Error: ${error.message}`));
193-
console.log(chalk.cyan(`PUTER_API_KEY="${authToken}"`));
235+
password: answers.password,
236+
}),
237+
});
238+
239+
const data = await response.json();
240+
241+
if (data.proceed && data.next_step === 'otp') {
242+
// Handle 2FA
243+
spinner.stop();
244+
const otpAnswer = await inquirer.prompt([
245+
{
246+
type: 'input',
247+
name: 'otp',
248+
message: 'Enter your 2FA code:',
249+
validate: input => input.length >= 1 || '2FA code is required'
194250
}
251+
]);
252+
253+
spinner = ora('Verifying 2FA code...').start();
254+
const otpResponse = await fetch(`${apiHost}/login/otp`, {
255+
method: 'POST',
256+
headers: getHeaders(),
257+
body: JSON.stringify({
258+
token: data.otp_jwt_token,
259+
code: otpAnswer.otp,
260+
}),
261+
});
262+
263+
const otpData = await otpResponse.json();
264+
265+
if (otpData.token) {
266+
this.createProfileFromToken(otpData.token, answers.username, host, spinner, save);
267+
} else {
268+
spinner.fail(chalk.red('2FA verification failed.'));
195269
}
270+
} else if (data.token) {
271+
this.createProfileFromToken(data.token, answers.username, host, spinner, save);
196272
} else {
197-
spinner.fail(chalk.red('Login failed. Please check your credentials.'));
273+
spinner.fail(chalk.red(data.error?.message || 'Login failed. Please check your credentials.'));
198274
}
199275
} catch (error) {
200276
if (spinner) {
201277
spinner.fail(chalk.red(`Failed to login: ${error.message}`));
202-
console.log(error);
203278
} else {
204279
console.error(chalk.red(`Failed to login: ${error.message}`));
205280
}
206281
}
207282
}
283+
284+
createProfileFromToken(token, username, host, spinner, save) {
285+
const profileUUID = crypto.randomUUID();
286+
const profile = {
287+
host,
288+
username,
289+
cwd: `/${username}`,
290+
token,
291+
uuid: profileUUID,
292+
};
293+
294+
this.addProfile(profile);
295+
this.selectProfile(profile);
296+
spinner.succeed(chalk.green(`Successfully logged in as ${username}!`));
297+
298+
// Handle --save option
299+
this.saveTokenToEnv(token, save);
300+
}
301+
302+
saveTokenToEnv(token, save) {
303+
if (!save) return;
304+
305+
const localEnvFile = '.env';
306+
try {
307+
if (fs.existsSync(localEnvFile)) {
308+
console.log(chalk.yellow(`File "${localEnvFile}" already exists... Adding token.`));
309+
fs.appendFileSync(localEnvFile, `\nPUTER_API_KEY="${token}"`, 'utf8');
310+
} else {
311+
console.log(chalk.cyan(`Saving token to ${chalk.green(localEnvFile)} file.`));
312+
fs.writeFileSync(localEnvFile, `PUTER_API_KEY="${token}"`, 'utf8');
313+
}
314+
} catch (error) {
315+
console.error(chalk.red(`Cannot save token to .env file. Error: ${error.message}`));
316+
console.log(chalk.cyan(`PUTER_API_KEY="${token}"`));
317+
}
318+
}
208319
}
209320

210321
export const initProfileModule = () => {

0 commit comments

Comments
 (0)