From 19638326dd9b95586815c3479e6b66d538f9dd00 Mon Sep 17 00:00:00 2001 From: maddalax Date: Sun, 20 Oct 2024 07:48:58 -0500 Subject: [PATCH] simple auth example --- examples/simple-auth/.dockerignore | 11 ++ examples/simple-auth/.gitignore | 6 + examples/simple-auth/Dockerfile | 38 ++++++ examples/simple-auth/Taskfile.yml | 20 +++ examples/simple-auth/assets.go | 13 ++ examples/simple-auth/assets/css/input.css | 3 + .../assets/public/apple-touch-icon.png | Bin 0 -> 3429 bytes .../simple-auth/assets/public/favicon.ico | Bin 0 -> 5238 bytes .../assets/public/icon-192-maskable.png | Bin 0 -> 3732 bytes .../simple-auth/assets/public/icon-192.png | Bin 0 -> 7032 bytes .../assets/public/icon-512-maskable.png | Bin 0 -> 14025 bytes .../simple-auth/assets/public/icon-512.png | Bin 0 -> 23888 bytes examples/simple-auth/assets_prod.go | 16 +++ examples/simple-auth/go.mod | 11 ++ examples/simple-auth/go.sum | 18 +++ examples/simple-auth/htmgo-user-example.db | Bin 0 -> 36864 bytes examples/simple-auth/htmgo.yml | 10 ++ examples/simple-auth/internal/db/db.go | 31 +++++ examples/simple-auth/internal/db/models.go | 26 ++++ examples/simple-auth/internal/db/provider.go | 25 ++++ examples/simple-auth/internal/db/queries.sql | 31 +++++ .../simple-auth/internal/db/queries.sql.go | 123 ++++++++++++++++++ examples/simple-auth/internal/db/schema.sql | 28 ++++ examples/simple-auth/internal/embedded/os.go | 17 +++ examples/simple-auth/internal/user/handler.go | 118 +++++++++++++++++ examples/simple-auth/internal/user/http.go | 18 +++ .../simple-auth/internal/user/password.go | 18 +++ examples/simple-auth/internal/user/session.go | 83 ++++++++++++ examples/simple-auth/main.go | 35 +++++ examples/simple-auth/pages/index.go | 74 +++++++++++ examples/simple-auth/pages/login.go | 49 +++++++ examples/simple-auth/pages/logout.go | 23 ++++ examples/simple-auth/pages/register.go | 49 +++++++ examples/simple-auth/pages/root.go | 32 +++++ examples/simple-auth/partials/profile.go | 36 +++++ examples/simple-auth/partials/user.go | 62 +++++++++ examples/simple-auth/sqlc.yaml | 9 ++ examples/simple-auth/tailwind.config.js | 5 + examples/simple-auth/ui/button.go | 41 ++++++ examples/simple-auth/ui/error.go | 17 +++ examples/simple-auth/ui/input.go | 55 ++++++++ examples/simple-auth/ui/login.go | 34 +++++ 42 files changed, 1185 insertions(+) create mode 100644 examples/simple-auth/.dockerignore create mode 100644 examples/simple-auth/.gitignore create mode 100644 examples/simple-auth/Dockerfile create mode 100644 examples/simple-auth/Taskfile.yml create mode 100644 examples/simple-auth/assets.go create mode 100644 examples/simple-auth/assets/css/input.css create mode 100644 examples/simple-auth/assets/public/apple-touch-icon.png create mode 100644 examples/simple-auth/assets/public/favicon.ico create mode 100644 examples/simple-auth/assets/public/icon-192-maskable.png create mode 100644 examples/simple-auth/assets/public/icon-192.png create mode 100644 examples/simple-auth/assets/public/icon-512-maskable.png create mode 100644 examples/simple-auth/assets/public/icon-512.png create mode 100644 examples/simple-auth/assets_prod.go create mode 100644 examples/simple-auth/go.mod create mode 100644 examples/simple-auth/go.sum create mode 100644 examples/simple-auth/htmgo-user-example.db create mode 100644 examples/simple-auth/htmgo.yml create mode 100644 examples/simple-auth/internal/db/db.go create mode 100644 examples/simple-auth/internal/db/models.go create mode 100644 examples/simple-auth/internal/db/provider.go create mode 100644 examples/simple-auth/internal/db/queries.sql create mode 100644 examples/simple-auth/internal/db/queries.sql.go create mode 100644 examples/simple-auth/internal/db/schema.sql create mode 100644 examples/simple-auth/internal/embedded/os.go create mode 100644 examples/simple-auth/internal/user/handler.go create mode 100644 examples/simple-auth/internal/user/http.go create mode 100644 examples/simple-auth/internal/user/password.go create mode 100644 examples/simple-auth/internal/user/session.go create mode 100644 examples/simple-auth/main.go create mode 100644 examples/simple-auth/pages/index.go create mode 100644 examples/simple-auth/pages/login.go create mode 100644 examples/simple-auth/pages/logout.go create mode 100644 examples/simple-auth/pages/register.go create mode 100644 examples/simple-auth/pages/root.go create mode 100644 examples/simple-auth/partials/profile.go create mode 100644 examples/simple-auth/partials/user.go create mode 100644 examples/simple-auth/sqlc.yaml create mode 100644 examples/simple-auth/tailwind.config.js create mode 100644 examples/simple-auth/ui/button.go create mode 100644 examples/simple-auth/ui/error.go create mode 100644 examples/simple-auth/ui/input.go create mode 100644 examples/simple-auth/ui/login.go diff --git a/examples/simple-auth/.dockerignore b/examples/simple-auth/.dockerignore new file mode 100644 index 0000000..fb47686 --- /dev/null +++ b/examples/simple-auth/.dockerignore @@ -0,0 +1,11 @@ +# Project exclude paths +/tmp/ +node_modules/ +dist/ +js/dist +js/node_modules +go.work +go.work.sum +.idea +!framework/assets/dist +__htmgo \ No newline at end of file diff --git a/examples/simple-auth/.gitignore b/examples/simple-auth/.gitignore new file mode 100644 index 0000000..3d6a979 --- /dev/null +++ b/examples/simple-auth/.gitignore @@ -0,0 +1,6 @@ +/assets/dist +tmp +node_modules +.idea +__htmgo +dist \ No newline at end of file diff --git a/examples/simple-auth/Dockerfile b/examples/simple-auth/Dockerfile new file mode 100644 index 0000000..8f3a358 --- /dev/null +++ b/examples/simple-auth/Dockerfile @@ -0,0 +1,38 @@ +# Stage 1: Build the Go binary +FROM golang:1.23-alpine AS builder + +RUN apk update +RUN apk add git +RUN apk add curl + +# Set the working directory inside the container +WORKDIR /app + +# Copy go.mod and go.sum files +COPY go.mod go.sum ./ + +# Download and cache the Go modules +RUN go mod download + +# Copy the source code into the container +COPY . . + +# Build the Go binary for Linux +RUN GOPRIVATE=github.com/maddalax GOPROXY=direct go run github.com/maddalax/htmgo/cli/htmgo@latest build + + +# Stage 2: Create the smallest possible image +FROM gcr.io/distroless/base-debian11 + +# Set the working directory inside the container +WORKDIR /app + +# Copy the Go binary from the builder stage +COPY --from=builder /app/dist . + +# Expose the necessary port (replace with your server port) +EXPOSE 3000 + + +# Command to run the binary +CMD ["./simple-auth"] diff --git a/examples/simple-auth/Taskfile.yml b/examples/simple-auth/Taskfile.yml new file mode 100644 index 0000000..28f1902 --- /dev/null +++ b/examples/simple-auth/Taskfile.yml @@ -0,0 +1,20 @@ +version: '3' + +tasks: + run: + cmds: + - htmgo run + silent: true + + build: + cmds: + - htmgo build + + docker: + cmds: + - docker build . + + watch: + cmds: + - htmgo watch + silent: true diff --git a/examples/simple-auth/assets.go b/examples/simple-auth/assets.go new file mode 100644 index 0000000..9a76f11 --- /dev/null +++ b/examples/simple-auth/assets.go @@ -0,0 +1,13 @@ +//go:build !prod +// +build !prod + +package main + +import ( + "io/fs" + "simpleauth/internal/embedded" +) + +func GetStaticAssets() fs.FS { + return embedded.NewOsFs() +} diff --git a/examples/simple-auth/assets/css/input.css b/examples/simple-auth/assets/css/input.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/examples/simple-auth/assets/css/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/examples/simple-auth/assets/public/apple-touch-icon.png b/examples/simple-auth/assets/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d10e9fe56b9576167656a1270d4ab42bec72d659 GIT binary patch literal 3429 zcmeHK*H_cc7X5`#1VIp#UPMI!siJggL5e_t5ITt9M==NyK}w{GfE3|Fnv_VSNEhiK zB%*+H2)!E=A)$m8di~1uHi7E0F!3@00KlTJrwyZW z^&gy}r^Y+I^f##l@`mYZ0U!JL7paA|vz5M!kr5zH&CdX!s7CDY>ns9#*8O`|jMO~iuFIcM`+$DqYyYbERdYs=} zG@Cn2HOG@*am(sCU8YU4lC`n<5GSkiFgm4DCN(<0;*+7cW;6U2dl#+g)$>=`9yCDe z*SxKFdIVPOFVxtohIP8BWJYOV{o+!MCNMy1CZ{@Ue;$H_nO?k1t=WRfOd7$4& z#`VAe$hoRtbV&OP0A7m5vC5j-@BdEyX(8AxEm+*6uhX!=m@5JE9(-Wtf)W?GE@EnD z$6r%hWBQQdTwMAwAFCTbv>M)?rJNrg>6o1SRxkfb)IdijJL?yJqGdL20SuyT9-2+b@-uyScj;)b8vom%KhA>}tfmHQT^{z4g$k z2pn&Fp&(9x+>CYZ54OR2-7trb)RS|t$JaK(Gt}O{yr;vQ+xqe)<}fW@BBw#_+R(|v z0+K^!*CS$B%kV^ibJ|8*%i>R^Y}U`}$iQR5@*vQ z&^j@LY^&2C66f)(3j|1mHIHz6lO^nOP|&E)2FX&eY>%iSEEMIpl9_bZSSm|bb%n7Y z`3Fop{OsXCj!1XWY^2&x{;rOoAV!~5MLG48W`0*g-snQ3Y=0HBs)Htq z`-qlwr@c3Crk9t#;P#gdFmwC&WWGft>;-v^j)`TV!us?B~V9 zXUwLcQN)SU&E3hRH>bG!E_d(R9j+Eg<7kzWf%2Iq`+&L!s+1Q|OLwnjqqE1jS4CCa zDuhEVm_3cKebz9w=d#bHJz^x;%}0l$r?hQT$c}onn2TbhIJ|Qz;+(s0iPX#YkxqW4 zx50SRb7g(glmqe!KSLA;J0K8`5?(3sX5jC)z>ug`zP-gXrEEVfUTibjn(H8nqB_hS z?6+Fzpp_1?rTcaDHa&jwO+;OIA9qFp*<_1qL?~2*$IM5s(br~yM)u=SYaaj*n13K2 zy9F+u?`q^SFLP{X0!X6X`0>18`^f0Ic?(LBv5Gb`{zotCF^Tg(18 zX6nHn)!(yu3E~mN(rmVb5V}SI^LfSKN)TR9+tzhl_EQM1BeK}ft7F%;|3>C7&Hg4N zyT)@iu%K@tr<%bjiI0*@8QuVYcEC}peid!c_TMh9MP>+u?p$(;f@TdsTN+6f`?p;7 z@Rqq*+)dz$Y^bFmnRaeUF8pzdKzL z^7@`_FoIU&IX9VuDEi?bT>0r8|%N1B$=Y?cL?Qn%X3kE|=ul z(d+9`@j2cMT%Pmrvf0wel$qSnFkzQnV!PaZPS%A@;dHUBSRthC?eb1zi|d3>Vj$W~ z=n0H+)bw}>on=^7RtC-ZG{hxdgsVtg;>*a=CWpmH9Km>IWEV!8#TR$e2zHR#tA4FK zHoPut3hNNU_OI>sBPC1g{CPIlElWvMz#%+g>9v3q6LtVQWpQQuomO9V0VSp(X88vb zzRWukPDA2mywGwKi3??Pn`BKw zjz+`+UDUiQV6JIz>*(-;k)qbBK)1qb_vSHW0@XD>aI47Y!tGn~rd3vSzj;{ivlyPQ zOWZVA+xH!WZ&>;=Tkr09e#t&MNpYu)-c%V{0#!$yfBwWb9h{$emRbJGsSKnvWj$#& zQFJZ#u;NB>@fEMax61V2@q!WI1*6|*P*vBf#4g6}8YE!-E?v$jM`2kBicIAq$ziD$?)9^k02kLY4T}&>8nja#e}QpOd3` z*4(QfhVVZ+UeJu#Dr=|`wYLM7`fPlN%B+dTSy+uPsQG7=`=$Kjet@=LmhZUQwqkEeVZBB1ZepQx>oC%NWF zdmo=0@^$&na^HaJYV3|C9v`Sr*q-{Y?$I=W&1L4|UO1qU$)n&eX$oiEt|!5$e%Bc{#~f{N391h{sB|aM6Cb- literal 0 HcmV?d00001 diff --git a/examples/simple-auth/assets/public/favicon.ico b/examples/simple-auth/assets/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..040cccf58ea179321b86f78b5f0a0817f1ec773a GIT binary patch literal 5238 zcmc&%TTc^F5MGTxz&GO?PsX3&laKxa5?VDrsPP_Skg8Epp+!_s6a+*B?}`GJic-7) zB8Vu0f{KVn1m!020|D?VOqU=FFKnXI9e|XbZK)i!~ZGZTTWi zTSj9-g8rZQtBALpc=W0fo8j(S%O?&OATMu&mKe zdP>qtnj0j7LmFfqw!yZd)Rh>62A?G_s?o4sOTFvGNAbC*3$0Is$VuPwznqMf;^%m` zBKz=0LHWmb;c{Ub?%pm!S4T6d$_kX69w!-R#Sd0pLiv>(lwHh1XL}Qx>Z;K4v6xbM9Jy;a*q_>sNeIh9UtF!AnVWu+_|1-rZ4yRAAQDaYk4Tv{GzEA4G+rkyzxG& z%g-x09iR2t2gPUnrpJEN_)C$Oxf8h=J5Y0{1eMovQJA$`$r=3T7$`o=T`lsd|He6! zcaf7Oe5d*%pJUaRwLtpO?`2~<^a>m*ML<#bnL!)#Fjqw{qI90=JUF4i7m%~ zaW5kKJ?njzwjGY{M41bEt~u13D?gK}8DHP`xybWq{SPrV*XV3>?qu2~_-~v=T}>I? zB}F*x+vdm@!`b3U-po(Fmk0WK0Zb7^o^UvX(zB_yHr2T{i!&y3|2hQy7g2RHAEkMz zXsEr7$%%3Ff9b)A)b(~g=x3^RPMxOpH9o4ulZO?`7T>#kf6Kn5_{@Is|IMqXs1M%4 zub*QW9_$zQv{}p)Z~8n}w$+>-lXKY~j0}Gh_T=3A&Q?rKg;8)K8SG#GmyZjI)T z=H75@bja!Y8J}ZdiyxYtL~r+Ne0bX?*30$9`7i4KE7w27T;Jyp8vCXM| z&C<46{l$i55@WqroXOr9-*#}0H`fO&eVj?(W=`~}HZt>pD{RUhIbQg(KPUXS^&i{= z#k2p4;Pdx}Nq){NZ@T}CcmK^Xi0AhQQkrtwK@C# F{tLVTip2l` literal 0 HcmV?d00001 diff --git a/examples/simple-auth/assets/public/icon-192-maskable.png b/examples/simple-auth/assets/public/icon-192-maskable.png new file mode 100644 index 0000000000000000000000000000000000000000..d4d6efb61bf6c212e3c466edca6256d9a3000e86 GIT binary patch literal 3732 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 U`U+xTzkh@n(+eiG#@FKh0jyU=3;+NC literal 0 HcmV?d00001 diff --git a/examples/simple-auth/assets/public/icon-192.png b/examples/simple-auth/assets/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..f5334355c00b1ff31781f93b97b0efaa0e049d60 GIT binary patch literal 7032 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 x%cimilvfPSdV(Q_^}+tza4QzW{-_?Ogx> literal 0 HcmV?d00001 diff --git a/examples/simple-auth/assets/public/icon-512-maskable.png b/examples/simple-auth/assets/public/icon-512-maskable.png new file mode 100644 index 0000000000000000000000000000000000000000..db61f3dbc056f541e9b215d4be7ec22a8f436e38 GIT binary patch literal 14025 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 KG(KIojr>11q;Vnu literal 0 HcmV?d00001 diff --git a/examples/simple-auth/assets/public/icon-512.png b/examples/simple-auth/assets/public/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..ba0665d887cf19d3a45122767f0d6609ccb1afbb GIT binary patch literal 23888 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 B&S(Gt literal 0 HcmV?d00001 diff --git a/examples/simple-auth/assets_prod.go b/examples/simple-auth/assets_prod.go new file mode 100644 index 0000000..f0598e1 --- /dev/null +++ b/examples/simple-auth/assets_prod.go @@ -0,0 +1,16 @@ +//go:build prod +// +build prod + +package main + +import ( + "embed" + "io/fs" +) + +//go:embed assets/dist/* +var staticAssets embed.FS + +func GetStaticAssets() fs.FS { + return staticAssets +} diff --git a/examples/simple-auth/go.mod b/examples/simple-auth/go.mod new file mode 100644 index 0000000..8e15c29 --- /dev/null +++ b/examples/simple-auth/go.mod @@ -0,0 +1,11 @@ +module simpleauth + +go 1.23.0 + +require github.com/maddalax/htmgo/framework v0.0.0-20241018222959-a7110576d234 + +require ( + github.com/go-chi/chi/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + golang.org/x/crypto v0.28.0 // indirect +) diff --git a/examples/simple-auth/go.sum b/examples/simple-auth/go.sum new file mode 100644 index 0000000..b173b66 --- /dev/null +++ b/examples/simple-auth/go.sum @@ -0,0 +1,18 @@ +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/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +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/maddalax/htmgo/framework v0.0.0-20241018222959-a7110576d234 h1:1WfY9h8EoZXwzM8hmfCXolZVKr4/p1dgLoW9rKQ5Lso= +github.com/maddalax/htmgo/framework v0.0.0-20241018222959-a7110576d234/go.mod h1:NGGzWVXWksrQJ9kV9SGa/A1F1Bjsgc08cN7ZVb98RqY= +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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +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/examples/simple-auth/htmgo-user-example.db b/examples/simple-auth/htmgo-user-example.db new file mode 100644 index 0000000000000000000000000000000000000000..f572cf5c2006e7992f372b29b42617548405ed2d GIT binary patch literal 36864 zcmeI(U2oe|7yw{9X+Bn#7$mA*6-?RGMq5+c$6pB&BW;{5U7GYG+1d)JI<}8XNAFo^1aZX=K;mXUfs0)t?Jt1DA3)r}&Bg^F#1+SO-L$0>h;p&A*HTjZ^!U8zJnu;) zpV;fGm8MI0r`u~|mq*O&49hYf@I1pX)AV(TzQQd=cgDg4`j1&t2xcojXfDHsd00ck)1V8`;KmY_l;G6=x$;9N1DRvlZc5HIc zv=6rN!2M{uPx}34x6|Jq^hs~KX@|#`7T1hI-QdgBqOrlhusUC>^5H4`>|q<3JBj#Y zW{Mpgg*vQqto`I)+8@<9+32V|Q;ySiCs^yKT|Xdc<7Ug-OZ=tH(f_?Ry(50sub%nz z=2&8KYKr{~yVz`za17z!*kM!k!a~J38sYftI8V2x?Y;Y3xmq`F8f*N@T6wv!w#nZ% zHu*xMUMp8=1ItFWe%(LA??-T2ct*8W=c|oMC8#hOWYBKi*rE55oWPIK?u4Qu=!>dfn(vt*(DO znqOi_8()dWC$C*&Hv{#N@;T``O!m zo1V7!ikvvxEBV(6^e4@+)SQ`@d$`}f-|cy`!_P}tDNt?`7t;h``roB>-!aJA%%e|c z{#9NF_q=@ngK03HnTf+0VAg z6$xJvg)7a2yT-zv(On`BN~Q8X+_v-%0jPsdtqV4sTT(856Q}o znte2FHhbRM(Y8P2X=zX3oAN_m|e2 zZuxGpI$zry%s*_Eg~EO(dpwAg7uCFg&M4X+8^uMZz1y!ZXO7& z-CCpH+n6s4J8p4DFRf-5oQ;Q#O<{hJ-KyVTesCf#9j}^K^y6_4i|9<9qv`R-`?u*C z-iTac-leI1+v7Ri%SwB3Nn6VnUAJ(1Yqhu2*jcD%2OC@WOVZY@_5J>ixr6GhTPvejnnataZefUd+r5)3;` zp&JoRl{A4M0ZT;07!%7zM9fL1j1&`Til&;jCCjE_sg|tEsxI5QqnWbeXp)R^P7H4@ z_Z|SCH{$DgdUE(ofLIr_plD7|@`Av<$?OMFiJ2p|BC9!MDVAg^vZk1-AP_|rHA|FD zEvH*ZQxTCRY#~8)Bu6r_Bj`4*gH-D%L>N^V-G(1V74#sgn+(UQiYn1;NtBhGX=-%b zmS}5&hGkPEnobXpY~@s9+mfs(B0>T^k{m~}C5?z`?o?EI>${>Ib;OVA7ac}Tgi&o9 z*}5&MmSS5HF&z`-97RW}DS5aO5;ctE9CmE%V8zBnMY@tBIbzC!jgd1Nm_JtUhWpW@ z4q;RQ9Y&3ZQHdbxj)@gPrulS`Oz{*^as=v2?M=;+HCwYV9g~8wZd)cHSd)>Jqhq&C zWt7Aqs+>os$LbXYQN8>BqgW)L|9>TC4h$LuKmY_l00ck)1V8`;KmY_@MgjQ!|Exy~ z{=?t@queuw`-A(1d&WKIp1+J!0Qv+1AOHd&00JNY0w4eaAOHd&00JQJHv+LJ%SK|{ QuS`7R? datetime('now'); + +-- name: GetUserByID :one +SELECT * +FROM user +WHERE id = ?; + + +-- name: GetUserByEmail :one +SELECT * +FROM user +WHERE email = ?; + +-- name: UpdateUserMetadata :exec +UPDATE user SET metadata = json_patch(COALESCE(metadata, '{}'), ?) WHERE id = ?; diff --git a/examples/simple-auth/internal/db/queries.sql.go b/examples/simple-auth/internal/db/queries.sql.go new file mode 100644 index 0000000..eee80ac --- /dev/null +++ b/examples/simple-auth/internal/db/queries.sql.go @@ -0,0 +1,123 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: queries.sql + +package db + +import ( + "context" +) + +const createSession = `-- name: CreateSession :exec +INSERT INTO sessions (user_id, session_id, expires_at) +VALUES (?, ?, ?) +` + +type CreateSessionParams struct { + UserID int64 + SessionID string + ExpiresAt string +} + +func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) error { + _, err := q.db.ExecContext(ctx, createSession, arg.UserID, arg.SessionID, arg.ExpiresAt) + return err +} + +const createUser = `-- name: CreateUser :one + +INSERT INTO user (email, password, metadata) +VALUES (?, ?, ?) +RETURNING id +` + +type CreateUserParams struct { + Email string + Password string + Metadata interface{} +} + +// Queries for User Management +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (int64, error) { + row := q.db.QueryRowContext(ctx, createUser, arg.Email, arg.Password, arg.Metadata) + var id int64 + err := row.Scan(&id) + return id, err +} + +const getUserByEmail = `-- name: GetUserByEmail :one +SELECT id, email, password, metadata, created_at, updated_at +FROM user +WHERE email = ? +` + +func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByEmail, email) + var i User + err := row.Scan( + &i.ID, + &i.Email, + &i.Password, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getUserByID = `-- name: GetUserByID :one +SELECT id, email, password, metadata, created_at, updated_at +FROM user +WHERE id = ? +` + +func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByID, id) + var i User + err := row.Scan( + &i.ID, + &i.Email, + &i.Password, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getUserByToken = `-- name: GetUserByToken :one +SELECT u.id, u.email, u.password, u.metadata, u.created_at, u.updated_at +FROM user u + JOIN sessions t ON u.id = t.user_id +WHERE t.session_id = ? + AND t.expires_at > datetime('now') +` + +func (q *Queries) GetUserByToken(ctx context.Context, sessionID string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByToken, sessionID) + var i User + err := row.Scan( + &i.ID, + &i.Email, + &i.Password, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateUserMetadata = `-- name: UpdateUserMetadata :exec +UPDATE user SET metadata = json_patch(COALESCE(metadata, '{}'), ?) WHERE id = ? +` + +type UpdateUserMetadataParams struct { + JsonPatch interface{} + ID int64 +} + +func (q *Queries) UpdateUserMetadata(ctx context.Context, arg UpdateUserMetadataParams) error { + _, err := q.db.ExecContext(ctx, updateUserMetadata, arg.JsonPatch, arg.ID) + return err +} diff --git a/examples/simple-auth/internal/db/schema.sql b/examples/simple-auth/internal/db/schema.sql new file mode 100644 index 0000000..e7b53a9 --- /dev/null +++ b/examples/simple-auth/internal/db/schema.sql @@ -0,0 +1,28 @@ +-- SQLite schema for User Management + +-- User table +CREATE TABLE IF NOT EXISTS user +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + metadata JSON DEFAULT '{}', + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) +); + +-- Auth Token table +CREATE TABLE IF NOT EXISTS sessions +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + session_id TEXT NOT NULL UNIQUE, + created_at TEXT DEFAULT (datetime('now')), + expires_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE +); + +-- Indexes to improve query performance +CREATE INDEX IF NOT EXISTS idx_user_email ON user (email); +CREATE INDEX IF NOT EXISTS idx_session_id ON sessions (session_id); +CREATE INDEX IF NOT EXISTS idx_auth_sessions_user_id ON sessions (user_id); diff --git a/examples/simple-auth/internal/embedded/os.go b/examples/simple-auth/internal/embedded/os.go new file mode 100644 index 0000000..ddfd55f --- /dev/null +++ b/examples/simple-auth/internal/embedded/os.go @@ -0,0 +1,17 @@ +package embedded + +import ( + "io/fs" + "os" +) + +type OsFs struct { +} + +func (receiver OsFs) Open(name string) (fs.File, error) { + return os.Open(name) +} + +func NewOsFs() OsFs { + return OsFs{} +} diff --git a/examples/simple-auth/internal/user/handler.go b/examples/simple-auth/internal/user/handler.go new file mode 100644 index 0000000..ad60648 --- /dev/null +++ b/examples/simple-auth/internal/user/handler.go @@ -0,0 +1,118 @@ +package user + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/maddalax/htmgo/framework/h" + "github.com/maddalax/htmgo/framework/service" + "simpleauth/internal/db" +) + +type CreateUserRequest struct { + Email string + Password string +} + +type LoginUserRequest struct { + Email string + Password string +} + +type CreatedUser struct { + Id string + Email string +} + +func Create(ctx *h.RequestContext, request CreateUserRequest) (int64, error) { + + fmt.Printf("%+v\n", request) + + if len(request.Password) < 6 { + return 0, errors.New("password must be at least 6 characters long") + } + + queries := service.Get[db.Queries](ctx.ServiceLocator()) + + hashedPassword, err := HashPassword(request.Password) + + if err != nil { + return 0, errors.New("something went wrong") + } + + id, err := queries.CreateUser(context.Background(), db.CreateUserParams{ + Email: request.Email, + Password: hashedPassword, + }) + + if err != nil { + + if err.Error() == "UNIQUE constraint failed: user.email" { + return 0, errors.New("email already exists") + } + + return 0, err + } + + return id, nil +} + +func Login(ctx *h.RequestContext, request LoginUserRequest) (int64, error) { + + queries := service.Get[db.Queries](ctx.ServiceLocator()) + + user, err := queries.GetUserByEmail(context.Background(), request.Email) + + if err != nil { + fmt.Printf("error: %s\n", err.Error()) + return 0, errors.New("email or password is incorrect") + } + + if !PasswordMatches(request.Password, user.Password) { + return 0, errors.New("email or password is incorrect") + } + + session, err := CreateSession(ctx, user.ID) + + if err != nil { + return 0, errors.New("something went wrong") + } + + WriteSessionCookie(ctx, session) + + return user.ID, nil +} + +func ParseMeta(meta any) map[string]interface{} { + if meta == nil { + return map[string]interface{}{} + } + if m, ok := meta.(string); ok { + var dest map[string]interface{} + json.Unmarshal([]byte(m), &dest) + return dest + } + return meta.(map[string]interface{}) +} + +func GetMetaKey(meta map[string]interface{}, key string) string { + if val, ok := meta[key]; ok { + return val.(string) + } + return "" +} + +func SetMeta(ctx *h.RequestContext, userId int64, meta map[string]interface{}) error { + queries := service.Get[db.Queries](ctx.ServiceLocator()) + serialized, _ := json.Marshal(meta) + fmt.Printf("serialized: %s\n", string(serialized)) + err := queries.UpdateUserMetadata(context.Background(), db.UpdateUserMetadataParams{ + JsonPatch: serialized, + ID: userId, + }) + if err != nil { + return err + } + return nil +} diff --git a/examples/simple-auth/internal/user/http.go b/examples/simple-auth/internal/user/http.go new file mode 100644 index 0000000..d6865d1 --- /dev/null +++ b/examples/simple-auth/internal/user/http.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/maddalax/htmgo/framework/h" + "simpleauth/internal/db" +) + +func GetUserOrRedirect(ctx *h.RequestContext) (db.User, bool) { + user, err := GetUserFromSession(ctx) + + if err != nil { + ctx.Response.Header().Set("Location", "/login") + ctx.Response.WriteHeader(302) + return db.User{}, false + } + + return user, true +} diff --git a/examples/simple-auth/internal/user/password.go b/examples/simple-auth/internal/user/password.go new file mode 100644 index 0000000..fd6a6a7 --- /dev/null +++ b/examples/simple-auth/internal/user/password.go @@ -0,0 +1,18 @@ +package user + +import ( + "golang.org/x/crypto/bcrypt" +) + +func HashPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashedPassword), nil +} + +func PasswordMatches(password string, hashedPassword string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) + return err == nil +} diff --git a/examples/simple-auth/internal/user/session.go b/examples/simple-auth/internal/user/session.go new file mode 100644 index 0000000..f8d54f4 --- /dev/null +++ b/examples/simple-auth/internal/user/session.go @@ -0,0 +1,83 @@ +package user + +import ( + "context" + "crypto/rand" + "encoding/hex" + "github.com/maddalax/htmgo/framework/h" + "github.com/maddalax/htmgo/framework/service" + "net/http" + "simpleauth/internal/db" + "time" +) + +type CreatedSession struct { + Id string + Expiration time.Time + UserId int64 +} + +func CreateSession(ctx *h.RequestContext, userId int64) (CreatedSession, error) { + sessionId, err := GenerateSessionID() + + if err != nil { + return CreatedSession{}, err + } + + // create a session in the database + queries := service.Get[db.Queries](ctx.ServiceLocator()) + + created := CreatedSession{ + Id: sessionId, + Expiration: time.Now().Add(time.Hour * 24), + UserId: userId, + } + + err = queries.CreateSession(context.Background(), db.CreateSessionParams{ + UserID: created.UserId, + SessionID: created.Id, + ExpiresAt: created.Expiration.Format(time.RFC3339), + }) + + if err != nil { + return CreatedSession{}, err + } + + return created, nil +} + +func GetUserFromSession(ctx *h.RequestContext) (db.User, error) { + cookie, err := ctx.Request.Cookie("session_id") + if err != nil { + return db.User{}, err + } + queries := service.Get[db.Queries](ctx.ServiceLocator()) + user, err := queries.GetUserByToken(context.Background(), cookie.Value) + if err != nil { + return db.User{}, err + } + return user, nil +} + +func WriteSessionCookie(ctx *h.RequestContext, session CreatedSession) { + cookie := http.Cookie{ + Name: "session_id", + Value: session.Id, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Expires: session.Expiration, + Path: "/", + } + ctx.Response.Header().Add("Set-Cookie", cookie.String()) +} + +func GenerateSessionID() (string, error) { + // Create a byte slice for storing the random bytes + bytes := make([]byte, 32) // 32 bytes = 256 bits, which is a secure length + // Read random bytes from crypto/rand + if _, err := rand.Read(bytes); err != nil { + return "", err + } + // Encode to hexadecimal to get a string representation + return hex.EncodeToString(bytes), nil +} diff --git a/examples/simple-auth/main.go b/examples/simple-auth/main.go new file mode 100644 index 0000000..ca237d4 --- /dev/null +++ b/examples/simple-auth/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "github.com/maddalax/htmgo/framework/h" + "github.com/maddalax/htmgo/framework/service" + "io/fs" + "net/http" + "simpleauth/__htmgo" + "simpleauth/internal/db" +) + +func main() { + locator := service.NewLocator() + + service.Set(locator, service.Singleton, func() *db.Queries { + return db.Provide() + }) + + h.Start(h.AppOpts{ + ServiceLocator: locator, + LiveReload: true, + Register: func(app *h.App) { + sub, err := fs.Sub(GetStaticAssets(), "assets/dist") + + if err != nil { + panic(err) + } + + http.FileServerFS(sub) + + app.Router.Handle("/public/*", http.StripPrefix("/public", http.FileServerFS(sub))) + __htmgo.Register(app.Router) + }, + }) +} diff --git a/examples/simple-auth/pages/index.go b/examples/simple-auth/pages/index.go new file mode 100644 index 0000000..364f0f3 --- /dev/null +++ b/examples/simple-auth/pages/index.go @@ -0,0 +1,74 @@ +package pages + +import ( + "github.com/maddalax/htmgo/framework/h" + "simpleauth/internal/db" + "simpleauth/internal/user" + "simpleauth/partials" + "simpleauth/ui" +) + +func IndexPage(ctx *h.RequestContext) *h.Page { + u, ok := user.GetUserOrRedirect(ctx) + if !ok { + return nil + } + return h.NewPage( + RootPage(UserProfilePage(u)), + ) +} + +func UserProfilePage(u db.User) *h.Element { + + meta := user.ParseMeta(u.Metadata) + + return h.Div( + h.Class("flex flex-col gap-6 items-center pt-10 min-h-screen bg-neutral-100"), + h.H3F("User Profile", h.Class("text-2xl font-bold")), + h.Pf("Welcome, %s!", u.Email), + h.Form( + h.Attribute("hx-swap", "none"), + h.PostPartial(partials.UpdateProfile), + h.TriggerChildren(), + h.Class("flex flex-col gap-4 w-full max-w-md p-6 bg-white rounded-md shadow-md"), + + ui.Input(ui.InputProps{ + Id: "email", + Name: "email", + Label: "Email Address", + Type: "email", + DefaultValue: u.Email, + Children: []h.Ren{ + h.Disabled(), + }, + }), + + ui.Input(ui.InputProps{ + Name: "birth-date", + Label: "Birth Date", + DefaultValue: user.GetMetaKey(meta, "birthDate"), + Type: "date", + }), + + ui.Input(ui.InputProps{ + Name: "favorite-color", + Label: "Favorite Color", + DefaultValue: user.GetMetaKey(meta, "favoriteColor"), + }), + + ui.Input(ui.InputProps{ + Name: "occupation", + Label: "Occupation", + DefaultValue: user.GetMetaKey(meta, "occupation"), + }), + + ui.FormError(""), + ui.SubmitButton("Save Changes"), + ), + h.A( + h.Text("Log out"), + h.Href("/logout"), + h.Class("text-blue-400"), + ), + ) +} diff --git a/examples/simple-auth/pages/login.go b/examples/simple-auth/pages/login.go new file mode 100644 index 0000000..a9b148d --- /dev/null +++ b/examples/simple-auth/pages/login.go @@ -0,0 +1,49 @@ +package pages + +import ( + "github.com/maddalax/htmgo/framework/h" + "simpleauth/partials" + "simpleauth/ui" +) + +func Login(ctx *h.RequestContext) *h.Page { + return h.NewPage( + RootPage( + ui.CenteredForm(ui.CenteredFormProps{ + Title: "Sign In", + SubmitText: "Sign In", + PostUrl: h.GetPartialPath(partials.LoginUser), + Children: []h.Ren{ + ui.Input(ui.InputProps{ + Id: "username", + Name: "email", + Label: "Email Address", + Type: "email", + Required: true, + Children: []h.Ren{ + h.Attribute("autocomplete", "off"), + h.MaxLength(50), + }, + }), + + ui.Input(ui.InputProps{ + Id: "password", + Name: "password", + Label: "Password", + Type: "password", + Required: true, + Children: []h.Ren{ + h.MinLength(6), + }, + }), + + h.A( + h.Href("/register"), + h.Text("Don't have an account? Register here"), + h.Class("text-blue-500"), + ), + }, + }), + ), + ) +} diff --git a/examples/simple-auth/pages/logout.go b/examples/simple-auth/pages/logout.go new file mode 100644 index 0000000..3655a42 --- /dev/null +++ b/examples/simple-auth/pages/logout.go @@ -0,0 +1,23 @@ +package pages + +import "github.com/maddalax/htmgo/framework/h" + +func LogoutPage(ctx *h.RequestContext) *h.Page { + + // clear the session cookie + ctx.Response.Header().Set( + "Set-Cookie", + "session_id=; Path=/; Max-Age=0", + ) + + ctx.Response.Header().Set( + "Location", + "/login", + ) + + ctx.Response.WriteHeader( + 302, + ) + + return nil +} diff --git a/examples/simple-auth/pages/register.go b/examples/simple-auth/pages/register.go new file mode 100644 index 0000000..476c180 --- /dev/null +++ b/examples/simple-auth/pages/register.go @@ -0,0 +1,49 @@ +package pages + +import ( + "github.com/maddalax/htmgo/framework/h" + "simpleauth/partials" + "simpleauth/ui" +) + +func Register(ctx *h.RequestContext) *h.Page { + return h.NewPage( + RootPage( + ui.CenteredForm(ui.CenteredFormProps{ + PostUrl: h.GetPartialPath(partials.RegisterUser), + Title: "Create an Account", + SubmitText: "Register", + Children: []h.Ren{ + ui.Input(ui.InputProps{ + Id: "username", + Name: "email", + Label: "Email Address", + Type: "email", + Required: true, + Children: []h.Ren{ + h.Attribute("autocomplete", "off"), + h.MaxLength(50), + }, + }), + + ui.Input(ui.InputProps{ + Id: "password", + Name: "password", + Label: "Password", + Type: "password", + Required: true, + Children: []h.Ren{ + h.MinLength(6), + }, + }), + + h.A( + h.Href("/login"), + h.Text("Already have an account? Login here"), + h.Class("text-blue-500"), + ), + }, + }), + ), + ) +} diff --git a/examples/simple-auth/pages/root.go b/examples/simple-auth/pages/root.go new file mode 100644 index 0000000..bacdd61 --- /dev/null +++ b/examples/simple-auth/pages/root.go @@ -0,0 +1,32 @@ +package pages + +import ( + "github.com/maddalax/htmgo/framework/h" +) + +func RootPage(children ...h.Ren) h.Ren { + return h.Html( + h.HxExtensions(h.BaseExtensions()), + h.Head( + h.Meta("viewport", "width=device-width, initial-scale=1"), + h.Link("/public/favicon.ico", "icon"), + h.Link("/public/apple-touch-icon.png", "apple-touch-icon"), + h.Meta("title", "htmgo template"), + h.Meta("charset", "utf-8"), + h.Meta("author", "htmgo"), + h.Meta("description", "this is a template"), + h.Meta("og:title", "htmgo template"), + h.Meta("og:url", "https://htmgo.dev"), + h.Link("canonical", "https://htmgo.dev"), + h.Meta("og:description", "this is a template"), + h.Link("/public/main.css", "stylesheet"), + h.Script("/public/htmgo.js"), + ), + h.Body( + h.Div( + h.Class("flex flex-col gap-2 bg-white h-full"), + h.Fragment(children...), + ), + ), + ) +} diff --git a/examples/simple-auth/partials/profile.go b/examples/simple-auth/partials/profile.go new file mode 100644 index 0000000..8f18d2f --- /dev/null +++ b/examples/simple-auth/partials/profile.go @@ -0,0 +1,36 @@ +package partials + +import ( + "github.com/maddalax/htmgo/framework/h" + "log/slog" + "simpleauth/internal/user" + "simpleauth/ui" +) + +func UpdateProfile(ctx *h.RequestContext) *h.Partial { + if !ctx.IsHttpPost() { + return nil + } + + patch := map[string]any{ + "birthDate": ctx.FormValue("birth-date"), + "favoriteColor": ctx.FormValue("favorite-color"), + "occupation": ctx.FormValue("occupation"), + } + + u, ok := user.GetUserOrRedirect(ctx) + + if !ok { + return nil + } + + err := user.SetMeta(ctx, u.ID, patch) + + if err != nil { + slog.Error("failed to update user profile", slog.String("error", err.Error())) + ctx.Response.WriteHeader(400) + return ui.SwapFormError(ctx, "something went wrong") + } + + return h.RedirectPartial("/") +} diff --git a/examples/simple-auth/partials/user.go b/examples/simple-auth/partials/user.go new file mode 100644 index 0000000..1023e6f --- /dev/null +++ b/examples/simple-auth/partials/user.go @@ -0,0 +1,62 @@ +package partials + +import ( + "github.com/maddalax/htmgo/framework/h" + "simpleauth/internal/user" + "simpleauth/ui" +) + +func RegisterUser(ctx *h.RequestContext) *h.Partial { + if !ctx.IsHttpPost() { + return nil + } + + payload := user.CreateUserRequest{ + Email: ctx.FormValue("email"), + Password: ctx.FormValue("password"), + } + + id, err := user.Create( + ctx, + payload, + ) + + if err != nil { + ctx.Response.WriteHeader(400) + return ui.SwapFormError(ctx, err.Error()) + } + + session, err := user.CreateSession(ctx, id) + + if err != nil { + ctx.Response.WriteHeader(500) + return ui.SwapFormError(ctx, "something went wrong") + } + + user.WriteSessionCookie(ctx, session) + + return h.RedirectPartial("/") +} + +func LoginUser(ctx *h.RequestContext) *h.Partial { + if !ctx.IsHttpPost() { + return nil + } + + payload := user.LoginUserRequest{ + Email: ctx.FormValue("email"), + Password: ctx.FormValue("password"), + } + + _, err := user.Login( + ctx, + payload, + ) + + if err != nil { + ctx.Response.WriteHeader(400) + return ui.SwapFormError(ctx, err.Error()) + } + + return h.RedirectPartial("/") +} diff --git a/examples/simple-auth/sqlc.yaml b/examples/simple-auth/sqlc.yaml new file mode 100644 index 0000000..30c0518 --- /dev/null +++ b/examples/simple-auth/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - schema: "internal/db/schema.sql" + queries: "internal/db/queries.sql" + engine: "sqlite" + gen: + go: + package: "db" + out: "internal/db" diff --git a/examples/simple-auth/tailwind.config.js b/examples/simple-auth/tailwind.config.js new file mode 100644 index 0000000..b18125c --- /dev/null +++ b/examples/simple-auth/tailwind.config.js @@ -0,0 +1,5 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["**/*.go"], + plugins: [], +}; diff --git a/examples/simple-auth/ui/button.go b/examples/simple-auth/ui/button.go new file mode 100644 index 0000000..015a5e0 --- /dev/null +++ b/examples/simple-auth/ui/button.go @@ -0,0 +1,41 @@ +package ui + +import ( + "github.com/maddalax/htmgo/framework/h" + "github.com/maddalax/htmgo/framework/js" +) + +func SubmitButton(submitText string) *h.Element { + buttonClasses := "rounded items-center px-3 py-2 bg-slate-800 text-white w-full text-center" + + return h.Div( + h.HxBeforeRequest( + js.RemoveClassOnChildren(".loading", "hidden"), + js.SetClassOnChildren(".submit", "hidden"), + ), + h.HxAfterRequest( + js.SetClassOnChildren(".loading", "hidden"), + js.RemoveClassOnChildren(".submit", "hidden"), + ), + h.Class("flex gap-2 justify-center"), + h.Button( + h.Class("loading hidden relative text-center", buttonClasses), + spinner(), + h.Disabled(), + h.Text("Submitting..."), + ), + h.Button( + h.Type("submit"), + h.Class("submit", buttonClasses), + h.Text(submitText), + ), + ) +} + +func spinner(children ...h.Ren) *h.Element { + return h.Div( + h.Children(children...), + h.Class("absolute left-1 spinner spinner-border animate-spin inline-block w-6 h-6 border-4 rounded-full border-slate-200 border-t-transparent"), + h.Attribute("role", "status"), + ) +} diff --git a/examples/simple-auth/ui/error.go b/examples/simple-auth/ui/error.go new file mode 100644 index 0000000..a410e13 --- /dev/null +++ b/examples/simple-auth/ui/error.go @@ -0,0 +1,17 @@ +package ui + +import "github.com/maddalax/htmgo/framework/h" + +func FormError(error string) *h.Element { + return h.Div( + h.Id("form-error"), + h.Text(error), + h.If(error != "", h.Class("p-4 bg-rose-400 text-white rounded")), + ) +} + +func SwapFormError(ctx *h.RequestContext, error string) *h.Partial { + return h.SwapPartial(ctx, + FormError(error), + ) +} diff --git a/examples/simple-auth/ui/input.go b/examples/simple-auth/ui/input.go new file mode 100644 index 0000000..f465766 --- /dev/null +++ b/examples/simple-auth/ui/input.go @@ -0,0 +1,55 @@ +package ui + +import ( + "github.com/maddalax/htmgo/framework/h" + "github.com/maddalax/htmgo/framework/hx" +) + +type InputProps struct { + Id string + Label string + Name string + Type string + DefaultValue string + Placeholder string + Required bool + ValidationPath string + Error string + Children []h.Ren +} + +func Input(props InputProps) *h.Element { + validation := h.If(props.ValidationPath != "", h.Children( + h.Post(props.ValidationPath, hx.BlurEvent), + h.Attribute("hx-swap", "innerHTML transition:true"), + h.Attribute("hx-target", "next div"), + )) + + if props.Type == "" { + props.Type = "text" + } + + input := h.Input( + props.Type, + h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"), + h.If(props.Name != "", h.Name(props.Name)), + h.If(props.Children != nil, h.Children(props.Children...)), + h.If(props.Required, h.Required()), + h.If(props.Placeholder != "", h.Placeholder(props.Placeholder)), + h.If(props.DefaultValue != "", h.Attribute("value", props.DefaultValue)), + validation, + ) + + wrapped := h.Div( + h.If(props.Id != "", h.Id(props.Id)), + h.Class("flex flex-col gap-1"), + h.If(props.Label != "", h.Label(h.Text(props.Label))), + input, + h.Div( + h.Id(props.Id+"-error"), + h.Class("text-red-500"), + ), + ) + + return wrapped +} diff --git a/examples/simple-auth/ui/login.go b/examples/simple-auth/ui/login.go new file mode 100644 index 0000000..1c18b7d --- /dev/null +++ b/examples/simple-auth/ui/login.go @@ -0,0 +1,34 @@ +package ui + +import ( + "chat/components" + "github.com/maddalax/htmgo/framework/h" +) + +type CenteredFormProps struct { + Title string + Children []h.Ren + SubmitText string + PostUrl string +} + +func CenteredForm(props CenteredFormProps) *h.Element { + return h.Div( + h.Class("flex flex-col items-center justify-center min-h-screen bg-neutral-100"), + h.Div( + h.Class("bg-white p-8 rounded-lg shadow-lg w-full max-w-md"), + h.H2F(props.Title, h.Class("text-3xl font-bold text-center mb-6")), + h.Form( + h.TriggerChildren(), + h.Post(props.PostUrl), + h.Attribute("hx-swap", "none"), + h.Class("flex flex-col gap-4"), + h.Children(props.Children...), + // Error message + components.FormError(""), + // Submit button at the bottom + SubmitButton(props.SubmitText), + ), + ), + ) +}