Frontend: - Add runtime configuration service for deployment-time API URL injection - Create docker-entrypoint.sh to generate config.json from environment variables - Update ApiService, ApprovalService, and DocumentViewer to use RuntimeConfigService - Add APP_INITIALIZER to load runtime config before app starts Backend: - Fix init-blockchain.js to properly quote mnemonic phrases in .env file - Improve docker-entrypoint.sh with health checks and better error handling Docker: - Add API_BASE_URL environment variable to frontend container - Update docker-compose.yml with clear documentation for remote deployment - Reorganize .env.example with clear categories (REQUIRED FOR REMOTE, PRODUCTION, AUTO-GENERATED) Workflow fixes: - Fix DepartmentApproval interface to match backend schema - Fix stage transformation for 0-indexed stageOrder - Fix workflow list to show correct stage count from definition.stages Cleanup: - Move development artifacts to .trash directory - Remove root-level package.json (was only for utility scripts) - Add .trash/ to .gitignore
78 lines
2.4 KiB
TypeScript
78 lines
2.4 KiB
TypeScript
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
|
|
import * as bcrypt from 'bcrypt';
|
|
import * as crypto from 'crypto';
|
|
|
|
export async function hash(data: string): Promise<string> {
|
|
return bcrypt.hash(data, 10);
|
|
}
|
|
|
|
export async function generateApiKey(): Promise<{
|
|
apiKey: string;
|
|
apiSecret: string;
|
|
apiKeyHash: string;
|
|
apiSecretHash: string;
|
|
}> {
|
|
const apiKey = `goa_${crypto.randomBytes(16).toString('hex')}`;
|
|
const apiSecret = crypto.randomBytes(32).toString('hex');
|
|
|
|
const [apiKeyHash, apiSecretHash] = await Promise.all([hash(apiKey), hash(apiSecret)]);
|
|
|
|
return {
|
|
apiKey,
|
|
apiSecret,
|
|
apiKeyHash,
|
|
apiSecretHash,
|
|
};
|
|
}
|
|
|
|
export class CryptoUtil {
|
|
private static readonly ALGORITHM = 'aes-256-gcm';
|
|
private static readonly SALT_LENGTH = 16;
|
|
private static readonly TAG_LENGTH = 16;
|
|
private static readonly IV_LENGTH = 16;
|
|
|
|
static encrypt(data: string, password: string): string {
|
|
const salt = randomBytes(CryptoUtil.SALT_LENGTH);
|
|
const key = scryptSync(password, salt, 32);
|
|
const iv = randomBytes(CryptoUtil.IV_LENGTH);
|
|
|
|
const cipher = createCipheriv(CryptoUtil.ALGORITHM, key, iv);
|
|
const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
|
|
|
|
const authTag = cipher.getAuthTag();
|
|
|
|
return Buffer.concat([salt, iv, authTag, encrypted]).toString('hex');
|
|
}
|
|
|
|
static decrypt(encryptedData: string, password: string): string {
|
|
const buffer = Buffer.from(encryptedData, 'hex');
|
|
|
|
const salt = buffer.subarray(0, CryptoUtil.SALT_LENGTH);
|
|
const iv = buffer.subarray(
|
|
CryptoUtil.SALT_LENGTH,
|
|
CryptoUtil.SALT_LENGTH + CryptoUtil.IV_LENGTH,
|
|
);
|
|
const authTag = buffer.subarray(
|
|
CryptoUtil.SALT_LENGTH + CryptoUtil.IV_LENGTH,
|
|
CryptoUtil.SALT_LENGTH + CryptoUtil.IV_LENGTH + CryptoUtil.TAG_LENGTH,
|
|
);
|
|
const encrypted = buffer.subarray(
|
|
CryptoUtil.SALT_LENGTH + CryptoUtil.IV_LENGTH + CryptoUtil.TAG_LENGTH,
|
|
);
|
|
|
|
const key = scryptSync(password, salt, 32);
|
|
const decipher = createDecipheriv(CryptoUtil.ALGORITHM, key, iv);
|
|
decipher.setAuthTag(authTag);
|
|
|
|
return decipher.update(encrypted) + decipher.final('utf8');
|
|
}
|
|
|
|
static generateKey(length: number = 32): string {
|
|
return randomBytes(length).toString('hex');
|
|
}
|
|
|
|
static generateIV(length: number = 16): string {
|
|
return randomBytes(length).toString('hex');
|
|
}
|
|
}
|