Public Access
feat(admin): add the admin users page and view as
This commit is contained in:
1 parent
46587dfb5c
commit
afe00ceae9
14 files changed
+649
-8
No files matched your search
@@ -0,0 +1,34 @@
|
||||
import {apiFetch, rotatingFetch} from './api.js';
|
||||
|
||||
const AUTH = () => import.meta.env.VITE_API_URL;
|
||||
|
||||
export const ROLES = ['USER', 'AUDITOR', 'PLATFORM_ADMIN'];
|
||||
|
||||
/**
|
||||
* GET /api/admin/users: {content, page, size, totalElements, totalPages}. PLATFORM_ADMIN only (403 otherwise).
|
||||
*/
|
||||
export const fetchUsers = (query, page, size) =>
|
||||
apiFetch(`${AUTH()}/api/admin/users?${new URLSearchParams({query, page, size})}`);
|
||||
|
||||
/**
|
||||
* 204, or 409 with code SELF_ACTION, TARGET_IS_ADMIN or ALREADY_DELETED.
|
||||
*/
|
||||
export const setUserBlocked = (userId, blocked) =>
|
||||
apiFetch(`${AUTH()}/api/admin/users/${userId}/blocked`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({blocked}),
|
||||
});
|
||||
|
||||
export const deleteUser = (userId) => apiFetch(`${AUTH()}/api/admin/users/${userId}`, {method: 'DELETE'});
|
||||
|
||||
/**
|
||||
* "View as": new cookies that act with the role; null goes back to the own role. Under /api/auth, so it needs the
|
||||
* refresh cookie and no access token; 403 ROLE_NOT_ALLOWED above the own role.
|
||||
*/
|
||||
export const viewAs = (role) =>
|
||||
rotatingFetch(`${AUTH()}/api/auth/view-as`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({role}),
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// @vitest-environment jsdom
|
||||
import {describe, expect, it, vi} from 'vitest';
|
||||
import {refreshSession} from './api.js';
|
||||
import {viewAs} from './admin.js';
|
||||
|
||||
describe('viewAs', () => {
|
||||
it('waits for a running refresh before it sends the refresh cookie', async () => {
|
||||
let finishRefresh;
|
||||
globalThis.fetch = vi.fn((url) => (url.endsWith('/api/auth/refresh')
|
||||
? new Promise((resolve) => {
|
||||
finishRefresh = () => resolve({ok: true, status: 204});
|
||||
})
|
||||
: Promise.resolve({ok: true, status: 204})));
|
||||
|
||||
const refreshing = refreshSession();
|
||||
const switching = viewAs('USER');
|
||||
await Promise.resolve();
|
||||
|
||||
expect(fetch.mock.calls.map(([url]) => url).filter((url) => url.endsWith('/api/auth/view-as'))).toHaveLength(0);
|
||||
|
||||
finishRefresh();
|
||||
await refreshing;
|
||||
await switching;
|
||||
});
|
||||
});
|
||||
@@ -44,6 +44,16 @@ export function refreshSession() {
|
||||
return runningRefresh;
|
||||
}
|
||||
|
||||
/**
|
||||
* A request that rotates the refresh token like a refresh (e.g. "view as"). It waits for a running refresh and
|
||||
* takes the same lock, so no two requests send one refresh token.
|
||||
*/
|
||||
export async function rotatingFetch(url, init = {}) {
|
||||
await runningRefresh?.catch(() => undefined);
|
||||
const send = () => clientFetch(url, init);
|
||||
return globalThis.navigator?.locks ? navigator.locks.request('tbp-refresh', send) : send();
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch for the logged-in user. A 401 renews the access cookie once and sends the request again. When that fails,
|
||||
* the 401 goes back to the caller. Ending the session is the caller's job (useSessionExpired), so the user sees
|
||||
|
||||
Reference in new issue
Block a user