Skip to content

Commit 7cb7cfe

Browse files
authored
fix(auth): display user info on login and status commands (#143)
- Use user info from OAuth token response instead of calling /users/me/ (which returns 403 for OAuth tokens) - Add user info display to status command via getUserInfo() - Add 'name' field to UserInfo type and database schema - Clean up excessive newlines in command output
1 parent bfc20ee commit 7cb7cfe

7 files changed

Lines changed: 264 additions & 44 deletions

File tree

src/commands/auth/login.ts

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,14 @@ import { getDbPath } from "../../lib/db/index.js";
1010
import { setUserInfo } from "../../lib/db/user.js";
1111
import { AuthError } from "../../lib/errors.js";
1212
import { muted, success } from "../../lib/formatters/colors.js";
13-
import { formatDuration } from "../../lib/formatters/human.js";
13+
import {
14+
formatDuration,
15+
formatUserIdentity,
16+
} from "../../lib/formatters/human.js";
1417
import { completeOAuthFlow, performDeviceFlow } from "../../lib/oauth.js";
1518
import { generateQRCode } from "../../lib/qrcode.js";
1619
import type { SentryUser } from "../../types/index.js";
1720

18-
/**
19-
* Format user identity for display.
20-
* Handles missing username/email gracefully.
21-
*/
22-
function formatUserIdentity(user: SentryUser): string {
23-
const { username, email, id } = user;
24-
25-
if (username && email) {
26-
return `${username} <${email}>`;
27-
}
28-
if (username) {
29-
return username;
30-
}
31-
if (email) {
32-
return email;
33-
}
34-
// Fallback to user ID if no username/email
35-
return `user ${id}`;
36-
}
37-
3821
type LoginFlags = {
3922
readonly token?: string;
4023
readonly timeout: number;
@@ -100,14 +83,15 @@ export const loginCommand = buildCommand({
10083
userId: user.id,
10184
email: user.email,
10285
username: user.username,
86+
name: user.name,
10387
});
10488
} catch (error) {
10589
// Report to Sentry but don't block auth - user info is not critical
10690
Sentry.captureException(error);
10791
}
10892

10993
stdout.write(`${success("✓")} Authenticated with API token\n`);
110-
stdout.write(` Logged in as ${muted(formatUserIdentity(user))}\n`);
94+
stdout.write(` Logged in as: ${muted(formatUserIdentity(user))}\n`);
11195
stdout.write(` Config saved to: ${getDbPath()}\n`);
11296
return;
11397
}
@@ -166,28 +150,29 @@ export const loginCommand = buildCommand({
166150
);
167151

168152
// Clear the polling dots
169-
stdout.write("\n\n");
153+
stdout.write("\n");
170154

171155
// Store the token
172156
await completeOAuthFlow(tokenResponse);
173157

174-
// Fetch and store user info for telemetry
175-
let user: SentryUser | undefined;
176-
try {
177-
user = await getCurrentUser();
178-
setUserInfo({
179-
userId: user.id,
180-
email: user.email,
181-
username: user.username,
182-
});
183-
} catch (error) {
184-
// Report to Sentry but don't block auth - user info is not critical
185-
Sentry.captureException(error);
158+
// Store user info from token response for telemetry and display
159+
const user = tokenResponse.user;
160+
if (user) {
161+
try {
162+
setUserInfo({
163+
userId: user.id,
164+
email: user.email,
165+
name: user.name,
166+
});
167+
} catch (error) {
168+
// Report to Sentry but don't block auth - user info is not critical
169+
Sentry.captureException(error);
170+
}
186171
}
187172

188173
stdout.write(`${success("✓")} Authentication successful!\n`);
189174
if (user) {
190-
stdout.write(` Logged in as ${muted(formatUserIdentity(user))}\n`);
175+
stdout.write(` Logged in as: ${muted(formatUserIdentity(user))}\n`);
191176
}
192177
stdout.write(` Config saved to: ${getDbPath()}\n`);
193178

src/commands/auth/status.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,31 @@ import {
1717
getDefaultProject,
1818
} from "../../lib/db/defaults.js";
1919
import { getDbPath } from "../../lib/db/index.js";
20+
import { getUserInfo } from "../../lib/db/user.js";
2021
import { AuthError } from "../../lib/errors.js";
21-
import { error, success } from "../../lib/formatters/colors.js";
22-
import { formatExpiration, maskToken } from "../../lib/formatters/human.js";
22+
import { error, muted, success } from "../../lib/formatters/colors.js";
23+
import {
24+
formatExpiration,
25+
formatUserIdentity,
26+
maskToken,
27+
} from "../../lib/formatters/human.js";
2328
import type { Writer } from "../../types/index.js";
2429

2530
type StatusFlags = {
2631
readonly showToken: boolean;
2732
};
2833

34+
/**
35+
* Write user identity information
36+
*/
37+
function writeUserInfo(stdout: Writer): void {
38+
const user = getUserInfo();
39+
if (!user) {
40+
return;
41+
}
42+
stdout.write(`User: ${muted(formatUserIdentity(user))}\n`);
43+
}
44+
2945
/**
3046
* Write token information
3147
*/
@@ -123,13 +139,15 @@ export const statusCommand = buildCommand({
123139
const auth = await getAuthConfig();
124140
const authenticated = await isAuthenticated();
125141

126-
stdout.write(`Config: ${getDbPath()}\n\n`);
142+
stdout.write(`Config: ${getDbPath()}\n`);
127143

128144
if (!authenticated) {
129145
throw new AuthError("not_authenticated");
130146
}
131147

132-
stdout.write(`Status: Authenticated ${success("✓")}\n\n`);
148+
stdout.write(`Status: Authenticated ${success("✓")}\n`);
149+
writeUserInfo(stdout);
150+
stdout.write("\n");
133151

134152
writeTokenInfo(stdout, auth, flags.showToken);
135153
await writeDefaults(stdout);

src/lib/db/schema.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import type { Database } from "bun:sqlite";
66

7-
const CURRENT_SCHEMA_VERSION = 2;
7+
const CURRENT_SCHEMA_VERSION = 3;
88

99
/** User identity for telemetry (single row, id=1) */
1010
const USER_INFO_TABLE = `
@@ -13,6 +13,7 @@ const USER_INFO_TABLE = `
1313
user_id TEXT NOT NULL,
1414
email TEXT,
1515
username TEXT,
16+
name TEXT,
1617
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
1718
)
1819
`;
@@ -141,6 +142,24 @@ export function runMigrations(db: Database): void {
141142
`);
142143
}
143144

145+
// Migration 2 -> 3: Add name column to user_info table
146+
// Check if column exists first to handle concurrent CLI processes
147+
// (SQLite lacks ADD COLUMN IF NOT EXISTS)
148+
if (currentVersion < 3 && currentVersion >= 2) {
149+
const hasNameColumn =
150+
(
151+
db
152+
.query(
153+
"SELECT COUNT(*) as count FROM pragma_table_info('user_info') WHERE name='name'"
154+
)
155+
.get() as { count: number }
156+
).count > 0;
157+
158+
if (!hasNameColumn) {
159+
db.exec("ALTER TABLE user_info ADD COLUMN name TEXT");
160+
}
161+
}
162+
144163
// Update schema version if needed
145164
if (currentVersion < CURRENT_SCHEMA_VERSION) {
146165
db.query("UPDATE schema_version SET version = ?").run(

src/lib/db/user.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ export type UserInfo = {
1111
userId: string;
1212
email?: string;
1313
username?: string;
14+
/** Display name (different from username) */
15+
name?: string;
1416
};
1517

1618
type UserRow = {
1719
user_id: string;
1820
email: string | null;
1921
username: string | null;
22+
name: string | null;
2023
};
2124

2225
/**
@@ -37,6 +40,7 @@ export function getUserInfo(): UserInfo | undefined {
3740
userId: row.user_id,
3841
email: row.email ?? undefined,
3942
username: row.username ?? undefined,
43+
name: row.name ?? undefined,
4044
};
4145
}
4246

@@ -56,6 +60,7 @@ export function setUserInfo(info: UserInfo): void {
5660
user_id: info.userId,
5761
email: info.email ?? null,
5862
username: info.username ?? null,
63+
name: info.name ?? null,
5964
updated_at: now,
6065
},
6166
["id"]

src/lib/formatters/human.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,6 +1472,50 @@ export function formatProjectDetails(
14721472
return lines;
14731473
}
14741474

1475+
// ─────────────────────────────────────────────────────────────────────────────
1476+
// User Identity Formatting
1477+
// ─────────────────────────────────────────────────────────────────────────────
1478+
1479+
/**
1480+
* User identity fields for display formatting.
1481+
* Accepts both UserInfo (userId) and token response user (id) shapes.
1482+
*/
1483+
type UserIdentityInput = {
1484+
/** User ID (from token response) */
1485+
id?: string;
1486+
/** User ID (from stored UserInfo) */
1487+
userId?: string;
1488+
email?: string;
1489+
username?: string;
1490+
/** Display name (different from username) */
1491+
name?: string;
1492+
};
1493+
1494+
/**
1495+
* Format user identity for display.
1496+
* Prefers name over username, handles missing fields gracefully.
1497+
*
1498+
* @param user - User identity object (supports both id and userId fields)
1499+
* @returns Formatted string like "Name <email>" or fallback to available fields
1500+
*/
1501+
export function formatUserIdentity(user: UserIdentityInput): string {
1502+
const { name, username, email, id, userId } = user;
1503+
const displayName = name ?? username;
1504+
const finalId = id ?? userId;
1505+
1506+
if (displayName && email) {
1507+
return `${displayName} <${email}>`;
1508+
}
1509+
if (displayName) {
1510+
return displayName;
1511+
}
1512+
if (email) {
1513+
return email;
1514+
}
1515+
// Fallback to user ID if no name/username/email
1516+
return `user ${finalId}`;
1517+
}
1518+
14751519
// ─────────────────────────────────────────────────────────────────────────────
14761520
// Token Formatting
14771521
// ─────────────────────────────────────────────────────────────────────────────

test/lib/db/user.test.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,36 @@ describe("getUserInfo", () => {
3636
userId: "12345",
3737
email: "test@example.com",
3838
username: "testuser",
39+
name: undefined,
3940
});
4041
});
4142

42-
test("handles missing email and username", () => {
43+
test("returns stored user info with name", () => {
44+
setUserInfo({
45+
userId: "12345",
46+
email: "test@example.com",
47+
username: "testuser",
48+
name: "Test User",
49+
});
50+
51+
const result = getUserInfo();
52+
expect(result).toEqual({
53+
userId: "12345",
54+
email: "test@example.com",
55+
username: "testuser",
56+
name: "Test User",
57+
});
58+
});
59+
60+
test("handles missing email, username, and name", () => {
4361
setUserInfo({ userId: "12345" });
4462

4563
const result = getUserInfo();
4664
expect(result).toEqual({
4765
userId: "12345",
4866
email: undefined,
4967
username: undefined,
68+
name: undefined,
5069
});
5170
});
5271
});
@@ -57,21 +76,24 @@ describe("setUserInfo", () => {
5776
userId: "user123",
5877
email: "user@test.com",
5978
username: "myuser",
79+
name: "My User",
6080
});
6181

6282
const result = getUserInfo();
6383
expect(result?.userId).toBe("user123");
6484
expect(result?.email).toBe("user@test.com");
6585
expect(result?.username).toBe("myuser");
86+
expect(result?.name).toBe("My User");
6687
});
6788

