chore: build
ci-api / test (push) Successful in 26m55s
ci-web / test (push) Successful in 14m57s

This commit is contained in:
Xor290
2026-08-18 14:46:05 +02:00
parent 02a5edd1b7
commit 955f2325fb
5 changed files with 87 additions and 0 deletions
@@ -220,6 +220,24 @@ func (h *Handler) SetDomain(c *gin.Context) {
}
// Extend : POST /demos/:id/extend — prolonge de 30 jours.
// TransferToPremium : POST /demos/:id/premium — passe la démo en
// abonnement payant (n'expire plus). La migration vers le namespace
// premium-<client> tourne en tâche de fond (voir Service.TransferToPaid) :
// la réponse reflète le changement de statut immédiat, pas la fin de la
// migration.
func (h *Handler) TransferToPremium(c *gin.Context) {
d, err := h.svc.TransferToPaidByID(c.Param("id"))
if err != nil {
if errors.Is(err, ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "démo introuvable"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
return
}
c.JSON(http.StatusOK, d)
}
func (h *Handler) Extend(c *gin.Context) {
d, err := h.svc.Extend(c.Param("id"))
if err != nil {
@@ -180,6 +180,22 @@ func (s *Service) TransferToPaid(username string) error {
return nil
}
// TransferToPaidByID passe en premium la démo désignée par son id (résout
// le username propriétaire puis délègue à TransferToPaid, qui opère par
// client — un client n'a qu'une démo active à la fois, cf. Create).
// Retourne la démo mise à jour (TypeAbo="premium" déjà appliqué ; la
// migration de namespace tourne en tâche de fond, voir TransferToPaid).
func (s *Service) TransferToPaidByID(id string) (Demo, error) {
d, err := s.Get(id)
if err != nil {
return Demo{}, err
}
if err := s.TransferToPaid(d.Username); err != nil {
return Demo{}, err
}
return s.Get(id)
}
// urlFor construit l'URL publique d'une démo pour un namespace donné. Port
// omis s'il s'agit du 443 standard ; sinon affiché explicitement (ex.
// NodePort HTTPS sans LoadBalancer). Ne concerne que l'URL affichée : le
@@ -83,6 +83,7 @@ func New(d Deps) *gin.Engine {
admin.DELETE("/demos/:id", d.DemosH.Delete)
admin.POST("/demos/:id/extend", d.DemosH.Extend)
admin.POST("/demos/:id/domain", d.DemosH.SetDomain)
admin.POST("/demos/:id/premium", d.DemosH.TransferToPremium)
admin.POST("/demos/details", d.DemosH.ListDetails)
}
}
+1
View File
@@ -210,6 +210,7 @@ export const api = {
deleteDemo: (id: string) => request<Demo>('DELETE', `/demos/${id}`),
setDemoDomain: (id: string, domain: string) =>
request<Demo>('POST', `/demos/${id}/domain`, { domain }),
transferDemoToPremium: (id: string) => request<Demo>('POST', `/demos/${id}/premium`),
listCodes: () => request<{ items: CodeBuySub[] }>('GET', '/codes'),
createCode: (username: string) =>
+51
View File
@@ -45,6 +45,7 @@ export function Demos() {
const [loading, setLoading] = useState(true)
const [busy, setBusy] = useState<string | null>(null)
const [toDelete, setToDelete] = useState<Demo | null>(null)
const [toPremium, setToPremium] = useState<Demo | null>(null)
const [createModalOpen, setCreateModalOpen] = useState(false)
const [editingDomain, setEditingDomain] = useState<Demo | null>(null)
@@ -105,6 +106,27 @@ export function Demos() {
}
}
const confirmTransferToPremium = async () => {
if (!toPremium) return
const d = toPremium
setBusy(d.id)
try {
await api.transferDemoToPremium(d.id)
toast({
status: 'success',
title: 'Démo passée en premium',
description: 'La migration des données tourne en tâche de fond.',
})
setToPremium(null)
await load()
} catch (err) {
const msg = err instanceof ApiError ? err.message : 'Erreur'
toast({ status: 'error', title: 'Passage en premium impossible', description: msg })
} finally {
setBusy(null)
}
}
const toggleRow = async (d: Demo) => {
if (expandedId === d.id) {
setExpandedId(null)
@@ -159,6 +181,18 @@ export function Demos() {
>
+30 j
</Button>
<Button
size="sm"
colorScheme="purple"
variant="outline"
isDisabled={!isAlive(d.status) || busy === d.id}
onClick={(e) => {
e.stopPropagation()
setToPremium(d)
}}
>
Passer en premium
</Button>
<Button
size="sm"
colorScheme="red"
@@ -336,6 +370,23 @@ export function Demos() {
libérées. Cette action est irréversible.
</ConfirmDialog>
<ConfirmDialog
isOpen={!!toPremium}
title="Passer cette démo en premium ?"
confirmLabel="Passer en premium"
confirmColorScheme="purple"
isLoading={!!toPremium && busy === toPremium.id}
onConfirm={confirmTransferToPremium}
onClose={() => setToPremium(null)}
>
La démo{' '}
<Text as="span" fontFamily="mono" fontWeight="semibold">
{toPremium?.namespace}
</Text>{' '}
n'expirera plus et sera migrée vers un namespace dédié (données conservées). Cette
opération tourne en tâche de fond et n'est pas instantanée.
</ConfirmDialog>
<EditDomainModal
demo={editingDomain}
onClose={() => setEditingDomain(null)}