From 24b5b31201f343e14c3597ba040b288bc46eca3a Mon Sep 17 00:00:00 2001 From: Jonni Liljamo Date: Mon, 16 Oct 2023 23:39:19 +0300 Subject: [PATCH] feat: ability to update user email and password, and delete user --- internal/components/base.templ | 1 + internal/components/base_templ.go | 9 ++ internal/components/manage.templ | 29 ++++++- internal/components/manage_templ.go | 70 ++++++++++++++- internal/db/domains.go | 8 ++ internal/db/users.go | 61 +++++++++++++ internal/handlers/manage.go | 19 ++++- internal/handlers/user.go | 127 ++++++++++++++++++++++++++++ internal/routers/frontend.go | 6 +- static/styles.css | 10 +++ 10 files changed, 330 insertions(+), 10 deletions(-) create mode 100644 internal/handlers/user.go diff --git a/internal/components/base.templ b/internal/components/base.templ index 363b72d..7455a1f 100644 --- a/internal/components/base.templ +++ b/internal/components/base.templ @@ -45,6 +45,7 @@ templ ManageBase(title string) {
{ children... } diff --git a/internal/components/base_templ.go b/internal/components/base_templ.go index f540c44..e14a8b4 100644 --- a/internal/components/base_templ.go +++ b/internal/components/base_templ.go @@ -158,6 +158,15 @@ func ManageBase(title string) templ.Component { if err != nil { return err } + _, err = templBuffer.WriteString("") + if err != nil { + return err + } + var_11 := `User Settings` + _, err = templBuffer.WriteString(var_11) + if err != nil { + return err + } _, err = templBuffer.WriteString("
") if err != nil { return err diff --git a/internal/components/manage.templ b/internal/components/manage.templ index e1fb0d3..d2f7c5e 100644 --- a/internal/components/manage.templ +++ b/internal/components/manage.templ @@ -27,10 +27,33 @@ templ Manage() { } } -templ ManageSettings() { +templ ManageUser(currentEmail string) { @ManageBase("Settings") { -
- user settings here, like updating email and password +
+
+ + + + + + +
+ +
+
+
+
+ +
+ + +
+
+ +
+
+
+
} } diff --git a/internal/components/manage_templ.go b/internal/components/manage_templ.go index 0b32bc2..ac1ba18 100644 --- a/internal/components/manage_templ.go +++ b/internal/components/manage_templ.go @@ -90,7 +90,7 @@ func Manage() templ.Component { }) } -func ManageSettings() templ.Component { +func ManageUser(currentEmail string) templ.Component { return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) { templBuffer, templIsBuffer := w.(*bytes.Buffer) if !templIsBuffer { @@ -109,16 +109,78 @@ func ManageSettings() templ.Component { templBuffer = templ.GetBuffer() defer templ.ReleaseBuffer(templBuffer) } - _, err = templBuffer.WriteString("
") + _, err = templBuffer.WriteString("
") + _, err = templBuffer.WriteString("
") if err != nil { return err } diff --git a/internal/db/domains.go b/internal/db/domains.go index 1133a58..cb62078 100644 --- a/internal/db/domains.go +++ b/internal/db/domains.go @@ -62,3 +62,11 @@ func DeleteDomain(id string) error { } return nil } + +func DeleteDomainsForUser(userId string) error { + _, err := DBConn.Exec(`DELETE FROM domains WHERE owner = $1`, userId) + if err != nil { + return err + } + return nil +} diff --git a/internal/db/users.go b/internal/db/users.go index f376596..d1a4bf2 100644 --- a/internal/db/users.go +++ b/internal/db/users.go @@ -116,3 +116,64 @@ func FetchAllUsers() ([]User, error) { return users, nil } + +func DeleteUser(id string) error { + err := DeleteDomainsForUser(id) + if err != nil { + return err + } + _, err = DBConn.Exec(`DELETE FROM users WHERE id = $1`, id) + if err != nil { + return err + } + return nil +} + +func UpdateUserEmail(id string, email string) error { + _, err := DBConn.Exec(`UPDATE users SET email = $1 WHERE id = $2`, + email, id) + if err != nil { + return err + } + return nil +} + +func UpdateUserPassword(id string, pwd string) error { + argon := argon2.DefaultConfig() + encoded, err := argon.HashEncoded([]byte(pwd)) + if err != nil { + return err + } + + _, err = DBConn.Exec(`UPDATE users SET pwd = $1 WHERE id = $2`, + string(encoded), id) + if err != nil { + return err + } + + return nil +} + +func VerifyUserPassword(id string, pwd string) bool { + // FIXME: Currently doesn't return any errors, and just return false in error cases + // I mean... Shouldn't really error, but who knows + var encodedPwd string + err := DBConn.QueryRow(`SELECT pwd FROM users WHERE id = $1`, + id).Scan(&encodedPwd) + if err == sql.ErrNoRows { + return false + } + if err != nil { + return false + } + + ok, err := argon2.VerifyEncoded([]byte(pwd), []byte(encodedPwd)) + if err != nil { + return false + } + if !ok { + return false + } + + return true +} diff --git a/internal/handlers/manage.go b/internal/handlers/manage.go index 52de618..4c49185 100644 --- a/internal/handlers/manage.go +++ b/internal/handlers/manage.go @@ -10,6 +10,7 @@ import ( "net/http" "git.src.quest/~skye/felu-ddns/internal/components" + "git.src.quest/~skye/felu-ddns/internal/db" "github.com/gin-gonic/gin" ) @@ -19,8 +20,22 @@ func Manage() gin.HandlerFunc { } } -func ManageSettings() gin.HandlerFunc { +func ManageUser() gin.HandlerFunc { return func(c *gin.Context) { - c.HTML(http.StatusOK, "", components.ManageSettings()) + user_id, exists := c.Get("user_id") + if !exists { + c.String(http.StatusInternalServerError, "This should not be possible, but don't quote me on that, S01E01") + c.Abort() + return + } + + user, err := db.FetchUserWithId(user_id.(string)) + if err != nil { + c.String(http.StatusInternalServerError, "This should not be possible, but don't quote me on that, S01E02") + c.Abort() + return + } + + c.HTML(http.StatusOK, "", components.ManageUser(user.Email)) } } diff --git a/internal/handlers/user.go b/internal/handlers/user.go new file mode 100644 index 0000000..a1622c0 --- /dev/null +++ b/internal/handlers/user.go @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 Jonni Liljamo + * + * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ +package handlers + +import ( + "log" + "net/http" + + "git.src.quest/~skye/felu-ddns/internal/db" + "github.com/alexedwards/scs/v2" + "github.com/gin-gonic/gin" +) + +type postUserPasswordData struct { + CurrentPassword string `form:"current_password"` + NewPassword string `form:"new_password"` + ConfirmNewPassword string `form:"confirm_new_password"` +} + +func PostUserPassword() gin.HandlerFunc { + return func(c *gin.Context) { + data := &postUserPasswordData{} + if err := c.Bind(data); err != nil { + log.Printf("[felu] ERROR: Could not bind password data: %v", err) + c.String(http.StatusBadRequest, "Could not bind password data") + return + } + + if len(data.NewPassword) < 10 { + c.String(http.StatusBadRequest, "Password should be at least 10 chars") + c.Abort() + return + } + if data.NewPassword != data.ConfirmNewPassword { + c.String(http.StatusBadRequest, "New and confirm do not match") + c.Abort() + return + } + + userId, exists := c.Get("user_id") + if !exists { + c.String(http.StatusInternalServerError, "This should not be possible, but don't quote me on that") + c.Abort() + return + } + + if !db.VerifyUserPassword(userId.(string), data.CurrentPassword) { + c.String(http.StatusBadRequest, "Current password is not correct") + c.Abort() + return + } + + err := db.UpdateUserPassword(userId.(string), data.NewPassword) + if err != nil { + // FIXME: Handle better + c.String(http.StatusInternalServerError, "Something went wrong while deleting the user") + c.Abort() + return + } + + c.Header("HX-Refresh", "true") + } +} + +type postUserEmailData struct { + Email string `form:"email"` +} + +func PostUserEmail() gin.HandlerFunc { + return func(c *gin.Context) { + data := &postUserEmailData{} + if err := c.Bind(data); err != nil { + log.Printf("[felu] ERROR: Could not bind email data: %v", err) + c.String(http.StatusBadRequest, "Could not bind email data") + return + } + + if data.Email == "" { + c.String(http.StatusBadRequest, "Email can't be empty") + c.Abort() + return + } + + userId, exists := c.Get("user_id") + if !exists { + c.String(http.StatusInternalServerError, "This should not be possible, but don't quote me on that") + c.Abort() + return + } + + err := db.UpdateUserEmail(userId.(string), data.Email) + if err != nil { + // FIXME: Handle better + c.String(http.StatusInternalServerError, "Something went wrong while deleting the user") + c.Abort() + return + } + + c.Header("HX-Refresh", "true") + } +} + +func DeleteUser(sm *scs.SessionManager) gin.HandlerFunc { + return func(c *gin.Context) { + userId, exists := c.Get("user_id") + if !exists { + c.String(http.StatusInternalServerError, "This should not be possible, but don't quote me on that") + c.Abort() + return + } + + err := db.DeleteUser(userId.(string)) + if err != nil { + // FIXME: Handle better + c.String(http.StatusInternalServerError, "Something went wrong while deleting the user") + c.Abort() + return + } + + sm.Destroy(c.Request.Context()) + c.Header("HX-Refresh", "true") + } +} diff --git a/internal/routers/frontend.go b/internal/routers/frontend.go index d5d4ce4..10c10b1 100644 --- a/internal/routers/frontend.go +++ b/internal/routers/frontend.go @@ -36,7 +36,11 @@ func SetupFrontendRouter(sm *scs.SessionManager) *gin.Engine { manage := r.Group("/manage", middlewares.SessionExists(sm)) { manage.GET("/", handlers.Manage()) - manage.GET("/settings", handlers.ManageSettings()) + + manage.GET("/user", handlers.ManageUser()) + manage.POST("/user/password", handlers.PostUserPassword()) + manage.POST("/user/email", handlers.PostUserEmail()) + manage.DELETE("/user", handlers.DeleteUser(sm)) manage.POST("/domains", handlers.PostDomain()) manage.PATCH("/domains/:id") // TODO: diff --git a/static/styles.css b/static/styles.css index dbbf670..90d63af 100644 --- a/static/styles.css +++ b/static/styles.css @@ -594,6 +594,16 @@ video { background-color: rgb(254 240 138 / var(--tw-bg-opacity)); } +.bg-violet-200 { + --tw-bg-opacity: 1; + background-color: rgb(221 214 254 / var(--tw-bg-opacity)); +} + +.bg-emerald-200 { + --tw-bg-opacity: 1; + background-color: rgb(167 243 208 / var(--tw-bg-opacity)); +} + .p-1 { padding: 0.25rem; } -- 2.44.1