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() +
+
+
+
+

Login to SuperKit

+ @LoginForm(data.FormValues, data.FormErrors) + Don't have an account? Signup here. +
+
+
+ } +} + +templ LoginForm(values LoginFormValues, errors v.Errors) { +
+
+ + + if errors.Has("email") { +
{ errors.Get("email")[0] }
+ } +
+
+ + + if errors.Has("password") { + + } + if errors.Has("credentials") { +
{ errors.Get("credentials")[0] }
+ } + if errors.Has("verified") { +
{ errors.Get("verified")[0] }
+ } +
+ +
+} + +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 }

+
+ back to home + +
+
+ @ProfileForm(formValues, v.Errors{}) +
+ } +} + +templ ProfileForm(values ProfileFormValues, errors v.Errors) { +
+ +
+ + + if errors.Has("firstName") { +
{ errors.Get("firstName")[0] }
+ } +
+
+ + + if errors.Has("lastName") { +
{ errors.Get("lastName")[0] }
+ } +
+
+ +
{ values.Email }
+
+ + if len(values.Success) > 0 { +
{ values.Success }
+ } +
+} 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) { +
+
+ + + if errors.Has("email") { +
{ errors.Get("email")[0] }
+ } +
+
+ + + if errors.Has("fistName") { + + } +
+
+ + + if errors.Has("lastName") { + + } +
+
+ + + if errors.Has("password") { + + } +
+
+ + + if errors.Has("passwordConfirm") { +
{ errors.Get("passwordConfirm")[0] }
+ } +
+ + Already have an account? Login here. +
+} + +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"` +}