Updated Command to allow client_response. First implementation or UserProfile control
This commit is contained in:
@@ -63,14 +63,12 @@ def auth_before(request, session):
|
||||
Args:
|
||||
request: Starlette request object
|
||||
session: FastHTML session object
|
||||
|
||||
Returns:
|
||||
RedirectResponse to login page if authentication fails, None otherwise
|
||||
"""
|
||||
# Get tokens from session
|
||||
access_token = session.get('access_token')
|
||||
refresh_token = session.get('refresh_token')
|
||||
print(f"path={request.scope['path']}, {session=}, {access_token=}, {refresh_token=}")
|
||||
# If no access token, redirect to login
|
||||
if not access_token:
|
||||
return RedirectResponse('/login', status_code=303)
|
||||
@@ -281,6 +279,23 @@ def get_user_info(access_token: str, base_url: str = None) -> Optional[Dict[str,
|
||||
return None
|
||||
|
||||
|
||||
def save_user_info(access_token: str, user_profile: dict, base_url: str = None):
|
||||
try:
|
||||
response = http_client.patch(
|
||||
f"{base_url or API_BASE_URL}/auth/me",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
timeout=10.0,
|
||||
json=user_profile
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
|
||||
return None
|
||||
except httpx.HTTPError:
|
||||
return None
|
||||
|
||||
|
||||
def logout_user(refresh_token: str, base_url: str = None) -> bool:
|
||||
"""
|
||||
Logout user by revoking the refresh token.
|
||||
|
||||
@@ -23,8 +23,8 @@ logger = logging.getLogger("LayoutControl")
|
||||
|
||||
@dataclass
|
||||
class LayoutState(DbObject):
|
||||
def __init__(self, session, owner):
|
||||
super().__init__(session, owner.get_id())
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner.get_session(), owner.get_id())
|
||||
|
||||
left_drawer_open: bool = True
|
||||
right_drawer_open: bool = False
|
||||
@@ -74,7 +74,7 @@ class Layout(SingleInstance):
|
||||
self._header_content = None
|
||||
self._footer_content = None
|
||||
self._main_content = None
|
||||
self._state = LayoutState(session, self)
|
||||
self._state = LayoutState(self)
|
||||
self.commands = Commands(self)
|
||||
self.left_drawer = self.DrawerContent(self, "left")
|
||||
self.right_drawer = self.DrawerContent(self, "right")
|
||||
|
||||
@@ -1,15 +1,48 @@
|
||||
from fasthtml.components import *
|
||||
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.helpers import Ids, mk
|
||||
from myfasthtml.core.instances import SingleInstance
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.instances import SingleInstance, InstancesManager
|
||||
from myfasthtml.core.utils import retrieve_user_info
|
||||
from myfasthtml.icons.material import dark_mode_filled, person_outline_sharp
|
||||
from myfasthtml.icons.material_p1 import light_mode_filled, alternate_email_filled
|
||||
|
||||
|
||||
class UserProfileState:
|
||||
def __init__(self, owner):
|
||||
self._owner = owner
|
||||
self._session = owner.get_session()
|
||||
|
||||
self.theme = "light"
|
||||
|
||||
def load(self):
|
||||
user_info = retrieve_user_info(self._session)
|
||||
user_settings = user_info.get("user_settings", {})
|
||||
for k, v in user_settings.items():
|
||||
if hasattr(self, k):
|
||||
setattr(self, k, v)
|
||||
|
||||
def save(self):
|
||||
user_settings = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
||||
auth_proxy = InstancesManager.get_auth_proxy()
|
||||
auth_proxy.save_user_info(self._session["access_token"], {"user_settings": user_settings})
|
||||
|
||||
|
||||
class Commands(BaseCommands):
|
||||
def update_dark_mode(self):
|
||||
return Command("UpdateDarkMode", "Set the dark mode", self._owner.update_dark_mode).htmx(target=None)
|
||||
|
||||
|
||||
class UserProfile(SingleInstance):
|
||||
def __init__(self, session):
|
||||
super().__init__(session, Ids.UserProfile)
|
||||
self._state = UserProfileState(self)
|
||||
self._commands = Commands(self)
|
||||
|
||||
def update_dark_mode(self, client_response):
|
||||
self._state.theme = client_response.get("theme", "light")
|
||||
self._state.save()
|
||||
|
||||
def render(self):
|
||||
user_info = retrieve_user_info(self._session)
|
||||
@@ -29,14 +62,15 @@ class UserProfile(SingleInstance):
|
||||
cls="dropdown dropdown-end"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def mk_dark_mode():
|
||||
def mk_dark_mode(self):
|
||||
return Label(
|
||||
Input(type="checkbox",
|
||||
name='theme',
|
||||
aria_label='Dark',
|
||||
value='dark',
|
||||
cls='theme-controller'),
|
||||
mk.mk(Input(type="checkbox",
|
||||
name='theme',
|
||||
aria_label='Dark',
|
||||
value="dark",
|
||||
checked='true' if self._state.theme == 'dark' else None,
|
||||
cls='theme-controller'),
|
||||
command=self._commands.update_dark_mode()),
|
||||
light_mode_filled,
|
||||
dark_mode_filled,
|
||||
cls="toggle text-base-content"
|
||||
|
||||
@@ -6,6 +6,7 @@ from myfasthtml.core.utils import merge_classes
|
||||
|
||||
|
||||
class Ids:
|
||||
AuthProxy = "mf-auth-proxy"
|
||||
DbManager = "mf-dbmanager"
|
||||
Layout = "mf-layout"
|
||||
UserProfile = "mf-user-profile"
|
||||
|
||||
18
src/myfasthtml/core/AuthProxy.py
Normal file
18
src/myfasthtml/core/AuthProxy.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from myfasthtml.auth.utils import login_user, save_user_info, register_user
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
from myfasthtml.core.instances import special_session, UniqueInstance
|
||||
|
||||
|
||||
class AuthProxy(UniqueInstance):
|
||||
def __init__(self, base_url: str = None):
|
||||
super().__init__(special_session, Ids.AuthProxy)
|
||||
self._base_url = base_url
|
||||
|
||||
def login_user(self, email: str, password: str):
|
||||
return login_user(email, password, self._base_url)
|
||||
|
||||
def register_user(self, email: str, username: str, password: str):
|
||||
return register_user(email, username, password, self._base_url)
|
||||
|
||||
def save_user_info(self, access_token: str, user_profile: dict):
|
||||
return save_user_info(access_token, user_profile, self._base_url)
|
||||
@@ -1,3 +1,4 @@
|
||||
import inspect
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
@@ -40,7 +41,7 @@ class BaseCommand:
|
||||
"hx-vals": f'{{"c_id": "{self.id}"}}',
|
||||
} | self._htmx_extra
|
||||
|
||||
def execute(self):
|
||||
def execute(self, client_response: dict = None):
|
||||
raise NotImplementedError
|
||||
|
||||
def htmx(self, target="this", swap="innerHTML"):
|
||||
@@ -84,6 +85,9 @@ class BaseCommand:
|
||||
self._htmx_extra["hx-swap"] = "none"
|
||||
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return f"Command({self.name})"
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -110,8 +114,9 @@ class Command(BaseCommand):
|
||||
self.callback = callback
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.requires_client_response = 'client_response' in inspect.signature(callback).parameters
|
||||
|
||||
def execute(self):
|
||||
def execute(self, client_response: dict = None):
|
||||
ret_from_bindings = []
|
||||
|
||||
def binding_result_callback(attr, old, new, results):
|
||||
@@ -120,7 +125,10 @@ class Command(BaseCommand):
|
||||
for data in self._bindings:
|
||||
add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "", binding_result_callback)
|
||||
|
||||
ret = self.callback(*self.args, **self.kwargs)
|
||||
if self.requires_client_response:
|
||||
ret = self.callback(client_response=client_response, *self.args, **self.kwargs)
|
||||
else:
|
||||
ret = self.callback(*self.args, **self.kwargs)
|
||||
|
||||
for data in self._bindings:
|
||||
remove_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "", binding_result_callback)
|
||||
@@ -138,9 +146,6 @@ class Command(BaseCommand):
|
||||
return list(ret) + ret_from_bindings
|
||||
else:
|
||||
return [ret] + ret_from_bindings
|
||||
|
||||
def __str__(self):
|
||||
return f"Command({self.name})"
|
||||
|
||||
|
||||
class CommandsManager:
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import uuid
|
||||
|
||||
from myfasthtml.controls.helpers import Ids
|
||||
|
||||
special_session = {
|
||||
"user_info": {"id": "** SPECIAL SESSION **"}
|
||||
}
|
||||
|
||||
|
||||
class DuplicateInstanceError(Exception):
|
||||
def __init__(self, instance):
|
||||
@@ -34,6 +40,16 @@ class SingleInstance(BaseInstance):
|
||||
self._instance = None
|
||||
|
||||
|
||||
class UniqueInstance(BaseInstance):
|
||||
"""
|
||||
Base class for instances that can only have one instance at a time.
|
||||
Does not throw exception if the instance already exists, it simply overwrites it.
|
||||
"""
|
||||
|
||||
def __init__(self, session: dict, prefix: str, auto_register: bool = True):
|
||||
super().__init__(session, prefix, auto_register)
|
||||
self._instance = None
|
||||
|
||||
class MultipleInstance(BaseInstance):
|
||||
"""
|
||||
Base class for instances that can have multiple instances at a time.
|
||||
@@ -79,7 +95,10 @@ class InstancesManager:
|
||||
|
||||
return InstancesManager.instances[key]
|
||||
except KeyError:
|
||||
return instance_type(session, *args, **kwargs) # it will be automatically registered
|
||||
if instance_type:
|
||||
return instance_type(session, *args, **kwargs) # it will be automatically registered
|
||||
else:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def _get_session_id(session):
|
||||
@@ -88,3 +107,7 @@ class InstancesManager:
|
||||
if "user_info" not in session:
|
||||
return "** UNKNOWN USER **"
|
||||
return session["user_info"].get("id", "** INVALID SESSION **")
|
||||
|
||||
@staticmethod
|
||||
def get_auth_proxy():
|
||||
return InstancesManager.get(special_session, Ids.AuthProxy)
|
||||
|
||||
@@ -198,6 +198,7 @@ def retrieve_user_info(session: dict):
|
||||
"email": "** NOT LOGGED IN **",
|
||||
"username": "** NOT LOGGED IN **",
|
||||
"role": [],
|
||||
"user_settings": {}
|
||||
}
|
||||
|
||||
if "user_info" not in session:
|
||||
@@ -206,24 +207,37 @@ def retrieve_user_info(session: dict):
|
||||
"email": "** UNKNOWN USER **",
|
||||
"username": "** UNKNOWN USER **",
|
||||
"role": [],
|
||||
"user_settings": {}
|
||||
}
|
||||
|
||||
return session["user_info"]
|
||||
|
||||
|
||||
def debug_session(session):
|
||||
if session is None:
|
||||
return "None"
|
||||
|
||||
if not isinstance(session, dict):
|
||||
return str(session)
|
||||
|
||||
return session.get("user_info", {}).get("email", "** UNKNOWN USER **")
|
||||
|
||||
|
||||
@utils_rt(Routes.Commands)
|
||||
def post(session, c_id: str):
|
||||
def post(session, c_id: str, client_response: dict = None):
|
||||
"""
|
||||
Default routes for all commands.
|
||||
:param session:
|
||||
:param c_id:
|
||||
:param c_id: id of the command set
|
||||
:param client_response: extra data received from the client (from the browser)
|
||||
:return:
|
||||
"""
|
||||
logger.debug(f"Entering {Routes.Commands} with {session=}, {c_id=}")
|
||||
client_response.pop("c_id", None)
|
||||
logger.debug(f"Entering {Routes.Commands} with session='{debug_session(session)}', {c_id=}, {client_response=}")
|
||||
from myfasthtml.core.commands import CommandsManager
|
||||
command = CommandsManager.get_command(c_id)
|
||||
if command:
|
||||
return command.execute()
|
||||
return command.execute(client_response)
|
||||
|
||||
raise ValueError(f"Command with ID '{c_id}' not found.")
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from starlette.responses import Response
|
||||
|
||||
from myfasthtml.auth.routes import setup_auth_routes
|
||||
from myfasthtml.auth.utils import create_auth_beforeware
|
||||
from myfasthtml.core.AuthProxy import AuthProxy
|
||||
from myfasthtml.core.utils import utils_app
|
||||
|
||||
logger = logging.getLogger("MyFastHtml")
|
||||
@@ -96,4 +97,7 @@ def create_app(daisyui: Optional[bool] = True,
|
||||
# Setup authentication routes
|
||||
setup_auth_routes(app, rt, base_url=base_url)
|
||||
|
||||
# create the AuthProxy instance
|
||||
AuthProxy(base_url) # using the auto register mechanism to expose it
|
||||
|
||||
return app, rt
|
||||
|
||||
Reference in New Issue
Block a user