package web import ( "errors" "fmt" "io" "mime" "net/http" "strconv" "strings" "time" "git.veillax.com/Veillax/basking/internal/config" "git.veillax.com/Veillax/basking/internal/store" ) func (s *Server) me(w http.ResponseWriter, r *http.Request, p principal) { writeJSON(w, map[string]any{"version": s.version, "api": p.API}) } func (s *Server) summary(w http.ResponseWriter, r *http.Request, p principal) { counts, tags := s.st.Summary() writeJSON(w, map[string]any{"counts": counts, "tags": tags}) } func (s *Server) listNotes(w http.ResponseWriter, r *http.Request, p principal) { q := r.URL.Query() limit, _ := strconv.Atoi(q.Get("limit")) if limit <= 0 || limit > 200 { limit = 50 } offset, _ := strconv.Atoi(q.Get("offset")) text := q.Get("q") if len(text) > 200 { text = text[:200] } notes, total := s.st.List(store.Query{Text: text, Tag: q.Get("tag"), View: q.Get("view"), Offset: offset, Limit: limit}) writeJSON(w, map[string]any{"notes": notes, "total": total}) } // createNote takes {"body": "..."} as JSON, or the note itself as plain text, so a script can // just post what it has: curl -d 'buy milk' -H 'Content-Type: text/plain' ... func (s *Server) createNote(w http.ResponseWriter, r *http.Request, p principal) { var body string if mt, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")); mt == "application/json" { var in struct{ Body, Text string } if !decode(w, r, &in) { return } body = in.Body + in.Text } else { b, err := io.ReadAll(r.Body) if err != nil { fail(w, http.StatusRequestEntityTooLarge, "request too large") return } body = string(b) } n, err := s.st.Create(body) if err != nil { storeFail(w, err) return } writeJSONStatus(w, http.StatusCreated, n) } func (s *Server) getNote(w http.ResponseWriter, r *http.Request, p principal) { n, err := s.st.Get(r.PathValue("id")) if err != nil { storeFail(w, err) return } writeJSON(w, n) } func (s *Server) updateNote(w http.ResponseWriter, r *http.Request, p principal) { var in struct { Body *string `json:"body"` Pinned *bool `json:"pinned"` Base string `json:"base"` } if !decode(w, r, &in) { return } var base time.Time if in.Base != "" { t, err := time.Parse(time.RFC3339Nano, in.Base) if err != nil { fail(w, http.StatusBadRequest, "base must be the note's updated time") return } base = t } n, err := s.st.Update(r.PathValue("id"), in.Body, in.Pinned, base) if errors.Is(err, store.ErrConflict) { writeJSONStatus(w, http.StatusConflict, map[string]any{"error": "This note changed somewhere else", "note": n}) return } if err != nil { storeFail(w, err) return } writeJSON(w, n) } func (s *Server) deleteNote(w http.ResponseWriter, r *http.Request, p principal) { id := r.PathValue("id") var err error if r.URL.Query().Get("purge") == "1" { err = s.st.Purge(id) } else { _, err = s.st.Trash(id) } if err != nil { storeFail(w, err) return } writeJSON(w, map[string]bool{"ok": true}) } func (s *Server) restoreNote(w http.ResponseWriter, r *http.Request, p principal) { n, err := s.st.Restore(r.PathValue("id")) if err != nil { storeFail(w, err) return } writeJSON(w, n) } func (s *Server) emptyTrash(w http.ResponseWriter, r *http.Request, p principal) { n, err := s.st.EmptyTrash() if err != nil { storeFail(w, err) return } writeJSON(w, map[string]int{"deleted": n}) } func (s *Server) daily(w http.ResponseWriter, r *http.Request, p principal) { var in struct{ Date string } if !decode(w, r, &in) { return } n, err := s.st.Daily(in.Date) if err != nil { storeFail(w, err) return } writeJSON(w, n) } func (s *Server) export(w http.ResponseWriter, r *http.Request, p principal) { w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="basking-%s.zip"`, time.Now().Format("2006-01-02"))) if err := s.st.WriteZip(w); err != nil { // The download has started, so all that can be done is to cut it short. panic(http.ErrAbortHandler) } } // ---- password and tokens ---- func (s *Server) changePassword(w http.ResponseWriter, r *http.Request, p principal) { var in struct{ Current, New string } if !decode(w, r, &in) { return } if !config.CheckPassword(s.cfg.Get().PasswordHash, in.Current) { fail(w, http.StatusForbidden, "the current password is wrong") return } if len(in.New) < 8 { fail(w, http.StatusBadRequest, "the new password must be at least 8 characters") return } hash, err := config.HashPassword(in.New) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } if err := s.cfg.Update(func(c *config.Config) error { c.PasswordHash = hash; return nil }); err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } s.startSession(w, r) // the old cookie no longer matches, so give this browser a new one writeJSON(w, map[string]bool{"ok": true}) } func (s *Server) listTokens(w http.ResponseWriter, r *http.Request, p principal) { type row struct { Name string `json:"name"` Created time.Time `json:"created"` } out := []row{} for _, t := range s.cfg.Get().Tokens { out = append(out, row{t.Name, t.Created}) } writeJSON(w, out) } func (s *Server) newToken(w http.ResponseWriter, r *http.Request, p principal) { if p.API { fail(w, http.StatusForbidden, "tokens are managed from the dashboard") return } var in struct{ Name string } if !decode(w, r, &in) { return } token, hash, err := config.NewToken() if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } name := strings.TrimSpace(in.Name) err = s.cfg.Update(func(c *config.Config) error { c.Tokens = append(c.Tokens, config.Token{Name: name, Hash: hash, Created: time.Now().UTC()}) return nil }) if err != nil { fail(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, map[string]string{"name": name, "token": token}) } func (s *Server) deleteToken(w http.ResponseWriter, r *http.Request, p principal) { if p.API { fail(w, http.StatusForbidden, "tokens are managed from the dashboard") return } name := r.PathValue("name") found := false err := s.cfg.Update(func(c *config.Config) error { keep := c.Tokens[:0] for _, t := range c.Tokens { if t.Name == name { found = true continue } keep = append(keep, t) } c.Tokens = keep return nil }) switch { case err != nil: fail(w, http.StatusInternalServerError, err.Error()) case !found: fail(w, http.StatusNotFound, "no such token") default: writeJSON(w, map[string]bool{"ok": true}) } }