package webserver import ( "context" "fmt" "net/http" "strconv" "time" "git.wntrmute.dev/kyle/eng-pad-server/internal/auth" "git.wntrmute.dev/kyle/eng-pad-server/internal/share" "github.com/go-chi/chi/v5" ) func (ws *WebServer) handleLoginPage(w http.ResponseWriter, r *http.Request) { ws.render(w, "login.html", nil) } func (ws *WebServer) handleLoginSubmit(w http.ResponseWriter, r *http.Request) { username := r.FormValue("username") password := r.FormValue("password") userID, err := auth.AuthenticateUser(ws.db, username, password) if err != nil { ws.render(w, "login.html", map[string]string{"Error": "Invalid credentials"}) return } token, err := auth.CreateToken(ws.db, userID, 24*time.Hour) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } http.SetCookie(w, &http.Cookie{ Name: "session", Value: token, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteStrictMode, MaxAge: 86400, }) http.Redirect(w, r, "/notebooks", http.StatusFound) } func (ws *WebServer) handleLogout(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie("session"); err == nil { _ = auth.DeleteToken(ws.db, cookie.Value) } http.SetCookie(w, &http.Cookie{ Name: "session", Value: "", Path: "/", MaxAge: -1, }) http.Redirect(w, r, "/login", http.StatusFound) } func (ws *WebServer) handleNotebooks(w http.ResponseWriter, r *http.Request) { userID := r.Context().Value(userIDKey).(int64) rows, err := ws.db.QueryContext(r.Context(), `SELECT n.id, n.title, n.page_size, n.synced_at, (SELECT COUNT(*) FROM pages WHERE notebook_id = n.id) FROM notebooks n WHERE n.user_id = ? ORDER BY n.synced_at DESC`, userID) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } defer func() { _ = rows.Close() }() type notebook struct { ID int64 Title string PageSize string SyncedAt string PageCount int } var notebooks []notebook for rows.Next() { var nb notebook var syncedAt int64 if err := rows.Scan(&nb.ID, &nb.Title, &nb.PageSize, &syncedAt, &nb.PageCount); err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } nb.SyncedAt = time.UnixMilli(syncedAt).Format("2006-01-02 15:04") notebooks = append(notebooks, nb) } ws.render(w, "notebooks.html", map[string]any{"Notebooks": notebooks}) } func (ws *WebServer) handleNotebook(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) var title, pageSize string err := ws.db.QueryRow("SELECT title, page_size FROM notebooks WHERE id = ?", id).Scan(&title, &pageSize) if err != nil { http.Error(w, "Not found", http.StatusNotFound) return } rows, err := ws.db.Query("SELECT page_number FROM pages WHERE notebook_id = ? ORDER BY page_number", id) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } defer func() { _ = rows.Close() }() type pageInfo struct { Number int SVGLink string ViewLink string } var pages []pageInfo for rows.Next() { var num int if err := rows.Scan(&num); err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } pages = append(pages, pageInfo{ Number: num, SVGLink: fmt.Sprintf("/v1/notebooks/%d/pages/%d/svg", id, num), ViewLink: fmt.Sprintf("/notebooks/%d/pages/%d", id, num), }) } ws.render(w, "notebook.html", map[string]any{ "Title": title, "Pages": pages, "PDFLink": fmt.Sprintf("/v1/notebooks/%d/pdf", id), }) } func (ws *WebServer) handlePage(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) num, _ := strconv.Atoi(chi.URLParam(r, "num")) var title string err := ws.db.QueryRow("SELECT title FROM notebooks WHERE id = ?", id).Scan(&title) if err != nil { http.Error(w, "Not found", http.StatusNotFound) return } ws.render(w, "page.html", map[string]any{ "NotebookTitle": title, "PageNumber": num, "BackLink": fmt.Sprintf("/notebooks/%d", id), "SVGLink": fmt.Sprintf("/v1/notebooks/%d/pages/%d/svg", id, num), "JPGLink": fmt.Sprintf("/v1/notebooks/%d/pages/%d/jpg", id, num), "PDFLink": fmt.Sprintf("/v1/notebooks/%d/pdf", id), }) } func (ws *WebServer) handleShareNotebook(w http.ResponseWriter, r *http.Request) { token := chi.URLParam(r, "token") notebookID, err := share.ValidateLink(ws.db, token) if err != nil { http.Error(w, "Link not found or expired", http.StatusGone) return } var title string _ = ws.db.QueryRow("SELECT title FROM notebooks WHERE id = ?", notebookID).Scan(&title) rows, err := ws.db.Query("SELECT page_number FROM pages WHERE notebook_id = ? ORDER BY page_number", notebookID) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } defer func() { _ = rows.Close() }() type pageInfo struct { Number int SVGLink string ViewLink string } var pages []pageInfo for rows.Next() { var num int if err := rows.Scan(&num); err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } pages = append(pages, pageInfo{ Number: num, SVGLink: fmt.Sprintf("/s/%s/pages/%d/svg", token, num), ViewLink: fmt.Sprintf("/s/%s/pages/%d", token, num), }) } ws.render(w, "notebook.html", map[string]any{ "Title": title, "Pages": pages, "PDFLink": fmt.Sprintf("/s/%s/pdf", token), }) } func (ws *WebServer) handleSharePage(w http.ResponseWriter, r *http.Request) { token := chi.URLParam(r, "token") notebookID, err := share.ValidateLink(ws.db, token) if err != nil { http.Error(w, "Link not found or expired", http.StatusGone) return } num, _ := strconv.Atoi(chi.URLParam(r, "num")) var title string _ = ws.db.QueryRow("SELECT title FROM notebooks WHERE id = ?", notebookID).Scan(&title) ws.render(w, "page.html", map[string]any{ "NotebookTitle": title, "PageNumber": num, "BackLink": fmt.Sprintf("/s/%s", token), "SVGLink": fmt.Sprintf("/s/%s/pages/%d/svg", token, num), "JPGLink": fmt.Sprintf("/s/%s/pages/%d/jpg", token, num), "PDFLink": fmt.Sprintf("/s/%s/pdf", token), }) } // --- auth middleware --- type ctxKey string const userIDKey ctxKey = "user_id" func (ws *WebServer) authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session") if err != nil { http.Redirect(w, r, "/login", http.StatusFound) return } userID, err := auth.ValidateToken(ws.db, cookie.Value) if err != nil { http.Redirect(w, r, "/login", http.StatusFound) return } ctx := r.Context() ctx = context.WithValue(ctx, userIDKey, userID) next.ServeHTTP(w, r.WithContext(ctx)) }) } func (ws *WebServer) render(w http.ResponseWriter, name string, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := ws.tmpl.ExecuteTemplate(w, name, data); err != nil { http.Error(w, "Template error", http.StatusInternalServerError) } }