Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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));
|
||||
});
|
||||
Reference in New Issue
Block a user