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>
This commit is contained in:
117
handler.go
117
handler.go
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
@@ -11,26 +10,44 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/config"
|
||||
"git.wntrmute.dev/mc/mcdsl/auth"
|
||||
"git.wntrmute.dev/mc/mcdsl/csrf"
|
||||
"git.wntrmute.dev/mc/mcdsl/web"
|
||||
"git.wntrmute.dev/kyle/kls/links"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
db *sql.DB
|
||||
db *sql.DB
|
||||
auth *auth.Authenticator
|
||||
csrf *csrf.Protect
|
||||
}
|
||||
|
||||
//go:embed templates/*.tpl
|
||||
//go:embed templates/*.html
|
||||
var templateFiles embed.FS
|
||||
|
||||
var templates = template.Must(template.ParseFS(templateFiles, "templates/*.tpl"))
|
||||
//go:embed static
|
||||
var staticFiles embed.FS
|
||||
|
||||
var templates = template.Must(template.ParseFS(templateFiles, "templates/*.html"))
|
||||
|
||||
type page struct {
|
||||
Short string
|
||||
URLs []*links.URL
|
||||
Username string
|
||||
Short string
|
||||
URLs []*links.URL
|
||||
}
|
||||
|
||||
func (srv *server) servePage(w http.ResponseWriter, p page, tpl string) {
|
||||
err := templates.ExecuteTemplate(w, tpl, p)
|
||||
func (srv *server) servePage(w http.ResponseWriter, p page, tpl string, funcs ...template.FuncMap) {
|
||||
t := templates
|
||||
if len(funcs) > 0 {
|
||||
t = template.Must(t.Clone())
|
||||
for _, fm := range funcs {
|
||||
t.Funcs(fm)
|
||||
}
|
||||
// Re-parse to pick up the funcs.
|
||||
t = template.Must(template.New("").Funcs(funcs[0]).ParseFS(templateFiles, "templates/*.html"))
|
||||
}
|
||||
err := t.ExecuteTemplate(w, tpl, p)
|
||||
if err != nil {
|
||||
log.Printf("error executing template: %s", err)
|
||||
http.Error(w, fmt.Sprintf("template execution failed: %s", err.Error()),
|
||||
@@ -39,7 +56,42 @@ func (srv *server) servePage(w http.ResponseWriter, p page, tpl string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *server) handleLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
web.RenderTemplate(w, templateFiles, "login.html", page{}, srv.csrf.TemplateFunc(w))
|
||||
}
|
||||
|
||||
func (srv *server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
totpCode := r.FormValue("totp_code")
|
||||
|
||||
token, _, err := srv.auth.Login(username, password, totpCode)
|
||||
if err != nil {
|
||||
web.RenderTemplate(w, templateFiles, "login.html", page{Short: "Invalid credentials"}, srv.csrf.TemplateFunc(w))
|
||||
return
|
||||
}
|
||||
|
||||
web.SetSessionCookie(w, cookieName, token)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func (srv *server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
token := web.GetSessionToken(r, cookieName)
|
||||
if token != "" {
|
||||
_ = srv.auth.Logout(token)
|
||||
}
|
||||
web.ClearSessionCookie(w, cookieName)
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
|
||||
func (srv *server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
info := auth.TokenInfoFromContext(r.Context())
|
||||
web.RenderTemplate(w, templateFiles, "index.html", page{Username: info.Username}, srv.csrf.TemplateFunc(w))
|
||||
}
|
||||
|
||||
func (srv *server) postURL(w http.ResponseWriter, r *http.Request) {
|
||||
info := auth.TokenInfoFromContext(r.Context())
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -67,11 +119,14 @@ func (srv *server) postURL(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
srv.servePage(w, page{Short: short}, "index.tpl")
|
||||
web.RenderTemplate(w, templateFiles, "index.html", page{Username: info.Username, Short: short}, srv.csrf.TemplateFunc(w))
|
||||
}
|
||||
|
||||
func (srv *server) redirect(w http.ResponseWriter, r *http.Request) {
|
||||
short := strings.TrimPrefix(r.URL.Path, "/")
|
||||
short := chi.URLParam(r, "short")
|
||||
if short == "" {
|
||||
short = strings.TrimPrefix(r.URL.Path, "/")
|
||||
}
|
||||
u, err := links.RetrieveURL(context.Background(), srv.db, short)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@@ -82,47 +137,17 @@ func (srv *server) redirect(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (srv *server) listAll(w http.ResponseWriter, r *http.Request) {
|
||||
info := auth.TokenInfoFromContext(r.Context())
|
||||
allPosts, err := links.FetchAll(context.Background(), srv.db)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
p := page{
|
||||
URLs: allPosts,
|
||||
}
|
||||
|
||||
srv.servePage(w, p, "list.tpl")
|
||||
}
|
||||
|
||||
func (srv *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet && links.ValidShortCode.MatchString(r.URL.Path) {
|
||||
srv.redirect(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
user, pass, ok := r.BasicAuth()
|
||||
username := config.Get("HTTP_USER")
|
||||
password := config.Get("HTTP_PASS")
|
||||
|
||||
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="kls"`)
|
||||
w.WriteHeader(401)
|
||||
w.Write([]byte("Unauthorised.\n"))
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
srv.postURL(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == "/list" {
|
||||
srv.listAll(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
srv.servePage(w, page{}, "index.tpl")
|
||||
web.RenderTemplate(w, templateFiles, "list.html", page{
|
||||
Username: info.Username,
|
||||
URLs: allPosts,
|
||||
}, srv.csrf.TemplateFunc(w))
|
||||
}
|
||||
|
||||
func boolean(s string) bool {
|
||||
|
||||
Reference in New Issue
Block a user