Coverage for src/couchers/config.py: 52%
46 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-07-02 02:47 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-07-02 02:47 +0000
1"""
2A simple config system
3"""
5import os
7# Allowed config options, as tuples (name, type, default).
8# All fields are required
9CONFIG_OPTIONS = [
10 # Whether we're in dev mode
11 ("DEV", bool),
12 # Whether we're `api` mode (answering API queries) or `scheduler` (scheduling background jobs), or `worker`
13 # (servicing background jobs). Can also be set to `all` to do all three simultaneously
14 ("ROLE", ["api", "scheduler", "worker", "all"], "all"),
15 # number of bg worker processes, requires worker or all above
16 ("BACKGROUND_WORKER_COUNT", int, 2),
17 # Version string
18 ("VERSION", str, "unknown"),
19 # Base URL of frontend, e.g. https://couchers.org
20 ("BASE_URL", str),
21 # URL of the backend, e.g. https://api.couchers.org
22 ("BACKEND_BASE_URL", str),
23 # URL of the console, e.g. https://console.couchers.org
24 ("CONSOLE_BASE_URL", str),
25 # Used to generate a variety of secrets
26 ("SECRET", bytes),
27 # Domain that cookies should set as their domain value
28 ("COOKIE_DOMAIN", str),
29 # SQLAlchemy database connection string
30 ("DATABASE_CONNECTION_STRING", str),
31 # OpenTelemetry endpoint to send traces to
32 ("OPENTELEMETRY_ENDPOINT", str, ""),
33 # Path to a GeoLite2-City.mmdb file for geocoding IPs in user session info
34 ("GEOLITE2_CITY_MMDB_FILE_LOCATION", str, ""),
35 ("GEOLITE2_ASN_MMDB_FILE_LOCATION", str, ""),
36 # Whether to try adding dummy data
37 ("ADD_DUMMY_DATA", bool),
38 # Donations
39 ("ENABLE_DONATIONS", bool),
40 ("STRIPE_API_KEY", str),
41 ("STRIPE_WEBHOOK_SECRET", str),
42 ("STRIPE_RECURRING_PRODUCT_ID", str),
43 # Strong verification through Iris ID
44 ("ENABLE_STRONG_VERIFICATION", bool),
45 ("IRIS_ID_PUBKEY", str),
46 ("IRIS_ID_SECRET", str),
47 ("VERIFICATION_DATA_PUBLIC_KEY", bytes),
48 # SMS
49 ("ENABLE_SMS", bool),
50 ("SMS_SENDER_ID", str),
51 # Email
52 ("ENABLE_EMAIL", bool),
53 # Sender name for outgoing notification emails e.g. "Couchers.org"
54 ("NOTIFICATION_EMAIL_SENDER", str),
55 # Sender email, e.g. "notify@couchers.org"
56 ("NOTIFICATION_EMAIL_ADDRESS", str),
57 # An optional prefix for email subject, e.g. [STAGING]
58 ("NOTIFICATION_PREFIX", str, ""),
59 # Address to send emails about reported users
60 ("REPORTS_EMAIL_RECIPIENT", str),
61 # Address to send contributor forms when users sign up/fill the form
62 ("CONTRIBUTOR_FORM_EMAIL_RECIPIENT", str),
63 # Address to moderation notifications
64 ("MODS_EMAIL_RECIPIENT", str),
65 # SMTP settings
66 ("SMTP_HOST", str),
67 ("SMTP_PORT", int),
68 ("SMTP_USERNAME", str),
69 ("SMTP_PASSWORD", str),
70 # Media server
71 ("ENABLE_MEDIA", bool),
72 ("MEDIA_SERVER_SECRET_KEY", bytes),
73 ("MEDIA_SERVER_BEARER_TOKEN", str),
74 ("MEDIA_SERVER_BASE_URL", str),
75 ("MEDIA_SERVER_UPLOAD_BASE_URL", str),
76 # Bug reporting tool
77 ("BUG_TOOL_ENABLED", bool),
78 ("BUG_TOOL_GITHUB_REPO", str),
79 ("BUG_TOOL_GITHUB_USERNAME", str),
80 ("BUG_TOOL_GITHUB_TOKEN", str),
81 # Sentry
82 ("SENTRY_ENABLED", bool),
83 ("SENTRY_URL", str),
84 # Push notifications
85 ("PUSH_NOTIFICATIONS_ENABLED", bool),
86 ("PUSH_NOTIFICATIONS_VAPID_PRIVATE_KEY", str),
87 ("PUSH_NOTIFICATIONS_VAPID_SUBJECT", str),
88 # Whether to initiate new activeness probes
89 ("ACTIVENESS_PROBES_ENABLED", bool),
90 # Listmonk (mailing list)
91 ("LISTMONK_ENABLED", bool),
92 ("LISTMONK_BASE_URL", str),
93 ("LISTMONK_API_USERNAME", str),
94 ("LISTMONK_API_KEY", str),
95 ("LISTMONK_LIST_ID", int),
96 # Google recaptcha antibot
97 ("RECAPTHCA_ENABLED", bool),
98 ("RECAPTHCA_PROJECT_ID", str),
99 ("RECAPTHCA_API_KEY", str),
100 ("RECAPTHCA_SITE_KEY", str),
101 # Whether we're in test
102 ("IN_TEST", bool, "0"),
103]
105config = {}
107for config_option in CONFIG_OPTIONS:
108 if len(config_option) == 2:
109 name, type_ = config_option
110 optional = False
111 elif len(config_option) == 3:
112 name, type_, default_value = config_option
113 optional = True
114 else:
115 raise ValueError("Invalid CONFIG_OPTIONS")
117 value = os.getenv(name)
119 if not value:
120 if not optional:
121 # config value not set - will cause a KeyError when trying
122 # to access it.
123 continue
124 else:
125 value = default_value
127 if type_ is bool:
128 # 1 is true, 0 is false, everything else is illegal
129 if value not in ["0", "1"]:
130 raise ValueError(f'Invalid bool for {name}, need "0" or "1"')
131 value = value == "1"
132 elif type_ is bytes:
133 # decode from hex
134 value = bytes.fromhex(value)
135 elif isinstance(type_, list):
136 # list of allowed string values
137 if value not in type_:
138 raise ValueError(f"Invalid value for {name}, need one of {', '.join(type_)}")
139 else:
140 value = type_(value)
142 config[name] = value
145## Config checks
146def check_config():
147 for name, *_ in CONFIG_OPTIONS:
148 if name not in config:
149 raise ValueError(f"Required config value {name} not set")
151 if not config["DEV"]:
152 # checks for prod
153 if "https" not in config["BASE_URL"]:
154 raise Exception("Production site must be over HTTPS")
155 if not config["ENABLE_EMAIL"]:
156 raise Exception("Production site must have email enabled")
157 if not config["ENABLE_SMS"]:
158 raise Exception("Production site must have SMS enabled")
159 if config["IN_TEST"]:
160 raise Exception("IN_TEST while not DEV")
162 if config["ENABLE_DONATIONS"]:
163 if (
164 not config["STRIPE_API_KEY"]
165 or not config["STRIPE_WEBHOOK_SECRET"]
166 or not config["STRIPE_RECURRING_PRODUCT_ID"]
167 ):
168 raise Exception("No Stripe API key/recurring donation ID but donations enabled")
170 if config["ENABLE_STRONG_VERIFICATION"]:
171 if not config["IRIS_ID_PUBKEY"] or not config["IRIS_ID_SECRET"] or not config["VERIFICATION_DATA_PUBLIC_KEY"]:
172 raise Exception("No Iris ID pubkey/secret or verification data pubkey but strong verification enabled")