diff --git a/src/app.py b/src/app.py index dc223d5..913d23b 100644 --- a/src/app.py +++ b/src/app.py @@ -266,7 +266,7 @@ def _auth_nav(_): nav=True, in_navbar=True, children=[ - dbc.DropdownMenuItem("Mon compte", href="/compte"), + dbc.DropdownMenuItem("Mon compte", href="/compte/admin"), dbc.DropdownMenuItem( html.Form( method="POST", diff --git a/src/auth/routes.py b/src/auth/routes.py index d85d3bc..ecb393e 100644 --- a/src/auth/routes.py +++ b/src/auth/routes.py @@ -156,15 +156,15 @@ def change_password(): row = db.get_user_by_id(current_user.id) if not check_password_hash(row["password_hash"], current_pw): - return _redirect_with_error("/compte", "invalid_current_password") + return _redirect_with_error("/compte/admin", "invalid_current_password") if len(password) < MIN_PASSWORD_LENGTH: - return _redirect_with_error("/compte", "password_too_short") + return _redirect_with_error("/compte/admin", "password_too_short") if password != password_confirm: - return _redirect_with_error("/compte", "password_mismatch") + return _redirect_with_error("/compte/admin", "password_mismatch") db.update_password_hash(current_user.id, generate_password_hash(password)) - return redirect("/compte?password_changed=1") + return redirect("/compte/admin?password_changed=1") @auth_bp.route("/change-email", methods=["POST"]) diff --git a/src/pages/compte.py b/src/pages/compte.py index 63c8293..311fd9b 100644 --- a/src/pages/compte.py +++ b/src/pages/compte.py @@ -1,89 +1,13 @@ -import dash_bootstrap_components as dbc -from dash import dcc, html, register_page -from flask_login import current_user - -NAME = "Mon compte" +from dash import dcc, register_page register_page( __name__, path="/compte", title="Mon compte | decp.info", - name=NAME, - description="Page de gestion de compte.", + name="Mon compte", + description="Redirection vers la gestion de compte.", ) -ERROR_MESSAGES = { - "invalid_current_password": "Le mot de passe actuel est incorrect.", - "password_too_short": "Le nouveau mot de passe doit faire au moins 8 caractères.", - "password_mismatch": "Les nouveaux mots de passe ne correspondent pas.", -} - -def layout(error: str | None = None, password_changed: str | None = None, **_): - if not current_user.is_authenticated: - return dcc.Location(href="/connexion?next=/compte", id="compte-redirect") - - alerts = [] - if error and error in ERROR_MESSAGES: - alerts.append(dbc.Alert(ERROR_MESSAGES[error], color="danger")) - if password_changed == "1": - alerts.append(dbc.Alert("Mot de passe mis à jour.", color="success")) - - return dbc.Container( - className="py-4", - style={"maxWidth": "600px"}, - children=[ - html.H2("Mon compte"), - html.P([html.Strong("Email : "), current_user.email]), - *alerts, - html.H4("Changer le mot de passe", className="mt-4"), - html.Form( - method="POST", - action="/auth/change-password", - children=[ - dcc.Input( - type="hidden", - id={"type": "csrf-input", "index": "change"}, - name="csrf_token", - ), - dbc.Label("Mot de passe actuel"), - dbc.Input( - type="password", - name="current_password", - required=True, - className="mb-3", - ), - dbc.Label("Nouveau mot de passe (8 caractères minimum)"), - dbc.Input( - type="password", - name="password", - required=True, - minLength=8, - className="mb-3", - ), - dbc.Label("Confirmer le nouveau mot de passe"), - dbc.Input( - type="password", - name="password_confirm", - required=True, - minLength=8, - className="mb-3", - ), - dbc.Button("Changer", type="submit", color="primary"), - ], - ), - html.Hr(className="mt-5"), - html.Form( - method="POST", - action="/auth/logout", - children=[ - dcc.Input( - type="hidden", - id={"type": "csrf-input", "index": "logout"}, - name="csrf_token", - ), - dbc.Button("Déconnexion", type="submit", color="secondary"), - ], - ), - ], - ) +def layout(**_): + return dcc.Location(href="/compte/admin", id="compte-root-redirect") diff --git a/src/pages/compte_admin.py b/src/pages/compte_admin.py new file mode 100644 index 0000000..de7d6b4 --- /dev/null +++ b/src/pages/compte_admin.py @@ -0,0 +1,218 @@ +import dash_bootstrap_components as dbc +from dash import Input, Output, State, callback, dcc, html, register_page +from flask_login import current_user + +from src.pages._compte_shell import account_guard, account_shell + +register_page( + __name__, + path="/compte/admin", + title="Mon compte | decp.info", + name="Mon compte", + description="Gestion de votre compte decp.info.", +) + +ERROR_MESSAGES = { + "invalid_current_password": "Le mot de passe actuel est incorrect.", + "password_too_short": "Le nouveau mot de passe doit faire au moins 8 caractères.", + "password_mismatch": "Les nouveaux mots de passe ne correspondent pas.", + "invalid_email": "L'adresse email n'est pas valide.", + "email_taken": "Cette adresse email est déjà utilisée.", + "email_send_failed": "L'envoi de l'email de confirmation a échoué.", + "invalid_token": "Le lien de confirmation est invalide ou expiré.", +} + +SUCCESS_MESSAGES = { + "password_changed": "Mot de passe mis à jour.", + "email_pending": "Un email de confirmation a été envoyé à la nouvelle adresse.", + "email_changed": "Votre adresse email a été mise à jour.", +} + + +def _csrf(index: str): + return dcc.Input( + type="hidden", + id={"type": "csrf-input", "index": index}, + name="csrf_token", + ) + + +def _email_section(): + return html.Div( + [ + html.H4("Adresse email", className="mt-2"), + html.P([html.Strong("Email actuel : "), current_user.email]), + html.Form( + method="POST", + action="/auth/change-email", + children=[ + _csrf("change-email"), + dbc.Label("Nouvelle adresse email"), + dbc.Input( + type="email", name="email", required=True, className="mb-3" + ), + dbc.Button("Mettre à jour l'email", type="submit", color="primary"), + ], + ), + ] + ) + + +def _password_section(): + return html.Div( + [ + html.H4("Mot de passe", className="mt-4"), + html.Form( + method="POST", + action="/auth/change-password", + children=[ + _csrf("change-password"), + dbc.Label("Mot de passe actuel"), + dbc.Input( + type="password", + name="current_password", + required=True, + className="mb-3", + ), + dbc.Label("Nouveau mot de passe (8 caractères minimum)"), + dbc.Input( + type="password", + name="password", + required=True, + minLength=8, + className="mb-3", + ), + dbc.Label("Confirmer le nouveau mot de passe"), + dbc.Input( + type="password", + name="password_confirm", + required=True, + minLength=8, + className="mb-3", + ), + dbc.Button( + "Changer le mot de passe", type="submit", color="primary" + ), + ], + ), + ] + ) + + +def _danger_section(): + return html.Div( + [ + html.H4("Zone danger", className="mt-5 text-danger"), + html.P("La suppression de votre compte est définitive."), + dbc.Button( + "Supprimer mon compte", id="delete-open", color="danger", outline=True + ), + dbc.Modal( + id="delete-modal", + is_open=False, + children=[ + dbc.ModalHeader(dbc.ModalTitle("Supprimer le compte")), + html.Form( + method="POST", + action="/auth/delete-account", + children=[ + dbc.ModalBody( + [ + _csrf("delete-account"), + html.P( + "Cette action est définitive. " + "Saisissez votre mot de passe pour confirmer." + ), + dbc.Input( + type="password", + name="current_password", + required=True, + placeholder="Mot de passe", + ), + ] + ), + dbc.ModalFooter( + [ + dbc.Button( + "Annuler", id="delete-cancel", color="secondary" + ), + dbc.Button( + "Supprimer définitivement", + type="submit", + color="danger", + ), + ] + ), + ], + ), + ], + ), + ], + className="border border-danger rounded p-3 mt-4", + ) + + +def _logout_section(): + return html.Form( + method="POST", + action="/auth/logout", + className="mt-4", + children=[ + _csrf("logout"), + dbc.Button("Déconnexion", type="submit", color="secondary"), + ], + ) + + +def layout( + error=None, password_changed=None, email_pending=None, email_changed=None, **_ +): + guard = account_guard("/compte/admin", require_subscription=False) + if guard is not None: + return guard + + alerts = [] + if error in ERROR_MESSAGES: + alerts.append(dbc.Alert(ERROR_MESSAGES[error], color="danger")) + for flag, value in ( + ("password_changed", password_changed), + ("email_pending", email_pending), + ("email_changed", email_changed), + ): + if value == "1": + alerts.append(dbc.Alert(SUCCESS_MESSAGES[flag], color="success")) + + contenu = html.Div( + [ + html.H2("Compte"), + *alerts, + _email_section(), + html.Hr(className="mt-4"), + _password_section(), + html.Hr(className="mt-4"), + _danger_section(), + _logout_section(), + ] + ) + return account_shell("admin", contenu) + + +@callback( + Output("delete-modal", "is_open"), + Input("delete-open", "n_clicks"), + Input("delete-cancel", "n_clicks"), + State("delete-modal", "is_open"), + prevent_initial_call=True, +) +def _toggle_delete_modal(_open, _cancel, is_open): + return not is_open + + +@callback( + Output("compte-offcanvas", "is_open"), + Input("compte-offcanvas-open", "n_clicks"), + State("compte-offcanvas", "is_open"), + prevent_initial_call=True, +) +def _toggle_offcanvas(_n, is_open): + return not is_open