Skip to content

Commit cbbf29b

Browse files
authored
Merge pull request #2 from devhelmhq/feat/auth-whoami
feat: fix auth login validation and add auth whoami command
2 parents ab21c23 + 6806779 commit cbbf29b

2 files changed

Lines changed: 86 additions & 5 deletions

File tree

src/commands/auth/login.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as readline from 'node:readline'
66

77
export default class AuthLogin extends Command {
88
static description = 'Authenticate with the DevHelm API'
9-
static examples = ['<%= config.bin %> auth login', '<%= config.bin %> auth login --token sk_live_...']
9+
static examples = ['<%= config.bin %> auth login', '<%= config.bin %> auth login --token dh_live_...']
1010
static flags = {
1111
...globalFlags,
1212
token: Flags.string({description: 'API token (skips interactive prompt)'}),
@@ -23,14 +23,35 @@ export default class AuthLogin extends Command {
2323
const apiUrl = flags['api-url'] || resolveApiUrl()
2424
this.log('Validating token...')
2525
const client = createApiClient({baseUrl: apiUrl, token})
26+
27+
// Try /api/v1/auth/me first (API key — returns rich identity info).
28+
// Falls back to /api/v1/dashboard/overview for non-API-key tokens (dev tokens, JWTs).
2629
try {
2730
// eslint-disable-next-line @typescript-eslint/no-explicit-any
28-
const me = await checkedFetch(client.GET('/platform/me' as any, {} as any))
31+
const resp = await checkedFetch(client.GET('/api/v1/auth/me' as any, {} as any))
32+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
33+
const me = (resp as any)?.data ?? resp
34+
35+
saveContext({name: flags.name, apiUrl, token}, true)
36+
this.log('')
37+
this.log(` Authenticated successfully.`)
38+
this.log(` Organization: ${me.organization?.name ?? 'unknown'} (ID: ${me.organization?.id ?? '?'})`)
39+
this.log(` Key: ${me.key?.name ?? 'unknown'}`)
40+
this.log(` Plan: ${me.plan?.tier ?? 'unknown'}`)
41+
this.log('')
42+
this.log(` Context '${flags.name}' saved to ~/.devhelm/contexts.json`)
43+
return
44+
} catch {
45+
// /auth/me failed — might be a non-API-key token; try basic validation
46+
}
47+
48+
try {
2949
// eslint-disable-next-line @typescript-eslint/no-explicit-any
30-
const email = (me as any)?.data?.email ?? (me as any)?.email
50+
await checkedFetch(client.GET('/api/v1/dashboard/overview' as any, {} as any))
3151
saveContext({name: flags.name, apiUrl, token}, true)
32-
this.log(`\nAuthenticated as ${email}`)
33-
this.log(`Context '${flags.name}' saved to ~/.devhelm/contexts.json`)
52+
this.log('')
53+
this.log(` Authenticated successfully.`)
54+
this.log(` Context '${flags.name}' saved to ~/.devhelm/contexts.json`)
3455
} catch {
3556
this.error('Invalid token. Authentication failed.', {exit: 2})
3657
}

src/commands/auth/me.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {Command} from '@oclif/core'
2+
import {globalFlags, buildClient} from '../../lib/base-command.js'
3+
import {checkedFetch} from '../../lib/api-client.js'
4+
import {formatOutput, OutputFormat} from '../../lib/output.js'
5+
6+
export default class AuthMe extends Command {
7+
static description = 'Show current API key identity, organization, plan, and rate limits'
8+
static examples = ['<%= config.bin %> auth me', '<%= config.bin %> auth me --output json']
9+
static flags = {...globalFlags}
10+
11+
async run() {
12+
const {flags} = await this.parse(AuthMe)
13+
const client = buildClient(flags)
14+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
15+
const resp = await checkedFetch(client.GET('/api/v1/auth/me' as any, {} as any))
16+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17+
const me = (resp as any)?.data ?? resp
18+
19+
const format = flags.output as OutputFormat
20+
if (format === 'json' || format === 'yaml') {
21+
this.log(formatOutput(me, format))
22+
return
23+
}
24+
25+
const k = me.key ?? {}
26+
const o = me.organization ?? {}
27+
const p = me.plan ?? {}
28+
const r = me.rateLimits ?? {}
29+
30+
this.log('')
31+
this.log(' API Key')
32+
this.log(` Name: ${k.name ?? '–'} ID: ${k.id ?? '–'}`)
33+
this.log(` Created: ${k.createdAt ?? '–'} Expires: ${k.expiresAt ?? 'never'}`)
34+
this.log(` Last used: ${k.lastUsedAt ?? 'never'}`)
35+
this.log('')
36+
this.log(' Organization')
37+
this.log(` Name: ${o.name ?? '–'} ID: ${o.id ?? '–'}`)
38+
this.log('')
39+
this.log(' Plan')
40+
this.log(` Tier: ${p.tier ?? '–'} Status: ${p.subscriptionStatus ?? '–'} Trial: ${p.trialActive ? `active (expires ${p.trialExpiresAt})` : 'no'}`)
41+
this.log('')
42+
this.log(' Rate Limits')
43+
this.log(` Limit: ${r.requestsPerMinute ?? '–'} req/min Remaining: ${r.remaining ?? '–'} Window: ${r.windowMs ? `${r.windowMs / 1000}s` : '–'}`)
44+
45+
const usage = p.usage as Record<string, number> | undefined
46+
const entitlements = p.entitlements as Record<string, {value: number}> | undefined
47+
if (usage && entitlements) {
48+
this.log('')
49+
this.log(' Usage')
50+
for (const [key, used] of Object.entries(usage)) {
51+
const limit = entitlements[key]?.value
52+
const limitStr = limit != null && limit < Number.MAX_SAFE_INTEGER ? String(limit) : '∞'
53+
const label = key.replace(/\./g, ' ').replace(/_/g, ' ')
54+
this.log(` ${label}: ${used} / ${limitStr}`)
55+
}
56+
}
57+
58+
this.log('')
59+
}
60+
}

0 commit comments

Comments
 (0)