From 29ef485e29c8394208413e9be2450e0b8a2debdb Mon Sep 17 00:00:00 2001 From: ION606 Date: Fri, 28 Feb 2025 19:08:48 -0500 Subject: [PATCH] added admin routes --- shared/admin.go | 172 ++++++++++++++++++++++++++++++++++++++++++++++++ shared/go.mod | 15 +++++ shared/utils.go | 21 ++++-- 3 files changed, 202 insertions(+), 6 deletions(-) create mode 100644 shared/admin.go diff --git a/shared/admin.go b/shared/admin.go new file mode 100644 index 0000000..741a449 --- /dev/null +++ b/shared/admin.go @@ -0,0 +1,172 @@ +package shared + +import ( + "database/sql" + "encoding/json" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + + _ "modernc.org/sqlite" +) + +var ( + adminOnce sync.Once + adminServer *http.Server +) + +type AdminServer struct { + DB *sql.DB + auth *AdminAuth +} + +type AdminAuth struct { + Password string +} + +func (a *AdminAuth) Middleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + provided := r.Header.Get("X-Admin-Password") + if provided != a.Password { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + next(w, r) + } +} + +func StartAdminServer(dbdir string) { + var port, _, _ = GetArgs() + + adminOnce.Do(func() { + adminPassword := os.Getenv("ADMIN_PASSWORD") + if adminPassword == "" { + log.Fatal("ADMIN_PASSWORD environment variable required") + } + + db, err := sql.Open("sqlite", filepath.Join(dbdir, "admin.db")) + if err != nil { + log.Fatal("Failed to open admin database:", err) + } + + createTables(db) + + as := &AdminServer{ + DB: db, + auth: &AdminAuth{Password: adminPassword}, + } + + mux := http.NewServeMux() + mux.HandleFunc("/forms/", as.handleForms) + mux.HandleFunc("/submit/", as.handleSubmit) + + adminServer = &http.Server{ + Addr: ":" + port, + Handler: mux, + } + + log.Println("Starting admin server on port", port) + go func() { + if err := adminServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal("Admin server failed:", err) + } + }() + }) +} + +func createTables(db *sql.DB) { + db.Exec(`CREATE TABLE IF NOT EXISTS forms ( + name TEXT PRIMARY KEY, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )`) + + db.Exec(`CREATE TABLE IF NOT EXISTS submissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + form_name TEXT, + data TEXT, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(form_name) REFERENCES forms(name) ON DELETE CASCADE + )`) +} + +func (as *AdminServer) handleForms(w http.ResponseWriter, r *http.Request) { + formName := strings.TrimPrefix(r.URL.Path, "/forms/") + + switch r.Method { + case http.MethodPost: + as.auth.Middleware(func(w http.ResponseWriter, r *http.Request) { + _, err := as.DB.Exec("INSERT INTO forms(name) VALUES (?)", formName) + if err != nil { + http.Error(w, "Form creation failed", http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusCreated) + })(w, r) + + case http.MethodDelete: + as.auth.Middleware(func(w http.ResponseWriter, r *http.Request) { + _, err := as.DB.Exec("DELETE FROM forms WHERE name = ?", formName) + if err != nil { + http.Error(w, "Form deletion failed", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + })(w, r) + + case http.MethodGet: + var exists bool + err := as.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM forms WHERE name = ?)", formName).Scan(&exists) + if err != nil || !exists { + http.Error(w, "Form not found", http.StatusNotFound) + return + } + + rows, err := as.DB.Query("SELECT data FROM submissions WHERE form_name = ?", formName) + if err != nil { + http.Error(w, "Failed to retrieve submissions", http.StatusInternalServerError) + return + } + defer rows.Close() + + var submissions []string + for rows.Next() { + var data string + if err := rows.Scan(&data); err != nil { + continue + } + submissions = append(submissions, data) + } + json.NewEncoder(w).Encode(submissions) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func (as *AdminServer) handleSubmit(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + formName := strings.TrimPrefix(r.URL.Path, "/submit/") + data := r.FormValue("data") + + var exists bool + err := as.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM forms WHERE name = ?)", formName).Scan(&exists) + if err != nil || !exists { + http.Error(w, "Form does not exist", http.StatusNotFound) + return + } + + _, err = as.DB.Exec(`INSERT INTO submissions(form_name, data) VALUES (?, ?)`, formName, data) + if err != nil { + http.Error(w, "Submission failed", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) +} diff --git a/shared/go.mod b/shared/go.mod index 2131a02..bc7babd 100644 --- a/shared/go.mod +++ b/shared/go.mod @@ -1,3 +1,18 @@ module shared go 1.23.6 + +require modernc.org/sqlite v1.36.0 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect + golang.org/x/sys v0.30.0 // indirect + modernc.org/libc v1.61.13 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect +) diff --git a/shared/utils.go b/shared/utils.go index 4a272c1..c22e4fc 100644 --- a/shared/utils.go +++ b/shared/utils.go @@ -5,15 +5,24 @@ import ( "os" ) - -func GetArgs() (string, string) { +func GetArgs() (string, string, string) { var PORT string - if len(os.Args) > 1 { + var ADMINPORT string + if len(os.Args) > 2 { + ADMINPORT = os.Args[2] PORT = os.Args[1] - } else { + } else if len(os.Args) > 1 { + PORT = os.Args[1] + } + + if PORT == "" { PORT = "15521" } + if ADMINPORT == "" { + ADMINPORT = "15522" + } + dbdir := "data" isDocker := os.Getenv("container") == "docker" || os.Getenv("DOCKER") == "true" || func() bool { _, err := os.Stat("/.dockerenv"); return err == nil }() @@ -27,5 +36,5 @@ func GetArgs() (string, string) { } } - return PORT, dbdir -} \ No newline at end of file + return ADMINPORT, PORT, dbdir +}