chore: build
ci-web / test (push) Canceled after 0s
ci-api / test (push) Successful in 26m5s

This commit is contained in:
Xor290
2026-08-18 22:06:23 +02:00
parent 47a11b6601
commit e19fa4e82c
8 changed files with 167 additions and 46 deletions
@@ -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)
}
@@ -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)
-42
View File
@@ -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<React.ComponentProps<typeof ConfirmDialog>> = {}) {
const onConfirm = vi.fn()
const onClose = vi.fn()
render(
<SaasProvider>
<ConfirmDialog
isOpen
title="Détruire la démo ?"
confirmLabel="Détruire"
onConfirm={onConfirm}
onClose={onClose}
{...props}
/>
</SaasProvider>,
)
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()
})
})
+1 -1
View File
@@ -49,7 +49,7 @@ export function ConfirmDialog({
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title}
</AlertDialogHeader>
<AlertDialogBody color="gray.600">{children}</AlertDialogBody>
<AlertDialogBody>{children}</AlertDialogBody>
<AlertDialogFooter gap={3}>
<Button ref={cancelRef} onClick={onClose} variant="ghost" isDisabled={isLoading}>
{cancelLabel}
+12
View File
@@ -147,6 +147,16 @@ export interface AlertSettings {
telegram_chat_id: string
}
export interface AlertChannelResult {
ok: boolean
error?: string
}
export interface TestAlertSettingsResponse {
discord?: AlertChannelResult
telegram?: AlertChannelResult
}
export interface AppDownload {
name: string
size_bytes: number
@@ -232,6 +242,8 @@ export const api = {
getAlertSettings: () => request<AlertSettings>('GET', '/profile/alerts'),
setAlertSettings: (settings: AlertSettings) =>
request<AlertSettings>('POST', '/profile/alerts', settings),
testAlertSettings: (settings: AlertSettings) =>
request<TestAlertSettingsResponse>('POST', '/profile/alerts/test', settings),
listAppDownloads: () => request<AppDownloadsResponse>('GET', '/apps'),
}
+28
View File
@@ -0,0 +1,28 @@
import { Box, Button, Container, Heading, Stack, Text } from '@chakra-ui/react'
export function Contact() {
return (
<Box>
<Box bgGradient="linear(to-b, blackAlpha.50, transparent)" py={{ base: 16, md: 24 }}>
<Container maxW="container.lg">
<Stack spacing={6} textAlign="center" align="center">
<Heading size="2xl">Contactez-nous</Heading>
<Text fontSize="xl" color="gray.600" maxW="2xl">
Une question ? Notre équipe vous répond directement sur Telegram.
</Text>
<Button
as="a"
href="https://t.me/OMNEX_CORP"
target="_blank"
rel="noopener noreferrer"
colorScheme="primary"
size="lg"
>
Nous contacter sur Telegram
</Button>
</Stack>
</Container>
</Box>
</Box>
)
}
+21 -2
View File
@@ -46,6 +46,7 @@ export function Demos() {
const [busy, setBusy] = useState<string | null>(null)
const [toDelete, setToDelete] = useState<Demo | null>(null)
const [toPremium, setToPremium] = useState<Demo | null>(null)
const [toExtend, setToExtend] = useState<Demo | null>(null)
const [createModalOpen, setCreateModalOpen] = useState(false)
const [editingDomain, setEditingDomain] = useState<Demo | null>(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() {
</>
)}
<ConfirmDialog
isOpen={!!toExtend}
title="Prolonger la démo de 30 jours ?"
confirmLabel="Prolonger"
isLoading={!!toExtend && busy === toExtend.id}
onConfirm={confirmExtend}
onClose={() => setToExtend(null)}
>
La démo{' '}
<Text as="span" fontFamily="mono" fontWeight="semibold">
{toExtend?.namespace}
</Text>{' '}
verra sa date d'expiration repoussée de 30 jours.
</ConfirmDialog>
<ConfirmDialog
isOpen={!!toDelete}
title="Détruire la démo ?"
+51 -1
View File
@@ -116,6 +116,7 @@ export function Profile() {
const [telegramBotToken, setTelegramBotToken] = useState('')
const [telegramChatId, setTelegramChatId] = useState('')
const [savingAlerts, setSavingAlerts] = useState(false)
const [testingAlerts, setTestingAlerts] = useState(false)
useEffect(() => {
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() {
</FormControl>
</SimpleGrid>
<Flex justify="flex-end">
<Flex justify="flex-end" gap={2}>
<Button
size="sm"
variant="outline"
isDisabled={!discordWebhookUrl.trim() && !(telegramBotToken.trim() && telegramChatId.trim())}
isLoading={testingAlerts}
onClick={handleTestAlertSettings}
>
Tester les notifications
</Button>
<Button
size="sm"
colorScheme="primary"