diff --git a/cmd/serve/api.go b/cmd/serve/api.go index cdf3c8f..d76977c 100644 --- a/cmd/serve/api.go +++ b/cmd/serve/api.go @@ -11,21 +11,17 @@ import ( ) type Router struct { - static fs.FS - lib Library - rcol RecordCollection + static fs.FS + lib Library + rcol RecordCollection + ts *tailscale.LocalClient + isAdmin bool } -type AdminRouter struct { - static fs.FS - lib Library - ts *tailscale.LocalClient -} +type path map[string]func() -type handler map[string]func() - -func (h handler) Handle(w http.ResponseWriter, req *http.Request) { - if f, ok := h[req.Method]; ok { +func (h path) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if f, ok := h[r.Method]; ok { f() return } @@ -50,31 +46,33 @@ func writeJSON(w http.ResponseWriter, b any, status int) { 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/mode": + path{ + http.MethodGet: func() { + writeJSON(w, struct{ Admin bool }{Admin: router.isAdmin}, http.StatusOK) + }, + }.ServeHTTP(w, r) case "/api/whoami": - handler{ + if !router.isAdmin { + http.NotFoundHandler().ServeHTTP(w, r) + return + } + path{ http.MethodGet: func() { getWhoAmI(router.ts, w, r) }, - }.Handle(w, r) + }.ServeHTTP(w, r) + case "/api/records": + path{ + http.MethodGet: func() { getRecords(router.rcol, w, r) }, + }.ServeHTTP(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) + 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) default: static(router.static).ServeHTTP(w, r) } diff --git a/cmd/serve/main.go b/cmd/serve/main.go index 267a23d..2c2e6dc 100644 --- a/cmd/serve/main.go +++ b/cmd/serve/main.go @@ -55,24 +55,26 @@ func main() { c.DiscogsToken, time.Hour*24, c.DiscogsUser, c.DiscogsPersist, c.DiscogsCacheFile, )) - frontendRoot := must.Get(frontend.Root()) - adminRoot := must.Get(frontend.AdminRoot()) + staticRoot := must.Get(frontend.Root()) servers := make(chan (*http.Server), 3) errGroup := errgroup.Group{} errGroup.Go(func() error { return start(servers)( publicServer(8080, &Router{ - static: frontendRoot, - lib: lib, - rcol: discogsCache, + static: staticRoot, + lib: lib, + rcol: discogsCache, + isAdmin: false, })) }) errGroup.Go(func() error { return start(servers)( - tailscaleListener("library-admin", &AdminRouter{ - static: adminRoot, - lib: lib, + tailscaleListener("library-admin", &Router{ + static: staticRoot, + lib: lib, + rcol: discogsCache, + isAdmin: true, })) }) errGroup.Go(func() error { @@ -143,10 +145,15 @@ func publicServer(port int, handler http.Handler) (*http.Server, net.Listener, e return server, ln, nil } -func tailscaleListener(hostname string, handler *AdminRouter) (*http.Server, net.Listener, error) { +func tailscaleListener(hostname string, handler *Router) (*http.Server, net.Listener, error) { s := &tsnet.Server{ Dir: ".config/" + hostname, Hostname: hostname, + Logf: func(s string, a ...any) { // silence most tsnet logs + if strings.HasPrefix(s, "To start this tsnet server") { + log.Printf(s, a...) + } + }, } ln, err := s.Listen("tcp", ":80") if err != nil { diff --git a/frontend/admin/app.js b/frontend/admin/app.js deleted file mode 100644 index d8c2d4c..0000000 --- a/frontend/admin/app.js +++ /dev/null @@ -1,217 +0,0 @@ -var sortState = { - sortBy: "sortAuthor", - sortOrder: "asc", -}; - -function init() { - fetch("/api/books") - .then((response) => response.json()) - .then((books) => { - // prepare response - books.forEach(apiResponseParsing); - document.getElementById("search").addEventListener("input", (e) => { - renderTable( - search( - books, - e.target.value, - document.getElementById("childrens").checked - ) - ); - }); - document.getElementById("childrens").addEventListener("change", (e) => { - renderTable( - search( - books, - document.getElementById("search").value, - e.target.checked - ) - ); - }); - renderTable( - search(books, "", document.getElementById("childrens").checked) - ); - }); -} - -function renderTable(books, sortField) { - if (sortField) { - if (sortState.sortBy === sortField && sortState.sortOrder === "asc") { - sortState.sortOrder = "desc"; - } else { - sortState.sortOrder = "asc"; - } - sortState.sortBy = sortField; - } - books.sort((one, two) => - (one[sortState.sortBy] + one["sortTitle"]).localeCompare( - two[sortState.sortBy] + two["sortTitle"] - ) - ); - if (sortState.sortOrder === "desc") { - books.reverse(); - } - books.forEach((e, i) => (e.rowNumber = i)); // re-key - - // rendering - var bookElement = document.getElementById("books"); - bookElement.innerHTML = TableTemplate(books); - - var bookCount = document.getElementById("bookCount"); - bookCount.innerHTML = `${books.length} books`; - - // add listeners for selecting book to view - Array.from(bookElement.querySelectorAll("tbody tr")) - .slice(1) // remove header from Array - .forEach((row) => { - row.addEventListener("click", (e) => { - // add listener to swap current book - document.getElementById("current").innerHTML = BookTemplate( - books[e.currentTarget.id] - ); - }); - }); - // add sorting callbacks - Array.from(bookElement.querySelectorAll("tbody tr th[data-sort-by]")).forEach( - (row) => { - row.addEventListener("click", function (e) { - renderTable(books, e.target.dataset.sortBy); // only add callback when there's a sortBy attribute - }); - } - ); - // mark currently active column - bookElement - .querySelector("tbody tr th[data-sort-by=" + sortState.sortBy + "]") - .classList.add(sortState.sortOrder); -} - -function apiResponseParsing(book) { - book.sortTitle = titleCleaner(book.title); - if (!book["isbn-10"] && book["isbn-13"]) { - book["isbn-10"] = ISBNfromEAN(book["isbn-13"]); - } - if (!book.coverURL && book["isbn-10"]) { - book.coverURL = - `https://images-na.ssl-images-amazon.com/images/P/` + - book["isbn-10"] + - `.01.LZZ.jpg`; - } - return book; -} - -function search(books, searchBy, includeChildrensBooks) { - searchBy = searchCleaner(searchBy); - books = books.filter( - ({ title, authors, genre, publisher, series, year, childrens }) => { - var inSearch = true; - if (searchBy !== "") { - inSearch = Object.values({ - title, - authors: authors.join(" "), - genre, - publisher, - series, - year, - }).find((field) => searchCleaner(field).indexOf(searchBy) !== -1); - } - if (!includeChildrensBooks) { - return inSearch && !childrens; - } - return inSearch; - } - ); - return books; -} - -function titleCleaner(title) { - return title - .replace('"', "") - .replace(":", "") - .replace(/^(An?|The)\s/i, ""); -} - -function searchCleaner(str) { - return str - .toLowerCase() - .replaceAll('"', "") - .replaceAll(":", "") - .replaceAll("'", "") - .replaceAll(" ", ""); -} - -function ISBNfromEAN(EAN) { - ISBN = EAN.slice(3, 12); - var checkdigit = - (11 - (ISBN.split("").reduce((s, n, k) => s + n * (10 - k), 0) % 11)) % 11; - return ISBN + (checkdigit === 10 ? "X" : checkdigit); -} - -function BookTemplate({ - "isbn-13": isbn13, - "isbn-10": isbn10, - authors, - coverURL, - description, - format, - notes, - publisher, - series, - signed, - title, - volume, - year, -}) { - return ` -
Title | -Author | -Publisher | -Year | -ISBN | -
---|