fix: Implement user API enhancements with masked API keys and updated invite link handling

This commit is contained in:
GitHub Actions
2026-02-25 06:14:03 +00:00
parent c156183666
commit 690480e181
8 changed files with 119 additions and 167 deletions

View File

@@ -0,0 +1,69 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import client from '../client'
import { getProfile, regenerateApiKey, updateProfile } from '../user'
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
},
}))
describe('user api', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches profile using masked API key fields', async () => {
vi.mocked(client.get).mockResolvedValueOnce({
data: {
id: 1,
email: 'admin@example.com',
name: 'Admin',
role: 'admin',
has_api_key: true,
api_key_masked: '********',
},
})
const profile = await getProfile()
expect(client.get).toHaveBeenCalledWith('/user/profile')
expect(profile.has_api_key).toBe(true)
expect(profile.api_key_masked).toBe('********')
})
it('regenerates API key and returns metadata-only response', async () => {
vi.mocked(client.post).mockResolvedValueOnce({
data: {
message: 'API key regenerated successfully',
has_api_key: true,
api_key_masked: '********',
api_key_updated: '2026-02-25T00:00:00Z',
},
})
const result = await regenerateApiKey()
expect(client.post).toHaveBeenCalledWith('/user/api-key')
expect(result.has_api_key).toBe(true)
expect(result.api_key_masked).toBe('********')
expect(result.api_key_updated).toBe('2026-02-25T00:00:00Z')
})
it('updates profile with optional current password', async () => {
vi.mocked(client.post).mockResolvedValueOnce({ data: { message: 'ok' } })
await updateProfile({
name: 'Updated Name',
email: 'updated@example.com',
current_password: 'current-password',
})
expect(client.post).toHaveBeenCalledWith('/user/profile', {
name: 'Updated Name',
email: 'updated@example.com',
current_password: 'current-password',
})
})
})

View File

@@ -50,7 +50,7 @@ describe('users api', () => {
})
it('invites users and updates permissions', async () => {
vi.mocked(client.post).mockResolvedValueOnce({ data: { invite_token: 't', invite_url: 'https://charon.example.com/accept-invite?token=t' } })
vi.mocked(client.post).mockResolvedValueOnce({ data: { invite_token_masked: '********', invite_url: '[REDACTED]' } })
await inviteUser({ email: 'i', permission_mode: 'allow_all' })
expect(client.post).toHaveBeenCalledWith('/users/invite', { email: 'i', permission_mode: 'allow_all' })

View File

@@ -6,7 +6,8 @@ export interface UserProfile {
email: string
name: string
role: string
api_key: string
has_api_key: boolean
api_key_masked: string
}
/**
@@ -24,8 +25,15 @@ export const getProfile = async (): Promise<UserProfile> => {
* @returns Promise resolving to object containing the new API key
* @throws {AxiosError} If regeneration fails
*/
export const regenerateApiKey = async (): Promise<{ api_key: string }> => {
const response = await client.post('/user/api-key')
export interface RegenerateApiKeyResponse {
message: string
has_api_key: boolean
api_key_masked: string
api_key_updated: string
}
export const regenerateApiKey = async (): Promise<RegenerateApiKeyResponse> => {
const response = await client.post<RegenerateApiKeyResponse>('/user/api-key')
return response.data
}

View File

@@ -50,7 +50,7 @@ describe('users api', () => {
it('creates, invites, updates, and deletes users', async () => {
mockedClient.post
.mockResolvedValueOnce({ data: { id: 3, uuid: 'u3', email: 'c@example.com', name: 'C', role: 'user', enabled: true, permission_mode: 'allow_all', created_at: '', updated_at: '' } })
.mockResolvedValueOnce({ data: { id: 4, uuid: 'u4', email: 'invite@example.com', role: 'user', invite_token: 'token', invite_url: 'https://charon.example.com/accept-invite?token=token', email_sent: true, expires_at: '' } })
.mockResolvedValueOnce({ data: { id: 4, uuid: 'u4', email: 'invite@example.com', role: 'user', invite_token_masked: '********', invite_url: '[REDACTED]', email_sent: true, expires_at: '' } })
mockedClient.put.mockResolvedValueOnce({ data: { message: 'updated' } })
mockedClient.delete.mockResolvedValueOnce({ data: { message: 'deleted' } })
@@ -61,7 +61,7 @@ describe('users api', () => {
const invite = await inviteUser({ email: 'invite@example.com', role: 'user' })
expect(mockedClient.post).toHaveBeenCalledWith('/users/invite', { email: 'invite@example.com', role: 'user' })
expect(invite.invite_token).toBe('token')
expect(invite.invite_token_masked).toBe('********')
await updateUser(3, { enabled: false })
expect(mockedClient.put).toHaveBeenCalledWith('/users/3', { enabled: false })

View File

@@ -44,8 +44,8 @@ export interface InviteUserResponse {
uuid: string
email: string
role: string
invite_token: string
invite_url: string
invite_token_masked: string
invite_url?: string
email_sent: boolean
expires_at: string
}