diff --git a/control-plane/api/internal/profile/handler.go b/control-plane/api/internal/profile/handler.go index e6a7880..02138c6 100644 --- a/control-plane/api/internal/profile/handler.go +++ b/control-plane/api/internal/profile/handler.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/omnex/control-plane/api/internal/alerts" "github.com/omnex/control-plane/api/internal/auth" ) @@ -173,3 +174,55 @@ func (h *Handler) SetAlerts(c *gin.Context) { } c.JSON(http.StatusOK, toAlertSettingsResponse(user)) } + +type testChannelResult struct { + OK bool `json:"ok"` + Error string `json:"error,omitempty"` +} + +type testAlertsResponse struct { + Discord *testChannelResult `json:"discord,omitempty"` + Telegram *testChannelResult `json:"telegram,omitempty"` +} + +const testAlertMessage = "🔔 Test Omnex — si vous recevez ce message, ce canal d'alerte est bien configuré." + +// TestAlerts : envoie un message de test aux canaux fournis dans la requête, +// sans les persister — permet de vérifier la config (webhook Discord valide, +// bot Telegram démarré, chat_id correct...) avant d'enregistrer, ou de +// vérifier des réglages déjà enregistrés sans les ressaisir. +func (h *Handler) TestAlerts(c *gin.Context) { + var req alertSettingsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"}) + return + } + p := auth.PrincipalFrom(c) + if p == nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"}) + return + } + if req.DiscordWebhookURL == "" && (req.TelegramBotToken == "" || req.TelegramChatID == "") { + c.JSON(http.StatusBadRequest, gin.H{"error": "aucun canal renseigné"}) + return + } + + var res testAlertsResponse + if req.DiscordWebhookURL != "" { + n := &alerts.DiscordNotifier{WebhookURL: req.DiscordWebhookURL} + if err := n.Notify(c.Request.Context(), testAlertMessage); err != nil { + res.Discord = &testChannelResult{Error: err.Error()} + } else { + res.Discord = &testChannelResult{OK: true} + } + } + if req.TelegramBotToken != "" && req.TelegramChatID != "" { + n := &alerts.TelegramNotifier{BotToken: req.TelegramBotToken, ChatID: req.TelegramChatID} + if err := n.Notify(c.Request.Context(), testAlertMessage); err != nil { + res.Telegram = &testChannelResult{Error: err.Error()} + } else { + res.Telegram = &testChannelResult{OK: true} + } + } + c.JSON(http.StatusOK, res) +} diff --git a/control-plane/api/internal/router/router.go b/control-plane/api/internal/router/router.go index e161dae..cdf028e 100644 --- a/control-plane/api/internal/router/router.go +++ b/control-plane/api/internal/router/router.go @@ -76,6 +76,7 @@ func New(d Deps) *gin.Engine { admin.POST("/codes", d.SubH.CreateCodeForBuy) admin.GET("/profile/alerts", d.ProfileH.GetAlerts) admin.POST("/profile/alerts", d.ProfileH.SetAlerts) + admin.POST("/profile/alerts/test", d.ProfileH.TestAlerts) if d.DemosH != nil { admin.POST("/demos", d.DemosH.Create) admin.GET("/demos", d.DemosH.List) diff --git a/web/src/components/ConfirmDialog.test.tsx b/web/src/components/ConfirmDialog.test.tsx deleted file mode 100644 index f79c510..0000000 --- a/web/src/components/ConfirmDialog.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { SaasProvider } from '@saas-ui/react' -import { ConfirmDialog } from './ConfirmDialog' - -function renderDialog(props: Partial> = {}) { - const onConfirm = vi.fn() - const onClose = vi.fn() - render( - - - , - ) - return { onConfirm, onClose } -} - -describe('ConfirmDialog', () => { - it('appelle onConfirm au clic sur le bouton de confirmation', async () => { - const { onConfirm } = renderDialog() - await userEvent.setup().click(screen.getByRole('button', { name: /détruire/i })) - expect(onConfirm).toHaveBeenCalledOnce() - }) - - it('appelle onClose au clic sur Annuler', async () => { - const { onClose } = renderDialog() - await userEvent.setup().click(screen.getByRole('button', { name: /annuler/i })) - expect(onClose).toHaveBeenCalledOnce() - }) - - it('ne rend pas le contenu quand il est fermé', () => { - renderDialog({ isOpen: false }) - expect(screen.queryByRole('button', { name: /détruire/i })).not.toBeInTheDocument() - }) -}) diff --git a/web/src/components/ConfirmDialog.tsx b/web/src/components/ConfirmDialog.tsx index cf56988..43ab5a3 100644 --- a/web/src/components/ConfirmDialog.tsx +++ b/web/src/components/ConfirmDialog.tsx @@ -49,7 +49,7 @@ export function ConfirmDialog({ {title} - {children} + {children} + + + + + ) +} diff --git a/web/src/pages/backoffice/Demos.tsx b/web/src/pages/backoffice/Demos.tsx index d6d7563..3f7fb84 100644 --- a/web/src/pages/backoffice/Demos.tsx +++ b/web/src/pages/backoffice/Demos.tsx @@ -46,6 +46,7 @@ export function Demos() { const [busy, setBusy] = useState(null) const [toDelete, setToDelete] = useState(null) const [toPremium, setToPremium] = useState(null) + const [toExtend, setToExtend] = useState(null) const [createModalOpen, setCreateModalOpen] = useState(false) const [editingDomain, setEditingDomain] = useState(null) @@ -75,11 +76,14 @@ export function Demos() { return () => clearInterval(id) }, [load]) - const extend = async (d: Demo) => { + const confirmExtend = async () => { + if (!toExtend) return + const d = toExtend setBusy(d.id) try { await api.extendDemo(d.id) toast({ status: 'success', title: 'Démo prolongée de 30 jours' }) + setToExtend(null) await load() } catch (err) { const msg = err instanceof ApiError ? err.message : 'Erreur' @@ -176,7 +180,7 @@ export function Demos() { isDisabled={!isAlive(d.status) || busy === d.id} onClick={(e) => { e.stopPropagation() - extend(d) + setToExtend(d) }} > +30 j @@ -354,6 +358,21 @@ export function Demos() { )} + setToExtend(null)} + > + La démo{' '} + + {toExtend?.namespace} + {' '} + verra sa date d'expiration repoussée de 30 jours. + + { let cancelled = false @@ -172,6 +173,46 @@ export function Profile() { } } + const handleTestAlertSettings = async () => { + setTestingAlerts(true) + try { + const res = await api.testAlertSettings({ + discord_webhook_url: discordWebhookUrl.trim(), + telegram_bot_token: telegramBotToken.trim(), + telegram_chat_id: telegramChatId.trim(), + }) + const results = [ + res.discord && { label: 'Discord', ...res.discord }, + res.telegram && { label: 'Telegram', ...res.telegram }, + ].filter((r): r is { label: string; ok: boolean; error?: string } => !!r) + + if (results.every((r) => r.ok)) { + toast({ status: 'success', title: 'Notification de test envoyée', description: results.map((r) => r.label).join(' et ') }) + } else { + toast({ + status: 'error', + title: 'Échec du test', + description: results + .filter((r) => !r.ok) + .map((r) => `${r.label} : ${r.error}`) + .join(' — '), + }) + } + } catch (err) { + if (err instanceof ApiError && err.status === 401) { + navigate('/login') + return + } + toast({ + status: 'warning', + title: 'Test impossible', + description: err instanceof ApiError ? err.message : undefined, + }) + } finally { + setTestingAlerts(false) + } + } + const handleUpdateUsername = async (newUsername: string) => { const trimmed = newUsername.trim() if (!me || !trimmed || trimmed === me.username) return @@ -469,7 +510,16 @@ export function Profile() { - + +