| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "strconv" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/stevedylandev/andromeda/pkg/auth" |
| 9 | "github.com/stevedylandev/andromeda/pkg/web" |
| 10 | ) |
| 11 | |
| 12 | var sectionDefs = []struct { |
| 13 | Slug string |
| 14 | Status string |
| 15 | }{ |
| 16 | {"reading", "reading"}, |
| 17 | {"read", "read"}, |
| 18 | {"want", "want"}, |
| 19 | } |
| 20 | |
| 21 | func bookToView(b Book) bookView { |
| 22 | v := bookView{Title: b.Title, Authors: b.Authors} |
| 23 | if b.CoverURL != nil { |
| 24 | v.CoverURL = *b.CoverURL |
| 25 | } |
| 26 | if b.Notes != nil { |
| 27 | v.Notes = *b.Notes |
| 28 | } |
| 29 | return v |
| 30 | } |
| 31 | |
| 32 | func bookToRow(b Book) adminBookRow { |
| 33 | r := adminBookRow{ID: b.ID, Title: b.Title, Authors: b.Authors, Status: b.Status} |
| 34 | if b.ISBN != nil { |
| 35 | r.ISBN = *b.ISBN |
| 36 | } |
| 37 | if b.CoverURL != nil { |
| 38 | r.CoverURL = *b.CoverURL |
| 39 | } |
| 40 | if b.Notes != nil { |
| 41 | r.Notes = *b.Notes |
| 42 | } |
| 43 | return r |
| 44 | } |
| 45 | |
| 46 | func (a *App) indexHandler(w http.ResponseWriter, r *http.Request) { |
| 47 | all, _ := listBooks(a.DB, "") |
| 48 | labels, _ := getCategoryLabels(a.DB) |
| 49 | |
| 50 | makeSection := func(status string) sectionView { |
| 51 | sec := sectionView{Label: labelFor(labels, status)} |
| 52 | for _, b := range all { |
| 53 | if b.Status == status { |
| 54 | sec.Books = append(sec.Books, bookToView(b)) |
| 55 | } |
| 56 | } |
| 57 | return sec |
| 58 | } |
| 59 | |
| 60 | data := indexPageData{BaseURL: a.BaseURL, NavMode: a.DisplayMode == DisplayModeNav} |
| 61 | if data.NavMode { |
| 62 | selected := sectionDefs[0].Slug |
| 63 | if q := r.URL.Query().Get("category"); q != "" { |
| 64 | for _, s := range sectionDefs { |
| 65 | if s.Slug == q { |
| 66 | selected = q |
| 67 | break |
| 68 | } |
| 69 | } |
| 70 | } |
| 71 | for _, s := range sectionDefs { |
| 72 | data.NavCategories = append(data.NavCategories, navCategory{Slug: s.Slug, Label: labelFor(labels, s.Status), Active: s.Slug == selected}) |
| 73 | } |
| 74 | for _, s := range sectionDefs { |
| 75 | if s.Slug == selected { |
| 76 | data.Sections = []sectionView{makeSection(s.Status)} |
| 77 | break |
| 78 | } |
| 79 | } |
| 80 | } else { |
| 81 | for _, s := range sectionDefs { |
| 82 | sec := makeSection(s.Status) |
| 83 | if len(sec.Books) > 0 { |
| 84 | data.Sections = append(data.Sections, sec) |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | web.Render(a.Templates, w, "index.html", data, a.Log) |
| 89 | } |
| 90 | |
| 91 | func (a *App) loginGetHandler(w http.ResponseWriter, r *http.Request) { |
| 92 | web.Render(a.Templates, w, "login.html", loginPageData{Error: r.URL.Query().Get("error")}, a.Log) |
| 93 | } |
| 94 | |
| 95 | func (a *App) loginPostHandler(w http.ResponseWriter, r *http.Request) { |
| 96 | if a.AdminPassword == "" { |
| 97 | web.RedirectWithError(w, r, "/admin/login", "No admin password configured") |
| 98 | return |
| 99 | } |
| 100 | if err := r.ParseForm(); err != nil { |
| 101 | web.RedirectWithError(w, r, "/admin/login", "Bad request") |
| 102 | return |
| 103 | } |
| 104 | if !auth.VerifyPassword(r.FormValue("password"), a.AdminPassword) { |
| 105 | web.RedirectWithError(w, r, "/admin/login", "Invalid password") |
| 106 | return |
| 107 | } |
| 108 | token, err := a.Sessions.Create() |
| 109 | if err != nil { |
| 110 | a.Log.Error("create session failed", "err", err) |
| 111 | web.RedirectWithError(w, r, "/admin/login", "Session error") |
| 112 | return |
| 113 | } |
| 114 | a.Sessions.PruneExpired() |
| 115 | http.SetCookie(w, a.Sessions.SessionCookie(token)) |
| 116 | http.Redirect(w, r, "/admin", http.StatusSeeOther) |
| 117 | } |
| 118 | |
| 119 | func (a *App) logoutHandler(w http.ResponseWriter, r *http.Request) { |
| 120 | if c, err := r.Cookie(a.Sessions.CookieName); err == nil && c.Value != "" { |
| 121 | a.Sessions.Delete(c.Value) |
| 122 | } |
| 123 | http.SetCookie(w, a.Sessions.ClearCookie()) |
| 124 | http.Redirect(w, r, "/admin/login", http.StatusSeeOther) |
| 125 | } |
| 126 | |
| 127 | func (a *App) adminHandler(w http.ResponseWriter, r *http.Request) { |
| 128 | all, _ := listBooks(a.DB, "") |
| 129 | labels, _ := getCategoryLabels(a.DB) |
| 130 | rows := make([]adminBookRow, 0, len(all)) |
| 131 | for _, b := range all { |
| 132 | rows = append(rows, bookToRow(b)) |
| 133 | } |
| 134 | |
| 135 | libraryQuery := r.URL.Query().Get("q") |
| 136 | searched := strings.TrimSpace(libraryQuery) != "" |
| 137 | var results []adminBookRow |
| 138 | if searched { |
| 139 | found, _ := searchBooks(a.DB, libraryQuery) |
| 140 | results = make([]adminBookRow, 0, len(found)) |
| 141 | for _, b := range found { |
| 142 | results = append(results, bookToRow(b)) |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | web.Render(a.Templates, w, "admin.html", adminPageData{ |
| 147 | Success: r.URL.Query().Get("success"), |
| 148 | Error: r.URL.Query().Get("error"), |
| 149 | Books: rows, |
| 150 | Labels: labels, |
| 151 | LibraryQuery: libraryQuery, |
| 152 | LibraryResults: results, |
| 153 | LibrarySearched: searched, |
| 154 | }, a.Log) |
| 155 | } |
| 156 | |
| 157 | func (a *App) adminUpdateLabels(w http.ResponseWriter, r *http.Request) { |
| 158 | if err := r.ParseForm(); err != nil { |
| 159 | web.RedirectWithError(w, r, "/admin", "Bad request") |
| 160 | return |
| 161 | } |
| 162 | reading := strings.TrimSpace(r.FormValue("reading")) |
| 163 | read := strings.TrimSpace(r.FormValue("read")) |
| 164 | want := strings.TrimSpace(r.FormValue("want")) |
| 165 | if reading == "" || read == "" || want == "" { |
| 166 | web.RedirectWithError(w, r, "/admin", "Labels cannot be empty") |
| 167 | return |
| 168 | } |
| 169 | if err := setSetting(a.DB, "category_label.reading", reading); err != nil { |
| 170 | web.RedirectWithError(w, r, "/admin", "Failed to save labels") |
| 171 | return |
| 172 | } |
| 173 | _ = setSetting(a.DB, "category_label.read", read) |
| 174 | _ = setSetting(a.DB, "category_label.want", want) |
| 175 | web.RedirectWithSuccess(w, r, "/admin", "Labels updated") |
| 176 | } |
| 177 | |
| 178 | func (a *App) adminSearch(w http.ResponseWriter, r *http.Request) { |
| 179 | hits, err := googleBooksSearch(r.Context(), r.URL.Query().Get("q"), a.GoogleBooksKey) |
| 180 | if err != nil { |
| 181 | a.Log.Warn("google books search failed", "err", err) |
| 182 | web.WriteError(w, http.StatusBadGateway, err.Error()) |
| 183 | return |
| 184 | } |
| 185 | if hits == nil { |
| 186 | hits = []SearchHit{} |
| 187 | } |
| 188 | web.WriteJSON(w, http.StatusOK, hits) |
| 189 | } |
| 190 | |
| 191 | func (a *App) adminAddBook(w http.ResponseWriter, r *http.Request) { |
| 192 | if err := r.ParseForm(); err != nil { |
| 193 | web.RedirectWithError(w, r, "/admin", "Bad request") |
| 194 | return |
| 195 | } |
| 196 | status := r.FormValue("status") |
| 197 | if !validStatus(status) { |
| 198 | web.RedirectWithError(w, r, "/admin", "Invalid status") |
| 199 | return |
| 200 | } |
| 201 | b := NewBook{Title: r.FormValue("title"), Authors: r.FormValue("authors"), Status: status} |
| 202 | if v := strings.TrimSpace(r.FormValue("google_id")); v != "" { |
| 203 | b.GoogleID = &v |
| 204 | } |
| 205 | if v := strings.TrimSpace(r.FormValue("isbn")); v != "" { |
| 206 | b.ISBN = &v |
| 207 | } |
| 208 | if v := strings.TrimSpace(r.FormValue("cover_url")); v != "" { |
| 209 | b.CoverURL = &v |
| 210 | } |
| 211 | if _, err := insertBook(a.DB, b); err != nil { |
| 212 | a.Log.Error("insert book", "err", err) |
| 213 | web.RedirectWithError(w, r, "/admin", "Failed to add book") |
| 214 | return |
| 215 | } |
| 216 | web.RedirectWithSuccess(w, r, "/admin", "Book added") |
| 217 | } |
| 218 | |
| 219 | func (a *App) adminUpdateStatus(w http.ResponseWriter, r *http.Request) { |
| 220 | id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) |
| 221 | if err != nil { |
| 222 | web.RedirectWithError(w, r, "/admin", "Bad id") |
| 223 | return |
| 224 | } |
| 225 | if err := r.ParseForm(); err != nil { |
| 226 | web.RedirectWithError(w, r, "/admin", "Bad request") |
| 227 | return |
| 228 | } |
| 229 | status := r.FormValue("status") |
| 230 | if !validStatus(status) { |
| 231 | web.RedirectWithError(w, r, "/admin", "Invalid status") |
| 232 | return |
| 233 | } |
| 234 | _ = updateBookStatus(a.DB, id, status) |
| 235 | web.RedirectWithSuccess(w, r, "/admin", "Status updated") |
| 236 | } |
| 237 | |
| 238 | func (a *App) adminUpdateNotes(w http.ResponseWriter, r *http.Request) { |
| 239 | id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) |
| 240 | if err != nil { |
| 241 | web.RedirectWithError(w, r, "/admin", "Bad id") |
| 242 | return |
| 243 | } |
| 244 | if err := r.ParseForm(); err != nil { |
| 245 | web.RedirectWithError(w, r, "/admin", "Bad request") |
| 246 | return |
| 247 | } |
| 248 | notes := strings.TrimSpace(r.FormValue("notes")) |
| 249 | var n *string |
| 250 | if notes != "" { |
| 251 | n = ¬es |
| 252 | } |
| 253 | _ = updateBookNotes(a.DB, id, n) |
| 254 | web.RedirectWithSuccess(w, r, "/admin", "Notes saved") |
| 255 | } |
| 256 | |
| 257 | func (a *App) adminDeleteBook(w http.ResponseWriter, r *http.Request) { |
| 258 | id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) |
| 259 | if err == nil { |
| 260 | _ = deleteBook(a.DB, id) |
| 261 | } |
| 262 | web.RedirectWithSuccess(w, r, "/admin", "Book removed") |
| 263 | } |