new version
This commit is contained in:
@@ -1,55 +1,158 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { fetchVaultSecrets } from './vault';
|
||||
import { vaultAppRoleLogin, fetchVaultKV2 } from './vault';
|
||||
import { logger } from '../lib/logger';
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../../../.env') });
|
||||
|
||||
const p = process.env;
|
||||
|
||||
export let env = {
|
||||
db: {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
name: process.env.DB_NAME || 'cryptowallet',
|
||||
host: p.DB_HOST || 'localhost',
|
||||
port: parseInt(p.DB_PORT || '5432'),
|
||||
user: p.DB_USER || 'postgres',
|
||||
password: p.DB_PASSWORD || 'postgres',
|
||||
name: p.DB_NAME || 'cryptowallet_v2',
|
||||
poolSize: parseInt(p.DATABASE_POOL_SIZE || '10'),
|
||||
maxOverflow: parseInt(p.DATABASE_MAX_OVERFLOW || '20'),
|
||||
poolTimeout: parseInt(p.DATABASE_POOL_TIMEOUT || '30'),
|
||||
poolRecycle: parseInt(p.DATABASE_POOL_RECYCLE || '3600'),
|
||||
echo: p.DATABASE_ECHO === 'true',
|
||||
},
|
||||
port: parseInt(process.env.API_PORT || '3001'),
|
||||
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3000',
|
||||
relayApiKey: process.env.RELAY_API_KEY || null,
|
||||
tronApiKey: process.env.TRON_API_KEY || null,
|
||||
jupiterApiKey: process.env.JUPITER_API_KEY || null,
|
||||
jupiterReferralAccount: process.env.JUPITER_REFERRAL_ACCOUNT || null,
|
||||
jupiterFeeBps: parseInt(process.env.JUPITER_FEE_BPS || '70'), // 0.7%
|
||||
|
||||
// BITOK auth service
|
||||
bitokJwksUrl: process.env.BITOK_JWKS_URL || 'http://localhost:8000/.well-known/jwks.json',
|
||||
bitokIssuer: process.env.BITOK_ISSUER || 'auth-service',
|
||||
bitokAudience: process.env.BITOK_AUDIENCE || 'wallet-service',
|
||||
|
||||
// RabbitMQ
|
||||
rabbitmqUrl: process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:5672/',
|
||||
rabbitmqExchange: process.env.RABBITMQ_EXCHANGE || 'bitok.events',
|
||||
rabbitmqWalletQueue: process.env.RABBITMQ_WALLET_QUEUE || 'wallet.user_events',
|
||||
jwt: {
|
||||
algorithm: p.JWT_ALGORITHM || 'RS256',
|
||||
issuer: p.JWT_ISSUER || 'auth-service',
|
||||
audience: p.JWT_AUDIENCE || 'bitforce',
|
||||
accessTtl: parseInt(p.JWT_ACCESS_TTL_SECONDS || '900'),
|
||||
refreshTtl: parseInt(p.JWT_REFRESH_TTL_SECONDS || '2592000'),
|
||||
},
|
||||
vault: {
|
||||
addr: p.VAULT_ADDR || '',
|
||||
roleId: p.VAULT_ROLE_ID || '',
|
||||
secretId: p.VAULT_SECRET_ID || '',
|
||||
mount: p.VAULT_MOUNT_POINT || 'dev-secrets',
|
||||
secretPath: p.VAULT_SECRET_PATH || 'database',
|
||||
jwtKidPath: p.VAULT_JWT_KID_PATH || 'jwt/kid',
|
||||
jwtKidsPrefix: p.VAULT_JWT_KIDS_PREFIX || 'jwt/kids',
|
||||
},
|
||||
csrf: {
|
||||
cookieSecure: p.CSRF_COOKIE_SECURE === 'true',
|
||||
cookieHttpOnly: p.CSRF_COOKIE_HTTPONLY !== 'false',
|
||||
cookieSameSite: p.CSRF_COOKIE_SAMESITE || 'Lax',
|
||||
cookiePath: p.CSRF_COOKIE_PATH || '/',
|
||||
cookieDomain: p.CSRF_COOKIE_DOMAIN || '',
|
||||
},
|
||||
docs: {
|
||||
username: p.DOCS_USERNAME || 'admin',
|
||||
password: p.DOCS_PASSWORD || 'admin',
|
||||
},
|
||||
redis: {
|
||||
host: p.REDIS_HOST || 'keydb',
|
||||
port: parseInt(p.REDIS_PORT || '6379'),
|
||||
password: p.REDIS_PASSWORD || 'keydb',
|
||||
db: parseInt(p.REDIS_DB || '0'),
|
||||
},
|
||||
rabbit: {
|
||||
emailCodeQueue: p.RABBIT_EMAIL_CODE_QUEUE || 'email.verification_code',
|
||||
publishPersist: p.RABBIT_PUBLISH_PERSIST !== 'false',
|
||||
connectTimeout: parseInt(p.RABBIT_CONNECT_TIMEOUT || '5'),
|
||||
},
|
||||
log: {
|
||||
level: p.LOG_LEVEL || 'INFO',
|
||||
format: p.LOG_FORMAT || 'JSON',
|
||||
},
|
||||
cors: {
|
||||
origins: (p.CORS_ORIGINS || 'http://localhost:3000').split(','),
|
||||
allowCredentials: p.CORS_ALLOW_CREDENTIALS !== 'false',
|
||||
},
|
||||
rateLimit: {
|
||||
requests: parseInt(p.RATE_LIMIT_REQUESTS || '60'),
|
||||
window: parseInt(p.RATE_LIMIT_WINDOW || '60'),
|
||||
},
|
||||
port: parseInt(p.API_PORT || '3001'),
|
||||
frontendUrl: p.FRONTEND_URL || 'http://localhost:3000',
|
||||
relayApiKey: p.RELAY_API_KEY || null,
|
||||
tronApiKey: p.TRON_API_KEY || null,
|
||||
jupiterApiKey: p.JUPITER_API_KEY || null,
|
||||
jupiterReferralAccount: p.JUPITER_REFERRAL_ACCOUNT || null,
|
||||
jupiterFeeBps: parseInt(p.JUPITER_FEE_BPS || '70'),
|
||||
};
|
||||
|
||||
export async function initEnv(): Promise<void> {
|
||||
const secrets = await fetchVaultSecrets();
|
||||
let vaultToken: string | null = null;
|
||||
|
||||
if (secrets) {
|
||||
console.log('[ENV] Loaded secrets from Vault');
|
||||
env = {
|
||||
...env,
|
||||
db: {
|
||||
host: secrets.db_host,
|
||||
port: parseInt(secrets.db_port),
|
||||
user: secrets.db_user,
|
||||
password: secrets.db_password,
|
||||
name: secrets.db_name,
|
||||
},
|
||||
relayApiKey: secrets.relay_api_key || null,
|
||||
tronApiKey: secrets.tron_api_key || env.tronApiKey,
|
||||
jupiterApiKey: secrets.jupiter_api_key || env.jupiterApiKey,
|
||||
};
|
||||
} else {
|
||||
console.log('[ENV] Vault not available, using env vars');
|
||||
}
|
||||
export function getVaultToken(): string | null {
|
||||
return vaultToken;
|
||||
}
|
||||
|
||||
export async function initEnv(): Promise<void> {
|
||||
const { addr, roleId, secretId, mount, secretPath } = env.vault;
|
||||
|
||||
if (!addr || !roleId || !secretId) {
|
||||
logger.info('Vault not configured, using .env');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await vaultAppRoleLogin(addr, roleId, secretId);
|
||||
if (!token) {
|
||||
logger.warn('Vault AppRole login failed, using .env fallback');
|
||||
return;
|
||||
}
|
||||
|
||||
vaultToken = token;
|
||||
logger.info('Vault AppRole login successful');
|
||||
|
||||
const secrets = await fetchVaultKV2(addr, token, mount, secretPath);
|
||||
if (!secrets) {
|
||||
logger.warn('Failed to read DB secrets from Vault');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Loaded DB secrets from Vault');
|
||||
|
||||
const s = (key: string) => secrets[key];
|
||||
const si = (key: string, fallback: number) => {
|
||||
const v = secrets[key];
|
||||
return v ? parseInt(v) : fallback;
|
||||
};
|
||||
|
||||
env = {
|
||||
...env,
|
||||
db: {
|
||||
host: s('DB_HOST') || env.db.host,
|
||||
port: si('DB_PORT', env.db.port),
|
||||
user: s('DB_USER') || env.db.user,
|
||||
password: s('DB_PASSWORD') || env.db.password,
|
||||
name: s('DB_NAME') || env.db.name,
|
||||
poolSize: si('DATABASE_POOL_SIZE', env.db.poolSize),
|
||||
maxOverflow: si('DATABASE_MAX_OVERFLOW', env.db.maxOverflow),
|
||||
poolTimeout: si('DATABASE_POOL_TIMEOUT', env.db.poolTimeout),
|
||||
poolRecycle: si('DATABASE_POOL_RECYCLE', env.db.poolRecycle),
|
||||
echo: secrets['DATABASE_ECHO'] === 'true',
|
||||
},
|
||||
jwt: {
|
||||
...env.jwt,
|
||||
issuer: s('JWT_ISSUER') || env.jwt.issuer,
|
||||
audience: s('JWT_AUDIENCE') || env.jwt.audience,
|
||||
accessTtl: si('JWT_ACCESS_TTL_SECONDS', env.jwt.accessTtl),
|
||||
refreshTtl: si('JWT_REFRESH_TTL_SECONDS', env.jwt.refreshTtl),
|
||||
},
|
||||
redis: {
|
||||
host: s('REDIS_HOST') || env.redis.host,
|
||||
port: si('REDIS_PORT', env.redis.port),
|
||||
password: s('REDIS_PASSWORD') || env.redis.password,
|
||||
db: si('REDIS_DB', env.redis.db),
|
||||
},
|
||||
cors: {
|
||||
origins: s('CORS_ORIGINS') ? s('CORS_ORIGINS')!.split(',') : env.cors.origins,
|
||||
allowCredentials: secrets['CORS_ALLOW_CREDENTIALS'] !== 'false',
|
||||
},
|
||||
rateLimit: {
|
||||
requests: si('RATE_LIMIT_REQUESTS', env.rateLimit.requests),
|
||||
window: si('RATE_LIMIT_WINDOW', env.rateLimit.window),
|
||||
},
|
||||
relayApiKey: s('RELAY_API_KEY') || env.relayApiKey,
|
||||
tronApiKey: s('TRON_API_KEY') || env.tronApiKey,
|
||||
jupiterApiKey: s('JUPITER_API_KEY') || env.jupiterApiKey,
|
||||
};
|
||||
}
|
||||
|
||||
5
apps/api/src/config/swagger.ts
Normal file
5
apps/api/src/config/swagger.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const swaggerPath = path.resolve(__dirname, '../../swagger.json');
|
||||
export const swaggerSpec = JSON.parse(fs.readFileSync(swaggerPath, 'utf-8'));
|
||||
@@ -1,29 +1,42 @@
|
||||
interface VaultSecrets {
|
||||
db_host: string;
|
||||
db_port: string;
|
||||
db_user: string;
|
||||
db_password: string;
|
||||
db_name: string;
|
||||
relay_api_key: string;
|
||||
tron_api_key: string;
|
||||
jupiter_api_key: string;
|
||||
}
|
||||
|
||||
export async function fetchVaultSecrets(): Promise<VaultSecrets | null> {
|
||||
const vaultAddr = process.env.VAULT_ADDR;
|
||||
const vaultToken = process.env.VAULT_TOKEN;
|
||||
|
||||
if (!vaultAddr || !vaultToken) return null;
|
||||
|
||||
export async function vaultAppRoleLogin(
|
||||
addr: string,
|
||||
roleId: string,
|
||||
secretId: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch(`${vaultAddr}/v1/kv/data/cryptowallet`, {
|
||||
headers: { 'X-Vault-Token': vaultToken },
|
||||
const res = await fetch(`${addr}/v1/auth/approle/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role_id: roleId, secret_id: secretId }),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
const body = (await res.json()) as { data: { data: VaultSecrets } };
|
||||
return body.data.data;
|
||||
const body = (await res.json()) as { auth?: { client_token?: string } };
|
||||
return body?.auth?.client_token ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchVaultKV2(
|
||||
addr: string,
|
||||
token: string,
|
||||
mount: string,
|
||||
path: string,
|
||||
): Promise<Record<string, string> | null> {
|
||||
try {
|
||||
const url = `${addr}/v1/${mount}/data/${path}`;
|
||||
const res = await fetch(url, {
|
||||
headers: { 'X-Vault-Token': token },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
const body = (await res.json()) as { data?: { data?: Record<string, string> } };
|
||||
return body?.data?.data ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user