@@ -1231,50 +1231,184 @@ async def _create_initial_admin(self, admin_data: Union[dict, BaseModel]) -> Non
12311231 raise
12321232
12331233 def use_redis_sessions (
1234- self , redis_url : str = "redis://localhost:6379" , ** kwargs : Any
1234+ self ,
1235+ redis_url : Optional [str ] = None ,
1236+ host : Optional [str ] = None ,
1237+ port : Optional [int ] = None ,
1238+ db : Optional [int ] = None ,
1239+ username : Optional [str ] = None ,
1240+ password : Optional [str ] = None ,
1241+ ** kwargs : Any ,
12351242 ) -> "CRUDAdmin" :
12361243 """Configure Redis session backend.
12371244
1245+ You can configure Redis connection using either a URL or individual parameters,
1246+ but not both.
1247+
12381248 Args:
1239- redis_url: Redis connection URL
1240- **kwargs: Additional Redis configuration options
1249+ redis_url: Redis connection URL (e.g., "redis://user:pass@localhost:6379/0")
1250+ host: Redis host (default: "localhost")
1251+ port: Redis port (default: 6379)
1252+ db: Redis database number (default: 0)
1253+ username: Redis username for ACL authentication (Redis 6.0+, default: None)
1254+ password: Redis password (default: None)
1255+ **kwargs: Additional Redis configuration options (pool_size, connect_timeout, etc.)
12411256
12421257 Returns:
12431258 Self for method chaining
1259+
1260+ Raises:
1261+ ValueError: If both redis_url and individual parameters are provided
1262+
1263+ Examples:
1264+ Using URL:
1265+ ```python
1266+ admin.use_redis_sessions(redis_url="redis://myuser:mypass@localhost:6379/1")
1267+ ```
1268+
1269+ Using individual parameters:
1270+ ```python
1271+ admin.use_redis_sessions(host="localhost", port=6379, db=1, username="myuser", password="secret")
1272+ ```
12441273 """
12451274 self ._session_backend = "redis"
1246- self ._session_backend_kwargs = {"redis_url" : redis_url , ** kwargs }
1275+
1276+ individual_params = [host , port , db , username , password ]
1277+ has_individual_params = any (param is not None for param in individual_params )
1278+
1279+ if redis_url is not None and has_individual_params :
1280+ raise ValueError (
1281+ "Cannot specify both redis_url and individual parameters (host, port, db, username, password). "
1282+ "Use either redis_url='redis://...' OR individual parameters like host='localhost'."
1283+ )
1284+
1285+ if redis_url is not None :
1286+ parsed_redis_kwargs = self ._parse_redis_url (redis_url )
1287+ elif has_individual_params :
1288+ parsed_redis_kwargs = {
1289+ "host" : host or "localhost" ,
1290+ "port" : port or 6379 ,
1291+ "db" : db or 0 ,
1292+ }
1293+ if username is not None :
1294+ parsed_redis_kwargs ["username" ] = username
1295+ if password is not None :
1296+ parsed_redis_kwargs ["password" ] = password
1297+ else :
1298+ parsed_redis_kwargs = {
1299+ "host" : "localhost" ,
1300+ "port" : 6379 ,
1301+ "db" : 0 ,
1302+ }
1303+
1304+ if "track_sessions_in_db" in kwargs :
1305+ self .track_sessions_in_db = kwargs .pop ("track_sessions_in_db" )
1306+ else :
1307+ self .track_sessions_in_db = False
1308+
1309+ parsed_redis_kwargs .update (kwargs )
1310+ self ._session_backend_kwargs = parsed_redis_kwargs
1311+
1312+ if hasattr (self , "session_manager" ):
1313+ self ._recreate_session_manager ()
1314+
12471315 return self
12481316
12491317 def use_memcached_sessions (
1250- self , servers : Optional [List [str ]] = None , ** kwargs : Any
1318+ self ,
1319+ servers : Optional [List [str ]] = None ,
1320+ host : Optional [str ] = None ,
1321+ port : Optional [int ] = None ,
1322+ ** kwargs : Any ,
12511323 ) -> "CRUDAdmin" :
12521324 """Configure Memcached session backend.
12531325
1326+ You can configure Memcached connection using either a servers list or individual parameters,
1327+ but not both.
1328+
12541329 Args:
1255- servers: List of memcached server addresses
1256- **kwargs: Additional Memcached configuration options
1330+ servers: List of memcached server addresses (e.g., ["localhost:11211", "server2:11211"])
1331+ host: Memcached host (default: "localhost")
1332+ port: Memcached port (default: 11211)
1333+ **kwargs: Additional Memcached configuration options (pool_size, etc.)
12571334
12581335 Returns:
12591336 Self for method chaining
1337+
1338+ Raises:
1339+ ValueError: If both servers and individual parameters are provided
1340+
1341+ Examples:
1342+ Using servers list:
1343+ ```python
1344+ admin.use_memcached_sessions(servers=["localhost:11211", "server2:11211"])
1345+ ```
1346+
1347+ Using individual parameters:
1348+ ```python
1349+ admin.use_memcached_sessions(host="localhost", port=11211)
1350+ ```
1351+
1352+ Note:
1353+ aiomcache currently supports only single server connections,
1354+ so only the first server in the list will be used.
12601355 """
1261- if servers is None :
1262- servers = ["localhost:11211" ]
1356+ individual_params = [host , port ]
1357+ has_individual_params = any (param is not None for param in individual_params )
1358+
1359+ if servers is not None and has_individual_params :
1360+ raise ValueError (
1361+ "Cannot specify both servers and individual parameters (host, port). "
1362+ "Use either servers=['host:port', ...] OR individual parameters like host='localhost'."
1363+ )
1364+
1365+ if servers is not None :
1366+ parsed_memcached_kwargs = self ._parse_memcached_servers (servers )
1367+ elif has_individual_params :
1368+ parsed_memcached_kwargs = {
1369+ "host" : host or "localhost" ,
1370+ "port" : port or 11211 ,
1371+ }
1372+ else :
1373+ parsed_memcached_kwargs = {
1374+ "host" : "localhost" ,
1375+ "port" : 11211 ,
1376+ }
1377+
1378+ if "track_sessions_in_db" in kwargs :
1379+ self .track_sessions_in_db = kwargs .pop ("track_sessions_in_db" )
1380+ else :
1381+ self .track_sessions_in_db = False
1382+
1383+ parsed_memcached_kwargs .update (kwargs )
1384+
12631385 self ._session_backend = "memcached"
1264- self ._session_backend_kwargs = {"servers" : servers , ** kwargs }
1386+ self ._session_backend_kwargs = parsed_memcached_kwargs
1387+
1388+ if hasattr (self , "session_manager" ):
1389+ self ._recreate_session_manager ()
1390+
12651391 return self
12661392
12671393 def use_memory_sessions (self , ** kwargs : Any ) -> "CRUDAdmin" :
12681394 """Configure in-memory session backend.
12691395
12701396 Args:
12711397 **kwargs: Additional memory storage configuration options
1398+ Note: track_sessions_in_db is ignored for memory sessions
12721399
12731400 Returns:
12741401 Self for method chaining
12751402 """
1403+ kwargs .pop ("track_sessions_in_db" , None )
1404+
12761405 self ._session_backend = "memory"
12771406 self ._session_backend_kwargs = kwargs
1407+ self .track_sessions_in_db = False
1408+
1409+ if hasattr (self , "session_manager" ):
1410+ self ._recreate_session_manager ()
1411+
12781412 return self
12791413
12801414 def use_database_sessions (self , ** kwargs : Any ) -> "CRUDAdmin" :
@@ -1292,4 +1426,130 @@ def use_database_sessions(self, **kwargs: Any) -> "CRUDAdmin":
12921426 self ._session_backend = "database"
12931427 self ._session_backend_kwargs = kwargs
12941428 self .track_sessions_in_db = True
1429+
1430+ if hasattr (self , "session_manager" ):
1431+ self ._recreate_session_manager ()
1432+
12951433 return self
1434+
1435+ def _parse_redis_url (self , redis_url : str ) -> Dict [str , Any ]:
1436+ """Parse Redis URL to extract connection parameters.
1437+
1438+ Args:
1439+ redis_url: Redis connection URL (e.g., redis://user:pass@localhost:6379/0)
1440+
1441+ Returns:
1442+ Dictionary of Redis connection parameters
1443+ """
1444+ from urllib .parse import urlparse
1445+
1446+ parsed = urlparse (redis_url )
1447+
1448+ redis_kwargs = {
1449+ "host" : parsed .hostname or "localhost" ,
1450+ "port" : parsed .port or 6379 ,
1451+ "db" : int (parsed .path .lstrip ("/" ))
1452+ if parsed .path and parsed .path != "/"
1453+ else 0 ,
1454+ }
1455+
1456+ if parsed .username :
1457+ redis_kwargs ["username" ] = parsed .username
1458+
1459+ if parsed .password :
1460+ redis_kwargs ["password" ] = parsed .password
1461+
1462+ return redis_kwargs
1463+
1464+ def _parse_memcached_servers (self , servers : List [str ]) -> Dict [str , Any ]:
1465+ """Parse Memcached servers list to extract connection parameters.
1466+
1467+ Args:
1468+ servers: List of server addresses (e.g., ["localhost:11211", "server2:11211"])
1469+
1470+ Returns:
1471+ Dictionary of Memcached connection parameters
1472+
1473+ Note:
1474+ aiomcache currently supports only single server connections,
1475+ so we use the first server in the list.
1476+ """
1477+ if not servers :
1478+ return {"host" : "localhost" , "port" : 11211 }
1479+
1480+ server = servers [0 ]
1481+
1482+ if ":" in server :
1483+ host , port_str = server .split (":" , 1 )
1484+ try :
1485+ port = int (port_str )
1486+ except ValueError :
1487+ port = 11211
1488+ else :
1489+ host = server
1490+ port = 11211
1491+
1492+ return {"host" : host , "port" : port }
1493+
1494+ def _recreate_session_manager (self ) -> None :
1495+ """Recreate session manager with current backend configuration."""
1496+ session_backend = getattr (self , "_session_backend" , "memory" )
1497+ backend_kwargs = getattr (self , "_session_backend_kwargs" , {})
1498+
1499+ if self .track_sessions_in_db :
1500+ if session_backend in ["redis" , "memcached" ]:
1501+ actual_backend = "hybrid"
1502+ backend_kwargs ["db_config" ] = self .db_config
1503+ backend_kwargs ["_cache_backend" ] = session_backend
1504+ else :
1505+ actual_backend = "database"
1506+ backend_kwargs ["db_config" ] = self .db_config
1507+ else :
1508+ actual_backend = session_backend
1509+
1510+ session_timeout_minutes = 30 # default
1511+ if hasattr (self , "session_manager" ):
1512+ session_timeout_minutes = int (
1513+ self .session_manager .session_timeout .total_seconds () / 60
1514+ )
1515+
1516+ storage : AbstractSessionStorage [SessionData ] = get_session_storage (
1517+ backend = actual_backend ,
1518+ model_type = SessionData ,
1519+ prefix = "session:" ,
1520+ expiration = session_timeout_minutes * 60 ,
1521+ ** backend_kwargs ,
1522+ )
1523+
1524+ if hasattr (self , "session_manager" ):
1525+ max_sessions = self .session_manager .max_sessions
1526+ cleanup_interval_minutes = int (
1527+ self .session_manager .cleanup_interval .total_seconds () / 60
1528+ )
1529+ csrf_token_bytes = self .session_manager .csrf_token_bytes
1530+ rate_limiter = self .session_manager .rate_limiter
1531+ login_max_attempts = self .session_manager .login_max_attempts
1532+ login_window_minutes = int (
1533+ self .session_manager .login_window .total_seconds () / 60
1534+ )
1535+ else :
1536+ max_sessions = 5
1537+ cleanup_interval_minutes = 15
1538+ csrf_token_bytes = 32
1539+ rate_limiter = None
1540+ login_max_attempts = 5
1541+ login_window_minutes = 15
1542+
1543+ self .session_manager = SessionManager (
1544+ session_storage = storage ,
1545+ max_sessions_per_user = max_sessions ,
1546+ session_timeout_minutes = session_timeout_minutes ,
1547+ cleanup_interval_minutes = cleanup_interval_minutes ,
1548+ csrf_token_bytes = csrf_token_bytes ,
1549+ rate_limiter = rate_limiter ,
1550+ login_max_attempts = login_max_attempts ,
1551+ login_window_minutes = login_window_minutes ,
1552+ )
1553+
1554+ if hasattr (self , "admin_authentication" ):
1555+ self .admin_authentication .session_manager = self .session_manager
0 commit comments