feat: add app download
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/omnex/control-plane/api/internal/config"
|
||||
"github.com/omnex/control-plane/api/internal/db"
|
||||
"github.com/omnex/control-plane/api/internal/demos"
|
||||
"github.com/omnex/control-plane/api/internal/downloads"
|
||||
"github.com/omnex/control-plane/api/internal/k8s"
|
||||
"github.com/omnex/control-plane/api/internal/profile"
|
||||
"github.com/omnex/control-plane/api/internal/router"
|
||||
@@ -184,13 +185,14 @@ func main() {
|
||||
}
|
||||
|
||||
deps := router.Deps{
|
||||
Cfg: cfg,
|
||||
Issuer: iss,
|
||||
Sessions: sessions,
|
||||
AuthH: auth.NewHandler(userStore, sessions, iss, cfg.Secure()),
|
||||
DemosH: demos.NewHandler(demoSvc, helmProv),
|
||||
SubH: sub.NewHandler(codeStore, demoSvc),
|
||||
ProfileH: profile.NewHandler(profileStore),
|
||||
Cfg: cfg,
|
||||
Issuer: iss,
|
||||
Sessions: sessions,
|
||||
AuthH: auth.NewHandler(userStore, sessions, iss, cfg.Secure()),
|
||||
DemosH: demos.NewHandler(demoSvc, helmProv),
|
||||
SubH: sub.NewHandler(codeStore, demoSvc),
|
||||
ProfileH: profile.NewHandler(profileStore),
|
||||
DownloadsH: downloads.NewHandler(cfg.AppDownloadsDir, userStore, demoSvc),
|
||||
}
|
||||
|
||||
r := router.New(deps)
|
||||
|
||||
@@ -21,6 +21,7 @@ type Config struct {
|
||||
FrontendImage string
|
||||
BackendImage string
|
||||
LBTelegramImage string
|
||||
AppDownloadsDir string // OMNEX_APP_DOWNLOADS_DIR : répertoire des .apk téléchargeables (voir internal/downloads)
|
||||
}
|
||||
|
||||
// Load lit la config. Fail-secure : secret JWT obligatoire ; en prod base + Redis aussi.
|
||||
@@ -45,6 +46,7 @@ func Load() (Config, error) {
|
||||
FrontendImage: os.Getenv("FRONTEND_IMAGE_APP"),
|
||||
BackendImage: os.Getenv("BACKEND_IMAGE_APP"),
|
||||
LBTelegramImage: os.Getenv("LBTELEGRAM_IMAGE_APP"),
|
||||
AppDownloadsDir: getenv("OMNEX_APP_DOWNLOADS_DIR", "/app-downloads"),
|
||||
}
|
||||
|
||||
if cfg.Env == "prod" {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// 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
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/omnex/control-plane/api/internal/auth"
|
||||
"github.com/omnex/control-plane/api/internal/config"
|
||||
"github.com/omnex/control-plane/api/internal/demos"
|
||||
"github.com/omnex/control-plane/api/internal/downloads"
|
||||
"github.com/omnex/control-plane/api/internal/httpx"
|
||||
"github.com/omnex/control-plane/api/internal/profile"
|
||||
"github.com/omnex/control-plane/api/internal/session"
|
||||
@@ -17,13 +18,14 @@ import (
|
||||
|
||||
// Deps : dépendances injectées (facilite les tests).
|
||||
type Deps struct {
|
||||
Cfg config.Config
|
||||
Issuer *auth.Issuer
|
||||
Sessions session.Manager
|
||||
AuthH *auth.Handler
|
||||
DemosH *demos.Handler
|
||||
SubH *sub.Handler
|
||||
ProfileH *profile.Handler
|
||||
Cfg config.Config
|
||||
Issuer *auth.Issuer
|
||||
Sessions session.Manager
|
||||
AuthH *auth.Handler
|
||||
DemosH *demos.Handler
|
||||
SubH *sub.Handler
|
||||
ProfileH *profile.Handler
|
||||
DownloadsH *downloads.Handler
|
||||
}
|
||||
|
||||
// New construit l'engine Gin avec toute la chaîne de sécurité.
|
||||
@@ -61,6 +63,10 @@ func New(d Deps) *gin.Engine {
|
||||
if d.DemosH != nil {
|
||||
client.GET("/demos/mine", d.DemosH.ListMine)
|
||||
}
|
||||
if d.DownloadsH != nil {
|
||||
client.GET("/apps", d.DownloadsH.List)
|
||||
client.GET("/apps/:name", d.DownloadsH.Download)
|
||||
}
|
||||
}
|
||||
// Espace admin : provisioning des démos (admin uniquement).
|
||||
admin := authed.Group("")
|
||||
|
||||
Reference in New Issue
Block a user