DEVELOPMENT ENVIRONMENT

~liljamo/felu

30b7704d74e201483ba110fe911f5977fd41617b — Jonni Liljamo 6 months ago 50e36dd
chore: implement changes from linters, update copyright years
M cmd/felu/main.go => cmd/felu/main.go +10 -8
@@ 1,9 1,11 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// nolint
package main

import (


@@ 25,7 27,7 @@ import (
var version = "notset-builtin"

var (
	g errgroup.Group
	g              errgroup.Group
	sessionManager *scs.SessionManager
)



@@ 49,16 51,16 @@ func main() {
	sessionManager.Lifetime = 6 * time.Hour

	frontend := &http.Server{
		Addr: config.FeluConfig.FrontendBindAddr,
		Handler: sessionManager.LoadAndSave(routers.SetupFrontendRouter(sessionManager)),
		ReadTimeout: 5 * time.Second,
		Addr:         config.FeluConfig.FrontendBindAddr,
		Handler:      sessionManager.LoadAndSave(routers.SetupFrontendRouter(sessionManager)),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	api := &http.Server{
		Addr: config.FeluConfig.APIBindAddr,
		Handler: routers.SetupAPIRouter(version),
		ReadTimeout: 5 * time.Second,
		Addr:         config.FeluConfig.APIBindAddr,
		Handler:      routers.SetupAPIRouter(version),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}


M internal/api/update.go => internal/api/update.go +9 -6
@@ 1,9 1,11 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// Package api implements API route handlers.
package api

import (


@@ 14,13 16,14 @@ import (
	"github.com/gin-gonic/gin"
)

// UpdateA updates an A record based on query params.
func UpdateA() gin.HandlerFunc {
	return func(c *gin.Context) {
		domain := c.Query("domain")
		if domain == "" {
			c.JSON(http.StatusBadRequest, gin.H{
				"status": "error",
				"error": "no domain was provided",
				"error":  "no domain was provided",
			})
			c.Abort()
			return


@@ 30,7 33,7 @@ func UpdateA() gin.HandlerFunc {
		if apiKey == "" {
			c.JSON(http.StatusBadRequest, gin.H{
				"status": "error",
				"error": "no api key was provided",
				"error":  "no api key was provided",
			})
			c.Abort()
			return


@@ 44,7 47,7 @@ func UpdateA() gin.HandlerFunc {
		if err := util.CheckARecord(aRecord); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{
				"status": "error",
				"error": err.Error(),
				"error":  err.Error(),
			})
			c.Abort()
			return


@@ 55,14 58,14 @@ func UpdateA() gin.HandlerFunc {
			// FIXME: Handle better, "bad api key" is just the most likely scenario
			c.JSON(http.StatusBadRequest, gin.H{
				"status": "error",
				"error": "bad api key",
				"error":  "bad api key",
			})
			c.Abort()
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"status": "success",
			"status":   "success",
			"a_record": aRecord,
		})
	}

M internal/components/adminpartials.templ => internal/components/adminpartials.templ +2 -2
@@ 23,7 23,7 @@ templ AdminPartialUsersList(users []db.User) {
				<tr class="border">
					<td>
						<div class="p-2">
							<input class="border" value={ user.Id } disabled/>
							<input class="border" value={ user.ID } disabled/>
						</div>
					</td>
					<td>


@@ 65,7 65,7 @@ templ AdminPartialDomainsList(domains []db.Domain) {
				<tr class="border">
					<td>
						<div class="p-2">
							<input class="border" value={ domain.Id } disabled/>
							<input class="border" value={ domain.ID } disabled/>
						</div>
					</td>
					<td>

M internal/components/adminpartials_templ.go => internal/components/adminpartials_templ.go +2 -2
@@ 35,7 35,7 @@ func AdminPartialUsersList(users []db.User) templ.Component {
			if templ_7745c5c3_Err != nil {
				return templ_7745c5c3_Err
			}
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(user.Id))
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(user.ID))
			if templ_7745c5c3_Err != nil {
				return templ_7745c5c3_Err
			}


@@ 103,7 103,7 @@ func AdminPartialDomainsList(domains []db.Domain) templ.Component {
			if templ_7745c5c3_Err != nil {
				return templ_7745c5c3_Err
			}
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(domain.Id))
			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(domain.ID))
			if templ_7745c5c3_Err != nil {
				return templ_7745c5c3_Err
			}

M internal/components/managepartials_templ.go => internal/components/managepartials_templ.go +11 -11
@@ 81,7 81,7 @@ func ManagePartialDomains(domains []db.Domain) templ.Component {
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("/manage/domains/%s", domain.Id)))
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("/manage/domains/%s", domain.ID)))
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}


@@ 89,7 89,7 @@ func ManagePartialDomains(domains []db.Domain) templ.Component {
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("#domain_patch_error_%s", domain.Id)))
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("#domain_patch_error_%s", domain.ID)))
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}


