89 lines
4.7 KiB
JavaScript
89 lines
4.7 KiB
JavaScript
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.ok(login.json().data.access.menus.includes('products'));
|
|
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.');
|