From 40bcd02c363f98a274579c4809bbdec29a3bd79a Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Wed, 9 Jul 2025 17:46:47 +0700 Subject: [PATCH] added backend --- .gitignore | 1 + backend/.env | 7 ++ backend/api/addservice.go | 67 ++++++++++++++++ backend/api/services.go | 32 ++++++++ backend/api/spa.go | 38 ++++++++++ backend/go.mod | 7 ++ backend/main.go | 35 +++++++++ backend/services.json | 1 + index.html | 72 +++++++++++++++++- js/icons.js | 155 ++++++++++++++++++++------------------ 10 files changed, 340 insertions(+), 75 deletions(-) create mode 100644 .gitignore create mode 100644 backend/.env create mode 100644 backend/api/addservice.go create mode 100644 backend/api/services.go create mode 100644 backend/api/spa.go create mode 100644 backend/go.mod create mode 100644 backend/main.go create mode 100644 backend/services.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08cb523 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +go.sum diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..b85a9f6 --- /dev/null +++ b/backend/.env @@ -0,0 +1,7 @@ +ENABLE_TLS=false +ROOT_DIRECTORY=../ +INDEX_FILE=index.html +SERVER_ADDRESS=0.0.0.0 +SERVER_PORT=8090 +SERVICES_FILE=services.json +IMG_UPLOAD_PATH=../img diff --git a/backend/api/addservice.go b/backend/api/addservice.go new file mode 100644 index 0000000..e7b5579 --- /dev/null +++ b/backend/api/addservice.go @@ -0,0 +1,67 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/google/uuid" +) + +func AddService(w http.ResponseWriter, req *http.Request) { + req.ParseMultipartForm(10 << 20) + file, handler, err := req.FormFile("img") + if err != nil { + fmt.Println("error retrieving the file: ", err) + return + } + defer file.Close() + + fmt.Printf("Uploaded File: %+v\n", handler.Filename) + fmt.Printf("File Size: %+v\n", handler.Size) + fmt.Printf("MIME Header: %+v\n", handler.Header) + + dst, err := os.Create(os.Getenv("IMG_UPLOAD_PATH") + "/" + handler.Filename) + if err != nil { + fmt.Println("Error creating destination file:", err) + return + } + defer dst.Close() + if _, err := io.Copy(dst, file); err != nil { + fmt.Println("Error copying file:", err) + return + } + newService := make(map[string]any) + newService["img"] = "img/" + handler.Filename + newService["href"] = req.FormValue("href") + newService["text"] = req.FormValue("text") + newService["alt"] = req.FormValue("text") + "icon" + newService["key"] = uuid.New().String() + fmt.Println(newService) + allservices := ReadServicesFile() + var allservicesmap []map[string]any + err = json.Unmarshal([]byte(allservices), &allservicesmap) + if err != nil { + panic(err) + } + exists := false + for i := range allservicesmap { + if allservicesmap[i]["href"] == newService["href"] { + exists = true + break + } + } + if !exists { + allservicesmap = append(allservicesmap, newService) + res, err := json.Marshal(allservicesmap) + if err != nil { + panic(err) + } + os.WriteFile(os.Getenv("SERVICES_FILE"), res, 4755) + fmt.Fprintf(w, "%s", res) + return + } + fmt.Fprint(w, "{\"status\":\"already exists\"}") +} diff --git a/backend/api/services.go b/backend/api/services.go new file mode 100644 index 0000000..2c7fdd8 --- /dev/null +++ b/backend/api/services.go @@ -0,0 +1,32 @@ +package api + +import ( + "fmt" + "io" + "net/http" + "os" +) + +func ReadServicesFile() string { + data, e := os.ReadFile(os.Getenv("SERVICES_FILE")) + if e != nil { + panic(e) + } + return string(data) +} + +func Services(w http.ResponseWriter, req *http.Request) { + switch req.Method { + case "GET": + fmt.Fprintf(w, "%s", ReadServicesFile()) + case "POST": + data, err := io.ReadAll(req.Body) + if err != nil { + fmt.Fprintf(w, "Error to read body: %v", err) + return + } + fmt.Println(string(data)) + os.WriteFile(os.Getenv("SERVICES_FILE"), data, 4755) + fmt.Fprint(w, string(data)) + } +} diff --git a/backend/api/spa.go b/backend/api/spa.go new file mode 100644 index 0000000..4bcf49e --- /dev/null +++ b/backend/api/spa.go @@ -0,0 +1,38 @@ +package api + +import ( + "net/http" + "os" + "path/filepath" + "slices" + "strings" +) + +func SpaHandler(staticPath, indexPath string) http.Handler { + fs := http.FileServer(http.Dir(staticPath)) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Игнорируем API-запросы + if strings.HasPrefix(r.URL.Path, "/api/") { + http.NotFound(w, r) + return + } + + // Проверяем существование файла + filePath := filepath.Join(staticPath, r.URL.Path) + _, err := os.Stat(filePath) + + // Если файл не существует или путь - SPA-маршрут + if os.IsNotExist(err) || isSPARoute(r.URL.Path) { + r.URL.Path = "/" // Переписываем путь для index.html + } + + // Отдаем статический файл + fs.ServeHTTP(w, r) + }) +} + +func isSPARoute(path string) bool { + spaRoutes := []string{"/", "/update", "/trx"} // Добавьте свои маршруты + return slices.Contains(spaRoutes, path) +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..8bd40a6 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,7 @@ +module dshbrd + +go 1.24.5 + +require github.com/joho/godotenv v1.5.1 + +require github.com/google/uuid v1.6.0 // indirect diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..c256a7b --- /dev/null +++ b/backend/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "dshbrd/api" + "log" + "net/http" + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load(".env") + if err != nil { + log.Fatal("error loading .env file") + } + + http.Handle("/", api.SpaHandler(os.Getenv("ROOT_DIRECTORY"), os.Getenv("INDEX_FILE"))) + http.HandleFunc("/api/services", api.Services) + http.HandleFunc("/api/services/add", api.AddService) + + if os.Getenv("ENABLE_TLS") == "true" { + http.ListenAndServeTLS( + os.Getenv("SERVER_ADDRESS")+":"+os.Getenv("SERVER_PORT"), + os.Getenv("CERT_FILE"), + os.Getenv("KEY_FILE"), + nil, + ) + } else { + http.ListenAndServe( + os.Getenv("SERVER_ADDRESS")+":"+os.Getenv("SERVER_PORT"), + nil, + ) + } +} diff --git a/backend/services.json b/backend/services.json new file mode 100644 index 0000000..a00f13e --- /dev/null +++ b/backend/services.json @@ -0,0 +1 @@ +[{"alt":"telegram-icon","href":"https://web.telegram.org/k/","img":"img/telegram-icon.jpeg","key":"0","text":"Telegram"},{"alt":"github-icon","href":"https://github.com","img":"img/github-icon.png","key":"1","text":"github.com"},{"alt":"gitea-icon","href":"https://gitea.home.com:3000","img":"img/gitea-icon.png","key":"2","text":"gitea.home.com"},{"alt":"jellyfin-icon","href":"http://jellyfin.home.com:8096","img":"img/jellyfin.png","key":"3","text":"jellyfin.home.com"},{"alt":"immich-icon","href":"http://immich.home.com:2283","img":"img/immich-icon.png","key":"4","text":"immich.home.com"},{"alt":"navidrome-icon","href":"http://navidrome.home.com:4533","img":"img/navidrome-icon.png","key":"4","text":"navidrome.home.com"},{"alt":"portainer-icon","href":"https://portainer.home.com:9443","img":"img/portainer-icon.webp","key":"5","text":"portainer.home.com"},{"alt":"dnsmasq-icon","href":"https://dns.home.com","img":"img/dnsmasq-icon.jpeg","key":"6","text":"dns.home.com"},{"alt":"router-icon","href":"http://router.home.com","img":"img/router-icon.svg","key":"7","text":"router.home.com"}] \ No newline at end of file diff --git a/index.html b/index.html index c101918..f172b65 100644 --- a/index.html +++ b/index.html @@ -6,12 +6,12 @@ + - Dashboard @@ -43,7 +43,15 @@

-
+ + + diff --git a/js/icons.js b/js/icons.js index b5de7a4..8d6b829 100644 --- a/js/icons.js +++ b/js/icons.js @@ -1,77 +1,86 @@ const iconsArea = document.querySelector("#icons"); +const servicenameinput = document.querySelector("#servicenameinput"); +const servicelinkinput = document.querySelector("#servicelinkinput"); +const serviceimageinput = document.querySelector("#serviceimageinput"); +const savecardbutton = document.querySelector("#savecardbutton"); +const addcardmodalel = document.querySelector("#addcard"); +const addcardmodal = new bootstrap.Modal(addcardmodalel); -const icons = [ - { - key: 0, - href: "https://web.telegram.org/k/", - img: "img/telegram-icon.jpeg", - alt: "telegram-icon", - text: "Telegram", - }, - { - key: 1, - href: "https://github.com", - img: "img/github-icon.png", - alt: "github-icon", - text: "github.com", - }, - { - key: 0, - href: "https://gitea.home.com:3000", - img: "img/gitea-icon.png", - alt: "gitea-icon", - text: "gitea.home.com", - }, - { - key: 1, - href: "http://jellyfin.home.com:8096", - img: "img/jellyfin.png", - alt: "jellyfin-icon", - text: "jellyfin.home.com", - }, - { - key: 2, - href: "http://immich.home.com:2283", - img: "img/immich-icon.png", - alt: "immich-icon", - text: "immich.home.com", - }, - { - key: 3, - href: "http://navidrome.home.com:4533", - img: "img/navidrome-icon.png", - alt: "navidrome-icon", - text: "navidrome.home.com", - }, - { - key: 4, - href: "https://portainer.home.com:9443", - img: "img/portainer-icon.webp", - alt: "portainer-icon", - text: "portainer.home.com", - }, - { - key: 5, - href: "https://dns.home.com", - img: "img/dnsmasq-icon.jpeg", - alt: "dnsmasq-icon", - text: "dns.home.com", - }, - { - key: 7, - href: "http://router.home.com", - img: "img/router-icon.svg", - alt: "router-icon", - text: "router.home.com", - }, -]; +var icons = []; -icons.map((icon) => { - iconsArea.innerHTML += `
- ${icon.alt} -
-
${icon.text}
- Visit -
-
`; +const renderIcons = () => { + iconsArea.innerHTML = ""; + icons.map((icon) => { + iconsArea.innerHTML += `
+ ${icon.alt} +
+
${icon.text}
+
+ Visit + +
+
+
`; + }); +}; + +const getIcons = () => { + fetch(window.location.origin + "/api/services") + .then((data) => data.json()) + .then((jsondata) => { + icons = jsondata; + renderIcons(); + }); +}; + +getIcons(); + +savecardbutton.addEventListener("click", () => { + if (confirm("Add card?")) { + const file = serviceimageinput.files[0]; + if (!file) { + alert("Please select a file to upload"); + return; + } + + const formdata = new FormData(); + formdata.append("img", file); + formdata.append("text", servicenameinput.value); + formdata.append("href", servicelinkinput.value); + fetch(window.location.origin + "/api/services/add", { + method: "POST", + body: formdata, + }) + .then((data) => data.json()) + .then((jsondata) => { + if (jsondata.status === "already exists") { + alert("Service already exists"); + return; + } + icons = jsondata; + renderIcons(); + addcardmodal.hide(); + }) + .catch((error) => { + alert(error); + }); + } +}); + +iconsArea.addEventListener("click", (e) => { + if (e.target.classList.contains("delcard")) { + if (confirm("Delete card?")) { + icons.splice(icons.indexOf(icons.find((i) => i.key === e.target.id)), 1); + fetch(window.location.origin + "/api/services", { + method: "POST", + body: JSON.stringify(icons), + }) + .then((data) => data.json()) + .then((jsondata) => { + icons = jsondata; + renderIcons(); + }) + .catch((error) => alert(error)); + } + } });