diff --git a/handler/auth.go b/handler/auth.go index 74ade85..d92aa21 100644 --- a/handler/auth.go +++ b/handler/auth.go @@ -171,11 +171,17 @@ func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc { err := handler.service.VerifyUserEmail(token) - if err != nil { - utils.DoRedirect(w, r, "/auth/signin") + isVerified := err == nil + comp := auth.VerifyResponseComp(isVerified) + + var status int + if isVerified { + status = http.StatusOK } else { - utils.DoRedirect(w, r, "/") + status = http.StatusBadRequest } + + handler.render.RenderLayoutWithStatus(r, w, comp, nil, status) } } diff --git a/handler/render.go b/handler/render.go index c3f007c..28cb3b9 100644 --- a/handler/render.go +++ b/handler/render.go @@ -21,8 +21,9 @@ func NewRender(settings *types.Settings) *Render { } } -func (render *Render) Render(r *http.Request, w http.ResponseWriter, comp templ.Component) { +func (render *Render) RenderWithStatus(r *http.Request, w http.ResponseWriter, comp templ.Component, status int) { w.Header().Set("Content-Type", "text/html") + w.WriteHeader(status) err := comp.Render(r.Context(), w) if err != nil { log.Error("Failed to render layout: %v", err) @@ -30,11 +31,19 @@ func (render *Render) Render(r *http.Request, w http.ResponseWriter, comp templ. } } +func (render *Render) Render(r *http.Request, w http.ResponseWriter, comp templ.Component) { + render.RenderWithStatus(r, w, comp, http.StatusOK) +} + func (render *Render) RenderLayout(r *http.Request, w http.ResponseWriter, slot templ.Component, user *types.User) { + render.RenderLayoutWithStatus(r, w, slot, user, http.StatusOK) +} + +func (render *Render) RenderLayoutWithStatus(r *http.Request, w http.ResponseWriter, slot templ.Component, user *types.User, status int) { userComp := render.getUserComp(user) layout := template.Layout(slot, userComp, render.settings.Environment) - render.Render(r, w, layout) + render.RenderWithStatus(r, w, layout, status) } func (render *Render) getUserComp(user *types.User) templ.Component { diff --git a/main_test.go b/main_test.go index b7ceb89..3e14f75 100644 --- a/main_test.go +++ b/main_test.go @@ -618,6 +618,77 @@ func TestIntegrationAuth(t *testing.T) { err = db.QueryRow("SELECT COUNT(*) FROM user WHERE email = ? AND email_verified = FALSE", "mail@mail.de").Scan(&rows) assert.Nil(t, err) assert.Equal(t, 1, rows) + var token string + err = db.QueryRow("SELECT t.token FROM token t INNER JOIN user u ON u.user_id = t.user_id WHERE u.email = ? AND t.type = ?", "mail@mail.de", types.TokenTypeEmailVerify).Scan(&token) + assert.Nil(t, err) + assert.NotEqual(t, "", token) + }) + }) + t.Run("SignUpVerification", func(t *testing.T) { + t.Run(`should fail verifying email with non existent token`, func(t *testing.T) { + t.Parallel() + + db, basePath, ctx := setupIntegrationTest(t) + + userId := uuid.New() + + _, err := db.Exec(` + INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) + VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt")) + assert.Nil(t, err) + + req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/verify-email?token=invalid-token", nil) + assert.Nil(t, err) + resp, err := httpClient.Do(req) + assert.Nil(t, err) + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + t.Run(`should fail verifying email with outdated token`, func(t *testing.T) { + t.Parallel() + + db, basePath, ctx := setupIntegrationTest(t) + + userId := uuid.New() + token := "my-outdated-verifying-token" + + _, err := db.Exec(` + INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) + VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt")) + assert.Nil(t, err) + _, err = db.Exec(` + INSERT INTO token (token, user_id, type, created_at, expires_at) + VALUES (?, ?, ?, datetime("now", "-16 minute"), datetime("now", "-1 minute"))`, token, userId, types.TokenTypeEmailVerify) + + req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/verify-email?token="+token, nil) + assert.Nil(t, err) + resp, err := httpClient.Do(req) + assert.Nil(t, err) + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + t.Run(`should verify email with correct token`, func(t *testing.T) { + t.Parallel() + + db, basePath, ctx := setupIntegrationTest(t) + + userId := uuid.New() + token := "my-verifying-token" + + _, err := db.Exec(` + INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) + VALUES (?, "mail@mail.de", TRUE, FALSE, ?, ?, datetime())`, userId, []byte("pass"), []byte("salt")) + assert.Nil(t, err) + _, err = db.Exec(` + INSERT INTO token (token, user_id, session_id, type, created_at, expires_at) + VALUES (?, ?, "", ?, datetime("now"), datetime("now", "+15 minute"))`, token, userId, types.TokenTypeEmailVerify) + + req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/verify-email?token="+token, nil) + assert.Nil(t, err) + resp, err := httpClient.Do(req) + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) }) }) t.Run("SignOut", func(t *testing.T) { diff --git a/template/auth/verify_response.templ b/template/auth/verify_response.templ new file mode 100644 index 0000000..84327c3 --- /dev/null +++ b/template/auth/verify_response.templ @@ -0,0 +1,29 @@ +package auth + +templ VerifyResponseComp(isVerified bool) { +
+
+ if isVerified { +

+ Your email has been verified +

+

+ You have completed the verification process. Thank you! +

+ + Go Home + + } else { +

+ Error during verification +

+

+ Please try again by sign up process +

+ + Sign Up + + } +
+
+} diff --git a/template/mail/register.templ b/template/mail/register.templ index 2c15dfc..9eed467 100644 --- a/template/mail/register.templ +++ b/template/mail/register.templ @@ -3,17 +3,21 @@ package mail; import "net/url" templ Register(baseUrl string, token string) { - - - - - - Welcome - - -

Thank you for Sign Up!

-

Click + + + + + Welcome + + + +

Thank you for Sign Up!

+

Click