Skip to content

Commit c1a8082

Browse files
author
Lasim
committed
feat(gateway): enhance login and teams commands with team selection
1 parent 237b590 commit c1a8082

File tree

6 files changed

+162
-26
lines changed

6 files changed

+162
-26
lines changed

services/gateway/src/commands/login.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import chalk from 'chalk';
33
import ora from 'ora';
44
import { OAuth2Client } from '../core/auth/oauth';
55
import { CredentialStorage } from '../core/auth/storage';
6+
import { DeployStackAPI } from '../core/auth/api-client';
67
import { AuthenticationError } from '../types/auth';
78

89
export function registerLoginCommand(program: Command) {
@@ -45,7 +46,24 @@ export function registerLoginCommand(program: Command) {
4546
// Store credentials
4647
await storage.storeCredentials(authResult.credentials);
4748

48-
spinner.succeed('Credentials stored securely');
49+
// Set default team as selected team
50+
spinner.text = 'Setting up default team...';
51+
try {
52+
const api = new DeployStackAPI(authResult.credentials, options.url);
53+
const teams = await api.getUserTeams();
54+
const defaultTeam = teams.find(team => team.is_default);
55+
56+
if (defaultTeam) {
57+
await storage.updateSelectedTeam(defaultTeam.id, defaultTeam.name);
58+
spinner.succeed('Credentials stored and default team selected');
59+
} else {
60+
spinner.succeed('Credentials stored securely');
61+
console.log(chalk.yellow('⚠️ No default team found - you may need to select a team manually'));
62+
}
63+
} catch (teamError) {
64+
spinner.succeed('Credentials stored securely');
65+
console.log(chalk.yellow('⚠️ Could not auto-select default team - you can select one later'));
66+
}
4967
spinner = null;
5068

5169
console.log(chalk.green(`✅ Successfully authenticated as ${authResult.credentials.userEmail}`));

services/gateway/src/commands/teams.ts

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export function registerTeamsCommand(program: Command) {
1010
.command('teams')
1111
.description('List your teams and team information')
1212
.option('--url <url>', 'DeployStack backend URL (override stored URL)')
13+
.option('--switch <team-name>', 'Switch to a different team')
1314
.action(async (options) => {
1415
const storage = new CredentialStorage();
1516
let backendUrl = 'https://cloud.deploystack.io'; // Default fallback
@@ -33,49 +34,112 @@ export function registerTeamsCommand(program: Command) {
3334
backendUrl = options.url || credentials.baseUrl || 'https://cloud.deploystack.io';
3435
const api = new DeployStackAPI(credentials, backendUrl);
3536

36-
// Get teams
37+
// Get fresh teams data from the API (real-time verification)
3738
const teams = await api.getUserTeams();
39+
40+
// Handle team switching
41+
if (options.switch) {
42+
const teamToSwitch = teams.find(team =>
43+
team.name.toLowerCase() === options.switch.toLowerCase() ||
44+
team.slug.toLowerCase() === options.switch.toLowerCase()
45+
);
46+
47+
if (!teamToSwitch) {
48+
console.log(chalk.red(`❌ Team "${options.switch}" not found`));
49+
console.log(chalk.gray('Available teams:'));
50+
teams.forEach(team => console.log(chalk.gray(` - ${team.name}`)));
51+
process.exit(1);
52+
}
53+
54+
await storage.updateSelectedTeam(teamToSwitch.id, teamToSwitch.name);
55+
console.log(chalk.green(`✅ Switched to team: ${chalk.cyan(teamToSwitch.name)}`));
56+
console.log(chalk.gray(`🌐 Using backend: ${backendUrl}`));
57+
return;
58+
}
3859

3960
if (teams.length === 0) {
4061
console.log(chalk.yellow('📭 You are not a member of any teams'));
4162
console.log(chalk.gray('💡 Contact your administrator to be added to a team'));
63+
console.log(chalk.gray(`🌐 Using backend: ${backendUrl}`));
4264
return;
4365
}
4466

45-
console.log(chalk.blue(`👥 Your Teams (${teams.length} team${teams.length === 1 ? '' : 's'} found)\n`));
67+
console.log(chalk.blue(`👥 Your Teams (${teams.length} team${teams.length === 1 ? '' : 's'} found)`));
68+
69+
// Show currently selected team
70+
const selectedTeam = await storage.getSelectedTeam();
71+
if (selectedTeam) {
72+
console.log(chalk.gray(`🎯 Currently selected: ${chalk.cyan(selectedTeam.name)}`));
73+
} else {
74+
console.log(chalk.yellow('⚠️ No team selected - use --switch <team-name> to select one'));
75+
}
76+
77+
console.log(chalk.gray(`🌐 Using backend: ${backendUrl}\n`));
4678

4779
// Create table
4880
const table = TableFormatter.createTable({
49-
head: ['Team Name', 'Role', 'Members', 'Default', 'Created'],
50-
colWidths: [25, 15, 8, 7, 12]
81+
head: ['Team Name', 'Role', 'Ownership', 'Default', 'Selected'],
82+
colWidths: [25, 18, 15, 10, 10]
5183
});
5284

5385
teams.forEach(team => {
86+
// Format role with colors and descriptions
87+
let roleDisplay: string;
88+
if (team.role === 'team_admin') {
89+
roleDisplay = team.is_owner ? chalk.green('Owner/Admin') : chalk.cyan('Admin');
90+
} else {
91+
roleDisplay = chalk.gray('User');
92+
}
93+
94+
// Format ownership status
95+
const ownershipDisplay = team.is_owner ? chalk.green('✅ Owner') : chalk.gray('👤 Member');
96+
97+
// Format default team status
98+
const defaultDisplay = team.is_default ? chalk.yellow('✅ Default') : chalk.gray('Regular');
99+
100+
// Format selected team status
101+
const isSelected = selectedTeam && selectedTeam.id === team.id;
102+
const selectedDisplay = isSelected ? chalk.green('✅ Active') : chalk.gray('Inactive');
103+
54104
table.push([
55105
TableFormatter.truncate(team.name, 23),
56-
team.role === 'team_admin' ? chalk.cyan('admin') : chalk.gray('user'),
57-
team.member_count.toString(),
58-
TableFormatter.formatBoolean(team.is_default),
59-
TableFormatter.formatDate(team.created_at)
106+
roleDisplay,
107+
ownershipDisplay,
108+
defaultDisplay,
109+
selectedDisplay
60110
]);
61111
});
62112

63113
console.log(table.toString());
64114

65115
// Show helpful tips
66116
console.log();
67-
console.log(chalk.gray(`💡 Use 'deploystack start --team <team-name>' to start gateway for specific team`));
117+
console.log(chalk.gray(`💡 Use 'deploystack teams --switch <team-name>' to switch to a different team`));
68118

69-
// Show default team info
119+
// Show ownership summary
120+
const ownedTeams = teams.filter(team => team.is_owner);
121+
const adminTeams = teams.filter(team => team.role === 'team_admin' && !team.is_owner);
122+
const userTeams = teams.filter(team => team.role === 'team_user');
70123
const defaultTeam = teams.find(team => team.is_default);
124+
125+
if (ownedTeams.length > 0) {
126+
console.log(chalk.gray(`💡 You own ${ownedTeams.length} team${ownedTeams.length === 1 ? '' : 's'}: ${ownedTeams.map(t => chalk.cyan(t.name)).join(', ')}`));
127+
}
128+
if (adminTeams.length > 0) {
129+
console.log(chalk.gray(`💡 You have admin access to ${adminTeams.length} additional team${adminTeams.length === 1 ? '' : 's'}`));
130+
}
131+
if (userTeams.length > 0) {
132+
console.log(chalk.gray(`💡 You have user access to ${userTeams.length} team${userTeams.length === 1 ? '' : 's'}`));
133+
}
71134
if (defaultTeam) {
72135
console.log(chalk.gray(`💡 Your default team is: ${chalk.cyan(defaultTeam.name)}`));
73136
}
74-
75-
// Show admin teams
76-
const adminTeams = teams.filter(team => team.role === 'team_admin');
77-
if (adminTeams.length > 0) {
78-
console.log(chalk.gray(`💡 You have admin access to ${adminTeams.length} team${adminTeams.length === 1 ? '' : 's'}`));
137+
138+
// Show member count if available
139+
const teamsWithMemberCount = teams.filter(team => team.member_count !== undefined);
140+
if (teamsWithMemberCount.length > 0) {
141+
const totalMembers = teamsWithMemberCount.reduce((sum, team) => sum + (team.member_count || 0), 0);
142+
console.log(chalk.gray(`📊 Total team members across all teams: ${totalMembers}`));
79143
}
80144

81145
} catch (error) {

services/gateway/src/core/auth/api-client.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,27 @@ export class DeployStackAPI {
3939
}
4040

4141
/**
42-
* Get user's teams
43-
* @returns Array of teams
42+
* Get user's teams from the backend API
43+
* Makes a real-time API call to /api/teams/me to fetch current team memberships
44+
* @returns Array of teams with user's role and ownership status
4445
*/
4546
async getUserTeams(): Promise<Team[]> {
4647
const config = buildAuthConfig(this.baseUrl);
47-
const response = await this.makeRequest(config.teamsUrl) as TeamsResponse;
48-
return response.teams;
48+
const response = await this.makeRequest(config.teamsUrl);
49+
50+
// Check if response has the expected structure
51+
if (!response) {
52+
return [];
53+
}
54+
55+
// The API returns teams in response.data, not response.teams
56+
const teams = response.data || response.teams || [];
57+
58+
if (!Array.isArray(teams)) {
59+
return [];
60+
}
61+
62+
return teams;
4963
}
5064

5165
/**

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,37 @@ export class CredentialStorage {
106106
return null;
107107
}
108108

109+
/**
110+
* Update the selected team in stored credentials
111+
* @param teamId Team ID to select
112+
* @param teamName Team name to select
113+
*/
114+
async updateSelectedTeam(teamId: string, teamName: string): Promise<void> {
115+
const credentials = await this.getCredentials();
116+
if (!credentials) {
117+
throw new AuthenticationError(
118+
AuthError.STORAGE_ERROR,
119+
'No stored credentials found to update'
120+
);
121+
}
122+
123+
credentials.selectedTeam = {
124+
id: teamId,
125+
name: teamName
126+
};
127+
128+
await this.storeCredentials(credentials);
129+
}
130+
131+
/**
132+
* Get the currently selected team
133+
* @returns Selected team info or null if none selected
134+
*/
135+
async getSelectedTeam(): Promise<{ id: string; name: string } | null> {
136+
const credentials = await this.getCredentials();
137+
return credentials?.selectedTeam || null;
138+
}
139+
109140
/**
110141
* Clear stored credentials
111142
* @param userEmail Optional specific user email to clear

services/gateway/src/types/auth.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ export interface StoredCredentials {
88
id: string;
99
name: string;
1010
}>;
11+
// Selected team information
12+
selectedTeam?: {
13+
id: string;
14+
name: string;
15+
};
1116
}
1217

1318
export interface UserInfo {
@@ -38,12 +43,16 @@ export interface Team {
3843
id: string;
3944
name: string;
4045
slug: string;
41-
description?: string;
42-
is_default: boolean;
43-
role: 'team_admin' | 'team_user';
44-
member_count: number;
46+
description?: string | null;
47+
owner_id: string;
4548
created_at: string;
4649
updated_at: string;
50+
role: 'team_admin' | 'team_user';
51+
is_owner: boolean;
52+
is_admin: boolean;
53+
// Legacy fields that might still be used
54+
is_default?: boolean;
55+
member_count?: number;
4756
}
4857

4958
export interface TeamsResponse {

services/gateway/src/utils/auth-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const DEFAULT_AUTH_CONFIG: AuthConfig = {
1919
authUrl: 'https://cloud.deploystack.io/api/oauth2/auth',
2020
tokenUrl: 'https://cloud.deploystack.io/api/oauth2/token',
2121
userInfoUrl: 'https://cloud.deploystack.io/api/oauth2/userinfo',
22-
teamsUrl: 'https://cloud.deploystack.io/api/teams',
22+
teamsUrl: 'https://cloud.deploystack.io/api/teams/me',
2323
redirectUri: 'http://localhost:8976/oauth/callback',
2424
scopes: [
2525
'mcp:read',
@@ -43,7 +43,7 @@ export function buildAuthConfig(baseUrl: string): AuthConfig {
4343
authUrl: `${baseUrl}/api/oauth2/auth`,
4444
tokenUrl: `${baseUrl}/api/oauth2/token`,
4545
userInfoUrl: `${baseUrl}/api/oauth2/userinfo`,
46-
teamsUrl: `${baseUrl}/api/teams`
46+
teamsUrl: `${baseUrl}/api/teams/me`
4747
};
4848
}
4949

0 commit comments

Comments
 (0)