Skip to content

Feat: Rewrite from Python to Go#1

Merged
blakeridgway merged 17 commits into
mainfrom
feat/go-rewrite
Nov 16, 2025
Merged

Feat: Rewrite from Python to Go#1
blakeridgway merged 17 commits into
mainfrom
feat/go-rewrite

Conversation

@blakeridgway
Copy link
Copy Markdown
Contributor

@blakeridgway blakeridgway commented Nov 13, 2025

Summary by CodeRabbit

  • New Features

    • Go-based admin panel: login, subscriber list, and send-newsletter flows.
    • Session-based admin authentication with bcrypt-secured passwords.
    • Newsletter delivery with per-recipient unsubscribe links and delivery logging.
  • Chores

    • Migrated runtime from Python/Flask to a Go application with multi-stage container builds.
    • Replaced legacy Python templates and removed Python dependencies; added Go templates and modules.
  • Documentation

    • Updated setup, deployment, and environment docs for Go and Podman workflows.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 13, 2025

Walkthrough

Replaces the Python/Flask admin with a Go/Gin implementation: removes the Flask app, Python DB utilities, templates, and Python requirements; adds a Go module, multi-stage Go container build, configuration, PostgreSQL DB layer, email service, middleware, Gin handlers, Go templates, and a Go entrypoint.

Changes

