diff --git a/bootstrap/Makefile b/bootstrap/Makefile
index 0487daa..07c9e6b 100644
--- a/bootstrap/Makefile
+++ b/bootstrap/Makefile
@@ -8,7 +8,7 @@ endif
# re-create _templ.txt files on change, then send reload event to browser.
# Default url: http://localhost:7331
templ:
- @templ generate --watch --proxy="http://localhost$(HTTP_LISTEN_ADDR)" --open-browser=false -v
+ @templ generate --watch --proxy="http://localhost$(HTTP_LISTEN_ADDR)" --open-browser=false
# run air to detect any go file changes to re-build and re-run the server.
server:
diff --git a/bootstrap/go.mod b/bootstrap/go.mod
new file mode 100644
index 0000000..0783e32
--- /dev/null
+++ b/bootstrap/go.mod
@@ -0,0 +1,27 @@
+module AABBCCDD
+
+go 1.22.4
+
+require (
+ github.com/a-h/templ v0.2.707
+ github.com/anthdm/gothkit v0.0.0-20240615140127-d74e729d7c71
+ github.com/go-chi/chi/v5 v5.0.12
+ github.com/joho/godotenv v1.5.1
+ github.com/mattn/go-sqlite3 v1.14.22
+ github.com/uptrace/bun v1.2.1
+ github.com/uptrace/bun/dialect/sqlitedialect v1.2.1
+ github.com/uptrace/bun/extra/bundebug v1.2.1
+)
+
+require (
+ github.com/fatih/color v1.17.0 // indirect
+ github.com/gorilla/securecookie v1.1.2 // indirect
+ github.com/gorilla/sessions v1.3.0 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
+ github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
+ github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+ golang.org/x/sys v0.21.0 // indirect
+)
diff --git a/bootstrap/go.sum b/bootstrap/go.sum
new file mode 100644
index 0000000..3215512
--- /dev/null
+++ b/bootstrap/go.sum
@@ -0,0 +1,57 @@
+github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
+github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
+github.com/anthdm/gothkit v0.0.0-20240615140127-d74e729d7c71 h1:lTQ0cqCoXVCJRdAPjJTUhqHcAdF4EcAxARSgCO4Ny9o=
+github.com/anthdm/gothkit v0.0.0-20240615140127-d74e729d7c71/go.mod h1:SFEB0yWZV7/rEtE+I3u3rJ7HXRWYAami+3gyAH+2d8c=
+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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
+github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
+github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
+github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+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/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+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/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg=
+github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+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=
+github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
+github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
+github.com/uptrace/bun v1.2.1 h1:2ENAcfeCfaY5+2e7z5pXrzFKy3vS8VXvkCag6N2Yzfk=
+github.com/uptrace/bun v1.2.1/go.mod h1:cNg+pWBUMmJ8rHnETgf65CEvn3aIKErrwOD6IA8e+Ec=
+github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 h1:IprvkIKUjEjvt4VKpcmLpbMIucjrsmUPJOSlg19+a0Q=
+github.com/uptrace/bun/dialect/sqlitedialect v1.2.1/go.mod h1:mMQf4NUpgY8bnOanxGmxNiHCdALOggS4cZ3v63a9D/o=
+github.com/uptrace/bun/extra/bundebug v1.2.1 h1:85MYpX3QESYI02YerKxUi1CD9mHuLrc2BXs1eOCtQus=
+github.com/uptrace/bun/extra/bundebug v1.2.1/go.mod h1:sfGKIi0HSGxsTC/sgIHGwpnYduHHYhdMeOIwurgSY+Y=
+github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
+github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+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/bootstrap/plugins/auth/auth.templ b/bootstrap/plugins/auth/auth.templ
new file mode 100644
index 0000000..f3b731d
--- /dev/null
+++ b/bootstrap/plugins/auth/auth.templ
@@ -0,0 +1,85 @@
+package auth
+
+import (
+ "auth/app/views/layouts"
+ v "github.com/anthdm/gothkit/validate"
+ "auth/app/views/components"
+)
+
+type AuthIndexPageData struct {
+ FormValues LoginFormValues
+ FormErrors v.Errors
+}
+
+type LoginFormValues struct {
+ Email string `form:"email"`
+ Password string `form:"password"`
+}
+
+templ AuthIndex(data AuthIndexPageData) {
+ @layouts.BaseLayout() {
+
+ @components.ThemeSwitcher()
+
+
+ }
+}
+
+templ LoginForm(values LoginFormValues, errors v.Errors) {
+
+}
+
+func buttonAttrs() templ.Attributes {
+ class := "inline-flex text-primary-foreground items-center justify-center px-4 py-2 font-medium text-sm tracking-wide transition-colors duration-200 rounded-md bg-primary text-foreground hover:bg-primary/90 focus:ring focus:ring-primary focus:shadow-outline focus:outline-none"
+ return templ.Attributes{
+ "class": class,
+ }
+}
+
+func inputAttrs(hasError bool) templ.Attributes {
+ class := "flex w-full px-3 py-2 bg-transparent text-sm border rounded-md ring-offset-background placeholder:text-neutral-500 focus:border-neutral-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
+ if hasError {
+ class += " border-red-500"
+ } else {
+ class += " border-input"
+ }
+ return templ.Attributes{
+ "class": class,
+ }
+}
diff --git a/bootstrap/plugins/auth/auth_handler.go b/bootstrap/plugins/auth/auth_handler.go
new file mode 100644
index 0000000..0ce45a1
--- /dev/null
+++ b/bootstrap/plugins/auth/auth_handler.go
@@ -0,0 +1,174 @@
+package auth
+
+import (
+ "auth/app/db"
+ "cmp"
+ "database/sql"
+ "net/http"
+ "os"
+ "strconv"
+ "time"
+
+ "github.com/anthdm/gothkit/kit"
+ v "github.com/anthdm/gothkit/validate"
+ "github.com/google/uuid"
+ "golang.org/x/crypto/bcrypt"
+)
+
+const (
+ userSessionName = "user-session"
+)
+
+var authSchema = v.Schema{
+ "email": v.Rules(v.Email),
+ "password": v.Rules(v.Required),
+}
+
+var signupSchema = v.Schema{
+ "email": v.Rules(v.Email),
+ "password": v.Rules(
+ v.ContainsSpecial,
+ v.ContainsUpper,
+ v.Min(7),
+ v.Max(50),
+ ),
+ "firstName": v.Rules(v.Min(2), v.Max(50)),
+ "lastName": v.Rules(v.Min(2), v.Max(50)),
+}
+
+func HandleAuthIndex(kit *kit.Kit) error {
+ if kit.Auth().Check() {
+ redirectURL := cmp.Or(os.Getenv("SUPERKIT_AUTH_REDIRECT_AFTER_LOGIN"), "/profile")
+ return kit.Redirect(http.StatusSeeOther, redirectURL)
+ }
+ return kit.Render(AuthIndex(AuthIndexPageData{}))
+}
+
+func HandleAuthCreate(kit *kit.Kit) error {
+ var values LoginFormValues
+ errors, ok := v.Request(kit.Request, &values, authSchema)
+ if !ok {
+ return kit.Render(LoginForm(values, errors))
+ }
+
+ var user User
+ err := db.Query.NewSelect().
+ Model(&user).
+ Where("user.email = ?", values.Email).
+ Scan(kit.Request.Context())
+ if err != nil {
+ if err == sql.ErrNoRows {
+ errors.Add("credentials", "invalid credentials")
+ return kit.Render(LoginForm(values, errors))
+ }
+ }
+
+ err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(values.Password))
+ if err != nil {
+ errors.Add("credentials", "invalid credentials")
+ return kit.Render(LoginForm(values, errors))
+ }
+
+ // todo: use the kit.Getenv instead of the comp thingy
+ skipVerify := cmp.Or(os.Getenv("SUPERKIT_AUTH_SKIP_VERIFY"), "false")
+ if skipVerify != "true" {
+ if user.EmailVerifiedAt.Equal(time.Time{}) {
+ errors.Add("verified", "please verify your email")
+ return kit.Render(LoginForm(values, errors))
+ }
+ }
+
+ sessionExpiryStr := os.Getenv("SUPERKIT_AUTH_SESSION_EXPIRY_IN_HOURS")
+ sessionExpiry, err := strconv.Atoi(sessionExpiryStr)
+ if err != nil {
+ sessionExpiry = 48
+ }
+ session := Session{
+ UserID: user.ID,
+ Token: uuid.New().String(),
+ CreatedAt: time.Now(),
+ LastLoginAt: time.Now(),
+ ExpiresAt: time.Now().Add(time.Hour * time.Duration(sessionExpiry)),
+ }
+ _, err = db.Query.NewInsert().
+ Model(&session).
+ Exec(kit.Request.Context())
+ if err != nil {
+ return err
+ }
+
+ // TODO change this with kit.Getenv
+ sess := kit.GetSession(userSessionName)
+ sess.Values["sessionToken"] = session.Token
+ sess.Save(kit.Request, kit.Response)
+
+ redirectURL := cmp.Or(os.Getenv("SUPERKIT_AUTH_REDIRECT_AFTER_LOGIN"), "/profile")
+
+ return kit.Redirect(http.StatusSeeOther, redirectURL)
+}
+
+func HandleAuthDelete(kit *kit.Kit) error {
+ sess := kit.GetSession(userSessionName)
+ defer func() {
+ sess.Values = map[any]any{}
+ sess.Save(kit.Request, kit.Response)
+ }()
+ _, err := db.Query.NewDelete().
+ Model((*Session)(nil)).
+ Where("token = ?", sess.Values["sessionToken"]).
+ Exec(kit.Request.Context())
+ if err != nil {
+ return err
+ }
+ return kit.Redirect(http.StatusSeeOther, "/")
+}
+
+func HandleSignupIndex(kit *kit.Kit) error {
+ return kit.Render(SignupIndex(SignupIndexPageData{}))
+}
+
+func HandleSignupCreate(kit *kit.Kit) error {
+ var values SignupFormValues
+ errors, ok := v.Request(kit.Request, &values, signupSchema)
+ if !ok {
+ return kit.Render(SignupForm(values, errors))
+ }
+ if values.Password != values.PasswordConfirm {
+ errors.Add("passwordConfirm", "passwords do not match")
+ return kit.Render(SignupForm(values, errors))
+ }
+ user, err := createUserFromFormValues(values)
+ if err != nil {
+ return err
+ }
+ return kit.Render(ConfirmEmail(user.Email))
+}
+
+func AuthenticateUser(kit *kit.Kit) (kit.Auth, error) {
+ auth := Auth{}
+ sess := kit.GetSession(userSessionName)
+ token, ok := sess.Values["sessionToken"]
+ if !ok {
+ return auth, nil
+ }
+
+ var session Session
+ err := db.Query.NewSelect().
+ Model(&session).
+ Relation("User").
+ Where("session.token = ? AND session.expires_at > ?", token, time.Now()).
+ Scan(kit.Request.Context())
+ if err != nil {
+ return auth, nil
+ }
+ // TODO: do we really need to check if the user is verified
+ // even if we check that already in the login process.
+ // if session.User.EmailVerifiedAt.Equal(time.Time{}) {
+ // return Auth{}, nil
+ // }
+ return Auth{
+ LoggedIn: true,
+ UserID: session.User.ID,
+ Email: session.User.Email,
+ }, nil
+}
diff --git a/bootstrap/plugins/auth/profile_handler.go b/bootstrap/plugins/auth/profile_handler.go
new file mode 100644
index 0000000..136e9de
--- /dev/null
+++ b/bootstrap/plugins/auth/profile_handler.go
@@ -0,0 +1,71 @@
+package auth
+
+import (
+ "auth/app/db"
+ "fmt"
+
+ "github.com/anthdm/gothkit/kit"
+ v "github.com/anthdm/gothkit/validate"
+)
+
+var profileSchema = v.Schema{
+ "firstName": v.Rules(v.Min(3), v.Max(50)),
+ "lastName": v.Rules(v.Min(3), v.Max(50)),
+}
+
+type ProfileFormValues struct {
+ ID int `form:"id"`
+ FirstName string `form:"firstName"`
+ LastName string `form:"lastName"`
+ Email string
+ Success string
+}
+
+func HandleProfileShow(kit *kit.Kit) error {
+ auth := kit.Auth().(Auth)
+
+ var user User
+ err := db.Query.NewSelect().
+ Model(&user).
+ Where("id = ?", auth.UserID).
+ Scan(kit.Request.Context())
+ if err != nil {
+ return err
+ }
+
+ formValues := ProfileFormValues{
+ ID: user.ID,
+ FirstName: user.FirstName,
+ LastName: user.LastName,
+ Email: user.Email,
+ }
+
+ return kit.Render(ProfileShow(formValues))
+}
+
+func HandleProfileUpdate(kit *kit.Kit) error {
+ var values ProfileFormValues
+ errors, ok := v.Request(kit.Request, &values, profileSchema)
+ if !ok {
+ return kit.Render(ProfileForm(values, errors))
+ }
+
+ auth := kit.Auth().(Auth)
+ if auth.UserID != values.ID {
+ return fmt.Errorf("unauthorized request for profile %d", values.ID)
+ }
+ _, err := db.Query.NewUpdate().
+ Model((*User)(nil)).
+ Set("first_name = ?", values.FirstName).
+ Set("last_name = ?", values.LastName).
+ Where("id = ?", auth.UserID).
+ Exec(kit.Request.Context())
+ if err != nil {
+ return err
+ }
+
+ values.Success = "Profile successfully updated!"
+ values.Email = auth.Email
+
+ return kit.Render(ProfileForm(values, v.Errors{}))
+}
diff --git a/bootstrap/plugins/auth/profile_show.templ b/bootstrap/plugins/auth/profile_show.templ
new file mode 100644
index 0000000..f713b3d
--- /dev/null
+++ b/bootstrap/plugins/auth/profile_show.templ
@@ -0,0 +1,50 @@
+package auth
+
+import (
+ "auth/app/views/layouts"
+ v "github.com/anthdm/gothkit/validate"
+ "fmt"
+)
+
+templ ProfileShow(formValues ProfileFormValues) {
+ @layouts.App() {
+
+
+
Welcome, { formValues.FirstName } { formValues.LastName }
+
+
+ @ProfileForm(formValues, v.Errors{})
+
+ }
+}
+
+templ ProfileForm(values ProfileFormValues, errors v.Errors) {
+
+}
diff --git a/bootstrap/plugins/auth/routes.go b/bootstrap/plugins/auth/routes.go
new file mode 100644
index 0000000..b138bda
--- /dev/null
+++ b/bootstrap/plugins/auth/routes.go
@@ -0,0 +1,29 @@
+package auth
+
+import (
+ "github.com/anthdm/gothkit/kit"
+ "github.com/go-chi/chi/v5"
+)
+
+func InitializeRoutes(router chi.Router) {
+ authConfig := kit.AuthenticationConfig{
+ AuthFunc: AuthenticateUser,
+ RedirectURL: "/login",
+ }
+
+ router.Group(func(auth chi.Router) {
+ auth.Use(kit.WithAuthentication(authConfig, false))
+ auth.Get("/login", kit.Handler(HandleAuthIndex))
+ auth.Post("/login", kit.Handler(HandleAuthCreate))
+ auth.Delete("/logout", kit.Handler(HandleAuthDelete))
+
+ auth.Get("/signup", kit.Handler(HandleSignupIndex))
+ auth.Post("/signup", kit.Handler(HandleSignupCreate))
+ })
+
+ router.Group(func(auth chi.Router) {
+ auth.Use(kit.WithAuthentication(authConfig, true))
+ auth.Get("/profile", kit.Handler(HandleProfileShow))
+ auth.Put("/profile", kit.Handler(HandleProfileUpdate))
+ })
+}
diff --git a/bootstrap/plugins/auth/signup.templ b/bootstrap/plugins/auth/signup.templ
new file mode 100644
index 0000000..dfb8265
--- /dev/null
+++ b/bootstrap/plugins/auth/signup.templ
@@ -0,0 +1,96 @@
+package auth
+
+import (
+ v "github.com/anthdm/gothkit/validate"
+ "auth/app/views/layouts"
+ "auth/app/views/components"
+)
+
+type SignupIndexPageData struct {
+ FormValues SignupFormValues
+ FormErrors v.Errors
+}
+
+type SignupFormValues struct {
+ Email string `form:"email"`
+ FirstName string `form:"firstName"`
+ LastName string `form:"lastName"`
+ Password string `form:"password"`
+ PasswordConfirm string `form:"passwordConfirm"`
+}
+
+templ SignupIndex(data SignupIndexPageData) {
+ @layouts.BaseLayout() {
+
+ @components.ThemeSwitcher()
+
+
+
+
+
Signup
+ @SignupForm(data.FormValues, data.FormErrors)
+
+
+
+ }
+}
+
+templ SignupForm(values SignupFormValues, errors v.Errors) {
+
+}
+
+templ ConfirmEmail(email string) {
+ An email confirmation link has been sent to: { email }
+}
diff --git a/bootstrap/plugins/auth/types.go b/bootstrap/plugins/auth/types.go
new file mode 100644
index 0000000..d008071
--- /dev/null
+++ b/bootstrap/plugins/auth/types.go
@@ -0,0 +1,60 @@
+package auth
+
+import (
+ "auth/app/db"
+ "context"
+ "time"
+
+ "golang.org/x/crypto/bcrypt"
+)
+
+type Auth struct {
+ UserID int
+ Email string
+ LoggedIn bool
+}
+
+func (auth Auth) Check() bool {
+ return auth.LoggedIn
+}
+
+type User struct {
+ ID int `bun:"id,pk,autoincrement"`
+ Email string
+ FirstName string
+ LastName string
+ PasswordHash string
+ EmailVerifiedAt time.Time
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+func createUserFromFormValues(values SignupFormValues) (User, error) {
+ hash, err := bcrypt.GenerateFromPassword([]byte(values.Password), bcrypt.DefaultCost)
+ if err != nil {
+ return User{}, err
+ }
+ user := User{
+ Email: values.Email,
+ FirstName: values.FirstName,
+ LastName: values.LastName,
+ PasswordHash: string(hash),
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }
+ _, err = db.Query.NewInsert().Model(&user).Exec(context.Background())
+ return user, err
+}
+
+type Session struct {
+ ID int `bun:"id,pk,autoincrement"`
+ UserID int
+ Token string
+ IPAddress string
+ UserAgent string
+ ExpiresAt time.Time
+ LastLoginAt time.Time
+ CreatedAt time.Time
+
+ User User `bun:"rel:belongs-to,join:user_id=id"`
+}