added admin routes

This commit is contained in:
2025-02-28 19:08:48 -05:00
parent c5cd1a4399
commit 29ef485e29
3 changed files with 202 additions and 6 deletions
+172
View File
@@ -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)
}
+15
View File
@@ -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
)
+14 -5
View File
@@ -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
return ADMINPORT, PORT, dbdir
}