fix: Implement user API enhancements with masked API keys and updated invite link handling
This commit is contained in:
69
frontend/src/api/__tests__/user.test.ts
Normal file
69
frontend/src/api/__tests__/user.test.ts
Normal 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',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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' })
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user