feat(auth): enable users to delete their account #164 #168
@@ -30,11 +30,13 @@ func getHandler(db *sql.DB) http.Handler {
|
|||||||
// Don't use auth middleware for these routes, as it makes redirecting very difficult, if the mail is not yet verified
|
// Don't use auth middleware for these routes, as it makes redirecting very difficult, if the mail is not yet verified
|
||||||
router.Handle("/auth/signin", service.HandleSignInPage(db))
|
router.Handle("/auth/signin", service.HandleSignInPage(db))
|
||||||
router.Handle("/auth/signup", service.HandleSignUpPage(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/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("/auth/verify-email", service.HandleSignUpVerifyResponsePage(db)) // The link contained in the email
|
||||||
router.Handle("/api/auth/signup", service.HandleSignUpComp(db))
|
router.Handle("/api/auth/signup", service.HandleSignUpComp(db))
|
||||||
router.Handle("/api/auth/signin", service.HandleSignInComp(db))
|
router.Handle("/api/auth/signin", service.HandleSignInComp(db))
|
||||||
router.Handle("/api/auth/signout", service.HandleSignOutComp(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))
|
router.Handle("/api/auth/verify-resend", service.HandleVerifyResendComp(db))
|
||||||
|
|
||||||
return middleware.Logging(middleware.EnableCors(router))
|
return middleware.Logging(middleware.EnableCors(router))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"database/sql"
|
"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 {
|
func HandleSignUpVerifyResponsePage(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
token := r.URL.Query().Get("token")
|
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 {
|
func HandleVerifyResendComp(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
user := utils.GetUserFromSession(db, r)
|
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) {
|
func sendVerificationEmail(db *sql.DB, r *http.Request, userId string, email string) {
|
||||||
var b []byte = make([]byte, 32)
|
|
||||||
_, err := rand.Reader.Read(b)
|
var token string
|
||||||
if err != nil {
|
err := db.QueryRow("SELECT token FROM user_token WHERE user_uuid = ? AND type = 'email_verify'", userId).Scan(&token)
|
||||||
utils.LogError("Could not generate token", err)
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
utils.LogError("Could not get token", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
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 token == "" {
|
||||||
if err != nil {
|
var b []byte = make([]byte, 32)
|
||||||
utils.LogError("Could not insert token", err)
|
_, err = rand.Reader.Read(b)
|
||||||
return
|
if err != nil {
|
||||||
|
utils.LogError("Could not generate token", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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
|
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())
|
utils.SendMail(email, "Welcome to ME-FIT", w.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
template/auth/delete_account.templ
Normal file
16
template/auth/delete_account.templ
Normal 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>
|
||||||
|
}
|
||||||
@@ -3,8 +3,30 @@ package auth
|
|||||||
templ UserComp(user string) {
|
templ UserComp(user string) {
|
||||||
<div id="user-info" class="flex gap-5 items-center">
|
<div id="user-info" class="flex gap-5 items-center">
|
||||||
if user != "" {
|
if user != "" {
|
||||||
<a hx-get="/api/auth/signout" hx-target="#user-info" class="btn btn-sm">Sign Out</a>
|
<div class="group inline-block relative">
|
||||||
<p>{ user }</p>
|
<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 {
|
} else {
|
||||||
<a href="/auth/signup" class="btn btn-sm">Sign Up</a>
|
<a href="/auth/signup" class="btn btn-sm">Sign Up</a>
|
||||||
<a href="/auth/signin" class="btn btn-sm">Sign In</a>
|
<a href="/auth/signin" class="btn btn-sm">Sign In</a>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ templ Layout(slot templ.Component, user templ.Component) {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="h-screen flex flex-col">
|
<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">
|
<a href="/" class="flex-1 flex gap-2">
|
||||||
<img src="/static/favicon.svg" alt="ME-FIT logo"/>
|
<img src="/static/favicon.svg" alt="ME-FIT logo"/>
|
||||||
<span>ME-FIT</span>
|
<span>ME-FIT</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user