fix architecture and write new tests
This commit is contained in:
Executable → Regular
Executable → Regular
+13
-6
@@ -53,13 +53,15 @@ read_env_value() {
|
||||
}
|
||||
|
||||
validate_prod_env_file() {
|
||||
local api_domain public_site_domain cors_origins pgmanage_domain portainer_domain
|
||||
local api_domain public_site_domain cors_origins portainer_domain postgres_password redis_password jwt_secret
|
||||
|
||||
api_domain="$(read_env_value API_DOMAIN)"
|
||||
public_site_domain="$(read_env_value PUBLIC_SITE_DOMAIN)"
|
||||
cors_origins="$(read_env_value CORS_ORIGINS)"
|
||||
pgmanage_domain="$(read_env_value PGMANAGE_DOMAIN)"
|
||||
portainer_domain="$(read_env_value PORTAINER_DOMAIN)"
|
||||
postgres_password="$(read_env_value POSTGRES_PASSWORD)"
|
||||
redis_password="$(read_env_value REDIS_PASSWORD)"
|
||||
jwt_secret="$(read_env_value JWT_SECRET)"
|
||||
|
||||
if [[ -z "${api_domain}" || "${api_domain}" == *example.com* ]]; then
|
||||
echo "Invalid API_DOMAIN in ${ENV_FILE}. Set it to the real production API hostname, not example.com." >&2
|
||||
@@ -76,10 +78,15 @@ validate_prod_env_file() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${pgmanage_domain}" && "${pgmanage_domain}" == *example.com* ]]; then
|
||||
echo "Invalid PGMANAGE_DOMAIN in ${ENV_FILE}. Set a real hostname or disable the pgmanage router." >&2
|
||||
exit 1
|
||||
fi
|
||||
for pair in "POSTGRES_PASSWORD:${postgres_password}" "REDIS_PASSWORD:${redis_password}" "JWT_SECRET:${jwt_secret}"; do
|
||||
key="${pair%%:*}"
|
||||
value="${pair#*:}"
|
||||
if [[ -z "${value}" || "${value}" == replace-with-* || "${value}" == *changeme* || "${value}" == *change-me* ]]; then
|
||||
echo "Invalid ${key} in ${ENV_FILE}. Set a real production secret." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
if [[ -n "${portainer_domain}" && "${portainer_domain}" == *example.com* ]]; then
|
||||
echo "Invalid PORTAINER_DOMAIN in ${ENV_FILE}. Set a real hostname or disable the portainer router." >&2
|
||||
|
||||
@@ -25,7 +25,7 @@ echo "Running database migrations"
|
||||
run_prod_migrations
|
||||
|
||||
echo "Starting application services"
|
||||
prod_compose up -d api marketplace dashboard admin pgmanage
|
||||
prod_compose up -d api marketplace dashboard admin
|
||||
wait_for_healthy api 180
|
||||
wait_for_healthy marketplace 180
|
||||
wait_for_healthy dashboard 180
|
||||
|
||||
@@ -91,11 +91,12 @@ prod_compose run --rm migrate
|
||||
|
||||
echo "Starting application services"
|
||||
app_services=(api marketplace dashboard admin)
|
||||
if [[ "${INCLUDE_PGMANAGE}" == "1" ]]; then
|
||||
app_services+=(pgmanage)
|
||||
fi
|
||||
|
||||
prod_compose up -d "${app_services[@]}"
|
||||
if [[ "${INCLUDE_PGMANAGE}" == "1" ]]; then
|
||||
prod_compose --profile private-tools up -d "${app_services[@]}" pgmanage
|
||||
else
|
||||
prod_compose up -d "${app_services[@]}"
|
||||
fi
|
||||
wait_for_healthy api 180
|
||||
wait_for_healthy marketplace 180
|
||||
wait_for_healthy dashboard 180
|
||||
|
||||
Executable → Regular
Executable → Regular
+2
-2
@@ -6,10 +6,10 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||
|
||||
start_traefik
|
||||
start_prod_services postgres redis api marketplace dashboard admin pgmanage
|
||||
start_prod_services postgres redis api marketplace dashboard admin
|
||||
wait_for_healthy api
|
||||
wait_for_healthy marketplace
|
||||
wait_for_healthy dashboard
|
||||
wait_for_healthy admin
|
||||
|
||||
echo "Production stack is up and healthy: traefik, postgres, redis, api, marketplace, dashboard, admin, pgmanage"
|
||||
echo "Production stack is up and healthy: traefik, postgres, redis, api, marketplace, dashboard, admin"
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+13
-2
@@ -5,6 +5,17 @@ set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source "${ROOT_DIR}/scripts/docker-prod-common.sh"
|
||||
|
||||
start_prod_services pgmanage
|
||||
ensure_env_file
|
||||
|
||||
echo "pgManage is starting."
|
||||
pgmanage_user="$(read_env_value PGMANAGE_DEFAULT_USERNAME)"
|
||||
pgmanage_password="$(read_env_value PGMANAGE_DEFAULT_PASSWORD)"
|
||||
if [[ -z "${pgmanage_user}" || -z "${pgmanage_password}" || "${pgmanage_user}" == "admin" || "${pgmanage_password}" == replace-with-* ]]; then
|
||||
echo "Set strong PGMANAGE_DEFAULT_USERNAME and PGMANAGE_DEFAULT_PASSWORD before starting pgManage." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting pgManage on the private Docker network only."
|
||||
echo "Access it with an SSH tunnel or VPN path to the Docker host, not through public Traefik."
|
||||
prod_compose --profile private-tools up --build -d pgmanage
|
||||
|
||||
echo "pgManage is starting as a private tool."
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
@@ -0,0 +1,74 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const root = process.cwd()
|
||||
const ignoredDirs = new Set(['.git', 'node_modules', 'dist', '.next', 'coverage', '.turbo'])
|
||||
const files = []
|
||||
|
||||
function walk(dir) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (ignoredDirs.has(entry.name)) continue
|
||||
const p = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) walk(p)
|
||||
else files.push(p)
|
||||
}
|
||||
}
|
||||
|
||||
walk(root)
|
||||
|
||||
const findings = []
|
||||
const authTokenStorage = /localStorage\.(?:setItem|getItem|removeItem)\(\s*(?:['"](?:employee_token|admin_token|renter_token|employee_session|admin_session|renter_session)['"]|[A-Z0-9_]*TOKEN[A-Z0-9_]*)|document\.cookie\s*=\s*['"](?:employee_token|admin_token|renter_token|employee_session|admin_session|renter_session)=/i
|
||||
const rawWebhookStringify = /JSON\.stringify\(req\.body\)/i
|
||||
const secretAssignment = /^(JWT_SECRET|DATABASE_URL|POSTGRES_PASSWORD|SMTP_PASSWORD|REGISTRY_PASSWORD|REGISTRY_HTTP_SECRET|AMANPAY_WEBHOOK_SECRET|PAYPAL_WEBHOOK_SECRET|CLERK_SECRET_KEY|RESEND_API_KEY|MAIL_PASSWORD|FIREBASE_PRIVATE_KEY|TWILIO_AUTH_TOKEN|PAYPAL_CLIENT_SECRET|AMANPAY_SECRET_KEY|CLOUDINARY_API_SECRET|ADMIN_SEED_PASSWORD)\s*=\s*(.+)$/i
|
||||
|
||||
function isTestPath(rel) {
|
||||
return rel.includes('/test/') || rel.includes('/tests/') || /\.(test|spec)\.[tj]sx?$/.test(rel)
|
||||
}
|
||||
|
||||
function isEnvLike(rel) {
|
||||
const base = path.basename(rel)
|
||||
return base === '.env' || base.startsWith('.env') || base.endsWith('.env')
|
||||
}
|
||||
|
||||
function looksPlaceholder(value) {
|
||||
const v = value.trim().replace(/^['"]|['"]$/g, '')
|
||||
if (!v || /^\$\{?[A-Z0-9_]+\}?$/i.test(v)) return true
|
||||
if (/replace-with|placeholder|example|your-|your_|dummy|changeme|change-me|test-secret|localhost|127\.0\.0\.1/i.test(v)) return true
|
||||
if (/^postgresql:\/\/[^:]+:replace-with-/i.test(v)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const rel = path.relative(root, file)
|
||||
if (rel.startsWith('.claude/') || rel === '.gitlab-ci.yml' || isTestPath(rel)) continue
|
||||
if (!/\.(ts|tsx|js|jsx|json|env|example|yml|yaml|toml|md|prisma|sql)$/.test(rel) && !path.basename(rel).startsWith('.env')) continue
|
||||
|
||||
let text
|
||||
try { text = fs.readFileSync(file, 'utf8') } catch { continue }
|
||||
if (text.includes('\u0000')) continue
|
||||
const lines = text.split(/\r?\n/)
|
||||
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const line = lines[i]
|
||||
if (authTokenStorage.test(line)) {
|
||||
findings.push(`${rel}:${i + 1}: script-readable auth token storage`)
|
||||
}
|
||||
if (rel.includes('/webhooks/') || rel.includes('payment.routes') || rel.includes('subscription.routes')) {
|
||||
if (rawWebhookStringify.test(line)) findings.push(`${rel}:${i + 1}: webhook signature code must use raw body`)
|
||||
}
|
||||
if (isEnvLike(rel)) {
|
||||
const match = line.match(secretAssignment)
|
||||
if (match && !looksPlaceholder(match[2])) {
|
||||
findings.push(`${rel}:${i + 1}: possible committed secret`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (findings.length) {
|
||||
console.error('Security static check failed:')
|
||||
for (const finding of findings) console.error(`- ${finding}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('Security static check passed')
|
||||
Executable → Regular
Reference in New Issue
Block a user