212 lines
5.8 KiB
Go
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)
|
|
}
|