Skip to content

Commit bf6fba0

Browse files
author
Lasim
committed
feat(gateway): enhance login command to exit successfully after auth
1 parent 40e88c8 commit bf6fba0

File tree

4 files changed

+140
-28
lines changed

4 files changed

+140
-28
lines changed

services/gateway/src/commands/login.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ export function registerLoginCommand(program: Command) {
6161
console.log(chalk.yellow(` ⚠️ For MCP changes, visit: ${options.url}`));
6262
}
6363

64+
// Exit successfully
65+
process.exit(0);
66+
6467
} catch (error) {
6568
if (spinner) {
6669
spinner.fail('Authentication failed');

services/gateway/src/commands/whoami.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export function registerWhoamiCommand(program: Command) {
3737
const accounts = api.getUserAccounts();
3838

3939
// Display user information
40-
console.log(chalk.blue(`👋 You are logged in with an OAuth Token, associated with ${userInfo.email}\n`));
40+
const userEmail = userInfo.email || api.getUserEmail();
41+
console.log(chalk.blue(`👋 You are logged in with an OAuth Token, associated with ${userEmail}\n`));
4142

4243
// Display account info in table format if accounts exist
4344
if (accounts.length > 0) {

services/gateway/src/core/auth/callback-server.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export class CallbackServer {
115115
<!DOCTYPE html>
116116
<html>
117117
<head>
118+
<meta charset="UTF-8">
118119
<title>DeployStack Gateway - Authentication Successful</title>
119120
<style>
120121
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 40px; background: #f5f5f5; }
@@ -143,7 +144,7 @@ export class CallbackServer {
143144
</html>`;
144145

145146
res.writeHead(200, {
146-
'Content-Type': 'text/html',
147+
'Content-Type': 'text/html; charset=utf-8',
147148
'Content-Length': Buffer.byteLength(html)
148149
});
149150
res.end(html);
@@ -157,6 +158,7 @@ export class CallbackServer {
157158
<!DOCTYPE html>
158159
<html>
159160
<head>
161+
<meta charset="UTF-8">
160162
<title>DeployStack Gateway - Authentication Error</title>
161163
<style>
162164
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 40px; background: #f5f5f5; }
@@ -183,7 +185,7 @@ export class CallbackServer {
183185
</html>`;
184186

185187
res.writeHead(400, {
186-
'Content-Type': 'text/html',
188+
'Content-Type': 'text/html; charset=utf-8',
187189
'Content-Length': Buffer.byteLength(html)
188190
});
189191
res.end(html);
@@ -195,7 +197,7 @@ export class CallbackServer {
195197
private send404(res: ServerResponse): void {
196198
const html = '<h1>404 Not Found</h1><p>This is the DeployStack Gateway OAuth callback server.</p>';
197199
res.writeHead(404, {
198-
'Content-Type': 'text/html',
200+
'Content-Type': 'text/html; charset=utf-8',
199201
'Content-Length': Buffer.byteLength(html)
200202
});
201203
res.end(html);

services/gateway/src/core/auth/storage.ts

Lines changed: 130 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,47 @@ export class CredentialStorage {
99
private readonly serviceName = 'deploystack-gateway';
1010
private readonly fallbackDir = join(homedir(), '.deploystack');
1111
private readonly fallbackFile = join(this.fallbackDir, 'credentials.enc');
12+
private readonly accountsFile = join(this.fallbackDir, 'accounts.json');
1213
private readonly encryptionKey = 'deploystack-gateway-key';
1314

1415
/**
1516
* Store credentials securely using OS keychain with encrypted file fallback
1617
* @param credentials Credentials to store
1718
*/
1819
async storeCredentials(credentials: StoredCredentials): Promise<void> {
20+
let keychainError: Error | null = null;
21+
1922
try {
2023
// Try OS keychain first
2124
await keyring.setPassword(
2225
this.serviceName,
2326
credentials.userEmail,
2427
JSON.stringify(credentials)
2528
);
29+
30+
// Also maintain a list of accounts for retrieval
31+
await this.addToAccountsList(credentials.userEmail);
32+
33+
console.log('✓ Credentials stored in OS keychain');
34+
return; // Success, no need for fallback
2635
} catch (error) {
27-
// Fallback to encrypted file storage
28-
try {
29-
await this.storeEncrypted(credentials);
30-
} catch (fallbackError) {
31-
throw new AuthenticationError(
32-
AuthError.STORAGE_ERROR,
33-
'Failed to store credentials securely',
34-
fallbackError as Error
35-
);
36-
}
36+
keychainError = error as Error;
37+
console.log('⚠ Keychain storage failed, trying encrypted file fallback...');
38+
}
39+
40+
// Fallback to encrypted file storage
41+
try {
42+
await this.storeEncrypted(credentials);
43+
console.log('✓ Credentials stored in encrypted file');
44+
} catch (fallbackError) {
45+
console.error('❌ Both keychain and file storage failed:');
46+
console.error('Keychain error:', keychainError?.message);
47+
console.error('File error:', (fallbackError as Error)?.message);
48+
throw new AuthenticationError(
49+
AuthError.STORAGE_ERROR,
50+
'Failed to store credentials securely',
51+
fallbackError as Error
52+
);
3753
}
3854
}
3955

@@ -42,21 +58,52 @@ export class CredentialStorage {
4258
* @returns Stored credentials or null if not found
4359
*/
4460
async getCredentials(): Promise<StoredCredentials | null> {
61+
console.log('🔍 Attempting to retrieve stored credentials...');
62+
63+
// First try encrypted file (more reliable for single-user scenario)
4564
try {
46-
// Try to get all stored credentials and find the most recent one
47-
const accounts = await this.getStoredAccounts();
48-
if (accounts.length === 0) {
49-
return await this.retrieveEncrypted();
65+
console.log('📁 Checking encrypted file:', this.fallbackFile);
66+
const encryptedCredentials = await this.retrieveEncrypted();
67+
if (encryptedCredentials) {
68+
console.log('✓ Found credentials in encrypted file');
69+
return encryptedCredentials;
70+
} else {
71+
console.log('⚠ No credentials found in encrypted file');
5072
}
73+
} catch (error) {
74+
console.log('❌ Error reading encrypted file:', (error as Error)?.message);
75+
}
5176

52-
// Get the most recently stored credentials
53-
const mostRecent = accounts[0];
54-
const stored = await keyring.getPassword(this.serviceName, mostRecent);
55-
return stored ? JSON.parse(stored) : await this.retrieveEncrypted();
77+
// Fallback to keychain
78+
try {
79+
console.log('🔑 Checking OS keychain...');
80+
const accounts = await this.getStoredAccounts();
81+
console.log('📋 Found accounts:', accounts);
82+
83+
if (accounts.length > 0) {
84+
// Try each account until we find valid credentials
85+
for (const account of accounts) {
86+
try {
87+
const stored = await keyring.getPassword(this.serviceName, account);
88+
if (stored) {
89+
const credentials = JSON.parse(stored);
90+
console.log('✓ Found credentials in OS keychain for:', account);
91+
return credentials;
92+
}
93+
} catch (error) {
94+
console.log('⚠ Failed to retrieve credentials for account:', account);
95+
continue;
96+
}
97+
}
98+
} else {
99+
console.log('⚠ No accounts found in keychain');
100+
}
56101
} catch (error) {
57-
// Fallback to encrypted file
58-
return await this.retrieveEncrypted();
102+
console.log('❌ Error accessing keychain:', (error as Error)?.message);
59103
}
104+
105+
console.log('❌ No credentials found in any storage method');
106+
return null;
60107
}
61108

62109
/**
@@ -67,6 +114,7 @@ export class CredentialStorage {
67114
try {
68115
if (userEmail) {
69116
await keyring.deletePassword(this.serviceName, userEmail);
117+
await this.removeFromAccountsList(userEmail);
70118
} else {
71119
// Clear all stored accounts
72120
const accounts = await this.getStoredAccounts();
@@ -77,6 +125,7 @@ export class CredentialStorage {
77125
// Continue clearing other accounts even if one fails
78126
}
79127
}
128+
await this.clearAccountsList();
80129
}
81130
} catch (error) {
82131
// Continue to clear encrypted file even if keychain fails
@@ -114,11 +163,68 @@ export class CredentialStorage {
114163
*/
115164
private async getStoredAccounts(): Promise<string[]> {
116165
try {
117-
// This is a simplified approach - in a real implementation,
118-
// you might want to maintain a separate list of accounts
119-
return [];
166+
if (existsSync(this.accountsFile)) {
167+
const data = readFileSync(this.accountsFile, 'utf8');
168+
const accounts = JSON.parse(data);
169+
return Array.isArray(accounts) ? accounts : [];
170+
}
171+
} catch (error) {
172+
// If we can't read the accounts file, return empty array
173+
}
174+
return [];
175+
}
176+
177+
/**
178+
* Add account to the accounts list
179+
* @param email User email to add
180+
*/
181+
private async addToAccountsList(email: string): Promise<void> {
182+
try {
183+
// Ensure directory exists
184+
const { mkdirSync } = await import('fs');
185+
try {
186+
mkdirSync(this.fallbackDir, { recursive: true });
187+
} catch (error) {
188+
// Directory might already exist
189+
}
190+
191+
const accounts = await this.getStoredAccounts();
192+
if (!accounts.includes(email)) {
193+
accounts.unshift(email); // Add to beginning (most recent first)
194+
writeFileSync(this.accountsFile, JSON.stringify(accounts, null, 2));
195+
}
196+
} catch (error) {
197+
// Non-critical error, don't throw
198+
console.log('⚠ Failed to update accounts list:', (error as Error)?.message);
199+
}
200+
}
201+
202+
/**
203+
* Remove account from the accounts list
204+
* @param email User email to remove
205+
*/
206+
private async removeFromAccountsList(email: string): Promise<void> {
207+
try {
208+
const accounts = await this.getStoredAccounts();
209+
const filtered = accounts.filter(account => account !== email);
210+
if (filtered.length !== accounts.length) {
211+
writeFileSync(this.accountsFile, JSON.stringify(filtered, null, 2));
212+
}
213+
} catch (error) {
214+
// Non-critical error, don't throw
215+
}
216+
}
217+
218+
/**
219+
* Clear the accounts list
220+
*/
221+
private async clearAccountsList(): Promise<void> {
222+
try {
223+
if (existsSync(this.accountsFile)) {
224+
unlinkSync(this.accountsFile);
225+
}
120226
} catch (error) {
121-
return [];
227+
// Non-critical error, don't throw
122228
}
123229
}
124230

0 commit comments

Comments
 (0)