pcmt/handlers/handlers.go
leo f129606b8f
All checks were successful
continuous-integration/drone/push Build is passing
add bulk changes
* add handlers for signin,singup,logout...
* introduce ent ORM and add user schema
* add live mode, devel mode to selectively turn on features via
  config/flags
* add templates, handle embedding moar smarter:
  * live mode uses live folder structure, else embedded templates are
    used
* start using tailwindcss to style stuff
* add development goodies for hot-reloading (browser-sync - bs.js)
* pimp-up config.dhall with actual custom config Type (enables remote
  schema and local values only as needed)
* add justfile (alternative to makefile for process automation)
2023-04-13 00:07:08 +02:00

638 lines
15 KiB
Go

package handlers
import (
"context"
"errors"
"fmt"
"html/template"
"io/fs"
"log"
"net/http"
"path/filepath"
"strings"
"git.dotya.ml/mirre-mt/pcmt/ent"
moduser "git.dotya.ml/mirre-mt/pcmt/modules/user"
"github.com/labstack/echo/v4"
)
type tplMap map[string]*template.Template
var (
// tmpl *template.Template
tmpls = template.New("")
templateMap tplMap
tmplFS fs.FS
)
func listAllTmpls() []string {
files, err := filepath.Glob("templates/*.tmpl")
if err != nil {
panic(err)
}
// log.Println(files)
for i, v := range files {
files[i] = strings.TrimPrefix(v, "templates/")
}
log.Println("returning files")
return files
}
func setFuncMap(t *template.Template) {
t = t.Funcs(template.FuncMap{
"ifIE": func() template.HTML { return template.HTML("<!--[if IE]>") },
"endifIE": func() template.HTML { return template.HTML("<![endif]>") },
"htmlSafe": func(html string) template.HTML {
return template.HTML(html)
},
"pageIs": func(want, got string) bool {
log.Printf("want: %s, got: %s", want, got)
if want == got {
return true
}
return false
},
})
}
func getFuncMap() []template.FuncMap {
return []template.FuncMap{
map[string]interface{}{
"ifIE": func() template.HTML { return template.HTML("<!--[if IE]>") },
"endifIE": func() template.HTML { return template.HTML("<![endif]>") },
"htmlSafe": func(html string) template.HTML {
return template.HTML(html)
},
"pageIs": func(want, got string) bool {
if want == got {
return true
}
return false
},
},
}
}
func initTemplates(f fs.FS) {
// tmpls := tmpl
log.Println(tmplFS == nil)
if f != nil || f != tmplFS {
tmplFS = f
}
log.Println(tmplFS == nil)
// for _, funcs := range getFuncMap() {
// // log.Println(funcs)
// tmpls.Funcs(funcs)
// }
setFuncMap(tmpls)
// tmpls, err := tmpls.ParseFS(f, listAllTmpls()...)
// if err != nil {
// panic(err)
// }
allTmpls := listAllTmpls()
// ensure this fails at compile time, if at all ("Must").
tmpls = template.Must(tmpls.ParseFS(tmplFS, allTmpls...))
makeTplMap(tmpls)
// tp := make(tplMap, len(allTmpls))
// for _, v := range allTmpls {
// tp[v] = tmpls.Lookup(v)
// }
//
// templateMap = tp
// tmpl = tmpls
}
func makeTplMap(tpl *template.Template) {
allTmpls := listAllTmpls()
var tp tplMap
if templateMap == nil {
tp = make(tplMap, len(allTmpls))
} else {
tp = templateMap
}
for _, v := range allTmpls {
tp[v] = tpl.Lookup(v)
}
templateMap = tp
}
func getTmpl(name string) *template.Template {
liveMode := conf.LiveMode
if liveMode {
// reload the requested template before returning it.
// tmpls = template.Must(tmpls.ParseFS(tmplFS, name))
// nuTmpls, err := tmpls.ParseFS(tmplFS, name)
// nuTmpls, err := tmpls.ParseFS(tmplFS, name)
//if err != nil {
// log.Printf("error refreshing tmpl %s, '%q'", name, err)
//} else {
// tmpls = nuTmpls
// makeTplMap(tmpls)
//}
// tpl := template.Must(template.New(name).ParseFS(tmplFS, name))
// templateMap[name] = tpl
// return tpl
log.Println("re-reading tmpls")
tpl := template.New("")
setFuncMap(tpl)
allTmpls := listAllTmpls()
// ensure this fails at compile time, if at all ("Must").
tmpls = template.Must(tpl.ParseFS(tmplFS, allTmpls...))
// InitTemplates(tmplFS)
}
return tmpls.Lookup(name)
}
func Admin() echo.HandlerFunc {
return func(c echo.Context) error {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}
}
// func Index(conf *config.Config) echo.HandlerFunc {
func Index() echo.HandlerFunc {
return func(c echo.Context) error {
// tpl, err := template.New("index").ParseFS(templates, "index.tmpl")
// tpl := template.New("index")
// tpl := theTmpls.New("index.tmpl")
// tpl := tmpls.Lookup("index.tmpl")
tpl := getTmpl("index.tmpl")
// if conf.LiveMode {
// // reload tmpls.
// LoadTemplates(tmplFS)
// }
// tpl = tpl.Funcs(template.FuncMap{
// "pageIs": func(want, got string) bool {
// if want == got {
// return true
// }
// return false
// },
// })
// log.Printf("%v", tpl)
// tpl := templateMap["index.tmpl"]
// secure := c.Request().URL.Scheme == "https"
// secure := true
csrf := c.Get("csrf").(string)
// cookieCSRF := &http.Cookie{
// Name: "_csrf",
// Value: csrf,
// // SameSite: http.SameSiteStrictMode,
// SameSite: http.SameSiteLaxMode,
// MaxAge: 3600,
// Secure: secure,
// HttpOnly: true,
// }
// c.SetCookie(cookieCSRF)
err := tpl.Execute(c.Response().Writer,
// struct {
// AppName string
// AppVer string
// Title string
// CSRF string
// DevelMode bool
// }{
// AppName: conf.AppName,
// AppVer: appver,
// Title: "Welcome!",
// CSRF: csrf,
// DevelMode: conf.DevelMode,
// },
page{
AppName: conf.AppName,
AppVer: appver,
Title: "Welcome!",
CSRF: csrf,
DevelMode: conf.DevelMode,
Current: "home",
},
)
if err != nil {
log.Printf("error when executing tmpl: '%q'", err)
return err
}
return nil
}
}
func Signin() echo.HandlerFunc {
return func(c echo.Context) error {
session, err := c.Cookie("session")
if err != nil {
if err == http.ErrNoCookie {
log.Println("no session cookie found")
}
}
if session != nil {
log.Println("got session")
// if err := session.Valid(); err == nil && session.Expires.After(time.Now()) {
if err := session.Valid(); err == nil {
c.Redirect(302, "/home")
} else {
log.Println("invalid (or expired) session")
log.Println("error:", err)
}
}
tpl := getTmpl("signin.tmpl")
// secure := c.Request().URL.Scheme == "https"
// secure := true
// csrf := c.Get("csrf").(string)
// cookieCSRF := &http.Cookie{
// Name: "_csrf",
// Value: csrf,
// // SameSite: http.SameSiteStrictMode,
// SameSite: http.SameSiteLaxMode,
// MaxAge: 3600,
// Secure: secure,
// HttpOnly: true,
// }
// c.SetCookie(cookieCSRF)
err = tpl.Execute(c.Response().Writer,
// struct {
// AppName string
// AppVer string
// Title string
// DevelMode bool
// }{
// AppName: conf.AppName,
// AppVer: appver,
// Title: "Sign in",
// DevelMode: conf.DevelMode,
// },
page{
AppName: conf.AppName,
AppVer: appver,
Title: "Sign in",
DevelMode: conf.DevelMode,
Current: "signin",
},
)
if err != nil {
return err
}
return nil
}
}
func SigninPost(client *ent.Client) echo.HandlerFunc {
return func(c echo.Context) error {
ct := c.Request().Header.Get("Content-Type")
fmt.Printf("signin(POST): content-type: %s\n", ct)
err := c.Request().ParseForm()
if err != nil {
return err
}
var username string
var password string
if uname := c.Request().FormValue("username"); uname != "" {
username = uname
log.Printf("authenticating user '%s' at /signin", username)
} else {
log.Printf("username was not set, returning to /signin")
c.Redirect(302, "/signin")
return nil
}
if passwd := c.Request().FormValue("password"); passwd != "" {
password = passwd
} else {
log.Printf("password was not set, returning to /signin")
c.Redirect(302, "/signin")
return nil
}
// if usr := client.User.Query().
// Where(
// user.Username(username),
// ); usr != nil {
if usr, err := moduser.QueryUser(context.Background(), client, username); err == nil {
log.Println(usr)
if usr.Password != password {
log.Println("wrong user credentials, redirecting to /signin")
c.Redirect(302, "/signin")
}
// if user.Password(password) {
// c.Redirect(302, "/signin")
// }
// if err != nil || p != password {
// log.Println(echo.NewHTTPError(
// http.StatusUnauthorized,
// http.StatusText(http.StatusUnauthorized)+" "+err.Error(),
// ))
//
// c.Redirect(302, "/signin")
// return nil
// }
} else {
// just log the error instead of returning it to the user and
// redirect back to /signin.
log.Println(echo.NewHTTPError(
http.StatusUnauthorized,
http.StatusText(http.StatusUnauthorized)+" "+err.Error(),
))
c.Redirect(302, "/signin")
return nil
}
secure := c.Request().URL.Scheme == "https"
cookieSession := &http.Cookie{
Name: "session",
Value: username,
// SameSite: http.SameSiteStrictMode,
SameSite: http.SameSiteLaxMode,
MaxAge: 3600,
Secure: secure,
HttpOnly: true,
}
c.SetCookie(cookieSession)
c.Redirect(301, "/home")
return nil
}
}
func Signup() echo.HandlerFunc {
return func(c echo.Context) error {
session, err := c.Cookie("session")
if err != nil {
if err == http.ErrNoCookie {
log.Println("no session cookie found")
}
}
if session != nil {
log.Println("got session")
// if err := session.Valid(); err == nil && session.Expires.After(time.Now()) {
if err := session.Valid(); err == nil {
c.Redirect(302, "/home")
} else {
log.Println("invalid (or expired) session")
log.Println("error:", err)
}
}
tpl := getTmpl("signup.tmpl")
csrf := c.Get("csrf").(string)
// secure := c.Request().URL.Scheme == "https"
// cookieCSRF := &http.Cookie{
// Name: "_csrf",
// Value: csrf,
// // SameSite: http.SameSiteStrictMode,
// SameSite: http.SameSiteLaxMode,
// MaxAge: 3600,
// Secure: secure,
// HttpOnly: true,
// }
// c.SetCookie(cookieCSRF)
err = tpl.Execute(c.Response().Writer,
page{
AppName: conf.AppName,
AppVer: appver,
Title: "Sign up",
CSRF: csrf,
DevelMode: conf.DevelMode,
Current: "signup",
},
)
if err != nil {
log.Printf("error: %q", err)
http.Error(c.Response().Writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return nil
}
}
func SignupPost(client *ent.Client) echo.HandlerFunc {
return func(c echo.Context) error {
// tpl := getTmpl("signup.tmpl", conf.LiveMode)
ct := c.Request().Header.Get("Content-Type")
fmt.Printf("content-type: %s\n", ct)
err := c.Request().ParseForm()
if err != nil {
return err
}
var username string
if uname := c.Request().FormValue("username"); uname != "" {
username = uname
if passwd := c.Request().FormValue("password"); passwd != "" {
if u, err := moduser.CreateUser(context.Background(), client, username, passwd); err != nil {
// TODO: don't return the error to the user, perhaps based
// on the devel mode.
return echo.NewHTTPError(
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError)+" failed to create schema resources "+err.Error(),
)
} else {
log.Printf("successfully registered user '%s': %#v", username, u)
}
} else {
log.Println("password was not set, returning to /signup")
}
} else {
log.Printf("username was not set, returning to /signup")
c.Redirect(302, "/signup")
return nil
}
secure := c.Request().URL.Scheme == "https"
// secure := true
// csrf := c.Get("csrf").(string)
cookieSession := &http.Cookie{
Name: "session",
Value: username,
// SameSite: http.SameSiteStrictMode,
SameSite: http.SameSiteLaxMode,
MaxAge: 3600,
Secure: secure,
HttpOnly: true,
}
c.SetCookie(cookieSession)
// cookieCSRF := &http.Cookie{
// Name: "_csrf",
// Value: csrf,
// // SameSite: http.SameSiteStrictMode,
// SameSite: http.SameSiteLaxMode,
// MaxAge: 3600,
// Secure: secure,
// HttpOnly: true,
// }
// c.SetCookie(cookieCSRF)
c.Redirect(301, "/home")
return nil
}
}
func Home() echo.HandlerFunc {
return func(c echo.Context) error {
var username string
tpl := getTmpl("home.tmpl")
session, err := c.Cookie("session")
if err != nil {
if err == http.ErrNoCookie {
// return err
log.Printf("error no cookie: %q", err)
http.Error(c.Response().Writer, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return nil
}
log.Println("error:", err)
http.Error(c.Response().Writer, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
// fmt.Println("session cookie not found, redirecting to signin page")
return nil
}
if session != nil {
log.Println("got session")
// if err := session.Valid(); err == nil && session.Expires.After(time.Now()) {
if err := session.Valid(); err == nil {
username = session.Value
} else {
log.Println("invalid or expired session?")
return echo.NewHTTPError(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))
}
}
// example denial.
// if _, err := c.Cookie("aha"); err != nil {
// log.Printf("error: %q", err)
// return echo.NewHTTPError(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))
// }
err = tpl.Execute(c.Response().Writer,
page{
AppName: conf.AppName,
AppVer: appver,
Title: "Home",
Name: username,
DevelMode: conf.DevelMode,
Current: "home",
},
)
if err != nil {
// return err
log.Printf("error: %q", err)
return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
return nil
}
}
func Logout() echo.HandlerFunc {
return func(c echo.Context) error {
if c.Request().Method == "POST" {
session, err := c.Cookie("session")
if err != nil {
if errors.Is(err, http.ErrNoCookie) {
fmt.Println("nobody to log out, redirecting to /signin")
c.Redirect(302, "/signin")
return nil
}
log.Printf("error: %q", err)
return nil
}
var username string
if err := session.Valid(); err == nil {
username = session.Value
}
log.Printf("logging out user '%s'", username)
secure := c.Request().URL.Scheme == "https"
cookieSession := &http.Cookie{
Name: "session",
Value: "",
// SameSite: http.SameSiteStrictMode,
SameSite: http.SameSiteLaxMode,
MaxAge: -1,
Secure: secure,
HttpOnly: true,
}
c.SetCookie(cookieSession)
}
tpl := getTmpl("logout.tmpl")
err := tpl.Execute(c.Response().Writer,
struct {
AppName string
AppVer string
Title string
DevelMode bool
}{
AppName: conf.AppName,
AppVer: appver,
Title: "Logout",
DevelMode: conf.DevelMode,
},
)
if err != nil {
log.Printf("error: %q", err)
return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
return nil
}
}
// experimental global redirect handler?
// http.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
// loc, err := url.QueryUnescape(r.URL.Query().Get("loc"))
// if err != nil {
// http.Error(w, fmt.Sprintf("invalid redirect: %q", r.URL.Query().Get("loc")), http.StatusBadRequest)
// return
// }
// http.Redirect(w, r, loc, http.StatusFound)
// })