Cohort / File(s) Summary
Build & Container
Dockerfile, Containerfile
Replaces Python Docker build with a multi-stage Go build; builder uses golang:1.23-alpine to compile admin-panel; runtime uses alpine, installs CA certs, copies the binary, .env, and web assets, exposes port 5001, and runs ./admin-panel.
Module & Tooling
go.mod, requirements.txt
Adds go.mod (module path, Go 1.23, toolchain) and Go dependency declarations; removes Python runtime dependencies from requirements.txt (Flask, gunicorn, psycopg2, python-dotenv, Werkzeug).
Removed Python app & DB
Removed files:
app.py, database.py, templates/*.html
Deletes Flask application, routes, session/auth decorator, email broadcast logic, DB utilities and schema creation, default admin provisioning, and original Jinja templates.
Go entrypoint
cmd/admin-panel/main.go
New Gin-based main: loads config, initializes middleware and DB, configures Gin (templates/static), registers login/logout and protected routes, wires handlers, and starts the HTTP server with deferred DB cleanup.
Configuration (Go)
internal/config/config.go
Adds Config struct, Load() to read .env/env vars with defaults, enforces SECRET_KEY, exposes Current, and includes helpers for env parsing.
Database (Go)
internal/database/database.go
New PostgreSQL layer with package-level *sql.DB: Init/Close, connection pooling and ping, DDL creation (subscribers, admin_users, newsletters), GetAllEmails, GetAdmin, LogNewsletter, bcrypt hashing/verification, and Admin model.
Email service (Go)
internal/email/email.go
New SendUpdate(subject, body) that fetches subscribers, builds HTML emails with per-recipient unsubscribe links, sends via SMTP (TLS), logs the newsletter, and returns a status and error.
Middleware (Go)
internal/middleware/auth.go
Adds Gorilla Sessions cookie store init, GetStore(), and Auth() Gin middleware enforcing session-based auth (redirects to /login when unauthorized) and configures cookie attributes.
HTTP handlers (Go)
internal/handlers/*
internal/handlers/auth.go, internal/handlers/newsletter.go, internal/handlers/subscribers.go
Adds login GET/POST and logout handlers, subscribers index, send_update GET/POST handlers; integrate sessions, DB, email service, and render Go templates with validation and error handling.
Templates — removed & added
Removed: templates/*
Added: web/templates/*.html
Removes Flask/Jinja templates and adds Go templates under web/templates for login.html, admin_index.html, and send_update.html.
Docs
README.md
Rewrites README to reflect Go/Gin stack, Podman/Podman Compose deployment, updated .env variables, setup, and Go-specific development and troubleshooting guidance.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant Server as Gin Server
    participant Middleware as Auth Middleware
    participant Handlers as Handlers
    participant DB as Database
    participant Session as Session Store

    Browser->>Server: POST /login (username,password)
    Server->>Handlers: LoginPost
    Handlers->>DB: GetAdmin(username)
    DB-->>Handlers: admin / not found
    Handlers->>Handlers: VerifyPassword(hash,password)
    alt success
        Handlers->>Session: Save session (username)
        Handlers-->>Browser: Redirect /
    else
        Handlers-->>Browser: Render login (401)
    end

    Browser->>Server: GET /
    Server->>Middleware: Auth()
    Middleware->>Session: Retrieve session
    alt session valid
        Middleware->>Handlers: IndexGet
        Handlers->>DB: GetAllEmails()
        DB-->>Handlers: emails[]
        Handlers-->>Browser: Render admin_index.html
    else
        Middleware-->>Browser: Redirect /login
    end
Loading
sequenceDiagram
    participant Admin
    participant Browser
    participant Server as Gin Server
    participant Handler as SendUpdatePost
    participant Email as Email Service
    participant DB as Database
    participant SMTP as SMTP Server

    Admin->>Browser: POST /send_update (subject,body)
    Browser->>Server: POST /send_update
    Server->>Handler: SendUpdatePost
    Handler->>Email: SendUpdate(subject,body)
    Email->>DB: GetAllEmails()
    DB-->>Email: emails[]
    loop per subscriber
        Email->>SMTP: Send email to subscriber (HTML + unsubscribe)
        SMTP-->>Email: OK / Error
    end
    Email->>DB: LogNewsletter(subject,body)
    DB-->>Email: Acknowledged
    Email-->>Handler: result
    Handler-->>Browser: Render send_update.html (success/error)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay attention to SQL DDL, parameterization, and error handling in internal/database/database.go.
  • Verify session secret handling, cookie attributes (Secure, SameSite), and store initialization in internal/middleware/auth.go.
  • Review SMTP TLS usage, HTML construction/escaping, and per-recipient failure handling in internal/email/email.go.
  • Confirm route registration, template data bindings, static asset paths, and graceful DB shutdown in cmd/admin-panel/main.go.
  • Validate config loading, defaulting, and required env validation (e.g., SECRET_KEY, DB/SMTP creds) in internal/config/config.go.

Poem

🐇 I hopped from Python burrows into Gin’s bright sun,
I built my little binary and said “The move is done.”
Cookies snug and templates new, subscribers in a row,
I mail a merry note and watch the inbox grow—
A rabbit guards the admin, nibbling bugs for fun.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: Rewrite from Python to Go' directly and clearly summarizes the main change: a complete rewrite of the codebase from Python to Go, as evidenced by removal of Python files (app.py, database.py, requirements.txt), Flask/Werkzeug dependencies, and replacement with Go modules, Dockerfile, and Go source files.
Docstring Coverage ✅ Passed Docstring coverage is 96.15% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/go-rewrite

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (5)
internal/handlers/subscribers.go (1)

11-19: Consider rendering a user-friendly error page.

Using c.AbortWithError logs the error but displays a generic error page. Consider rendering a custom error template for better UX.

 func IndexGet(c *gin.Context) {
 	emails, err := database.GetAllEmails()
 	if err != nil {
-		c.AbortWithError(http.StatusInternalServerError, err)
+		c.HTML(http.StatusInternalServerError, "admin_index.html",
+			gin.H{"error": "Failed to load subscribers", "emails": []string{}})
 		return
 	}
 	c.HTML(http.StatusOK, "admin_index.html",
 		gin.H{"emails": emails})
 }

Also, for large subscriber lists, consider adding pagination to improve performance and UX.

cmd/admin-panel/main.go (1)

14-23: Consider adding graceful shutdown handling.

The server doesn't handle graceful shutdown, which could lead to abrupt termination of in-flight requests. Consider using context and signal handling to allow graceful shutdown.

 package main
 
 import (
+	"context"
 	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
 
 	"github.com/rideaware/admin-panel/internal/config"
 	"github.com/rideaware/admin-panel/internal/database"
 	"github.com/rideaware/admin-panel/internal/handlers"
 	"github.com/rideaware/admin-panel/internal/middleware"
 
 	"github.com/gin-gonic/gin"
 )
 
 func main() {
 	cfg := config.Load()
 	middleware.Init()
 	database.Init(cfg)
 	defer database.Close()
 
 	router := gin.Default()
 
 	router.LoadHTMLGlob("web/templates/*.html")
 	router.Static("/static", "web/static")
 
 	router.GET("/login", handlers.LoginGet)
 	router.POST("/login", handlers.LoginPost)
 	router.GET("/logout", handlers.Logout)
 
 	protected := router.Group("/")
 	protected.Use(middleware.Auth())
 	protected.GET("/", handlers.IndexGet)
 	protected.GET("/send_update", handlers.SendUpdateGet)
 	protected.POST("/send_update", handlers.SendUpdatePost)
 
+	srv := &http.Server{
+		Addr:    ":" + cfg.Port,
+		Handler: router,
+	}
+
+	go func() {
+		log.Printf("Server running on port %s", cfg.Port)
-		log.Printf("Server running on port %s", cfg.Port)
-		router.Run(":" + cfg.Port)
+		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+			log.Fatalf("Server error: %v", err)
+		}
+	}()
+
+	quit := make(chan os.Signal, 1)
+	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+	<-quit
+	log.Println("Shutting down server...")
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	if err := srv.Shutdown(ctx); err != nil {
+		log.Fatal("Server forced to shutdown:", err)
+	}
+
+	log.Println("Server exited")
 }
README.md (1)

211-221: Use proper headings instead of bold text.
markdownlint (MD036) is flagging these pseudo-headings. Swapping **Title** for an actual ### Title keeps the formatting and satisfies the linter.

Example diff:

-**Database Connection Failed**
+### Database Connection Failed

Apply the same heading conversion to the other emphasized troubleshooting titles below.

internal/database/database.go (2)

38-39: Consider making connection pool settings configurable.

The connection pool settings are hard-coded (25 max open, 5 idle). Consider adding these to the Config struct to allow tuning based on deployment environment and load characteristics.

Example configuration fields to add to internal/config/config.go:

type Config struct {
	// ... existing fields ...
	DBMaxOpenConns int
	DBMaxIdleConns int
}

121-137: Consider failing fast if default admin creation fails.

Similar to the table creation issue, if the default admin creation fails, the application continues but may be unusable for authentication. Consider using log.Fatalf instead of log.Printf on line 133 to ensure the admin user is created before the application starts accepting requests.

Apply this diff:

 	_, err = db.Exec(
 		"INSERT INTO admin_users (username, password) VALUES ($1, $2) "+
 			"ON CONFLICT (username) DO NOTHING",
 		cfg.AdminUsername, hashedPassword,
 	)
 	if err != nil {
-		log.Printf("Error creating default admin: %v", err)
+		log.Fatalf("Error creating default admin: %v", err)
 	} else {
 		log.Println("Default admin user ready.")
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d78f1f and 899313d.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (20)
  • Dockerfile (1 hunks)
  • README.md (2 hunks)
  • app.py (0 hunks)
  • cmd/admin-panel/main.go (1 hunks)
  • database.py (0 hunks)
  • go.mod (1 hunks)
  • internal/config/config.go (1 hunks)
  • internal/database/database.go (1 hunks)
  • internal/email/email.go (1 hunks)
  • internal/handlers/auth.go (1 hunks)
  • internal/handlers/newsletter.go (1 hunks)
  • internal/handlers/subscribers.go (1 hunks)
  • internal/middleware/auth.go (1 hunks)
  • requirements.txt (0 hunks)
  • templates/admin_index.html (0 hunks)
  • templates/login.html (0 hunks)
  • templates/send_update.html (0 hunks)
  • web/templates/admin_index.html (1 hunks)
  • web/templates/login.html (1 hunks)
  • web/templates/send_update.html (1 hunks)
💤 Files with no reviewable changes (6)
  • requirements.txt
  • app.py
  • templates/admin_index.html
  • templates/send_update.html
  • templates/login.html
  • database.py
🧰 Additional context used
🧬 Code graph analysis (7)
internal/handlers/subscribers.go (1)
internal/database/database.go (1)
  • GetAllEmails (84-102)
internal/handlers/auth.go (2)
internal/database/database.go (2)
  • GetAdmin (104-119)
  • VerifyPassword (155-160)
internal/middleware/auth.go (1)
  • GetStore (28-30)
internal/handlers/newsletter.go (1)
internal/email/email.go (1)
  • SendUpdate (14-35)
internal/database/database.go (1)
internal/config/config.go (1)
  • Config (11-27)
internal/middleware/auth.go (1)
internal/config/config.go (1)
  • Current (29-29)
cmd/admin-panel/main.go (6)
internal/config/config.go (1)
  • Load (31-60)
internal/database/database.go (2)
  • Init (22-48)
  • Close (50-54)
internal/middleware/auth.go (2)
  • Init (14-26)
  • Auth (32-42)
internal/handlers/auth.go (3)
  • LoginGet (12-14)
  • LoginPost (16-40)
  • Logout (42-49)
internal/handlers/subscribers.go (1)
  • IndexGet (11-19)
internal/handlers/newsletter.go (2)
  • SendUpdateGet (11-13)
  • SendUpdatePost (15-28)
internal/email/email.go (2)
internal/database/database.go (3)
  • GetAllEmails (84-102)
  • LogNewsletter (139-145)
  • Close (50-54)
internal/config/config.go (1)
  • Current (29-29)
🪛 markdownlint-cli2 (0.18.1)
README.md

148-148: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


211-211: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


216-216: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


222-222: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🪛 OSV Scanner (2.2.4)
go.mod

[HIGH] 12-12: github.com/wneessen/go-mail 0.4.0: Insufficient address encoding when passing mail addresses to the SMTP client in github.com/wneessen/go-mail

(GO-2025-3988)


[HIGH] 12-12: github.com/wneessen/go-mail 0.4.0: go-mail has insufficient address encoding when passing mail addresses to the SMTP client

(GHSA-wpwj-69cm-q9c5)


[CRITICAL] 13-13: golang.org/x/crypto 0.14.0: Man-in-the-middle attacker can compromise integrity of secure channel in golang.org/x/crypto

(GO-2023-2402)


[CRITICAL] 13-13: golang.org/x/crypto 0.14.0: Misuse of connection.serverAuthenticate may cause authorization bypass in golang.org/x/crypto

(GO-2024-3321)


[CRITICAL] 13-13: golang.org/x/crypto 0.14.0: Potential denial of service in golang.org/x/crypto

(GO-2025-3487)


[CRITICAL] 13-13: golang.org/x/crypto 0.14.0: Prefix Truncation Attack against ChaCha20-Poly1305 and Encrypt-then-MAC aka Terrapin

(GHSA-45x7-px36-x8w8)


[CRITICAL] 13-13: golang.org/x/crypto 0.14.0: golang.org/x/crypto Vulnerable to Denial of Service (DoS) via Slow or Incomplete Key Exchange

(GHSA-hcg3-q754-cr77)


[CRITICAL] 13-13: golang.org/x/crypto 0.14.0: Misuse of ServerConfig.PublicKeyCallback may cause authorization bypass in golang.org/x/crypto

(GHSA-v778-237x-gjrc)


[HIGH] 36-36: golang.org/x/net 0.10.0: Improper rendering of text nodes in golang.org/x/net/html

(GO-2023-1988)


[HIGH] 36-36: golang.org/x/net 0.10.0: HTTP/2 rapid reset can cause excessive work in net/http

(GO-2023-2102)


[HIGH] 36-36: golang.org/x/net 0.10.0: HTTP/2 CONTINUATION flood in net/http

(GO-2024-2687)


[HIGH] 36-36: golang.org/x/net 0.10.0: Non-linear parsing of case-insensitive content in golang.org/x/net/html

(GO-2024-3333)


[HIGH] 36-36: golang.org/x/net 0.10.0: HTTP Proxy bypass using IPv6 Zone IDs in golang.org/x/net

(GO-2025-3503)


[HIGH] 36-36: golang.org/x/net 0.10.0: Incorrect Neutralization of Input During Web Page Generation in x/net in golang.org/x/net

(GO-2025-3595)


[HIGH] 36-36: golang.org/x/net 0.10.0: Improper rendering of text nodes in golang.org/x/net/html

(GHSA-2wrh-6pvc-2jm9)


[HIGH] 36-36: golang.org/x/net 0.10.0: HTTP/2 rapid reset can cause excessive work in net/http

(GHSA-4374-p667-p6c8)


[HIGH] 36-36: golang.org/x/net 0.10.0: net/http, x/net/http2: close connections when receiving too many headers

(GHSA-4v7x-pqxf-cx7m)


[HIGH] 36-36: golang.org/x/net 0.10.0: HTTP/2 Stream Cancellation Attack

(GHSA-qppj-fm5r-hxr3)


[HIGH] 36-36: golang.org/x/net 0.10.0: HTTP Proxy bypass using IPv6 Zone IDs in golang.org/x/net

(GHSA-qxp5-gwg8-xv66)


[HIGH] 36-36: golang.org/x/net 0.10.0: golang.org/x/net vulnerable to Cross-site Scripting

(GHSA-vvgc-356p-c3xw)


[HIGH] 39-39: google.golang.org/protobuf 1.30.0: Infinite loop in JSON unmarshaling in google.golang.org/protobuf

(GO-2024-2611)


[HIGH] 39-39: google.golang.org/protobuf 1.30.0: Golang protojson.Unmarshal function infinite loop when unmarshaling certain forms of invalid JSON

(GHSA-8r3f-844c-mc37)

🔇 Additional comments (10)
web/templates/admin_index.html (1)

1-32: LGTM!

The template structure is clean and uses Go template syntax correctly. The dynamic rendering of subscriber count and email list is well-implemented.

cmd/admin-panel/main.go (1)

25-33: LGTM!

The route structure is well-organized with proper separation between public and protected routes. The authentication middleware is correctly applied to admin-only endpoints.

Dockerfile (1)

1-15: Build stage looks good.

The multi-stage build with CGO enabled is appropriate for the PostgreSQL driver (lib/pq). The layer caching optimization (copying go.mod/go.sum first) is well-implemented.

Minor note: The -installsuffix cgo flag is largely unnecessary in modern Go versions but doesn't cause issues.

go.mod (1)

7-14: Dependency upgrades successfully resolved all critical vulnerabilities.

The go.mod file confirms all four flagged dependencies have been upgraded to secure versions:

  • golang.org/x/crypto v0.14.0 → v0.44.0
  • golang.org/x/net v0.10.0 → v0.47.0
  • github.com/wneessen/go-mail v0.4.0 → v0.7.2
  • google.golang.org/protobuf v1.30.0 → v1.36.10

All upgraded versions are free of known CVEs and include fixes for the previously flagged vulnerabilities (Terrapin attack, DoS issues, address encoding flaws, and JSON unmarshaling infinite loops).

internal/database/database.go (6)

1-15: LGTM!

The imports are appropriate and the global db variable is an acceptable pattern since sql.DB is designed to be thread-safe and used as a long-lived connection pool.


17-20: LGTM!

The Admin struct is appropriately simple for its purpose. The Password field will hold the bcrypt hash retrieved from the database.


84-102: LGTM!

The function properly handles database queries with correct resource cleanup using defer rows.Close(), error handling during iteration, and checking rows.Err() at the end.


104-119: LGTM!

The function correctly distinguishes between "not found" and other database errors, and uses parameterized queries to prevent SQL injection.


139-145: LGTM!

The function is straightforward with proper error propagation and SQL injection protection via parameterized queries.


147-160: LGTM!

Both password functions correctly use bcrypt with appropriate settings. The DefaultCost (10) provides a good balance between security and performance, and error handling is appropriate.

Comment thread Containerfile
Comment thread internal/config/config.go
Comment thread internal/database/database.go Outdated
Comment thread internal/database/database.go
Comment thread internal/database/database.go
Comment on lines +56 to +82
func createTables() {
queries := []string{
`CREATE TABLE IF NOT EXISTS subscribers (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS admin_users (
id SERIAL PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS newsletters (
id SERIAL PRIMARY KEY,
subject TEXT NOT NULL,
body TEXT NOT NULL,
sent_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)`,
}

for _, query := range queries {
if _, err := db.Exec(query); err != nil {
log.Printf("Error creating table: %v", err)
}
}

log.Println("Database tables ready.")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Improve error handling for table creation failures.

Table creation errors are only logged, which could allow the application to start with missing tables and fail later during runtime. Consider either:

  1. Failing fast with log.Fatal if any table creation fails, or
  2. Returning an error from createTables() and handling it in Init()

Apply this diff to fail fast on table creation errors:

 func createTables() {
 	queries := []string{
 		`CREATE TABLE IF NOT EXISTS subscribers (
 			id SERIAL PRIMARY KEY,
 			email TEXT UNIQUE NOT NULL
 		)`,
 		`CREATE TABLE IF NOT EXISTS admin_users (
 			id SERIAL PRIMARY KEY,
 			username TEXT UNIQUE NOT NULL,
 			password TEXT NOT NULL
 		)`,
 		`CREATE TABLE IF NOT EXISTS newsletters (
 			id SERIAL PRIMARY KEY,
 			subject TEXT NOT NULL,
 			body TEXT NOT NULL,
 			sent_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
 		)`,
 	}
 
 	for _, query := range queries {
 		if _, err := db.Exec(query); err != nil {
-			log.Printf("Error creating table: %v", err)
+			log.Fatalf("Error creating table: %v", err)
 		}
 	}
 
 	log.Println("Database tables ready.")
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func createTables() {
queries := []string{
`CREATE TABLE IF NOT EXISTS subscribers (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS admin_users (
id SERIAL PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS newsletters (
id SERIAL PRIMARY KEY,
subject TEXT NOT NULL,
body TEXT NOT NULL,
sent_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)`,
}
for _, query := range queries {
if _, err := db.Exec(query); err != nil {
log.Printf("Error creating table: %v", err)
}
}
log.Println("Database tables ready.")
}
func createTables() {
queries := []string{
`CREATE TABLE IF NOT EXISTS subscribers (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS admin_users (
id SERIAL PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS newsletters (
id SERIAL PRIMARY KEY,
subject TEXT NOT NULL,
body TEXT NOT NULL,
sent_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)`,
}
for _, query := range queries {
if _, err := db.Exec(query); err != nil {
log.Fatalf("Error creating table: %v", err)
}
}
log.Println("Database tables ready.")
}
🤖 Prompt for AI Agents
internal/database/database.go around lines 56 to 82: the createTables function
currently only logs table creation errors which can let the app start with
missing tables; change it to fail fast by replacing the non-fatal log.Printf
call with log.Fatalf (or otherwise call log.Fatal) so the process exits
immediately on any table creation error, ensuring the application does not
continue running with incomplete schema.

Comment thread internal/email/email.go Outdated
Comment thread internal/middleware/auth.go
Comment thread web/templates/login.html
Comment on lines +21 to +25
<form method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add CSRF protection and verify rate limiting.

The login form lacks CSRF protection, which could allow attackers to perform login CSRF attacks. Additionally, ensure that rate limiting is implemented to prevent brute-force attacks on admin credentials.

Add CSRF token to the form and verify that the LoginPost handler in internal/handlers/auth.go implements rate limiting or account lockout mechanisms.


🏁 Script executed:

#!/bin/bash
# Check for rate limiting implementation in auth handlers and middleware
rg -n -C3 'rate.?limit|throttle|attempts|lockout' internal/handlers/auth.go internal/middleware/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# 1. Check the actual login.html template for CSRF token
echo "=== Checking login.html for CSRF token ==="
cat web/templates/login.html 2>/dev/null || echo "File not found"

echo ""
echo "=== Checking LoginPost handler implementation ==="
# 2. Search for LoginPost handler
rg -n "func.*LoginPost|func.*login.*post" --type=go -i

echo ""
echo "=== Check middleware and main.go for rate limiting setup ==="
# 3. Broader search in middleware and main config files
rg -n "middleware|rate|limit|throttle" internal/middleware/ cmd/ 2>/dev/null || echo "Checking main files..."

echo ""
echo "=== Check for external libraries/imports related to rate limiting ==="
# 4. Search for any imports/usage of rate limiting packages
rg -n "github.com.*rate|rate.*limit.*package" --type=go

Length of output: 1721


🏁 Script executed:

#!/bin/bash
# Check the LoginPost handler implementation
echo "=== LoginPost handler implementation ==="
rg -n -A 30 "func LoginPost" internal/handlers/auth.go

echo ""
echo "=== Check if any rate limiting middleware or logic exists in the codebase ==="
rg -n "attempt|failed.*login|brute|lock.*account" --type=go -i internal/

Length of output: 1078


Add CSRF protection and implement rate limiting for the login endpoint.

The login form lacks CSRF protection and the LoginPost handler has no rate limiting, leaving the admin credentials vulnerable to both CSRF and brute-force attacks.

  1. CSRF token missing: The form (line 21-25 in web/templates/login.html) contains no CSRF token field, and the handler (internal/handlers/auth.go:16-40) performs no CSRF validation.

  2. No rate limiting: The LoginPost handler accepts unlimited login attempts with no failed attempt tracking, throttling, or account lockout mechanism.

Add a CSRF token to the template, validate it in the handler, and implement rate limiting or account lockout to prevent brute-force attacks on admin credentials.

🤖 Prompt for AI Agents
In web/templates/login.html around lines 21-25 and internal/handlers/auth.go
around lines 16-40, the login form lacks a CSRF token and the LoginPost handler
has no CSRF validation or rate limiting; add a hidden CSRF input to the form
(render token from server/session) and update LoginPost to validate that token
before authenticating, and implement rate limiting by tracking failed attempts
per IP or username (in-memory store, cache, or DB) with exponential backoff,
temporary lockout after N failures, and reset on successful login; ensure
rate-limit responses are generic (no user existence info) and that CSRF token is
rotated/renewed per session and validated server-side before processing
credentials.

Comment on lines +31 to +39
<form method="POST">
<label for="subject">Subject:</label>
<input type="text" id="subject" name="subject" required>

<label for="body">Message:</label>
<textarea id="body" name="body" rows="10" required></textarea>

<button type="submit">Send to All Subscribers</button>
</form>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add CSRF protection to prevent cross-site request forgery.

The form lacks CSRF token protection, making it vulnerable to CSRF attacks where an attacker could trick an authenticated admin into sending malicious newsletters.

Consider using Gin's CSRF middleware or adding CSRF tokens manually to all state-changing forms.

Additionally, consider adding maxlength attributes to prevent excessively large inputs:

 <label for="subject">Subject:</label>
-<input type="text" id="subject" name="subject" required>
+<input type="text" id="subject" name="subject" maxlength="200" required>
 
 <label for="body">Message:</label>
-<textarea id="body" name="body" rows="10" required></textarea>
+<textarea id="body" name="body" rows="10" maxlength="10000" required></textarea>

🏁 Script executed:

#!/bin/bash
# Find the send_update.html file and examine it
fd -t f "send_update.html" -x cat {}

Length of output: 1521


🏁 Script executed:

#!/bin/bash
# Search for CSRF-related code in the entire codebase
rg -i "csrf" -C 3 --type=go --type=html

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find all HTML form elements to check for CSRF token patterns
rg -n '<form' -A 5 --type=html

Length of output: 926


🏁 Script executed:

#!/bin/bash
# Search for middleware setup and Gin configuration
rg -n "middleware|csrf|token" -C 2 --type=go | head -100

Length of output: 1846


🏁 Script executed:

#!/bin/bash
# Check for any template functions or helpers related to CSRF
rg -n "CSRF|csrf" --type=go -C 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find the POST handler for send_update to check if CSRF validation exists there
rg -n "SendUpdatePost|send_update.*POST" -A 15 --type=go

Length of output: 1092


🏁 Script executed:

#!/bin/bash
# Search for any token validation logic in handlers
rg -n "token|Token|validate" --type=go -C 2 internal/handlers/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check all POST route definitions to see if middleware is applied
rg -n "\.POST" -C 1 --type=go

Length of output: 401


🏁 Script executed:

#!/bin/bash
# Look for any CSRF-like patterns or security headers
rg -n "X-CSRF|SameSite|csrf" -i --type=go

Length of output: 45


Add CSRF protection to prevent cross-site request forgery.

The form lacks CSRF token protection, making it vulnerable to CSRF attacks where an attacker could trick an authenticated admin into sending malicious newsletters. Verification confirms:

  • No CSRF token field in the template (lines 31-39 of web/templates/send_update.html)
  • No CSRF validation in SendUpdatePost handler (internal/handlers/newsletter.go:15-28)
  • No CSRF middleware applied to the route (cmd/admin-panel/main.go:33)

Implement CSRF protection by adding a CSRF middleware to Gin (e.g., using gin-contrib/csrf) and including the token in the form:

 <form method="POST">
+    {{ .csrf_token }}
     <label for="subject">Subject:</label>

Additionally, consider adding maxlength attributes to constrain input sizes:

 <label for="subject">Subject:</label>
-<input type="text" id="subject" name="subject" required>
+<input type="text" id="subject" name="subject" maxlength="200" required>
 
 <label for="body">Message:</label>
-<textarea id="body" name="body" rows="10" required></textarea>
+<textarea id="body" name="body" rows="10" maxlength="10000" required></textarea>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<form method="POST">
<label for="subject">Subject:</label>
<input type="text" id="subject" name="subject" required>
<label for="body">Message:</label>
<textarea id="body" name="body" rows="10" required></textarea>
<button type="submit">Send to All Subscribers</button>
</form>
<form method="POST">
<label for="subject">Subject:</label>
<input type="text" id="subject" name="subject" maxlength="200" required>
<label for="body">Message:</label>
<textarea id="body" name="body" rows="10" maxlength="10000" required></textarea>
<button type="submit">Send to All Subscribers</button>
</form>

@blakeridgway
Copy link
Copy Markdown
Contributor Author

@coderabbitai generate docstrings

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 13, 2025

✅ Actions performed

Initiated docstring generation; will generate only if new commits exist.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 13, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #2

Docstrings generation was requested by @blakeridgway.

* #1 (comment)

The following files were modified:

* `cmd/admin-panel/main.go`
* `internal/config/config.go`
* `internal/database/database.go`
* `internal/email/email.go`
* `internal/handlers/auth.go`
* `internal/handlers/newsletter.go`
* `internal/handlers/subscribers.go`
* `internal/middleware/auth.go`
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (7)
internal/handlers/newsletter.go (1)

20-33: Add input validation and use appropriate HTTP status codes.

The handler still lacks server-side validation and returns incorrect HTTP status codes, as previously noted:

  1. Missing validation: Subject and body should be validated (non-empty, length limits)
  2. Wrong status code: Line 26 returns HTTP 200 on error; should return HTTP 500

Apply this diff to add validation and fix status codes:

+import (
+	"strings"
+	// ... existing imports
+)
+
 func SendUpdatePost(c *gin.Context) {
 	subject := c.PostForm("subject")
 	body := c.PostForm("body")
+
+	// Validate inputs
+	if strings.TrimSpace(subject) == "" || strings.TrimSpace(body) == "" {
+		c.HTML(http.StatusBadRequest, "send_update.html",
+			gin.H{"error": "Subject and message cannot be empty"})
+		return
+	}
+
+	if len(subject) > 200 || len(body) > 10000 {
+		c.HTML(http.StatusBadRequest, "send_update.html",
+			gin.H{"error": "Subject or message too long"})
+		return
+	}
 
 	message, err := email.SendUpdate(subject, body)
 	if err != nil {
-		c.HTML(http.StatusOK, "send_update.html",
+		c.HTML(http.StatusInternalServerError, "send_update.html",
 			gin.H{"error": message})
 		return
 	}
 
 	c.HTML(http.StatusOK, "send_update.html",
 		gin.H{"success": message})
 }
internal/middleware/auth.go (1)

17-21: Guard against nil config before dereferencing.

If Init() runs before config.Load(), the code panics with a nil-pointer dereference instead of the intended SECRET_KEY validation. This issue was previously flagged.

Apply this diff:

 func Init() {
+	if config.Current == nil {
+		panic("config not loaded; call config.Load() before middleware.Init()")
+	}
+
 	if config.Current.SecretKey == "" {
 		panic("SECRET_KEY not set")
 	}
internal/email/email.go (1)

74-75: Encode email addresses in unsubscribe links.

The unsubscribe link embeds the raw recipient email address, which breaks for addresses containing +, space, or other reserved characters. This was previously flagged.

Apply this diff:

+import (
+	"fmt"
+	"log"
+	"net/url"
+	"time"
+	// ... existing imports
+)
+
 	unsubLink := fmt.Sprintf("https://%s/unsubscribe?email=%s",
-		cfg.BaseURL, recipient)
+		cfg.BaseURL, url.QueryEscape(recipient))
internal/database/database.go (4)

29-34: Fix PostgreSQL DSN password escaping.

Using url.QueryEscape for the password (line 30) can cause authentication failures for passwords with special characters like spaces (encoded as + instead of %20). This was previously flagged.

Apply this diff:

-	password := url.QueryEscape(cfg.PGPassword)
+	password := url.PathEscape(cfg.PGPassword)
 
 	psqlInfo := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=require",
 		cfg.PGUser, password, cfg.PGHost, cfg.PGPort, cfg.PGDatabase,
 	)

Alternatively, use url.UserPassword() for more idiomatic URL construction.


40-50: Add connection timeout to prevent indefinite hangs.

The database connection and ping operations lack timeouts, which could cause the application to hang indefinitely during startup if the database is unreachable. This was previously flagged.

Apply this diff to add a timeout:

+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"log"
+	"net/url"
+	"time"
+	// ... existing imports
+)
+
 func Init(cfg *config.Config) {
 	// ... existing code ...
 
 	var err error
 	db, err = sql.Open("postgres", psqlInfo)
 	if err != nil {
 		log.Fatalf("Database connection error: %v", err)
 	}
 
 	db.SetMaxOpenConns(25)
 	db.SetMaxIdleConns(5)
 
-	if err = db.Ping(); err != nil {
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	if err = db.PingContext(ctx); err != nil {
 		log.Fatalf("Failed to ping database: %v", err)
 	}

59-63: Log error from database close operation.

The db.Close() operation can return an error that should be logged for debugging. This was previously flagged.

Apply this diff:

 func Close() {
 	if db != nil {
-		db.Close()
+		if err := db.Close(); err != nil {
+			log.Printf("Error closing database connection: %v", err)
+		}
 	}
 }

68-94: Fail fast on table creation errors.

Table creation errors are only logged (line 89), allowing the application to start with missing tables and fail later during runtime. This was previously flagged.

Apply this diff to fail fast:

 	for _, query := range queries {
 		if _, err := db.Exec(query); err != nil {
-			log.Printf("Error creating table: %v", err)
+			log.Fatalf("Error creating table: %v", err)
 		}
 	}
🧹 Nitpick comments (1)
internal/email/email.go (1)

49-60: Consider making SMTP timeout configurable.

The 10-second timeout (line 55) may be insufficient for slow SMTP servers or when sending to large batches, potentially causing false negatives. Consider making this configurable via the config struct.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 899313d and fbb871f.

📒 Files selected for processing (8)
  • cmd/admin-panel/main.go (1 hunks)
  • internal/config/config.go (1 hunks)
  • internal/database/database.go (1 hunks)
  • internal/email/email.go (1 hunks)
  • internal/handlers/auth.go (1 hunks)
  • internal/handlers/newsletter.go (1 hunks)
  • internal/handlers/subscribers.go (1 hunks)
  • internal/middleware/auth.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/config/config.go
  • internal/handlers/auth.go
  • cmd/admin-panel/main.go
🧰 Additional context used
🧬 Code graph analysis (5)
internal/handlers/newsletter.go (1)
internal/email/email.go (1)
  • SendUpdate (21-42)
internal/database/database.go (1)
internal/config/config.go (1)
  • Config (11-27)
internal/middleware/auth.go (1)
internal/config/config.go (1)
  • Current (29-29)
internal/handlers/subscribers.go (1)
internal/database/database.go (1)
  • GetAllEmails (98-116)
internal/email/email.go (2)
internal/database/database.go (3)
  • GetAllEmails (98-116)
  • LogNewsletter (161-167)
  • Close (59-63)
internal/config/config.go (1)
  • Current (29-29)
🔇 Additional comments (8)
internal/handlers/newsletter.go (1)

12-14: LGTM!

The handler correctly renders the update form template.

internal/middleware/auth.go (1)

33-35: LGTM!

The accessor is straightforward and the nil possibility is documented.

internal/database/database.go (6)

98-116: LGTM!

The function correctly handles database queries with proper error checking and resource cleanup.


120-135: LGTM!

The function uses parameterized queries to prevent SQL injection and properly distinguishes between "not found" and other database errors.


141-157: LGTM!

The function uses idempotent insert with ON CONFLICT DO NOTHING and appropriately fails fast if password hashing fails during startup.


161-167: LGTM!

The function correctly uses parameterized queries to safely insert newsletter records.


171-177: LGTM!

The function uses bcrypt with the default cost factor, which is appropriate for password hashing.


181-186: LGTM!

The function correctly uses bcrypt to verify password hashes, returning a boolean for easy validation.

Comment thread internal/email/email.go
Comment thread internal/handlers/subscribers.go
Comment on lines +22 to +28
store.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 7,
HttpOnly: true,
Secure: false,
SameSite: 0,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Set Secure: true and explicit SameSite for production security.

The cookie configuration has security concerns:

  • Line 26: Secure: false allows cookies over HTTP, exposing session tokens to interception
  • Line 27: SameSite: 0 relies on default behavior; should be explicit (e.g., http.SameSiteStrictMode)

Apply this diff to improve security:

 	store.Options = &sessions.Options{
 		Path:     "/",
 		MaxAge:   86400 * 7,
 		HttpOnly: true,
-		Secure:   false,
-		SameSite: 0,
+		Secure:   true,
+		SameSite: http.SameSiteStrictMode,
 	}

Consider making Secure configurable via environment variable for local development.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
store.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 7,
HttpOnly: true,
Secure: false,
SameSite: 0,
}
store.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 7,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
}
🤖 Prompt for AI Agents
In internal/middleware/auth.go around lines 22 to 28, the session cookie options
are insecure: change Secure to true for production but make it configurable via
an environment variable (e.g., READ from ENV like COOKIE_SECURE or use a dev
flag) so local HTTP development can still set false; replace the ambiguous
SameSite: 0 with an explicit value (e.g., http.SameSiteStrictMode or
http.SameSiteLaxMode) and ensure the code selects the appropriate mode for your
cross-site requirements; keep the other options but gate Secure by env and
document the default behavior.

Comment thread internal/middleware/auth.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (12)
internal/config/config.go (1)

53-59: SecretKey enforcement looks good.

The insecure default has been removed and the code now enforces that SECRET_KEY must be set via environment variable. This addresses the security concern from the previous review.

internal/middleware/auth.go (2)

17-33: Security improvements successfully implemented.

All previous security concerns have been addressed:

  • Config nil check prevents panic if Load() wasn't called (lines 18-20)
  • Secure: true ensures cookies only transmit over HTTPS (line 30)
  • SameSite: http.SameSiteStrictMode provides CSRF protection (line 31)

45-60: Store nil guard properly implemented.

The middleware correctly checks for nil store and returns a 500 error, preventing panics if Init() failed or wasn't called.

internal/database/database.go (5)

32-36: Password escaping correctly uses PathEscape.

The DSN now uses url.PathEscape() which properly handles special characters in PostgreSQL connection strings. This addresses the previous concern about QueryEscape encoding spaces as +.


50-54: Connection timeout properly implemented.

The code now uses context.WithTimeout and PingContext to prevent indefinite hangs during database initialization.


63-69: Database close error handling implemented.

The Close() function now logs any error returned from db.Close(), which aids debugging.


74-100: Table creation approach is acceptable for idempotent operations.

The previous review suggested failing fast on table creation errors. However, the current approach of logging errors (line 95) is reasonable for CREATE TABLE IF NOT EXISTS statements, which are idempotent and may legitimately fail in concurrent initialization scenarios without causing operational issues.

If strict schema validation is needed, consider querying for table existence after createTables() completes instead.


50-54: Fix missing parentheses on deferred cancel call.

Line 51 calls defer cancel without parentheses, deferring the function value rather than invoking it. The context will never be canceled, causing a resource leak.

Apply this diff:

 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-	defer cancel
+	defer cancel()
 	if err = db.PingContext(ctx); err != nil {
internal/handlers/subscribers.go (1)

14-23: Error handling improved with template rendering.

The handler now renders errors within the admin_index.html template instead of aborting, providing better UX consistency with the rest of the admin interface.

internal/handlers/newsletter.go (1)

31-36: HTTP status code properly uses 500 for server errors.

The error response now correctly uses http.StatusInternalServerError instead of 200, improving API semantics.

internal/email/email.go (2)

77-78: Unsubscribe link properly URL-encodes email addresses.

The code now uses url.QueryEscape(recipient) to safely encode email addresses in the query string, handling special characters like + and spaces correctly.


22-45: Partial failure handling implemented correctly.

The function now:

  • Continues sending to all subscribers even if some fail (lines 30-36)
  • Tracks success and failure counts
  • Returns a detailed message indicating how many sends succeeded vs. failed (lines 41-44)

This is much better for newsletter operations where partial delivery is acceptable and operators need visibility into results.

🧹 Nitpick comments (1)
internal/config/config.go (1)

52-52: Consider requiring AdminPassword via environment variable.

The default "changeme" is a well-known weak password. While it's configurable, having this default could lead to deployments with insecure credentials if the operator forgets to set ADMIN_PASSWORD.

Consider either:

  1. Requiring ADMIN_PASSWORD to be set (similar to SECRET_KEY), or
  2. Generating a random password on first startup and logging it for the operator to change
-		AdminPassword: getEnv("ADMIN_PASSWORD", "changeme"),
+		AdminPassword: getEnv("ADMIN_PASSWORD", ""),

Then add validation after the SecretKey check:

	if cfg.AdminPassword == "" {
		log.Fatal("ADMIN_PASSWORD environment variable must be set!")
	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fbb871f and 268df98.

📒 Files selected for processing (6)
  • internal/config/config.go (1 hunks)
  • internal/database/database.go (1 hunks)
  • internal/email/email.go (1 hunks)
  • internal/handlers/newsletter.go (1 hunks)
  • internal/handlers/subscribers.go (1 hunks)
  • internal/middleware/auth.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
internal/handlers/subscribers.go (1)
internal/database/database.go (1)
  • GetAllEmails (104-122)
internal/handlers/newsletter.go (1)
internal/email/email.go (1)
  • SendUpdate (22-45)
internal/database/database.go (1)
internal/config/config.go (1)
  • Config (11-27)
internal/email/email.go (2)
internal/database/database.go (3)
  • GetAllEmails (104-122)
  • LogNewsletter (167-173)
  • Close (63-69)
internal/config/config.go (1)
  • Current (29-29)
internal/middleware/auth.go (1)
internal/config/config.go (1)
  • Current (29-29)
🔇 Additional comments (1)
internal/config/config.go (1)

50-59: Fix missing parentheses on deferred cancel call.

Line 51 calls defer cancel without parentheses, which defers the function value instead of invoking it. This means the context will never be canceled, leading to a resource leak.

Apply this diff:

 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
-	defer cancel
+	defer cancel()
 	if err = db.PingContext(ctx); err != nil {

Likely an incorrect or invalid review comment.

Comment on lines +20 to +40
func SendUpdatePost(c *gin.Context) {
subject := c.PostForm("subject")
body := c.PostForm("body")

// validate inputs
if strings.TrimSpace(subject) == "" || strings.TrimSpace(body) == {
c.HTML(http,StatusBadRequest, "send_update.html",
gin.H{"error": "Subject and message cannot be empty"})
return
}

message, err := email.SendUpdate(subject, body)
if err != nil {
c.HTML(http.StatusInternalServerError, "send_update.html",
gin.H{"error": message})
return
}

c.HTML(http.StatusOK, "send_update.html",
gin.H{"success": message})
} No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix syntax errors in validation code.

The validation code has multiple syntax errors that will prevent compilation:

  1. Line 25: Incomplete comparison strings.TrimSpace(body) == { should be strings.TrimSpace(body) == ""
  2. Line 26: Wrong separator http,StatusBadRequest should be http.StatusBadRequest (use dot, not comma)
  3. Missing "strings" import at the top of the file

Apply this diff:

+import (
+	"net/http"
+	"strings"
+
+	"github.com/rideaware/admin-panel/internal/email"
+
+	"github.com/gin-gonic/gin"
+)
+
-	// validate inputs
-	if strings.TrimSpace(subject) == "" || strings.TrimSpace(body) == {
-		c.HTML(http,StatusBadRequest, "send_update.html",
+	// Validate inputs
+	if strings.TrimSpace(subject) == "" || strings.TrimSpace(body) == "" {
+		c.HTML(http.StatusBadRequest, "send_update.html",
 			gin.H{"error": "Subject and message cannot be empty"})
 		return
 	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In internal/handlers/newsletter.go around lines 20 to 40, the request validation
and response code contains syntax errors: fix the empty-body comparison to use
strings.TrimSpace(body) == "" (not == {), correct the StatusBadRequest call to
http.StatusBadRequest (use dot not comma), and add the missing import "strings"
at the top of the file; ensure the imports block includes "strings" and update
the two lines in the function so the validation compiles and the error response
uses http.StatusBadRequest.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
internal/handlers/newsletter.go (1)

27-27: CRITICAL: Fix syntax error preventing compilation.

Line 27 contains a comma instead of a dot separator: http,StatusBadRequest should be http.StatusBadRequest. This syntax error will prevent the code from compiling.

Apply this diff:

-	c.HTML(http,StatusBadRequest, "send_update.html",
+	c.HTML(http.StatusBadRequest, "send_update.html",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 268df98 and 09a01c5.

📒 Files selected for processing (1)
  • internal/handlers/newsletter.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/handlers/newsletter.go (1)
internal/email/email.go (1)
  • SendUpdate (22-45)

Comment thread internal/handlers/newsletter.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
internal/handlers/newsletter.go (1)

23-43: Critical syntax error still present - compilation will fail.

Line 29 contains a syntax error that was previously flagged but remains unfixed: http,StatusBadRequest uses a comma instead of a dot separator and will prevent the code from compiling.

Apply this diff:

 	// validate inputs
 	if strings.TrimSpace(subject) == "" || strings.TrimSpace(body) == "" {
-		c.HTML(http,StatusBadRequest, "send_update.html",
+		c.HTML(http.StatusBadRequest, "send_update.html",
 			gin.H{"error": "Subject and message cannot be empty"})
 		return
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 09a01c5 and 84e4919.

📒 Files selected for processing (1)
  • internal/handlers/newsletter.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/handlers/newsletter.go (1)
internal/email/email.go (1)
  • SendUpdate (22-45)
🔇 Additional comments (1)
internal/handlers/newsletter.go (1)

12-15: LGTM!

The GET handler correctly renders the newsletter form template with HTTP 200.

Comment on lines +17 to +22
// SendUpdatePost handles POST requests to submit a newsletter update.
// It reads "subject" and "body" from the form, calls email.SendUpdate(subject, body),
//and renders the "send_update.html" template with gin.H{"error": message} on validation
// or email failures (returning HTTP 400 or 500 respectively), or gin.H{"success": message}
// on success (returning HTTP 200)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix comment formatting.

Line 19 is missing a space after the comment marker.

Apply this diff:

 // SendUpdatePost handles POST requests to submit a newsletter update.
 // It reads "subject" and "body" from the form, calls email.SendUpdate(subject, body),
-//and renders the "send_update.html" template with gin.H{"error": message} on validation
+// and renders the "send_update.html" template with gin.H{"error": message} on validation
 // or email failures (returning HTTP 400 or 500 respectively), or gin.H{"success": message}
 // on success (returning HTTP 200)
🤖 Prompt for AI Agents
In internal/handlers/newsletter.go around lines 17 to 22 the inline comment on
line 19 lacks a space after the '//' marker; update that comment to include a
single space after '//' (and ensure consistent spacing for the surrounding
comment lines) so all comment lines begin with "// " for proper formatting.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
internal/handlers/newsletter.go (1)

17-21: Fix comment formatting on line 19.

Line 19 is missing a space after the // comment marker, which was previously flagged but remains unfixed.

Apply this diff:

 // SendUpdatePost handles POST requests to submit a newsletter update.
 // It reads "subject" and "body" from the form, calls email.SendUpdate(subject, body),
-//and renders the "send_update.html" template with gin.H{"error": message} on validation
+// and renders the "send_update.html" template with gin.H{"error": message} on validation
 // or email failures (returning HTTP 400 or 500 respectively), or gin.H{"success": message}
 // on success (returning HTTP 200)
internal/database/database.go (1)

94-98: Table creation errors should fail fast.

The function still logs table creation errors with log.Printf instead of failing fast. This allows the application to start with potentially missing tables, which will cause runtime failures later when handlers attempt to query those tables.

Apply this diff to fail fast on table creation errors:

 	for _, query := range queries {
 		if _, err := db.Exec(query); err != nil {
-			log.Printf("Error creating table: %v", err)
+			log.Fatalf("Error creating table: %v", err)
 		}
 	}
Containerfile (1)

24-24: Critical security issue: .env file still copied into Docker image.

Despite being marked as addressed in previous reviews, the .env file is still being copied into the Docker image at line 24. This embeds secrets directly into image layers where they can be extracted by anyone with access to the image.

Remove this line and pass configuration via runtime environment variables instead. The application's internal/config/config.go already supports environment variables, so no code changes are needed.

Apply this diff:

 COPY --from=builder /build/admin-panel .
-COPY .env .env
 COPY web ./web

Ensure .env is added to .dockerignore to prevent accidental inclusion:

#!/bin/bash
# Verify .env is in .dockerignore
if ! grep -q "^\.env$" .dockerignore 2>/dev/null; then
  echo ".env" >> .dockerignore
  echo "Added .env to .dockerignore"
fi
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 84e4919 and 0918aff.

📒 Files selected for processing (4)
  • Containerfile (1 hunks)
  • internal/database/database.go (1 hunks)
  • internal/email/email.go (1 hunks)
  • internal/handlers/newsletter.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
internal/email/email.go (2)
internal/database/database.go (3)
  • GetAllEmails (105-123)
  • LogNewsletter (168-174)
  • Close (64-70)
internal/config/config.go (1)
  • Current (29-29)
internal/handlers/newsletter.go (1)
internal/email/email.go (1)
  • SendUpdate (23-46)
internal/database/database.go (1)
internal/config/config.go (1)
  • Config (11-27)

Comment thread Containerfile
Comment thread Containerfile
COPY go.mod go.sum ./
RUN go mod download

COPY . .
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Ensure .dockerignore is comprehensive.

The COPY . . command copies the entire build context. Verify that a .dockerignore file exists and excludes unnecessary files to optimize build performance and prevent accidental inclusion of sensitive data.

Run this script to verify .dockerignore exists and contains essential exclusions:


🏁 Script executed:

#!/bin/bash
# Check if .dockerignore exists and contains essential patterns

if [ ! -f .dockerignore ]; then
  echo "❌ .dockerignore not found"
  exit 1
fi

echo "✓ .dockerignore exists"
echo "Checking for essential patterns..."

patterns=(".env" ".git" "*.md" "*.pyc" "__pycache__" "*.log")
missing=()

for pattern in "${patterns[@]}"; do
  if grep -qE "^${pattern}$|^${pattern/\*/.\*}$" .dockerignore; then
    echo "$pattern"
  else
    echo "$pattern (missing)"
    missing+=("$pattern")
  fi
