Compare commits

..

No commits in common. "121a5b590f8a4f474d58d309bfca0b39ae1965f4" and "88b382bd7e4efa985d0473d57e61d358ac092456" have entirely different histories.

4 changed files with 293 additions and 374 deletions

View File

@ -1,5 +1,7 @@
# WebDesk 3rd party App Market server # WebDesk 3rd party App Market server
# THIS APP MARKET SERVER IS IN BETA AND MISSING FEATURES SO DON'T USE IT YET FOR PROD
This allows you to host custom (known as 3rd party) App Market repositories of WebDesk applications. This allows you to host custom (known as 3rd party) App Market repositories of WebDesk applications.
## Features ## Features
@ -41,11 +43,11 @@ In the .env file this is the only thing you can set
``` ```
PORT=8080 PORT=8080
AUTH_TOKEN=bearer-token-here # The program will automatically make a new token if not found so do not bother putting your own one 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 # Client demonstrations
[Go](https://git.fluffy.pw/matu6968/webdesk-app-market-client) [JavaScript](https://git.fluffy.pw/matu6968/webdesk-app-market-server/src/branch/main/client-demo/demo.js)
## Autostart with systemd or OpenRC ## Autostart with systemd or OpenRC

31
go.mod
View File

@ -3,34 +3,3 @@ module main.go
go 1.23.1 go 1.23.1
require github.com/joho/godotenv v1.5.1 require github.com/joho/godotenv v1.5.1
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

81
go.sum
View File

@ -1,83 +1,2 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

523
main.go
View File

@ -1,284 +1,313 @@
package main package main
import ( import (
"crypto/rand"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"math/rand" "log"
"mime/multipart"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"strings" "strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
type App struct { type App struct {
Name string `json:"name"` Name string `json:"name"`
Ver string `json:"ver"` Version string `json:"ver"`
AppID string `json:"appid"` ID string `json:"appid"`
Info string `json:"info"` Info string `json:"info"`
Pub string `json:"pub"` Developer string `json:"pub"`
Path string `json:"path"` Path string `json:"path"`
} }
type AppMetadata struct { var (
Name string `json:"name"` apps []App
Ver string `json:"ver"` appsFilePath = "apps.json"
Info string `json:"info"` uploadDir = "uploads"
Pub string `json:"pub"` bearerToken string
mutex sync.Mutex
)
func loadApps() error {
data, err := ioutil.ReadFile(appsFilePath)
if err != nil {
return err
}
return json.Unmarshal(data, &apps)
} }
func init() { func saveApps() error {
// Load .env file data, err := json.MarshalIndent(apps, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(appsFilePath, data, 0644)
}
func generateToken() (string, error) {
tokenBytes := make([]byte, 16)
if _, err := rand.Read(tokenBytes); err != nil {
return "", err
}
return hex.EncodeToString(tokenBytes), nil
}
func loadOrGenerateToken() error {
if err := godotenv.Load(); err != nil { if err := godotenv.Load(); err != nil {
// Create .env if it doesn't exist log.Println("No .env file found. Generating a new token.")
authToken := uuid.New().String()
defaultPort := "8080"
envContent := fmt.Sprintf("AUTH_TOKEN=%s\nPORT=%s", authToken, defaultPort)
ioutil.WriteFile(".env", []byte(envContent), 0644)
godotenv.Load()
} }
} bearerToken = os.Getenv("TOKEN")
if bearerToken == "" {
func authMiddleware() gin.HandlerFunc { var err error
return func(c *gin.Context) { bearerToken, err = generateToken()
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "No authorization header"})
c.Abort()
return
}
token := strings.Replace(authHeader, "Bearer ", "", 1)
if token != os.Getenv("AUTH_TOKEN") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
c.Abort()
return
}
c.Next()
}
}
func main() {
r := gin.Default()
// Serve static files
r.Static("/apps/files", "./apps/files")
// Group routes for /apps
apps := r.Group("/apps")
{
apps.GET("", func(c *gin.Context) {
data, err := ioutil.ReadFile("apps.json")
if err != nil { if err != nil {
c.JSON(http.StatusOK, []App{}) return fmt.Errorf("failed to generate token: %v", err)
return
} }
err = saveTokenToEnv(bearerToken)
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"))
}
// 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 { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "No file provided"}) return fmt.Errorf("failed to save token to .env: %v", err)
return
} }
log.Printf("Generated new token: %s\n", bearerToken)
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") port := os.Getenv("PORT")
if port == "" { if port == "" {
port = "8080" port = "8080"
} }
r.Run(":" + port) return nil
} }
// Helper function to create method not allowed handlers func saveTokenToEnv(token string) error {
func methodNotAllowedHandler(allowedMethod string) gin.HandlerFunc { return ioutil.WriteFile(".env", []byte("TOKEN="+token), 0644)
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), 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)
}
func main() {
if err := loadOrGenerateToken(); err != nil {
log.Fatalf("Error loading or generating token: %v", err)
}
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)
}
}
http.Handle("/uploads/", http.StripPrefix("/uploads", http.FileServer(http.Dir(uploadDir))))
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)
}
}) })
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))
} }
}