diff --git a/README.md b/README.md index f948613..e0340d2 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,11 @@ In the .env file this is the only thing you can set ``` PORT=8080 -TOKEN=bearer-token-here # The program will automatically make a new token if not found so do not bother putting your own one +AUTH_TOKEN=bearer-token-here # The program will automatically make a new token if not found so do not bother putting your own one ``` # Client demonstrations -[JavaScript](https://git.fluffy.pw/matu6968/webdesk-app-market-server/src/branch/main/client-demo/demo.js) +[Go](https://git.fluffy.pw/matu6968/webdesk-app-market-client) ## Autostart with systemd or OpenRC diff --git a/main.go b/main.go index 9a90e39..87cfa45 100644 --- a/main.go +++ b/main.go @@ -1,313 +1,284 @@ package main import ( - "crypto/rand" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "log" - "mime/multipart" - "net/http" - "os" - "path/filepath" - "sync" - "strings" + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "net/http" + "os" + "path/filepath" + "strings" + "time" - "github.com/joho/godotenv" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/joho/godotenv" ) type App struct { - Name string `json:"name"` - Version string `json:"ver"` - ID string `json:"appid"` - Info string `json:"info"` - Developer string `json:"pub"` - Path string `json:"path"` + Name string `json:"name"` + Ver string `json:"ver"` + AppID string `json:"appid"` + Info string `json:"info"` + Pub string `json:"pub"` + Path string `json:"path"` } -var ( - apps []App - appsFilePath = "apps.json" - uploadDir = "uploads" - bearerToken string - mutex sync.Mutex -) - -func loadApps() error { - data, err := ioutil.ReadFile(appsFilePath) - if err != nil { - return err - } - return json.Unmarshal(data, &apps) +type AppMetadata struct { + Name string `json:"name"` + Ver string `json:"ver"` + Info string `json:"info"` + Pub string `json:"pub"` } -func saveApps() error { - data, err := json.MarshalIndent(apps, "", " ") - if err != nil { - return err - } - return ioutil.WriteFile(appsFilePath, data, 0644) +func init() { + // Load .env file + if err := godotenv.Load(); err != nil { + // Create .env if it doesn't exist + authToken := uuid.New().String() + defaultPort := "8080" + envContent := fmt.Sprintf("AUTH_TOKEN=%s\nPORT=%s", authToken, defaultPort) + ioutil.WriteFile(".env", []byte(envContent), 0644) + godotenv.Load() + } } -func generateToken() (string, error) { - tokenBytes := make([]byte, 16) - if _, err := rand.Read(tokenBytes); err != nil { - return "", err - } - return hex.EncodeToString(tokenBytes), nil -} +func authMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "No authorization header"}) + c.Abort() + return + } -func loadOrGenerateToken() error { - if err := godotenv.Load(); err != nil { - log.Println("No .env file found. Generating a new token.") - } - bearerToken = os.Getenv("TOKEN") - if bearerToken == "" { - var err error - bearerToken, err = generateToken() - if err != nil { - return fmt.Errorf("failed to generate token: %v", err) - } - err = saveTokenToEnv(bearerToken) - if err != nil { - return fmt.Errorf("failed to save token to .env: %v", err) - } - log.Printf("Generated new token: %s\n", bearerToken) - } - port := os.Getenv("PORT") - if port == "" { - port = "8080" - } - return nil -} + token := strings.Replace(authHeader, "Bearer ", "", 1) + if token != os.Getenv("AUTH_TOKEN") { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + c.Abort() + return + } -func saveTokenToEnv(token string) error { - return ioutil.WriteFile(".env", []byte("TOKEN="+token), 0644) -} - -func listAppsHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - mutex.Lock() - defer mutex.Unlock() - json.NewEncoder(w).Encode(apps) -} - -func saveUploadedFile(customPath, fileName string, file multipart.File) (string, error) { - appDir := filepath.Join(uploadDir, customPath) - if err := os.MkdirAll(appDir, 0755); err != nil { - return "", err - } - - filePath := filepath.Join(appDir, fileName) - dst, err := os.Create(filePath) - if err != nil { - return "", err - } - defer dst.Close() - - if _, err := io.Copy(dst, file); err != nil { - return "", err - } - - return fmt.Sprintf("/%s/%s/%s", uploadDir, customPath, fileName), nil -} - -func uploadAppHandler(w http.ResponseWriter, r *http.Request) { - token := r.Header.Get("Authorization") - if token != "Bearer "+bearerToken { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - if err := r.ParseMultipartForm(10 << 20); err != nil { - http.Error(w, "Invalid form data", http.StatusBadRequest) - return - } - - metadata := r.FormValue("metadata") - var newApp App - if err := json.Unmarshal([]byte(metadata), &newApp); err != nil { - http.Error(w, "Invalid metadata JSON", http.StatusBadRequest) - return - } - - file, handler, err := r.FormFile("file") - if err != nil { - http.Error(w, "File upload error", http.StatusBadRequest) - return - } - defer file.Close() - - customPath := r.FormValue("customPath") - if customPath == "" { - customPath = strings.ReplaceAll(newApp.Name, " ", "_") - } - - filePath, err := saveUploadedFile(customPath, handler.Filename, file) - if err != nil { - http.Error(w, "Failed to save file", http.StatusInternalServerError) - return - } - newApp.Path = filePath - - mutex.Lock() - apps = append(apps, newApp) - if err := saveApps(); err != nil { - mutex.Unlock() - http.Error(w, "Failed to save app data", http.StatusInternalServerError) - return - } - mutex.Unlock() - - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(newApp) -} - -func deleteAppHandler(w http.ResponseWriter, r *http.Request) { - token := r.Header.Get("Authorization") - if token != "Bearer "+bearerToken { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - appID := r.URL.Query().Get("id") - if appID == "" { - http.Error(w, "App ID is required", http.StatusBadRequest) - return - } - - mutex.Lock() - defer mutex.Unlock() - - for i, app := range apps { - if app.ID == appID { - // Remove the app's file if it exists - if err := os.Remove(filepath.Join(".", app.Path)); err != nil { - log.Printf("Failed to delete file: %v\n", err) - } - - // Remove the app from the list - apps = append(apps[:i], apps[i+1:]...) - if err := saveApps(); err != nil { - http.Error(w, "Failed to save apps", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte("App deleted successfully")) - return - } - } - - http.Error(w, "App not found", http.StatusNotFound) -} - -func editAppHandler(w http.ResponseWriter, r *http.Request) { - token := r.Header.Get("Authorization") - if token != "Bearer "+bearerToken { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - if err := r.ParseMultipartForm(10 << 20); err != nil { - http.Error(w, "Invalid form data", http.StatusBadRequest) - return - } - - appID := r.FormValue("id") - if appID == "" { - http.Error(w, "App ID is required", http.StatusBadRequest) - return - } - - var updatedApp *App - mutex.Lock() - for i := range apps { - if apps[i].ID == appID { - updatedApp = &apps[i] - break - } - } - mutex.Unlock() - - if updatedApp == nil { - http.Error(w, "App not found", http.StatusNotFound) - return - } - - if name := r.FormValue("name"); name != "" { - updatedApp.Name = name - } - - if info := r.FormValue("info"); info != "" { - updatedApp.Info = info - } - - // Check if a new file is uploaded - file, handler, err := r.FormFile("file") - if err == nil { - defer file.Close() - - customPath := r.FormValue("customPath") - if customPath == "" { - customPath = strings.ReplaceAll(updatedApp.Name, " ", "_") - } - - filePath, err := saveUploadedFile(customPath, handler.Filename, file) - if err != nil { - http.Error(w, "Failed to save file", http.StatusInternalServerError) - return - } - - // Remove the old file - os.Remove(filepath.Join(".", updatedApp.Path)) - updatedApp.Path = filePath - } - - mutex.Lock() - defer mutex.Unlock() - if err := saveApps(); err != nil { - http.Error(w, "Failed to save app data", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(updatedApp) + c.Next() + } } func main() { - if err := loadOrGenerateToken(); err != nil { - log.Fatalf("Error loading or generating token: %v", err) - } + r := gin.Default() - if err := loadApps(); err != nil { - if os.IsNotExist(err) { - log.Println("apps.json not found. Starting with an empty app list.") - } else { - log.Fatalf("Failed to load apps.json: %v", err) - } - } + // Serve static files + r.Static("/apps/files", "./apps/files") - http.Handle("/uploads/", http.StripPrefix("/uploads", http.FileServer(http.Dir(uploadDir)))) + // Group routes for /apps + apps := r.Group("/apps") + { + apps.GET("", func(c *gin.Context) { + data, err := ioutil.ReadFile("apps.json") + if err != nil { + c.JSON(http.StatusOK, []App{}) + return + } - http.HandleFunc("/apps", func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - listAppsHandler(w, r) - case http.MethodPost: - uploadAppHandler(w, r) - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } - }) + var apps []App + json.Unmarshal(data, &apps) + c.JSON(http.StatusOK, apps) + }) + // Handle other methods for /apps + apps.Handle("POST", "", methodNotAllowedHandler("GET")) + apps.Handle("PUT", "", methodNotAllowedHandler("GET")) + apps.Handle("DELETE", "", methodNotAllowedHandler("GET")) + apps.Handle("PATCH", "", methodNotAllowedHandler("GET")) + } - http.HandleFunc("/delete", deleteAppHandler) - http.HandleFunc("/editapp", editAppHandler) - - port := os.Getenv("PORT") - fmt.Printf("Server starting on port %s\n", port) - log.Fatal(http.ListenAndServe(":"+port, nil)) + // Group routes for /uploadapp + uploadapp := r.Group("/uploadapp") + { + uploadapp.POST("", authMiddleware(), func(c *gin.Context) { + metadataStr := c.PostForm("metadata") + file, err := c.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"}) + return + } + + var metadata AppMetadata + if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid metadata"}) + return + } + + // Generate 12-digit app ID + rand.Seed(time.Now().UnixNano()) + appID := fmt.Sprintf("%012d", rand.Intn(1000000000000)) + + // Create new app entry + newApp := App{ + Name: metadata.Name, + Ver: metadata.Ver, + AppID: appID, + Info: metadata.Info, + Pub: metadata.Pub, + Path: fmt.Sprintf("/apps/files/%s_%s%s", metadata.Name, metadata.Ver, filepath.Ext(file.Filename)), + } + + // Save file + if err := os.MkdirAll("apps/files", 0755); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create directory"}) + return + } + + if err := c.SaveUploadedFile(file, "."+newApp.Path); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) + return + } + + // Update apps.json + var apps []App + data, _ := ioutil.ReadFile("apps.json") + json.Unmarshal(data, &apps) + apps = append(apps, newApp) + appsJSON, _ := json.Marshal(apps) + ioutil.WriteFile("apps.json", appsJSON, 0644) + + c.JSON(http.StatusOK, newApp) + }) + uploadapp.Handle("GET", "", methodNotAllowedHandler("POST")) + uploadapp.Handle("PUT", "", methodNotAllowedHandler("POST")) + uploadapp.Handle("DELETE", "", methodNotAllowedHandler("POST")) + uploadapp.Handle("PATCH", "", methodNotAllowedHandler("POST")) + } + + // Group routes for /editapp + editapp := r.Group("/editapp") + { + editapp.PUT("", authMiddleware(), func(c *gin.Context) { + // Get metadata from form + metadataStr := c.PostForm("metadata") + if metadataStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Metadata is required"}) + return + } + + var updateData struct { + AppID string `json:"appid"` + App AppMetadata `json:"app"` + } + if err := json.Unmarshal([]byte(metadataStr), &updateData); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid metadata format"}) + return + } + + var apps []App + data, _ := ioutil.ReadFile("apps.json") + json.Unmarshal(data, &apps) + + found := false + var oldFilePath string + for i := range apps { + if apps[i].AppID == updateData.AppID { + oldFilePath = "." + apps[i].Path + apps[i].Name = updateData.App.Name + apps[i].Ver = updateData.App.Ver + apps[i].Info = updateData.App.Info + + // Handle file update if provided + if file, err := c.FormFile("file"); err == nil { + // Delete old file + os.Remove(oldFilePath) + + // Generate new path + newPath := fmt.Sprintf("/apps/files/%s_%s%s", + apps[i].Name, apps[i].Ver, filepath.Ext(file.Filename)) + apps[i].Path = newPath + + // Save new file + if err := c.SaveUploadedFile(file, "."+newPath); err != nil { + c.JSON(http.StatusInternalServerError, + gin.H{"error": "Failed to save new file"}) + return + } + } + + apps[i].Pub = updateData.App.Pub + found = true + break + } + } + + if !found { + c.JSON(http.StatusNotFound, gin.H{"error": "App not found"}) + return + } + + appsJSON, _ := json.Marshal(apps) + ioutil.WriteFile("apps.json", appsJSON, 0644) + c.JSON(http.StatusOK, gin.H{"message": "App updated successfully"}) + }) + editapp.Handle("GET", "", methodNotAllowedHandler("PUT")) + editapp.Handle("POST", "", methodNotAllowedHandler("PUT")) + editapp.Handle("DELETE", "", methodNotAllowedHandler("PUT")) + editapp.Handle("PATCH", "", methodNotAllowedHandler("PUT")) + } + + // Group routes for /deleteapp + deleteapp := r.Group("/deleteapp") + { + deleteapp.DELETE("", authMiddleware(), func(c *gin.Context) { + var request struct { + AppID string `json:"appid"` + } + if err := c.BindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + var apps []App + data, _ := ioutil.ReadFile("apps.json") + json.Unmarshal(data, &apps) + + found := false + var filePath string + for i := range apps { + if apps[i].AppID == request.AppID { + filePath = "." + apps[i].Path + apps = append(apps[:i], apps[i+1:]...) + found = true + break + } + } + + if !found { + c.JSON(http.StatusNotFound, gin.H{"error": "App not found"}) + return + } + + // Delete the file + os.Remove(filePath) + + // Update apps.json + appsJSON, _ := json.Marshal(apps) + ioutil.WriteFile("apps.json", appsJSON, 0644) + c.JSON(http.StatusOK, gin.H{"message": "App deleted successfully"}) + }) + deleteapp.Handle("GET", "", methodNotAllowedHandler("DELETE")) + deleteapp.Handle("POST", "", methodNotAllowedHandler("DELETE")) + deleteapp.Handle("PUT", "", methodNotAllowedHandler("DELETE")) + deleteapp.Handle("PATCH", "", methodNotAllowedHandler("DELETE")) + } + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + r.Run(":" + port) } +// Helper function to create method not allowed handlers +func methodNotAllowedHandler(allowedMethod string) gin.HandlerFunc { + return func(c *gin.Context) { + c.JSON(http.StatusMethodNotAllowed, gin.H{ + "error": fmt.Sprintf("Method not allowed. Only %s is supported for this endpoint.", allowedMethod), + }) + } +}