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 writeError[T any](t T, err error) func(w http.ResponseWriter) (T, bool) { return func(w http.ResponseWriter) (T, bool) { if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return t, err == nil } } func writeNoBody(w http.ResponseWriter, status int) { w.WriteHeader(status) } func writeJSON(w http.ResponseWriter, b any, status int) { bytes, err := json.Marshal(b) if err != nil { http.Error(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) { writeJSON(w, struct{ Error string }{Error: "method not supported"}, http.StatusMethodNotAllowed) } func getBooks(l Library, w http.ResponseWriter, r *http.Request) { books, err := l.GetAllBooks(r.Context()) if err != nil { http.Error(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 { http.Error(w, "no body provided", http.StatusBadRequest) return } defer r.Body.Close() b, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "error reading body", http.StatusBadRequest) return } book := &media.Book{} err = json.Unmarshal(b, book) if err != nil { http.Error(w, "error parsing body", http.StatusBadRequest) return } err = l.AddBook(r.Context(), book) if err != nil { http.Error(w, "error parsing body", http.StatusBadRequest) return } writeNoBody(w, http.StatusAccepted) } func deleteBook(l Library, w http.ResponseWriter, r *http.Request) { if r.Body == nil { http.Error(w, "no body provided", http.StatusBadRequest) return } defer r.Body.Close() b, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "error reading body", http.StatusBadRequest) return } book := &media.Book{} err = json.Unmarshal(b, book) if err != nil { http.Error(w, "error parsing body", http.StatusBadRequest) return } err = l.DeleteBook(r.Context(), book) if err != nil { http.Error(w, "error parsing body", http.StatusBadRequest) return } writeNoBody(w, http.StatusAccepted) } func getRecords(l RecordCollection, w http.ResponseWriter, r *http.Request) { records, err := l.GetAllRecords(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, records, http.StatusOK) } func getWhoAmI(ts *tailscale.LocalClient, w http.ResponseWriter, r *http.Request) { whois, ok := writeError(ts.WhoIs(r.Context(), r.RemoteAddr))(w) if !ok { return } writeJSON(w, struct { Username string `json:"Username"` DisplayName string `json:"DisplayName"` ProfilePicURL string `json:"ProfilePicURL"` }{ Username: whois.UserProfile.LoginName, DisplayName: whois.UserProfile.DisplayName, ProfilePicURL: whois.UserProfile.ProfilePicURL, }, http.StatusOK) } func static(f fs.FS) http.Handler { return http.FileServer(http.FS(f)) }