chore: build
This commit is contained in:
@@ -220,6 +220,24 @@ func (h *Handler) SetDomain(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extend : POST /demos/:id/extend — prolonge de 30 jours.
|
// 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) {
|
func (h *Handler) Extend(c *gin.Context) {
|
||||||
d, err := h.svc.Extend(c.Param("id"))
|
d, err := h.svc.Extend(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -180,6 +180,22 @@ func (s *Service) TransferToPaid(username string) error {
|
|||||||
return nil
|
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
|
// 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.
|
// omis s'il s'agit du 443 standard ; sinon affiché explicitement (ex.
|
||||||
// NodePort HTTPS sans LoadBalancer). Ne concerne que l'URL affichée : le
|
// 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.DELETE("/demos/:id", d.DemosH.Delete)
|
||||||
admin.POST("/demos/:id/extend", d.DemosH.Extend)
|
admin.POST("/demos/:id/extend", d.DemosH.Extend)
|
||||||
admin.POST("/demos/:id/domain", d.DemosH.SetDomain)
|
admin.POST("/demos/:id/domain", d.DemosH.SetDomain)
|
||||||
|
admin.POST("/demos/:id/premium", d.DemosH.TransferToPremium)
|
||||||
admin.POST("/demos/details", d.DemosH.ListDetails)
|
admin.POST("/demos/details", d.DemosH.ListDetails)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ export const api = {
|
|||||||
deleteDemo: (id: string) => request<Demo>('DELETE', `/demos/${id}`),
|
deleteDemo: (id: string) => request<Demo>('DELETE', `/demos/${id}`),
|
||||||
setDemoDomain: (id: string, domain: string) =>
|
setDemoDomain: (id: string, domain: string) =>
|
||||||
request<Demo>('POST', `/demos/${id}/domain`, { domain }),
|
request<Demo>('POST', `/demos/${id}/domain`, { domain }),
|
||||||
|
transferDemoToPremium: (id: string) => request<Demo>('POST', `/demos/${id}/premium`),
|
||||||
|
|
||||||
listCodes: () => request<{ items: CodeBuySub[] }>('GET', '/codes'),
|
listCodes: () => request<{ items: CodeBuySub[] }>('GET', '/codes'),
|
||||||
createCode: (username: string) =>
|
createCode: (username: string) =>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export function Demos() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
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 [createModalOpen, setCreateModalOpen] = useState(false)
|
const [createModalOpen, setCreateModalOpen] = useState(false)
|
||||||
const [editingDomain, setEditingDomain] = useState<Demo | null>(null)
|
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) => {
|
const toggleRow = async (d: Demo) => {
|
||||||
if (expandedId === d.id) {
|
if (expandedId === d.id) {
|
||||||
setExpandedId(null)
|
setExpandedId(null)
|
||||||
@@ -159,6 +181,18 @@ export function Demos() {
|
|||||||
>
|
>
|
||||||
+30 j
|
+30 j
|
||||||
</Button>
|
</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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
colorScheme="red"
|
colorScheme="red"
|
||||||
@@ -336,6 +370,23 @@ export function Demos() {
|
|||||||
libérées. Cette action est irréversible.
|
libérées. Cette action est irréversible.
|
||||||
</ConfirmDialog>
|
</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
|
<EditDomainModal
|
||||||
demo={editingDomain}
|
demo={editingDomain}
|
||||||
onClose={() => setEditingDomain(null)}
|
onClose={() => setEditingDomain(null)}
|
||||||
|
|||||||
Reference in New Issue
Block a user