Coverage for src/couchers/config.py: 52%
46 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-20 18:03 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-20 18:03 +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 # Version string
16 ("VERSION", str, "unknown"),
17 # Base URL of frontend, e.g. https://couchers.org
18 ("BASE_URL", str),
19 # URL of the backend, e.g. https://api.couchers.org
20 ("BACKEND_BASE_URL", str),
21 # URL of the console, e.g. https://console.couchers.org
22 ("CONSOLE_BASE_URL", str),
23 # Used to generate a variety of secrets
24 ("SECRET", bytes),
25 # Domain that cookies should set as their domain value
26 ("COOKIE_DOMAIN", str),
27 # SQLAlchemy database connection string
28 ("DATABASE_CONNECTION_STRING", str),
29 # OpenTelemetry endpoint to send traces to
30 ("OPENTELEMETRY_ENDPOINT", str, ""),
31 # Path to a GeoLite2-City.mmdb file for geocoding IPs in user session info
32 ("GEOLITE2_CITY_MMDB_FILE_LOCATION", str, ""),
33 # Whether to try adding dummy data
34 ("ADD_DUMMY_DATA", bool),
35 # Donations
36 ("ENABLE_DONATIONS", bool),
37 ("STRIPE_API_KEY", str),
38 ("STRIPE_WEBHOOK_SECRET", str),
39 ("STRIPE_RECURRING_PRODUCT_ID", str),
40 # Strong verification through Iris ID
41 ("ENABLE_STRONG_VERIFICATION", bool),
42 ("IRIS_ID_PUBKEY", str),
43 ("IRIS_ID_SECRET", str),
44 ("VERIFICATION_DATA_PUBLIC_KEY", bytes),
45 # SMS
46 ("ENABLE_SMS", bool),
47 ("SMS_SENDER_ID", str),
48 # Email
49 ("ENABLE_EMAIL", bool),
50 # Sender name for outgoing notification emails e.g. "Couchers.org"
51 ("NOTIFICATION_EMAIL_SENDER", str),
52 # Sender email, e.g. "notify@couchers.org"
53 ("NOTIFICATION_EMAIL_ADDRESS", str),
54 # An optional prefix for email subject, e.g. [STAGING]
55 ("NOTIFICATION_PREFIX", str, ""),
56 # Address to send emails about reported users
57 ("REPORTS_EMAIL_RECIPIENT", str),
58 # Address to send contributor forms when users sign up/fill the form
59 ("CONTRIBUTOR_FORM_EMAIL_RECIPIENT", str),
60 # Address to moderation notifications
61 ("MODS_EMAIL_RECIPIENT", str),
62 # SMTP settings
63 ("SMTP_HOST", str),
64 ("SMTP_PORT", int),
65 ("SMTP_USERNAME", str),
66 ("SMTP_PASSWORD", str),
67 # Media server
68 ("ENABLE_MEDIA", bool),
69 ("MEDIA_SERVER_SECRET_KEY", bytes),
70 ("MEDIA_SERVER_BEARER_TOKEN", str),
71 ("MEDIA_SERVER_BASE_URL", str),
72 ("MEDIA_SERVER_UPLOAD_BASE_URL", str),
73 # Bug reporting tool
74 ("BUG_TOOL_ENABLED", bool),
75 ("BUG_TOOL_GITHUB_REPO", str),
76 ("BUG_TOOL_GITHUB_USERNAME", str),
77 ("BUG_TOOL_GITHUB_TOKEN", str),
78 # Sentry
79 ("SENTRY_ENABLED", bool),
80 ("SENTRY_URL", str),
81 # Push notifications
82 ("PUSH_NOTIFICATIONS_ENABLED", bool),
83 ("PUSH_NOTIFICATIONS_VAPID_PRIVATE_KEY", str),
84 ("PUSH_NOTIFICATIONS_VAPID_SUBJECT", str),
85 # Listmonk (mailing list)
86 ("LISTMONK_ENABLED", bool),
87 ("LISTMONK_BASE_URL", str),
88 ("LISTMONK_API_KEY", str),
89 ("LISTMONK_LIST_UUID", str),
90 # Whether we're in test
91 ("IN_TEST", bool, "0"),
92]
94config = {}
96for config_option in CONFIG_OPTIONS:
97 if len(config_option) == 2:
98 name, type_ = config_option
99 optional = False
100 elif len(config_option) == 3:
101 name, type_, default_value = config_option
102 optional = True
103 else:
104 raise ValueError("Invalid CONFIG_OPTIONS")
106 value = os.getenv(name)
108 if not value:
109 if not optional:
110 # config value not set - will cause a KeyError when trying
111 # to access it.
112 continue
113 else:
114 value = default_value
116 if type_ is bool:
117 # 1 is true, 0 is false, everything else is illegal
118 if value not in ["0", "1"]:
119 raise ValueError(f'Invalid bool for {name}, need "0" or "1"')
120 value = value == "1"
121 elif type_ is bytes:
122 # decode from hex
123 value = bytes.fromhex(value)
124 elif isinstance(type_, list):
125 # list of allowed string values
126 if value not in type_:
127 raise ValueError(f'Invalid value for {name}, need one of {", ".join(type_)}')
128 else:
129 value = type_(value)
131 config[name] = value
134## Config checks
135def check_config():
136 for name, *_ in CONFIG_OPTIONS:
137 if name not in config:
138 raise ValueError(f"Required config value {name} not set")
140 if not config["DEV"]:
141 # checks for prod
142 if "https" not in config["BASE_URL"]:
143 raise Exception("Production site must be over HTTPS")
144 if not config["ENABLE_EMAIL"]:
145 raise Exception("Production site must have email enabled")
146 if not config["ENABLE_SMS"]:
147 raise Exception("Production site must have SMS enabled")
148 if config["IN_TEST"]:
149 raise Exception("IN_TEST while not DEV")
151 if config["ENABLE_DONATIONS"]:
152 if (
153 not config["STRIPE_API_KEY"]
154 or not config["STRIPE_WEBHOOK_SECRET"]
155 or not config["STRIPE_RECURRING_PRODUCT_ID"]
156 ):
157 raise Exception("No Stripe API key/recurring donation ID but donations enabled")
159 if config["ENABLE_STRONG_VERIFICATION"]:
160 if not config["IRIS_ID_PUBKEY"] or not config["IRIS_ID_SECRET"] or not config["VERIFICATION_DATA_PUBLIC_KEY"]:
161 raise Exception("No Iris ID pubkey/secret or verification data pubkey but strong verification enabled")