@kernelminds/create-enclave 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +6 -0
- package/README.md +202 -0
- package/bin/create-enclave.js +616 -0
- package/img/favicon.ico +0 -0
- package/img/logo.png +0 -0
- package/package.json +46 -0
- package/server/golang/go.mod +50 -0
- package/server/golang/go.sum +198 -0
- package/server/golang/server.go +488 -0
- package/server/golang/utils.go +7 -0
- package/server/node/server.ts +281 -0
- package/server/node/utils.ts +3 -0
- package/server/python/.python-version +1 -0
- package/server/python/pyproject.toml +14 -0
- package/server/python/server.py +446 -0
- package/server/python/utils.py +2 -0
- package/server/python/uv.lock +798 -0
- package/src/create-enclave.ts +752 -0
- package/src/package.ts +179 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
# Import aiohttp library for async requests
|
|
3
|
+
import aiohttp
|
|
4
|
+
from aiohttp_session import setup, get_session
|
|
5
|
+
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import logging
|
|
9
|
+
import random
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from aiohttp import web
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
import redis.asyncio as redis
|
|
14
|
+
import hashlib
|
|
15
|
+
|
|
16
|
+
# Import login module
|
|
17
|
+
from scailo_sdk.login_api import AsyncLoginServiceClient, login
|
|
18
|
+
from scailo_sdk.vault_api import AsyncVaultServiceClient
|
|
19
|
+
from scailo_sdk.vault_commons import VerifyEnclaveIngressRequest
|
|
20
|
+
from scailo_sdk.vendors_api import AsyncVendorsServiceClient
|
|
21
|
+
from scailo_sdk.vendors import VendorsServiceFilterReq
|
|
22
|
+
from scailo_sdk.base import BOOL_FILTER_TRUE
|
|
23
|
+
import utils
|
|
24
|
+
|
|
25
|
+
# --- Configuration and Globals ---
|
|
26
|
+
|
|
27
|
+
# Set up basic logging configuration
|
|
28
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
29
|
+
log = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
class Config:
|
|
32
|
+
"""Holds all necessary environment variables, matching the Go structure."""
|
|
33
|
+
ENCLAVE_NAME: str = ""
|
|
34
|
+
SCAILO_API: str = ""
|
|
35
|
+
PORT: int = 8080 # Default port
|
|
36
|
+
USERNAME: str = ""
|
|
37
|
+
PASSWORD: str = ""
|
|
38
|
+
|
|
39
|
+
REDIS_USERNAME: str = ""
|
|
40
|
+
REDIS_PASSWORD: str = ""
|
|
41
|
+
REDIS_URL: str = ""
|
|
42
|
+
WORKFLOW_EVENTS_CHANNEL: str = ""
|
|
43
|
+
COOKIE_SIGNATURE_SECRET: str = ""
|
|
44
|
+
|
|
45
|
+
# Global state variables
|
|
46
|
+
global_config = Config()
|
|
47
|
+
production: bool = False
|
|
48
|
+
index_page_cache: str = ""
|
|
49
|
+
enclave_prefix: str = ""
|
|
50
|
+
auth_token: str = ""
|
|
51
|
+
|
|
52
|
+
encoded_cookie_signature_secret: bytes = b""
|
|
53
|
+
|
|
54
|
+
# Constants
|
|
55
|
+
LOGIN_INTERVAL_SECONDS = 3600 * 12 # 12 hour
|
|
56
|
+
INDEX_HTML_FILE = "index.html"
|
|
57
|
+
|
|
58
|
+
# --- Initialization and Config Loading ---
|
|
59
|
+
|
|
60
|
+
def load_config():
|
|
61
|
+
"""Reads and validates environment variables."""
|
|
62
|
+
load_dotenv(override=True)
|
|
63
|
+
|
|
64
|
+
global production
|
|
65
|
+
# Determine production status
|
|
66
|
+
production_flag = os.getenv("PRODUCTION", "false").lower() == "true"
|
|
67
|
+
|
|
68
|
+
# Matching Go's logic: if GIN_MODE is 'release' OR PRODUCTION is 'true'
|
|
69
|
+
if production_flag:
|
|
70
|
+
production = True
|
|
71
|
+
|
|
72
|
+
log.info(f"Server operating in Production mode: {production}")
|
|
73
|
+
|
|
74
|
+
# 2. Read environment variables
|
|
75
|
+
global_config.ENCLAVE_NAME = os.getenv("ENCLAVE_NAME") or ""
|
|
76
|
+
global_config.SCAILO_API = os.getenv("SCAILO_API") or ""
|
|
77
|
+
global_config.USERNAME = os.getenv("USERNAME") or ""
|
|
78
|
+
global_config.PASSWORD = os.getenv("PASSWORD") or ""
|
|
79
|
+
|
|
80
|
+
global_config.REDIS_USERNAME = os.getenv("REDIS_USERNAME") or ""
|
|
81
|
+
global_config.REDIS_PASSWORD = os.getenv("REDIS_PASSWORD") or ""
|
|
82
|
+
global_config.REDIS_URL = os.getenv("REDIS_URL") or ""
|
|
83
|
+
global_config.WORKFLOW_EVENTS_CHANNEL = os.getenv("WORKFLOW_EVENTS_CHANNEL") or ""
|
|
84
|
+
global_config.COOKIE_SIGNATURE_SECRET = os.getenv("COOKIE_SIGNATURE_SECRET") or ""
|
|
85
|
+
|
|
86
|
+
port_str = os.getenv("PORT", "8080")
|
|
87
|
+
try:
|
|
88
|
+
global_config.PORT = int(port_str)
|
|
89
|
+
except ValueError:
|
|
90
|
+
log.error(f"Invalid PORT value: {port_str}. Using default 8080.")
|
|
91
|
+
global_config.PORT = 8080
|
|
92
|
+
|
|
93
|
+
# 3. Validate environment variables (matches Go's exit logic)
|
|
94
|
+
exit_code = 0
|
|
95
|
+
if not global_config.ENCLAVE_NAME:
|
|
96
|
+
log.error("ENCLAVE_NAME not set")
|
|
97
|
+
exit_code = 1
|
|
98
|
+
if not global_config.SCAILO_API:
|
|
99
|
+
log.error("SCAILO_API not set")
|
|
100
|
+
exit_code = 1
|
|
101
|
+
if global_config.PORT == 0:
|
|
102
|
+
log.error("PORT not set or is 0")
|
|
103
|
+
exit_code = 1
|
|
104
|
+
if not global_config.USERNAME:
|
|
105
|
+
log.error("USERNAME not set (required for API login stub)")
|
|
106
|
+
exit_code = 1
|
|
107
|
+
if not global_config.PASSWORD:
|
|
108
|
+
log.error("PASSWORD not set (required for API login stub)")
|
|
109
|
+
exit_code = 1
|
|
110
|
+
if not global_config.REDIS_URL:
|
|
111
|
+
log.error("REDIS_URL not set")
|
|
112
|
+
exit_code = 1
|
|
113
|
+
if not global_config.WORKFLOW_EVENTS_CHANNEL:
|
|
114
|
+
log.error("WORKFLOW_EVENTS_CHANNEL not set")
|
|
115
|
+
exit_code = 1
|
|
116
|
+
if not global_config.COOKIE_SIGNATURE_SECRET:
|
|
117
|
+
log.error("COOKIE_SIGNATURE_SECRET not set")
|
|
118
|
+
exit_code = 1
|
|
119
|
+
|
|
120
|
+
global enclave_prefix
|
|
121
|
+
enclave_prefix = utils.get_enclave_prefix(global_config.ENCLAVE_NAME)
|
|
122
|
+
|
|
123
|
+
global encoded_cookie_signature_secret
|
|
124
|
+
encoded_cookie_signature_secret = hashlib.sha256(global_config.COOKIE_SIGNATURE_SECRET.encode('utf-8')).digest()
|
|
125
|
+
|
|
126
|
+
if exit_code != 0:
|
|
127
|
+
log.error("Configuration errors found. Exiting.")
|
|
128
|
+
sys.exit(exit_code)
|
|
129
|
+
|
|
130
|
+
async def perform_login():
|
|
131
|
+
global auth_token
|
|
132
|
+
|
|
133
|
+
log.info(f"Attempting login to API at: {global_config.SCAILO_API} with user: {global_config.USERNAME}")
|
|
134
|
+
|
|
135
|
+
async with aiohttp.ClientSession() as http_client:
|
|
136
|
+
# Create the login client
|
|
137
|
+
login_client = AsyncLoginServiceClient(global_config.SCAILO_API, http_client)
|
|
138
|
+
# Call the login method to retrieve the auth token
|
|
139
|
+
login_resp = await login_client.login_as_employee_primary(login.UserLoginRequest(username=global_config.USERNAME, plain_text_password=global_config.PASSWORD))
|
|
140
|
+
if login_resp.auth_token:
|
|
141
|
+
auth_token = login_resp.auth_token
|
|
142
|
+
|
|
143
|
+
# print("Auth token is: ", auth_token)
|
|
144
|
+
log.info(f"Successfully logged in...")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def setup_redis(app: web.Application):
|
|
148
|
+
# Connect to the Redis server and create a PubSub instance
|
|
149
|
+
try:
|
|
150
|
+
[redis_host, redis_port] = global_config.REDIS_URL.split(":")
|
|
151
|
+
redis_client = redis.Redis(host=redis_host, port=int(redis_port), decode_responses=True)
|
|
152
|
+
pubsub = redis_client.pubsub()
|
|
153
|
+
print("Connected to Redis server.")
|
|
154
|
+
|
|
155
|
+
# Subscribe and listen for messages
|
|
156
|
+
await pubsub.subscribe(global_config.WORKFLOW_EVENTS_CHANNEL)
|
|
157
|
+
print(f"Subscribed to {global_config.WORKFLOW_EVENTS_CHANNEL}. Waiting for messages...")
|
|
158
|
+
|
|
159
|
+
async for message in pubsub.listen():
|
|
160
|
+
print(f"Received: {message}")
|
|
161
|
+
except redis.exceptions.ConnectionError as e:
|
|
162
|
+
print(f"Error connecting to Redis: {e}")
|
|
163
|
+
exit()
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print(f"An error occurred: {e}")
|
|
166
|
+
finally:
|
|
167
|
+
await pubsub.unsubscribe(global_config.WORKFLOW_EVENTS_CHANNEL)
|
|
168
|
+
print(f"Unsubscribed from {global_config.WORKFLOW_EVENTS_CHANNEL}.")
|
|
169
|
+
await redis_client.aclose()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def append_default_header(auth_token_to_add: str | None = None):
|
|
173
|
+
if not auth_token_to_add:
|
|
174
|
+
auth_token_to_add = auth_token
|
|
175
|
+
d = dict[str, str]()
|
|
176
|
+
d["auth_token"] = auth_token_to_add
|
|
177
|
+
return d
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def login_to_api_task(app: web.Application):
|
|
181
|
+
"""
|
|
182
|
+
Background task to perform the login periodically, matching the Go code.
|
|
183
|
+
This runs indefinitely after server startup.
|
|
184
|
+
"""
|
|
185
|
+
log.info("Starting recurring API login task...")
|
|
186
|
+
|
|
187
|
+
# Perform initial login immediately
|
|
188
|
+
await perform_login()
|
|
189
|
+
|
|
190
|
+
while True:
|
|
191
|
+
try:
|
|
192
|
+
log.info(f"Waiting {LOGIN_INTERVAL_SECONDS} seconds until next login.")
|
|
193
|
+
await asyncio.sleep(LOGIN_INTERVAL_SECONDS)
|
|
194
|
+
await perform_login()
|
|
195
|
+
except asyncio.CancelledError:
|
|
196
|
+
log.info("API login task cancelled.")
|
|
197
|
+
break
|
|
198
|
+
except Exception as e:
|
|
199
|
+
log.error(f"Error in API login task: {e}")
|
|
200
|
+
await asyncio.sleep(60) # Wait a minute before retrying
|
|
201
|
+
|
|
202
|
+
# --- HTML & Cache Busting Logic ---
|
|
203
|
+
|
|
204
|
+
def replace_bundle_caches(page: str) -> str:
|
|
205
|
+
"""Implements the cache-busting logic by appending a version number."""
|
|
206
|
+
# Use current timestamp for cache busting version (YYYYMMDDhhmmss format)
|
|
207
|
+
version = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
208
|
+
|
|
209
|
+
log.debug(f"Applying cache-busting version: {version}")
|
|
210
|
+
|
|
211
|
+
# Asset paths to replace
|
|
212
|
+
script_preload_old = f'<link rel="preload" as="script" href="{enclave_prefix}/resources/dist/js/bundle.src.min.js">'
|
|
213
|
+
script_preload_new = f'<link rel="preload" as="script" href="{enclave_prefix}/resources/dist/js/bundle.src.min.js?v={version}">'
|
|
214
|
+
|
|
215
|
+
script_src_old = f'<script src="{enclave_prefix}/resources/dist/js/bundle.src.min.js"></script>'
|
|
216
|
+
script_src_new = f'<script src="{enclave_prefix}/resources/dist/js/bundle.src.min.js?v={version}"></script>'
|
|
217
|
+
|
|
218
|
+
style_link_old = f'<link rel="stylesheet" href="{enclave_prefix}/resources/dist/css/bundle.css">'
|
|
219
|
+
style_link_new = f'<link rel="stylesheet" href="{enclave_prefix}/resources/dist/css/bundle.css?v={version}">'
|
|
220
|
+
|
|
221
|
+
# Replacement execution
|
|
222
|
+
page = page.replace(script_preload_old, script_preload_new)
|
|
223
|
+
page = page.replace(script_src_old, script_src_new)
|
|
224
|
+
page = page.replace(style_link_old, style_link_new)
|
|
225
|
+
|
|
226
|
+
return page
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def get_index_page() -> str:
|
|
230
|
+
"""Reads index.html, using the cache if in production."""
|
|
231
|
+
global index_page_cache
|
|
232
|
+
|
|
233
|
+
if production and index_page_cache:
|
|
234
|
+
return index_page_cache
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
# For simplicity, we use a direct read as it is only done once in production.
|
|
238
|
+
with open(INDEX_HTML_FILE, 'r') as f:
|
|
239
|
+
content = f.read()
|
|
240
|
+
|
|
241
|
+
if production:
|
|
242
|
+
index_page_cache = content
|
|
243
|
+
|
|
244
|
+
return content
|
|
245
|
+
|
|
246
|
+
except FileNotFoundError:
|
|
247
|
+
log.error(f"Error: {INDEX_HTML_FILE} not found.")
|
|
248
|
+
return "Index page not found."
|
|
249
|
+
except Exception as e:
|
|
250
|
+
log.error(f"Error reading {INDEX_HTML_FILE}: {e}")
|
|
251
|
+
return "Error reading index page."
|
|
252
|
+
|
|
253
|
+
# --- Request Handlers ---
|
|
254
|
+
|
|
255
|
+
async def index_handler(request: web.Request):
|
|
256
|
+
"""
|
|
257
|
+
The single handler for all root/SPA UI routes (e.g., /enclave/name/ui, /enclave/name/ui/path).
|
|
258
|
+
Serves the cache-busted index.html.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
index_content = await get_index_page()
|
|
262
|
+
|
|
263
|
+
if index_content.startswith("Error") or index_content.startswith("Index page not found"):
|
|
264
|
+
return web.Response(text=index_content, status=500, content_type='text/plain')
|
|
265
|
+
|
|
266
|
+
page_with_cache = replace_bundle_caches(index_content)
|
|
267
|
+
|
|
268
|
+
return web.Response(text=page_with_cache, status=200, content_type='text/html')
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
async def health_check_handler(request: web.Request):
|
|
272
|
+
return web.json_response({"status": "OK"}, status=200)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
async def random_api_handler(request: web.Request):
|
|
276
|
+
"""Handles the /api/random endpoint."""
|
|
277
|
+
# Generate a random float between 0.0 and 1.0 (like Math.random())
|
|
278
|
+
random_number = random.random()
|
|
279
|
+
return web.json_response({"random": random_number}, status=200)
|
|
280
|
+
|
|
281
|
+
async def no_route_handler(request: web.Request):
|
|
282
|
+
"""Handles all unmatched routes and redirects them to the UI path."""
|
|
283
|
+
ui_path = f"{enclave_prefix}/ui"
|
|
284
|
+
log.info(f"No route found for {request.path}. Redirecting to {ui_path}")
|
|
285
|
+
raise web.HTTPTemporaryRedirect(location=ui_path)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
async def ingress_handler(request: web.Request):
|
|
289
|
+
"""Handles the ingress -> sets the auth token and redirects to the entry point"""
|
|
290
|
+
try:
|
|
291
|
+
if not production:
|
|
292
|
+
# In dev, use the default auth token
|
|
293
|
+
session = await get_session(request)
|
|
294
|
+
session[f'{global_config.ENCLAVE_NAME}_auth_token'] = auth_token
|
|
295
|
+
session.max_age = 3600
|
|
296
|
+
# Redirect to the UI path
|
|
297
|
+
ui_path = f"{enclave_prefix}/ui"
|
|
298
|
+
return web.HTTPTemporaryRedirect(location=ui_path)
|
|
299
|
+
else:
|
|
300
|
+
# `${enclavePrefix}/ingress/:token`
|
|
301
|
+
# Get the token
|
|
302
|
+
token = request.match_info.get('token')
|
|
303
|
+
if not token:
|
|
304
|
+
return web.Response(status=400, text="Missing token")
|
|
305
|
+
async with aiohttp.ClientSession() as http_client:
|
|
306
|
+
vault_client = AsyncVaultServiceClient(global_config.SCAILO_API, http_client)
|
|
307
|
+
ingress = await vault_client.verify_enclave_ingress(VerifyEnclaveIngressRequest(token=token), extra_headers=append_default_header(auth_token))
|
|
308
|
+
session = await get_session(request)
|
|
309
|
+
session[f'{global_config.ENCLAVE_NAME}_auth_token'] = ingress.auth_token
|
|
310
|
+
session.max_age = ingress.expires_at
|
|
311
|
+
# Redirect to the UI path
|
|
312
|
+
ui_path = f"{enclave_prefix}/ui"
|
|
313
|
+
return web.HTTPTemporaryRedirect(location=ui_path)
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
return web.Response(status=500, text=str(e))
|
|
317
|
+
|
|
318
|
+
async def protected_api_random_handler(request: web.Request):
|
|
319
|
+
"""Handles the /protected/api/random endpoint."""
|
|
320
|
+
try:
|
|
321
|
+
session = await get_session(request)
|
|
322
|
+
user_auth_token = session.get(f'{global_config.ENCLAVE_NAME}_auth_token')
|
|
323
|
+
|
|
324
|
+
if not user_auth_token:
|
|
325
|
+
return web.Response(text="Session expired or invalid. Please login again.", status=401)
|
|
326
|
+
if len(user_auth_token) == 0:
|
|
327
|
+
return web.Response(text="Session expired or invalid. Please login again.", status=401)
|
|
328
|
+
|
|
329
|
+
random_number = random.random()
|
|
330
|
+
async with aiohttp.ClientSession() as http_client:
|
|
331
|
+
vendors_client = AsyncVendorsServiceClient(global_config.SCAILO_API, http_client)
|
|
332
|
+
vendors_list = (await vendors_client.filter(VendorsServiceFilterReq(is_active=BOOL_FILTER_TRUE, count=-1), extra_headers=append_default_header(auth_token))).list
|
|
333
|
+
resp_vendors = []
|
|
334
|
+
for vendor in vendors_list:
|
|
335
|
+
d = {}
|
|
336
|
+
d["code"] = vendor.code
|
|
337
|
+
resp_vendors.append(d)
|
|
338
|
+
return web.json_response({"random": random_number, "vendors": resp_vendors}, status=200)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
return web.Response(status=500, text=str(e))
|
|
341
|
+
|
|
342
|
+
# Redirects to "{enclavePrefix}/ui"
|
|
343
|
+
async def direct_url_entry_point_handler(request: web.Request):
|
|
344
|
+
return web.HTTPTemporaryRedirect(location=f"{enclave_prefix}/ui")
|
|
345
|
+
|
|
346
|
+
# --- Background Task Management ---
|
|
347
|
+
|
|
348
|
+
async def start_background_tasks(app: web.Application):
|
|
349
|
+
"""Starts the recurring login task and stores the reference."""
|
|
350
|
+
# Create the task and immediately schedule it, storing the task object
|
|
351
|
+
_login_to_api_task = asyncio.create_task(login_to_api_task(app))
|
|
352
|
+
app['login_to_api_task'] = _login_to_api_task
|
|
353
|
+
|
|
354
|
+
_setup_redis_task = asyncio.create_task(setup_redis(app))
|
|
355
|
+
app['_setup_redis_task'] = _setup_redis_task
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
async def cleanup_background_tasks(app: web.Application):
|
|
359
|
+
"""Cancels the recurring login task on application shutdown."""
|
|
360
|
+
_login_to_api_task = app.get('login_to_api_task')
|
|
361
|
+
if _login_to_api_task:
|
|
362
|
+
log.info("Cancelling API login task...")
|
|
363
|
+
_login_to_api_task.cancel()
|
|
364
|
+
# Wait for the task to finish, ignoring the expected CancelledError
|
|
365
|
+
await asyncio.gather(_login_to_api_task, return_exceptions=True)
|
|
366
|
+
log.info("API login task cancelled successfully.")
|
|
367
|
+
|
|
368
|
+
_setup_redis_task = app.get('_setup_redis_task')
|
|
369
|
+
if _setup_redis_task:
|
|
370
|
+
log.info("Cancelling API login task...")
|
|
371
|
+
_setup_redis_task.cancel()
|
|
372
|
+
# Wait for the task to finish, ignoring the expected CancelledError
|
|
373
|
+
await asyncio.gather(_setup_redis_task, return_exceptions=True)
|
|
374
|
+
log.info("API login task cancelled successfully.")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# --- Main Application Setup ---
|
|
378
|
+
|
|
379
|
+
def create_app() -> web.Application:
|
|
380
|
+
"""Sets up the aiohttp application with all routes."""
|
|
381
|
+
|
|
382
|
+
load_config()
|
|
383
|
+
|
|
384
|
+
# Initialize aiohttp application
|
|
385
|
+
app = web.Application()
|
|
386
|
+
|
|
387
|
+
# 4. Global Middleware Setup
|
|
388
|
+
# max_age here sets the default for all sessions (e.g., 24 hours)
|
|
389
|
+
setup(app, EncryptedCookieStorage(
|
|
390
|
+
encoded_cookie_signature_secret,
|
|
391
|
+
cookie_name=f"{global_config.ENCLAVE_NAME}_auth_token",
|
|
392
|
+
max_age=86400, # 24 hours in seconds
|
|
393
|
+
httponly=True, # Prevents JavaScript access (XSS protection)
|
|
394
|
+
samesite="Lax" # CSRF protection
|
|
395
|
+
))
|
|
396
|
+
|
|
397
|
+
# --- 1. Register Static Routes ---
|
|
398
|
+
static_route_path = f"{enclave_prefix}/resources/dist"
|
|
399
|
+
app.router.add_static(static_route_path, "resources/dist", name="static_resources")
|
|
400
|
+
# log.info(f"Static route registered: {static_route_path} -> resources/dist")
|
|
401
|
+
|
|
402
|
+
# --- 2. Health Checks ---
|
|
403
|
+
app.router.add_get(f"{enclave_prefix}/health/startup", health_check_handler)
|
|
404
|
+
app.router.add_get(f"{enclave_prefix}/health/liveliness", health_check_handler)
|
|
405
|
+
app.router.add_get(f"{enclave_prefix}/health/readiness", health_check_handler)
|
|
406
|
+
|
|
407
|
+
# --- 3. API Endpoint ---
|
|
408
|
+
app.router.add_get(f"{enclave_prefix}/api/random", random_api_handler)
|
|
409
|
+
|
|
410
|
+
app.router.add_get(f"{enclave_prefix}/ingress/{{token}}", ingress_handler)
|
|
411
|
+
app.router.add_get(f"{enclave_prefix}/protected/api/random", protected_api_random_handler)
|
|
412
|
+
|
|
413
|
+
# Implicit redirect for entry_point_management = direct_url
|
|
414
|
+
app.router.add_get(f"/", direct_url_entry_point_handler)
|
|
415
|
+
|
|
416
|
+
# --- 4. Index Page / SPA Routes ---
|
|
417
|
+
ui_path_root = f"{enclave_prefix}/ui"
|
|
418
|
+
ui_path_spa = f"{enclave_prefix}/ui/{{tail:.*}}"
|
|
419
|
+
|
|
420
|
+
app.router.add_get(ui_path_root, index_handler)
|
|
421
|
+
app.router.add_get(ui_path_spa, index_handler)
|
|
422
|
+
# log.info(f"UI/SPA routes registered: {ui_path_root} and {ui_path_spa}")
|
|
423
|
+
|
|
424
|
+
# --- 5. Not Found Handler (NoRoute replacement) ---
|
|
425
|
+
app.router.add_route('*', '/{tail:.*}', no_route_handler)
|
|
426
|
+
|
|
427
|
+
# --- 6. Start/Stop the recurring login task ---
|
|
428
|
+
# Use explicit handlers to manage the task's full lifecycle
|
|
429
|
+
app.on_startup.append(start_background_tasks)
|
|
430
|
+
app.on_cleanup.append(cleanup_background_tasks)
|
|
431
|
+
|
|
432
|
+
return app
|
|
433
|
+
|
|
434
|
+
# --- Main Entry Point ---
|
|
435
|
+
|
|
436
|
+
if __name__ == '__main__':
|
|
437
|
+
# Start the application
|
|
438
|
+
app = create_app()
|
|
439
|
+
address = f"0.0.0.0:{global_config.PORT}"
|
|
440
|
+
log.info(f"Server listening on address {address} with Production: {production}")
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
web.run_app(app, host='0.0.0.0', port=global_config.PORT)
|
|
444
|
+
except Exception as e:
|
|
445
|
+
log.critical(f"Server failed to start: {e}")
|
|
446
|
+
sys.exit(1)
|