@@ 105,7 105,7 @@ func ManagePartialDomains(domains []db.Domain) templ.Component {
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("domain_patch_error_%s", domain.Id)))
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("domain_patch_error_%s", domain.ID)))
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}


@@ 113,7 113,7 @@ func ManagePartialDomains(domains []db.Domain) templ.Component {
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(domain.ApiKey))
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(domain.APIKey))
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}


@@ 121,7 121,7 @@ func ManagePartialDomains(domains []db.Domain) templ.Component {
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("domain_apikey_%s", domain.Id)))
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("domain_apikey_%s", domain.ID)))
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}


@@ 129,7 129,7 @@ func ManagePartialDomains(domains []db.Domain) templ.Component {
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}
				templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, toggleApiKeyVisibility(domain.Id))
				templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, toggleApiKeyVisibility(domain.ID))
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}


@@ 137,7 137,7 @@ func ManagePartialDomains(domains []db.Domain) templ.Component {
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}
				var templ_7745c5c3_Var4 templ.ComponentScript = toggleApiKeyVisibility(domain.Id)
				var templ_7745c5c3_Var4 templ.ComponentScript = toggleApiKeyVisibility(domain.ID)
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4.Call)
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err


@@ 146,7 146,7 @@ func ManagePartialDomains(domains []db.Domain) templ.Component {
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("domain_eye_vis_%s", domain.Id)))
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("domain_eye_vis_%s", domain.ID)))
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}


@@ 154,7 154,7 @@ func ManagePartialDomains(domains []db.Domain) templ.Component {
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("domain_eye_hid_%s", domain.Id)))
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("domain_eye_hid_%s", domain.ID)))
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}


@@ 162,7 162,7 @@ func ManagePartialDomains(domains []db.Domain) templ.Component {
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("/manage/domains/%s/api_key", domain.Id)))
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("/manage/domains/%s/api_key", domain.ID)))
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}


@@ 170,7 170,7 @@ func ManagePartialDomains(domains []db.Domain) templ.Component {
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("/manage/domains/%s", domain.Id)))
				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(fmt.Sprintf("/manage/domains/%s", domain.ID)))
				if templ_7745c5c3_Err != nil {
					return templ_7745c5c3_Err
				}

M internal/config/config.go => internal/config/config.go +11 -7
@@ 1,13 1,16 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// Package config implements the global program config.
package config

import "git.src.quest/~skye/erya-go/util"

// FeluConfig is a global for accessing the global config.
var FeluConfig *config

