package main import ( "encoding/json" "io" "io/fs" "net/http" "git.yetaga.in/alazyreader/library/media" "tailscale.com/client/tailscale" ) type Router struct { static fs.FS lib Library rcol RecordCollection } type AdminRouter struct { static fs.FS lib Library ts *tailscale.LocalClient } type handler map[string]func() func (h handler) Handle(w http.ResponseWriter, req *http.Request) { if f, ok := h[req.Method]; ok { f() return } badMethod(w) } func writeJSONerror(w http.ResponseWriter, err string, status int) { writeJSON(w, struct{ Status, Reason string }{Status: "error", Reason: err}, status) } func writeJSON(w http.ResponseWriter, b any, status int) { bytes, err := json.Marshal(b) if err != nil { writeJSONerror(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) w.Write(bytes) w.Write([]byte("\n")) } func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/records": handler{ http.MethodGet: func() { getRecords(router.rcol, w, r) }, }.Handle(w, r) case "/api/books": handler{ http.MethodGet: func() { getBooks(router.lib, w, r) }, }.Handle(w, r) default: static(router.static).ServeHTTP(w, r) } } func (router *AdminRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/whoami": handler{ http.MethodGet: func() { getWhoAmI(router.ts, w, r) }, }.Handle(w, r) case "/api/books": handler{ http.MethodGet: func() { getBooks(router.lib, w, r) }, http.MethodPost: func() { addBook(router.lib, w, r) }, http.MethodDelete: func() { deleteBook(router.lib, w, r) }, }.Handle(w, r) default: static(router.static).ServeHTTP(w, r) } } func badMethod(w http.ResponseWriter) { writeJSONerror(w, "method not supported", http.StatusMethodNotAllowed) } func getBooks(l Library, w http.ResponseWriter, r *http.Request) { books, err := l.GetAllBooks(r.Context()) if err != nil { writeJSONerror(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, books, http.StatusOK) } func addBook(l Library, w http.ResponseWriter, r *http.Request) { if r.Body == nil { writeJSONerror(w, "no body provided", http.StatusBadRequest) return } defer r.Body.Close() b, err := io.ReadAll(r.Body) if err != nil { writeJSONerror(w, "error reading body", http.StatusBadRequest) return } book := &media.Book{} err = json.Unmarshal(b, book) if err != nil { writeJSONerror(w, "error parsing body", http.StatusBadRequest) return } err = l.AddBook(r.Context(), book) if err != nil { writeJSONerror(w, "error parsing body", http.StatusBadRequest) return } w.WriteHeader(http.StatusAccepted) } func deleteBook(l Library, w http.ResponseWriter, r *http.Request) { if r.Body == nil { writeJSONerror(w, "no body provided", http.StatusBadRequest) return } defer r.Body.Close() b, err := io.ReadAll(r.Body) if err != nil { writeJSONerror(w, "error reading body", http.StatusBadRequest) return } book := &media.Book{} err = json.Unmarshal(b, book) if err != nil { writeJSONerror(w, "error parsing body", http.StatusBadRequest) return } err = l.DeleteBook(r.Context(), book) if err != nil { writeJSONerror(w, "error deleting book", http.StatusInternalServerError) return } w.WriteHeader(http.StatusAccepted) } func getRecords(l RecordCollection, w http.ResponseWriter, r *http.Request) { records, err := l.GetAllRecords(r.Context()) if err != nil { writeJSONerror(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, records, http.StatusOK) } func getWhoAmI(ts *tailscale.LocalClient, w http.ResponseWriter, r *http.Request) { whois, err := ts.WhoIs(r.Context(), r.RemoteAddr) if err != nil { writeJSONerror(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, whois.UserProfile, http.StatusOK) } func static(f fs.FS) http.Handler { return http.FileServer(http.FS(f)) }