inou/web/main.go

135 lines
3.4 KiB
Go
Executable File

package main
import (
"crypto/tls"
"flag"
"html/template"
"log"
"net/http"
"os"
"path/filepath"
"golang.org/x/crypto/acme/autocert"
)
var (
dev = flag.Bool("dev", false, "Development mode (HTTP on port 8080)")
domain = flag.String("domain", "inou.com", "Domain for TLS certificate")
certDir = flag.String("certs", "/tank/inou/certs", "Certificate cache directory")
tmplDir = flag.String("templates", "templates", "Templates directory")
staticDir = flag.String("static", "static", "Static files directory")
)
var templates *template.Template
func main() {
flag.Parse()
loadTemplates()
mux := http.NewServeMux()
mux.HandleFunc("/", handleLanding)
mux.HandleFunc("/login", handleLogin)
mux.HandleFunc("/dashboard", handleDashboard)
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(*staticDir))))
handler := securityHeaders(mux)
if *dev {
log.Println("Development mode: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", handler))
} else {
go redirectHTTP()
serveTLS(handler)
}
}
func loadTemplates() {
pattern := filepath.Join(*tmplDir, "*.tmpl")
var err error
templates, err = template.ParseGlob(pattern)
if err != nil {
log.Fatalf("Failed to load templates: %v", err)
}
log.Printf("Loaded templates: %s", pattern)
}
func render(w http.ResponseWriter, name string, data any) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := templates.ExecuteTemplate(w, name, data)
if err != nil {
log.Printf("Template error: %v", err)
http.Error(w, "Internal error", http.StatusInternalServerError)
}
}
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Del("Server")
next.ServeHTTP(w, r)
})
}
func redirectHTTP() {
srv := &http.Server{
Addr: ":80",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
target := "https://" + r.Host + r.URL.Path
if r.URL.RawQuery != "" {
target += "?" + r.URL.RawQuery
}
http.Redirect(w, r, target, http.StatusMovedPermanently)
}),
}
log.Println("HTTP redirect: :80 → :443")
if err := srv.ListenAndServe(); err != nil {
log.Printf("HTTP redirect server error: %v", err)
}
}
func serveTLS(handler http.Handler) {
if err := os.MkdirAll(*certDir, 0700); err != nil {
log.Fatalf("Failed to create cert directory: %v", err)
}
m := &autocert.Manager{
Cache: autocert.DirCache(*certDir),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(*domain),
}
srv := &http.Server{
Addr: ":443",
Handler: handler,
TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
}
log.Printf("HTTPS server: https://%s", *domain)
log.Fatal(srv.ListenAndServeTLS("", ""))
}
// Handlers
func handleLanding(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
render(w, "landing.tmpl", nil)
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
render(w, "login.tmpl", nil)
}
func handleDashboard(w http.ResponseWriter, r *http.Request) {
data := map[string]any{
"Email": "johan@jongsma.me",
}
render(w, "dashboard.tmpl", data)
}