diff options
Diffstat (limited to 'api')
| -rw-r--r-- | api/dto/entry.go | 9 | ||||
| -rw-r--r-- | api/dto/response.go | 38 | ||||
| -rw-r--r-- | api/dto/util.go | 54 | ||||
| -rw-r--r-- | api/handlers/html.go | 27 | ||||
| -rw-r--r-- | api/handlers/record.go | 35 | ||||
| -rw-r--r-- | api/middleware/cors.go | 12 | ||||
| -rw-r--r-- | api/router.go | 27 |
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 +} |
