summaryrefslogtreecommitdiffstats
path: root/api
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.net>2025-09-09 22:44:09 +0100
committerLeonardo Bishop <me@leonardobishop.net>2025-09-09 22:44:09 +0100
commitf1741a7faa9538e9b12ac60e0fbf6c7721a36059 (patch)
tree2b70b14fc20a8520718910a0cd2e0134790c6f53 /api
Initial commit
Diffstat (limited to 'api')
-rw-r--r--api/dto/entry.go9
-rw-r--r--api/dto/response.go38
-rw-r--r--api/dto/util.go54
-rw-r--r--api/handlers/html.go27
-rw-r--r--api/handlers/record.go35
-rw-r--r--api/middleware/cors.go12
-rw-r--r--api/router.go27
7 files changed, 202 insertions, 0 deletions
diff --git a/api/dto/entry.go b/api/dto/entry.go
new file mode 100644
index 0000000..85e39f3
--- /dev/null
+++ b/api/dto/entry.go
@@ -0,0 +1,9 @@
+package dto
+
+type CreateEntryRequest struct {
+ Title string `json:"title"`
+ Kind string `json:"kind"`
+ Url string `json:"url"`
+ Description string `json:"description"`
+ Token string `json:"token"`
+}
diff --git a/api/dto/response.go b/api/dto/response.go
new file mode 100644
index 0000000..2c13218
--- /dev/null
+++ b/api/dto/response.go
@@ -0,0 +1,38 @@
+package dto
+
+import "fmt"
+
+type Response interface {
+ Error() string
+ Status() int
+}
+
+type OkResponse struct {
+ Code int `json:"code"`
+ Data interface{} `json:"data,omitempty"`
+}
+
+var _ Response = (*OkResponse)(nil)
+
+type ErrorResponse struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+}
+
+var _ Response = (*ErrorResponse)(nil)
+
+func (r *OkResponse) Status() int {
+ return r.Code
+}
+
+func (r *OkResponse) Error() string {
+ return fmt.Sprintf("HTTP status %d", r.Code)
+}
+
+func (r *ErrorResponse) Status() int {
+ return r.Code
+}
+
+func (r *ErrorResponse) Error() string {
+ return fmt.Sprintf("HTTP status %d: %s", r.Code, r.Message)
+}
diff --git a/api/dto/util.go b/api/dto/util.go
new file mode 100644
index 0000000..651f026
--- /dev/null
+++ b/api/dto/util.go
@@ -0,0 +1,54 @@
+package dto
+
+import (
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "net/http"
+
+ "github.com/go-playground/validator/v10"
+)
+
+var validate = validator.New(validator.WithRequiredStructEnabled())
+
+func ReadDto(r *http.Request, o interface{}) Response {
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(o); err != nil {
+ return &ErrorResponse{
+ Code: http.StatusBadRequest,
+ Message: fmt.Errorf("Invalid request (%w)", err).Error(),
+ }
+ }
+
+ if err := validate.Struct(o); err != nil {
+ return &ErrorResponse{
+ Code: http.StatusBadRequest,
+ Message: err.Error(),
+ }
+ }
+
+ return nil
+}
+
+func WrapResponseFunc(dtoFunc func(http.ResponseWriter, *http.Request) error) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ WriteDto(w, r, dtoFunc(w, r))
+ }
+}
+
+func WriteDto(w http.ResponseWriter, r *http.Request, err error) {
+ if o, ok := err.(Response); ok {
+ data, err := json.Marshal(o)
+ if err != nil {
+ w.WriteHeader(500)
+ slog.Error("could not serialise JSON", "error", err)
+ return
+ }
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(o.Status())
+ w.Write(data)
+ } else {
+ w.WriteHeader(500)
+ slog.Error("internal server error handling request", "error", err)
+ }
+}
diff --git a/api/handlers/html.go b/api/handlers/html.go
new file mode 100644
index 0000000..dd5d4e8
--- /dev/null
+++ b/api/handlers/html.go
@@ -0,0 +1,27 @@
+package handlers
+
+import (
+ "net/http"
+
+ "git.leonardobishop.net/history/pkg/entries"
+ "git.leonardobishop.net/history/pkg/html"
+)
+
+func GetEntriesHtml(entriesService entries.Service, htmlService html.Service) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ entries, err := entriesService.GetEntries()
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ html, err := htmlService.GenerateHtml(entries)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/html;charset=UTF-8")
+ w.Write([]byte(html))
+ }
+}
diff --git a/api/handlers/record.go b/api/handlers/record.go
new file mode 100644
index 0000000..0f98f11
--- /dev/null
+++ b/api/handlers/record.go
@@ -0,0 +1,35 @@
+package handlers
+
+import (
+ "crypto/subtle"
+ "net/http"
+
+ "git.leonardobishop.net/history/api/dto"
+ "git.leonardobishop.net/history/pkg/entries"
+)
+
+func RecordEntry(service entries.Service, token string) http.HandlerFunc {
+ return dto.WrapResponseFunc(func(w http.ResponseWriter, r *http.Request) error {
+ var request dto.CreateEntryRequest
+ if err := dto.ReadDto(r, &request); err != nil {
+ return err
+ }
+
+ if subtle.ConstantTimeCompare([]byte(token), []byte(request.Token)) != 1 {
+ return &dto.ErrorResponse{
+ Code: http.StatusForbidden,
+ Message: "Forbidden",
+ }
+ }
+
+ entry, err := service.CreateEntry(request.Title, request.Kind, request.Url, request.Description)
+ if err != nil {
+ return err
+ }
+
+ return &dto.OkResponse{
+ Code: http.StatusCreated,
+ Data: entry,
+ }
+ })
+}
diff --git a/api/middleware/cors.go b/api/middleware/cors.go
new file mode 100644
index 0000000..926c1ed
--- /dev/null
+++ b/api/middleware/cors.go
@@ -0,0 +1,12 @@
+package middleware
+
+import "net/http"
+
+func Cors(next http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+
+ next(w, r)
+ }
+}
diff --git a/api/router.go b/api/router.go
new file mode 100644
index 0000000..9ea7ac0
--- /dev/null
+++ b/api/router.go
@@ -0,0 +1,27 @@
+package api
+
+import (
+ "net/http"
+
+ "git.leonardobishop.net/history/api/handlers"
+ "git.leonardobishop.net/history/api/middleware"
+ "git.leonardobishop.net/history/internal/config"
+ "git.leonardobishop.net/history/pkg/entries"
+ "git.leonardobishop.net/history/pkg/html"
+)
+
+type ApiServices struct {
+ EntiresService entries.Service
+ HtmlService html.Service
+
+ Config config.Config
+}
+
+func NewServer(api ApiServices) *http.ServeMux {
+ mux := http.NewServeMux()
+
+ mux.HandleFunc("POST /record", middleware.Cors(handlers.RecordEntry(api.EntiresService, api.Config.Token)))
+ mux.HandleFunc("GET /html", handlers.GetEntriesHtml(api.EntiresService, api.HtmlService))
+
+ return mux
+}