added auth

This commit is contained in:
anthgg 2024-06-16 10:28:09 +02:00
parent 6f82fd85bf
commit 52b39899dc
10 changed files with 650 additions and 1 deletions

View file

@ -8,7 +8,7 @@ endif
# re-create _templ.txt files on change, then send reload event to browser. # re-create _templ.txt files on change, then send reload event to browser.
# Default url: http://localhost:7331 # Default url: http://localhost:7331
templ: 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. # run air to detect any go file changes to re-build and re-run the server.
server: server:

27
bootstrap/go.mod Normal file
View file

@ -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
)

57
bootstrap/go.sum Normal file
View file

@ -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=

View file

@ -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() {
<div class="fixed top-6 right-6">
@components.ThemeSwitcher()
</div>
<div class="w-full justify-center gap-10">
<div class="mt-10 lg:mt-40">
<div class="max-w-sm mx-auto border rounded-md shadow-sm py-12 px-8 flex flex-col gap-8">
<h2 class="text-center text-2xl font-medium">Login to SuperKit</h2>
@LoginForm(data.FormValues, data.FormErrors)
<a class="text-sm underline" href="/signup">Don't have an account? Signup here.</a>
</div>
</div>
</div>
}
}
templ LoginForm(values LoginFormValues, errors v.Errors) {
<form hx-post="/login" class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="email">Email *</label>
<input { inputAttrs(errors.Has("email"))... } name="email" id="email" value={ values.Email }/>
if errors.Has("email") {
<div class="text-red-500 text-xs">{ errors.Get("email")[0] }</div>
}
</div>
<div class="flex flex-col gap-1">
<label for="password">Password *</label>
<input { inputAttrs(errors.Has("password"))... } type="password" name="password" id="password"/>
if errors.Has("password") {
<ul class="list-disc ml-4">
for _, err := range errors.Get("password") {
<li class="text-red-500 text-xs">password { err }</li>
}
</ul>
}
if errors.Has("credentials") {
<div class="text-red-500 text-xs">{ errors.Get("credentials")[0] }</div>
}
if errors.Has("verified") {
<div class="text-red-500 text-xs">{ errors.Get("verified")[0] }</div>
}
</div>
<button { buttonAttrs()... }>
Login
</button>
</form>
}
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,
}
}

View file

@ -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
}

View file

@ -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{}))
}

View file

@ -0,0 +1,50 @@
package auth
import (
"auth/app/views/layouts"
v "github.com/anthdm/gothkit/validate"
"fmt"
)
templ ProfileShow(formValues ProfileFormValues) {
@layouts.App() {
<div class="mt-32 flex flex-col gap-12">
<div class="flex flex-col gap-2">
<h1 class="text-4xl">Welcome, <span class="font-medium">{ formValues.FirstName } { formValues.LastName }</span></h1>
<div class="flex gap-4">
<a href="/" class="text-sm underline">back to home</a>
<button hx-delete="/logout" class="text-sm underline">sign me out</button>
</div>
</div>
@ProfileForm(formValues, v.Errors{})
</div>
}
}
templ ProfileForm(values ProfileFormValues, errors v.Errors) {
<form hx-put="/profile" class="w-full max-w-sm flex flex-col gap-6">
<input type="hidden" name="id" value={ fmt.Sprint(values.ID) }/>
<div class="flex flex-col gap-2">
<label for="firstName">First Name</label>
<input { inputAttrs(errors.Has("firstName"))... } name="firstName" id="firstName" value={ values.FirstName }/>
if errors.Has("firstName") {
<div class="text-red-500 text-xs">{ errors.Get("firstName")[0] }</div>
}
</div>
<div class="flex flex-col gap-2">
<label for="lastName">Last Name</label>
<input { inputAttrs(errors.Has("lastName"))... } name="lastName" id="lastName" value={ values.LastName }/>
if errors.Has("lastName") {
<div class="text-red-500 text-xs">{ errors.Get("lastName")[0] }</div>
}
</div>
<div class="flex flex-col gap-2">
<label for="email">Email</label>
<div { inputAttrs(false)... }>{ values.Email }</div>
</div>
<button { buttonAttrs()... }>Update profile</button>
if len(values.Success) > 0 {
<div>{ values.Success }</div>
}
</form>
}

View file

@ -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))
})
}

View file

@ -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() {
<div class="fixed top-6 right-6">
@components.ThemeSwitcher()
</div>
<div class="w-full justify-center">
<div class="mt-10 lg:mt-20">
<div class="max-w-md mx-auto border rounded-md shadow-sm py-12 px-6 flex flex-col gap-8">
<h2 class="text-center text-2xl font-medium">Signup</h2>
@SignupForm(data.FormValues, data.FormErrors)
</div>
</div>
</div>
}
}
templ SignupForm(values SignupFormValues, errors v.Errors) {
<form hx-post="/signup" class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="email">Email *</label>
<input { inputAttrs(errors.Has("email"))... } name="email" id="email" value={ values.Email }/>
if errors.Has("email") {
<div class="text-red-500 text-xs">{ errors.Get("email")[0] }</div>
}
</div>
<div class="flex flex-col gap-1">
<label for="firstName">First Name *</label>
<input { inputAttrs(errors.Has("firstName"))... } name="firstName" id="firstName" value={ values.FirstName }/>
if errors.Has("fistName") {
<ul>
for _, err := range errors.Get("firstName") {
<li class="text-red-500 text-xs">{ err }</li>
}
</ul>
}
</div>
<div class="flex flex-col gap-1">
<label for="lastName">Last Name *</label>
<input { inputAttrs(errors.Has("lastName"))... } name="lastName" id="lastName" value={ values.LastName }/>
if errors.Has("lastName") {
<ul>
for _, err := range errors.Get("lastName") {
<li class="text-red-500 text-xs">{ err }</li>
}
</ul>
}
</div>
<div class="flex flex-col gap-1">
<label for="password">Password *</label>
<input { inputAttrs(errors.Has("password"))... } type="password" name="password" id="password"/>
if errors.Has("password") {
<ul>
for _, err := range errors.Get("password") {
<li class="text-red-500 text-xs">{ err }</li>
}
</ul>
}
</div>
<div class="flex flex-col gap-1">
<label for="passwordConfirm">Confirm Password *</label>
<input { inputAttrs(errors.Has("passwordConfirm"))... } type="password" name="passwordConfirm" id="passwordConfirm"/>
if errors.Has("passwordConfirm") {
<div class="text-red-500 text-xs">{ errors.Get("passwordConfirm")[0] }</div>
}
</div>
<button { buttonAttrs()... }>
Signup
</button>
<a class="text-sm underline" href="/login">Already have an account? Login here.</a>
</form>
}
templ ConfirmEmail(email string) {
<div class="text-sm">An email confirmation link has been sent to: <span class="underline font-medium">{ email }</span></div>
}

View file

@ -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"`
}