Files
2026-03-12 20:09:16 +01:00

212 lines
5.8 KiB
Go

package httpapi
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"time"
"netisjwt/internal/jws"
"netisjwt/internal/statuslist"
"netisjwt/internal/storage"
)
type Handler struct {
store storage.Store
privatePEM []byte
now func() time.Time
}
// NewObravnalnik ustvari HTTP obravnalnik za status API.
func NewObravnalnik(store storage.Store, privatePEM []byte) *Handler {
return &Handler{
store: store,
privatePEM: privatePEM,
now: time.Now,
}
}
// obravnavajStatusRoot obdela /api/status za GET in POST.
func (h *Handler) obravnavajStatusRoot(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
ids, err := h.store.SeznamIDjev(r.Context())
if err != nil {
zapisiNapako(w, http.StatusInternalServerError, err)
return
}
zapisiJSON(w, http.StatusOK, map[string]any{"statusIds": ids})
case http.MethodPost:
statusID, err := newStatusID()
if err != nil {
zapisiNapako(w, http.StatusInternalServerError, err)
return
}
if err := h.store.Ustvari(r.Context(), statusID, statuslist.New()); err != nil {
zapisiNapako(w, http.StatusInternalServerError, err)
return
}
zapisiJSON(w, http.StatusCreated, map[string]any{"statusId": statusID})
default:
zapisiMethodNotAllowed(w)
}
}
// obravnavajStatusPoPoti razcleni path parametre statusId in index.
func (h *Handler) obravnavajStatusPoPoti(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/status/")
path = strings.Trim(path, "/")
if path == "" {
http.NotFound(w, r)
return
}
parts := strings.Split(path, "/")
if len(parts) == 1 {
if r.Method != http.MethodPost {
zapisiMethodNotAllowed(w)
return
}
h.obravnavajDodajStatus(w, r, parts[0])
return
}
if len(parts) == 2 {
index, err := strconv.Atoi(parts[1])
if err != nil {
zapisiNapako(w, http.StatusBadRequest, errors.New("index must be integer"))
return
}
h.obravnavajIndeksiranStatus(w, r, parts[0], index)
return
}
http.NotFound(w, r)
}
// obravnavajDodajStatus doda novo stanje v status listo.
func (h *Handler) obravnavajDodajStatus(w http.ResponseWriter, r *http.Request, statusID string) {
list, err := h.store.Dobi(r.Context(), statusID)
if errors.Is(err, storage.ErrNotFound) {
zapisiNapako(w, http.StatusNotFound, err)
return
}
if err != nil {
zapisiNapako(w, http.StatusInternalServerError, err)
return
}
index := list.Dodaj(false)
if err := h.store.Posodobi(r.Context(), statusID, list); err != nil {
zapisiNapako(w, http.StatusInternalServerError, err)
return
}
zapisiJSON(w, http.StatusCreated, map[string]any{"index": index})
}
// obravnavajIndeksiranStatus vodi GET/PUT/DELETE za en status index.
func (h *Handler) obravnavajIndeksiranStatus(w http.ResponseWriter, r *http.Request, statusID string, index int) {
switch r.Method {
case http.MethodGet:
h.obravnavajDobiZeton(w, r, statusID, index)
case http.MethodPut:
h.obravnavajNastavi(w, r, statusID, index, true)
case http.MethodDelete:
h.obravnavajNastavi(w, r, statusID, index, false)
default:
zapisiMethodNotAllowed(w)
}
}
// obravnavajNastavi nastavi vrednost enega status bita.
func (h *Handler) obravnavajNastavi(w http.ResponseWriter, r *http.Request, statusID string, index int, value bool) {
list, err := h.store.Dobi(r.Context(), statusID)
if errors.Is(err, storage.ErrNotFound) {
zapisiNapako(w, http.StatusNotFound, err)
return
}
if err != nil {
zapisiNapako(w, http.StatusInternalServerError, err)
return
}
if err := list.Nastavi(index, value); err != nil {
zapisiNapako(w, http.StatusBadRequest, err)
return
}
if err := h.store.Posodobi(r.Context(), statusID, list); err != nil {
zapisiNapako(w, http.StatusInternalServerError, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
// obravnavajDobiZeton vrne podpisan JWS token za podan index.
func (h *Handler) obravnavajDobiZeton(w http.ResponseWriter, r *http.Request, statusID string, index int) {
list, err := h.store.Dobi(r.Context(), statusID)
if errors.Is(err, storage.ErrNotFound) {
zapisiNapako(w, http.StatusNotFound, err)
return
}
if err != nil {
zapisiNapako(w, http.StatusInternalServerError, err)
return
}
if _, err := list.Dobi(index); err != nil {
zapisiNapako(w, http.StatusBadRequest, err)
return
}
issuer := izdajaIzdajatelja(r, statusID)
claims, err := jws.SestaviTrditve(issuer, list, index, h.now())
if err != nil {
zapisiNapako(w, http.StatusInternalServerError, err)
return
}
token, err := jws.UstvariZetonIzPEM(h.privatePEM, claims)
if err != nil {
zapisiNapako(w, http.StatusInternalServerError, err)
return
}
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(token))
}
// izdajaIzdajatelja pripravi iss vrednost za JWT trditve.
func izdajaIzdajatelja(r *http.Request, statusID string) string {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
return scheme + "://" + r.Host + "/api/status/" + statusID
}
// newStatusID ustvari kratek URL-safe identifikator seznama.
func newStatusID() (string, error) {
raw := make([]byte, 6)
if _, err := rand.Read(raw); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(raw), nil
}
// zapisiMethodNotAllowed vrne standarden odgovor 405.
func zapisiMethodNotAllowed(w http.ResponseWriter) {
zapisiNapako(w, http.StatusMethodNotAllowed, errors.New("method not allowed"))
}
// zapisiNapako serializira napako v JSON odziv.
func zapisiNapako(w http.ResponseWriter, code int, err error) {
zapisiJSON(w, code, map[string]any{"error": err.Error()})
}
// zapisiJSON zapise poljuben objekt kot JSON odgovor.
func zapisiJSON(w http.ResponseWriter, code int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(payload)
}