fix: enforce strong hashes in maintenance tools
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { createHash, randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { writeFileSync } from 'node:fs';
|
import { writeFileSync } from 'node:fs';
|
||||||
import { PrismaPg } from '../../api/node_modules/@prisma/adapter-pg/dist/index.js';
|
import { PrismaPg } from '../../api/node_modules/@prisma/adapter-pg/dist/index.js';
|
||||||
import { PrismaClient } from '../../api/node_modules/@prisma/client/index.js';
|
import { PrismaClient } from '../../api/node_modules/@prisma/client/index.js';
|
||||||
@@ -14,16 +14,16 @@ const email = process.env.PROD_ADMIN_EMAIL || 'admin@example.com';
|
|||||||
const configuredPassword = process.env.PROD_ADMIN_PASSWORD;
|
const configuredPassword = process.env.PROD_ADMIN_PASSWORD;
|
||||||
const credentialFile = process.env.PROD_ADMIN_CREDENTIAL_FILE;
|
const credentialFile = process.env.PROD_ADMIN_CREDENTIAL_FILE;
|
||||||
|
|
||||||
function hashPassword(value) {
|
|
||||||
return createHash('sha256').update(value).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
// production-deploy.sh builds the API before invoking this helper, so every
|
||||||
|
// administrative password write shares the same versioned hasher as runtime APIs.
|
||||||
|
const { hashPassword } = await import('../../api/dist/auth/password-hasher.js');
|
||||||
const existingUser = await prisma.user.findFirst({
|
const existingUser = await prisma.user.findFirst({
|
||||||
where: { username, deletedAt: null },
|
where: { username, deletedAt: null },
|
||||||
});
|
});
|
||||||
const createPassword = configuredPassword || randomBytes(18).toString('base64url');
|
const createPassword = configuredPassword || randomBytes(18).toString('base64url');
|
||||||
const updatePassword = configuredPassword;
|
const updatePassword = configuredPassword;
|
||||||
|
const passwordHash = await hashPassword(updatePassword || createPassword);
|
||||||
const role = await prisma.role.upsert({
|
const role = await prisma.role.upsert({
|
||||||
where: { code: 'platform_admin' },
|
where: { code: 'platform_admin' },
|
||||||
update: { name: '平台管理员', scope: 'platform' },
|
update: { name: '平台管理员', scope: 'platform' },
|
||||||
@@ -32,7 +32,7 @@ async function main() {
|
|||||||
const userData = {
|
const userData = {
|
||||||
email,
|
email,
|
||||||
displayName: '生产平台管理员',
|
displayName: '生产平台管理员',
|
||||||
...(updatePassword ? { passwordHash: hashPassword(updatePassword) } : {}),
|
...(updatePassword ? { passwordHash } : {}),
|
||||||
status: 'active',
|
status: 'active',
|
||||||
failedLoginCount: 0,
|
failedLoginCount: 0,
|
||||||
lockedUntil: null,
|
lockedUntil: null,
|
||||||
@@ -49,7 +49,7 @@ async function main() {
|
|||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
displayName: '生产平台管理员',
|
displayName: '生产平台管理员',
|
||||||
passwordHash: hashPassword(createPassword),
|
passwordHash,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,6 +34,18 @@ const authService = readFileSync(resolve(root, 'api/src/auth/auth.service.ts'),
|
|||||||
if (/passwordHash\s*===|===\s*[^\n;]*passwordHash/.test(authService)) {
|
if (/passwordHash\s*===|===\s*[^\n;]*passwordHash/.test(authService)) {
|
||||||
violations.push('api/src/auth/auth.service.ts: password hashes must not be compared directly');
|
violations.push('api/src/auth/auth.service.ts: password hashes must not be compared directly');
|
||||||
}
|
}
|
||||||
|
const ensureAdmin = readFileSync(resolve(root, 'tools/deploy/ensure-production-admin.mjs'), 'utf8');
|
||||||
|
if (/createHash\(['"]sha256['"]\)|function\s+hashPassword\s*\(/.test(ensureAdmin)
|
||||||
|
|| !ensureAdmin.includes("api/dist/auth/password-hasher.js")) {
|
||||||
|
violations.push('tools/deploy/ensure-production-admin.mjs: administrative password writes must use the API password hasher');
|
||||||
|
}
|
||||||
|
for (const relativePath of ['tools/deploy/ensure-production-admin.mjs', 'tools/smoke/real-env-smoke.mjs']) {
|
||||||
|
const content = readFileSync(resolve(root, relativePath), 'utf8');
|
||||||
|
if (/function\s+hashPassword\s*\(|passwordHash:\s*(?:createHash|legacyHashPassword)/.test(content)
|
||||||
|
|| !content.includes("api/dist/auth/password-hasher.js")) {
|
||||||
|
violations.push(`${relativePath}: user password writes must use the compiled API password hasher`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const packageJson = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
|
const packageJson = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
|
||||||
if (packageJson.dependencies?.['react-router-dom'] !== '7.18.2') {
|
if (packageJson.dependencies?.['react-router-dom'] !== '7.18.2') {
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ const email = 'smoke-client@example.com';
|
|||||||
const phone = '13800138099';
|
const phone = '13800138099';
|
||||||
const password = 'SmokePass123!';
|
const password = 'SmokePass123!';
|
||||||
|
|
||||||
function hashPassword(value) {
|
function hashApplicationSecret(value) {
|
||||||
return createHash('sha256').update(value).digest('hex');
|
return createHash('sha256').update(value).digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureSmokeData() {
|
async function ensureSmokeData() {
|
||||||
|
const { hashPassword } = await import('../../api/dist/auth/password-hasher.js');
|
||||||
|
const userPasswordHash = await hashPassword(password);
|
||||||
const tenant = await prisma.tenant.upsert({
|
const tenant = await prisma.tenant.upsert({
|
||||||
where: { code: tenantCode },
|
where: { code: tenantCode },
|
||||||
update: { name: 'Smoke Test Enterprise', status: 'active' },
|
update: { name: 'Smoke Test Enterprise', status: 'active' },
|
||||||
@@ -36,7 +38,7 @@ async function ensureSmokeData() {
|
|||||||
email,
|
email,
|
||||||
phone,
|
phone,
|
||||||
displayName: 'Smoke Client Admin',
|
displayName: 'Smoke Client Admin',
|
||||||
passwordHash: hashPassword(password),
|
passwordHash: userPasswordHash,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
failedLoginCount: 0,
|
failedLoginCount: 0,
|
||||||
lockedUntil: null,
|
lockedUntil: null,
|
||||||
@@ -50,7 +52,7 @@ async function ensureSmokeData() {
|
|||||||
email,
|
email,
|
||||||
phone,
|
phone,
|
||||||
displayName: 'Smoke Client Admin',
|
displayName: 'Smoke Client Admin',
|
||||||
passwordHash: hashPassword(password),
|
passwordHash: userPasswordHash,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -130,7 +132,7 @@ async function ensureSmokeData() {
|
|||||||
tenantId: tenant.id,
|
tenantId: tenant.id,
|
||||||
name: 'Smoke SMS App',
|
name: 'Smoke SMS App',
|
||||||
scene: 'smoke',
|
scene: 'smoke',
|
||||||
secretHash: hashPassword('smoke-secret'),
|
secretHash: hashApplicationSecret('smoke-secret'),
|
||||||
dailyLimit: 100000,
|
dailyLimit: 100000,
|
||||||
customerUnitPrice: 5,
|
customerUnitPrice: 5,
|
||||||
maxPhonesPerTask: 100000,
|
maxPhonesPerTask: 100000,
|
||||||
|
|||||||
Reference in New Issue
Block a user