Files
tkmind_go/ui/h5/user-auth.test.mjs
john 4e21ca937a
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Add TKMind platform extensions, H5/MindSpace stack, and deployment tooling.
Fork goose with custom MCP widgets, platform extensions (aider, git, web, search),
MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 21:30:20 +08:00

184 lines
5.8 KiB
JavaScript

import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import test from 'node:test';
import { createUserAuth } from './user-auth.mjs';
function hashPasswordPbkdf2(password, salt) {
return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
}
function hashPasswordArgon2id(password, salt) {
return crypto
.argon2Sync('argon2id', {
message: password,
nonce: Buffer.from(salt, 'hex'),
parallelism: 1,
tagLength: 32,
memory: 64 * 1024,
passes: 3,
})
.toString('hex');
}
function createAuthPool(userRow) {
const sessions = [];
return {
async query(sql, params = []) {
if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.username = ?')) {
return userRow ? [[userRow]] : [[]];
}
if (sql.includes('UPDATE h5_users') && sql.includes('password_algorithm')) {
userRow.salt = params[0];
userRow.password_hash = params[1];
userRow.password_algorithm = params[2];
return [[]];
}
if (sql.includes('INSERT INTO h5_login_sessions')) {
sessions.push({
token_hash: params[2],
user_id: params[1],
expires_at: params[3],
revoked_at: null,
});
return [[]];
}
if (sql.includes('FROM h5_login_sessions s')) {
const row = sessions.find((item) => item.token_hash === params[0] && !item.revoked_at);
if (!row) return [[]];
return [[
{
user_id: row.user_id,
expires_at: row.expires_at,
role: userRow.role,
status: userRow.status,
},
]];
}
if (sql.includes('UPDATE h5_login_sessions SET expires_at')) {
const row = sessions.find((item) => item.token_hash === params[1]);
if (row) row.expires_at = params[0];
return [[]];
}
if (sql.includes('UPDATE h5_login_sessions SET revoked_at')) {
for (const row of sessions) {
if (row.user_id === params[1]) row.revoked_at = params[0];
}
return [[]];
}
if (sql.includes('FROM h5_users u') && sql.includes('WHERE u.id = ?')) {
return [[userRow]];
}
return [[]];
},
};
}
test('login rate limits repeated failures', async () => {
const auth = createUserAuth(createAuthPool(null), {
loginMaxFailures: 2,
loginFailureWindowMs: 60_000,
persistSessions: false,
});
assert.equal((await auth.login({ username: 'john', password: 'bad', ip: '1.1.1.1' })).ok, false);
assert.equal((await auth.login({ username: 'john', password: 'bad', ip: '1.1.1.1' })).ok, false);
const blocked = await auth.login({ username: 'john', password: 'secret', ip: '1.1.1.1' });
assert.equal(blocked.ok, false);
assert.ok(blocked.retryAfterMs > 0);
});
test('verify rejects disabled users after login', async () => {
const salt = '00112233445566778899aabbccddeeff';
const userRow = {
id: 'user-1',
username: 'john',
slug: 'john',
email: 'john@example.com',
display_name: 'John',
role: 'user',
status: 'active',
plan_type: 'free',
workspace_root: '/tmp/john',
salt,
password_hash: hashPasswordArgon2id('secret', salt),
password_algorithm: 'argon2id',
balance_cents: 100,
tokens_used: 0,
};
const auth = createUserAuth(createAuthPool(userRow), { persistSessions: true });
const login = await auth.login({ username: 'john', password: 'secret', ip: '1.1.1.1' });
assert.equal(login.ok, true);
userRow.status = 'disabled';
assert.equal(await auth.verify(login.token), null);
});
test('resetPassword updates hash when username and email match', async () => {
const salt = '11112222333344445555666677778888';
const userRow = {
id: 'user-1',
username: 'john',
email: 'john@example.com',
status: 'active',
salt,
password_hash: hashPasswordPbkdf2('old-secret', salt),
password_algorithm: 'pbkdf2-sha512',
};
const pool = {
async query(sql, params = []) {
if (sql.includes('SELECT id, email, status FROM h5_users WHERE username = ?')) {
return userRow.username === params[0] ? [[userRow]] : [[]];
}
if (sql.includes('UPDATE h5_users SET salt = ?')) {
userRow.salt = params[0];
userRow.password_hash = params[1];
userRow.password_algorithm = params[2];
return [[]];
}
if (sql.includes('UPDATE h5_login_sessions SET revoked_at')) {
return [[]];
}
return [[]];
},
};
const auth = createUserAuth(pool, { persistSessions: false });
const ok = await auth.resetPassword({
username: 'john',
email: 'john@example.com',
password: 'new-secret',
});
assert.equal(ok.ok, true);
assert.equal(userRow.password_algorithm, 'argon2id');
assert.equal(userRow.password_hash, hashPasswordArgon2id('new-secret', userRow.salt));
const bad = await auth.resetPassword({
username: 'john',
email: 'wrong@example.com',
password: 'another',
});
assert.equal(bad.ok, false);
});
test('login migrates pbkdf2 passwords to argon2id after successful verification', async () => {
const salt = '9999aaaabbbbccccddddeeeeffff0000';
const userRow = {
id: 'user-1',
username: 'john',
slug: 'john',
email: 'john@example.com',
display_name: 'John',
role: 'user',
status: 'active',
plan_type: 'free',
workspace_root: '/tmp/john',
salt,
password_hash: hashPasswordPbkdf2('secret', salt),
password_algorithm: 'pbkdf2-sha512',
balance_cents: 100,
tokens_used: 0,
};
const auth = createUserAuth(createAuthPool(userRow), { persistSessions: false });
const login = await auth.login({ username: 'john', password: 'secret', ip: '1.1.1.1' });
assert.equal(login.ok, true);
assert.equal(userRow.password_algorithm, 'argon2id');
assert.equal(userRow.password_hash, hashPasswordArgon2id('secret', userRow.salt));
});