DEVELOPMENT ENVIRONMENT

~liljamo/felu

24b5b31201f343e14c3597ba040b288bc46eca3a — Jonni Liljamo 1 year, 1 month ago 7a32f00
feat: ability to update user email and password, and delete user
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;
}