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>
92 lines
2.2 KiB
Go
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))
|
|
}
|