From da40f652a7722883b897dfac69df19c5b042f8cd Mon Sep 17 00:00:00 2001 From: ION606 Date: Fri, 28 Feb 2025 19:11:16 -0500 Subject: [PATCH] added admin routes --- Dockerfile.batched | 2 +- Dockerfile.sqldb | 2 +- Makefile | 5 ++-- README.md | 3 +++ batched-server/go.mod | 14 ++++++++++ batched-server/main.go | 51 ++++++++++++++++++++++++------------ sqlite-server/go.mod | 9 ++++--- sqlite-server/main.go | 52 +++++++++++++++++++++++++++++-------- test/batched.sh | 37 ++++++++++++++------------ test/sqlite.sh | 59 +++++++++++++++++++++--------------------- 10 files changed, 152 insertions(+), 82 deletions(-) diff --git a/Dockerfile.batched b/Dockerfile.batched index 797c0d9..dae07c1 100644 --- a/Dockerfile.batched +++ b/Dockerfile.batched @@ -15,5 +15,5 @@ RUN go mod download RUN go build -o batched-server . -EXPOSE 15521 +EXPOSE 15521 15522 CMD ["./batched-server", "15521"] \ No newline at end of file diff --git a/Dockerfile.sqldb b/Dockerfile.sqldb index 50ef695..ef9b0a4 100644 --- a/Dockerfile.sqldb +++ b/Dockerfile.sqldb @@ -15,5 +15,5 @@ RUN go mod download RUN go build -o sqlite-server . -EXPOSE 15521 +EXPOSE 15521 15522 CMD ["./sqlite-server", "15521"] \ No newline at end of file diff --git a/Makefile b/Makefile index 9b92051..500fddb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ PORT=15521 +ADMINPORT=15522 VOLUME_NAME=mailpocket-data .PHONY: run-batched run-sqlite stop reset @@ -11,11 +12,11 @@ test-vol: run-batched: test-vol stop docker build -t batched-server -f Dockerfile.batched . - docker run -p $(PORT):$(PORT) --name batched-server -v $(VOLUME_NAME):/app/data batched-server + docker run -d -p $(PORT):$(PORT) -p $(ADMINPORT):$(ADMINPORT) --name batched-server -v $(VOLUME_NAME):/app/data --env-file=.env batched-server run-sqlite: test-vol stop docker build -t sqlite-server -f Dockerfile.sqldb . - docker run -d -p $(PORT):$(PORT) --name sqlite-server -v $(VOLUME_NAME):/app/data sqlite-server + docker run -d -p $(PORT):$(PORT) -p $(ADMINPORT):$(ADMINPORT) --name sqlite-server -v $(VOLUME_NAME):/app/data --env-file=.env sqlite-server stop: docker stop batched-server || true diff --git a/README.md b/README.md index 2207ecd..1906aa6 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ Both servers are designed to be simple, efficient, and easy to deploy. git clone https://github.com/your-username/forms-server.git cd forms-server ``` +1.1 ENV + - create an env file with `ADMIN_PASSWORD=your_password_here` in it + - put this in the base directory (i.e. on the same level as the Dockerfiles/Makefile) 2.0 Using the Makefile: - ```bash diff --git a/batched-server/go.mod b/batched-server/go.mod index 644babe..55785cc 100644 --- a/batched-server/go.mod +++ b/batched-server/go.mod @@ -5,3 +5,17 @@ go 1.23.6 replace shared => ../shared require shared v0.0.0-00010101000000-000000000000 + +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 + modernc.org/sqlite v1.36.0 // indirect +) diff --git a/batched-server/main.go b/batched-server/main.go index 462d2b9..3df95d5 100644 --- a/batched-server/main.go +++ b/batched-server/main.go @@ -13,8 +13,13 @@ import ( "time" ) +type EmailEntry struct { + Email string + FormName string +} + var ( - emailQueue []string + emailQueue []EmailEntry // Changed to struct slice queueLock sync.Mutex dbdir string PORT string @@ -46,26 +51,24 @@ func saveEmails() { // Write header if file is new if !fileExists { - if err := writer.Write([]string{"email", "timestamp"}); err != nil { + if err := writer.Write([]string{"email", "formname", "timestamp"}); err != nil { log.Println("Failed to write header:", err) return } } - // Write each email with timestamp - for _, email := range emailQueue { + // Write each entry + for _, entry := range emailQueue { timestamp := time.Now().Format(time.RFC3339) - if err := writer.Write([]string{email, timestamp}); err != nil { + if err := writer.Write([]string{entry.Email, entry.FormName, timestamp}); err != nil { log.Println("Failed to write email:", err) return } } - // Clear queue - emailQueue = emailQueue[:0] + emailQueue = emailQueue[:0] // Clear queue } -// background goroutine to batch writes func init() { go func() { for { @@ -81,12 +84,28 @@ func init() { }() } -func submitHandler(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { +func handler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte("Batched Write Server is running")) + return + } + + if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } + formName := strings.Trim(r.URL.Path, "/") + if formName == "" { + http.Error(w, "Form name required", http.StatusBadRequest) + return + } + email := strings.TrimSpace(r.FormValue("email")) if email == "" { http.Error(w, "Email required", http.StatusBadRequest) @@ -94,7 +113,7 @@ func submitHandler(w http.ResponseWriter, r *http.Request) { } queueLock.Lock() - emailQueue = append(emailQueue, email) + emailQueue = append(emailQueue, EmailEntry{Email: email, FormName: formName}) shouldFlush := len(emailQueue) >= 100 queueLock.Unlock() @@ -107,14 +126,12 @@ func submitHandler(w http.ResponseWriter, r *http.Request) { } func main() { - PORT, dbdir = shared.GetArgs() + _, PORT, dbdir = shared.GetArgs() fpath = filepath.Join(dbdir, "emails.csv") - http.HandleFunc("/submit", submitHandler) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("Batched Write Server is running")) - }) + shared.StartAdminServer(dbdir) + + http.HandleFunc("/", handler) // Single handler for all routes log.Println("Starting server on port", PORT) log.Fatal(http.ListenAndServe(":"+PORT, nil)) diff --git a/sqlite-server/go.mod b/sqlite-server/go.mod index cfc9113..45dd0af 100644 --- a/sqlite-server/go.mod +++ b/sqlite-server/go.mod @@ -2,6 +2,11 @@ module sqlite-server go 1.23.6 +require ( + modernc.org/sqlite v1.36.0 + shared v0.0.0-00010101000000-000000000000 +) + require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect @@ -9,12 +14,10 @@ require ( 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.28.0 // 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 - modernc.org/sqlite v1.35.0 // indirect - shared v0.0.0-00010101000000-000000000000 // indirect ) replace shared => ../shared diff --git a/sqlite-server/main.go b/sqlite-server/main.go index 40a518c..2a2bd52 100644 --- a/sqlite-server/main.go +++ b/sqlite-server/main.go @@ -5,28 +5,63 @@ import ( "encoding/json" "log" "net/http" + "os" "path/filepath" - "strings" "shared" + "strings" + _ "modernc.org/sqlite" ) func main() { - PORT, dbdir := shared.GetArgs() + _, PORT, dbdir := shared.GetArgs() db, _ := sql.Open("sqlite", filepath.Join(dbdir, "emails.db")) db.Exec(`CREATE TABLE IF NOT EXISTS emails ( - email TEXT PRIMARY KEY, - created TIMESTAMP DEFAULT CURRENT_TIMESTAMP + email TEXT, + formname TEXT, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (email, formname) )`) - http.HandleFunc("/submit", func(w http.ResponseWriter, r *http.Request) { + adminPassword := os.Getenv("ADMIN_PASSWORD") + + if adminPassword == "" { + log.Fatal("ADMIN_PASSWORD environment variable required") + } + + shared.StartAdminServer(dbdir) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + // Handle root GET request + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte("SQLite Write Server is running")) + return + } + + // Handle form submissions for other paths + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + formname := strings.TrimPrefix(r.URL.Path, "/") + if formname == "" { + http.Error(w, "Form name required", http.StatusBadRequest) + return + } + email := strings.TrimSpace(r.FormValue("email")) if email == "" { http.Error(w, "Email required", http.StatusBadRequest) return } - _, err := db.Exec(`INSERT OR IGNORE INTO emails(email) VALUES (?)`, email) + _, err := db.Exec(`INSERT OR IGNORE INTO emails(email, formname) VALUES (?, ?)`, email, formname) w.Header().Set("Content-Type", "application/json") if err != nil { @@ -36,11 +71,6 @@ func main() { json.NewEncoder(w).Encode(map[string]string{"message": "data received"}) }) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("SQLite Write Server is running")) - }) - log.Println("Starting server on port", PORT) log.Fatal(http.ListenAndServe(":"+PORT, nil)) } diff --git a/test/batched.sh b/test/batched.sh index 060d23b..4db2372 100644 --- a/test/batched.sh +++ b/test/batched.sh @@ -1,6 +1,8 @@ #!/bin/bash +CONTAINER_NAME="batched-server" SERVER_URL="http://localhost:15521" +VOLUME_PATH="/app/data" # Path inside container test_server_running() { echo "Testing if the server is running..." @@ -14,40 +16,41 @@ test_server_running() { fi } - -test_submit_email() { - echo "Testing email submission..." - RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "email=test@example.com" "$SERVER_URL/submit") +test_form_submission() { + echo "Testing form submission to dynamic endpoint..." + FORM_NAME="testform" + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "email=test@example.com" "$SERVER_URL/$FORM_NAME") if [ "$RESPONSE" -eq 200 ]; then - echo "✅ Email submission successful (Status: 200 OK)" + echo "✅ Form submission to /$FORM_NAME successful (Status: 200 OK)" else - echo "❌ Email submission failed (Received status: $RESPONSE)" + echo "❌ Form submission failed (Received status: $RESPONSE)" exit 1 fi } - test_csv_written() { - CSV_FILE="batched-server/emails.csv" + CSV_FILE="$VOLUME_PATH/emails.csv" + FORM_NAME="testform" - if [ -f "$CSV_FILE" ]; then - if grep -q "test@example.com" "$CSV_FILE"; then - echo "✅ Email found in $CSV_FILE" + # Use docker exec to check CSV inside container + if docker exec $CONTAINER_NAME /bin/sh -c "[ -f $CSV_FILE ]"; then + if docker exec $CONTAINER_NAME grep -q "test@example.com,$FORM_NAME" "$CSV_FILE"; then + echo "✅ Entry found in CSV (form: $FORM_NAME)" else - echo "❌ Email NOT found in $CSV_FILE" + echo "❌ Entry NOT found in CSV" exit 1 fi else - echo "❌ CSV file not found!" + echo "❌ CSV file not found in container!" exit 1 fi } -# run the tests +# Run tests test_server_running -test_submit_email -sleep 2 # wait for the batched write to complete +test_form_submission +sleep 2 # Allow time for batched write test_csv_written -echo "✅ All tests passed successfully!" +echo "✅ All batched CSV tests passed!" \ No newline at end of file diff --git a/test/sqlite.sh b/test/sqlite.sh index 7f1bf4a..b6a011a 100644 --- a/test/sqlite.sh +++ b/test/sqlite.sh @@ -1,58 +1,57 @@ #!/bin/bash +CONTAINER_NAME="sqlite-server" SERVER_URL="http://localhost:15521" +VOLUME_PATH="/app/data" test_server_running() { - # testing if the server is running... RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$SERVER_URL/") if [ "$RESPONSE" -eq 200 ]; then - echo "✅ server is running (status: 200 ok)"; + echo "✅ Server is running (status: 200 OK)" else - echo "❌ server is not running (received status: $RESPONSE)"; - exit 1; + echo "❌ Server is not running (received status: $RESPONSE)" + exit 1 fi } -test_submit_email() { - # testing email submission... - RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "email=test@example.com" "$SERVER_URL/submit") +test_form_submission() { + FORM_NAME="testform" + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -d "email=test@example.com" "$SERVER_URL/$FORM_NAME") if [ "$RESPONSE" -eq 200 ]; then - echo "✅ email submission successful (status: 200 ok)"; + echo "✅ Form submission to /$FORM_NAME successful (status: 200 OK)" else - echo "❌ email submission failed (received status: $RESPONSE)"; - exit 1; + echo "❌ Form submission failed (received status: $RESPONSE)" + exit 1 fi } test_db_entry() { - # testing if the email was written to the sqlite database... - DB_FILE="sqlite-server/emails.db" + DB_FILE="$VOLUME_PATH/emails.db" + FORM_NAME="testform" - if [ -f "$DB_FILE" ]; then - if ! command -v sqlite3 >/dev/null 2>&1; then - echo "❌ sqlite3 is not installed. please install sqlite3 to run this test"; - exit 1; - fi; + # Use docker exec to check database inside container + if docker exec $CONTAINER_NAME /bin/sh -c "[ -f $DB_FILE ]"; then + result=$(docker exec $CONTAINER_NAME sqlite3 "$DB_FILE" \ + "SELECT email FROM emails WHERE email='test@example.com' AND formname='$FORM_NAME';") - result=$(sqlite3 "$DB_FILE" "select email from emails where email='test@example.com';") if [ "$result" == "test@example.com" ]; then - echo "✅ email found in $DB_FILE"; + echo "✅ Entry found in database (form: $FORM_NAME)" else - echo "❌ email not found in $DB_FILE"; - exit 1; - fi; + echo "❌ Entry not found in database" + exit 1 + fi else - echo "❌ database file $DB_FILE not found!"; - exit 1; + echo "❌ Database file not found in container!" + exit 1 fi } -# run the tests -test_server_running; -test_submit_email; -sleep 2; # wait for the write to complete -test_db_entry; +# Run tests +test_server_running +test_form_submission +sleep 2 # Allow time for async write +test_db_entry -echo "✅ all tests passed successfully!"; +echo "✅ All SQLite tests passed!" \ No newline at end of file