feat(auth): enable users to delete their account #164 #168

Merged
tim merged 1 commits from 164-delete-account into master 2024-09-12 15:55:21 +00:00
5 changed files with 129 additions and 15 deletions

View File

@@ -31,10 +31,12 @@ func getHandler(db *sql.DB) http.Handler {
router.Handle("/auth/signin", service.HandleSignInPage(db))
router.Handle("/auth/signup", service.HandleSignUpPage(db))
router.Handle("/auth/verify", service.HandleSignUpVerifyPage(db)) // Hint for the user to verify their email
router.Handle("/auth/delete-account", service.HandleDeleteAccountPage(db))
router.Handle("/auth/verify-email", service.HandleSignUpVerifyResponsePage(db)) // The link contained in the email
router.Handle("/api/auth/signup", service.HandleSignUpComp(db))
router.Handle("/api/auth/signin", service.HandleSignInComp(db))
router.Handle("/api/auth/signout", service.HandleSignOutComp(db))
router.Handle("/api/auth/delete-account", service.HandleDeleteAccountComp(db))
router.Handle("/api/auth/verify-resend", service.HandleVerifyResendComp(db))
return middleware.Logging(middleware.EnableCors(router))

View File

@@ -1,6 +1,7 @@
package service
import (
"context"
"crypto/rand"
"crypto/subtle"
"database/sql"
@@ -84,6 +85,24 @@ func HandleSignUpVerifyPage(db *sql.DB) http.HandlerFunc {
}
}
func HandleDeleteAccountPage(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// An enverified email should be able to delete their account
user := utils.GetUserFromSession(db, r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
} else {
userComp := UserInfoComp(user)
comp := auth.DeleteAccountComp()
err := template.Layout(comp, userComp).Render(r.Context(), w)
if err != nil {
utils.LogError("Failed to render verify page", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
}
}
func HandleSignUpVerifyResponsePage(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
@@ -283,6 +302,48 @@ func HandleSignOutComp(db *sql.DB) http.HandlerFunc {
}
}
func HandleDeleteAccountComp(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := utils.GetUserFromSession(db, r)
if user == nil {
utils.DoRedirect(w, r, "/auth/signin")
return
}
_, err := db.Exec("DELETE FROM workout WHERE user_id = ?", user.Id)
if err != nil {
utils.LogError("Could not delete workouts", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
_, err = db.Exec("DELETE FROM user_token WHERE user_uuid = ?", user.Id)
if err != nil {
utils.LogError("Could not delete user tokens", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
_, err = db.Exec("DELETE FROM session WHERE user_uuid = ?", user.Id)
if err != nil {
utils.LogError("Could not delete sessions", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
_, err = db.Exec("DELETE FROM user WHERE user_uuid = ?", user.Id)
if err != nil {
utils.LogError("Could not delete user", err)
utils.TriggerToast(w, r, "error", "Internal Server Error")
return
}
go utils.SendMail(user.Email, "Account deleted", "Your account has been deleted")
utils.DoRedirect(w, r, "/")
}
}
func HandleVerifyResendComp(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := utils.GetUserFromSession(db, r)
@@ -298,23 +359,36 @@ func HandleVerifyResendComp(db *sql.DB) http.HandlerFunc {
}
func sendVerificationEmail(db *sql.DB, r *http.Request, userId string, email string) {
var token string
err := db.QueryRow("SELECT token FROM user_token WHERE user_uuid = ? AND type = 'email_verify'", userId).Scan(&token)
if err != nil && err != sql.ErrNoRows {
utils.LogError("Could not get token", err)
return
}
if token == "" {
var b []byte = make([]byte, 32)
_, err := rand.Reader.Read(b)
_, err = rand.Reader.Read(b)
if err != nil {
utils.LogError("Could not generate token", err)
return
}
token := base64.StdEncoding.EncodeToString(b)
token = base64.StdEncoding.EncodeToString(b)
_, err = db.Exec("INSERT INTO user_token (user_uuid, type, token, created_at) VALUES (?, 'email_verify', ?, datetime())", userId, token)
if err != nil {
utils.LogError("Could not insert token", err)
return
}
}
var w strings.Builder
tempMail.Register(token).Render(r.Context(), &w)
err = tempMail.Register(token).Render(context.Background(), &w)
if err != nil {
utils.LogError("Could not render welcome email", err)
return
}
utils.SendMail(email, "Welcome to ME-FIT", w.String())
}

View File

@@ -0,0 +1,16 @@
package auth
templ DeleteAccountComp() {
<main class="h-full flex items-center justify-center">
<div class="card bg-neutral text-neutral-content w-96">
<div class="card-body items-center text-center">
<h2 class="card-title">Delete Account</h2>
<p>Do you really want to delete all your data? This cannot be undone!</p>
<div class="card-actions justify-end mt-4">
<a href="/" class="btn btn-ghost">Cancel</a>
<button hx-get="/api/auth/delete-account" class="btn btn-primary">Delete Account</button>
</div>
</div>
</div>
</main>
}

View File

@@ -3,8 +3,30 @@ package auth
templ UserComp(user string) {
<div id="user-info" class="flex gap-5 items-center">
if user != "" {
<a hx-get="/api/auth/signout" hx-target="#user-info" class="btn btn-sm">Sign Out</a>
<p>{ user }</p>
<div class="group inline-block relative">
<button
class="font-semibold py-2 px-4 inline-flex items-center"
>
<span class="mr-1">{ user }</span>
<svg
class="fill-current h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<path
d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
></path>
</svg>
</button>
<div class="absolute hidden group-hover:block w-full">
<ul class="menu bg-base-300 rounded-box w-fit float-right mr-4 p-3">
<li class="mb-1">
<a hx-get="/api/auth/signout" hx-target="#user-info">Sign Out</a>
</li>
<li><a href="/auth/delete-account" class="text-error">Delete Account</a></li>
</ul>
</div>
</div>
} else {
<a href="/auth/signup" class="btn btn-sm">Sign Up</a>
<a href="/auth/signin" class="btn btn-sm">Sign In</a>

View File

@@ -15,7 +15,7 @@ templ Layout(slot templ.Component, user templ.Component) {
</head>
<body>
<div class="h-screen flex flex-col">
<div class="flex justify-end items-center gap-2 py-1 px-2 md:gap-10 md:px-10 md:py-2 shadow">
<div class="flex justify-end items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2 shadow">
<a href="/" class="flex-1 flex gap-2">
<img src="/static/favicon.svg" alt="ME-FIT logo"/>
<span>ME-FIT</span>