DEVELOPMENT ENVIRONMENT

~liljamo/felu

c097efe694ca9af4e6652ee9c4e4e8c9609f15e2 — Jonni Liljamo 1 year, 1 month ago 6dd64c0
feat: adding and deleting of domains
A api/key.go => api/key.go +20 -0
@@ 0,0 1,20 @@
/*
 * 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 api

import "math/rand"

const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

func GenKey() string {
	// NOTE: "Good enough"
	b := make([]byte, 48)
	for i := range b {
		b[i] = chars[rand.Int63() % int64(len(chars))]
	}
	return string(b)
}

M components/manage.templ => components/manage.templ +21 -3
@@ 1,10 1,28 @@
package components

import "git.src.quest/~skye/felu-ddns/config"

func getDomainPattern() string {
	return config.FeluConfig.DNSPattern
}

templ Manage() {
	@ManageBase("Manage") {
		<div>
			something something manaag
			<br/>have the domains of a user listed here, with also the edit stuff and all that
		<div class="bg-teal-200">
			<form class="flex flex-col p-2 gap-2" hx-post="/manage/domains" hx-target="#add_domain_error">
				<label for="domain">Domain</label>
				<div>
					<input class="border" type="text" placeholder="..." name="domain" id="domain"/>
					<span>{ getDomainPattern() }</span>
				</div>
				<label for="a_record">A Record</label>
				<input class="border" type="text" placeholder="..." name="a_record" id="a_record"/>
				<div class="text-rose-600 text-center" id="add_domain_error"></div>
				<button class="border p-1" type="submit">Add</button>
			</form>
		</div>

		<div hx-get="/manage/partials/domains" hx-trigger="load, update-domain-list from:body" hx-target="this">
		</div>
	}
}

M components/manage_templ.go => components/manage_templ.go +37 -13
@@ 9,6 9,12 @@ import "context"
import "io"
import "bytes"

import "git.src.quest/~skye/felu-ddns/config"

func getDomainPattern() string {
	return config.FeluConfig.DNSPattern
}

func Manage() templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
		templBuffer, templIsBuffer := w.(*bytes.Buffer)


@@ 28,25 34,43 @@ func Manage() templ.Component {
				templBuffer = templ.GetBuffer()
				defer templ.ReleaseBuffer(templBuffer)
			}
			_, err = templBuffer.WriteString("<div>")
			_, err = templBuffer.WriteString("<div class=\"bg-teal-200\"><form class=\"flex flex-col p-2 gap-2\" hx-post=\"/manage/domains\" hx-target=\"#add_domain_error\"><label for=\"domain\">")
			if err != nil {
				return err
			}
			var_3 := `something something manaag`
			var_3 := `Domain`
			_, err = templBuffer.WriteString(var_3)
			if err != nil {
				return err
			}
			_, err = templBuffer.WriteString(" <br>")
			_, err = templBuffer.WriteString("</label><div><input class=\"border\" type=\"text\" placeholder=\"...\" name=\"domain\" id=\"domain\"><span>")
			if err != nil {
				return err
			}
			var_4 := `have the domains of a user listed here, with also the edit stuff and all that`
			_, err = templBuffer.WriteString(var_4)
			var var_4 string = getDomainPattern()
			_, err = templBuffer.WriteString(templ.EscapeString(var_4))
			if err != nil {
				return err
			}
			_, err = templBuffer.WriteString("</div>")
			_, err = templBuffer.WriteString("</span></div><label for=\"a_record\">")
			if err != nil {
				return err
			}
			var_5 := `A Record`
			_, err = templBuffer.WriteString(var_5)
			if err != nil {
				return err
			}
			_, err = templBuffer.WriteString("</label><input class=\"border\" type=\"text\" placeholder=\"...\" name=\"a_record\" id=\"a_record\"><div class=\"text-rose-600 text-center\" id=\"add_domain_error\"></div><button class=\"border p-1\" type=\"submit\">")
			if err != nil {
				return err
			}
			var_6 := `Add`
			_, err = templBuffer.WriteString(var_6)
			if err != nil {
				return err
			}
			_, err = templBuffer.WriteString("</button></form></div> <div hx-get=\"/manage/partials/domains\" hx-trigger=\"load, update-domain-list from:body\" hx-target=\"this\"></div>")
			if err != nil {
				return err
			}


@@ 74,12 98,12 @@ func ManageSettings() templ.Component {
			defer templ.ReleaseBuffer(templBuffer)
		}
		ctx = templ.InitializeContext(ctx)
		var_5 := templ.GetChildren(ctx)
		if var_5 == nil {
			var_5 = templ.NopComponent
		var_7 := templ.GetChildren(ctx)
		if var_7 == nil {
			var_7 = templ.NopComponent
		}
		ctx = templ.ClearChildren(ctx)
		var_6 := templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
		var_8 := templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
			templBuffer, templIsBuffer := w.(*bytes.Buffer)
			if !templIsBuffer {
				templBuffer = templ.GetBuffer()


@@ 89,8 113,8 @@ func ManageSettings() templ.Component {
			if err != nil {
				return err
			}
			var_7 := `user settings here, like updating email and password`
			_, err = templBuffer.WriteString(var_7)
			var_9 := `user settings here, like updating email and password`
			_, err = templBuffer.WriteString(var_9)
			if err != nil {
				return err
			}


@@ 103,7 127,7 @@ func ManageSettings() templ.Component {
			}
			return err
		})
		err = ManageBase("Settings").Render(templ.WithChildren(ctx, var_6), templBuffer)
		err = ManageBase("Settings").Render(templ.WithChildren(ctx, var_8), templBuffer)
		if err != nil {
			return err
		}

A components/managepartials.templ => components/managepartials.templ +74 -0
@@ 0,0 1,74 @@
package components

import "git.src.quest/~skye/felu-ddns/db"
import "fmt"

templ ManagePartialDomains(domains []db.Domain) {
	if len(domains) > 0 {
		<script>
			function toggleApiKeyVisibility(inputId) {
				var input = document.getElementById(inputId)
				if (input.type === "password") {
					input.type = "text";
				} else {
					input.type = "password";
				}
			}
		</script>
		<table class="table-auto">
			<thead>
				<tr>
					<th class="text-start p-2">
						Domain
					</th>
					<th class="text-start p-2">
						A Record
					</th>
					<th class="text-start p-2">
						Api Key
					</th>
				</tr>
			</thead>
			<tbody>
				for _, domain := range domains {
					<tr class="border">
						<td>
							<div class="p-2">
								<span class="font-bold">{ domain.Domain }</span>
								<span class="">{ getDomainPattern() }</span>
							</div>
						</td>
						<td>
							<div class="p-2">
								<!-- TODO: Make this editable
									And refresh the div with /manage/partials/domains
								-->
								<input class="border" value={ domain.A }/>
							</div>
						</td>
						<td>
							<div class="p-2">
								<!-- TODO: Add refreshing -->
								<input class="border" disabled type="password" value={ domain.ApiKey }
									id={ fmt.Sprintf("domain_apikey_%s", domain.Id) }
								/>
								<!-- TODO: Replace with one of those eye thingies -->
								<input type="checkbox" onclick="toggleApiKeyVisibility(this.previousElementSibling.id)"/>
								<span>show</span>
							</div>
						</td>
						<td>
							<div class="p-2">
								<button class="border p-1" hx-confirm="Sure?"
									hx-delete={ fmt.Sprintf("/manage/domains/%s", domain.Id) }
								>
									Delete
								</button>
							</div>
						</td>
					</tr>
				}
			</tbody>
		</table>
	}
}

A components/managepartials_templ.go => components/managepartials_templ.go +191 -0
@@ 0,0 1,191 @@
// Code generated by templ@(devel) DO NOT EDIT.

package components

//lint:file-ignore SA4006 This context is only used if a nested component is present.

import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"

import "git.src.quest/~skye/felu-ddns/db"
import "fmt"

func ManagePartialDomains(domains []db.Domain) templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
		templBuffer, templIsBuffer := w.(*bytes.Buffer)
		if !templIsBuffer {
			templBuffer = templ.GetBuffer()
			defer templ.ReleaseBuffer(templBuffer)
		}
		ctx = templ.InitializeContext(ctx)
		var_1 := templ.GetChildren(ctx)
		if var_1 == nil {
			var_1 = templ.NopComponent
		}
		ctx = templ.ClearChildren(ctx)
		if len(domains) > 0 {
			_, err = templBuffer.WriteString("<script>")
			if err != nil {
				return err
			}
			var_2 := `
			function toggleApiKeyVisibility(inputId) {
				var input = document.getElementById(inputId)
				if (input.type === "password") {
					input.type = "text";
				} else {
					input.type = "password";
				}
			}
		`
			_, err = templBuffer.WriteString(var_2)
			if err != nil {
				return err
			}
			_, err = templBuffer.WriteString("</script> <table class=\"table-auto\"><thead><tr><th class=\"text-start p-2\">")
			if err != nil {
				return err
			}
			var_3 := `Domain`
			_, err = templBuffer.WriteString(var_3)
			if err != nil {
				return err
			}
			_, err = templBuffer.WriteString("</th><th class=\"text-start p-2\">")
			if err != nil {
				return err
			}
			var_4 := `A Record`
			_, err = templBuffer.WriteString(var_4)
			if err != nil {
				return err
			}
			_, err = templBuffer.WriteString("</th><th class=\"text-start p-2\">")
			if err != nil {
				return err
			}
			var_5 := `Api Key`
			_, err = templBuffer.WriteString(var_5)
			if err != nil {
				return err
			}
			_, err = templBuffer.WriteString("</th></tr></thead><tbody>")
			if err != nil {
				return err
			}
			for _, domain := range domains {
				_, err = templBuffer.WriteString("<tr class=\"border\"><td><div class=\"p-2\"><span class=\"font-bold\">")
				if err != nil {
					return err
				}
				var var_6 string = domain.Domain
				_, err = templBuffer.WriteString(templ.EscapeString(var_6))
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString("</span><span class=\"\">")
				if err != nil {
					return err
				}
				var var_7 string = getDomainPattern()
				_, err = templBuffer.WriteString(templ.EscapeString(var_7))
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString("</span></div></td><td><div class=\"p-2\"><!--")
				if err != nil {
					return err
				}
				var_8 := ` TODO: Make this editable
									And refresh the div with /manage/partials/domains
								`
				_, err = templBuffer.WriteString(var_8)
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString("--><input class=\"border\" value=\"")
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString(templ.EscapeString(domain.A))
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString("\"></div></td><td><div class=\"p-2\"><!--")
				if err != nil {
					return err
				}
				var_9 := ` TODO: Add refreshing `
				_, err = templBuffer.WriteString(var_9)
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString("--><input class=\"border\" disabled type=\"password\" value=\"")
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString(templ.EscapeString(domain.ApiKey))
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString("\" id=\"")
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString(templ.EscapeString(fmt.Sprintf("domain_apikey_%s", domain.Id)))
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString("\"><!--")
				if err != nil {
					return err
				}
				var_10 := ` TODO: Replace with one of those eye thingies `
				_, err = templBuffer.WriteString(var_10)
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString("--><input type=\"checkbox\" onclick=\"toggleApiKeyVisibility(this.previousElementSibling.id)\"><span>")
				if err != nil {
					return err
				}
				var_11 := `show`
				_, err = templBuffer.WriteString(var_11)
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString("</span></div></td><td><div class=\"p-2\"><button class=\"border p-1\" hx-confirm=\"Sure?\" hx-delete=\"")
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString(templ.EscapeString(fmt.Sprintf("/manage/domains/%s", domain.Id)))
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString("\">")
				if err != nil {
					return err
				}
				var_12 := `Delete`
				_, err = templBuffer.WriteString(var_12)
				if err != nil {
					return err
				}
				_, err = templBuffer.WriteString("</button></div></td></tr>")
				if err != nil {
					return err
				}
			}
			_, err = templBuffer.WriteString("</tbody></table>")
			if err != nil {
				return err
			}
		}
		if !templIsBuffer {
			_, err = templBuffer.WriteTo(w)
		}
		return err
	})
}

A db/domains.go => db/domains.go +64 -0
@@ 0,0 1,64 @@
/*
 * 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 db

import (
	"git.src.quest/~skye/felu-ddns/api"
	"github.com/oklog/ulid/v2"
)

type Domain struct {
	Id      string
	ApiKey  string
	Domain  string
	A       string
}

func FetchDomainsForUser(userId string) ([]Domain, error) {
	rows, err := DBConn.Query(`SELECT id, apikey, ddns_domain, a_record
		FROM domains WHERE owner = $1`, userId)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var domains []Domain
	for rows.Next() {
		var domain Domain
		err = rows.Scan(&domain.Id, &domain.ApiKey, &domain.Domain, &domain.A)
		if err != nil {
			return nil, err
		}
		domains = append(domains, domain)
	}
	err = rows.Err()
	if err != nil {
		return nil, err
	}

	return domains, nil
}

func CreateDomain(domain string, aRecord string, owner string) error {
	ulid := ulid.Make().String()
	apikey := api.GenKey()
	_, err := DBConn.Exec(`INSERT INTO domains(id, apikey, ddns_domain, a_record, owner)
		VALUES ($1, $2, $3, $4, $5)`, ulid, apikey, domain, aRecord, owner)
	if err != nil {
		return err
	}

	return nil
}

func DeleteDomain(id string) error {
	_, err := DBConn.Exec(`DELETE FROM domains WHERE id = $1`, id)
	if err != nil {
		return err
	}
	return nil
}

M felu.go => felu.go +6 -0
@@ 85,6 85,12 @@ func setupFrontendRouter() *gin.Engine {
		manage.GET("/settings", func(c *gin.Context) {
			c.HTML(http.StatusOK, "", components.ManageSettings())
		})

		manage.POST("/domains", handlers.PostDomain())
		manage.PATCH("/domains/:id") // TODO:
		manage.DELETE("/domains/:id", handlers.DeleteDomain())

		manage.GET("/partials/domains", handlers.ManagePartialDomains())
	}
	manageAdmin := r.Group("/manage/admin").Use(
		middlewares.SessionExists(sessionManager),

A handlers/managedomains.go => handlers/managedomains.go +76 -0
@@ 0,0 1,76 @@
/*
 * 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"
	"net/http"

	"git.src.quest/~skye/felu-ddns/db"
	"github.com/gin-gonic/gin"
)

type postDomainData struct {
	Domain  string `form:"domain"`
	ARecord string `form:"a_record"`
}

func PostDomain() gin.HandlerFunc {
	return func(c *gin.Context) {
		data := &postDomainData{}
		if err := c.Bind(data); err != nil {
			log.Printf("[felu] ERROR: Could not bind domain data: %v", err)
			c.String(http.StatusBadRequest, "Could not bind domain data")
			return
		}

		if data.Domain == "" {
			c.String(http.StatusBadRequest, "Domain can't be empty")
			c.Abort()
			return
		}
		if net.ParseIP(data.ARecord).To4() == nil {
			c.String(http.StatusBadRequest, "The A record is invalid")
			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.CreateDomain(data.Domain, data.ARecord, userId.(string))
		if err != nil {
			// FIXME: Handle better
			c.String(http.StatusInternalServerError, "Something went wrong while creating a new domain")
			c.Abort()
			return
		}

		c.Header("HX-Trigger", "update-domain-list")
	}
}

func DeleteDomain() gin.HandlerFunc {
	return func(c *gin.Context) {
		id := c.Param("id")

		err := db.DeleteDomain(id)
		if err != nil {
			// FIXME: Handle better
			c.String(http.StatusInternalServerError, "Something went wrong while creating deleting the domain")
			c.Abort()
			return
		}

		c.Header("HX-Trigger", "update-domain-list")
	}
}

A handlers/managepartials.go => handlers/managepartials.go +35 -0
@@ 0,0 1,35 @@
/*
 * 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 (
	"net/http"

	"git.src.quest/~skye/felu-ddns/components"
	"git.src.quest/~skye/felu-ddns/db"
	"github.com/gin-gonic/gin"
)

func ManagePartialDomains() gin.HandlerFunc {
	return func(c *gin.Context) {
		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")
			c.Abort()
			return
		}

		domains, err := db.FetchDomainsForUser(user_id.(string))
		if err != nil {
			c.String(http.StatusInternalServerError, "Failed to fetch domains for user")
			c.Abort()
			return
		}

		c.HTML(http.StatusOK, "", components.ManagePartialDomains(domains))
	}
}

M static/styles.css => static/styles.css +9 -0
@@ 584,6 584,11 @@ video {
  background-color: rgb(254 205 211 / var(--tw-bg-opacity));
}

.bg-teal-200 {
  --tw-bg-opacity: 1;
  background-color: rgb(153 246 228 / var(--tw-bg-opacity));
}

.p-1 {
  padding: 0.25rem;
}


@@ 609,6 614,10 @@ video {
  line-height: 2.5rem;
}

.font-bold {
  font-weight: 700;
}

.text-rose-600 {
  --tw-text-opacity: 1;
  color: rgb(225 29 72 / var(--tw-text-opacity));