type config struct {


@@ 19,7 22,7 @@ type config struct {
	// Initial email for the admin user, only used if no admin account (e.g. first boot)
	InitialAdminEmail string
	// Initial password for the admin user, only used if no admin account (e.g. first boot)
	InitialAdminPwd   string
	InitialAdminPwd string

	LogLevel string



@@ 33,17 36,18 @@ type config struct {
	DNSBindIP   string
	DNSBindPort int32
	// Domain pattern, no leading dot, but with trailing dot
	DNSPattern  string
	DNSPattern string
}

// InitConfig initializes the global program config from environment variables.
func InitConfig() {
	FeluConfig = &config {
	FeluConfig = &config{
		ServiceName: util.LoadEnvStr("FELU_SERVICE_NAME", "FeluDDNS"),

		APIUrl: util.LoadEnvStr("FELU_API_URL", "MUST_SET"),

		InitialAdminEmail: util.LoadEnvStr("FELU_INITIAL_ADMIN_EMAIL", "admin@example.com"),
		InitialAdminPwd: util.LoadEnvStr("FELU_INITIAL_ADMIN_PWD", "feluadmin"),
		InitialAdminPwd:   util.LoadEnvStr("FELU_INITIAL_ADMIN_PWD", "feluadmin"),

		LogLevel: util.LoadEnvStr("FELU_LOG_LEVEL", "info"),



@@ 53,8 57,8 @@ func InitConfig() {

		APIBindAddr: util.LoadEnvStr("FELU_API_BIND_ADDR", "0.0.0.0:8081"),

		DNSBindIP: util.LoadEnvStr("FELU_DNS_BIND_IP", "0.0.0.0"),
		DNSBindIP:   util.LoadEnvStr("FELU_DNS_BIND_IP", "0.0.0.0"),
		DNSBindPort: util.LoadEnvInt32("FELU_DNS_BIND_PORT", 53),
		DNSPattern: util.LoadEnvStr("FELU_DNS_PATTERN", "."),
		DNSPattern:  util.LoadEnvStr("FELU_DNS_PATTERN", "."),
	}
}

M internal/db/db.go => internal/db/db.go +10 -2
@@ 1,9 1,11 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// Package db implements database migrations and manipulation utilities.
package db

import (


@@ 11,15 13,18 @@ import (
	"log/slog"

	"git.src.quest/~skye/felu-ddns/internal/config"
	// nolint
	_ "github.com/mattn/go-sqlite3"
)

// DBConn is a global for accessing the database connection.
var DBConn *sql.DB

// InitDB initializes the programs database.
func InitDB() error {
	var err error
	DBConn, err = sql.Open("sqlite3",
		config.FeluConfig.DataDir + "felu.db?_foreign_keys=true")
		config.FeluConfig.DataDir+"felu.db?_foreign_keys=true")
	if err != nil {
		return err
	}


@@ 29,6 34,9 @@ func InitDB() error {
	return nil
}

// InitAdminUser initializes the programs admin user.
//
// Errors out if at least one admin user already exists.
func InitAdminUser() error {
	rows, err := DBConn.Query(`SELECT id FROM users WHERE is_admin = TRUE`)
	if err != nil {

M internal/db/domains.go => internal/db/domains.go +39 -28
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 13,23 14,26 @@ import (
	"github.com/oklog/ulid/v2"
)

// DomainOwner contains a domains owners information.
type DomainOwner struct {
	ID    string
	Email string
}

// Domain contains a domains information.
type Domain struct {
	Id      string
	ApiKey  string
	Domain  string
	A       string
	ID     string
	APIKey string
	Domain string
	A      string

	Owner DomainOwner
}

func FetchDomainsForUser(userId string) ([]Domain, error) {
// FetchDomainsForUser fetches all domains for a specific user.
func FetchDomainsForUser(userID string) ([]Domain, error) {
	rows, err := DBConn.Query(`SELECT id, apikey, ddns_domain, a_record
		FROM domains WHERE owner = $1`, userId)
		FROM domains WHERE owner = $1`, userID)
	if err != nil {
		return nil, err
	}


@@ 38,7 42,7 @@ func FetchDomainsForUser(userId string) ([]Domain, error) {
	var domains []Domain
	for rows.Next() {
		var domain Domain
		err = rows.Scan(&domain.Id, &domain.ApiKey, &domain.Domain, &domain.A)
		err = rows.Scan(&domain.ID, &domain.APIKey, &domain.Domain, &domain.A)
		if err != nil {
			return nil, err
		}


@@ 52,6 56,7 @@ func FetchDomainsForUser(userId string) ([]Domain, error) {
	return domains, nil
}

// FetchAllDomains fetches all domains.
func FetchAllDomains() ([]Domain, error) {
	rows, err := DBConn.Query(`SELECT id, ddns_domain, a_record, owner
		FROM domains`)


@@ 63,7 68,7 @@ func FetchAllDomains() ([]Domain, error) {
	var domains []Domain
	for rows.Next() {
		var domain Domain
		err = rows.Scan(&domain.Id, &domain.Domain, &domain.A, &domain.Owner.ID)
		err = rows.Scan(&domain.ID, &domain.Domain, &domain.A, &domain.Owner.ID)
		if err != nil {
			return nil, err
		}


@@ 84,9 89,10 @@ func FetchAllDomains() ([]Domain, error) {
	return domains, nil
}

// CreateDomain creates a domains.
func CreateDomain(domain string, aRecord string, owner string) error {
	ulid := ulid.Make().String()
	apikey := util.GenApiKey()
	apikey := util.GenAPIKey()
	_, 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 {


@@ 96,56 102,61 @@ func CreateDomain(domain string, aRecord string, owner string) error {
	return nil
}

func DeleteDomain(id string, user_id string) error {
	_, err := DBConn.Exec(`DELETE FROM domains WHERE id = $1 AND owner = $2`, id, user_id)
// DeleteDomain deletes a domain.
func DeleteDomain(id string, userID string) error {
	_, err := DBConn.Exec(`DELETE FROM domains WHERE id = $1 AND owner = $2`, id, userID)
	if err != nil {
		return err
	}
	return nil
}

func DeleteDomainsForUser(userId string) error {
	_, err := DBConn.Exec(`DELETE FROM domains WHERE owner = $1`, userId)
// DeleteDomainsForUser deletes all domains for a user.
func DeleteDomainsForUser(userID string) error {
	_, err := DBConn.Exec(`DELETE FROM domains WHERE owner = $1`, userID)
	if err != nil {
		return err
	}
	return nil
}

func FetchDomainARecord(ddns_domain string) (string, error) {
// FetchDomainARecord fetches the A record of a domain.
func FetchDomainARecord(ddnsDomain string) (string, error) {
	var aRecord string
	err := DBConn.QueryRow(`SELECT a_record FROM domains WHERE ddns_domain = $1`,
		ddns_domain).Scan(&aRecord)
		ddnsDomain).Scan(&aRecord)
	if err != nil {
		return "", err
	}
	return aRecord, nil
}

func UpdateDomainARecord(ddns_domain string, providedApiKey string, aRecord string) error {
	var domainApiKey string
// UpdateDomainARecord updates the A record of a domain.
func UpdateDomainARecord(ddnsDomain string, providedAPIKey string, aRecord string) error {
	var domainAPIKey string
	err := DBConn.QueryRow(`SELECT apikey FROM domains WHERE ddns_domain = $1`,
		ddns_domain).Scan(&domainApiKey)
		ddnsDomain).Scan(&domainAPIKey)
	if err != nil {
		return err
	}

	if domainApiKey != providedApiKey {
	if domainAPIKey != providedAPIKey {
		return errors.New("API key doesn't match")
	}

	_, err = DBConn.Exec(`UPDATE domains SET a_record = $1 WHERE ddns_domain = $2`,
		aRecord, ddns_domain)
		aRecord, ddnsDomain)
	if err != nil {
		return err
	}
	

	return nil
}

func UpdateDomainARecordManual(id string, userId string, aRecord string) error {
// UpdateDomainARecordManual updates the A record of a domain.
func UpdateDomainARecordManual(id string, userID string, aRecord string) error {
	_, err := DBConn.Exec(`UPDATE domains SET a_record = $1 WHERE id = $2 AND owner = $3`,
		aRecord, id, userId)
		aRecord, id, userID)
	if err != nil {
		return err
	}


@@ 153,10 164,10 @@ func UpdateDomainARecordManual(id string, userId string, aRecord string) error {
	return nil
}


func RefreshDomainApiKey(id string, user_id string) error {
	apiKey := util.GenApiKey()
	_, err := DBConn.Exec(`UPDATE domains SET apikey = $1 WHERE id = $2 AND owner = $3`, apiKey, id, user_id)
// RefreshDomainAPIKey refreshes the API key of a domain.
func RefreshDomainAPIKey(id string, userID string) error {
	apiKey := util.GenAPIKey()
	_, err := DBConn.Exec(`UPDATE domains SET apikey = $1 WHERE id = $2 AND owner = $3`, apiKey, id, userID)
	if err != nil {
		return err
	}

M internal/db/migrations.go => internal/db/migrations.go +6 -6
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 12,12 13,12 @@ import (
	"os"
)

var migrationsTable string = "schema_migrations"
var migrationsTable = "schema_migrations"

func runMigrations() {
	slog.Info("Running migrations")

	var schemaVersion int = 0
	var schemaVersion int

	schemaVersionQuery := fmt.Sprintf(
		`SELECT schema_version


@@ 36,7 37,7 @@ func runMigrations() {
	if schemaVersion != len(migrations) {
		for i := 0; i < len(migrations); i++ {
			if i >= schemaVersion {
				slog.Info("Running migration", slog.Int("version", i + 1)) // + 1 is just a visual thing
				slog.Info("Running migration", slog.Int("version", i+1)) // + 1 is just a visual thing
				_, err := DBConn.Exec(migrations[i])
				if err != nil {
					slog.Error("Migration failed to run!", slog.Int("version", i))


@@ 55,9 56,8 @@ func runMigrations() {
		if err != nil {
			slog.Error("Migrations ran, but was not able to create migration entry")
			os.Exit(1)
		} else {
			slog.Info("Migrations ran successfully")
		}
		slog.Info("Migrations ran successfully")
	} else {
		slog.Info("No migrations to run")
	}

M internal/db/users.go => internal/db/users.go +19 -7
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 14,6 15,7 @@ import (
	"github.com/oklog/ulid/v2"
)

// CreateUser creates a user.
func CreateUser(email string, pwd string) error {
	argon := argon2.DefaultConfig()
	encoded, err := argon.HashEncoded([]byte(pwd))


@@ 32,6 34,7 @@ func CreateUser(email string, pwd string) error {
	return nil
}

// CreateAdmin creates an admin user.
func CreateAdmin(email string, pwd string) error {
	argon := argon2.DefaultConfig()
	encoded, err := argon.HashEncoded([]byte(pwd))


@@ 50,17 53,19 @@ func CreateAdmin(email string, pwd string) error {
	return nil
}

// User contains a users relevant information.
type User struct {
	Id    string
	ID      string
	Email   string
	IsAdmin bool
}

// FetchUserWithCreds fetches a users information based on given credentials.
func FetchUserWithCreds(email string, pwd string) (*User, error) {
	user := User{ Email: email }
	user := User{Email: email}
	var encodedPwd string
	err := DBConn.QueryRow(`SELECT id, pwd FROM users WHERE email = $1`,
		email).Scan(&user.Id, &encodedPwd)
		email).Scan(&user.ID, &encodedPwd)
	if err == sql.ErrNoRows {
		return nil, errors.New("User not found")
	}


@@ 79,8 84,9 @@ func FetchUserWithCreds(email string, pwd string) (*User, error) {
	return &user, nil
}

func FetchUserWithId(id string) (*User, error) {
	user := User{ Id: id }
// FetchUserWithID fetches a users information.
func FetchUserWithID(id string) (*User, error) {
	user := User{ID: id}
	err := DBConn.QueryRow(`SELECT email, is_admin FROM users WHERE id = $1`,
		id).Scan(&user.Email, &user.IsAdmin)
	if err == sql.ErrNoRows {


@@ 93,6 99,7 @@ func FetchUserWithId(id string) (*User, error) {
	return &user, nil
}

// FetchAllUsers fetches all users.
func FetchAllUsers() ([]User, error) {
	rows, err := DBConn.Query(`SELECT id, email, is_admin FROM users`)
	if err != nil {


@@ 103,7 110,7 @@ func FetchAllUsers() ([]User, error) {
	var users []User
	for rows.Next() {
		var user User
		err = rows.Scan(&user.Id, &user.Email, &user.IsAdmin)
		err = rows.Scan(&user.ID, &user.Email, &user.IsAdmin)
		if err != nil {
			return nil, err
		}


@@ 117,6 124,7 @@ func FetchAllUsers() ([]User, error) {
	return users, nil
}

// FetchUserEmail fetches a users email address.
func FetchUserEmail(id string) (string, error) {
	var email string
	err := DBConn.QueryRow(`SELECT email FROM users WHERE id = $1`,


@@ 128,6 136,7 @@ func FetchUserEmail(id string) (string, error) {
	return email, nil
}

// DeleteUser deletes a user.
func DeleteUser(id string) error {
	err := DeleteDomainsForUser(id)
	if err != nil {


@@ 140,6 149,7 @@ func DeleteUser(id string) error {
	return nil
}

// UpdateUserEmail updates a users email address.
func UpdateUserEmail(id string, email string) error {
	_, err := DBConn.Exec(`UPDATE users SET email = $1 WHERE id = $2`,
		email, id)


@@ 149,6 159,7 @@ func UpdateUserEmail(id string, email string) error {
	return nil
}

// UpdateUserPassword updates a users password.
func UpdateUserPassword(id string, pwd string) error {
	argon := argon2.DefaultConfig()
	encoded, err := argon.HashEncoded([]byte(pwd))


@@ 165,6 176,7 @@ func UpdateUserPassword(id string, pwd string) error {
	return nil
}

// VerifyUserPassword verifies a users password.
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

M internal/dns/handle.go => internal/dns/handle.go +3 -2
@@ 1,14 1,15 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

package dns

import "github.com/miekg/dns"

func handleDnsRequest(w dns.ResponseWriter, r *dns.Msg) {
func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
	m := new(dns.Msg)
	m.SetReply(r)
	m.Compress = false

M internal/dns/query.go => internal/dns/query.go +4 -3
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

package dns

import (


@@ 33,8 34,8 @@ func handleARecord(q *dns.Question, m *dns.Msg, r *dns.Msg) {
			m.SetRcode(r, dns.RcodeNameError)
		} else {
			m.Answer = append(m.Answer, &dns.A{
				Hdr: dns.RR_Header{ Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60 },
				A: net.ParseIP(aRecord),
				Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
				A:   net.ParseIP(aRecord),
			})
		}
	} else {

M internal/dns/server.go => internal/dns/server.go +6 -3
@@ 1,9 1,11 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// Package dns implements all required DNS functionality for the program.
package dns

import (


@@ 11,12 13,13 @@ import (
	"github.com/miekg/dns"
)

// Run starts the DNS server.
func Run(addr string) error {
	dns.HandleFunc(config.FeluConfig.DNSPattern, handleDnsRequest)
	dns.HandleFunc(config.FeluConfig.DNSPattern, handleDNSRequest)

	server := &dns.Server{
		Addr: addr,
		Net: "udp",
		Net:  "udp",
	}

	return server.ListenAndServe()

M internal/handlers/admin.go => internal/handlers/admin.go +5 -1
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 13,18 14,21 @@ import (
	"github.com/gin-gonic/gin"
)

// ManageAdmin returns a gin handler
func ManageAdmin() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.HTML(http.StatusOK, "", components.ManageAdmin())
	}
}

// ManageAdminUsers returns a gin handler
func ManageAdminUsers() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.HTML(http.StatusOK, "", components.ManageAdminUsers())
	}
}

// ManageAdminDomains returns a gin handler
func ManageAdminDomains() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.HTML(http.StatusOK, "", components.ManageAdminDomains())

M internal/handlers/adminpartials.go => internal/handlers/adminpartials.go +4 -1
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 14,6 15,7 @@ import (
	"github.com/gin-gonic/gin"
)

// AdminPartialUsersList returns a gin handler
func AdminPartialUsersList() gin.HandlerFunc {
	return func(c *gin.Context) {
		users, err := db.FetchAllUsers()


@@ 27,6 29,7 @@ func AdminPartialUsersList() gin.HandlerFunc {
	}
}

// AdminPartialDomainsList returns a gin handler
func AdminPartialDomainsList() gin.HandlerFunc {
	return func(c *gin.Context) {
		users, err := db.FetchAllDomains()

M internal/handlers/auth.go => internal/handlers/auth.go +5 -2
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 19,6 20,7 @@ type postAuthLoginData struct {
	Password string `form:"password"`
}

// AuthLogin returns a gin handler
func AuthLogin(sm *scs.SessionManager) gin.HandlerFunc {
	return func(c *gin.Context) {
		data := &postAuthLoginData{}


@@ 34,12 36,13 @@ func AuthLogin(sm *scs.SessionManager) gin.HandlerFunc {
			return
		}

		sm.Put(c.Request.Context(), "user_id", user.Id)
		sm.Put(c.Request.Context(), "user_id", user.ID)

		c.Header("HX-Redirect", "/manage")
	}
}

// AuthLogout returns a gin handler
func AuthLogout(sm *scs.SessionManager) gin.HandlerFunc {
	return func(c *gin.Context) {
		sm.Destroy(c.Request.Context())

M internal/handlers/domains.go => internal/handlers/domains.go +15 -10
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 21,6 22,7 @@ type postDomainData struct {
	ARecord string `form:"a_record"`
}

// PostDomain returns a gin handler
func PostDomain() gin.HandlerFunc {
	return func(c *gin.Context) {
		data := &postDomainData{}


@@ 52,14 54,14 @@ func PostDomain() gin.HandlerFunc {
			return
		}

		userId, exists := c.Get("user_id")
		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))
		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")


@@ 71,9 73,10 @@ func PostDomain() gin.HandlerFunc {
	}
}

// PatchDomain returns a gin handler
func PatchDomain() gin.HandlerFunc {
	return func(c *gin.Context) {
		userId, exists := c.Get("user_id")
		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()


@@ 89,7 92,7 @@ func PatchDomain() gin.HandlerFunc {
				c.Abort()
				return
			}
			err := db.UpdateDomainARecordManual(id, userId.(string), aRecord)
			err := db.UpdateDomainARecordManual(id, userID.(string), aRecord)
			if err != nil {
				// FIXME: Handle better
				c.String(http.StatusInternalServerError, "Something went wrong while updating the a record")


@@ 102,18 105,19 @@ func PatchDomain() gin.HandlerFunc {
	}
}

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

		userId, exists := c.Get("user_id")
		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.DeleteDomain(id, userId.(string))
		err := db.DeleteDomain(id, userID.(string))
		if err != nil {
			// FIXME: Handle better
			c.String(http.StatusInternalServerError, "Something went wrong while deleting the domain")


@@ 125,18 129,19 @@ func DeleteDomain() gin.HandlerFunc {
	}
}

func RefreshDomainApiKey() gin.HandlerFunc {
// RefreshDomainAPIKey returns a gin handler
func RefreshDomainAPIKey() gin.HandlerFunc {
	return func(c *gin.Context) {
		id := c.Param("id")

		userId, exists := c.Get("user_id")
		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.RefreshDomainApiKey(id, userId.(string))
		err := db.RefreshDomainAPIKey(id, userID.(string))
		if err != nil {
			// FIXME: Handle better
			c.String(http.StatusInternalServerError, "Something went wrong while updating the api key")

M internal/handlers/index.go => internal/handlers/index.go +4 -1
@@ 1,9 1,11 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// Package handlers implements all request handlers.
package handlers

import (


@@ 13,6 15,7 @@ import (
	"github.com/gin-gonic/gin"
)

// Index returns a gin handler
func Index() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.HTML(http.StatusOK, "", components.Index())

M internal/handlers/login.go => internal/handlers/login.go +3 -1
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 13,6 14,7 @@ import (
	"github.com/gin-gonic/gin"
)

// Login returns a gin handler
func Login() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.HTML(http.StatusOK, "", components.Login())

M internal/handlers/manage.go => internal/handlers/manage.go +6 -3
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 14,22 15,24 @@ import (
	"github.com/gin-gonic/gin"
)

// Manage returns a gin handler
func Manage() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.HTML(http.StatusOK, "", components.Manage())
	}
}

// ManageUser returns a gin handler
func ManageUser() gin.HandlerFunc {
	return func(c *gin.Context) {
		user_id, exists := c.Get("user_id")
		userID, 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))
		user, err := db.FetchUserWithID(userID.(string))
		if err != nil {
			c.String(http.StatusInternalServerError, "This should not be possible, but don't quote me on that, S01E02")
			c.Abort()

M internal/handlers/managepartials.go => internal/handlers/managepartials.go +5 -3
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 14,16 15,17 @@ import (
	"github.com/gin-gonic/gin"
)

// ManagePartialDomains returns a gin handler
func ManagePartialDomains() gin.HandlerFunc {
	return func(c *gin.Context) {
		user_id, exists := c.Get("user_id")
		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
		}

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

M internal/handlers/user.go => internal/handlers/user.go +12 -8
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 20,6 21,7 @@ type postUserPasswordData struct {
	ConfirmNewPassword string `form:"confirm_new_password"`
}

// PostUserPassword returns a gin handler
func PostUserPassword() gin.HandlerFunc {
	return func(c *gin.Context) {
		data := &postUserPasswordData{}


@@ 40,20 42,20 @@ func PostUserPassword() gin.HandlerFunc {
			return
		}

		userId, exists := c.Get("user_id")
		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) {
		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)
		err := db.UpdateUserPassword(userID.(string), data.NewPassword)
		if err != nil {
			// FIXME: Handle better
			c.String(http.StatusInternalServerError, "Something went wrong while deleting the user")


@@ 69,6 71,7 @@ type postUserEmailData struct {
	Email string `form:"email"`
}

// PostUserEmail returns a gin handler
func PostUserEmail() gin.HandlerFunc {
	return func(c *gin.Context) {
		data := &postUserEmailData{}


@@ 84,14 87,14 @@ func PostUserEmail() gin.HandlerFunc {
			return
		}

		userId, exists := c.Get("user_id")
		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)
		err := db.UpdateUserEmail(userID.(string), data.Email)
		if err != nil {
			// FIXME: Handle better
			c.String(http.StatusInternalServerError, "Something went wrong while deleting the user")


@@ 103,16 106,17 @@ func PostUserEmail() gin.HandlerFunc {
	}
}

// DeleteUser returns a gin handler
func DeleteUser(sm *scs.SessionManager) gin.HandlerFunc {
	return func(c *gin.Context) {
		userId, exists := c.Get("user_id")
		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))
		err := db.DeleteUser(userID.(string))
		if err != nil {
			// FIXME: Handle better
			c.String(http.StatusInternalServerError, "Something went wrong while deleting the user")

M internal/handlers/users.go => internal/handlers/users.go +3 -1
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 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 (


@@ 18,6 19,7 @@ type postUserData struct {
	InitialPwd string `form:"initial_pwd"`
}

// PostUser returns a gin handler
func PostUser() gin.HandlerFunc {
	return func(c *gin.Context) {
		data := &postUserData{}

M internal/log/ginformat.go => internal/log/ginformat.go +3 -1
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

package log

import (


@@ 13,6 14,7 @@ import (
	"github.com/gin-gonic/gin"
)

// GinFormat is a custom gin log formatting function.
func GinFormat(param gin.LogFormatterParams, router string) string {
	var statusColor, methodColor, resetColor string
	if param.IsOutputColor() {

M internal/log/log.go => internal/log/log.go +6 -3
@@ 1,9 1,11 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// Package log implements logging utilities.
package log

import (


@@ 12,6 14,7 @@ import (
	"path/filepath"
)

// InitDefaultLogger initializes the default logger.
func InitDefaultLogger(logLevel string) {
	var feluLogLevel = new(slog.LevelVar)
	switch logLevel {


@@ 35,8 38,8 @@ func InitDefaultLogger(logLevel string) {
	}

	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		Level: feluLogLevel,
		AddSource: true,
		Level:       feluLogLevel,
		AddSource:   true,
		ReplaceAttr: replace,
	}))


M internal/middlewares/admin.go => internal/middlewares/admin.go +5 -3
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

package middlewares

import (


@@ 13,11 14,12 @@ import (
	"github.com/gin-gonic/gin"
)

// AdminOnly returns a gin middleware for checking if a user is an admin.
func AdminOnly() gin.HandlerFunc {
	return func(c *gin.Context) {
		user_id, exists := c.Get("user_id")
		userID, exists := c.Get("user_id")
		if exists {
			user, err := db.FetchUserWithId(user_id.(string))
			user, err := db.FetchUserWithID(userID.(string))
			if err == nil {
				if user.IsAdmin {
					c.Next()

M internal/middlewares/auth.go => internal/middlewares/auth.go +6 -4
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

package middlewares

import (


@@ 13,16 14,17 @@ import (
	"github.com/gin-gonic/gin"
)

// SessionExists returns a gin middleware for checking if a session exists.
func SessionExists(sm *scs.SessionManager) gin.HandlerFunc {
	return func(c *gin.Context) {
		user_id := sm.Get(c.Request.Context(), "user_id")
		if user_id != nil {
		userID := sm.Get(c.Request.Context(), "user_id")
		if userID != nil {
			if c.Request.URL.Path == "/login" {
				c.Redirect(http.StatusTemporaryRedirect, "/manage")
				c.Abort()
			} else {
				// Set user_id in context, if needed later (e.g. AdminOnly middleware)
				c.Set("user_id", user_id)
				c.Set("user_id", userID)
				// TODO: Validate in db?
				c.Next()
			}

A internal/middlewares/middlewares.go => internal/middlewares/middlewares.go +9 -0
@@ 0,0 1,9 @@
/*
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// Package middlewares implements gin middlewares.
package middlewares

M internal/renderer/renderer.go => internal/renderer/renderer.go +9 -3
@@ 1,9 1,11 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// Package renderer implements templ rendering.
package renderer

import (


@@ 14,12 16,14 @@ import (
	"github.com/gin-gonic/gin/render"
)

// TemplRender holds the required info for rendering templ components.
type TemplRender struct {
	Code int
	Data templ.Component
}

func (t TemplRender) Render (w http.ResponseWriter) error {
// Render renders templ templates to HTML (and friends).
func (t TemplRender) Render(w http.ResponseWriter) error {
	w.WriteHeader(t.Code)
	if t.Data != nil {
		return t.Data.Render(context.Background(), w)


@@ 28,11 32,13 @@ func (t TemplRender) Render (w http.ResponseWriter) error {
	return nil
}

// WriteContentType sets the Content-Type header.
func (t TemplRender) WriteContentType(w http.ResponseWriter) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
}

func (t *TemplRender) Instance(name string, data interface{}) render.Render {
// Instance returns an instance of the templ renderer.
func (t *TemplRender) Instance( /* name */ _ string, data interface{}) render.Render {
	if templData, ok := data.(templ.Component); ok {
		return &TemplRender{
			Code: http.StatusOK,

M internal/routers/api.go => internal/routers/api.go +3 -1
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

package routers

import (


@@ 14,6 15,7 @@ import (
	"github.com/gin-gonic/gin"
)

// SetupAPIRouter returns the API router.
func SetupAPIRouter(version string) *gin.Engine {
	r := gin.New()
	r.Use(gin.Recovery())

M internal/routers/frontend.go => internal/routers/frontend.go +4 -2
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

package routers

import (


@@ 15,6 16,7 @@ import (
	"github.com/gin-gonic/gin"
)

// SetupFrontendRouter returns the frontend router.
func SetupFrontendRouter(sm *scs.SessionManager) *gin.Engine {
	r := gin.New()
	r.Use(gin.Recovery())


@@ 46,7 48,7 @@ func SetupFrontendRouter(sm *scs.SessionManager) *gin.Engine {
		manage.POST("/domains", handlers.PostDomain())
		manage.PATCH("/domains/:id", handlers.PatchDomain())
		manage.DELETE("/domains/:id", handlers.DeleteDomain())
		manage.POST("/domains/:id/api_key", handlers.RefreshDomainApiKey())
		manage.POST("/domains/:id/api_key", handlers.RefreshDomainAPIKey())

		manage.GET("/partials/domains", handlers.ManagePartialDomains())
	}

A internal/routers/routers.go => internal/routers/routers.go +9 -0
@@ 0,0 1,9 @@
/*
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// Package routers implements both the frontend and API routers.
package routers

M internal/util/apikey.go => internal/util/apikey.go +5 -3
@@ 1,20 1,22 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

package util

import "math/rand"

const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

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

M internal/util/check.go => internal/util/check.go +3 -1
@@ 1,9 1,10 @@
/*
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

package util

import (


@@ 11,6 12,7 @@ import (
	"net"
)

// CheckARecord verifies the validity of an A record.
func CheckARecord(aRecord string) error {
	if net.ParseIP(aRecord).To4() == nil {
		return errors.New("Invalid A record")

A internal/util/util.go => internal/util/util.go +9 -0
@@ 0,0 1,9 @@
/*
 * Copyright (C) 2024 Jonni Liljamo <jonni@liljamo.com>
 *
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */

// Package util implements a variety of small utility functions.
package util