mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat(oauth): browser-initiated dynamic client registration (DCR)
Adds an OAuth 2.1 public client registration flow so MCP clients can
self-register via a user-facing consent page instead of requiring manual
setup in Settings.
Server:
- DB migration adds `is_public` and `created_via` columns to oauth_clients
- New GET /api/oauth/register/validate — validates DCR params, returns
requested scopes; unauthenticated callers get loginRequired flag
- New POST /api/oauth/register — creates a public client, saves consent,
and redirects with client_id (cookie auth required)
- `authenticateClient` / `refreshTokens` skip secret check for public
clients (PKCE provides the security guarantee)
- `createOAuthClient` accepts options for isPublic/createdVia; public
clients store an opaque secret hash instead of a usable secret
- `rotateOAuthClientSecret` blocked on public clients
- `isValidRedirectUri` extracted as a shared helper
- Discovery metadata now advertises registration_endpoint and auth method
`none`; token/revoke endpoints no longer require client_secret for
public clients
Client:
- New OAuthRegisterPage (/oauth/register) — loading → optional
login-required gate → scope selection → done states
- New ScopeGroupPicker component — collapsible groups, indeterminate
checkboxes, select-all per group or globally
- oauthApi.register.{validate,submit} added to api/client.ts
- apiClient exported so it can be reused outside api/client.ts
- IntegrationsTab tests fixed for new collapsible section structure
- collab_notes fallback changed from undefined to [] in MCP trip tools
This commit is contained in:
@@ -21,6 +21,7 @@ const wsMock = await import('../../../src/api/websocket');
|
||||
|
||||
// Import the API client AFTER the mock is set up so it picks up our getSocketId mock
|
||||
const {
|
||||
apiClient,
|
||||
authApi,
|
||||
tripsApi,
|
||||
placesApi,
|
||||
@@ -465,19 +466,25 @@ describe('API client interceptors', () => {
|
||||
});
|
||||
|
||||
it('FE-API-022: authApi.uploadAvatar sends multipart/form-data', async () => {
|
||||
let contentType = '';
|
||||
// jsdom's FormData ≠ undici's FormData, so Node.js's Request always
|
||||
// serialises it as text/plain — Content-Type header checks are unreliable.
|
||||
// MSW wraps XHR in a Proxy, so XHR prototype spies never fire. axios.create()
|
||||
// copies prototype methods onto the instance as bound functions, so prototype
|
||||
// spies don't fire either. Spy on the exported apiClient instance directly.
|
||||
server.use(
|
||||
http.post('/api/auth/avatar', ({ request }) => {
|
||||
contentType = request.headers.get('Content-Type') ?? '';
|
||||
http.post('/api/auth/avatar', () => {
|
||||
return HttpResponse.json({ avatar_url: '/uploads/avatar.jpg' });
|
||||
})
|
||||
);
|
||||
|
||||
const postSpy = vi.spyOn(apiClient, 'post');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', new Blob(['img'], { type: 'image/jpeg' }), 'avatar.jpg');
|
||||
|
||||
await authApi.uploadAvatar(formData);
|
||||
expect(contentType).toMatch(/multipart\/form-data/);
|
||||
expect(postSpy).toHaveBeenCalledWith('/auth/avatar', expect.any(FormData), expect.anything());
|
||||
postSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('FE-API-023: authApi.mcpTokens.create posts name to /api/auth/mcp-tokens', async () => {
|
||||
|
||||
Reference in New Issue
Block a user