feat(M08-D): 补后台登录与可撤销会话
This commit is contained in:
@@ -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.');
|
||||
Reference in New Issue
Block a user