2025-06-06 07:42:55 +00:00
|
|
|
|
package route
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"app/shelfly/internal/download"
|
|
|
|
|
|
"app/shelfly/internal/library"
|
|
|
|
|
|
"app/shelfly/internal/login"
|
|
|
|
|
|
"app/shelfly/internal/users"
|
|
|
|
|
|
"app/shelfly/renders"
|
2025-06-18 17:28:57 +00:00
|
|
|
|
"encoding/base64"
|
2025-06-06 07:42:55 +00:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"net/http"
|
2025-06-18 14:18:10 +00:00
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
2025-06-18 17:28:57 +00:00
|
|
|
|
"strings"
|
2025-06-18 17:23:23 +00:00
|
|
|
|
"time"
|
2025-06-19 12:04:58 +00:00
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
|
|
"app/shelfly/internal/models"
|
2025-06-18 17:23:23 +00:00
|
|
|
|
|
2025-06-06 07:42:55 +00:00
|
|
|
|
"github.com/gorilla/mux"
|
2025-06-18 17:23:23 +00:00
|
|
|
|
"golang.org/x/net/webdav"
|
2025-06-06 07:42:55 +00:00
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
|
)
|
2025-06-19 08:36:38 +00:00
|
|
|
|
|
|
|
|
|
|
func checkUserCredentials(db *gorm.DB, email string, password string) bool {
|
|
|
|
|
|
var user models.User
|
2025-06-18 14:15:29 +00:00
|
|
|
|
|
2025-06-19 08:36:38 +00:00
|
|
|
|
// On cherche l'utilisateur par email
|
|
|
|
|
|
result := db.Where("email = ?", email).First(&user)
|
|
|
|
|
|
if result.Error != nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// On vérifie le mot de passe via bcrypt comme dans ton LoginHandler
|
|
|
|
|
|
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
|
|
|
|
|
return err == nil
|
|
|
|
|
|
}
|
2025-06-06 07:42:55 +00:00
|
|
|
|
type spaHandler struct {
|
|
|
|
|
|
staticPath string
|
|
|
|
|
|
indexPath string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Routes non protégées
|
2025-06-18 17:28:57 +00:00
|
|
|
|
func checkAuth(authHeader, username, password string) bool {
|
|
|
|
|
|
const prefix = "Basic "
|
|
|
|
|
|
if !strings.HasPrefix(authHeader, prefix) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(authHeader[len(prefix):])
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
pair := strings.SplitN(string(decoded), ":", 2)
|
|
|
|
|
|
if len(pair) != 2 {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return pair[0] == username && pair[1] == password
|
|
|
|
|
|
}
|
2025-06-06 07:42:55 +00:00
|
|
|
|
|
2025-06-18 17:28:57 +00:00
|
|
|
|
func RoutesPublic(r *mux.Router, bd *gorm.DB) {
|
2025-06-18 14:15:29 +00:00
|
|
|
|
// Fichiers statiques (CSS, JS, etc.)
|
|
|
|
|
|
staticDir := "./templates/assets/"
|
|
|
|
|
|
r.PathPrefix("/templates/assets/").Handler(
|
|
|
|
|
|
http.StripPrefix("/templates/assets/", http.FileServer(http.Dir(staticDir))),
|
|
|
|
|
|
)
|
2025-06-21 16:51:32 +00:00
|
|
|
|
r.PathPrefix("/static/").Handler(
|
|
|
|
|
|
http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
|
2025-06-18 14:15:29 +00:00
|
|
|
|
|
|
|
|
|
|
// Page de login
|
|
|
|
|
|
r.HandleFunc("/login", renders.Login)
|
|
|
|
|
|
r.HandleFunc("/api/login", login.LoginHandler(bd)).Methods("POST")
|
|
|
|
|
|
r.HandleFunc("/api/scan/{id}", library.ScanFolder(bd)).Methods("GET")
|
|
|
|
|
|
r.HandleFunc("/api/download/stream", renders.HandleJobsStream(bd))
|
2025-06-18 17:28:57 +00:00
|
|
|
|
|
|
|
|
|
|
// Génération playlist
|
2025-06-18 14:15:29 +00:00
|
|
|
|
r.HandleFunc("/playlist.m3u", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
uploadDir := "/app/upload"
|
|
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "audio/x-mpegurl")
|
|
|
|
|
|
fmt.Fprintln(w, "#EXTM3U")
|
|
|
|
|
|
|
|
|
|
|
|
err := filepath.Walk(uploadDir, func(path string, info os.FileInfo, err error) error {
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if info.IsDir() {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
relPath, _ := filepath.Rel(uploadDir, path)
|
|
|
|
|
|
relPath = filepath.ToSlash(relPath)
|
2025-06-18 14:47:14 +00:00
|
|
|
|
fileURL := fmt.Sprintf("https://media.canguidev.fr/upload/%s", relPath)
|
2025-06-18 14:15:29 +00:00
|
|
|
|
fmt.Fprintln(w, fileURL)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
http.Error(w, "Erreur lors de la génération de la playlist", http.StatusInternalServerError)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2025-06-19 16:20:31 +00:00
|
|
|
|
r.PathPrefix("/webdav/").Handler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
|
|
|
|
authHeader := req.Header.Get("Authorization")
|
|
|
|
|
|
if authHeader == "" {
|
|
|
|
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
|
|
|
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Authentification HTTP Basic en base de données
|
|
|
|
|
|
email, password, ok := req.BasicAuth()
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
|
|
|
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Printf("✅ email saisi: %s", email)
|
|
|
|
|
|
log.Printf("✅ password saisi: %s", password)
|
|
|
|
|
|
|
|
|
|
|
|
var user models.User
|
|
|
|
|
|
result := bd.Where("email = ?", email).First(&user)
|
|
|
|
|
|
if result.Error != nil {
|
|
|
|
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
|
|
|
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
|
|
|
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Ici on autorise TOUTES les méthodes WebDAV (lecture/écriture/suppression)
|
|
|
|
|
|
log.Printf("✅ WebDAV FULL ACCESS for user: %s", email)
|
|
|
|
|
|
|
|
|
|
|
|
// Headers WebDAV que certains clients attendent
|
|
|
|
|
|
w.Header().Set("DAV", "1,2")
|
|
|
|
|
|
w.Header().Set("MS-Author-Via", "DAV")
|
|
|
|
|
|
|
|
|
|
|
|
// Handler WebDAV complet
|
|
|
|
|
|
webdavHandler := &webdav.Handler{
|
|
|
|
|
|
Prefix: "/webdav/",
|
|
|
|
|
|
FileSystem: webdav.Dir("/app/upload"),
|
|
|
|
|
|
LockSystem: webdav.NewMemLS(),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
webdavHandler.ServeHTTP(w, req)
|
|
|
|
|
|
}))
|
2025-06-18 17:28:57 +00:00
|
|
|
|
|
|
|
|
|
|
// WebDAV sécurisé
|
2025-06-19 08:36:38 +00:00
|
|
|
|
// username := "tonuser" // ton login
|
|
|
|
|
|
// password := "tonpassword" // ton password
|
|
|
|
|
|
|
|
|
|
|
|
// webdavHandler := &webdav.Handler{
|
|
|
|
|
|
// Prefix: "/webdav/",
|
|
|
|
|
|
// FileSystem: webdav.Dir("/app/upload"),
|
|
|
|
|
|
// LockSystem: webdav.NewMemLS(),
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
// r.PathPrefix("/webdav/").Handler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
|
|
|
|
// // Authentification
|
|
|
|
|
|
// auth := req.Header.Get("Authorization")
|
|
|
|
|
|
// if auth == "" || !checkAuth(auth, username, password) {
|
|
|
|
|
|
// w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
|
|
|
|
|
// http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
|
|
|
|
// return
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
// // Protection lecture seule
|
|
|
|
|
|
// if req.Method != "GET" && req.Method != "HEAD" && req.Method != "OPTIONS" && req.Method != "PROPFIND" {
|
|
|
|
|
|
// http.Error(w, "Read-Only", http.StatusForbidden)
|
|
|
|
|
|
// return
|
|
|
|
|
|
// }
|
|
|
|
|
|
// log.Printf("WebDAV request: %s %s", req.Method, req.URL.Path)
|
|
|
|
|
|
|
|
|
|
|
|
// // Headers WebDAV que VLC attend
|
|
|
|
|
|
// w.Header().Set("DAV", "1,2")
|
|
|
|
|
|
// w.Header().Set("MS-Author-Via", "DAV")
|
|
|
|
|
|
|
|
|
|
|
|
// webdavHandler.ServeHTTP(w, req)
|
|
|
|
|
|
// }))
|
2025-06-06 07:42:55 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Routes protégées
|
|
|
|
|
|
func RoutesProtected(r *mux.Router, bd *gorm.DB) {
|
|
|
|
|
|
|
2025-06-18 14:15:29 +00:00
|
|
|
|
// Ici on place les vues et API qui doivent être protégées
|
2025-06-06 07:42:55 +00:00
|
|
|
|
r.HandleFunc("/stream", StreamHandler)
|
2025-06-18 14:15:29 +00:00
|
|
|
|
r.HandleFunc("/dashboard", renders.Dashboard(bd))
|
2025-06-06 07:42:55 +00:00
|
|
|
|
r.HandleFunc("/settings", renders.Settings)
|
|
|
|
|
|
r.HandleFunc("/library", renders.Library)
|
2025-06-18 14:15:29 +00:00
|
|
|
|
r.HandleFunc("/menuLibary", renders.Library)
|
|
|
|
|
|
r.HandleFunc("/godownloader/downloads", renders.GoDownload)
|
|
|
|
|
|
r.HandleFunc("/godownloader/linkcollectors", renders.GoDownloadLinkCollectors)
|
|
|
|
|
|
r.HandleFunc("/godownloader/settings", renders.GoDownloadSetting(bd))
|
|
|
|
|
|
r.HandleFunc("/godownloader/poll-status", renders.PollStatusHandler(bd))
|
|
|
|
|
|
r.HandleFunc("/godownloader/table-refresh", renders.GoDownloadPartialTable(bd))
|
|
|
|
|
|
r.HandleFunc("/godownloader/settings/delete", renders.GoDownloadSettingDelete(bd))
|
2025-06-12 08:57:10 +00:00
|
|
|
|
r.HandleFunc("/api/download/add", renders.HandleAddJob(bd)).Methods("POST")
|
|
|
|
|
|
r.HandleFunc("/api/download/all", renders.HandleListJobsPartial(bd)).Methods("GET")
|
2025-06-18 14:15:29 +00:00
|
|
|
|
r.HandleFunc("/downloads", renders.GoDownload2(bd))
|
|
|
|
|
|
r.HandleFunc("/stream/{id}", download.HandleStreamPage()).Methods("GET")
|
|
|
|
|
|
r.HandleFunc("/api/download/start/{id}", renders.HandleStartJob(bd)).Methods("POST")
|
|
|
|
|
|
r.HandleFunc("/api/download/pause/{id}", renders.HandlePauseJob).Methods("POST")
|
|
|
|
|
|
r.HandleFunc("/api/download/resume/{id}", renders.HandleResumeJob(bd)).Methods("POST")
|
|
|
|
|
|
r.HandleFunc("/api/download/delete/{id}", renders.HandleDeleteJob(bd)).Methods("DELETE")
|
|
|
|
|
|
r.HandleFunc("/api/download/delete-multiple", renders.HandleDeleteMultipleJobs(bd)).Methods("POST")
|
|
|
|
|
|
|
|
|
|
|
|
// API user
|
|
|
|
|
|
r.HandleFunc("/api/user/create", users.CreateUser(bd)).Methods("POST")
|
|
|
|
|
|
r.HandleFunc("/api/user/update/{id}", users.UpdateUser(bd)).Methods("PUT")
|
|
|
|
|
|
r.HandleFunc("/api/user/delete/{id}", users.DeleteUser(bd)).Methods("DELETE")
|
|
|
|
|
|
r.HandleFunc("/api/user/all/", users.ReadAllUser(bd)).Methods("GET")
|
|
|
|
|
|
r.HandleFunc("/api/user/{id}", users.FindUserById(bd)).Methods("GET")
|
|
|
|
|
|
|
|
|
|
|
|
// API download
|
|
|
|
|
|
r.HandleFunc("/api/pathDownload/create", download.CreateSavePath(bd)).Methods("POST")
|
|
|
|
|
|
r.HandleFunc("/api/pathDownload/update/{id}", download.UpdateSavePath(bd)).Methods("PUT")
|
|
|
|
|
|
r.HandleFunc("/api/pathDownload/delete/{id}", download.DeleteSavePath(bd)).Methods("DELETE")
|
|
|
|
|
|
r.HandleFunc("/api/pathDownload/all/", download.ReadAllSavePath(bd)).Methods("GET")
|
2025-06-20 18:17:24 +00:00
|
|
|
|
r.HandleFunc("/api/download/add-multiple", renders.HandleAddJobsMultiple(bd)).Methods("POST")
|
2025-06-18 14:15:29 +00:00
|
|
|
|
//API Check path
|
|
|
|
|
|
r.HandleFunc("/validate-path", download.PathValidationHandler)
|
|
|
|
|
|
|
2025-06-20 14:26:56 +00:00
|
|
|
|
r.HandleFunc("/folders", renders.StreamHandler)
|
2025-06-20 15:09:46 +00:00
|
|
|
|
r.HandleFunc("/folders/detail", renders.DetailHandler).Methods("GET")
|
2025-06-20 13:41:37 +00:00
|
|
|
|
|
2025-06-21 16:17:16 +00:00
|
|
|
|
r.HandleFunc("/api/paths/{id:[0-9]+}/media", renders.PathMedia(bd)).Methods("GET")
|
|
|
|
|
|
r.HandleFunc("/stream/{partID:[0-9]+}", renders.Stream(bd)).Methods("GET")
|
|
|
|
|
|
r.HandleFunc("/media/{partID:[0-9]+}", renders.MediaDetail(bd)).Methods("GET")
|
|
|
|
|
|
|
2025-06-18 14:15:29 +00:00
|
|
|
|
//API Scan folder
|
2025-06-06 07:42:55 +00:00
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
func StreamHandler(w http.ResponseWriter, r *http.Request) {
|
2025-06-18 14:15:29 +00:00
|
|
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
|
|
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
|
|
|
|
w.Header().Set("Connection", "keep-alive")
|
|
|
|
|
|
|
|
|
|
|
|
flusher, ok := w.(http.Flusher)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
http.Error(w, "Le streaming n’est pas supporté par ce serveur", http.StatusInternalServerError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
|
|
|
|
// Boucle infinie (ou jusqu'à annulation)
|
|
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-ticker.C:
|
2025-06-06 07:42:55 +00:00
|
|
|
|
fmt.Fprintf(w, "data: <p>Message #%d</p>\n\n")
|
2025-06-18 14:15:29 +00:00
|
|
|
|
flusher.Flush()
|
|
|
|
|
|
case <-r.Context().Done():
|
|
|
|
|
|
// Le client a probablement fermé la connexion
|
|
|
|
|
|
log.Println("Client déconnecté")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|