done

if [ ${#missing[@]} -gt 0 ]; then
  echo ""
  echo "Consider adding these patterns to .dockerignore:"
  printf '%s\n' "${missing[@]}"
fi

echo ""
echo "Current .dockerignore contents:"
cat .dockerignore

Length of output: 25


🏁 Script executed:

#!/bin/bash
# Verify Containerfile exists and check for sensitive files in build context

echo "=== Checking Containerfile ==="
if [ -f "Containerfile" ]; then
  echo "✓ Containerfile exists"
  echo ""
  echo "First 20 lines of Containerfile:"
  head -20 Containerfile
else
  echo "❌ Containerfile not found in root"
fi

echo ""
echo "=== Checking for sensitive files in build context ==="
sensitive_files=(".env" ".git" ".ssh" ".aws" ".env.local" ".env.production" "*.pem" "*.key")

for pattern in "${sensitive_files[@]}"; do
  if [ -e "$pattern" ] 2>/dev/null; then
    echo "⚠️  Found: $pattern"
  fi
done | head -20

echo ""
echo "=== Repository root structure ==="
ls -la | head -20

Length of output: 1248


Add .dockerignore file to exclude unnecessary build context.

Verification confirms .dockerignore is missing and the .git directory will be copied into the Docker build context by COPY . . (line 11). This unnecessarily increases build context size, bloats the image, and introduces a security concern as git history may contain sensitive information. Create .dockerignore at the repository root with patterns to exclude:

  • .git
  • .gitignore
  • .env
  • *.md
  • *.pyc
  • __pycache__
  • *.log
  • Any other non-essential files
🤖 Prompt for AI Agents
In Containerfile around line 11, the Dockerfile uses COPY . . which will include
the entire repository (including .git) in the build context; create a
.dockerignore at the repo root listing patterns to exclude non-essential files
such as .git, .gitignore, .env, *.md, *.pyc, __pycache__, *.log and any other
build artifacts or local config; ensure the .dockerignore is committed so Docker
excludes these paths from the build context and re-run the build to verify the
context size is reduced.

Comment thread Containerfile
-o admin-panel ./cmd/admin-panel

# Stage 2: Runtime
FROM docker.io/library/alpine:latest
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pin the Alpine base image version.

Using alpine:latest can lead to non-reproducible builds and unexpected breaking changes when the :latest tag is updated. This is inconsistent with the pinned golang version in the builder stage.

Apply this diff to pin to a specific Alpine version:

-FROM docker.io/library/alpine:latest
+FROM docker.io/library/alpine:3.20

Choose the version that matches your golang builder's Alpine version (1.23-alpine uses Alpine 3.20).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FROM docker.io/library/alpine:latest
FROM docker.io/library/alpine:3.20
🤖 Prompt for AI Agents
In Containerfile around line 17, the base image uses the floating tag
"alpine:latest" which makes builds non-reproducible; change the FROM to a
specific Alpine release that matches the builder stage (for Go 1.23-alpine use
alpine:3.20) so both stages are pinned and reproducible—update the base image
tag accordingly and ensure any related dependency expectations or package names
still match that Alpine version.

Comment thread internal/email/email.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
internal/email/email.go (1)

16-22: Docstring still describes outdated behavior.

Lines 20-21 still reference the old early-return behavior ("If sending to a specific subscriber fails: returns 'Failed to send to ' and nil"), but the current implementation (lines 42-45) returns aggregate statistics like "Sent to X/Y subscribers; Z failed." This was previously flagged but remains unaddressed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae3a484 and 9444bab.

📒 Files selected for processing (1)
  • internal/email/email.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/email/email.go (2)
internal/database/database.go (3)
  • GetAllEmails (105-123)
  • LogNewsletter (168-174)
  • Close (64-70)
internal/config/config.go (1)
  • Current (29-29)
🔇 Additional comments (2)
internal/email/email.go (2)

23-46: LGTM!

The implementation correctly tracks all send attempts and provides aggregate statistics. The non-fatal newsletter logging and comprehensive error handling are appropriate.


48-101: LGTM!

The send function is well-structured with proper error handling, resource cleanup via defer, and correct URL encoding of the recipient in the unsubscribe link. The SSL/TLS configuration based on port is appropriate.

Comment thread internal/email/email.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
internal/email/email.go (2)

15-16: Expand docstring to document all return scenarios.

The docstring doesn't describe the different success/failure messages returned by the function. Based on the implementation (lines 39-42), document that the function returns:

  • "Email sent to all N subscribers." when all succeed
  • "Sent to X/Y subscribers; Z failed." when some fail
  • "No subscribers found." when the subscriber list is empty
  • "Failed to retrieve subscribers" with an error when retrieval fails

Also note that newsletter logging failures (line 35-37) are non-fatal and logged but don't affect the return value.

Apply this diff to expand the docstring:

 // SendUpdate sends a newsletter with the given subject and body to all subscriber emails stored in the database.
-// It returns a human-readable status message and, when subscriber retrieval fails, the underlying error.
+// It returns a human-readable status message and, when subscriber retrieval fails, the underlying error.
+// - If retrieving subscribers fails: returns "Failed to retrieve subscribers" and the error.
+// - If no subscribers are found: returns "No subscribers found." and nil.
+// - If all emails are sent successfully: returns "Email sent to all N subscribers." and nil.
+// - If some emails fail: returns "Sent to X/Y subscribers; Z failed." and nil.
+// Note: logging the newsletter entry in the database is attempted after sending and any logging failure is non-fatal.

139-145: Fix case-sensitivity mismatch in tag detection and replacement.

Lines 139 and 143 use strings.ToLower(body) for detection (case-insensitive) but then perform strings.Replace on the original body (case-sensitive). If users provide uppercase tags like </HTML> or </BODY>, the detection succeeds but the replacement fails, causing the footer to be appended at the end instead of inserted before the closing tag.

Apply this diff to use case-insensitive replacement:

 func buildHTMLBody(body, unsubLink string) string {
 	footer := fmt.Sprintf(
 		"<br><br><hr><p style='font-size: 12px; color: #666;'>If you ever wish to unsubscribe, "+
 			"please click <a href='%s'>here</a>.</p>",
 		unsubLink)
 
+	lowerBody := strings.ToLower(body)
+	
 	// If body contains closing html tag, insert before it
-	if strings.Contains(strings.ToLower(body), "</html>") {
-		return strings.Replace(body, "</html>", footer+"</html>", 1)
+	if idx := strings.Index(lowerBody, "</html>"); idx != -1 {
+		return body[:idx] + footer + body[idx:]
 	}
 
 	// If body contains closing body tag, insert before it
-	if strings.Contains(strings.ToLower(body), "</body>") {
-		return strings.Replace(body, "</body>", footer+"</body>", 1)
+	if idx := strings.Index(lowerBody, "</body>"); idx != -1 {
+		return body[:idx] + footer + body[idx:]
 	}
 
 	// Otherwise just append
 	return body + footer
 }
🧹 Nitpick comments (1)
internal/email/email.go (1)

59-62: Set explicit TLS minimum version.

The TLS configuration omits MinVersion, which means it defaults to TLS 1.2 for client connections. While this may be acceptable for many SMTP servers, explicitly setting the minimum version improves security posture and makes the intended security level clear.

Consider setting MinVersion: tls.VersionTLS12 as a baseline (many SMTP servers don't yet support TLS 1.3), or tls.VersionTLS13 if your SMTP provider supports it.

Based on learnings from static analysis.

Apply this diff:

 	tlsconfig := &tls.Config{
-		ServerName: cfg.SMTPServer,
+		ServerName: cfg.SMTPServer,
+		MinVersion: tls.VersionTLS12,
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9444bab and 85e49c9.

📒 Files selected for processing (1)
  • internal/email/email.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/email/email.go (2)
internal/database/database.go (3)
  • GetAllEmails (105-123)
  • LogNewsletter (168-174)
  • Close (64-70)
internal/config/config.go (2)
  • Current (29-29)
  • Config (11-27)
🪛 ast-grep (0.39.9)
internal/email/email.go

[warning] 59-61: MinVersionis missing from this TLS configuration. By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. General purpose web applications should default to TLS 1.3 with all other protocols disabled. Only where it is known that a web server must support legacy clients with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. AddMinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3.
Context: tls.Config{
ServerName: cfg.SMTPServer,
}
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(missing-ssl-minversion-go)

🔇 Additional comments (1)
internal/email/email.go (1)

121-130: LGTM!

The email message construction properly follows SMTP standards with correct headers, MIME version, content type, and CRLF line endings.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
internal/email/email.go (2)

15-16: Expand docstring to document all return scenarios.

The docstring lacks details about the various return cases. It should specify what messages are returned for each scenario (no subscribers, all succeeded, partial failures) and note that newsletter logging failures are non-fatal.

Consider this format:

 // SendUpdate sends a newsletter with the given subject and body to all subscriber emails stored in the database.
-// It returns a human-readable status message and, when subscriber retrieval fails, the underlying error.
+// It returns a human-readable status message and, when subscriber retrieval fails, the underlying error.
+// - If retrieving subscribers fails: returns "Failed to retrieve subscribers" and the error.
+// - If no subscribers are found: returns "No subscribers found." and nil.
+// - If all emails are sent successfully: returns "Email sent to all N subscribers." and nil.
+// - If some emails fail: returns "Sent to X/Y subscribers; Z failed." and nil.
+// Note: Logging the newsletter to the database is attempted after sending; logging failures are non-fatal.

140-146: Fix case-sensitivity mismatch in tag detection and replacement.

The detection uses strings.ToLower(body) (case-insensitive) but replacement operates on the original body (case-sensitive). If the body contains uppercase tags like </HTML> or </BODY>, detection succeeds but replacement fails, causing the footer to append at the end instead of before the closing tag.

Apply this diff to use case-insensitive replacement:

 func buildHTMLBody(body, unsubLink string) string {
 	footer := fmt.Sprintf(
 		"<br><br><hr><p style='font-size: 12px; color: #666;'>If you ever wish to unsubscribe, "+
 			"please click <a href='%s'>here</a>.</p>",
 		unsubLink)
 
+	lowerBody := strings.ToLower(body)
+	
 	// If body contains closing html tag, insert before it
-	if strings.Contains(strings.ToLower(body), "</html>") {
-		return strings.Replace(body, "</html>", footer+"</html>", 1)
+	if idx := strings.Index(lowerBody, "</html>"); idx != -1 {
+		return body[:idx] + footer + body[idx:]
 	}
 
 	// If body contains closing body tag, insert before it
-	if strings.Contains(strings.ToLower(body), "</body>") {
-		return strings.Replace(body, "</body>", footer+"</body>", 1)
+	if idx := strings.Index(lowerBody, "</body>"); idx != -1 {
+		return body[:idx] + footer + body[idx:]
 	}
 
 	// Otherwise just append
 	return body + footer
 }
🧹 Nitpick comments (1)
internal/email/email.go (1)

59-70: Consider adding timeout handling for TLS connection.

The TLS dial operation has no timeout, which could cause the function to hang indefinitely if the SMTP server is unresponsive. Consider using net.DialTimeout with a TLS wrapper or setting a deadline on the connection.

Example approach:

dialer := &net.Dialer{Timeout: 30 * time.Second}
conn, err := tls.DialWithDialer(dialer, "tcp", addr, tlsconfig)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85e49c9 and 1b91b72.

📒 Files selected for processing (1)
  • internal/email/email.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/email/email.go (2)
internal/database/database.go (3)
  • GetAllEmails (105-123)
  • LogNewsletter (168-174)
  • Close (64-70)
internal/config/config.go (2)
  • Current (29-29)
  • Config (11-27)
🔇 Additional comments (1)
internal/email/email.go (1)

122-131: LGTM!

The message construction properly follows RFC 822 format with correct CRLF line endings and appropriate MIME headers for HTML content.

@blakeridgway blakeridgway merged commit 97f2581 into main Nov 16, 2025
1 check passed
@blakeridgway blakeridgway deleted the feat/go-rewrite branch November 16, 2025 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant