Initial
This commit is contained in:
211
internal/httpapi/handlers.go
Normal file
211
internal/httpapi/handlers.go
Normal file
@@ -0,0 +1,211 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user