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 struct { get func() post func() put func() delete func() } func (h handler) Handle(w http.ResponseWriter, req *http.Request) { if req.Method == http.MethodHead && h.get != nil { h.get() } else if req.Method == http.MethodGet && h.get != nil { h.get() } else if req.Method == http.MethodPost && h.post != nil { h.post() } else if req.Method == http.MethodPut && h.put != nil { h.put() } else if req.Method == http.MethodDelete && h.delete != nil { h.delete() } else { 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{ get: func() { getRecords(router.rcol, w, r) }, }.Handle(w, r) case "/api/books": handler{ get: 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{ get: func() { getWhoAmI(router.ts, w, r) }, }.Handle(w, r) case "/api/books": handler{ get: func() { getBooks(router.lib, w, r) }, post: func() { addBook(router.lib, w, r) }, delete: 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)) }