@startanaicompany/cli 1.4.15 → 1.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +105 -3
- package/bin/saac.js +19 -1
- package/package.json +1 -1
- package/src/commands/keys.js +143 -0
- package/src/commands/login.js +143 -42
- package/src/lib/api.js +59 -0
package/CLAUDE.md
CHANGED
|
@@ -395,6 +395,101 @@ saac deploy --force
|
|
|
395
395
|
|
|
396
396
|
**Note:** The `--force` flag is defined in the CLI but not currently used by the API.
|
|
397
397
|
|
|
398
|
+
### OTP-Based Login (NEW in 1.4.16)
|
|
399
|
+
|
|
400
|
+
The login command now supports two authentication methods:
|
|
401
|
+
|
|
402
|
+
**Method 1: Login with API Key (Fast Path)**
|
|
403
|
+
```bash
|
|
404
|
+
saac login -e user@example.com -k cw_abc123
|
|
405
|
+
# → Immediate session token, no email verification needed
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Method 2: Login with OTP (Recovery Path)**
|
|
409
|
+
```bash
|
|
410
|
+
# Step 1: Request OTP
|
|
411
|
+
saac login -e user@example.com
|
|
412
|
+
# → Verification code sent to email (6 digits, 5-minute expiration)
|
|
413
|
+
|
|
414
|
+
# Step 2: Verify OTP
|
|
415
|
+
saac login -e user@example.com --otp 123456
|
|
416
|
+
# → Session token created, user is now logged in
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Why This Feature?**
|
|
420
|
+
|
|
421
|
+
Solves the **API key lockout problem**:
|
|
422
|
+
- User loses API key
|
|
423
|
+
- All sessions expire
|
|
424
|
+
- Cannot login (no API key)
|
|
425
|
+
- Cannot regenerate key (requires login)
|
|
426
|
+
- **LOCKED OUT** ❌
|
|
427
|
+
|
|
428
|
+
With OTP login:
|
|
429
|
+
- User can always recover via email ✅
|
|
430
|
+
- No support tickets needed ✅
|
|
431
|
+
- Self-service account recovery ✅
|
|
432
|
+
|
|
433
|
+
**Implementation Details:**
|
|
434
|
+
|
|
435
|
+
The `login.js` command now has three modes:
|
|
436
|
+
1. **API key login** - If `-k` flag provided (existing flow)
|
|
437
|
+
2. **OTP request** - If no `-k` or `--otp` flag (new flow)
|
|
438
|
+
3. **OTP verification** - If `--otp` flag provided (new flow)
|
|
439
|
+
|
|
440
|
+
**Backend Requirements:**
|
|
441
|
+
- `POST /api/v1/auth/login-otp` - Generate and send OTP
|
|
442
|
+
- `POST /api/v1/auth/verify-otp` - Verify OTP and create session
|
|
443
|
+
|
|
444
|
+
See `/data/sharedinfo/login-feature-update.md` for complete API specifications.
|
|
445
|
+
|
|
446
|
+
### API Key Management (NEW in 1.4.16)
|
|
447
|
+
|
|
448
|
+
Users can now regenerate their API keys if lost or compromised.
|
|
449
|
+
|
|
450
|
+
**Commands:**
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
# Regenerate API key (requires authentication)
|
|
454
|
+
saac keys regenerate
|
|
455
|
+
# → Shows new API key (only once!)
|
|
456
|
+
|
|
457
|
+
# Show API key info
|
|
458
|
+
saac keys show
|
|
459
|
+
# → Displays key prefix, created date, last used
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Recovery Flow:**
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
# 1. User loses API key but is not logged in
|
|
466
|
+
saac login -e user@example.com
|
|
467
|
+
# → OTP sent to email
|
|
468
|
+
|
|
469
|
+
# 2. Verify OTP
|
|
470
|
+
saac login -e user@example.com --otp 123456
|
|
471
|
+
# → Logged in with session token
|
|
472
|
+
|
|
473
|
+
# 3. Generate new API key
|
|
474
|
+
saac keys regenerate
|
|
475
|
+
# → New API key: cw_new_key_xyz...
|
|
476
|
+
|
|
477
|
+
# 4. On next machine, use new API key
|
|
478
|
+
saac login -e user@example.com -k cw_new_key_xyz...
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Security Notes:**
|
|
482
|
+
- Regenerating API key invalidates the old key immediately
|
|
483
|
+
- Existing session tokens remain valid (no disruption)
|
|
484
|
+
- Email notification sent when key is regenerated
|
|
485
|
+
- Full API key shown only once (must be saved)
|
|
486
|
+
|
|
487
|
+
**Backend Requirements:**
|
|
488
|
+
- `POST /api/v1/users/regenerate-key` - Generate new API key
|
|
489
|
+
- `GET /api/v1/users/api-key` - Get API key info (optional)
|
|
490
|
+
|
|
491
|
+
See `/data/sharedinfo/login-feature-update.md` for complete specifications.
|
|
492
|
+
|
|
398
493
|
### Init Command Implementation
|
|
399
494
|
|
|
400
495
|
The `init` command links an existing SAAC application to the current directory.
|
|
@@ -493,19 +588,26 @@ The wrapper API expects Git repositories to be hosted on the StartAnAiCompany Gi
|
|
|
493
588
|
- During registration, Gitea username can be auto-detected or manually provided
|
|
494
589
|
- Applications reference repositories in the format: `git@git.startanaicompany.com:user/repo.git`
|
|
495
590
|
|
|
496
|
-
## Current Status - Version 1.4.
|
|
591
|
+
## Current Status - Version 1.4.16
|
|
497
592
|
|
|
498
593
|
### Completed Features
|
|
499
594
|
|
|
500
595
|
**Authentication & Sessions:**
|
|
501
596
|
- ✅ `saac register` - Register with email only (git_username auto-detected from email)
|
|
502
|
-
- ✅ `saac login` -
|
|
597
|
+
- ✅ `saac login` - Two methods: API key (fast) or OTP (recovery)
|
|
598
|
+
- `saac login -e email -k api-key` - Login with API key
|
|
599
|
+
- `saac login -e email` - Request OTP via email
|
|
600
|
+
- `saac login -e email --otp code` - Verify OTP and login
|
|
503
601
|
- ✅ `saac verify` - Email verification, shows FULL API key for user to save
|
|
504
602
|
- ✅ `saac logout` - Logout from current device
|
|
505
603
|
- ✅ `saac logout-all` - Revoke all sessions
|
|
506
604
|
- ✅ `saac sessions` - List all active sessions
|
|
507
605
|
- ✅ `saac whoami` - Show current user info
|
|
508
606
|
|
|
607
|
+
**API Key Management (NEW in 1.4.16):**
|
|
608
|
+
- ✅ `saac keys regenerate` - Generate new API key (invalidates old one)
|
|
609
|
+
- ✅ `saac keys show` - Show API key information (prefix, created, last used)
|
|
610
|
+
|
|
509
611
|
**Git OAuth (NEW in 1.4.0):**
|
|
510
612
|
- ✅ `saac git connect [host]` - OAuth flow for Git authentication
|
|
511
613
|
- ✅ `saac git list` - List connected Git accounts
|
|
@@ -706,4 +808,4 @@ Before publishing to npm:
|
|
|
706
808
|
- `dotenv` - Environment variables
|
|
707
809
|
- `open` - Open browser for OAuth (v8.4.2 for compatibility with chalk v4)
|
|
708
810
|
|
|
709
|
-
**Version:** 1.4.
|
|
811
|
+
**Version:** 1.4.16 (current)
|
package/bin/saac.js
CHANGED
|
@@ -16,6 +16,7 @@ const verify = require('../src/commands/verify');
|
|
|
16
16
|
const logout = require('../src/commands/logout');
|
|
17
17
|
const logoutAll = require('../src/commands/logoutAll');
|
|
18
18
|
const sessions = require('../src/commands/sessions');
|
|
19
|
+
const keys = require('../src/commands/keys');
|
|
19
20
|
const git = require('../src/commands/git');
|
|
20
21
|
const init = require('../src/commands/init');
|
|
21
22
|
const create = require('../src/commands/create');
|
|
@@ -56,7 +57,8 @@ program
|
|
|
56
57
|
.command('login')
|
|
57
58
|
.description('Login with existing account')
|
|
58
59
|
.option('-e, --email <email>', 'Email address')
|
|
59
|
-
.option('-k, --api-key <key>', 'API key')
|
|
60
|
+
.option('-k, --api-key <key>', 'API key (for fast login)')
|
|
61
|
+
.option('--otp <code>', 'One-time password from email')
|
|
60
62
|
.action(login);
|
|
61
63
|
|
|
62
64
|
program
|
|
@@ -80,6 +82,22 @@ program
|
|
|
80
82
|
.description('List all active sessions')
|
|
81
83
|
.action(sessions);
|
|
82
84
|
|
|
85
|
+
// API Key management
|
|
86
|
+
const keysCommand = program
|
|
87
|
+
.command('keys')
|
|
88
|
+
.description('Manage API keys');
|
|
89
|
+
|
|
90
|
+
keysCommand
|
|
91
|
+
.command('regenerate')
|
|
92
|
+
.description('Generate a new API key (invalidates old one)')
|
|
93
|
+
.action(keys.regenerate);
|
|
94
|
+
|
|
95
|
+
keysCommand
|
|
96
|
+
.command('show')
|
|
97
|
+
.alias('info')
|
|
98
|
+
.description('Show API key information')
|
|
99
|
+
.action(keys.show);
|
|
100
|
+
|
|
83
101
|
// Git OAuth commands
|
|
84
102
|
const gitCommand = program
|
|
85
103
|
.command('git')
|
package/package.json
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Key management commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const api = require('../lib/api');
|
|
6
|
+
const { getUser, isAuthenticated } = require('../lib/config');
|
|
7
|
+
const logger = require('../lib/logger');
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Regenerate API key
|
|
12
|
+
*/
|
|
13
|
+
async function regenerate() {
|
|
14
|
+
try {
|
|
15
|
+
// Must be authenticated (via session token)
|
|
16
|
+
if (!isAuthenticated()) {
|
|
17
|
+
logger.error('Not logged in');
|
|
18
|
+
logger.newline();
|
|
19
|
+
logger.info('You must be logged in to regenerate your API key');
|
|
20
|
+
logger.newline();
|
|
21
|
+
logger.info('Login using email verification:');
|
|
22
|
+
logger.log(' saac login -e <email> # Request OTP');
|
|
23
|
+
logger.log(' saac login -e <email> --otp <code> # Verify OTP');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const user = getUser();
|
|
28
|
+
|
|
29
|
+
logger.section('Regenerate API Key');
|
|
30
|
+
logger.newline();
|
|
31
|
+
|
|
32
|
+
logger.warn('This will generate a new API key and invalidate your current one.');
|
|
33
|
+
logger.warn('Your active session tokens will continue to work.');
|
|
34
|
+
logger.newline();
|
|
35
|
+
|
|
36
|
+
// Confirmation prompt
|
|
37
|
+
const { confirm } = await inquirer.prompt([
|
|
38
|
+
{
|
|
39
|
+
type: 'confirm',
|
|
40
|
+
name: 'confirm',
|
|
41
|
+
message: 'Are you sure you want to regenerate your API key?',
|
|
42
|
+
default: false,
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
if (!confirm) {
|
|
47
|
+
logger.info('Cancelled');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const spin = logger.spinner('Generating new API key...').start();
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await api.regenerateApiKey();
|
|
55
|
+
|
|
56
|
+
spin.succeed('New API key generated!');
|
|
57
|
+
|
|
58
|
+
logger.newline();
|
|
59
|
+
logger.success('Your new API key:');
|
|
60
|
+
logger.newline();
|
|
61
|
+
|
|
62
|
+
// Show FULL API key (it's only shown once)
|
|
63
|
+
logger.field('API Key', result.api_key);
|
|
64
|
+
|
|
65
|
+
logger.newline();
|
|
66
|
+
logger.warn('Save this key securely. It will not be shown again.');
|
|
67
|
+
logger.newline();
|
|
68
|
+
|
|
69
|
+
logger.info('Your existing sessions remain active.');
|
|
70
|
+
logger.info('Use this new key for future logins or CI/CD.');
|
|
71
|
+
logger.newline();
|
|
72
|
+
|
|
73
|
+
logger.info('To login with the new key:');
|
|
74
|
+
logger.log(` saac login -e ${user.email} -k ${result.api_key}`);
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
spin.fail('Failed to regenerate API key');
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logger.error(error.response?.data?.message || error.message);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Show API key info (without revealing full key)
|
|
88
|
+
*/
|
|
89
|
+
async function show() {
|
|
90
|
+
try {
|
|
91
|
+
if (!isAuthenticated()) {
|
|
92
|
+
logger.error('Not logged in');
|
|
93
|
+
logger.newline();
|
|
94
|
+
logger.info('Login first:');
|
|
95
|
+
logger.log(' saac login -e <email>');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
logger.section('API Key Information');
|
|
100
|
+
logger.newline();
|
|
101
|
+
|
|
102
|
+
const spin = logger.spinner('Fetching API key info...').start();
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = await api.getApiKeyInfo();
|
|
106
|
+
|
|
107
|
+
spin.succeed('API key info retrieved');
|
|
108
|
+
|
|
109
|
+
logger.newline();
|
|
110
|
+
logger.field('Key Prefix', result.key_prefix); // e.g., "cw_RJ1gH8..."
|
|
111
|
+
logger.field('Created', new Date(result.created_at).toLocaleDateString());
|
|
112
|
+
logger.field('Last Used', result.last_used_at
|
|
113
|
+
? new Date(result.last_used_at).toLocaleString()
|
|
114
|
+
: 'Never');
|
|
115
|
+
|
|
116
|
+
logger.newline();
|
|
117
|
+
logger.info('Commands:');
|
|
118
|
+
logger.log(' saac keys regenerate Generate new API key');
|
|
119
|
+
logger.log(' saac sessions View active sessions');
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
spin.fail('Failed to fetch API key info');
|
|
123
|
+
|
|
124
|
+
// If endpoint doesn't exist yet, show helpful message
|
|
125
|
+
if (error.response?.status === 404) {
|
|
126
|
+
logger.newline();
|
|
127
|
+
logger.warn('API key info endpoint not available yet');
|
|
128
|
+
logger.info('You can still regenerate your key with:');
|
|
129
|
+
logger.log(' saac keys regenerate');
|
|
130
|
+
} else {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
logger.error(error.response?.data?.message || error.message);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
regenerate,
|
|
142
|
+
show,
|
|
143
|
+
};
|
package/src/commands/login.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Login command
|
|
2
|
+
* Login command - Two methods:
|
|
3
|
+
* 1. With API key (fast): saac login -e email -k api_key
|
|
4
|
+
* 2. With OTP (recovery): saac login -e email, then saac login -e email --otp code
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
const inquirer = require('inquirer');
|
|
@@ -10,21 +12,23 @@ const logger = require('../lib/logger');
|
|
|
10
12
|
|
|
11
13
|
async function login(options) {
|
|
12
14
|
try {
|
|
13
|
-
// Require
|
|
14
|
-
if (!options.email
|
|
15
|
-
logger.error('Email
|
|
15
|
+
// Require email flag
|
|
16
|
+
if (!options.email) {
|
|
17
|
+
logger.error('Email is required');
|
|
16
18
|
logger.newline();
|
|
17
19
|
logger.info('Usage:');
|
|
18
|
-
logger.log(' saac login -e <email> -k <api-key>');
|
|
19
|
-
logger.log(' saac login
|
|
20
|
+
logger.log(' saac login -e <email> -k <api-key> # Login with API key');
|
|
21
|
+
logger.log(' saac login -e <email> # Request OTP via email');
|
|
22
|
+
logger.log(' saac login -e <email> --otp <code> # Verify OTP and login');
|
|
20
23
|
logger.newline();
|
|
21
|
-
logger.info('
|
|
22
|
-
logger.log(' saac login -e user@example.com -k
|
|
24
|
+
logger.info('Examples:');
|
|
25
|
+
logger.log(' saac login -e user@example.com -k cw_abc123 # Fast login');
|
|
26
|
+
logger.log(' saac login -e user@example.com # Send OTP to email');
|
|
27
|
+
logger.log(' saac login -e user@example.com --otp 123456 # Verify OTP');
|
|
23
28
|
process.exit(1);
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
const email = options.email;
|
|
27
|
-
const apiKey = options.apiKey;
|
|
28
32
|
|
|
29
33
|
// Validate email format
|
|
30
34
|
if (!validator.isEmail(email)) {
|
|
@@ -33,46 +37,143 @@ async function login(options) {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
logger.section('Login to StartAnAiCompany');
|
|
40
|
+
logger.newline();
|
|
36
41
|
|
|
37
|
-
// Login
|
|
38
|
-
|
|
42
|
+
// CASE 1: Login with API key (fast path)
|
|
43
|
+
if (options.apiKey) {
|
|
44
|
+
return await loginWithApiKey(email, options.apiKey);
|
|
45
|
+
}
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
// CASE 2: Verify OTP (second step)
|
|
48
|
+
if (options.otp) {
|
|
49
|
+
return await verifyOtpAndLogin(email, options.otp);
|
|
50
|
+
}
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
// CASE 3: Request OTP (first step)
|
|
53
|
+
return await requestOtp(email);
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
expiresAt: result.expires_at,
|
|
52
|
-
verified: result.user.verified,
|
|
53
|
-
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.error(error.response?.data?.message || error.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Login with API key (existing flow)
|
|
63
|
+
*/
|
|
64
|
+
async function loginWithApiKey(email, apiKey) {
|
|
65
|
+
const spin = logger.spinner('Logging in with API key...').start();
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = await api.login(email, apiKey);
|
|
69
|
+
|
|
70
|
+
spin.succeed('Login successful!');
|
|
71
|
+
|
|
72
|
+
// Save session token
|
|
73
|
+
saveUser({
|
|
74
|
+
email: result.user.email || email,
|
|
75
|
+
userId: result.user.id,
|
|
76
|
+
sessionToken: result.session_token,
|
|
77
|
+
expiresAt: result.expires_at,
|
|
78
|
+
verified: result.user.verified,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
logger.newline();
|
|
82
|
+
logger.success('You are now logged in!');
|
|
83
|
+
logger.newline();
|
|
84
|
+
logger.field('Email', result.user.email || email);
|
|
85
|
+
logger.field('Verified', result.user.verified ? 'Yes' : 'No');
|
|
86
|
+
|
|
87
|
+
if (result.expires_at) {
|
|
88
|
+
const expirationDate = new Date(result.expires_at);
|
|
89
|
+
logger.field('Session expires', expirationDate.toLocaleDateString());
|
|
70
90
|
}
|
|
91
|
+
|
|
71
92
|
} catch (error) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
spin.fail('Login failed');
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Request OTP via email (new flow - step 1)
|
|
100
|
+
*/
|
|
101
|
+
async function requestOtp(email) {
|
|
102
|
+
const spin = logger.spinner('Sending verification code to email...').start();
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = await api.requestLoginOtp(email);
|
|
106
|
+
|
|
107
|
+
spin.succeed('Verification code sent!');
|
|
108
|
+
|
|
109
|
+
logger.newline();
|
|
110
|
+
logger.success(`A verification code has been sent to ${logger.chalk.cyan(email)}`);
|
|
111
|
+
logger.newline();
|
|
112
|
+
logger.info('Check your email and run:');
|
|
113
|
+
logger.log(` ${logger.chalk.yellow(`saac login -e ${email} --otp <code>`)}`);
|
|
114
|
+
logger.newline();
|
|
115
|
+
logger.warn('Note: Check MailHog at https://mailhog.goryan.io for the verification code');
|
|
116
|
+
logger.newline();
|
|
117
|
+
|
|
118
|
+
if (result.otp_expires_at) {
|
|
119
|
+
const expiresAt = new Date(result.otp_expires_at);
|
|
120
|
+
const now = new Date();
|
|
121
|
+
const minutesLeft = Math.ceil((expiresAt - now) / 60000);
|
|
122
|
+
logger.info(`Code expires in ${minutesLeft} minutes`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
} catch (error) {
|
|
126
|
+
spin.fail('Failed to send verification code');
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Verify OTP and complete login (new flow - step 2)
|
|
133
|
+
*/
|
|
134
|
+
async function verifyOtpAndLogin(email, otpCode) {
|
|
135
|
+
const spin = logger.spinner('Verifying code...').start();
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const result = await api.verifyLoginOtp(email, otpCode);
|
|
139
|
+
|
|
140
|
+
spin.succeed('Verification successful!');
|
|
141
|
+
|
|
142
|
+
// Save session token
|
|
143
|
+
saveUser({
|
|
144
|
+
email: result.user.email || email,
|
|
145
|
+
userId: result.user.id,
|
|
146
|
+
sessionToken: result.session_token,
|
|
147
|
+
expiresAt: result.expires_at,
|
|
148
|
+
verified: result.user.verified,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
logger.newline();
|
|
152
|
+
logger.success('You are now logged in!');
|
|
153
|
+
logger.newline();
|
|
154
|
+
logger.field('Email', result.user.email || email);
|
|
155
|
+
logger.field('Verified', result.user.verified ? 'Yes' : 'No');
|
|
156
|
+
|
|
157
|
+
if (result.expires_at) {
|
|
158
|
+
const expirationDate = new Date(result.expires_at);
|
|
159
|
+
logger.field('Session expires', expirationDate.toLocaleDateString());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
logger.newline();
|
|
163
|
+
logger.info('💡 Tip: Generate an API key for faster future logins:');
|
|
164
|
+
logger.log(' saac keys regenerate');
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
spin.fail('Verification failed');
|
|
168
|
+
|
|
169
|
+
if (error.response?.status === 401) {
|
|
170
|
+
logger.newline();
|
|
171
|
+
logger.error('Invalid or expired verification code');
|
|
172
|
+
logger.info('Request a new code:');
|
|
173
|
+
logger.log(` saac login -e ${email}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw error;
|
|
76
177
|
}
|
|
77
178
|
}
|
|
78
179
|
|
package/src/lib/api.js
CHANGED
|
@@ -181,6 +181,61 @@ async function healthCheck() {
|
|
|
181
181
|
return response.data;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Request login OTP (no API key required)
|
|
186
|
+
*/
|
|
187
|
+
async function requestLoginOtp(email) {
|
|
188
|
+
const client = axios.create({
|
|
189
|
+
baseURL: getApiUrl(),
|
|
190
|
+
timeout: 30000,
|
|
191
|
+
headers: {
|
|
192
|
+
'Content-Type': 'application/json',
|
|
193
|
+
'User-Agent': `saac-cli/${pkg.version} (${os.platform()}; ${os.arch()})`,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const response = await client.post('/auth/login-otp', { email });
|
|
198
|
+
return response.data;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Verify login OTP and get session token
|
|
203
|
+
*/
|
|
204
|
+
async function verifyLoginOtp(email, otpCode) {
|
|
205
|
+
const client = axios.create({
|
|
206
|
+
baseURL: getApiUrl(),
|
|
207
|
+
timeout: 30000,
|
|
208
|
+
headers: {
|
|
209
|
+
'Content-Type': 'application/json',
|
|
210
|
+
'User-Agent': `saac-cli/${pkg.version} (${os.platform()}; ${os.arch()})`,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const response = await client.post('/auth/verify-otp', {
|
|
215
|
+
email,
|
|
216
|
+
otp_code: otpCode,
|
|
217
|
+
});
|
|
218
|
+
return response.data;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Regenerate API key (requires authentication)
|
|
223
|
+
*/
|
|
224
|
+
async function regenerateApiKey() {
|
|
225
|
+
const client = createClient();
|
|
226
|
+
const response = await client.post('/users/regenerate-key');
|
|
227
|
+
return response.data;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get API key info (prefix, created date, last used)
|
|
232
|
+
*/
|
|
233
|
+
async function getApiKeyInfo() {
|
|
234
|
+
const client = createClient();
|
|
235
|
+
const response = await client.get('/users/api-key');
|
|
236
|
+
return response.data;
|
|
237
|
+
}
|
|
238
|
+
|
|
184
239
|
module.exports = {
|
|
185
240
|
createClient,
|
|
186
241
|
login,
|
|
@@ -197,4 +252,8 @@ module.exports = {
|
|
|
197
252
|
updateDomain,
|
|
198
253
|
deleteApplication,
|
|
199
254
|
healthCheck,
|
|
255
|
+
requestLoginOtp,
|
|
256
|
+
verifyLoginOtp,
|
|
257
|
+
regenerateApiKey,
|
|
258
|
+
getApiKeyInfo,
|
|
200
259
|
};
|