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 : 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,51 @@ 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 :
185
+ root = options .session .context (root , value = request .ctx .idom_sesssion_state )
186
+
172
187
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
- )
188
+ await serve_json_patch (Layout (root ), send , recv )
179
189
180
190
blueprint .add_websocket_route (model_stream , "/_api/stream" )
181
191
blueprint .add_websocket_route (model_stream , "/<path:path>/_api/stream" )
182
192
193
+ if options .session :
194
+ session = options .session
195
+
196
+ @blueprint .on_request
197
+ async def set_session_on_request (request : request .Request ) -> None :
198
+ if request .scheme not in ("http" , "https" ):
199
+ return
200
+ session_id = request .cookies .get (SESSION_COOKIE_NAME )
201
+ request .ctx .idom_session_state = await session .get_state (session_id )
202
+
203
+ @blueprint .on_response
204
+ async def set_session_cookie_header (
205
+ request : request .Request , response : response .ResponseStream
206
+ ):
207
+ session_state : SessionState [Any ] = request .ctx .idom_session_state
208
+ # only set cookie if it has not been set before
209
+ if session_state .fresh :
210
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
211
+ response .cookies [SESSION_COOKIE_NAME ] = session .sid
212
+ response .cookies [SESSION_COOKIE_NAME ]["secure" ] = True
213
+ response .cookies [SESSION_COOKIE_NAME ]["httponly" ] = True
214
+ response .cookies [SESSION_COOKIE_NAME ]["samesite" ] = "strict"
215
+ response .cookies [SESSION_COOKIE_NAME ]["expires" ] = session .expiry_date
216
+
217
+ await session .update_state (replace (session_state , fresh = False ))
218
+ logger .info (f"Setting cookie { response .cookies [SESSION_COOKIE_NAME ]} " )
219
+
183
220
184
221
def _make_send_recv_callbacks (
185
222
socket : WebSocketCommonProtocol ,
0 commit comments