Skip to content

Commit 98b54fb

Browse files
author
Lasim
committed
feat(gateway): add 'whoami' and 'teams' commands for user information and team management
- Implemented `registerWhoamiCommand` to display current user information, including email, accounts, and token permissions. - Implemented `registerTeamsCommand` to list user teams and their details, including roles and member counts. - Enhanced API client with methods to fetch user info and teams. - Added a callback server for handling OAuth2 authentication flow. - Introduced a browser manager for opening authentication URLs. - Updated authentication error handling and user feedback messages. - Added utility functions for PKCE (Proof Key for Code Exchange) to enhance security during OAuth2 flow. - Updated type definitions and authentication configuration for better clarity and maintainability.
1 parent 814e31b commit 98b54fb

File tree

19 files changed

+1990
-33
lines changed

19 files changed

+1990
-33
lines changed

package-lock.json

Lines changed: 401 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/gateway/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@
2424
"license": "SEE LICENSE IN ../../LICENSE",
2525
"dependencies": {
2626
"commander": "^12.0.0",
27-
"chalk": "^4.1.2"
27+
"chalk": "^4.1.2",
28+
"@zowe/secrets-for-zowe-sdk": "^8.0.0",
29+
"open": "^8.4.0",
30+
"ora": "^5.4.1",
31+
"inquirer": "^8.2.4",
32+
"node-fetch": "^3.3.0",
33+
"cli-table3": "^0.6.3"
2834
},
2935
"devDependencies": {
3036
"@types/node": "^24.1.0",

services/gateway/src/commands/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export { registerLoginCommand } from './login';
22
export { registerLogoutCommand } from './logout';
3+
export { registerWhoamiCommand } from './whoami';
4+
export { registerTeamsCommand } from './teams';
35
export { registerStartCommand } from './start';
46
export { registerStopCommand } from './stop';
57
export { registerStatusCommand } from './status';
Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,100 @@
11
import { Command } from 'commander';
22
import chalk from 'chalk';
3+
import ora from 'ora';
4+
import { OAuth2Client } from '../core/auth/oauth';
5+
import { CredentialStorage } from '../core/auth/storage';
6+
import { AuthenticationError } from '../types/auth';
37

48
export function registerLoginCommand(program: Command) {
59
program
610
.command('login')
711
.description('Authenticate with DeployStack cloud')
8-
.action(async () => {
9-
console.log(chalk.green('hello world from login'));
12+
.option('--no-browser', 'Skip automatic browser opening')
13+
.option('--url <url>', 'DeployStack backend URL (for development)', 'https://cloud.deploystack.io')
14+
.action(async (options) => {
15+
const storage = new CredentialStorage();
16+
const oauth = new OAuth2Client({
17+
baseUrl: options.url
18+
});
19+
20+
let spinner: ReturnType<typeof ora> | null = null;
21+
22+
try {
23+
// Check if already authenticated
24+
if (await storage.isAuthenticated()) {
25+
const credentials = await storage.getCredentials();
26+
console.log(chalk.green(`✅ You are already logged in as ${credentials?.userEmail}`));
27+
console.log(chalk.gray(`💡 Use 'deploystack whoami' to see details`));
28+
return;
29+
}
30+
31+
console.log(chalk.blue('🔐 Starting authentication flow...'));
32+
33+
if (options.url !== 'https://cloud.deploystack.io') {
34+
console.log(chalk.yellow(`🔧 Using development server: ${options.url}`));
35+
}
36+
37+
// Start OAuth flow
38+
const authResult = await oauth.authenticate({
39+
openBrowser: options.browser !== false,
40+
timeout: 120000 // 2 minutes
41+
});
42+
43+
spinner = ora('Storing credentials securely...').start();
44+
45+
// Store credentials
46+
await storage.storeCredentials(authResult.credentials);
47+
48+
spinner.succeed('Credentials stored securely');
49+
spinner = null;
50+
51+
console.log(chalk.green(`✅ Successfully authenticated as ${authResult.credentials.userEmail}`));
52+
console.log(chalk.green(`🎉 You can now use the DeployStack Gateway CLI`));
53+
54+
// Show available commands
55+
console.log(chalk.blue(`\n💡 Available commands:`));
56+
console.log(chalk.gray(` deploystack whoami - Show your user information`));
57+
console.log(chalk.gray(` deploystack teams - List your teams`));
58+
console.log(chalk.gray(` deploystack start - Start the gateway server`));
59+
60+
if (options.url !== 'https://cloud.deploystack.io') {
61+
console.log(chalk.yellow(` ⚠️ For MCP changes, visit: ${options.url}`));
62+
}
63+
64+
} catch (error) {
65+
if (spinner) {
66+
spinner.fail('Authentication failed');
67+
}
68+
69+
if (error instanceof AuthenticationError) {
70+
console.log(chalk.red(`❌ Authentication failed: ${error.message}`));
71+
72+
// Provide helpful suggestions based on error type
73+
switch (error.code) {
74+
case 'TIMEOUT':
75+
console.log(chalk.yellow('💡 Try running the command again, or use --no-browser to open the URL manually'));
76+
break;
77+
case 'ACCESS_DENIED':
78+
console.log(chalk.yellow('💡 Make sure you approve the authorization request in your browser'));
79+
break;
80+
case 'BROWSER_ERROR':
81+
console.log(chalk.yellow('💡 Try using --no-browser to open the URL manually'));
82+
break;
83+
case 'NETWORK_ERROR':
84+
console.log(chalk.yellow('💡 Check your internet connection and try again'));
85+
if (options.url !== 'https://cloud.deploystack.io') {
86+
console.log(chalk.yellow(`💡 Make sure your development server is running at ${options.url}`));
87+
}
88+
break;
89+
case 'STORAGE_ERROR':
90+
console.log(chalk.yellow('💡 Check that you have permission to access the system keychain'));
91+
break;
92+
}
93+
} else {
94+
console.log(chalk.red(`❌ Unexpected error: ${error instanceof Error ? error.message : String(error)}`));
95+
}
96+
97+
process.exit(1);
98+
}
1099
});
11100
}
Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,68 @@
11
import { Command } from 'commander';
22
import chalk from 'chalk';
3+
import ora from 'ora';
4+
import { CredentialStorage } from '../core/auth/storage';
5+
import { AuthenticationError } from '../types/auth';
36

47
export function registerLogoutCommand(program: Command) {
58
program
69
.command('logout')
7-
.description('Clear local credentials')
8-
.action(async () => {
9-
console.log(chalk.yellow('hello world from logout'));
10+
.description('Clear stored authentication credentials')
11+
.option('--all', 'Clear credentials for all users')
12+
.action(async (options) => {
13+
const storage = new CredentialStorage();
14+
let spinner: ReturnType<typeof ora> | null = null;
15+
16+
try {
17+
// Check if user is currently authenticated
18+
const isAuthenticated = await storage.isAuthenticated();
19+
const credentials = await storage.getCredentials();
20+
21+
if (!isAuthenticated && !credentials) {
22+
console.log(chalk.yellow('⚠️ You are not currently logged in'));
23+
console.log(chalk.gray(`💡 Use 'deploystack login' to authenticate`));
24+
return;
25+
}
26+
27+
const userEmail = credentials?.userEmail || 'unknown user';
28+
29+
if (options.all) {
30+
console.log(chalk.blue('🔐 Clearing all stored credentials...'));
31+
spinner = ora('Clearing credentials from secure storage...').start();
32+
33+
await storage.clearCredentials();
34+
35+
spinner.succeed('All credentials cleared');
36+
console.log(chalk.green('✅ Successfully logged out all users'));
37+
} else {
38+
console.log(chalk.blue(`🔐 Logging out ${userEmail}...`));
39+
spinner = ora('Clearing credentials from secure storage...').start();
40+
41+
await storage.clearCredentials(userEmail);
42+
43+
spinner.succeed('Credentials cleared');
44+
console.log(chalk.green(`✅ Successfully logged out ${userEmail}`));
45+
}
46+
47+
console.log(chalk.gray(`💡 Use 'deploystack login' to authenticate again`));
48+
49+
} catch (error) {
50+
if (spinner) {
51+
spinner.fail('Failed to clear credentials');
52+
}
53+
54+
if (error instanceof AuthenticationError) {
55+
console.log(chalk.red(`❌ Logout failed: ${error.message}`));
56+
57+
if (error.code === 'STORAGE_ERROR') {
58+
console.log(chalk.yellow('💡 There may be an issue accessing the system keychain'));
59+
console.log(chalk.yellow('💡 You may need to manually clear credentials from your system keychain'));
60+
}
61+
} else {
62+
console.log(chalk.red(`❌ Unexpected error during logout: ${error instanceof Error ? error.message : String(error)}`));
63+
}
64+
65+
process.exit(1);
66+
}
1067
});
1168
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { Command } from 'commander';
2+
import chalk from 'chalk';
3+
import { CredentialStorage } from '../core/auth/storage';
4+
import { DeployStackAPI } from '../core/auth/api-client';
5+
import { TableFormatter } from '../utils/table';
6+
import { AuthenticationError } from '../types/auth';
7+
8+
export function registerTeamsCommand(program: Command) {
9+
program
10+
.command('teams')
11+
.description('List your teams and team information')
12+
.option('--url <url>', 'DeployStack backend URL (for development)', 'https://cloud.deploystack.io')
13+
.action(async (options) => {
14+
const storage = new CredentialStorage();
15+
16+
try {
17+
// Check authentication
18+
if (!await storage.isAuthenticated()) {
19+
console.log(chalk.red('❌ Not authenticated'));
20+
console.log(chalk.gray(`💡 Run 'deploystack login' to authenticate`));
21+
process.exit(1);
22+
}
23+
24+
const credentials = await storage.getCredentials();
25+
if (!credentials) {
26+
console.log(chalk.red('❌ No stored credentials found'));
27+
console.log(chalk.gray(`💡 Run 'deploystack login' to authenticate`));
28+
process.exit(1);
29+
}
30+
31+
const api = new DeployStackAPI(credentials, options.url);
32+
33+
// Get teams
34+
const teams = await api.getUserTeams();
35+
36+
if (teams.length === 0) {
37+
console.log(chalk.yellow('📭 You are not a member of any teams'));
38+
console.log(chalk.gray('💡 Contact your administrator to be added to a team'));
39+
return;
40+
}
41+
42+
console.log(chalk.blue(`👥 Your Teams (${teams.length} team${teams.length === 1 ? '' : 's'} found)\n`));
43+
44+
// Create table
45+
const table = TableFormatter.createTable({
46+
head: ['Team Name', 'Role', 'Members', 'Default', 'Created'],
47+
colWidths: [25, 15, 8, 7, 12]
48+
});
49+
50+
teams.forEach(team => {
51+
table.push([
52+
TableFormatter.truncate(team.name, 23),
53+
team.role === 'team_admin' ? chalk.cyan('admin') : chalk.gray('user'),
54+
team.member_count.toString(),
55+
TableFormatter.formatBoolean(team.is_default),
56+
TableFormatter.formatDate(team.created_at)
57+
]);
58+
});
59+
60+
console.log(table.toString());
61+
62+
// Show helpful tips
63+
console.log();
64+
console.log(chalk.gray(`💡 Use 'deploystack start --team <team-name>' to start gateway for specific team`));
65+
66+
// Show default team info
67+
const defaultTeam = teams.find(team => team.is_default);
68+
if (defaultTeam) {
69+
console.log(chalk.gray(`💡 Your default team is: ${chalk.cyan(defaultTeam.name)}`));
70+
}
71+
72+
// Show admin teams
73+
const adminTeams = teams.filter(team => team.role === 'team_admin');
74+
if (adminTeams.length > 0) {
75+
console.log(chalk.gray(`💡 You have admin access to ${adminTeams.length} team${adminTeams.length === 1 ? '' : 's'}`));
76+
}
77+
78+
} catch (error) {
79+
if (error instanceof AuthenticationError) {
80+
console.log(chalk.red(`❌ Failed to get teams: ${error.message}`));
81+
82+
if (error.code === 'TOKEN_EXPIRED') {
83+
console.log(chalk.gray(`💡 Run 'deploystack login' to refresh your authentication`));
84+
} else if (error.code === 'NETWORK_ERROR') {
85+
console.log(chalk.gray('💡 Check your internet connection and try again'));
86+
if (options.url !== 'https://cloud.deploystack.io') {
87+
console.log(chalk.gray(`💡 Make sure your development server is running at ${options.url}`));
88+
}
89+
} else if (error.code === 'INVALID_TOKEN') {
90+
console.log(chalk.gray('💡 Your token may not have permission to view teams'));
91+
console.log(chalk.gray(`💡 Run 'deploystack login' to refresh your authentication`));
92+
}
93+
} else {
94+
console.log(chalk.red(`❌ Unexpected error: ${error instanceof Error ? error.message : String(error)}`));
95+
}
96+
97+
process.exit(1);
98+
}
99+
});
100+
}

0 commit comments

Comments
 (0)