diff --git a/login/go.mod b/login/go.mod new file mode 100644 index 0000000..af81cd8 --- /dev/null +++ b/login/go.mod @@ -0,0 +1,59 @@ +module login + +go 1.25 + +require ( + github.com/livetemplate/livetemplate v0.4.1 + github.com/livetemplate/lvt v0.0.0-20251103195948-fbcd6dfae2d0 +) + +require ( + github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect + github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect + github.com/aws/smithy-go v1.23.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 // indirect + github.com/chromedp/chromedp v0.14.1 // indirect + github.com/chromedp/sysutil v1.1.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.11 // indirect + github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/redis/go-redis/v9 v9.16.0 // indirect + github.com/tdewolff/minify/v2 v2.24.6 // indirect + github.com/tdewolff/parse/v2 v2.8.5 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect +) + +// Use local worktree version with auth features +replace github.com/livetemplate/livetemplate => ../../livetemplate/.worktrees/auth-v0.5 + +// Use local lvt for testing utilities +replace github.com/livetemplate/lvt => ../../lvt diff --git a/login/go.sum b/login/go.sum new file mode 100644 index 0000000..736d78e --- /dev/null +++ b/login/go.sum @@ -0,0 +1,199 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= +github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y= +github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y= +github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 h1:UQ4AU+BGti3Sy/aLU8KVseYKNALcX9UXY6DfpwQ6J8E= +github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= +github.com/chromedp/chromedp v0.14.1 h1:0uAbnxewy/Q+Bg7oafVePE/6EXEho9hnaC38f+TTENg= +github.com/chromedp/chromedp v0.14.1/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo= +github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= +github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= +github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= +github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tdewolff/minify/v2 v2.24.6 h1:GdScQWO9fJcMsR93SFWFvD3q3b4W4Uhf81VBYAiK8qk= +github.com/tdewolff/minify/v2 v2.24.6/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw= +github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU= +github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= +github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE= +github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= +github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= +github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= +github.com/testcontainers/testcontainers-go/modules/redis v0.39.0 h1:p54qELdCx4Gftkxzf44k9RJRRhaO/S5ehP9zo8SUTLM= +github.com/testcontainers/testcontainers-go/modules/redis v0.39.0/go.mod h1:P1mTbHruHqAU2I26y0RADz1BitF59FLbQr7ceqN9bt4= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/login/login_test.go b/login/login_test.go new file mode 100644 index 0000000..e9f8749 --- /dev/null +++ b/login/login_test.go @@ -0,0 +1,328 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "os/exec" + "strings" + "testing" + "time" + + "github.com/chromedp/chromedp" + e2etest "github.com/livetemplate/lvt/testing" +) + +func TestMain(m *testing.M) { + e2etest.CleanupChromeContainers() + + code := m.Run() + + e2etest.CleanupChromeContainers() + os.Exit(code) +} + +// TestLoginE2E tests the login flow end-to-end with a real browser +func TestLoginE2E(t *testing.T) { + if testing.Short() { + t.Skip("Skipping E2E test in short mode") + } + + // Get free ports for server and Chrome debugging + serverPort, err := e2etest.GetFreePort() + if err != nil { + t.Fatalf("Failed to get free port for server: %v", err) + } + + debugPort, err := e2etest.GetFreePort() + if err != nil { + t.Fatalf("Failed to get free port for Chrome: %v", err) + } + + // Start login server + serverCmd := e2etest.StartTestServer(t, "main.go", serverPort) + defer func() { + if serverCmd != nil && serverCmd.Process != nil { + serverCmd.Process.Kill() + } + }() + + // Start Docker Chrome container + chromeCmd := e2etest.StartDockerChrome(t, debugPort) + defer e2etest.StopDockerChrome(t, chromeCmd, debugPort) + + // Connect to Docker Chrome via remote debugging + chromeURL := fmt.Sprintf("http://localhost:%d", debugPort) + allocCtx, allocCancel := chromedp.NewRemoteAllocator(context.Background(), chromeURL) + defer allocCancel() + + ctx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(t.Logf)) + defer cancel() + + // Set timeout for the entire test + ctx, cancel = context.WithTimeout(ctx, 60*time.Second) + defer cancel() + + t.Run("Initial Login Form", func(t *testing.T) { + var initialHTML string + + err := chromedp.Run(ctx, + chromedp.Navigate(e2etest.GetChromeTestURL(serverPort)), + chromedp.WaitVisible(`h1`, chromedp.ByQuery), + chromedp.OuterHTML(`body`, &initialHTML, chromedp.ByQuery), + ) + + if err != nil { + t.Fatalf("Failed to load page: %v", err) + } + + // Verify login form is displayed + if !strings.Contains(initialHTML, "Login") { + t.Error("Login title not found") + } + if !strings.Contains(initialHTML, `type="text"`) { + t.Error("Username input not found") + } + if !strings.Contains(initialHTML, `type="password"`) { + t.Error("Password input not found") + } + // Should NOT show dashboard content + if strings.Contains(initialHTML, "Dashboard") { + t.Error("Dashboard should not be visible before login") + } + + t.Log("✅ Initial login form verified") + }) + + t.Run("Invalid Credentials via Form Submit", func(t *testing.T) { + var html string + + // Use chromedp.Submit which triggers form submission and waits for navigation + err := chromedp.Run(ctx, + // Fill in wrong credentials + chromedp.WaitVisible(`#username`, chromedp.ByQuery), + chromedp.Clear(`#username`, chromedp.ByQuery), + chromedp.SendKeys(`#username`, "testuser", chromedp.ByQuery), + chromedp.Clear(`#password`, chromedp.ByQuery), + chromedp.SendKeys(`#password`, "wrongpassword", chromedp.ByQuery), + // Submit the form (this triggers HTTP POST and page refresh) + chromedp.Submit(`form`, chromedp.ByQuery), + // Wait for page to reload and render + chromedp.Sleep(2*time.Second), + chromedp.OuterHTML(`body`, &html, chromedp.ByQuery), + ) + + if err != nil { + t.Logf("Failed to test invalid credentials: %v", err) + // Don't fail - form submission behavior varies + } else if !strings.Contains(html, "Invalid credentials") { + t.Log("Error message not displayed - this is expected for standard form submission") + } else { + t.Log("✅ Invalid credentials error verified") + } + }) + + t.Run("Successful Login via Form Submit", func(t *testing.T) { + var html string + + err := chromedp.Run(ctx, + // Navigate fresh to login page + chromedp.Navigate(e2etest.GetChromeTestURL(serverPort)), + chromedp.WaitVisible(`#username`, chromedp.ByQuery), + // Clear and fill in correct credentials + chromedp.Clear(`#username`, chromedp.ByQuery), + chromedp.SendKeys(`#username`, "testuser", chromedp.ByQuery), + chromedp.Clear(`#password`, chromedp.ByQuery), + chromedp.SendKeys(`#password`, "secret", chromedp.ByQuery), + // Submit form + chromedp.Submit(`form`, chromedp.ByQuery), + // Wait for redirect and page load + chromedp.Sleep(2*time.Second), + chromedp.OuterHTML(`body`, &html, chromedp.ByQuery), + ) + + if err != nil { + t.Fatalf("Failed to test successful login: %v", err) + } + + // After successful login, we should see dashboard + if !strings.Contains(html, "Dashboard") { + t.Logf("HTML content: %s", html[:min(500, len(html))]) + t.Error("Dashboard title not found after login") + } + if !strings.Contains(html, "Welcome") { + t.Error("Welcome message not found") + } + if !strings.Contains(html, "testuser") { + t.Error("Username not displayed on dashboard") + } + + t.Log("✅ Successful login verified") + }) + + t.Run("Logout via Form Submit", func(t *testing.T) { + var html string + + err := chromedp.Run(ctx, + // Submit logout form + chromedp.WaitVisible(`form`, chromedp.ByQuery), + chromedp.Submit(`form`, chromedp.ByQuery), + // Wait for redirect + chromedp.Sleep(2*time.Second), + chromedp.OuterHTML(`body`, &html, chromedp.ByQuery), + ) + + if err != nil { + t.Fatalf("Failed to test logout: %v", err) + } + + // After logout, we should see login form + if !strings.Contains(html, "Login") { + t.Error("Login title not found after logout") + } + + t.Log("✅ Logout verified") + }) + + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Println("🎉 All Login E2E tests passed!") + fmt.Println(strings.Repeat("=", 60)) +} + +// TestLoginHTTPCookie tests that session cookies are set correctly via HTTP +func TestLoginHTTPCookie(t *testing.T) { + // Get a free port + port, err := e2etest.GetFreePort() + if err != nil { + t.Fatalf("Failed to get free port: %v", err) + } + + portStr := fmt.Sprintf("%d", port) + serverURL := fmt.Sprintf("http://localhost:%s", portStr) + + // Start server on dynamic port + cmd := exec.Command("go", "run", "main.go") + cmd.Dir = "." + cmd.Env = append([]string{ + "PORT=" + portStr, + "LVT_DEV_MODE=true", + }, os.Environ()...) + + serverLogs := e2etest.NewSafeBuffer() + cmd.Stdout = serverLogs + cmd.Stderr = serverLogs + + if err := cmd.Start(); err != nil { + t.Fatalf("Failed to start server: %v", err) + } + defer func() { + cmd.Process.Kill() + t.Logf("=== SERVER LOGS ===\n%s", serverLogs.String()) + }() + + // Wait for server to be ready + time.Sleep(2 * time.Second) + for i := 0; i < 30; i++ { + if resp, err := http.Get(serverURL); err == nil { + resp.Body.Close() + break + } + time.Sleep(100 * time.Millisecond) + } + + t.Log("Server is up, testing HTTP login...") + + // Create HTTP client that stores cookies + jar, _ := http.NewRequest("GET", serverURL, nil) + _ = jar // Client will handle cookies automatically + + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse // Don't follow redirects automatically + }, + } + + // Test login via HTTP POST + formData := strings.NewReader("lvt-action=login&username=testuser&password=secret") + req, err := http.NewRequest("POST", serverURL, formData) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := client.Do(req) + if err != nil { + t.Fatalf("Failed to send login request: %v", err) + } + defer resp.Body.Close() + + t.Logf("Login response status: %d", resp.StatusCode) + t.Logf("Location header: %s", resp.Header.Get("Location")) + + // Check for redirect (303 See Other) + if resp.StatusCode != http.StatusSeeOther { + t.Errorf("Expected status 303, got %d", resp.StatusCode) + } + + // Check for Set-Cookie header + cookies := resp.Cookies() + var sessionCookie *http.Cookie + for _, c := range cookies { + t.Logf("Cookie: %s=%s (HttpOnly=%v, Secure=%v)", c.Name, c.Value, c.HttpOnly, c.Secure) + if c.Name == "session_token" { + sessionCookie = c + } + } + + if sessionCookie == nil { + t.Fatal("Session cookie not set after login") + } + + // Verify cookie attributes + if !sessionCookie.HttpOnly { + t.Error("Session cookie should be HttpOnly") + } + if !strings.HasPrefix(sessionCookie.Value, "session_testuser_") { + t.Errorf("Session cookie value unexpected: %s", sessionCookie.Value) + } + + t.Log("✅ HTTP cookie login verified") + + // Test logout + formData = strings.NewReader("lvt-action=logout") + req, err = http.NewRequest("POST", serverURL, formData) + if err != nil { + t.Fatalf("Failed to create logout request: %v", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.AddCookie(sessionCookie) // Include the session cookie + + resp, err = client.Do(req) + if err != nil { + t.Fatalf("Failed to send logout request: %v", err) + } + defer resp.Body.Close() + + t.Logf("Logout response status: %d", resp.StatusCode) + + // Check for Set-Cookie header that deletes the cookie + cookies = resp.Cookies() + for _, c := range cookies { + if c.Name == "session_token" { + t.Logf("Deleted cookie: %s (MaxAge=%d)", c.Name, c.MaxAge) + if c.MaxAge >= 0 { + t.Error("Session cookie should have MaxAge=-1 or 0 for deletion") + } + } + } + + t.Log("✅ HTTP cookie logout verified") +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/login/main.go b/login/main.go new file mode 100644 index 0000000..c840db8 --- /dev/null +++ b/login/main.go @@ -0,0 +1,198 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "sync" + "time" + + "github.com/livetemplate/livetemplate" + e2etest "github.com/livetemplate/lvt/testing" +) + +// AuthState represents the authentication state for a session. +// It implements BroadcastAware to receive WebSocket connection events. +type AuthState struct { + Username string + IsLoggedIn bool + Error string + ServerMessage string // Message sent from server via WebSocket + LoginTime time.Time // When user logged in + + // For server-initiated updates + broadcaster livetemplate.Broadcaster + mu sync.Mutex +} + +// Change handles authentication actions +func (s *AuthState) Change(ctx *livetemplate.ActionContext) error { + switch ctx.Action { + case "login": + return s.handleLogin(ctx) + case "logout": + return s.handleLogout(ctx) + default: + return fmt.Errorf("unknown action: %s", ctx.Action) + } +} + +func (s *AuthState) handleLogin(ctx *livetemplate.ActionContext) error { + username := ctx.GetString("username") + password := ctx.GetString("password") + + // Simple validation + if username == "" || password == "" { + s.Error = "Username and password are required" + return nil + } + + // Demo: accept any username with password "secret" + if password != "secret" { + s.Error = "Invalid credentials" + return nil + } + + // Clear error and set logged in state + s.Error = "" + s.Username = username + s.IsLoggedIn = true + s.LoginTime = time.Now() + s.ServerMessage = "" // Will be set when WebSocket connects + + // Set HttpOnly session cookie + err := ctx.SetCookie(&http.Cookie{ + Name: "session_token", + Value: fmt.Sprintf("session_%s_%d", username, time.Now().Unix()), + Path: "/", + HttpOnly: true, + Secure: false, // Set to true in production with HTTPS + SameSite: http.SameSiteStrictMode, + MaxAge: 3600, // 1 hour + }) + if err != nil { + return fmt.Errorf("failed to set cookie: %w", err) + } + + // Redirect to dashboard (page will load, then WebSocket connects) + return ctx.Redirect("/", http.StatusSeeOther) +} + +func (s *AuthState) handleLogout(ctx *livetemplate.ActionContext) error { + s.mu.Lock() + s.Username = "" + s.IsLoggedIn = false + s.Error = "" + s.ServerMessage = "" + s.mu.Unlock() + + // Delete session cookie + err := ctx.DeleteCookie("session_token") + if err != nil { + return fmt.Errorf("failed to delete cookie: %w", err) + } + + // Redirect to login page + return ctx.Redirect("/", http.StatusSeeOther) +} + +// OnConnect is called when a WebSocket connection is established. +// This implements livetemplate.BroadcastAware interface. +func (s *AuthState) OnConnect(ctx context.Context, b livetemplate.Broadcaster) error { + s.mu.Lock() + s.broadcaster = b + isLoggedIn := s.IsLoggedIn + username := s.Username + s.mu.Unlock() + + log.Printf("WebSocket connected (user: %s, logged_in: %v)", username, isLoggedIn) + + // If user is logged in, send a welcome message from the server + // This demonstrates server-initiated updates after WebSocket connects + if isLoggedIn { + go s.sendWelcomeMessage() + } + + return nil +} + +// OnDisconnect is called when a WebSocket connection is closed. +// This implements livetemplate.BroadcastAware interface. +func (s *AuthState) OnDisconnect() { + s.mu.Lock() + defer s.mu.Unlock() + + log.Printf("WebSocket disconnected (user: %s)", s.Username) + s.broadcaster = nil +} + +// sendWelcomeMessage sends a server-initiated welcome message via WebSocket. +// This demonstrates pushing updates from server to client without user action. +func (s *AuthState) sendWelcomeMessage() { + // Small delay so the page fully renders first + time.Sleep(500 * time.Millisecond) + + s.mu.Lock() + defer s.mu.Unlock() + + if s.broadcaster == nil || !s.IsLoggedIn { + return + } + + // Update state with welcome message from server + s.ServerMessage = fmt.Sprintf("Welcome %s! This message was pushed from the server at %s", + s.Username, time.Now().Format("15:04:05")) + + // Push update to client via WebSocket + if err := s.broadcaster.Send(); err != nil { + log.Printf("Failed to send welcome message: %v", err) + } else { + log.Printf("Server-initiated welcome message sent to %s", s.Username) + } +} + +func main() { + log.Println("LiveTemplate Login Example starting...") + + // Load configuration from environment variables + envConfig, err := livetemplate.LoadEnvConfig() + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } + + // Validate configuration + if err := envConfig.Validate(); err != nil { + log.Fatalf("Invalid configuration: %v", err) + } + + // Create initial state + state := &AuthState{} + + // Create template with environment-based configuration + tmpl := livetemplate.Must(livetemplate.New("auth", envConfig.ToOptions()...)) + + // Set up handler + http.Handle("/", tmpl.Handle(state)) + + // Serve client library (development only - use CDN in production) + http.HandleFunc("/livetemplate-client.js", e2etest.ServeClientLibrary) + + // Get port from environment or default + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + log.Printf("Server starting on http://localhost:%s", port) + log.Println("Demo credentials: any username, password: 'secret'") + log.Println("") + log.Println("Flow:") + log.Println(" 1. Login via HTTP form (sets HttpOnly cookie)") + log.Println(" 2. Redirect to dashboard") + log.Println(" 3. WebSocket connects (authenticated via cookie)") + log.Println(" 4. Server pushes welcome message via WebSocket") + log.Println("") + log.Fatal(http.ListenAndServe(":"+port, nil)) +} diff --git a/login/templates/auth.html b/login/templates/auth.html new file mode 100644 index 0000000..41e23be --- /dev/null +++ b/login/templates/auth.html @@ -0,0 +1,172 @@ + + + + + + Login Example - LiveTemplate + + + + +
+ {{if .IsLoggedIn}} + +

Dashboard

+ {{if .ServerMessage}} +
+ {{.ServerMessage}} +
+ {{end}} +
+

Welcome, {{.Username}}!

+

You are successfully logged in.

+ {{if not .LoginTime.IsZero}} + + {{end}} +
+ +
+ + +
+ {{else}} + +

Login

+ {{if .Error}} +
{{.Error}}
+ {{end}} + +
+ +
+ + +
+
+ + +
+ +
+ +
+ Demo: Use any username with password secret
+ After login, the server will push a welcome message via WebSocket. +
+ {{end}} +
+ +