wip - email verification

This commit is contained in:
anthdm 2024-06-20 17:37:03 +02:00
parent c2af812770
commit 2e732a8c79
10 changed files with 120 additions and 19 deletions

View file

@ -26,4 +26,5 @@ SUPERKIT_SECRET = {{app_secret}}
SUPERKIT_AUTH_REDIRECT_AFTER_LOGIN = /profile
SUPERKIT_AUTH_SESSION_EXPIRY_IN_HOURS = 48
# Skip user email verification
SUPERKIT_AUTH_SKIP_VERIFY = true
SUPERKIT_AUTH_SKIP_VERIFY = false
SUPERKIT_AUTH_EMAIL_VERIFICATION_EXPIRY_IN_HOURS = 1

View file

@ -3,12 +3,13 @@ module AABBCCDD
go 1.22.4
// uncomment for local development on the superkit core.
// replace github.com/anthdm/superkit => ../
replace github.com/anthdm/superkit => ../
require (
github.com/a-h/templ v0.2.707
github.com/anthdm/superkit v0.0.0-20240616155928-19996932bf4f
github.com/go-chi/chi/v5 v5.0.12
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.22
@ -21,7 +22,7 @@ require (
require (
github.com/fatih/color v1.16.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.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

View file

@ -1,13 +1,13 @@
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/superkit v0.0.0-20240616155928-19996932bf4f h1:/zDgsfZcIHH4HNUBsKNNA3+IU7lGZoCc15V269FppK4=
github.com/anthdm/superkit v0.0.0-20240616155928-19996932bf4f/go.mod h1:bh0FK9XGdU86dbLEnMXVaIvmx0d8YRsjutPuDMi7Ni8=
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/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/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
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=
@ -16,8 +16,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/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=

View file

@ -2,7 +2,6 @@ package auth
import (
"AABBCCDD/app/db"
"cmp"
"database/sql"
"fmt"
"net/http"
@ -10,8 +9,11 @@ import (
"strconv"
"time"
"github.com/anthdm/superkit/event"
"github.com/anthdm/superkit/kit"
"github.com/anthdm/superkit/mail"
v "github.com/anthdm/superkit/validate"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
@ -39,7 +41,7 @@ var signupSchema = v.Schema{
func HandleAuthIndex(kit *kit.Kit) error {
if kit.Auth().Check() {
redirectURL := cmp.Or(os.Getenv("SUPERKIT_AUTH_REDIRECT_AFTER_LOGIN"), "/profile")
redirectURL := kit.Getenv("SUPERKIT_AUTH_REDIRECT_AFTER_LOGIN", "/profile")
return kit.Redirect(http.StatusSeeOther, redirectURL)
}
return kit.Render(AuthIndex(AuthIndexPageData{}))
@ -71,7 +73,6 @@ func HandleAuthCreate(kit *kit.Kit) error {
}
skipVerify := kit.Getenv("SUPERKIT_AUTH_SKIP_VERIFY", "false")
fmt.Println(skipVerify)
if skipVerify != "true" {
if user.EmailVerifiedAt.Equal(time.Time{}) {
errors.Add("verified", "please verify your email")
@ -142,9 +143,89 @@ func HandleSignupCreate(kit *kit.Kit) error {
if err != nil {
return err
}
token, err := createVerificationToken(user.ID)
if err != nil {
return err
}
event.Emit("user.signup", user)
mail.NOOPMailer{}.SendEmail(kit.Request.Context(), mail.Contents{
Title: token,
})
return kit.Render(ConfirmEmail(user.Email))
}
func createVerificationToken(userID int) (string, error) {
expiryStr := kit.Getenv("SUPERKIT_AUTH_EMAIL_VERIFICATION_EXPIRY_IN_HOURS", "1")
expiry, err := strconv.Atoi(expiryStr)
if err != nil {
expiry = 1
}
claims := jwt.RegisteredClaims{
Subject: fmt.Sprint(userID),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(expiry))),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(os.Getenv("SUPERKIT_SECRET")))
}
func HandleEmailVerify(kit *kit.Kit) error {
tokenStr := kit.Request.URL.Query().Get("token")
if len(tokenStr) == 0 {
return kit.Render(EmailVerificationError("invalid verification token"))
}
token, err := jwt.ParseWithClaims(
tokenStr, &jwt.RegisteredClaims{}, func(token *jwt.Token) (any, error) {
return []byte(os.Getenv("SUPERKIT_SECRET")), nil
}, jwt.WithLeeway(5*time.Second))
if err != nil {
return err
}
if !token.Valid {
return kit.Render(EmailVerificationError("invalid verification token"))
}
claims, ok := token.Claims.(*jwt.RegisteredClaims)
if !ok {
return kit.Render(EmailVerificationError("invalid verification token"))
}
if claims.ExpiresAt.Time.Before(time.Now()) {
return kit.Render(EmailVerificationError("Email verification token expired"))
}
userID, err := strconv.Atoi(claims.Subject)
if err != nil {
return kit.Render(EmailVerificationError("Email verification token expired"))
}
var user User
err = db.Query.NewSelect().
Model(&user).
Where("id = ?", userID).
Scan(kit.Request.Context())
if err != nil {
return err
}
if user.EmailVerifiedAt.After(time.Time{}) {
return kit.Render(EmailVerificationError("Email already verified"))
}
user.EmailVerifiedAt = time.Now()
_, err = db.Query.NewUpdate().
Model(&user).
WherePK().
Exec(kit.Request.Context())
if err != nil {
return err
}
return kit.Redirect(http.StatusSeeOther, "/login")
}
func AuthenticateUser(kit *kit.Kit) (kit.Auth, error) {
auth := Auth{}
sess := kit.GetSession(userSessionName)
@ -162,12 +243,8 @@ func AuthenticateUser(kit *kit.Kit) (kit.Auth, error) {
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,

View file

@ -0,0 +1,14 @@
package auth
import (
"AABBCCDD/app/views/layouts"
)
templ EmailVerificationError(errorMessage string) {
@layouts.BaseLayout() {
<div class="h-screen flex flex-col justify-center items-center gap-4">
<div class="text-xl">{ errorMessage }</div>
<a href="/" class="underline text-sm">back to homepage</a>
</div>
}
}

View file

@ -11,6 +11,8 @@ func InitializeRoutes(router chi.Router) {
RedirectURL: "/login",
}
router.Get("/email/verify", kit.Handler(HandleEmailVerify))
router.Group(func(auth chi.Router) {
auth.Use(kit.WithAuthentication(authConfig, false))
auth.Get("/login", kit.Handler(HandleAuthIndex))
@ -19,6 +21,7 @@ func InitializeRoutes(router chi.Router) {
auth.Get("/signup", kit.Handler(HandleSignupIndex))
auth.Post("/signup", kit.Handler(HandleSignupCreate))
})
router.Group(func(auth chi.Router) {

View file

@ -878,6 +878,11 @@ body {
line-height: 1.25rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
@ -1005,14 +1010,14 @@ body {
margin-top: 5rem;
}
.lg\:mt-40 {
margin-top: 10rem;
}
.lg\:mt-32 {
margin-top: 8rem;
}
.lg\:mt-40 {
margin-top: 10rem;
}
.lg\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;