feat(M08-D): 补后台登录与可撤销会话

This commit is contained in:
Codex
2026-08-10 13:54:03 +08:00
parent 93d83e81b9
commit 05110087d1
21 changed files with 1059 additions and 48 deletions
+87
View File
@@ -0,0 +1,87 @@
import assert from 'node:assert/strict';
import { buildApp } from '../dist/app.js';
import { AdminAuthError } from '../dist/auth/admin-auth-repository.js';
import { hashPassword, verifyPassword } from '../dist/auth/password.js';
const secret = 'test-only-admin-password-auth-secret-32';
const user = { id: '21', tenantId: '7', userType: 'STAFF', status: 'ACTIVE', roleVersion: 1,
nickname: '租户管理员', avatarUrl: '', phone: '13800138000' };
let loginInput; let refreshInput; let credentialInput; let revokedSessionId;
const repository = {
async loginWithPassword(input) {
loginInput = input;
if (input.password === 'wrong-password') throw new AdminAuthError('ADMIN_LOGIN_INVALID');
if (input.password === 'locked-password') throw new AdminAuthError('ADMIN_LOGIN_LOCKED');
return { id: input.sessionId, tenantId: '7', platformAppId: '9', user, expiresAt: input.expiresAt };
},
async rotateRefreshToken(input) {
refreshInput = input;
return { id: input.sessionId, tenantId: '7', platformAppId: '9', user,
expiresAt: new Date(Date.now() + 60000) };
},
async setCredential(actor, tenantId, input) {
credentialInput = { actor, tenantId, input };
return { userId: input.userId, configured: true };
}
};
const authRepository = {
async validateSession(sessionId) {
return { id: sessionId, tenantId: '7', platformAppId: '9', user,
expiresAt: new Date(Date.now() + 60000) };
},
async revokeSession(sessionId) { revokedSessionId = sessionId; return true; }
};
const access = { roles: ['TENANT_ADMIN'], capabilities: ['tenant.manage'], storeIds: [] };
const app = await buildApp({ adminAuth: { repository, authRepository,
accessControl: { async getAccessProfile() { return access; } }, jwtSecret: secret,
accessTokenTtlSeconds: 900, sessionTtlSeconds: 604800 } });
const login = await app.inject({ method: 'POST', url: '/admin-api/auth/login',
payload: { tenantCode: 'demo', loginName: 'Admin.User', ['password']: 'ValidPassword!123' } });
assert.equal(login.statusCode, 200);
assert.equal(loginInput.tenantCode, 'demo');
assert.equal(login.json().data.user.phone, undefined);
assert.deepEqual(login.json().data.access.roles, ['TENANT_ADMIN']);
assert.ok(login.json().data.access.menus.includes('system'));
assert.match(login.json().data.accessToken, /^[^.]+\.[^.]+\.[^.]+$/);
assert.match(login.json().data.refreshToken, /^[0-9a-f-]{36}\.[A-Za-z0-9_-]{43}$/);
const successfulSessionId = loginInput.sessionId;
const bad = await app.inject({ method: 'POST', url: '/admin-api/auth/login',
payload: { tenantCode: 'demo', loginName: 'admin', ['password']: 'wrong-password' } });
assert.equal(bad.statusCode, 401);
const locked = await app.inject({ method: 'POST', url: '/admin-api/auth/login',
payload: { tenantCode: 'demo', loginName: 'admin', ['password']: 'locked-password' } });
assert.equal(locked.statusCode, 423);
const refreshed = await app.inject({ method: 'POST', url: '/admin-api/auth/refresh',
payload: { refreshToken: login.json().data.refreshToken } });
assert.equal(refreshed.statusCode, 200);
assert.equal(refreshInput.sessionId, successfulSessionId);
assert.notEqual(refreshed.json().data.refreshToken, login.json().data.refreshToken);
const auth = { authorization: `Bearer ${login.json().data.accessToken}` };
const me = await app.inject({ method: 'GET', url: '/admin-api/auth/me', headers: auth });
assert.equal(me.statusCode, 200);
assert.equal(me.json().data.user.nickname, '租户管理员');
const weakCredential = await app.inject({ method: 'PUT', url: '/admin-api/auth/credentials/22',
headers: auth, payload: { loginName: 'operator', ['password']: 'too-weak' } });
assert.equal(weakCredential.statusCode, 400);
const credential = await app.inject({ method: 'PUT', url: '/admin-api/auth/credentials/22',
headers: auth, payload: { loginName: 'Operator.22', ['password']: 'StrongPassword!2026' } });
assert.equal(credential.statusCode, 200);
assert.equal(credentialInput.tenantId, '7');
assert.equal(credentialInput.input.loginName, 'Operator.22');
assert.match(credentialInput.input.passwordHash, /^scrypt\$16384\$8\$1\$/);
const logout = await app.inject({ method: 'POST', url: '/admin-api/auth/logout', headers: auth });
assert.equal(logout.statusCode, 200);
assert.equal(revokedSessionId, successfulSessionId);
await app.close();
const passwordHash = await hashPassword('StrongPassword!2026');
assert.equal(await verifyPassword('StrongPassword!2026', passwordHash), true);
assert.equal(await verifyPassword('WrongPassword!2026', passwordHash), false);
assert.equal(await verifyPassword('anything', 'invalid-hash'), false);
console.log('PASS: M08-D admin password login, rotating refresh, session cleanup and credential setup are present.');