File size: 4,410 Bytes
6e13570
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4c6b1e8
 
 
 
6e13570
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4c6b1e8
 
 
 
 
 
 
 
 
 
 
 
 
6e13570
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
require("dotenv").config();

// Fail fast if critical secrets are missing or left as placeholders — this
// is a cheap, high-value guard against accidentally deploying with
// insecure defaults.
const REQUIRED_IN_PRODUCTION = [
  "MONGODB_URI",
  "JWT_ACCESS_SECRET",
  "JWT_REFRESH_SECRET",
  "FIELD_ENCRYPTION_KEY",
  "SMTP_HOST",
  "SMTP_USER",
  "SMTP_PASS",
  "CLOUDINARY_CLOUD_NAME",
  "CLOUDINARY_API_KEY",
  "CLOUDINARY_API_SECRET",
];

const env = {
  nodeEnv: process.env.NODE_ENV || "development",
  port: parseInt(process.env.PORT, 10) || 5000,
  clientOrigins: (process.env.CLIENT_ORIGINS || "http://localhost:5173")
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean),
  // Base URL used to build absolute redirect links (e.g. Payoneer
  // Checkout success/cancel URLs) sent to third-party APIs — falls
  // back to the first allowed CORS origin if not set explicitly.
  frontendUrl: process.env.FRONTEND_URL || (process.env.CLIENT_ORIGINS || "http://localhost:5173").split(",")[0].trim(),
  mongoUri: process.env.MONGODB_URI,

  jwtAccessSecret: process.env.JWT_ACCESS_SECRET,
  jwtRefreshSecret: process.env.JWT_REFRESH_SECRET,
  jwtAccessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN || "15m",
  jwtRefreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || "7d",

  fieldEncryptionKey: process.env.FIELD_ENCRYPTION_KEY,

  smtp: {
    host: process.env.SMTP_HOST,
    port: parseInt(process.env.SMTP_PORT, 10) || 587,
    secure: process.env.SMTP_SECURE === "true",
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS,
  },
  emailFrom: process.env.EMAIL_FROM || "Aynwerd Academy <no-reply@aynwerd.com>",
  adminNotifyEmail: process.env.ADMIN_NOTIFY_EMAIL,

  bank: {
    name: process.env.BANK_NAME || "",
    accountName: process.env.BANK_ACCOUNT_NAME || "",
    accountNumber: process.env.BANK_ACCOUNT_NUMBER || "",
    routingNumber: process.env.BANK_ROUTING_NUMBER || "",
    swiftBic: process.env.BANK_SWIFT_BIC || "",
    iban: process.env.BANK_IBAN || "",
    address: process.env.BANK_ADDRESS || "",
  },
  payoneer: {
    paymentUrl: process.env.PAYONEER_PAYMENT_URL || "https://www.payoneer.com/",
    accountEmail: process.env.PAYONEER_ACCOUNT_EMAIL || "",
  },

  // Payoneer Checkout (automated card/wallet checkout) — separate from
  // the manual "payoneer" bank-style transfer above. These credentials
  // are issued directly by Payoneer only after your business is
  // approved as a Payoneer Checkout merchant; see
  // utils/payoneerCheckout.js for what's still a placeholder pending
  // that approval and your integration-specific API docs from them.
  payoneerCheckout: {
    apiBase: process.env.PAYONEER_CHECKOUT_API_BASE || "",
    apiKey: process.env.PAYONEER_CHECKOUT_API_KEY || "",
    apiSecret: process.env.PAYONEER_CHECKOUT_API_SECRET || "",
    webhookSecret: process.env.PAYONEER_CHECKOUT_WEBHOOK_SECRET || "",
  },

  seedAdmin: {
    name: process.env.SEED_ADMIN_NAME || "Site Admin",
    email: process.env.SEED_ADMIN_EMAIL,
    password: process.env.SEED_ADMIN_PASSWORD,
  },

  // Cloudinary: free-tier media storage used exclusively for admin-uploaded
  // course videos/documents. Files are uploaded with delivery type
  // "authenticated" so nothing is reachable without a freshly signed,
  // short-lived URL minted server-side for a verified, paying learner.
  cloudinary: {
    cloudName: process.env.CLOUDINARY_CLOUD_NAME,
    apiKey: process.env.CLOUDINARY_API_KEY,
    apiSecret: process.env.CLOUDINARY_API_SECRET,
  },
};

function assertProductionSafety() {
  if (env.nodeEnv !== "production") return;

  const missing = REQUIRED_IN_PRODUCTION.filter((key) => {
    const value = process.env[key];
    return !value || value.startsWith("replace_with_") || value.includes("<");
  });

  if (missing.length > 0) {
    // eslint-disable-next-line no-console
    console.error(
      `FATAL: refusing to start in production with missing/placeholder env vars: ${missing.join(", ")}`
    );
    process.exit(1);
  }

  if (Buffer.from(env.fieldEncryptionKey || "", "hex").length !== 32) {
    // eslint-disable-next-line no-console
    console.error("FATAL: FIELD_ENCRYPTION_KEY must be a 32-byte (64 hex character) value.");
    process.exit(1);
  }
}

module.exports = { env, assertProductionSafety };