M internal/components/base.templ => internal/components/base.templ +1 -0
@@ 45,6 45,7 @@ templ ManageBase(title string) {
<div class="flex flex-col w-full items-center p-4">
<div class="flex w-full max-w-5xl items-center gap-4">
<a href="/manage">{ serviceName() }</a>
+ <a class="border p-1" href="/manage/user">User Settings</a>
</div>
<div class="flex flex-col w-full max-w-5xl items-center gap-2">
{ children... }
M internal/components/base_templ.go => internal/components/base_templ.go +9 -0
@@ 158,6 158,15 @@ func ManageBase(title string) templ.Component {
if err != nil {
return err
}
+ _, err = templBuffer.WriteString("</a><a class=\"border p-1\" href=\"/manage/user\">")
+ if err != nil {
+ return err
+ }
+ var_11 := `User Settings`
+ _, err = templBuffer.WriteString(var_11)
+ if err != nil {
+ return err
+ }
_, err = templBuffer.WriteString("</a></div><div class=\"flex flex-col w-full max-w-5xl items-center gap-2\">")
if err != nil {
return err
M internal/components/manage.templ => internal/components/manage.templ +26 -3
@@ 27,10 27,33 @@ templ Manage() {
}
}
-templ ManageSettings() {
+templ ManageUser(currentEmail string) {
@ManageBase("Settings") {
- <div>
- user settings here, like updating email and password
+ <div class="bg-violet-200">
+ <form class="flex flex-col p-2 gap-2" hx-confirm="Sure?" hx-post="/manage/user/password" hx-target="#update_password_error">
+ <label for="current_password">Current Password</label>
+ <input class="border" type="password" placeholder="..." name="current_password" id="current_password"/>
+ <label for="new_password">New Password</label>
+ <input class="border" type="password" placeholder="..." name="new_password" id="new_password"/>
+ <label for="confirm_new_password">Confirm</label>
+ <input class="border" type="password" placeholder="..." name="confirm_new_password" id="confirm_new_password"/>
+ <div class="text-rose-600 text-center" id="update_password_error"></div>
+ <button class="border p-1">Update</button>
+ </form>
+ </div>
+ <div class="bg-emerald-200">
+ <form class="flex flex-col p-2 gap-2" hx-confirm="Sure?" hx-post="/manage/user/email" hx-target="#update_email_error">
+ <label for="email">Email</label>
+ <div>
+ <input class="border" type="text" placeholder="..." name="email" id="email" value={ currentEmail }/>
+ <input class="border" type="reset"/>
+ </div>
+ <div class="text-rose-600 text-center" id="update_email_error"></div>
+ <button class="border p-1">Update</button>
+ </form>
+ </div>
+ <div class="bg-rose-200">
+ <button class="border p-1" hx-confirm="Sure? This will also delete all domain entries owned by the account." hx-delete="/manage/user">Delete Account</button>
</div>
}
}
M internal/components/manage_templ.go => internal/components/manage_templ.go +66 -4
@@ 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("<div>")
+ _, err = templBuffer.WriteString("<div class=\"bg-violet-200\"><form class=\"flex flex-col p-2 gap-2\" hx-confirm=\"Sure?\" hx-post=\"/manage/user/password\" hx-target=\"#update_password_error\"><label for=\"current_password\">")
if err != nil {
return err
}
- var_9 := `user settings here, like updating email and password`
+ var_9 := `Current Password`
_, err = templBuffer.WriteString(var_9)
if err != nil {
return err
}
- _, err = templBuffer.WriteString("</div>")
+ _, err = templBuffer.WriteString("</label><input class=\"border\" type=\"password\" placeholder=\"...\" name=\"current_password\" id=\"current_password\"><label for=\"new_password\">")
+ if err != nil {
+ return err
+ }
+ var_10 := `New Password`
+ _, err = templBuffer.WriteString(var_10)
+ if err != nil {
+ return err
+ }
+ _, err = templBuffer.WriteString("</label><input class=\"border\" type=\"password\" placeholder=\"...\" name=\"new_password\" id=\"new_password\"><label for=\"confirm_new_password\">")
+ if err != nil {
+ return err
+ }
+ var_11 := `Confirm`
+ _, err = templBuffer.WriteString(var_11)
+ if err != nil {
+ return err
+ }
+ _, err = templBuffer.WriteString("</label><input class=\"border\" type=\"password\" placeholder=\"...\" name=\"confirm_new_password\" id=\"confirm_new_password\"><div class=\"text-rose-600 text-center\" id=\"update_password_error\"></div><button class=\"border p-1\">")
+ if err != nil {
+ return err
+ }
+ var_12 := `Update`
+ _, err = templBuffer.WriteString(var_12)
+ if err != nil {
+ return err
+ }
+ _, err = templBuffer.WriteString("</button></form></div> <div class=\"bg-emerald-200\"><form class=\"flex flex-col p-2 gap-2\" hx-confirm=\"Sure?\" hx-post=\"/manage/user/email\" hx-target=\"#update_email_error\"><label for=\"email\">")
+ if err != nil {
+ return err
+ }
+ var_13 := `Email`
+ _, err = templBuffer.WriteString(var_13)
+ if err != nil {
+ return err
+ }
+ _, err = templBuffer.WriteString("</label><div><input class=\"border\" type=\"text\" placeholder=\"...\" name=\"email\" id=\"email\" value=\"")
+ if err != nil {
+ return err
+ }
+ _, err = templBuffer.WriteString(templ.EscapeString(currentEmail))
+ if err != nil {
+ return err
+ }
+ _, err = templBuffer.WriteString("\"><input class=\"border\" type=\"reset\"></div><div class=\"text-rose-600 text-center\" id=\"update_email_error\"></div><button class=\"border p-1\">")
+ if err != nil {
+ return err
+ }
+ var_14 := `Update`
+ _, err = templBuffer.WriteString(var_14)
+ if err != nil {
+ return err
+ }
+ _, err = templBuffer.WriteString("</button></form></div> <div class=\"bg-rose-200\"><button class=\"border p-1\" hx-confirm=\"Sure? This will also delete all domain entries owned by the account.\" hx-delete=\"/manage/user\">")
+ if err != nil {
+ return err
+ }
+ var_15 := `Delete Account`
+ _, err = templBuffer.WriteString(var_15)
+ if err != nil {
+ return err
+ }
+ _, err = templBuffer.WriteString("</button></div>")
if err != nil {
return err
}
M internal/db/domains.go => internal/db/domains.go +8 -0
@@ 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
+}
M internal/db/users.go => internal/db/users.go +61 -0
@@ 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
+}
M internal/handlers/manage.go => internal/handlers/manage.go +17 -2
@@ 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))
}
}
A internal/handlers/user.go => internal/handlers/user.go +127 -0
@@ 0,0 1,127 @@
+/*
+ * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
+ *
+ * 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")
+ }
+}
M internal/routers/frontend.go => internal/routers/frontend.go +5 -1
@@ 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:
M static/styles.css => static/styles.css +10 -0
@@ 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;
}