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) }