6889
test("overwrites existing user info", () => {
69-
setUserInfo({ userId: "first", email: "first@test.com" });
70-
setUserInfo({ userId: "second", email: "second@test.com" });
90+
setUserInfo({ userId: "first", email: "first@test.com", name: "First" });
91+
setUserInfo({ userId: "second", email: "second@test.com", name: "Second" });
7192

7293
const result = getUserInfo();
7394
expect(result?.userId).toBe("second");
7495
expect(result?.email).toBe("second@test.com");
96+
expect(result?.name).toBe("Second");
7597
});
7698

7799
test("stores user info with only userId", () => {
@@ -81,5 +103,20 @@ describe("setUserInfo", () => {
81103
expect(result?.userId).toBe("minimal");
82104
expect(result?.email).toBeUndefined();
83105
expect(result?.username).toBeUndefined();
106+
expect(result?.name).toBeUndefined();
107+
});
108+
109+
test("stores user info with name but no username", () => {
110+
setUserInfo({
111+
userId: "user456",
112+
email: "user@test.com",
113+
name: "Display Name",
114+
});
115+
116+
const result = getUserInfo();
117+
expect(result?.userId).toBe("user456");
118+
expect(result?.email).toBe("user@test.com");
119+
expect(result?.username).toBeUndefined();
120+
expect(result?.name).toBe("Display Name");
84121
});
85122
});

0 commit comments

Comments
 (0)