mirror of
https://github.com/Alexander-D-Karpov/webring.git
synced 2026-03-16 22:07:41 +03:00
278 lines
7.1 KiB
Go
278 lines
7.1 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"github.com/lib/pq"
|
|
"html/template"
|
|
"log"
|
|
"math"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"sync"
|
|
"webring/internal/favicon"
|
|
|
|
"webring/internal/models"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
var slugRegex = regexp.MustCompile(`^(?:[a-z0-9-]{3,50}|[0-9]+)$`)
|
|
var (
|
|
templates *template.Template
|
|
templatesMu sync.RWMutex
|
|
)
|
|
|
|
func InitTemplates(t *template.Template) {
|
|
templatesMu.Lock()
|
|
defer templatesMu.Unlock()
|
|
templates = t
|
|
}
|
|
|
|
func RegisterHandlers(r *mux.Router, db *sql.DB) {
|
|
dashboardRouter := r.PathPrefix("/dashboard").Subrouter()
|
|
dashboardRouter.Use(basicAuthMiddleware)
|
|
|
|
dashboardRouter.HandleFunc("", dashboardHandler(db)).Methods("GET")
|
|
dashboardRouter.HandleFunc("/add", addSiteHandler(db)).Methods("POST")
|
|
dashboardRouter.HandleFunc("/remove/{id}", removeSiteHandler(db)).Methods("POST")
|
|
dashboardRouter.HandleFunc("/update/{id}", updateSiteHandler(db)).Methods("POST")
|
|
dashboardRouter.HandleFunc("/reorder/{id}/{offset}", reorderSiteHandler(db)).Methods("POST")
|
|
}
|
|
|
|
func basicAuthMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
user, pass, ok := r.BasicAuth()
|
|
if !ok || user != os.Getenv("DASHBOARD_USER") || pass != os.Getenv("DASHBOARD_PASSWORD") {
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func dashboardHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
templatesMu.RLock()
|
|
t := templates
|
|
templatesMu.RUnlock()
|
|
|
|
if t == nil {
|
|
log.Println("Templates not initialized")
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
sites, err := getAllSites(db)
|
|
if err != nil {
|
|
log.Printf("Error fetching sites: %v", err)
|
|
http.Error(w, "Error fetching sites", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
err = t.ExecuteTemplate(w, "dashboard.html", sites)
|
|
if err != nil {
|
|
log.Printf("Error rendering template: %v", err)
|
|
http.Error(w, "Error rendering template", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
func addSiteHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.FormValue("id")
|
|
slug := r.FormValue("slug")
|
|
name := r.FormValue("name")
|
|
url := r.FormValue("url")
|
|
|
|
if slug == "" || idStr == "" || name == "" || url == "" {
|
|
http.Error(w, "ID, Slug, Name, and URL are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if !slugRegex.MatchString(slug) {
|
|
http.Error(w, "Invalid Slug", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
result, err := db.Exec("INSERT INTO sites (id, slug, name, url) VALUES ($1, $2, $3, $4)", id, slug, name, url)
|
|
if err != nil {
|
|
var pqErr *pq.Error
|
|
if errors.As(err, &pqErr) {
|
|
if pqErr.Code.Name() == "unique_violation" {
|
|
http.Error(w, "Slug or ID is already in use", http.StatusConflict)
|
|
return
|
|
}
|
|
}
|
|
http.Error(w, "Error adding site", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
insertedID, _ := result.LastInsertId()
|
|
|
|
// Start a goroutine to fetch and store the favicon
|
|
go func() {
|
|
mediaFolder := os.Getenv("MEDIA_FOLDER")
|
|
if mediaFolder == "" {
|
|
mediaFolder = "media"
|
|
}
|
|
|
|
faviconPath, err := favicon.GetAndStoreFavicon(url, mediaFolder, int(insertedID))
|
|
if err != nil {
|
|
log.Printf("Error retrieving favicon for %s: %v", url, err)
|
|
return
|
|
}
|
|
|
|
_, err = db.Exec("UPDATE sites SET favicon = $1 WHERE id = $2", faviconPath, insertedID)
|
|
if err != nil {
|
|
log.Printf("Error updating favicon for site %d: %v", insertedID, err)
|
|
}
|
|
}()
|
|
|
|
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func removeSiteHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id := mux.Vars(r)["id"]
|
|
_, err := db.Exec("DELETE FROM sites WHERE id = $1", id)
|
|
if err != nil {
|
|
http.Error(w, "Error removing site", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func updateSiteHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id := mux.Vars(r)["id"]
|
|
slug := r.FormValue("slug")
|
|
name := r.FormValue("name")
|
|
url := r.FormValue("url")
|
|
|
|
if slug == "" || name == "" || url == "" {
|
|
http.Error(w, "Slug, Name and URL are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if !slugRegex.MatchString(slug) {
|
|
http.Error(w, "Invalid Slug", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
_, err := db.Exec("UPDATE sites SET slug = $1, name = $2, url = $3 WHERE id = $4", slug, name, url, id)
|
|
if err != nil {
|
|
var pqErr *pq.Error
|
|
if errors.As(err, &pqErr) {
|
|
if pqErr.Code.Name() == "unique_violation" {
|
|
http.Error(w, "Slug or ID is already in use", http.StatusConflict)
|
|
return
|
|
}
|
|
}
|
|
http.Error(w, "Error updating site", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
mediaFolder := os.Getenv("MEDIA_FOLDER")
|
|
if mediaFolder == "" {
|
|
mediaFolder = "media"
|
|
}
|
|
|
|
siteId, _ := strconv.Atoi(id)
|
|
faviconPath, err := favicon.GetAndStoreFavicon(url, mediaFolder, siteId)
|
|
if err != nil {
|
|
log.Printf("Error retrieving favicon for %s: %v", url, err)
|
|
return
|
|
}
|
|
|
|
_, err = db.Exec("UPDATE sites SET favicon = $1 WHERE id = $2", faviconPath, id)
|
|
if err != nil {
|
|
log.Printf("Error updating favicon for site %d: %v", id, err)
|
|
}
|
|
}()
|
|
|
|
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func reorderSiteHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
idStr := mux.Vars(r)["id"]
|
|
offsetStr := mux.Vars(r)["offset"]
|
|
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
offset, err := strconv.Atoi(offsetStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid Offset", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
swapId := id + offset
|
|
|
|
_, err = db.Exec(`
|
|
WITH swap AS (
|
|
SELECT id
|
|
FROM sites
|
|
ORDER BY abs($2 - id)
|
|
LIMIT 1
|
|
)
|
|
UPDATE sites
|
|
SET id = CASE sites.id
|
|
WHEN $1 THEN swap.id
|
|
WHEN swap.id THEN $1
|
|
END
|
|
FROM swap
|
|
WHERE sites.id IN ($1, swap.id);
|
|
`, id, swapId)
|
|
if err != nil {
|
|
println(err.Error())
|
|
http.Error(w, "Error updating site", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func getAllSites(db *sql.DB) ([]models.Site, error) {
|
|
rows, err := db.Query("SELECT id, slug, name, url, is_up, last_check, favicon FROM sites ORDER BY id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func(rows *sql.Rows) {
|
|
err := rows.Close()
|
|
if err != nil {
|
|
log.Printf("Error closing rows: %v", err)
|
|
}
|
|
}(rows)
|
|
|
|
var sites []models.Site
|
|
for rows.Next() {
|
|
var site models.Site
|
|
err := rows.Scan(&site.ID, &site.Slug, &site.Name, &site.URL, &site.IsUp, &site.LastCheck, &site.Favicon)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
site.LastCheck = math.Round(site.LastCheck * 1000)
|
|
sites = append(sites, site)
|
|
}
|
|
return sites, nil
|
|
}
|