55
66from fastapi_forge .dtos import Model , ModelField , ModelFieldMetadata , ModelRelationship
77from fastapi_forge .enums import FieldDataType
8- from fastapi_forge .frontend .constants import FIELD_COLUMNS , RELATIONSHIP_COLUMNS
8+ from fastapi_forge .frontend .constants import (
9+ DEFAULT_AUTH_USER_FIELDS ,
10+ FIELD_COLUMNS ,
11+ RELATIONSHIP_COLUMNS ,
12+ )
913from fastapi_forge .frontend .modals import (
1014 AddFieldModal ,
1115 AddRelationModal ,
@@ -28,6 +32,7 @@ def __init__(self):
2832 state .select_model_fn = self .set_selected_model
2933 state .deselect_model_fn = self .deselect_model
3034 state .render_model_editor_fn = self .refresh
35+ state .render_actions_fn = self ._render_action_group
3136
3237 self .add_field_modal : AddFieldModal = AddFieldModal (
3338 on_add_field = self ._handle_modal_add_field ,
@@ -56,6 +61,63 @@ def _show_code_preview(self) -> None:
5661 ui .code (code ).classes ("w-full" )
5762 modal .open ()
5863
64+ def _toggle_auth_model (self ) -> None :
65+ if not state .selected_model or not state .use_builtin_auth :
66+ return
67+
68+ if not state .selected_model .metadata .is_auth_model and any (
69+ m .metadata .is_auth_model for m in state .models
70+ ):
71+ ui .notify (
72+ "Cannot have more than one authentication model." , type = "negative"
73+ )
74+ return
75+
76+ model = state .selected_model
77+ model .metadata .is_auth_model = not model .metadata .is_auth_model
78+
79+ if not model .metadata .is_auth_model :
80+ self ._remove_auth_fields (model )
81+ if state .render_model_editor_fn :
82+ state .render_model_editor_fn ()
83+
84+ if state .render_models_fn :
85+ state .render_models_fn ()
86+ self ._render_action_group .refresh ()
87+
88+ if model .metadata .is_auth_model :
89+ self ._setup_auth_model_fields (model )
90+
91+ def _remove_auth_fields (self , model : Model ) -> None :
92+ for field_name in ("email" , "password" ):
93+ if field := next ((f for f in model .fields if f .name == field_name ), None ):
94+ model .fields .remove (field )
95+
96+ def _setup_auth_model_fields (self , model : Model ) -> None :
97+ self ._remove_auth_fields (model )
98+ id_index = 0
99+ insert_position = id_index + 1 if id_index >= 0 else 0
100+ for field in reversed (DEFAULT_AUTH_USER_FIELDS ):
101+ model .fields .insert (insert_position , field )
102+
103+ self ._refresh_table (model .fields )
104+
105+ @ui .refreshable
106+ def _render_action_group (self ) -> None :
107+ ui .button (
108+ icon = "security" ,
109+ on_click = self ._toggle_auth_model ,
110+ color = (
111+ "green"
112+ if state .use_builtin_auth
113+ and state .selected_model
114+ and state .selected_model .metadata .is_auth_model
115+ else "grey"
116+ ),
117+ ).tooltip ("Authentication model" ).bind_visibility_from (
118+ state , "use_builtin_auth"
119+ )
120+
59121 def _build (self ) -> None :
60122 with self :
61123 with ui .row ().classes ("w-full justify-between items-center" ):
@@ -67,6 +129,7 @@ def _build(self) -> None:
67129 ).tooltip ("Preview SQLAlchemy model code" )
68130
69131 with ui .row ().classes ("gap-2 items-center" ):
132+ self ._render_action_group ()
70133 with (
71134 ui .button (icon = "menu" ).tooltip ("Generate" ),
72135 ui .menu (),
@@ -373,22 +436,35 @@ def _deselect_relation(self) -> None:
373436 self .relationship_table .selected = []
374437
375438 def _on_select_field (self , selection : list [dict [str , Any ]]) -> None :
376- if not state .selected_model :
439+ if not state .selected_model or not selection :
440+ self ._deselect_field ()
377441 return
378- if not selection :
442+
443+ name = selection [0 ].get ("name" )
444+
445+ if (
446+ state .selected_model .metadata .is_auth_model
447+ and state .use_builtin_auth
448+ and name in ("password" , "email" )
449+ ):
450+ ui .notify (
451+ f"Cannot edit { name } field in authentication model." ,
452+ type = "warning" ,
453+ )
379454 self ._deselect_field ()
380455 return
381- if selection [0 ].get ("name" ) == "id" :
456+
457+ if name == "id" :
382458 self ._deselect_field ()
383- else :
384- state .selected_field = next (
385- (
386- field
387- for field in state .selected_model .fields
388- if field .name == selection [0 ]["name" ]
389- ),
390- None ,
459+ ui .notify (
460+ "Cannot edit the 'id' field, it is automatically generated." ,
461+ type = "warning" ,
391462 )
463+ return
464+
465+ state .selected_field = next (
466+ (field for field in state .selected_model .fields if field .name == name ), None
467+ )
392468
393469 def _on_select_relation (self , selection : list [dict [str , Any ]]) -> None :
394470 if not state .selected_model :
@@ -417,11 +493,18 @@ def _handle_update_field(
417493 default_value : str | None = None ,
418494 extra_kwargs : dict [str , Any ] | None = None ,
419495 ) -> None :
496+ if not state .selected_model or not state .selected_field :
497+ return
498+
499+ exclude_set = {"id" }
420500 if (
421- not state .selected_model
422- or not state .selected_field
423- or state .selected_field . name == "id"
501+ state .selected_model
502+ and state .selected_model . metadata . is_auth_model
503+ and state .use_builtin_auth
424504 ):
505+ exclude_set .update ({"email" , "password" })
506+
507+ if name in exclude_set :
425508 return
426509
427510 if state .selected_field .name != name and self ._field_name_exists (name ):
0 commit comments