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.
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user