package route import ( "app/shelfly/internal/download" "app/shelfly/internal/library" "app/shelfly/internal/login" "app/shelfly/internal/models" "app/shelfly/internal/users" "app/shelfly/renders" "encoding/base64" "encoding/json" "fmt" "log" "net/http" "os" "path/filepath" "strings" "time" "golang.org/x/crypto/bcrypt" "github.com/gorilla/mux" "golang.org/x/net/webdav" "gorm.io/gorm" ) func PingHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "message": "pong", "service": "shelfy", "version": "1.0.0", // optionnel }) } } func checkUserCredentials(db *gorm.DB, email string, password string) bool { var user models.User // 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 } type spaHandler struct { staticPath string indexPath string } // Routes non protégées 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 } func RoutesPublic(r *mux.Router, bd *gorm.DB) { // Fichiers statiques (CSS, JS, etc.) staticDir := "./templates/assets/" r.PathPrefix("/templates/assets/").Handler( http.StripPrefix("/templates/assets/", http.FileServer(http.Dir(staticDir))), ) r.PathPrefix("/static/").Handler( http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) // Page de login r.HandleFunc("/api/ping", PingHandler()).Methods("GET") r.HandleFunc("/login", renders.Login) r.HandleFunc("/api/login", login.LoginHandler(bd)).Methods("POST") r.HandleFunc("/api/v2/login", login.LoginHandlerApi(bd)).Methods("POST") r.HandleFunc("/api/scan/{id}", library.ScanFolder(bd)).Methods("GET") r.HandleFunc("/api/download/stream", renders.HandleJobsStream(bd)) // Génération playlist 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) fileURL := fmt.Sprintf("https://media.canguidev.fr/upload/%s", relPath) fmt.Fprintln(w, fileURL) return nil }) if err != nil { http.Error(w, "Erreur lors de la génération de la playlist", http.StatusInternalServerError) } }) 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) })) // WebDAV sécurisé // 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) // })) } // Routes protégées func RoutesProtected(r *mux.Router, bd *gorm.DB) { // Ici on place les vues et API qui doivent être protégées r.HandleFunc("/stream", StreamHandler) r.HandleFunc("/dashboard", renders.Dashboard(bd)) r.HandleFunc("/settings", renders.Settings) r.HandleFunc("/library", renders.Library) 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)) r.HandleFunc("/api/download/add", renders.HandleAddJob(bd)).Methods("POST") r.HandleFunc("/api/download/all", renders.HandleListJobsPartial(bd)).Methods("GET") 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") r.HandleFunc("/api/download/add-multiple", renders.HandleAddJobsMultiple(bd)).Methods("POST") //API Check path r.HandleFunc("/validate-path", download.PathValidationHandler) r.HandleFunc("/folders", renders.StreamHandler) r.HandleFunc("/folders/detail", renders.DetailHandler).Methods("GET") 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") r.HandleFunc("/hls/{partID:[0-9]+}/{file}", renders.HLSStream(bd)).Methods("GET") r.HandleFunc("/hls/{partID:[0-9]+}/", renders.HLSStream(bd)).Methods("GET") //API Scan folder // —————— JSON API routes apiV2 —————— r.HandleFunc("/api/v2/dashboard", renders.DashboardJSON(bd)).Methods("GET") r.HandleFunc("/api/v2/menu-library", renders.MenuLibraryJSON(bd)).Methods("GET") r.HandleFunc("/api/v2/settings", renders.SettingsJSON()).Methods("GET") r.HandleFunc("/api/v2/library", renders.LibraryJSON()).Methods("GET") r.HandleFunc("/api/v2/godownloader/download", renders.GoDownloadJSON()).Methods("GET") r.HandleFunc("/api/v2/godownloader/linkcollectors", renders.GoDownloadLinkCollectorsJSON()).Methods("GET") r.HandleFunc("/api/v2/godownloader/settings/delete", renders.GoDownloadSettingDeleteJSON(bd)).Methods("POST") r.HandleFunc("/api/v2/godownloader/settings/toggle", renders.GoDownloadSettingToggleActiveJSON(bd)).Methods("POST") r.HandleFunc("/api/v2/godownloader/settings", renders.GoDownloadSettingJSON(bd)).Methods("GET", "POST") r.HandleFunc("/api/v2/godownloader/settings/table", renders.GoDownloadPartialTableJSON(bd)).Methods("GET") r.HandleFunc("/api/v2/godownloader2", renders.GoDownload2JSON(bd)).Methods("GET") r.HandleFunc("/api/v2/add-job", renders.HandleAddJobJSON(bd)).Methods("POST") r.HandleFunc("/api/v2/jobs/list", renders.HandleListJobsPartialJSON(bd)).Methods("GET") r.HandleFunc("/api/v2/add-jobs-multiple", renders.HandleAddJobsMultipleJSON(bd)).Methods("POST") r.HandleFunc("/api/v2/stream", renders.StreamHandlerJSON()).Methods("GET") r.HandleFunc("/api/v2/pathmedia/{id}", renders.PathMediaJSON(bd)).Methods("GET") //r.HandleFunc("/api/media/detail/{partID}", renders.MediaDetailJSON(bd)).Methods("GET") } // func RoutesProtected(r *mux.Router, db *gorm.DB) { // // —————— HTML routes —————— // r.HandleFunc("/login", Login).Methods("GET") // r.HandleFunc("/dashboard", Dashboard(db)).Methods("GET") // r.HandleFunc("/menu-library", MenuLibrary(db)).Methods("GET") // r.HandleFunc("/settings", Settings).Methods("GET") // r.HandleFunc("/library", Library).Methods("GET") // r.HandleFunc("/godownloader/download", GoDownload).Methods("GET") // r.HandleFunc("/godownloader/linkcollectors", GoDownloadLinkCollectors).Methods("GET") // r.HandleFunc("/godownloader/settings/delete", GoDownloadSettingDelete(db)).Methods("GET") // r.HandleFunc("/godownloader/settings/toggle", GoDownloadSettingToggleActive(db)).Methods("GET") // r.HandleFunc("/godownloader/settings", GoDownloadSetting(db)).Methods("GET", "POST") // r.HandleFunc("/godownloader/settings/table", GoDownloadPartialTable(db)).Methods("GET") // r.HandleFunc("/godownloader2", GoDownload2(db)).Methods("GET") // r.HandleFunc("/add-job", HandleAddJob(db)).Methods("POST") // r.HandleFunc("/jobs/stream", HandleJobsStream(db)).Methods("GET") // r.HandleFunc("/jobs/list", HandleListJobsPartial(db)).Methods("GET") // r.HandleFunc("/jobs/start/{id}", HandleStartJob(db)).Methods("POST") // r.HandleFunc("/jobs/pause/{id}", HandlePauseJob).Methods("POST") // r.HandleFunc("/jobs/resume/{id}", HandleResumeJob(db)).Methods("POST") // r.HandleFunc("/jobs/delete/{id}", HandleDeleteJob(db)).Methods("POST") // r.HandleFunc("/jobs/delete-multiple", HandleDeleteMultipleJobs(db)).Methods("POST") // r.HandleFunc("/stream", StreamHandler).Methods("GET") // r.HandleFunc("/detail", DetailHandler).Methods("GET") // r.HandleFunc("/add-jobs-multiple", HandleAddJobsMultiple(db)).Methods("POST") // r.HandleFunc("/pathmedia/{id}", PathMedia(db)).Methods("GET") // r.HandleFunc("/media/detail/{partID}", MediaDetail(db)).Methods("GET") // r.PathPrefix("/hls/").Handler(HLSStream(db)) // // —————— JSON API routes —————— // r.HandleFunc("/api/dashboard", DashboardJSON(db)).Methods("GET") // r.HandleFunc("/api/menu-library", MenuLibraryJSON(db)).Methods("GET") // r.HandleFunc("/api/settings", SettingsJSON()).Methods("GET") // r.HandleFunc("/api/library", LibraryJSON()).Methods("GET") // r.HandleFunc("/api/godownloader/download", GoDownloadJSON()).Methods("GET") // r.HandleFunc("/api/godownloader/linkcollectors", GoDownloadLinkCollectorsJSON()).Methods("GET") // r.HandleFunc("/api/godownloader/settings/delete", GoDownloadSettingDeleteJSON(db)).Methods("POST") // r.HandleFunc("/api/godownloader/settings/toggle", GoDownloadSettingToggleActiveJSON(db)).Methods("POST") // r.HandleFunc("/api/godownloader/settings", GoDownloadSettingJSON(db)).Methods("GET", "POST") // r.HandleFunc("/api/godownloader/settings/table", GoDownloadPartialTableJSON(db)).Methods("GET") // r.HandleFunc("/api/godownloader2", GoDownload2JSON(db)).Methods("GET") // r.HandleFunc("/api/add-job", HandleAddJobJSON(db)).Methods("POST") // r.HandleFunc("/api/jobs/list", HandleListJobsPartialJSON(db)).Methods("GET") // r.HandleFunc("/api/add-jobs-multiple", HandleAddJobsMultipleJSON(db)).Methods("POST") // r.HandleFunc("/api/stream", StreamHandlerJSON()).Methods("GET") // r.HandleFunc("/api/pathmedia/{id}", PathMediaJSON(db)).Methods("GET") // r.HandleFunc("/api/media/detail/{partID}", MediaDetailJSON(db)).Methods("GET") // } func StreamHandler(w http.ResponseWriter, r *http.Request) { 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: fmt.Fprintf(w, "data:

Message #%d

\n\n") flusher.Flush() case <-r.Context().Done(): // Le client a probablement fermé la connexion log.Println("Client déconnecté") return } } }