Files
kls/main.go
Kyle Isom 9db3883706 Switch from HTTP basic auth to MCIAS
Replace HTTP basic auth with MCIAS session cookies. Adds mcdsl auth,
csrf, and web packages. Chi router for route-level auth middleware.
Short code redirects (GET /{short}) remain public; all management
pages require MCIAS login. Nord dark theme, layout+page template
pattern matching other platform services.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:13:44 -07:00

92 lines
2.2 KiB
Go

package main
import (
"context"
"crypto/rand"
"flag"
"fmt"
"log"
"log/slog"
"net"
"net/http"
"os"
"github.com/go-chi/chi/v5"
"git.wntrmute.dev/mc/mcdsl/auth"
"git.wntrmute.dev/mc/mcdsl/csrf"
"git.wntrmute.dev/mc/mcdsl/web"
"git.wntrmute.dev/kyle/goutils/config"
"git.wntrmute.dev/kyle/goutils/die"
"git.wntrmute.dev/kyle/kls/links"
)
const cookieName = "kls_session"
var defaultConfigFile = config.DefaultConfigPath("kls", "kls.conf")
func main() {
cfgFile := flag.String("f", defaultConfigFile, "`path` to config file")
flag.Parse()
err := config.LoadFile(*cfgFile)
die.If(err)
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
ctx := context.Background()
db, err := links.Connect(ctx)
die.If(err)
// MCIAS authenticator.
authenticator, err := auth.New(auth.Config{
ServerURL: config.Get("MCIAS_SERVER_URL"),
CACert: config.Get("MCIAS_CA_CERT"),
ServiceName: config.GetDefault("MCIAS_SERVICE_NAME", "kls"),
}, logger)
die.If(err)
// CSRF protection.
csrfSecret := make([]byte, 32)
if _, err := rand.Read(csrfSecret); err != nil {
die.If(fmt.Errorf("generate CSRF secret: %w", err))
}
csrfProtect := csrf.New(csrfSecret, "_csrf", "csrf_token")
srv := &server{db: db, auth: authenticator, csrf: csrfProtect}
r := chi.NewRouter()
// Public: short code redirects (no auth).
r.Get("/{short:[a-zA-Z0-9]+}", srv.redirect)
// Public: login page.
r.Get("/login", srv.handleLoginPage)
r.Post("/login", csrfProtect.Middleware(http.HandlerFunc(srv.handleLogin)).ServeHTTP)
// Static files.
r.Get("/static/*", http.FileServer(http.FS(staticFiles)).ServeHTTP)
// Authenticated routes.
r.Group(func(r chi.Router) {
r.Use(web.RequireAuth(authenticator, cookieName, "/login"))
r.Use(csrfProtect.Middleware)
r.Get("/", srv.handleIndex)
r.Post("/", srv.postURL)
r.Get("/list", srv.listAll)
r.Post("/logout", srv.handleLogout)
})
// $PORT is set by the MCP agent for containers with route declarations.
// It takes precedence over config, matching the mcdsl convention.
port := os.Getenv("PORT")
if port == "" {
port = config.GetDefault("HTTP_PORT", "8000")
}
addr := net.JoinHostPort(config.Get("HTTP_ADDR"), port)
log.Print("listening on ", addr)
log.Fatal(http.ListenAndServe(addr, r))
}