/* Copyright (C) 2019 Monomax Software Pty Ltd * * This file is part of Dnote. * * Dnote is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Dnote is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Dnote. If not, see . */ package handlers import ( "encoding/json" "net/http" "net/url" "strconv" "strings" "time" "github.com/dnote/dnote/pkg/server/api/helpers" "github.com/dnote/dnote/pkg/server/api/presenters" "github.com/dnote/dnote/pkg/server/database" "github.com/gorilla/mux" "github.com/jinzhu/gorm" "github.com/pkg/errors" ) func respondWithNote(w http.ResponseWriter, note database.Note) { presentedNote := presenters.PresentNote(note) w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(presentedNote); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } func preloadNote(conn *gorm.DB) *gorm.DB { return conn.Preload("Book").Preload("User") } func (a *App) getDemoNote(w http.ResponseWriter, r *http.Request) { db := database.DBConn vars := mux.Vars(r) noteUUID := vars["noteUUID"] demoUserID, err := helpers.GetDemoUserID() if err != nil { http.Error(w, errors.Wrap(err, "finding demo user").Error(), http.StatusInternalServerError) return } var note database.Note conn := db.Where("uuid = ? AND user_id = ?", noteUUID, demoUserID) conn = preloadNote(conn) conn.Find(¬e) if conn.RecordNotFound() { http.Error(w, "not found", http.StatusNotFound) return } else if err := conn.Error; err != nil { http.Error(w, errors.Wrap(err, "finding note").Error(), http.StatusInternalServerError) return } respondWithNote(w, note) } func (a *App) getNote(w http.ResponseWriter, r *http.Request) { user, ok := r.Context().Value(helpers.KeyUser).(database.User) if !ok { http.Error(w, "No authenticated user found", http.StatusInternalServerError) return } db := database.DBConn vars := mux.Vars(r) noteUUID := vars["noteUUID"] var note database.Note conn := db.Debug().Where("uuid = ? AND user_id = ?", noteUUID, user.ID) conn = preloadNote(conn) conn.Find(¬e) if conn.RecordNotFound() { http.Error(w, "not found", http.StatusNotFound) return } else if err := conn.Error; err != nil { http.Error(w, errors.Wrap(err, "finding note").Error(), http.StatusInternalServerError) return } respondWithNote(w, note) } /**** getNotesHandler */ // GetNotesResponse is a reponse by getNotesHandler type GetNotesResponse struct { Notes []presenters.Note `json:"notes"` Total int `json:"total"` PrevDate *int64 `json:"prev_date"` } func (a *App) getDemoNotes(w http.ResponseWriter, r *http.Request) { userID, err := helpers.GetDemoUserID() if err != nil { http.Error(w, errors.Wrap(err, "finding demo user id").Error(), http.StatusInternalServerError) return } query := r.URL.Query() respondGetNotes(userID, query, w) } func (a *App) getNotes(w http.ResponseWriter, r *http.Request) { user, ok := r.Context().Value(helpers.KeyUser).(database.User) if !ok { http.Error(w, "No authenticated user found", http.StatusInternalServerError) return } query := r.URL.Query() respondGetNotes(user.ID, query, w) } func respondGetNotes(userID int, query url.Values, w http.ResponseWriter) { err := validateGetNotesQuery(query) if err != nil { http.Error(w, errors.Wrap(err, "validating query parameters").Error(), http.StatusBadRequest) return } q, err := parseGetNotesQuery(query) if err != nil { http.Error(w, errors.Wrap(err, "parsing query parameters").Error(), http.StatusBadRequest) return } dateLowerbound, dateUpperbound := getDateBounds(q.Year, q.Month) baseConn := getNotesBaseQuery(userID, q) conn := baseConn.Where("notes.added_on >= ? AND notes.added_on < ?", dateLowerbound, dateUpperbound) var total int err = conn.Debug().Model(database.Note{}).Count(&total).Error if err != nil { http.Error(w, errors.Wrap(err, "counting total").Error(), http.StatusInternalServerError) return } notes := []database.Note{} if total != 0 { conn = orderGetNotes(conn) conn = preloadNote(conn) conn = paginate(conn, q.Page) err = conn.Find(¬es).Error if err != nil { http.Error(w, errors.Wrap(err, "finding notes").Error(), http.StatusInternalServerError) return } } // peek the prev date var prevDateUpperbound int64 if len(notes) > 0 { lastNote := notes[len(notes)-1] prevDateUpperbound = lastNote.AddedOn } else { prevDateUpperbound = dateLowerbound } prevDate, err := getPrevDate(baseConn, prevDateUpperbound) if err != nil { http.Error(w, errors.Wrap(err, "getting prevDate").Error(), http.StatusInternalServerError) return } presentedNotes := presenters.PresentNotes(notes) response := GetNotesResponse{ Notes: presentedNotes, Total: total, PrevDate: prevDate, } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(response); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } func getPrevDate(baseConn *gorm.DB, dateUpperbound int64) (*int64, error) { var prevNote database.Note conn := baseConn. Select("notes.added_on"). Where("notes.added_on < ?", dateUpperbound). Order("notes.added_on DESC") if conn.First(&prevNote).RecordNotFound() { return nil, nil } if err := conn.Error; err != nil { return nil, errors.Wrap(err, "querying previous note") } return &prevNote.AddedOn, nil } func validateGetNotesQuery(q url.Values) error { if q.Get("year") == "" { return errors.New("'year' is required") } if q.Get("month") == "" { return errors.New("'month' is required") } return nil } type getNotesQuery struct { Year int Month int Page int BookUUID string Encrypted *bool } func parseGetNotesQuery(q url.Values) (getNotesQuery, error) { yearStr := q.Get("year") monthStr := q.Get("month") bookStr := q.Get("book") pageStr := q.Get("page") encryptedStr := q.Get("encrypted") var page int if len(pageStr) > 0 { p, err := strconv.Atoi(pageStr) if err != nil { return getNotesQuery{}, errors.Wrap(err, "parsing page") } page = p } else { page = 1 } year, err := strconv.Atoi(yearStr) if err != nil { return getNotesQuery{}, errors.Wrapf(err, "invalid year %s", yearStr) } month, err := strconv.Atoi(monthStr) if err != nil { return getNotesQuery{}, errors.Wrapf(err, "invalid month %s", monthStr) } if month < 1 || month > 12 { return getNotesQuery{}, errors.Errorf("Invalid month %s", monthStr) } var encrypted *bool if strings.ToLower(encryptedStr) == "true" { *encrypted = true } else if strings.ToLower(encryptedStr) == "false" { *encrypted = false } ret := getNotesQuery{ Year: year, Month: month, Page: page, BookUUID: bookStr, Encrypted: encrypted, } return ret, nil } func getDateBounds(year, month int) (int64, int64) { var yearUpperbound, monthUpperbound int if month == 12 { monthUpperbound = 1 yearUpperbound = year + 1 } else { monthUpperbound = month + 1 yearUpperbound = year } lower := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC).UnixNano() upper := time.Date(yearUpperbound, time.Month(monthUpperbound), 1, 0, 0, 0, 0, time.UTC).UnixNano() return lower, upper } func getNotesBaseQuery(userID int, q getNotesQuery) *gorm.DB { db := database.DBConn conn := db.Where("notes.user_id = ? AND notes.deleted = ?", userID, false) if len(q.BookUUID) > 0 { conn = conn.Joins("INNER JOIN books ON books.uuid = notes.book_uuid"). Where("books.uuid = ?", q.BookUUID) } if q.Encrypted != nil { conn = conn.Where("notes.encrypted = ?", *q.Encrypted) } return conn } func orderGetNotes(conn *gorm.DB) *gorm.DB { return conn.Order("notes.added_on DESC, notes.id DESC") } func (a *App) legacyGetNotes(w http.ResponseWriter, r *http.Request) { user, ok := r.Context().Value(helpers.KeyUser).(database.User) if !ok { http.Error(w, "No authenticated user found", http.StatusInternalServerError) return } var notes []database.Note db := database.DBConn if err := db.Where("user_id = ? AND encrypted = false", user.ID).Find(¬es).Error; err != nil { http.Error(w, "finding notes", http.StatusInternalServerError) return } presented := presenters.PresentNotes(notes) w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(presented); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }