chore: build
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/omnex/control-plane/api/internal/alerts"
|
||||||
"github.com/omnex/control-plane/api/internal/auth"
|
"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))
|
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.POST("/codes", d.SubH.CreateCodeForBuy)
|
||||||
admin.GET("/profile/alerts", d.ProfileH.GetAlerts)
|
admin.GET("/profile/alerts", d.ProfileH.GetAlerts)
|
||||||
admin.POST("/profile/alerts", d.ProfileH.SetAlerts)
|
admin.POST("/profile/alerts", d.ProfileH.SetAlerts)
|
||||||
|
admin.POST("/profile/alerts/test", d.ProfileH.TestAlerts)
|
||||||
if d.DemosH != nil {
|
if d.DemosH != nil {
|
||||||
admin.POST("/demos", d.DemosH.Create)
|
admin.POST("/demos", d.DemosH.Create)
|
||||||
admin.GET("/demos", d.DemosH.List)
|
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">
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
{title}
|
{title}
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogBody color="gray.600">{children}</AlertDialogBody>
|
<AlertDialogBody>{children}</AlertDialogBody>
|
||||||
<AlertDialogFooter gap={3}>
|
<AlertDialogFooter gap={3}>
|
||||||
<Button ref={cancelRef} onClick={onClose} variant="ghost" isDisabled={isLoading}>
|
<Button ref={cancelRef} onClick={onClose} variant="ghost" isDisabled={isLoading}>
|
||||||
{cancelLabel}
|
{cancelLabel}
|
||||||
|
|||||||
@@ -147,6 +147,16 @@ export interface AlertSettings {
|
|||||||
telegram_chat_id: string
|
telegram_chat_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AlertChannelResult {
|
||||||
|
ok: boolean
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestAlertSettingsResponse {
|
||||||
|
discord?: AlertChannelResult
|
||||||
|
telegram?: AlertChannelResult
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppDownload {
|
export interface AppDownload {
|
||||||
name: string
|
name: string
|
||||||
size_bytes: number
|
size_bytes: number
|
||||||
@@ -232,6 +242,8 @@ export const api = {
|
|||||||
getAlertSettings: () => request<AlertSettings>('GET', '/profile/alerts'),
|
getAlertSettings: () => request<AlertSettings>('GET', '/profile/alerts'),
|
||||||
setAlertSettings: (settings: AlertSettings) =>
|
setAlertSettings: (settings: AlertSettings) =>
|
||||||
request<AlertSettings>('POST', '/profile/alerts', settings),
|
request<AlertSettings>('POST', '/profile/alerts', settings),
|
||||||
|
testAlertSettings: (settings: AlertSettings) =>
|
||||||
|
request<TestAlertSettingsResponse>('POST', '/profile/alerts/test', settings),
|
||||||
|
|
||||||
listAppDownloads: () => request<AppDownloadsResponse>('GET', '/apps'),
|
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 [busy, setBusy] = useState<string | null>(null)
|
||||||
const [toDelete, setToDelete] = useState<Demo | null>(null)
|
const [toDelete, setToDelete] = useState<Demo | null>(null)
|
||||||
const [toPremium, setToPremium] = useState<Demo | null>(null)
|
const [toPremium, setToPremium] = useState<Demo | null>(null)
|
||||||
|
const [toExtend, setToExtend] = useState<Demo | null>(null)
|
||||||
const [createModalOpen, setCreateModalOpen] = useState(false)
|
const [createModalOpen, setCreateModalOpen] = useState(false)
|
||||||
const [editingDomain, setEditingDomain] = useState<Demo | null>(null)
|
const [editingDomain, setEditingDomain] = useState<Demo | null>(null)
|
||||||
|
|
||||||
@@ -75,11 +76,14 @@ export function Demos() {
|
|||||||
return () => clearInterval(id)
|
return () => clearInterval(id)
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
const extend = async (d: Demo) => {
|
const confirmExtend = async () => {
|
||||||
|
if (!toExtend) return
|
||||||
|
const d = toExtend
|
||||||
setBusy(d.id)
|
setBusy(d.id)
|
||||||
try {
|
try {
|
||||||
await api.extendDemo(d.id)
|
await api.extendDemo(d.id)
|
||||||
toast({ status: 'success', title: 'Démo prolongée de 30 jours' })
|
toast({ status: 'success', title: 'Démo prolongée de 30 jours' })
|
||||||
|
setToExtend(null)
|
||||||
await load()
|
await load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof ApiError ? err.message : 'Erreur'
|
const msg = err instanceof ApiError ? err.message : 'Erreur'
|
||||||
@@ -176,7 +180,7 @@ export function Demos() {
|
|||||||
isDisabled={!isAlive(d.status) || busy === d.id}
|
isDisabled={!isAlive(d.status) || busy === d.id}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
extend(d)
|
setToExtend(d)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
+30 j
|
+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
|
<ConfirmDialog
|
||||||
isOpen={!!toDelete}
|
isOpen={!!toDelete}
|
||||||
title="Détruire la démo ?"
|
title="Détruire la démo ?"
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export function Profile() {
|
|||||||
const [telegramBotToken, setTelegramBotToken] = useState('')
|
const [telegramBotToken, setTelegramBotToken] = useState('')
|
||||||
const [telegramChatId, setTelegramChatId] = useState('')
|
const [telegramChatId, setTelegramChatId] = useState('')
|
||||||
const [savingAlerts, setSavingAlerts] = useState(false)
|
const [savingAlerts, setSavingAlerts] = useState(false)
|
||||||
|
const [testingAlerts, setTestingAlerts] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
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 handleUpdateUsername = async (newUsername: string) => {
|
||||||
const trimmed = newUsername.trim()
|
const trimmed = newUsername.trim()
|
||||||
if (!me || !trimmed || trimmed === me.username) return
|
if (!me || !trimmed || trimmed === me.username) return
|
||||||
@@ -469,7 +510,16 @@ export function Profile() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</SimpleGrid>
|
</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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
colorScheme="primary"
|
colorScheme="primary"
|
||||||
|
|||||||
Reference in New Issue
Block a user