fix(settings): make the self-hosted routing engine an instance setting

The field was rendered in every non-managed user's own Map settings and written
to their personal settings row. The CSP is built from something else: bootstrap
reads the ADMIN default and puts that origin into `connect-src`. So filling the
field in was the one path the product offered, and it could not work — the route
calculator happily built its URLs against the typed origin and the browser
refused every one of them with a bare TypeError, not a status, so no fallback and
no retry could see it. Day-plan routes, transport legs, the mobile day route,
Studio's road route and the whole road-trip rail stopped routing, app-wide, with
a generic "route could not be calculated". Nothing in the UI could repair it
except emptying the field again, and there was no admin field to set the value
that would have made the origin allowed.

The repo has decided this shape once already: `llm_base_url` is admin-only
because it names where the server sends its own requests and only whoever runs
the instance can judge what is reachable. This is the same class for a different
reason, so it joins the same guard, which is now named for what it actually
covers. Clearing stays open to everyone, so a row written before the rule cleans
itself up rather than blocking the save.

The field moves to the admin's default-user settings, next to the tile URL and
the Carto key, where a value takes effect.
This commit is contained in:
Maurice
2026-09-07 16:34:01 +02:00
committed by Maurice
parent 65be8db122
commit 239856f35c
7 changed files with 90 additions and 42 deletions
+25
View File
@@ -150,6 +150,31 @@ describe('Settings e2e (real auth guard + temp SQLite)', () => {
.toEqual({ value: 'anthropic' });
});
it('PUT 403 for a non-admin naming a routing engine (#1797)', async () => {
// Same class for a different reason: this origin has to appear in the CSP
// connect-src the server emits at boot, and that list is built from the
// admin default. A value on a personal row is read by the route
// calculator and then refused by the browser, so the app stops routing
// with no error it could report. Refusing tells the caller instead.
const res = await request(server).put('/api/settings').set('Cookie', sessionCookie(1))
.send({ key: 'routing_base_url', value: 'https://osrm.example.org' });
expect(res.status).toBe(403);
expect(res.body).toEqual({ error: 'Admin access required' });
expect(countRows()).toBe(0);
});
it('PUT lets a non-admin clear a routing engine somebody set before the rule', async () => {
expect((await request(server).put('/api/settings').set('Cookie', sessionCookie(1))
.send({ key: 'routing_base_url', value: '' })).status).toBe(200);
});
it('POST bulk refuses a routing engine from a non-admin', async () => {
const res = await request(server).post('/api/settings/bulk').set('Cookie', sessionCookie(1))
.send({ settings: { routing_base_url: 'https://osrm.example.org' } });
expect(res.status).toBe(403);
expect(countRows()).toBe(0);
});
it('PUT is unaffected for a non-LLM key with the same value', async () => {
const res = await request(server).put('/api/settings').set('Cookie', sessionCookie(1))
.send({ key: 'start_page', value: 'local' });
+4 -4
View File
@@ -48,7 +48,7 @@ import { encrypt_api_key } from '../../../src/nest/common/crypto/apiKeyCrypto';
import { MASKED_SETTING_VALUE } from '@trek/shared';
import { DISPLAY_PREFERENCE_KEYS } from '../../../src/nest/settings/settings.mcp';
import { MANAGED_LOCKED_SETTING_KEYS } from '../../../src/nest/common/managed';
import { isAdminOnlyLlmSetting, ENCRYPTED_SETTING_KEYS, MASKED_SETTING_KEYS } from '../../../src/nest/settings/settings.service';
import { isAdminOnlyEndpointSetting, ENCRYPTED_SETTING_KEYS, MASKED_SETTING_KEYS } from '../../../src/nest/settings/settings.service';
beforeAll(() => {
createTables(testDb);
@@ -493,11 +493,11 @@ describe('Display-preference allow-list', () => {
expect(overlap).toEqual([]);
});
it('holds no key assertMayWriteLlmEndpoint would have to refuse', () => {
// isAdminOnlyLlmSetting is value-dependent, so probe it with the values
it('holds no key assertMayWriteInstanceEndpoint would have to refuse', () => {
// isAdminOnlyEndpointSetting is value-dependent, so probe it with the values
// that trip it rather than matching on the key name.
const offenders = DISPLAY_PREFERENCE_KEYS.filter((key) =>
isAdminOnlyLlmSetting(key, 'http://127.0.0.1:11434') || isAdminOnlyLlmSetting(key, 'local'));
isAdminOnlyEndpointSetting(key, 'http://127.0.0.1:11434') || isAdminOnlyEndpointSetting(key, 'local'));
expect(offenders).toEqual([]);
});