DEVELOPMENT ENVIRONMENT

~liljamo/felu

21812453f65234c90990bd07cc02539b5a1ee249 — Jonni Liljamo 1 year, 1 month ago 9527d36
feat: api endpoint to update a record
4 files changed, 99 insertions(+), 7 deletions(-)

A internal/api/update.go
M internal/db/domains.go
M internal/routers/api.go
R internal/{api/key.go => util/apikey.go}
A internal/api/update.go => internal/api/update.go +59 -0
@@ 0,0 1,59 @@
/*
 * 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 (
	"net/http"

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

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",
			})
			c.Abort()
			return
		}

		apiKey := c.Query("api_key")
		if apiKey == "" {
			c.JSON(http.StatusBadRequest, gin.H{
				"status": "error",
				"error": "no api key was provided",
			})
			c.Abort()
			return
		}

		aRecord := c.Query("record")
		if aRecord == "" {
			aRecord = c.ClientIP()
		}

		err := db.UpdateDomainARecord(domain, apiKey, aRecord)
		if err != nil {
			// FIXME: Handle better, "bad api key" is just the most likely scenario
			c.JSON(http.StatusBadRequest, gin.H{
				"status": "error",
				"error": "bad api key",
			})
			c.Abort()
			return
		}

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

M internal/db/domains.go => internal/db/domains.go +25 -2
@@ 7,7 7,9 @@
package db

import (
	"git.src.quest/~skye/felu-ddns/internal/api"
	"errors"

	"git.src.quest/~skye/felu-ddns/internal/util"
	"github.com/oklog/ulid/v2"
)



@@ 45,7 47,7 @@ func FetchDomainsForUser(userId string) ([]Domain, error) {

func CreateDomain(domain string, aRecord string, owner string) error {
	ulid := ulid.Make().String()
	apikey := api.GenKey()
	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 {


@@ 80,3 82,24 @@ func FetchDomainARecord(ddns_domain string) (string, error) {
	}
	return aRecord, nil
}

func UpdateDomainARecord(ddns_domain string, providedApiKey string, aRecord string) error {
	var domainApiKey string
	err := DBConn.QueryRow(`SELECT apikey FROM domains WHERE ddns_domain = $1`,
		ddns_domain).Scan(&domainApiKey)
	if err != nil {
		return err
	}

	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)
	if err != nil {
		return err
	}
	
	return nil
}

M internal/routers/api.go => internal/routers/api.go +13 -3
@@ 9,6 9,7 @@ package routers
import (
	"net/http"

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


@@ 16,9 17,16 @@ import (
func SetupAPIRouter(version string) *gin.Engine {
	r := gin.New()
	r.Use(gin.Recovery())
	r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
		return log.GinFormat(param, "api")
	}))

	var logger = gin.LoggerWithConfig(gin.LoggerConfig{
		Formatter: func(param gin.LogFormatterParams) string {
			return log.GinFormat(param, "api")
		},
		// NOTE: Don't log paths with query params, also update paths are used _a lot_
		SkipPaths: []string{"/update/a"},
	})

	r.Use(logger)

	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{


@@ 32,5 40,7 @@ func SetupAPIRouter(version string) *gin.Engine {
		})
	})

	r.GET("/update/a", api.UpdateA())

	return r
}

R internal/api/key.go => internal/util/apikey.go +2 -2
@@ 4,13 4,13 @@
 * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for
 * more information.
 */
package api
package util

import "math/rand"

const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

func GenKey() string {
func GenApiKey() string {
	// NOTE: "Good enough"
	b := make([]byte, 48)
	for i := range b {