chore: build
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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}
|
||||
|
||||
@@ -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'),
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 ?"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user