diff --git a/Opencode/config/sudoers-jh-photomaton b/Opencode/config/sudoers-jh-photomaton new file mode 100644 index 0000000..90d1c5c --- /dev/null +++ b/Opencode/config/sudoers-jh-photomaton @@ -0,0 +1,18 @@ +# JH Photomaton — droits sudo sans mot de passe pour la gestion des services +# +# INSTALLATION : +# sudo cp sudoers-jh-photomaton /etc/sudoers.d/jh-photomaton +# sudo chmod 440 /etc/sudoers.d/jh-photomaton +# sudo visudo -cf /etc/sudoers.d/jh-photomaton # vérification syntaxe +# +# Ces règles permettent à l'utilisateur "pi" de redémarrer les services +# nécessaires au photomaton ainsi que de rebooter le Pi, sans mot de passe. + +pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart photobooth-app.service +pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart photobooth-kiosk.service +pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart zoraxy.service +pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart hostapd.service +pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart dnsmasq.service +pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart cups.service +pi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart jh-photomaton.service +pi ALL=(ALL) NOPASSWD: /usr/sbin/reboot diff --git a/backend/api/system_api.py b/backend/api/system_api.py index bb01483..cde33fa 100644 --- a/backend/api/system_api.py +++ b/backend/api/system_api.py @@ -79,7 +79,6 @@ async def set_delete_button(request: Request, visible: bool): Écrit userdata/private.css puis redémarre photobooth-app pour que le CSS soit pris en compte. """ - import asyncio, subprocess pb = request.app.state.photobooth_service config_svc = request.app.state.config_service @@ -93,7 +92,6 @@ async def set_delete_button(request: Request, visible: bool): async def _restart_photobooth(): - import asyncio await asyncio.sleep(0.5) proc = await asyncio.create_subprocess_exec( "sudo", "systemctl", "restart", "photobooth-app.service", @@ -107,11 +105,71 @@ async def _restart_photobooth(): @router.post("/system/restart-photobooth") async def restart_photobooth(request: Request): """Redémarre photobooth-app.service pour recharger la config (actions, etc.).""" - import asyncio asyncio.create_task(_restart_photobooth()) return {"ok": True, "message": "Redémarrage de photobooth-app en cours…"} +# ── Gestion services & système ──────────────────────────────────────────────── + +@router.get("/system/ping") +async def ping(): + """Endpoint de health-check — utilisé par le polling de reconnexion côté client.""" + return {"ok": True} + + +@router.post("/system/service/{name}/restart") +async def restart_service(name: str): + """Redémarre un service systemd autorisé.""" + if name not in _ALLOWED_SERVICES: + return JSONResponse({"ok": False, "error": f"Service '{name}' non autorisé"}, status_code=403) + asyncio.create_task(_run_systemctl_restart(name)) + return {"ok": True, "message": f"Redémarrage de {name} en cours…"} + + +async def _run_systemctl_restart(name: str): + proc = await asyncio.create_subprocess_exec( + "sudo", "systemctl", "restart", f"{name}.service", + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await proc.wait() + logger.info("%s redémarré (exit %d)", name, proc.returncode) + + +@router.post("/system/restart-self") +async def restart_self(): + """Redémarre le service jh-photomaton lui-même (avec délai pour que la réponse parte d'abord).""" + asyncio.create_task(_restart_self_delayed()) + return {"ok": True, "message": "Redémarrage de JH-Photomaton en cours…"} + + +async def _restart_self_delayed(): + await asyncio.sleep(1.5) + proc = await asyncio.create_subprocess_exec( + "sudo", "systemctl", "restart", "jh-photomaton.service", + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await proc.wait() + + +@router.post("/system/reboot") +async def reboot_pi(): + """Redémarre le Raspberry Pi (avec délai pour que la réponse parte d'abord).""" + asyncio.create_task(_reboot_delayed()) + return {"ok": True, "message": "Redémarrage du Pi en cours…"} + + +async def _reboot_delayed(): + await asyncio.sleep(2) + proc = await asyncio.create_subprocess_exec( + "sudo", "reboot", + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await proc.wait() + + @router.post("/system/screen/refresh") async def screen_refresh(request: Request): """Envoie F5 à l'écran HDMI connecté (Chromium kiosque Wayland/X11).""" diff --git a/frontend/templates/admin/dashboard.html b/frontend/templates/admin/dashboard.html index a599700..f8fafb9 100644 --- a/frontend/templates/admin/dashboard.html +++ b/frontend/templates/admin/dashboard.html @@ -142,12 +142,19 @@
| Service | Statut | |
|---|---|---|
| Service | Statut | Action |
| {{ svc.label }} | {{ 'Actif' if svc.active else 'Arrêté' }} | ++ {% if svc.name == 'jh-photomaton' %} + + {% else %} + + {% endif %} + |