fix: serve the device paths with or without a trailing slash
The panel's HTTP client does not follow redirects. It asks for /api/setup/ with a trailing slash, Next answered 308 to normalise it, and the firmware reported "returned code is not OK. Code - 308" and gave up. Never having obtained a token, it then called /api/display with an empty one, got 401, and told the user it could not reach the API. Not TLS, not the network, not the port — a slash. Two earlier fixes were aimed at hypotheses the evidence did not support: a certificate chain the firmware genuinely cannot validate, and a port the shop's network turned out not to block. Both were reasoned from silence, because neither Traefik nor a production Next server logs requests by default. The answer came from a packet capture, and from the device's own words. /api/log now accepts a report from a panel that cannot authenticate. Refusing it with a 401 threw away the one diagnostic that mattered: the firmware was saying exactly what was wrong and we were discarding the message. Nothing is stored — the rows would reference a device that does not exist — but it reaches the server log, and the route was already rate-limited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
This commit is contained in:
+15
-2
@@ -1,7 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { authenticateDevice } from '@/lib/device/session';
|
import { authenticateDevice } from '@/lib/device/session';
|
||||||
import { clientIp } from '@/lib/device/headers';
|
import { clientIp, deviceHeader } from '@/lib/device/headers';
|
||||||
import { checkTransport } from '@/lib/device/transport';
|
import { checkTransport } from '@/lib/device/transport';
|
||||||
import { prisma } from '@/lib/db';
|
import { prisma } from '@/lib/db';
|
||||||
import { rateLimit } from '@/lib/ratelimit';
|
import { rateLimit } from '@/lib/ratelimit';
|
||||||
@@ -34,8 +34,21 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const device = await authenticateDevice(request);
|
const device = await authenticateDevice(request);
|
||||||
|
|
||||||
if (!device) {
|
if (!device) {
|
||||||
return NextResponse.json({ error: 'Jeton invalide' }, { status: 401 });
|
// A panel that cannot authenticate is precisely the panel whose own
|
||||||
|
// account of the failure is worth having. Refusing it with a 401 threw
|
||||||
|
// away the one diagnostic that mattered here: the firmware was reporting
|
||||||
|
// a 308 on /api/setup/, and we discarded the message saying so.
|
||||||
|
//
|
||||||
|
// Nothing is stored — the rows are tied to a device that does not exist —
|
||||||
|
// but it reaches the server log, where `docker compose logs app` will show
|
||||||
|
// it. The route is rate-limited above, so this is not an open write.
|
||||||
|
const body = await request.text().catch(() => '');
|
||||||
|
console.warn(
|
||||||
|
`[device] journal d'un appareil non authentifié (ID: ${deviceHeader(request, 'id') ?? 'absent'}) : ${body.slice(0, 1000)}`,
|
||||||
|
);
|
||||||
|
return new NextResponse(null, { status: 204 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries: IncomingLog[] = [];
|
let entries: IncomingLog[] = [];
|
||||||
|
|||||||
@@ -8,6 +8,28 @@ const nextConfig: NextConfig = {
|
|||||||
// Next writes its own AGENTS.md/CLAUDE.md otherwise; this project documents
|
// Next writes its own AGENTS.md/CLAUDE.md otherwise; this project documents
|
||||||
// itself in README.md and PLAN.md.
|
// itself in README.md and PLAN.md.
|
||||||
agentRules: false,
|
agentRules: false,
|
||||||
|
|
||||||
|
// The panel's HTTP client does not follow redirects: it reports any non-2xx
|
||||||
|
// as "returned code is not OK" and gives up. The firmware asks for
|
||||||
|
// /api/setup/ with a trailing slash, which Next would answer with a 308 —
|
||||||
|
// so the device never obtained a token, then failed /api/display with an
|
||||||
|
// empty one, and reported that it could not reach the API at all.
|
||||||
|
//
|
||||||
|
// Serving both spellings is the fix. The redirect is disabled and the
|
||||||
|
// slashed paths are rewritten, rather than the device being asked to behave
|
||||||
|
// differently: it cannot.
|
||||||
|
skipTrailingSlashRedirect: true,
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{ source: '/api/setup/', destination: '/api/setup' },
|
||||||
|
{ source: '/api/display/', destination: '/api/display' },
|
||||||
|
{ source: '/api/log/', destination: '/api/log' },
|
||||||
|
{ source: '/api/health/', destination: '/api/health' },
|
||||||
|
// Admin pages keep working when someone types the slash by hand, which
|
||||||
|
// Next would otherwise have redirected for us.
|
||||||
|
{ source: '/admin/', destination: '/admin' },
|
||||||
|
];
|
||||||
|
},
|
||||||
// These must be required from node_modules at runtime, not bundled.
|
// These must be required from node_modules at runtime, not bundled.
|
||||||
// satori loads harfbuzz and yoga as WebAssembly by relative path; bundling
|
// satori loads harfbuzz and yoga as WebAssembly by relative path; bundling
|
||||||
// rewrites that path and the renderer dies with a missing hb.wasm. resvg is
|
// rewrites that path and the renderer dies with a missing hb.wasm. resvg is
|
||||||
|
|||||||
@@ -217,11 +217,19 @@ describe.skipIf(!hasDatabase)('device API', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /api/log', () => {
|
describe('POST /api/log', () => {
|
||||||
it('refuses a request with no token', async () => {
|
it('accepts a log from a device that cannot authenticate, without storing it', async () => {
|
||||||
|
// A panel that cannot authenticate is precisely the panel whose account
|
||||||
|
// of the failure is worth having: this is how a 308 on /api/setup/ was
|
||||||
|
// finally diagnosed, after a 401 had been discarding the evidence.
|
||||||
const response = await log(
|
const response = await log(
|
||||||
deviceRequest('/api/log', {}, { method: 'POST', body: '{"logs":[]}' }),
|
deviceRequest(
|
||||||
|
'/api/log',
|
||||||
|
{ ID: MAC },
|
||||||
|
{ method: 'POST', body: '{"logs":[{"message":"returned code is not OK. Code - 308"}]}' },
|
||||||
|
),
|
||||||
);
|
);
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(204);
|
||||||
|
expect(await prisma.deviceLog.count()).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stores what the firmware reports', async () => {
|
it('stores what the firmware reports', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user