136 lines
3.8 KiB
Go
136 lines
3.8 KiB
Go
// Package downloads expose les applications mobiles (.apk) téléchargeables
|
|
// par un client ayant une démo active ou un abonnement premium en cours.
|
|
// Les fichiers sont déposés manuellement (pas de build automatisé) dans le
|
|
// répertoire configuré (voir config.AppDownloadsDir) : le contenu du
|
|
// répertoire est listé dynamiquement, aucun nom de fichier n'est en dur.
|
|
package downloads
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/omnex/control-plane/api/internal/auth"
|
|
"github.com/omnex/control-plane/api/internal/demos"
|
|
)
|
|
|
|
type App struct {
|
|
Name string `json:"name"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
}
|
|
|
|
type Handler struct {
|
|
dir string
|
|
users auth.UserStore
|
|
demos *demos.Service // optionnel : nil => éligibilité basée uniquement sur l'abonnement
|
|
}
|
|
|
|
func NewHandler(dir string, users auth.UserStore, demoSvc *demos.Service) *Handler {
|
|
return &Handler{dir: dir, users: users, demos: demoSvc}
|
|
}
|
|
|
|
// eligible : démo active OU abonnement premium non expiré.
|
|
func (h *Handler) eligible(username string) (bool, error) {
|
|
if user, found := h.users.ByUsername(username); found {
|
|
if user.TypeAbo == "premium" && time.Now().UTC().Before(user.ExpiredAt) {
|
|
return true, nil
|
|
}
|
|
}
|
|
if h.demos == nil {
|
|
return false, nil
|
|
}
|
|
list, err := h.demos.ListForUser(username)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, d := range list {
|
|
if d.Status.Active() {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// List renvoie les apps disponibles et si le client courant a le droit de
|
|
// les télécharger — la liste reste visible même si non éligible, pour
|
|
// afficher un message explicite côté front plutôt qu'une page vide.
|
|
func (h *Handler) List(c *gin.Context) {
|
|
p := auth.PrincipalFrom(c)
|
|
if p == nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
|
|
return
|
|
}
|
|
ok, err := h.eligible(p.Username)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
|
|
return
|
|
}
|
|
apps, err := h.listFiles()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"eligible": ok, "items": apps})
|
|
}
|
|
|
|
// Download sert un fichier .apk du répertoire — revérifie l'éligibilité
|
|
// côté serveur (le flag "eligible" de List n'est qu'un affichage).
|
|
func (h *Handler) Download(c *gin.Context) {
|
|
p := auth.PrincipalFrom(c)
|
|
if p == nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
|
|
return
|
|
}
|
|
ok, err := h.eligible(p.Username)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
|
|
return
|
|
}
|
|
if !ok {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "démo ou abonnement actif requis"})
|
|
return
|
|
}
|
|
|
|
name := c.Param("name")
|
|
// Un seul segment de chemin, extension .apk uniquement : exclut toute
|
|
// tentative de traversée de répertoire (pas de "/" ni "\\" autorisés).
|
|
if name == "" || strings.ContainsAny(name, "/\\") || filepath.Ext(name) != ".apk" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "nom de fichier invalide"})
|
|
return
|
|
}
|
|
full := filepath.Join(h.dir, name)
|
|
if _, err := os.Stat(full); err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "fichier introuvable"})
|
|
return
|
|
}
|
|
c.FileAttachment(full, name)
|
|
}
|
|
|
|
func (h *Handler) listFiles() ([]App, error) {
|
|
entries, err := os.ReadDir(h.dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return []App{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
out := make([]App, 0, len(entries))
|
|
for _, e := range entries {
|
|
if e.IsDir() || filepath.Ext(e.Name()) != ".apk" {
|
|
continue
|
|
}
|
|
info, err := e.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
out = append(out, App{Name: e.Name(), SizeBytes: info.Size()})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out, nil
|
|
}
|