mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-10-23 06:12:27 +00:00
fix(owner): preserve password whitespace during login
This commit is contained in:
1 parent
771fc6f613
commit
2be3357af6
4 files changed
+72
-9
No files matched your search
@@ -82,3 +82,36 @@ test('owner session route rejects cookies from an older credential generation',
|
|||||||
ownerServer.restore();
|
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,
|
lastHopSeriesCache,
|
||||||
linkBadge,
|
linkBadge,
|
||||||
nodeRoleLabel,
|
nodeRoleLabel,
|
||||||
|
ownerLoginCredentials,
|
||||||
|
ownerLoginValidationError,
|
||||||
publishOwnerSession,
|
publishOwnerSession,
|
||||||
type LastHopStrengthPoint,
|
type LastHopStrengthPoint,
|
||||||
type MappedPeer,
|
type MappedPeer,
|
||||||
@@ -128,13 +130,14 @@ export const OwnerPortalPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleLogin = (event: FormEvent) => {
|
const handleLogin = (event: FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!mqttUsername.trim() || !mqttPassword) {
|
const validationError = ownerLoginValidationError(mqttUsername, mqttPassword);
|
||||||
setError('Enter your MQTT username and password.');
|
if (validationError) {
|
||||||
|
setError(validationError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const loginUsername = mqttUsername.trim();
|
const credentials = ownerLoginCredentials(mqttUsername, mqttPassword);
|
||||||
fetchOwnerCsrfToken()
|
fetchOwnerCsrfToken()
|
||||||
.then((csrfToken) => fetchJson<OwnerSessionResponse>('/api/owner/login', {
|
.then((csrfToken) => fetchJson<OwnerSessionResponse>('/api/owner/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -142,10 +145,7 @@ export const OwnerPortalPage: React.FC = () => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-CSRF-Token': csrfToken,
|
'X-CSRF-Token': csrfToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(credentials),
|
||||||
mqttUsername: loginUsername,
|
|
||||||
mqttPassword,
|
|
||||||
}),
|
|
||||||
}, {
|
}, {
|
||||||
timeoutMs: 15_000,
|
timeoutMs: 15_000,
|
||||||
maxBytes: 2 * 1024 * 1024,
|
maxBytes: 2 * 1024 * 1024,
|
||||||
@@ -155,8 +155,8 @@ export const OwnerPortalPage: React.FC = () => {
|
|||||||
.then((json) => {
|
.then((json) => {
|
||||||
lastHopSeriesCache.clear();
|
lastHopSeriesCache.clear();
|
||||||
setDashboard(json.dashboard);
|
setDashboard(json.dashboard);
|
||||||
setOwnerSessionKey(json.mqttUsername ?? loginUsername);
|
setOwnerSessionKey(json.mqttUsername ?? credentials.mqttUsername);
|
||||||
publishOwnerSession(json.mqttUsername ?? loginUsername);
|
publishOwnerSession(json.mqttUsername ?? credentials.mqttUsername);
|
||||||
if (json.dashboard.nodes[0]?.node_id) {
|
if (json.dashboard.nodes[0]?.node_id) {
|
||||||
setSelectedNodeId(json.dashboard.nodes[0].node_id);
|
setSelectedNodeId(json.dashboard.nodes[0].node_id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,20 @@ import {
|
|||||||
isOwnerSessionResponse,
|
isOwnerSessionResponse,
|
||||||
isValidMapCoord,
|
isValidMapCoord,
|
||||||
nodeRoleLabel,
|
nodeRoleLabel,
|
||||||
|
ownerLoginCredentials,
|
||||||
|
ownerLoginValidationError,
|
||||||
} from './ownerPortalModel.js';
|
} 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', () => {
|
test('owner map coordinate policy rejects placeholders and invalid coordinates', () => {
|
||||||
assert.equal(isValidMapCoord(51.5, -0.1), true);
|
assert.equal(isValidMapCoord(51.5, -0.1), true);
|
||||||
assert.equal(isValidMapCoord(0, 0), false);
|
assert.equal(isValidMapCoord(0, 0), false);
|
||||||
|
|||||||
@@ -21,6 +21,24 @@ export function nodeRoleLabel(role: number | null): string {
|
|||||||
return 'Repeater';
|
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 = {
|
export type OwnerDashboard = {
|
||||||
nodes: OwnerNode[];
|
nodes: OwnerNode[];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user