Compare commits
No commits in common. "121a5b590f8a4f474d58d309bfca0b39ae1965f4" and "88b382bd7e4efa985d0473d57e61d358ac092456" have entirely different histories.
121a5b590f
...
88b382bd7e
|
@ -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
31
go.mod
|
@ -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
81
go.sum
|
@ -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=
|
|
||||||
|
|
549
main.go
549
main.go
|
@ -1,284 +1,313 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"crypto/rand"
|
||||||
"fmt"
|
"encoding/hex"
|
||||||
"io/ioutil"
|
"encoding/json"
|
||||||
"math/rand"
|
"fmt"
|
||||||
"net/http"
|
"io"
|
||||||
"os"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"log"
|
||||||
"strings"
|
"mime/multipart"
|
||||||
"time"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/joho/godotenv"
|
||||||
"github.com/google/uuid"
|
|
||||||
"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 := godotenv.Load(); err != nil {
|
if err != nil {
|
||||||
// Create .env if it doesn't exist
|
return err
|
||||||
authToken := uuid.New().String()
|
}
|
||||||
defaultPort := "8080"
|
return ioutil.WriteFile(appsFilePath, data, 0644)
|
||||||
envContent := fmt.Sprintf("AUTH_TOKEN=%s\nPORT=%s", authToken, defaultPort)
|
|
||||||
ioutil.WriteFile(".env", []byte(envContent), 0644)
|
|
||||||
godotenv.Load()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func authMiddleware() gin.HandlerFunc {
|
func generateToken() (string, error) {
|
||||||
return func(c *gin.Context) {
|
tokenBytes := make([]byte, 16)
|
||||||
authHeader := c.GetHeader("Authorization")
|
if _, err := rand.Read(tokenBytes); err != nil {
|
||||||
if authHeader == "" {
|
return "", err
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "No authorization header"})
|
}
|
||||||
c.Abort()
|
return hex.EncodeToString(tokenBytes), nil
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
token := strings.Replace(authHeader, "Bearer ", "", 1)
|
func loadOrGenerateToken() error {
|
||||||
if token != os.Getenv("AUTH_TOKEN") {
|
if err := godotenv.Load(); err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
log.Println("No .env file found. Generating a new token.")
|
||||||
c.Abort()
|
}
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
||||||
c.Next()
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
r := gin.Default()
|
if err := loadOrGenerateToken(); err != nil {
|
||||||
|
log.Fatalf("Error loading or generating token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Serve static files
|
if err := loadApps(); err != nil {
|
||||||
r.Static("/apps/files", "./apps/files")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Group routes for /apps
|
http.Handle("/uploads/", http.StripPrefix("/uploads", http.FileServer(http.Dir(uploadDir))))
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
var apps []App
|
http.HandleFunc("/apps", func(w http.ResponseWriter, r *http.Request) {
|
||||||
json.Unmarshal(data, &apps)
|
switch r.Method {
|
||||||
c.JSON(http.StatusOK, apps)
|
case http.MethodGet:
|
||||||
})
|
listAppsHandler(w, r)
|
||||||
// Handle other methods for /apps
|
case http.MethodPost:
|
||||||
apps.Handle("POST", "", methodNotAllowedHandler("GET"))
|
uploadAppHandler(w, r)
|
||||||
apps.Handle("PUT", "", methodNotAllowedHandler("GET"))
|
default:
|
||||||
apps.Handle("DELETE", "", methodNotAllowedHandler("GET"))
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
apps.Handle("PATCH", "", methodNotAllowedHandler("GET"))
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// Group routes for /uploadapp
|
http.HandleFunc("/delete", deleteAppHandler)
|
||||||
uploadapp := r.Group("/uploadapp")
|
http.HandleFunc("/editapp", editAppHandler)
|
||||||
{
|
|
||||||
uploadapp.POST("", authMiddleware(), func(c *gin.Context) {
|
port := os.Getenv("PORT")
|
||||||
metadataStr := c.PostForm("metadata")
|
fmt.Printf("Server starting on port %s\n", port)
|
||||||
file, err := c.FormFile("file")
|
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||||
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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user