You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

270 lines
7.9 KiB

package server
import (
"context"
"fmt"
"log"
"net/http"
"strings"
"time"
"gitcitadel-online/internal/cache"
"gitcitadel-online/internal/nostr"
)
// setupRoutes sets up all HTTP routes
func (s *Server) setupRoutes(mux *http.ServeMux) {
// Static files
mux.HandleFunc("/static/", s.handleStatic)
// Main routes
mux.HandleFunc("/", s.handleLanding)
mux.HandleFunc("/wiki/", s.handleWiki)
mux.HandleFunc("/blog", s.handleBlog)
mux.HandleFunc("/contact", s.handleContact)
// Health and metrics
mux.HandleFunc("/health", s.handleHealth)
mux.HandleFunc("/metrics", s.handleMetrics)
// SEO
mux.HandleFunc("/sitemap.xml", s.handleSitemap)
mux.HandleFunc("/robots.txt", s.handleRobots)
}
// handleLanding handles the landing page
func (s *Server) handleLanding(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
s.handle404(w, r)
return
}
page, exists := s.cache.Get("/")
if !exists {
http.Error(w, "Page not ready", http.StatusServiceUnavailable)
return
}
s.servePage(w, r, page)
}
// handleWiki handles wiki article pages
func (s *Server) handleWiki(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
page, exists := s.cache.Get(path)
if !exists {
s.handle404(w, r)
return
}
s.servePage(w, r, page)
}
// handleBlog handles the blog page
func (s *Server) handleBlog(w http.ResponseWriter, r *http.Request) {
page, exists := s.cache.Get("/blog")
if !exists {
http.Error(w, "Page not ready", http.StatusServiceUnavailable)
return
}
s.servePage(w, r, page)
}
// handleContact handles the contact form (GET and POST)
func (s *Server) handleContact(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
// Render the contact form
html, err := s.htmlGenerator.GenerateContactPage(false, "", "", nil)
if err != nil {
http.Error(w, "Failed to generate contact page", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
return
}
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse form data
if err := r.ParseForm(); err != nil {
html, _ := s.htmlGenerator.GenerateContactPage(false, "Failed to parse form data", "", nil)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
return
}
subject := strings.TrimSpace(r.FormValue("subject"))
content := strings.TrimSpace(r.FormValue("content"))
labelsStr := strings.TrimSpace(r.FormValue("labels"))
// Validate required fields
if subject == "" || content == "" {
formData := map[string]string{
"subject": subject,
"content": content,
"labels": labelsStr,
}
html, _ := s.htmlGenerator.GenerateContactPage(false, "Subject and message are required", "", formData)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
return
}
// Parse labels
var labels []string
if labelsStr != "" {
labelParts := strings.Split(labelsStr, ",")
for _, label := range labelParts {
label = strings.TrimSpace(label)
if label != "" {
labels = append(labels, label)
}
}
}
// Fetch repo announcement
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
repoAnnouncement, err := s.issueService.FetchRepoAnnouncement(ctx, s.repoAnnouncement)
if err != nil {
log.Printf("Failed to fetch repo announcement: %v", err)
formData := map[string]string{
"subject": subject,
"content": content,
"labels": labelsStr,
}
html, _ := s.htmlGenerator.GenerateContactPage(false, "Failed to connect to repository. Please try again later.", "", formData)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
return
}
// Create issue request
issueReq := &nostr.IssueRequest{
Subject: subject,
Content: content,
Labels: labels,
}
// Publish issue (using anonymous key - server generates random key)
eventID, err := s.issueService.PublishIssue(ctx, repoAnnouncement, issueReq, "")
if err != nil {
log.Printf("Failed to publish issue: %v", err)
formData := map[string]string{
"subject": subject,
"content": content,
"labels": labelsStr,
}
html, _ := s.htmlGenerator.GenerateContactPage(false, "Failed to submit your message. Please try again later.", "", formData)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
return
}
// Success - render success page
html, err := s.htmlGenerator.GenerateContactPage(true, "", eventID, nil)
if err != nil {
http.Error(w, "Failed to generate success page", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
// handleStatic serves static files
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) {
// Serve static files from the static directory
http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))).ServeHTTP(w, r)
}
// handleHealth handles health check requests
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
if s.cache.Size() == 0 {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("Not ready"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
// handleMetrics handles metrics requests
func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "cache_size %d\n", s.cache.Size())
fmt.Fprintf(w, "feed_items %d\n", len(s.feedCache.Get()))
}
// handleSitemap handles sitemap requests
func (s *Server) handleSitemap(w http.ResponseWriter, r *http.Request) {
// TODO: Generate sitemap from cache
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
</urlset>`))
}
// handleRobots handles robots.txt requests
func (s *Server) handleRobots(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("User-agent: *\nAllow: /\nSitemap: /sitemap.xml\n"))
}
// handle404 handles 404 errors
func (s *Server) handle404(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
// TODO: Serve custom 404 page from cache
w.Write([]byte("404 - Page Not Found"))
}
// servePage serves a cached page with proper headers
func (s *Server) servePage(w http.ResponseWriter, r *http.Request, page *cache.CachedPage) {
// Set headers
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("ETag", page.ETag)
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Header().Set("Last-Modified", page.LastUpdated.Format(http.TimeFormat))
// Check If-None-Match for conditional requests
if match := r.Header.Get("If-None-Match"); match == page.ETag {
w.WriteHeader(http.StatusNotModified)
return
}
// Check Accept-Encoding for compression
acceptEncoding := r.Header.Get("Accept-Encoding")
if strings.Contains(acceptEncoding, "gzip") && len(page.Compressed) > 0 {
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Vary", "Accept-Encoding")
w.Write(page.Compressed)
return
}
// Serve uncompressed
w.Write([]byte(page.Content))
}
// middleware adds security headers and logging
func (s *Server) middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Security headers
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")
// CSP header
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;")
// Log request
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}