Add new debug option for http

This commit is contained in:
Tom Wiesing 2023-11-22 17:28:46 +01:00
parent 0ba34fe80f
commit 0290a42d07
No known key found for this signature in database
39 changed files with 293 additions and 189 deletions

4
go.mod
View file

@ -10,13 +10,12 @@ require (
github.com/gliderlabs/ssh v0.3.5
github.com/gorilla/csrf v1.7.2
github.com/gorilla/sessions v1.2.2
github.com/gorilla/websocket v1.5.1
github.com/julienschmidt/httprouter v1.3.0
github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.4.0
github.com/rs/zerolog v1.31.0
github.com/tkw1536/goprogram v0.5.0
github.com/tkw1536/pkglib v0.0.0-20231114141909-8837d3186025
github.com/tkw1536/pkglib v0.0.0-20231122155813-969c635025aa
github.com/yuin/goldmark v1.6.0
github.com/yuin/goldmark-meta v1.1.0
golang.org/x/crypto v0.15.0
@ -42,6 +41,7 @@ require (
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/gosuri/uilive v0.0.4 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/jessevdk/go-flags v1.5.0 // indirect

29
go.sum
View file

@ -13,8 +13,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/compose-spec/compose-go v1.20.0 h1:h4ZKOst1EF/DwZp7dWkb+wbTVE4nEyT9Lc89to84Ol4=
github.com/compose-spec/compose-go v1.20.0/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM=
github.com/compose-spec/compose-go v1.20.1 h1:I6gCMGLl96kEf8XZwaozeTwnNfxA2eVsO46W+5ciTEg=
github.com/compose-spec/compose-go v1.20.1/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@ -48,11 +46,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
@ -124,10 +119,10 @@ github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM=
github.com/tdewolff/test v1.0.7/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tkw1536/goprogram v0.5.0 h1:7vcIjmMdcZPJyRhgdlCaGfHAoOG3oYlFrno1pWXy1Bs=
github.com/tkw1536/goprogram v0.5.0/go.mod h1:MDCwqLmvcc2QryMm6oSC9h/QAdE9PewZ2Mp2Lm7MmAg=
github.com/tkw1536/pkglib v0.0.0-20231110192201-b920fd9f7764 h1:rJStxc6PoFUQcsRRqoUNtaM80GZWT69WfWeA+EDbvXM=
github.com/tkw1536/pkglib v0.0.0-20231110192201-b920fd9f7764/go.mod h1:Qi/vpuxuxo5D40O9jLUSmcUF01B5LmJqDxs8o8Lc6bg=
github.com/tkw1536/pkglib v0.0.0-20231114141909-8837d3186025 h1:t3ewoi0rdqQo0a8zFFpmtsUi+O4C+kCYoOM/QkXV7b0=
github.com/tkw1536/pkglib v0.0.0-20231114141909-8837d3186025/go.mod h1:Qi/vpuxuxo5D40O9jLUSmcUF01B5LmJqDxs8o8Lc6bg=
github.com/tkw1536/pkglib v0.0.0-20231121123232-879eff5cea2d h1:J8EvqdNY/qJpn8qoYkxtHx+Uag6sbHxgXNClt6S3Zqc=
github.com/tkw1536/pkglib v0.0.0-20231121123232-879eff5cea2d/go.mod h1:Qi/vpuxuxo5D40O9jLUSmcUF01B5LmJqDxs8o8Lc6bg=
github.com/tkw1536/pkglib v0.0.0-20231122155813-969c635025aa h1:HQxorKzWcH3D0In/G6Y24IT9KVVcGjeJGcZzN0T3b30=
github.com/tkw1536/pkglib v0.0.0-20231122155813-969c635025aa/go.mod h1:Qi/vpuxuxo5D40O9jLUSmcUF01B5LmJqDxs8o8Lc6bg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@ -145,18 +140,12 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -165,15 +154,11 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -194,16 +179,12 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
@ -212,8 +193,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -39,6 +39,11 @@ http:
# This email address can be configured here.
certbot_email: null
# Debug determines if error messages should be written as html pages with stack traces to http clients.
# This potentially exposes sensitive information and may cause certain API responses to be of content type 'text/html' unexpectedly.
# It is not recommended to use this on production systems.
debug: null
# Serve the panel also on the toplevel domain, and not only on the "panel" domain.
# Enabled by default.
panel: null

View file

@ -25,6 +25,10 @@ type HTTPConfig struct {
// This email address can be configured here.
CertbotEmail string `yaml:"certbot_email" validate:"email"`
// Debug determines if error messages should be written as html pages with stack traces to http clients.
// This potentially exposes sensitive information and may cause certain API responses to be of content type 'text/html' unexpectedly.
Debug validators.NullableBool `yaml:"debug" validate:"bool" default:"false"`
// Also serve the panel on the toplevel domain.
// Note that the panel is *always* servered under the "panel" domain.
// Disabling this is not recommended.

View file

@ -10,6 +10,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/handling"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/users"
"github.com/tkw1536/pkglib/httpx"
@ -21,6 +22,7 @@ type Next struct {
Auth *auth.Auth
Policy *policy.Policy
Instances *instances.Instances
Handleing *handling.Handling
}
}
@ -72,7 +74,7 @@ func (next *Next) getInstance(r *http.Request) (wisski *wisski.WissKI, path stri
}
func (next *Next) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
return httpx.RedirectHandler(func(r *http.Request) (string, int, error) {
return next.dependencies.Handleing.Redirect(func(r *http.Request) (string, int, error) {
// get the instance and the path
instance, path, err := next.getInstance(r)
if err != nil {

View file

@ -11,25 +11,29 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/tokens"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/handling"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/ssh2/sshkeys"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/julienschmidt/httprouter"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/form"
)
type UserPanel struct {
component.Base
dependencies struct {
Auth *auth.Auth
Auth *auth.Auth
Handling *handling.Handling
Templating *templating.Templating
Policy *policy.Policy
Tokens *tokens.Tokens
Instances *instances.Instances
Next *next.Next
Keys *sshkeys.SSHKeys
SSH2 *ssh2.SSH2
Policy *policy.Policy
Tokens *tokens.Tokens
Instances *instances.Instances
Next *next.Next
Keys *sshkeys.SSHKeys
SSH2 *ssh2.SSH2
}
}
@ -142,12 +146,12 @@ func (panel *UserPanel) HandleRoute(ctx context.Context, route string) (http.Han
type userFormContext struct {
templating.RuntimeFlags
httpx.FormContext
form.FormContext
User *models.User
}
func (panel *UserPanel) UserFormContext(tpl *templating.Template[userFormContext], last component.MenuItem, funcs ...templating.FlagFunc) func(ctx httpx.FormContext, r *http.Request) any {
func (panel *UserPanel) UserFormContext(tpl *templating.Template[userFormContext], last component.MenuItem, funcs ...templating.FlagFunc) func(ctx form.FormContext, r *http.Request) any {
funcs = append(funcs, func(flags templating.Flags, r *http.Request) templating.Flags {
// append the last menu item, and prepend the menuUser one!
flags.Crumbs = append(flags.Crumbs, last, last)
@ -156,7 +160,7 @@ func (panel *UserPanel) UserFormContext(tpl *templating.Template[userFormContext
return flags
})
return func(ctx httpx.FormContext, r *http.Request) any {
return func(ctx form.FormContext, r *http.Request) any {
uctx := userFormContext{FormContext: ctx}
if user, err := panel.dependencies.Auth.UserOfSession(r); err == nil {
uctx.User = &user.User

View file

@ -9,14 +9,14 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/field"
"github.com/tkw1536/pkglib/httpx/form"
"github.com/tkw1536/pkglib/httpx/form/field"
)
//go:embed "templates/password.html"
var passwordHTML []byte
var passwordTemplate = templating.Parse[userFormContext](
"password.html", passwordHTML, httpx.FormTemplate,
"password.html", passwordHTML, form.FormTemplate,
templating.Title("Change Password"),
templating.Assets(assets.AssetsUser),
@ -34,17 +34,17 @@ var (
func (panel *UserPanel) routePassword(ctx context.Context) http.Handler {
tpl := passwordTemplate.Prepare(panel.dependencies.Templating)
return &httpx.Form[struct{}]{
return &form.Form[struct{}]{
Fields: []field.Field{
{Name: "old", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"},
{Name: "otp", Type: field.Text, Autocomplete: field.OneTimeCode, EmptyOnError: true, Label: "Current Passcode (optional)"},
{Name: "new", Type: field.Password, Autocomplete: field.NewPassword, EmptyOnError: true, Label: "New Password"},
{Name: "new2", Type: field.Password, Autocomplete: field.NewPassword, EmptyOnError: true, Label: "New Password (again)"},
},
FieldTemplate: field.PureCSSFieldTemplate,
FieldTemplate: assets.PureCSSFieldTemplate,
RenderTemplate: tpl.Template(),
RenderTemplateContext: panel.UserFormContext(tpl, menuChangePassword),
Template: tpl.Template(),
TemplateContext: panel.UserFormContext(tpl, menuChangePassword),
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
old, passcode, new, new2 := values["old"], values["otp"], values["new"], values["new2"]
@ -82,7 +82,7 @@ func (panel *UserPanel) routePassword(ctx context.Context) http.Handler {
return struct{}{}, nil
},
RenderSuccess: func(_ struct{}, values map[string]string, w http.ResponseWriter, r *http.Request) error {
Success: func(_ struct{}, values map[string]string, w http.ResponseWriter, r *http.Request) error {
return errPasswordSet
},
}

View file

@ -13,7 +13,8 @@ import (
"github.com/gliderlabs/ssh"
"github.com/rs/zerolog"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/field"
"github.com/tkw1536/pkglib/httpx/form"
"github.com/tkw1536/pkglib/httpx/form/field"
gossh "golang.org/x/crypto/ssh"
@ -57,7 +58,7 @@ func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandler(func(r *http.Request) (sc SSHTemplateContext, err error) {
return tpl.HTMLHandler(panel.dependencies.Handling, func(r *http.Request) (sc SSHTemplateContext, err error) {
user, err := panel.dependencies.Auth.UserOfSession(r)
if err != nil {
return sc, err
@ -129,7 +130,7 @@ func (panel *UserPanel) sshDeleteRoute(ctx context.Context) http.Handler {
//go:embed "templates/ssh_add.html"
var sshAddHTML []byte
var sshAddTemplate = templating.ParseForm(
"ssh_add.html", sshAddHTML, httpx.FormTemplate,
"ssh_add.html", sshAddHTML, form.FormTemplate,
templating.Title("Add SSH Key"),
templating.Assets(assets.AssetsUser),
)
@ -150,15 +151,15 @@ func (panel *UserPanel) sshAddRoute(ctx context.Context) http.Handler {
),
)
return &httpx.Form[addKeyResult]{
return &form.Form[addKeyResult]{
Fields: []field.Field{
{Name: "comment", Type: field.Text, Label: "Comment"},
{Name: "key", Type: field.Textarea, Label: "Key in authorized_keys format"}, // has hacked css!
},
FieldTemplate: field.PureCSSFieldTemplate,
FieldTemplate: assets.PureCSSFieldTemplate,
RenderTemplate: tpl.Template(),
RenderTemplateContext: templating.FormTemplateContext(tpl),
Template: tpl.Template(),
TemplateContext: templating.FormTemplateContext(tpl),
Validate: func(r *http.Request, values map[string]string) (ak addKeyResult, err error) {
ak.User, err = panel.dependencies.Auth.UserOfSession(r)
@ -181,7 +182,7 @@ func (panel *UserPanel) sshAddRoute(ctx context.Context) http.Handler {
return ak, nil
},
RenderSuccess: func(ak addKeyResult, values map[string]string, w http.ResponseWriter, r *http.Request) error {
Success: func(ak addKeyResult, values map[string]string, w http.ResponseWriter, r *http.Request) error {
// add the key to the user
if err := panel.dependencies.Keys.Add(r.Context(), ak.User.User.User, ak.Comment, ak.Key); err != nil {
return errAddKey

View file

@ -11,7 +11,8 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/rs/zerolog"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/field"
"github.com/tkw1536/pkglib/httpx/form"
"github.com/tkw1536/pkglib/httpx/form/field"
_ "embed"
)
@ -44,7 +45,7 @@ func (panel *UserPanel) tokensRoute(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandler(func(r *http.Request) (tc TokenTemplateContext, err error) {
return tpl.HTMLHandler(panel.dependencies.Handling, func(r *http.Request) (tc TokenTemplateContext, err error) {
// list the user
user, err := panel.dependencies.Auth.UserOfSession(r)
if err != nil || user == nil {
@ -94,7 +95,7 @@ func (panel *UserPanel) tokensDeleteRoute(ctx context.Context) http.Handler {
//go:embed "templates/tokens_add.html"
var tokensAddHTML []byte
var tokensAddTemplate = templating.ParseForm(
"tokens_add.html", tokensAddHTML, httpx.FormTemplate,
"tokens_add.html", tokensAddHTML, form.FormTemplate,
templating.Title("Add Token"),
templating.Assets(assets.AssetsUser),
)
@ -108,7 +109,7 @@ type addTokenResult struct {
//go:embed "templates/token_created.html"
var tokenCreatedHTML []byte
var tokenCreateTemplate = templating.Parse[TokenCreateContext](
"token_created.html", tokenCreatedHTML, httpx.FormTemplate,
"token_created.html", tokenCreatedHTML, form.FormTemplate,
templating.Title("Add Token"),
templating.Assets(assets.AssetsUser),
)
@ -139,14 +140,14 @@ func (panel *UserPanel) tokensAddRoute(ctx context.Context) http.Handler {
),
)
return &httpx.Form[addTokenResult]{
return &form.Form[addTokenResult]{
Fields: []field.Field{
{Name: "description", Type: field.Text, Label: "Description"},
},
FieldTemplate: field.PureCSSFieldTemplate,
FieldTemplate: assets.PureCSSFieldTemplate,
RenderTemplate: tplForm.Template(),
RenderTemplateContext: templating.FormTemplateContext(tplForm),
Template: tplForm.Template(),
TemplateContext: templating.FormTemplateContext(tplForm),
Validate: func(r *http.Request, values map[string]string) (at addTokenResult, err error) {
at.User, err = panel.dependencies.Auth.UserOfSession(r)
@ -164,7 +165,7 @@ func (panel *UserPanel) tokensAddRoute(ctx context.Context) http.Handler {
return at, nil
},
RenderSuccess: func(at addTokenResult, values map[string]string, w http.ResponseWriter, r *http.Request) error {
Success: func(at addTokenResult, values map[string]string, w http.ResponseWriter, r *http.Request) error {
// add the key to the user
tok, err := panel.dependencies.Tokens.Add(r.Context(), at.User.User.User, at.Description, at.Scopes)
if err != nil {
@ -175,10 +176,16 @@ func (panel *UserPanel) tokensAddRoute(ctx context.Context) http.Handler {
}
// render the created context
return httpx.WriteHTML(tplDone.Context(r, TokenCreateContext{
Domain: template.URL(panel.Config.HTTP.JoinPath().String()),
Token: tok,
}), nil, tplDone.Template(), "", w, r)
return panel.dependencies.Handling.WriteHTML(
tplDone.Context(r, TokenCreateContext{
Domain: template.URL(panel.Config.HTTP.JoinPath().String()),
Token: tok,
}),
nil,
tplDone.Template(),
w,
r,
)
},
}
}

View file

@ -8,8 +8,8 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/field"
"github.com/tkw1536/pkglib/httpx/form"
"github.com/tkw1536/pkglib/httpx/form/field"
_ "embed"
)
@ -17,7 +17,7 @@ import (
//go:embed "templates/totp_enable.html"
var totpEnableHTML []byte
var totpEnable = templating.Parse[userFormContext](
"totp_enable.html", totpEnableHTML, httpx.FormTemplate,
"totp_enable.html", totpEnableHTML, form.FormTemplate,
templating.Title("Enable TOTP"),
templating.Assets(assets.AssetsUser),
@ -26,19 +26,19 @@ var totpEnable = templating.Parse[userFormContext](
func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
tpl := totpEnable.Prepare(panel.dependencies.Templating)
return &httpx.Form[struct{}]{
return &form.Form[struct{}]{
Fields: []field.Field{
{Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"},
},
FieldTemplate: field.PureCSSFieldTemplate,
FieldTemplate: assets.PureCSSFieldTemplate,
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
Skip: func(r *http.Request) (data struct{}, skip bool) {
user, err := panel.dependencies.Auth.UserOfSession(r)
return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled()
},
RenderTemplate: tpl.Template(),
RenderTemplateContext: panel.UserFormContext(tpl, menuTOTPEnable),
Template: tpl.Template(),
TemplateContext: panel.UserFormContext(tpl, menuTOTPEnable),
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password := values["password"]
@ -64,7 +64,7 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
return struct{}{}, nil
},
RenderSuccess: func(_ struct{}, values map[string]string, w http.ResponseWriter, r *http.Request) error {
Success: func(_ struct{}, values map[string]string, w http.ResponseWriter, r *http.Request) error {
http.Redirect(w, r, "/user/totp/enroll", http.StatusSeeOther)
return nil
},
@ -74,7 +74,7 @@ func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler {
//go:embed "templates/totp_enroll.html"
var totpEnrollHTML []byte
var totpEnrollTemplate = templating.Parse[totpEnrollContext](
"totp_enroll.html", totpEnrollHTML, httpx.FormTemplate,
"totp_enroll.html", totpEnrollHTML, form.FormTemplate,
templating.Title("Enable TOTP"),
templating.Assets(assets.AssetsUser),
@ -97,18 +97,20 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
),
)
return &httpx.Form[struct{}]{
return &form.Form[struct{}]{
Fields: []field.Field{
{Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"},
{Name: "otp", Type: field.Text, Autocomplete: field.OneTimeCode, EmptyOnError: true, Label: "Passcode"},
},
FieldTemplate: field.PureCSSFieldTemplate,
FieldTemplate: assets.PureCSSFieldTemplate,
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
Skip: func(r *http.Request) (data struct{}, skip bool) {
user, err := panel.dependencies.Auth.UserOfSession(r)
return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled()
},
RenderTemplateContext: func(context httpx.FormContext, r *http.Request) any {
Template: tpl.Template(),
TemplateContext: func(context form.FormContext, r *http.Request) any {
user, err := panel.dependencies.Auth.UserOfSession(r)
ctx := totpEnrollContext{
@ -131,7 +133,6 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
return tpl.Context(r, ctx)
},
RenderTemplate: tpl.Template(),
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password, otp := values["password"], values["otp"]
@ -157,7 +158,7 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
return struct{}{}, nil
},
RenderSuccess: func(_ struct{}, values map[string]string, w http.ResponseWriter, r *http.Request) error {
Success: func(_ struct{}, values map[string]string, w http.ResponseWriter, r *http.Request) error {
http.Redirect(w, r, "/user/", http.StatusSeeOther)
return nil
},
@ -167,7 +168,7 @@ func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler {
//go:embed "templates/totp_disable.html"
var totpDisableHTML []byte
var totpDisableTemplate = templating.Parse[userFormContext](
"totp_disable.html", totpDisableHTML, httpx.FormTemplate,
"totp_disable.html", totpDisableHTML, form.FormTemplate,
templating.Title("Disable TOTP"),
templating.Assets(assets.AssetsUser),
@ -176,19 +177,20 @@ var totpDisableTemplate = templating.Parse[userFormContext](
func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler {
tpl := totpDisableTemplate.Prepare(panel.dependencies.Templating)
return &httpx.Form[struct{}]{
return &form.Form[struct{}]{
Fields: []field.Field{
{Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Current Password"},
{Name: "otp", Type: field.Text, Autocomplete: field.OneTimeCode, EmptyOnError: true, Label: "Current Passcode"},
},
FieldTemplate: field.PureCSSFieldTemplate,
FieldTemplate: assets.PureCSSFieldTemplate,
SkipForm: func(r *http.Request) (data struct{}, skip bool) {
Skip: func(r *http.Request) (data struct{}, skip bool) {
user, err := panel.dependencies.Auth.UserOfSession(r)
return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled()
},
RenderTemplate: tpl.Template(),
RenderTemplateContext: panel.UserFormContext(tpl, menuTOTPDisable),
Template: tpl.Template(),
TemplateContext: panel.UserFormContext(tpl, menuTOTPDisable),
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
password, otp := values["password"], values["otp"]
@ -214,7 +216,7 @@ func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler {
return struct{}{}, nil
},
RenderSuccess: func(_ struct{}, values map[string]string, w http.ResponseWriter, r *http.Request) error {
Success: func(_ struct{}, values map[string]string, w http.ResponseWriter, r *http.Request) error {
http.Redirect(w, r, "/user/", http.StatusSeeOther)
return nil
},

View file

@ -59,7 +59,7 @@ func (panel *UserPanel) routeUser(ctx context.Context) http.Handler {
templating.Actions(actions...),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (uc userContext, funcs []templating.FlagFunc, err error) {
return tpl.HTMLHandlerWithFlags(panel.dependencies.Handling, func(r *http.Request) (uc userContext, funcs []templating.FlagFunc, err error) {
// find the user
uc.AuthUser, err = panel.dependencies.Auth.UserOfSession(r)
if err != nil || uc.AuthUser == nil {

View file

@ -10,8 +10,8 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/field"
"github.com/tkw1536/pkglib/httpx/form"
"github.com/tkw1536/pkglib/httpx/form/field"
"github.com/gorilla/sessions"
@ -162,7 +162,7 @@ func (auth *Auth) Logout(w http.ResponseWriter, r *http.Request) error {
//go:embed "login.html"
var loginHTML []byte
var loginTemplate = templating.ParseForm(
"login.html", loginHTML, httpx.FormTemplate,
"login.html", loginHTML, form.FormTemplate,
templating.Title("Login Required"),
templating.Assets(assets.AssetsUser),
@ -182,21 +182,21 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler {
},
)
return &httpx.Form[*AuthUser]{
return &form.Form[*AuthUser]{
Fields: []field.Field{
{Name: "username", Type: field.Text, Autocomplete: field.Username, Label: "Username"},
{Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Password"},
{Name: "otp", Type: field.Text, Autocomplete: field.OneTimeCode, EmptyOnError: true, Label: "Passcode (optional)"},
},
FieldTemplate: field.PureCSSFieldTemplate,
FieldTemplate: assets.PureCSSFieldTemplate,
RenderTemplateContext: func(ctx httpx.FormContext, r *http.Request) any {
Template: tpl.Template(),
TemplateContext: func(ctx form.FormContext, r *http.Request) any {
if ctx.Err != nil {
ctx.Err = errLoginFailed
}
return tpl.Context(r, templating.NewFormContext(ctx))
},
RenderTemplate: tpl.Template(),
Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) {
username, password, passcode := values["username"], values["password"], values["otp"]
@ -215,12 +215,12 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler {
return user, nil
},
SkipForm: func(r *http.Request) (user *AuthUser, skip bool) {
Skip: func(r *http.Request) (user *AuthUser, skip bool) {
user, err := auth.UserOfSession(r)
return user, err == nil && user != nil
},
RenderSuccess: func(user *AuthUser, _ map[string]string, w http.ResponseWriter, r *http.Request) error {
Success: func(user *AuthUser, _ map[string]string, w http.ResponseWriter, r *http.Request) error {
if err := auth.Login(w, r, user); err != nil {
return err
}

View file

@ -14,9 +14,9 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/handling"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/rs/zerolog"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/lazy"
_ "embed"
@ -27,6 +27,7 @@ type Resolver struct {
dependencies struct {
Instances *instances.Instances
Templating *templating.Templating
Handling *handling.Handling
Auth *auth.Auth
}
@ -106,7 +107,7 @@ func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.H
if resolver.dependencies.Auth.CheckScope("", scopes.ScopeUserValid, r) != nil {
ctx.IndexContext.Prefixes = nil
}
httpx.WriteHTML(tpl.Context(r, ctx), nil, t, "", w, r)
resolver.dependencies.Handling.WriteHTML(tpl.Context(r, ctx), nil, t, w, r)
},
Resolver: resolvers.InOrder{

View file

@ -4,7 +4,7 @@ import (
"context"
"net/http"
"github.com/tkw1536/pkglib/mux"
"github.com/tkw1536/pkglib/httpx/mux"
)
// Routeable is a component that is servable
@ -51,10 +51,29 @@ type Routes struct {
Decorator func(http.Handler) http.Handler
}
type routeContextTyp int
const routeContextKey routeContextTyp = 0
// RouteContext represents the context passed to a given route
type RouteContext struct {
DefaultDomain bool
}
// WithRouteContext adds the given RouteContext to the context
func WithRouteContext(parent context.Context, value RouteContext) context.Context {
return context.WithValue(parent, routeContextKey, value)
}
// RouteContextOf returns the route context of the given context
func RouteContextOf(context context.Context) RouteContext {
ctx, ok := context.Value(routeContextKey).(RouteContext)
if !ok {
return RouteContext{}
}
return ctx
}
// Predicate returns the predicate corresponding to the given route
func (routes Routes) Predicate(context func(*http.Request) RouteContext) mux.Predicate {
if routes.MatchAllDomains || routes.Internal {

View file

@ -9,6 +9,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/admin/socket"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/handling"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/julienschmidt/httprouter"
"github.com/rs/zerolog"
@ -20,6 +21,7 @@ import (
type Admin struct {
component.Base
dependencies struct {
Handling *handling.Handling
Fetchers []component.DistilleryFetcher
Instances *instances.Instances
@ -178,7 +180,7 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
func (admin *Admin) loginHandler(ctx context.Context) http.Handler {
logger := zerolog.Ctx(ctx)
return httpx.RedirectHandler(func(r *http.Request) (string, int, error) {
return admin.dependencies.Handling.Redirect(func(r *http.Request) (string, int, error) {
// parse the form
if err := r.ParseForm(); err != nil {
logger.Err(err).Msg("failed to parse admin login")

View file

@ -120,7 +120,7 @@ func (admin *Admin) index(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandler(func(r *http.Request) (idx indexContext, err error) {
return tpl.HTMLHandler(admin.dependencies.Handling, func(r *http.Request) (idx indexContext, err error) {
idx.Distillery, idx.Instances, err = admin.Status(r.Context(), false)
return
})
@ -138,7 +138,7 @@ func (admin *Admin) instances(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandler(func(r *http.Request) (idx indexContext, err error) {
return tpl.HTMLHandler(admin.dependencies.Handling, func(r *http.Request) (idx indexContext, err error) {
idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true)
return
})

View file

@ -42,7 +42,7 @@ func (admin *Admin) instance(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ic instanceContext, funcs []templating.FlagFunc, err error) {
return tpl.HTMLHandlerWithFlags(admin.dependencies.Handling, func(r *http.Request) (ic instanceContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
// find the instance itself!

View file

@ -43,7 +43,7 @@ func (admin *Admin) instanceData(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceDataContext, funcs []templating.FlagFunc, err error) {
return tpl.HTMLHandlerWithFlags(admin.dependencies.Handling, func(r *http.Request) (ctx instanceDataContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
// setup the context with just the instance

View file

@ -50,7 +50,7 @@ func (admin *Admin) instanceDrupal(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceDrupalContext, funcs []templating.FlagFunc, err error) {
return tpl.HTMLHandlerWithFlags(admin.dependencies.Handling, func(r *http.Request) (ctx instanceDrupalContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
// setup the context with just the instance

View file

@ -26,7 +26,7 @@ func (admin *Admin) instanceProvision(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandler(func(r *http.Request) (ipc instanceSystemContext, err error) {
return tpl.HTMLHandler(admin.dependencies.Handling, func(r *http.Request) (ipc instanceSystemContext, err error) {
ipc.prepare(false)
ipc.DefaultProfile = manager.DefaultProfile()
ipc.Profiles = collection.MapValues(manager.Profiles(), func(_ string, profile manager.Profile) string { return profile.Description })

View file

@ -40,7 +40,7 @@ func (admin *Admin) instancePurge(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instancePurgeContext, funcs []templating.FlagFunc, err error) {
return tpl.HTMLHandlerWithFlags(admin.dependencies.Handling, func(r *http.Request) (ctx instancePurgeContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
// setup the context with just the instance

View file

@ -65,7 +65,7 @@ func (admin *Admin) instanceRebuild(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (isc instanceSystemContext, funcs []templating.FlagFunc, err error) {
return tpl.HTMLHandlerWithFlags(admin.dependencies.Handling, func(r *http.Request) (isc instanceSystemContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
var instance *wisski.WissKI

View file

@ -42,7 +42,7 @@ func (admin *Admin) instanceSnapshots(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceSnapshotsContext, funcs []templating.FlagFunc, err error) {
return tpl.HTMLHandlerWithFlags(admin.dependencies.Handling, func(r *http.Request) (ctx instanceSnapshotsContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
// setup the context with just the instance

View file

@ -47,7 +47,7 @@ func (admin *Admin) instanceSSH(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceSSHContext, funcs []templating.FlagFunc, err error) {
return tpl.HTMLHandlerWithFlags(admin.dependencies.Handling, func(r *http.Request) (ctx instanceSSHContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
// setup the context with just the instance

View file

@ -42,7 +42,7 @@ func (admin *Admin) instanceStats(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceStatsContext, funcs []templating.FlagFunc, err error) {
return tpl.HTMLHandlerWithFlags(admin.dependencies.Handling, func(r *http.Request) (ctx instanceStatsContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
// setup the context with just the instance

View file

@ -15,7 +15,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/field"
"github.com/tkw1536/pkglib/httpx/form/field"
"github.com/julienschmidt/httprouter"
"golang.org/x/exp/maps"
@ -55,7 +55,7 @@ func (admin *Admin) instanceUsers(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (instanceUsersContext, []templating.FlagFunc, error) {
return tpl.HTMLHandlerWithFlags(admin.dependencies.Handling, func(r *http.Request) (instanceUsersContext, []templating.FlagFunc, error) {
if r.Method == http.MethodGet {
return admin.getGrantsUsers(r)
} else {

View file

@ -10,8 +10,7 @@ import (
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/gorilla/websocket"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/websocket"
)
// ActionMap handles a set of WebSocket actions
@ -40,7 +39,7 @@ func (err errPanic) Error() string {
// Finally it will send a ResultMessage once handling is complete.
//
// A corresponding client implementation of this can be found in ..../remote/proto.ts
func (am ActionMap) Handle(auth *auth.Auth, conn httpx.WebSocketConnection) (name string, err error) {
func (am ActionMap) Handle(auth *auth.Auth, conn *websocket.Connection) (name string, err error) {
var wg sync.WaitGroup
// once we have finished executing send a binary message (indicating success) to the client.
@ -67,7 +66,7 @@ func (am ActionMap) Handle(auth *auth.Auth, conn httpx.WebSocketConnection) (nam
}
// encode the result message to json!
var message httpx.WebSocketMessage
var message websocket.Message
message.Type = websocket.BinaryMessage
message.Bytes, err = json.Marshal(result)

View file

@ -14,7 +14,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/admin/socket/actions"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/admin/socket/proto"
"github.com/rs/zerolog"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/websocket"
"github.com/tkw1536/pkglib/lazy"
)
@ -48,14 +48,14 @@ func (socket *Sockets) Routes() component.Routes {
}
func (sockets *Sockets) HandleRoute(ctx context.Context, path string) (http.Handler, error) {
return &httpx.WebSocket{
return &websocket.Server{
Context: ctx,
Handler: sockets.Serve,
}, nil
}
// Serve handles a connection to the websocket api
func (socket *Sockets) Serve(conn httpx.WebSocketConnection) {
func (socket *Sockets) Serve(conn *websocket.Connection) {
// handle the websocket connection!
name, err := socket.actions.Get(func() proto.ActionMap { return socket.Actions(conn.Context()) }).Handle(socket.dependencies.Auth, conn)
if err != nil {

View file

@ -11,7 +11,8 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/rs/zerolog"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/field"
"github.com/tkw1536/pkglib/httpx/form"
"github.com/tkw1536/pkglib/httpx/form/field"
_ "embed"
)
@ -43,7 +44,7 @@ func (admin *Admin) users(ctx context.Context) http.Handler {
),
)
return tpl.HTMLHandler(func(r *http.Request) (uc usersContext, err error) {
return tpl.HTMLHandler(admin.dependencies.Handling, func(r *http.Request) (uc usersContext, err error) {
uc.Error = r.URL.Query().Get("error")
uc.Users, err = admin.dependencies.Auth.Users(r.Context())
return
@ -53,7 +54,7 @@ func (admin *Admin) users(ctx context.Context) http.Handler {
//go:embed "html/user_create.html"
var userCreateHTML []byte
var userCreateTemplate = templating.ParseForm(
"user_create.html", userCreateHTML, httpx.FormTemplate,
"user_create.html", userCreateHTML, form.FormTemplate,
templating.Title("Create User"),
templating.Assets(assets.AssetsAdmin),
@ -80,16 +81,16 @@ func (admin *Admin) createUser(ctx context.Context) http.Handler {
),
)
return &httpx.Form[createUserResult]{
return &form.Form[createUserResult]{
Fields: []field.Field{
{Name: "username", Type: field.Text, Autocomplete: field.Username, Label: "Username"},
{Name: "password", Type: field.Password, Autocomplete: field.NewPassword, Label: "Password"},
{Name: "admin", Type: field.Checkbox, Label: "Distillery Administrator"},
},
FieldTemplate: field.PureCSSFieldTemplate,
FieldTemplate: assets.PureCSSFieldTemplate,
RenderTemplate: tpl.Template(),
RenderTemplateContext: templating.FormTemplateContext(tpl),
Template: tpl.Template(),
TemplateContext: templating.FormTemplateContext(tpl),
Validate: func(r *http.Request, values map[string]string) (cu createUserResult, err error) {
cu.User, cu.Passsword, cu.Admin = values["username"], values["password"], values["admin"] == field.CheckboxChecked
@ -110,7 +111,7 @@ func (admin *Admin) createUser(ctx context.Context) http.Handler {
return cu, nil
},
RenderSuccess: func(cu createUserResult, values map[string]string, w http.ResponseWriter, r *http.Request) error {
Success: func(cu createUserResult, values map[string]string, w http.ResponseWriter, r *http.Request) error {
// create the user
user, err := admin.dependencies.Auth.CreateUser(r.Context(), cu.User)
if err != nil {

View file

@ -21,4 +21,14 @@ type Assets struct {
Styles template.HTML // <link> tags inserted by the asset
}
var PureCSSFieldTemplate = template.Must(template.New("").Parse(`
<div class="pure-control-group">
<label for="{{.Name}}">{{.Label}}</label>
{{ if (eq .Type "textarea" )}}
<textarea name="{{.Name}}" id="{{.Name}}" placeholder="{{.Placeholder}}"{{if .Autocomplete }} autocomplete="{{.Autocomplete}}" {{end}}>{{.Value}}</textarea>
{{ else }}
<input type="{{.Type}}" value="{{.Value}}" name="{{.Name}}" id="{{.Name}}" placeholder="{{.Placeholder}}"{{if .Autocomplete }} autocomplete="{{.Autocomplete}}" {{end}}>
{{ end }}
</div>`))
//go:generate node build.mjs Default User Admin AdminProvision AdminRebuild

View file

@ -0,0 +1,60 @@
package handling
import (
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/rs/zerolog"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/content"
"github.com/tkw1536/pkglib/lazy"
)
type Handling struct {
component.Base
text lazy.Lazy[httpx.ErrInterceptor]
html lazy.Lazy[httpx.ErrInterceptor]
}
func (h *Handling) TextInterceptor() httpx.ErrInterceptor {
return h.text.Get(func() httpx.ErrInterceptor {
return h.interceptor(httpx.TextInterceptor)
})
}
func (h *Handling) HTMLInterceptor() httpx.ErrInterceptor {
return h.html.Get(func() httpx.ErrInterceptor {
return h.interceptor(httpx.TextInterceptor)
})
}
// Interceptor returns a copy of the parent interceptor with global distillery interceptor options enabled.
func (h *Handling) interceptor(parent httpx.ErrInterceptor) httpx.ErrInterceptor {
pf := parent.OnFallback
if pf == nil {
pf = func(r *http.Request, err error) {}
}
parent.RenderError = h.Config.HTTP.Debug.Set && h.Config.HTTP.Debug.Value
parent.OnFallback = func(r *http.Request, err error) {
pf(r, err)
zerolog.Ctx(r.Context()).
Err(err).
Str("path", r.URL.Path).
Msg("unknown error")
}
return parent
}
func (h *Handling) Redirect(Handler content.RedirectFunc) http.Handler {
r := content.Redirect(Handler)
r.Interceptor = h.TextInterceptor()
return r
}
func (h *Handling) WriteHTML(context any, err error, template *template.Template, w http.ResponseWriter, r *http.Request) error {
return content.WriteHTMLI(context, err, template, h.HTMLInterceptor(), w, r)
}

View file

@ -6,6 +6,7 @@ import (
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/handling"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/list"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
)
@ -15,6 +16,7 @@ type Home struct {
dependencies struct {
ListInstances *list.ListInstances
Templating *templating.Templating
Handling *handling.Handling
}
}

View file

@ -61,7 +61,7 @@ func (home *Home) publicHandler(ctx context.Context) http.Handler {
about := home.dependencies.Templating.GetCustomizable(aboutTemplate)
return tpl.HTMLHandler(func(r *http.Request) (pc publicContext, err error) {
return tpl.HTMLHandler(home.dependencies.Handling, func(r *http.Request) (pc publicContext, err error) {
// only act on the root path!
if strings.TrimSuffix(r.URL.Path, "/") != "" {
return pc, httpx.ErrNotFound

View file

@ -8,6 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/handling"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
_ "embed"
@ -18,6 +19,7 @@ type Legal struct {
dependencies struct {
Static *assets.Static
Templating *templating.Templating
Handling *handling.Handling
}
}
@ -65,7 +67,7 @@ func (legal *Legal) HandleRoute(ctx context.Context, route string) (http.Handler
),
)
return tpl.HTMLHandler(func(r *http.Request) (lc legalContext, err error) {
return tpl.HTMLHandler(legal.dependencies.Handling, func(r *http.Request) (lc legalContext, err error) {
lc.LegalNotices = cli.LegalNotices
lc.CSRFCookie = server.CSRFCookie

View file

@ -11,6 +11,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/handling"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/rs/zerolog"
"github.com/yuin/goldmark"
@ -23,6 +24,7 @@ type News struct {
component.Base
dependencies struct {
Templating *templating.Templating
Handling *handling.Handling
}
}
@ -143,7 +145,7 @@ func (news *News) HandleRoute(ctx context.Context, path string) (http.Handler, e
zerolog.Ctx(ctx).Err(itemsErr).Msg("Unable to load news items")
}
return tpl.HTMLHandler(func(r *http.Request) (nc newsContext, err error) {
return tpl.HTMLHandler(news.dependencies.Handling, func(r *http.Request) (nc newsContext, err error) {
nc.Items, err = items, itemsErr
return
}), nil

View file

@ -2,18 +2,17 @@ package server
import (
"context"
"fmt"
"io"
"net/http"
"runtime/debug"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/handling"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/tkw1536/pkglib/contextx"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/timewrap"
"github.com/tkw1536/pkglib/mux"
"github.com/tkw1536/pkglib/httpx/mux"
"github.com/tkw1536/pkglib/httpx/wrap"
"github.com/tkw1536/pkglib/recovery"
"github.com/gorilla/csrf"
"github.com/rs/zerolog"
@ -27,6 +26,7 @@ type Server struct {
Cronables []component.Cronable
Templating *templating.Templating
Handleing *handling.Handling
}
}
@ -39,30 +39,33 @@ var (
//
// Logging messages are directed to progress
func (server *Server) Server(ctx context.Context, progress io.Writer) (public http.Handler, internal http.Handler, err error) {
logger := zerolog.Ctx(ctx)
interceptor := server.dependencies.Handleing.TextInterceptor()
var publicM, internalM mux.Mux[component.RouteContext]
publicM.Context = func(r *http.Request) component.RouteContext {
slug, ok := server.Config.HTTP.NormSlugFromHost(r.Host)
return component.RouteContext{
DefaultDomain: slug == "" && ok,
}
}
publicM.Panic = func(p any, stack []byte, w http.ResponseWriter, r *http.Request) {
// log the panic
logger.Error().
Str("panic", fmt.Sprint(p)).
Str("stack", string(stack)).
Str("path", r.URL.Path).
Msg("panic serving handler")
// wrapHandler wraps individual handlers for errors
wrapHandler := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// handle any panic()s that occur
defer func() {
// intercept any panic() that wasn't caught
if err := recovery.Recover(recover()); err != nil {
interceptor.Intercept(w, r, err)
}
}()
// and send an internal server error
httpx.TextInterceptor.Fallback.ServeHTTP(w, r)
// determine if we are on a slug from a host
slug, ok := server.Config.HTTP.NormSlugFromHost(r.Host)
rctx := component.WithRouteContext(r.Context(), component.RouteContext{
DefaultDomain: slug == "" && ok,
})
ctx := contextx.WithValuesOf(rctx, ctx)
// serve with the next context
h.ServeHTTP(w, r.WithContext(ctx))
})
}
// setup the internal server identically
internalM.Panic = publicM.Panic
internalM.Context = publicM.Context
var publicM, internalM mux.Mux
// create a csrf protector
csrfProtector := server.csrf()
@ -95,7 +98,7 @@ func (server *Server) Server(ctx context.Context, progress io.Writer) (public ht
handler = routes.Decorate(handler, csrfProtector)
// determine the predicate
predicate := routes.Predicate(publicM.ContextOf)
predicate := routes.Predicate(func(r *http.Request) component.RouteContext { return component.RouteContextOf(r.Context()) })
// and add all the prefixes
for _, prefix := range append([]string{routes.Prefix}, routes.Aliases...) {
@ -107,15 +110,15 @@ func (server *Server) Server(ctx context.Context, progress io.Writer) (public ht
}
}
// apply the given context function
public = httpx.WithContextWrapper(&publicM, func(rcontext context.Context) context.Context { return contextx.WithValuesOf(rcontext, ctx) })
internal = httpx.WithContextWrapper(&internalM, func(rcontext context.Context) context.Context { return contextx.WithValuesOf(rcontext, ctx) })
// wrap the handlers
public = wrapHandler(&publicM)
internal = wrapHandler(&internalM)
// Add Content-Security-Policy
public = WithCSP(public, models.ContentSecurityPolicyDistilery)
internal = WithCSP(internal, models.ContentSecurityPolicyNothing)
public = timewrap.Wrap(public)
public = wrap.Time(public)
err = nil
return
@ -141,10 +144,3 @@ func WithCSP(handler http.Handler, policy string) http.Handler {
handler.ServeHTTP(w, r)
})
}
func init() {
httpx.InterceptorOnFallback = func(req *http.Request, err error) {
stack := debug.Stack()
zerolog.Ctx(req.Context()).Err(err).Str("stack", string(stack)).Msg("unknown error intercepted")
}
}

View file

@ -11,10 +11,12 @@ import (
"strings"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/handling"
"github.com/gorilla/csrf"
"github.com/rs/zerolog"
"github.com/tkw1536/pkglib/httpx"
"github.com/tkw1536/pkglib/httpx/timewrap"
"github.com/tkw1536/pkglib/httpx/content"
"github.com/tkw1536/pkglib/httpx/form"
"github.com/tkw1536/pkglib/httpx/wrap"
)
//go:embed "src/base.html"
@ -46,7 +48,7 @@ func (tpl *Template[C]) context(r *http.Request, funcs ...FlagFunc) (ctx *tConte
// setup the basic properties
ctx.ctx = r.Context()
ctx.Runtime.RequestURI = r.URL.RequestURI()
ctx.Runtime.StartedAt = timewrap.Start(r).UTC()
ctx.Runtime.StartedAt = wrap.TimeStart(r).UTC()
ctx.Runtime.GeneratedAt = time.Now().UTC()
ctx.Runtime.CSRF = csrf.TemplateField(r)
ctx.Runtime.Menu = tpl.templating.buildMenu(r)
@ -70,19 +72,19 @@ func (tpl *Template[C]) context(r *http.Request, funcs ...FlagFunc) (ctx *tConte
var ParseForm = Parse[FormContext]
type FormContext struct {
httpx.FormContext
form.FormContext
RuntimeFlags
}
// NewFormContext returns a new FormContext from an underlying context
func NewFormContext(context httpx.FormContext) FormContext {
func NewFormContext(context form.FormContext) FormContext {
return FormContext{FormContext: context}
}
// FormTemplateContext returns a new handler for a form with the given base context
func FormTemplateContext(tw *Template[FormContext]) func(ctx httpx.FormContext, r *http.Request) any {
func FormTemplateContext(tw *Template[FormContext]) func(ctx form.FormContext, r *http.Request) any {
// TODO: Is this needed?
return func(ctx httpx.FormContext, r *http.Request) any {
return func(ctx form.FormContext, r *http.Request) any {
return tw.Context(r, FormContext{FormContext: ctx})
}
}
@ -113,19 +115,21 @@ func (tw *Template[C]) HandlerWithFlags(worker func(r *http.Request) (C, []FlagF
// HTMLHandler creates a new httpx.HTMLHandler that calls tw.Handler(worker) and tw.Template.
// See also Handler.
func (tw *Template[C]) HTMLHandler(worker func(r *http.Request) (C, error)) httpx.HTMLHandler[any] {
return httpx.HTMLHandler[any]{
Handler: tw.Handler(worker),
Template: tw.Template(),
func (tw *Template[C]) HTMLHandler(handling *handling.Handling, worker func(r *http.Request) (C, error)) content.HTMLHandler[any] {
return content.HTMLHandler[any]{
Handler: tw.Handler(worker),
Template: tw.Template(),
Interceptor: handling.HTMLInterceptor(),
}
}
// HTMLHandlerWithFlags creates a new httpx.HTMLHandler that calls tw.HandlerWithFlags(worker) and tw.Template.
// See also HandlerWithFlags.
func (tw *Template[C]) HTMLHandlerWithFlags(worker func(r *http.Request) (C, []FlagFunc, error)) httpx.HTMLHandler[any] {
return httpx.HTMLHandler[any]{
Handler: tw.HandlerWithFlags(worker),
Template: tw.Template(),
func (tw *Template[C]) HTMLHandlerWithFlags(handling *handling.Handling, worker func(r *http.Request) (C, []FlagFunc, error)) content.HTMLHandler[any] {
return content.HTMLHandler[any]{
Handler: tw.HandlerWithFlags(worker),
Template: tw.Template(),
Interceptor: handling.HTMLInterceptor(),
}
}

View file

@ -5,7 +5,7 @@ import (
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/tkw1536/pkglib/mux"
"github.com/tkw1536/pkglib/httpx/mux"
"golang.org/x/exp/slices"
)

View file

@ -30,6 +30,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/admin/socket/actions"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/cron"
handleing "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/handling"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/home"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/legal"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/list"
@ -203,6 +204,7 @@ func (dis *Distillery) allComponents(context *lifetime.Registry[component.Compon
// Control server
lifetime.Place[*server.Server](context)
lifetime.Place[*handleing.Handling](context)
lifetime.Place[*home.Home](context)
lifetime.Place[*list.ListInstances](context)