package serve
import (
"context"
"strings"
// "encoding/base64"
// "encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"gopkg.awl.red/lx/internal/auth"
"gopkg.awl.red/lx/internal/config"
"gopkg.awl.red/lx/internal/db"
"gopkg.awl.red/lx/internal/h"
"gopkg.awl.red/lx/internal/log"
)
var msgBadFolder = "site only support folders 1 deep"
var msgBadFolderB = []byte(msgBadFolder)
var msg404 = "404 not found"
var msg404B = []byte(msg404)
func wBdTp(w http.ResponseWriter, r *http.Request, err error) {
if err == nil {
return
}
slog.Error("could not write route", "route", r.URL.Path, "err", err)
w.WriteHeader(http.StatusInternalServerError)
}
func r___GET(path string, f http.Handler) {
http.Handle("GET "+path, f)
}
func r__POST(path string, f http.Handler) {
http.Handle("POST "+path, f)
}
func Serve() error {
chAddrChange := make(chan config.AddrChange, 5)
dsn := config.GetDsn()
adsn := config.GetAdsn()
slog.Debug("dsn", "dsn", dsn, "adsn", adsn)
conn, err := db.New(dsn, adsn, chAddrChange)
if err != nil {
return err
}
cfg := conn.GetConfig()
log.SetFromConfig(cfg)
if cfg == nil {
return err
}
r___GET("/favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(nil)
}))
r___GET("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" || r.URL.Path == "" {
cookie, err := r.Cookie("Session")
if err != nil {
http.Redirect(w, r, "/_ui/login", http.StatusSeeOther)
return
}
c := conn.GetConfig()
session, err := conn.GetSession(cookie.Value, c)
if err != nil || session == nil {
http.Redirect(w, r, "/_ui/login", http.StatusSeeOther)
return
}
http.Redirect(w, r, fmt.Sprintf("/_ui/pn/%d/businesses", session.PersonId), http.StatusSeeOther)
}
numSlash := strings.Count(r.URL.Path, "/")
if numSlash > 1 {
w.WriteHeader(http.StatusBadRequest)
w.Write(msgBadFolderB)
return
}
host, _, _ := strings.Cut(r.URL.Host, ":")
// TODO: check if this works on real domains, and behind a proxy like nginx
if host == "" {
host = "localhost"
}
bh, err := conn.GetBackHalf(cfg, host, strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/"))
if err != nil {
w.WriteHeader(http.StatusNotFound)
w.Write(msg404B)
return
}
http.Redirect(w, r, bh.Redirect, http.StatusSeeOther)
// TODO do analytics
conn.InsertAnalytics(bh.Id, r)
}))
r___GET("/_ui/site_config", mkMiddlewareSession(conn)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, hasSession := r.Context().Value("auth").(auth.Session)
if !hasSession {
w.WriteHeader(http.StatusForbidden)
return
}
if _, isAdmin := session.Roles[auth.RoleAdmin]; !isAdmin {
w.WriteHeader(http.StatusForbidden)
return
}
wBdTp(w, r, h.TSiteConfigEdit(w, r.URL.Query(), conn.GetConfig()))
})))
r___GET("/_ui/pricing", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wBdTp(w, r, h.TPricing(w, r.URL.Query(), conn.GetConfig()))
}))
r___GET("/_ui/signup", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wBdTp(w, r, h.TSignup(w, conn.GetConfig()))
}))
r___GET("/_ui/login", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wBdTp(w, r, h.TLogin(w, conn.GetConfig(), r.URL.Query()))
}))
r__POST("/login", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
slog.Error("could not parse form", "route", r.URL.Path, "err", err)
w.WriteHeader(http.StatusBadRequest)
return
}
f := r.Form
username := f.Get("username")
password := f.Get("password")
email := f.Get("email")
cfg := conn.GetConfig()
token, session, err := conn.DoLogin(username, password, email, cfg)
if err != nil {
slog.Warn("could not log in", "err", err)
http.Redirect(w, r, "/login?bad_login", http.StatusSeeOther)
return
}
auth.SetSession(w.Header(), token, cfg)
http.Redirect(w, r, fmt.Sprintf("/pn/%d/businesses", session.PersonId), http.StatusSeeOther)
wBdTp(w, r, h.TLogin(w, conn.GetConfig(), r.URL.Query()))
}))
lggout := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("Session")
if err == nil {
err := conn.DeleteSession(cookie.Value)
if err != nil {
w.Write([]byte("could not log out"))
return
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Location", "/login?did_logout")
w.Header().Set("Set-Cookie", auth.DelSessionCookie)
w.WriteHeader(http.StatusSeeOther)
})
r___GET("/logout", lggout)
r__POST("/logout", lggout)
r__POST("/_ui/site_config", mkMiddlewareSession(conn)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, hasSession := r.Context().Value("auth").(auth.Session)
if !hasSession {
w.WriteHeader(http.StatusForbidden)
return
}
if _, isAdmin := session.Roles[auth.RoleAdmin]; !isAdmin {
w.WriteHeader(http.StatusForbidden)
return
}
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
cols, vals, err := config.ParseUpdateForm(r.Form, conn.GetConfig())
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
err = conn.SetConfig(cols, vals)
if err != nil {
http.Redirect(w, r, "/site_config?bad_edit", http.StatusSeeOther)
} else {
http.Redirect(w, r, "/site_config?edit_saved", http.StatusSeeOther)
}
})))
addr := cfg.ListenAddress + ":" + strconv.Itoa(int(cfg.Port))
server := &http.Server{Addr: addr}
chSig := make(chan os.Signal, 1)
signal.Notify(chSig, os.Interrupt, syscall.SIGTERM)
go func() {
dieSig := <-chSig
gracefulTime := 1 * time.Second
if dieSig == syscall.SIGTERM {
gracefulTime = 0
}
ctx, cancel := context.WithTimeout(context.Background(), gracefulTime)
slog.Info("signal received, shutting down server", "signal", dieSig, "grace_period", gracefulTime.String())
server.Shutdown(ctx)
cancel()
}()
go func() {
for msg := range chAddrChange {
slog.Info("got address change")
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
chAddrChange <- msg
server.Shutdown(ctx)
cancel()
}
}()
stopServeLoop:
for {
slog.Info("listening", "address", cfg.ListenAddress, "port", cfg.Port)
err = server.ListenAndServe()
slog.Info("server stopped listening", "err", err)
select {
case newAddr := <-chAddrChange:
addr = newAddr.Address + ":" + strconv.Itoa(int(newAddr.Port))
server = &http.Server{Addr: addr}
case <-time.After(10 * time.Millisecond):
break stopServeLoop
}
}
slog.Info("quitting")
return nil
}
func mkMiddlewareSession(d *db.DB) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("Session")
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
slog.Info("no session cookie", "err", err)
return
}
session, err := d.GetSession(cookie.Value, d.GetConfig())
if err != nil || session == nil {
w.WriteHeader(http.StatusUnauthorized)
slog.Info("error middleware session", "err", err)
return
}
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "auth", *session)))
})
}
}