3
3
import asyncio
4
4
import json
5
5
import logging
6
- from dataclasses import dataclass
6
+ from dataclasses import dataclass , replace
7
7
from typing import Any , Dict , Tuple , Union
8
8
from urllib import parse as urllib_parse
9
9
from uuid import uuid4
14
14
from sanic_cors import CORS
15
15
from websockets .legacy .protocol import WebSocketCommonProtocol
16
16
17
- from idom .backend .types import Location
17
+ from idom .backend .types import Location , SessionState
18
18
from idom .core .hooks import Context , create_context , use_context
19
19
from idom .core .layout import Layout , LayoutEvent
20
20
from idom .core .serve import (
27
27
from idom .core .types import RootComponentConstructor
28
28
29
29
from ._asgi import serve_development_asgi
30
- from .utils import safe_client_build_dir_path , safe_web_modules_dir_path
30
+ from .utils import (
31
+ SESSION_COOKIE_NAME ,
32
+ SessionManager ,
33
+ safe_client_build_dir_path ,
34
+ safe_web_modules_dir_path ,
35
+ )
31
36
32
37
33
38
logger = logging .getLogger (__name__ )
@@ -47,7 +52,7 @@ def configure(
47
52
_setup_common_routes (blueprint , options )
48
53
49
54
# this route should take priority so set up it up first
50
- _setup_single_view_dispatcher_route (blueprint , component )
55
+ _setup_single_view_dispatcher_route (blueprint , component , options )
51
56
52
57
app .blueprint (blueprint )
53
58
@@ -129,6 +134,9 @@ class Options:
129
134
url_prefix : str = ""
130
135
"""The URL prefix where IDOM resources will be served from"""
131
136
137
+ session_manager : SessionManager [Any ] | None = None
138
+ """Used to create session cookies to perserve client state"""
139
+
132
140
133
141
def _setup_common_routes (blueprint : Blueprint , options : Options ) -> None :
134
142
cors_options = options .cors
@@ -164,22 +172,55 @@ async def web_module_files(
164
172
165
173
166
174
def _setup_single_view_dispatcher_route (
167
- blueprint : Blueprint , constructor : RootComponentConstructor
175
+ blueprint : Blueprint ,
176
+ constructor : RootComponentConstructor ,
177
+ options : Options ,
168
178
) -> None :
169
179
async def model_stream (
170
180
request : request .Request , socket : WebSocketCommonProtocol , path : str = ""
171
181
) -> None :
182
+ root = ConnectionContext (constructor (), value = Connection (request , socket , path ))
183
+
184
+ if options .session_manager :
185
+ root = options .session_manager .context (
186
+ root , value = request .ctx .idom_sesssion_state
187
+ )
188
+
172
189
send , recv = _make_send_recv_callbacks (socket )
173
- conn = Connection (request , socket , path )
174
- await serve_json_patch (
175
- Layout (ConnectionContext (constructor (), value = conn )),
176
- send ,
177
- recv ,
178
- )
190
+ await serve_json_patch (Layout (root ), send , recv )
179
191
180
192
blueprint .add_websocket_route (model_stream , "/_api/stream" )
181
193
blueprint .add_websocket_route (model_stream , "/<path:path>/_api/stream" )
182
194
195
+ if options .session_manager :
196
+ smgr = options .session_manager
197
+
198
+ @blueprint .on_request
199
+ async def set_session_on_request (request : request .Request ) -> None :
200
+ if request .scheme not in ("http" , "https" ):
201
+ return
202
+ session_id = request .cookies .get (SESSION_COOKIE_NAME )
203
+ request .ctx .idom_session_state = await smgr .get_state (session_id )
204
+
205
+ @blueprint .on_response
206
+ async def set_session_cookie_header (
207
+ request : request .Request , response : response .ResponseStream
208
+ ):
209
+ session_state : SessionState [Any ] = request .ctx .idom_session_state
210
+ # only set cookie if it has not been set before
211
+ if session_state .fresh :
212
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
213
+ response .cookies [SESSION_COOKIE_NAME ] = session_state .id
214
+ response .cookies [SESSION_COOKIE_NAME ]["secure" ] = True
215
+ response .cookies [SESSION_COOKIE_NAME ]["httponly" ] = True
216
+ response .cookies [SESSION_COOKIE_NAME ]["samesite" ] = "strict"
217
+ response .cookies [SESSION_COOKIE_NAME ][
218
+ "expires"
219
+ ] = session_state .expiry_date
220
+
221
+ await smgr .update_state (replace (session_state , fresh = False ))
222
+ logger .info (f"Setting cookie { response .cookies [SESSION_COOKIE_NAME ]} " )
223
+
183
224
184
225
def _make_send_recv_callbacks (
185
226
socket : WebSocketCommonProtocol ,
0 commit comments