mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-10-15 10:40:01 +00:00
fix(owner): preserve password whitespace during login
This commit is contained in:
@@ -82,3 +82,36 @@ test('owner session route rejects cookies from an older credential generation',
|
||||
ownerServer.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('owner login verifies the exact password string, including surrounding whitespace', async () => {
|
||||
let receivedCredentials: [string, string] | undefined;
|
||||
const ownerServer = await startOwnerRoutes({
|
||||
verifyCredentials: async (username, password) => {
|
||||
receivedCredentials = [username, password];
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const csrfResponse = await fetch(`${ownerServer.baseUrl}/owner/csrf`);
|
||||
const { csrfToken } = await csrfResponse.json() as { csrfToken: string };
|
||||
const csrfCookie = csrfResponse.headers.get('set-cookie')?.split(';', 1)[0];
|
||||
assert.ok(csrfCookie);
|
||||
|
||||
const response = await fetch(`${ownerServer.baseUrl}/owner/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
cookie: csrfCookie,
|
||||
'x-csrf-token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({ mqttUsername: ' owner ', mqttPassword: ' password ' }),
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(receivedCredentials, ['owner', ' password ']);
|
||||
} finally {
|
||||
await ownerServer.close();
|
||||
ownerServer.restore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
lastHopSeriesCache,
|
||||
linkBadge,
|
||||
nodeRoleLabel,
|
||||
ownerLoginCredentials,
|
||||
ownerLoginValidationError,
|
||||
publishOwnerSession,
|
||||
type LastHopStrengthPoint,
|
||||
type MappedPeer,
|
||||
@@ -128,13 +130,14 @@ export const OwnerPortalPage: React.FC = () => {
|
||||
|
||||
const handleLogin = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!mqttUsername.trim() || !mqttPassword) {
|
||||
setError('Enter your MQTT username and password.');
|
||||
const validationError = ownerLoginValidationError(mqttUsername, mqttPassword);
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
const loginUsername = mqttUsername.trim();
|
||||
const credentials = ownerLoginCredentials(mqttUsername, mqttPassword);
|
||||
fetchOwnerCsrfToken()
|
||||
.then((csrfToken) => fetchJson<OwnerSessionResponse>('/api/owner/login', {
|
||||
method: 'POST',
|
||||
@@ -142,10 +145,7 @@ export const OwnerPortalPage: React.FC = () => {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mqttUsername: loginUsername,
|
||||
mqttPassword,
|
||||
}),
|
||||
body: JSON.stringify(credentials),
|
||||
}, {
|
||||
timeoutMs: 15_000,
|
||||
maxBytes: 2 * 1024 * 1024,
|
||||
@@ -155,8 +155,8 @@ export const OwnerPortalPage: React.FC = () => {
|
||||
.then((json) => {
|
||||
lastHopSeriesCache.clear();
|
||||
setDashboard(json.dashboard);
|
||||
setOwnerSessionKey(json.mqttUsername ?? loginUsername);
|
||||
publishOwnerSession(json.mqttUsername ?? loginUsername);
|
||||
setOwnerSessionKey(json.mqttUsername ?? credentials.mqttUsername);
|
||||
publishOwnerSession(json.mqttUsername ?? credentials.mqttUsername);
|
||||
if (json.dashboard.nodes[0]?.node_id) {
|
||||
setSelectedNodeId(json.dashboard.nodes[0].node_id);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,20 @@ import {
|
||||
isOwnerSessionResponse,
|
||||
isValidMapCoord,
|
||||
nodeRoleLabel,
|
||||
ownerLoginCredentials,
|
||||
ownerLoginValidationError,
|
||||
} from './ownerPortalModel.js';
|
||||
|
||||
test('owner login trims only the username and preserves password whitespace', () => {
|
||||
assert.equal(ownerLoginValidationError(' owner ', ' password '), null);
|
||||
assert.deepEqual(ownerLoginCredentials(' owner ', ' password '), {
|
||||
mqttUsername: 'owner',
|
||||
mqttPassword: ' password ',
|
||||
});
|
||||
assert.match(ownerLoginValidationError('owner', '\tpassword') ?? '', /invalid characters/);
|
||||
assert.match(ownerLoginValidationError('owner', 'x'.repeat(129)) ?? '', /too long/);
|
||||
});
|
||||
|
||||
test('owner map coordinate policy rejects placeholders and invalid coordinates', () => {
|
||||
assert.equal(isValidMapCoord(51.5, -0.1), true);
|
||||
assert.equal(isValidMapCoord(0, 0), false);
|
||||
|
||||
@@ -21,6 +21,24 @@ export function nodeRoleLabel(role: number | null): string {
|
||||
return 'Repeater';
|
||||
}
|
||||
|
||||
export type OwnerLoginCredentials = { mqttUsername: string; mqttPassword: string };
|
||||
|
||||
export function ownerLoginValidationError(mqttUsername: string, mqttPassword: string): string | null {
|
||||
const normalizedUsername = mqttUsername.trim();
|
||||
if (!normalizedUsername || !mqttPassword) return 'Enter your MQTT username and password.';
|
||||
if (normalizedUsername.length > 128 || mqttPassword.length > 128) {
|
||||
return 'MQTT username or password is too long.';
|
||||
}
|
||||
if (/[\u0000-\u001F\u007F]/.test(normalizedUsername) || /[\u0000-\u001F\u007F]/.test(mqttPassword)) {
|
||||
return 'MQTT username or password contains invalid characters.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ownerLoginCredentials(mqttUsername: string, mqttPassword: string): OwnerLoginCredentials {
|
||||
return { mqttUsername: mqttUsername.trim(), mqttPassword };
|
||||
}
|
||||
|
||||
export type OwnerDashboard = {
|
||||
nodes: OwnerNode[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user