package main import ( "encoding/json" "fmt" "io" "io/fs" "log" "net/http" "git.yetaga.in/alazyreader/library/media" "tailscale.com/client/tailscale" ) type Router struct { static fs.FS lib Library rcol RecordCollection query Query ts *tailscale.LocalClient isAdmin bool } type path map[string]func() func (h path) ServeHTTP(w http.ResponseWriter, r *http.Request) { if f, ok := h[r.Method]; ok { f() return } writeJSONerror(w, "method not supported", http.StatusMethodNotAllowed) } func writeJSONerror(w http.ResponseWriter, err string, status int) { log.Println(err) 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/mode": path{ http.MethodGet: func() { writeJSON(w, struct{ Admin bool }{Admin: router.isAdmin}, http.StatusOK) }, }.ServeHTTP(w, r) case "/api/whoami": if !router.isAdmin { http.NotFoundHandler().ServeHTTP(w, r) return } path{ http.MethodGet: func() { getWhoAmI(router.ts, w, r) }, }.ServeHTTP(w, r) case "/api/records": path{ http.MethodGet: func() { getRecords(router.rcol, w, r) }, }.ServeHTTP(w, r) case "/api/books": p := path{ http.MethodGet: func() { getBooks(router.lib, w, r) }, } if router.isAdmin { p[http.MethodPost] = func() { addBook(router.lib, w, r) } p[http.MethodDelete] = func() { deleteBook(router.lib, w, r) } } p.ServeHTTP(w, r) case "/api/query": if !router.isAdmin { http.NotFoundHandler().ServeHTTP(w, r) return } path{ http.MethodPost: func() { lookupBook(router.query, w, r) }, }.ServeHTTP(w, r) default: static(router.static).ServeHTTP(w, r) } } 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) { book, err := ReadBody[media.Book](r.Body) if err != nil { writeJSONerror(w, err.Error(), http.StatusBadRequest) return } if err = l.AddBook(r.Context(), book); err != nil { writeJSONerror(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusAccepted) } func deleteBook(l Library, w http.ResponseWriter, r *http.Request) { book, err := ReadBody[media.Book](r.Body) if err != nil { writeJSONerror(w, err.Error(), http.StatusBadRequest) return } if err = l.DeleteBook(r.Context(), book); err != nil { writeJSONerror(w, err.Error(), 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 lookupBook(query Query, w http.ResponseWriter, r *http.Request) { req, err := ReadBody[media.Book](r.Body) if err != nil { writeJSONerror(w, err.Error(), http.StatusBadRequest) return } book, err := query.GetByISBN(req.ISBN13) if err != nil { writeJSONerror(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, book, http.StatusOK) } func static(f fs.FS) http.Handler { return http.FileServer(http.FS(f)) } func ReadBody[T any](r io.ReadCloser) (*T, error) { t := new(T) if r == nil { return t, fmt.Errorf("no body provided") } defer r.Close() b, err := io.ReadAll(r) if err != nil { return t, fmt.Errorf("error reading body: %w", err) } err = json.Unmarshal(b, t) if err != nil { return t, fmt.Errorf("error reading body: %w", err) } return t, nil }