feat: remove Account page and add PassthroughLanding page

- Deleted the Account page and its associated logic.
- Introduced a new PassthroughLanding page for users without management access.
- Updated Settings page to conditionally display the Users link for admin users.
- Enhanced UsersPage to support passthrough user role, including invite functionality and user detail modal.
- Updated tests to reflect changes in user roles and navigation.
This commit is contained in:
GitHub Actions
2026-03-03 02:17:17 +00:00
parent 3632d0d88c
commit a681d6aa30
19 changed files with 708 additions and 630 deletions
@@ -10,7 +10,7 @@ const translations: Record<string, string> = {
'settings.system': 'System',
'navigation.notifications': 'Notifications',
'settings.smtp': 'Email (SMTP)',
'settings.account': 'Account',
'navigation.users': 'Users',
}
const t = (key: string) => translations[key] ?? key
@@ -19,6 +19,10 @@ vi.mock('react-i18next', () => ({
useTranslation: () => ({ t }),
}))
vi.mock('../../hooks/useAuth', () => ({
useAuth: () => ({ user: { user_id: 1, role: 'admin', name: 'Admin' } }),
}))
const renderWithRoute = (route: string) =>
render(
<MemoryRouter initialEntries={[route]}>
@@ -27,7 +31,7 @@ const renderWithRoute = (route: string) =>
<Route path="system" element={<div>System Page</div>} />
<Route path="notifications" element={<div>Notifications Page</div>} />
<Route path="smtp" element={<div>SMTP Page</div>} />
<Route path="account" element={<div>Account Page</div>} />
<Route path="users" element={<div>Users Page</div>} />
</Route>
</Routes>
</MemoryRouter>
@@ -46,12 +50,12 @@ describe('Settings page', () => {
expect(screen.getByText('System Page')).toBeInTheDocument()
})
it('keeps navigation order consistent', () => {
it('keeps navigation order consistent for admin', () => {
renderWithRoute('/settings/notifications')
const links = screen.getAllByRole('link')
const labels = links.map(link => link.textContent)
expect(labels).toEqual(['System', 'Notifications', 'Email (SMTP)', 'Account'])
expect(labels).toEqual(['System', 'Notifications', 'Email (SMTP)', 'Users'])
})
})
@@ -22,6 +22,20 @@ vi.mock('../../api/users', () => ({
acceptInvite: vi.fn(),
previewInviteURL: vi.fn(),
resendInvite: vi.fn(),
getProfile: vi.fn(),
updateProfile: vi.fn(),
regenerateApiKey: vi.fn(),
}))
vi.mock('../../hooks/useAuth', () => ({
useAuth: vi.fn().mockReturnValue({
user: { user_id: 1, role: 'admin', name: 'Admin User', email: 'admin@example.com' },
changePassword: vi.fn().mockResolvedValue(undefined),
isAuthenticated: true,
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
}),
}))
vi.mock('../../api/proxyHosts', () => ({
@@ -78,6 +92,18 @@ const mockUsers = [
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
},
{
id: 4,
uuid: '999-000',
email: 'passthrough@example.com',
name: 'Passthrough User',
role: 'passthrough' as const,
enabled: true,
invite_status: 'accepted' as const,
permission_mode: 'allow_all' as const,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
},
]
const mockProxyHosts = [
@@ -127,8 +153,8 @@ describe('UsersPage', () => {
expect(screen.getByText('User Management')).toBeTruthy()
})
expect(screen.getByText('Admin User')).toBeTruthy()
expect(screen.getByText('admin@example.com')).toBeTruthy()
expect(screen.getAllByText('Admin User').length).toBeGreaterThan(0)
expect(screen.getAllByText('admin@example.com').length).toBeGreaterThan(0)
expect(screen.getByText('Regular User')).toBeTruthy()
expect(screen.getByText('user@example.com')).toBeTruthy()
})
@@ -346,6 +372,58 @@ describe('UsersPage', () => {
})
})
it('renders passthrough role badge', async () => {
vi.mocked(usersApi.listUsers).mockResolvedValue(mockUsers)
renderWithQueryClient(<UsersPage />)
await waitFor(() => {
expect(screen.getByText('Passthrough')).toBeTruthy()
})
})
it('renders My Profile card for current user', async () => {
vi.mocked(usersApi.listUsers).mockResolvedValue(mockUsers)
renderWithQueryClient(<UsersPage />)
await waitFor(() => {
expect(screen.getByText('My Profile')).toBeTruthy()
})
})
it('shows passthrough option in invite role select', async () => {
vi.mocked(usersApi.listUsers).mockResolvedValue(mockUsers)
renderWithQueryClient(<UsersPage />)
const user = userEvent.setup()
await waitFor(() => expect(screen.getByText('Invite User')).toBeTruthy())
await user.click(screen.getByRole('button', { name: /Invite User/i }))
await waitFor(() => {
const roleSelect = screen.getByLabelText('Role') as HTMLSelectElement
const options = Array.from(roleSelect.options).map(o => o.value)
expect(options).toContain('passthrough')
})
})
it('opens detail modal when edit button is clicked', async () => {
vi.mocked(usersApi.listUsers).mockResolvedValue(mockUsers)
renderWithQueryClient(<UsersPage />)
await waitFor(() => expect(screen.getByText('Regular User')).toBeTruthy())
const user = userEvent.setup()
const editButtons = screen.getAllByTitle('Edit User')
await user.click(editButtons[1])
await waitFor(() => {
expect(screen.getByRole('dialog', { name: /Edit User/i })).toBeTruthy()
})
})
describe('URL Preview in InviteModal', () => {
afterEach(() => {
vi.useRealTimers()