diff --git a/domains/pastes/api_http.go b/domains/pastes/api_http.go
deleted file mode 100644
index 3328903..0000000
--- a/domains/pastes/api_http.go
+++ /dev/null
@@ -1,499 +0,0 @@
-// Fast Paste Bin - uberfast and easy-to-use pastebin.
-//
-// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
-// developers.
-//
-// Permission is hereby granted, free of charge, to any person obtaining
-// a copy of this software and associated documentation files (the
-// "Software"), to deal in the Software without restriction, including
-// without limitation the rights to use, copy, modify, merge, publish,
-// distribute, sublicense, and/or sell copies of the Software, and to
-// permit persons to whom the Software is furnished to do so, subject
-// to the following conditions:
-//
-// The above copyright notice and this permission notice shall be
-// included in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
-// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-package pastes
-
-import (
- // stdlib
- "bytes"
- "net/http"
- "regexp"
- "strconv"
- "strings"
- "time"
-
- // local
- "gitlab.com/pztrn/fastpastebin/internal/captcha"
- "gitlab.com/pztrn/fastpastebin/internal/pagination"
- "gitlab.com/pztrn/fastpastebin/internal/structs"
- "gitlab.com/pztrn/fastpastebin/internal/templater"
-
- // other
- "github.com/alecthomas/chroma"
- "github.com/alecthomas/chroma/formatters"
- htmlfmt "github.com/alecthomas/chroma/formatters/html"
- "github.com/alecthomas/chroma/lexers"
- "github.com/alecthomas/chroma/styles"
- //"gitlab.com/dchest/captcha"
- "github.com/labstack/echo"
-)
-
-var (
- regexInts = regexp.MustCompile("[0-9]+")
-)
-
-// GET for "/paste/PASTE_ID" and "/paste/PASTE_ID/TIMESTAMP" (private pastes).
-func pasteGET(ec echo.Context) error {
- // We should check if database connection available.
- dbConn := c.Database.GetDatabaseConnection()
- if c.Config.Database.Type != "flatfiles" && dbConn == nil {
- return ec.Redirect(http.StatusFound, "/database_not_available")
- }
-
- pasteIDRaw := ec.Param("id")
- // We already get numbers from string, so we will not check strconv.Atoi()
- // error.
- pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
- c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
-
- // Get paste.
- paste, err1 := c.Database.GetPaste(pasteID)
- if err1 != nil {
- c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
- errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
-
- if paste.IsExpired() {
- c.Logger.Error().Msgf("Paste #%d is expired", pasteID)
- errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
-
- // Check if we have a private paste and it's parameters are correct.
- if paste.Private {
- tsProvidedStr := ec.Param("timestamp")
- tsProvided, err2 := strconv.ParseInt(tsProvidedStr, 10, 64)
- if err2 != nil {
- c.Logger.Error().Msgf("Invalid timestamp '%s' provided for getting private paste #%d: %s", tsProvidedStr, pasteID, err2.Error())
- errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
- pasteTs := paste.CreatedAt.Unix()
- if tsProvided != pasteTs {
- c.Logger.Error().Msgf("Incorrect timestamp '%v' provided for private paste #%d, waiting for %v", tsProvidedStr, pasteID, strconv.FormatInt(pasteTs, 10))
- errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
- }
-
- if paste.Private && paste.Password != "" {
- // Check if cookie for this paste is defined. This means that user
- // previously successfully entered a password.
- cookie, err := ec.Cookie("PASTE-" + strconv.Itoa(pasteID))
- if err != nil {
- // No cookie, redirect to auth page.
- c.Logger.Info().Msg("Tried to access passworded paste without autorization, redirecting to auth page...")
- return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp")+"/verify")
- }
-
- // Generate cookie value to check.
- cookieValue := paste.GenerateCryptedCookieValue()
-
- if cookieValue != cookie.Value {
- c.Logger.Info().Msg("Invalid cookie, redirecting to auth page...")
- return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp")+"/verify")
- }
-
- // If all okay - do nothing :)
- }
-
- // Format paste data map.
- pasteData := make(map[string]string)
- pasteData["pasteTitle"] = paste.Title
- pasteData["pasteID"] = strconv.Itoa(paste.ID)
- pasteData["pasteDate"] = paste.CreatedAt.Format("2006-01-02 @ 15:04:05") + " UTC"
- pasteData["pasteLanguage"] = paste.Language
-
- pasteExpirationString := "Never"
- if paste.KeepFor != 0 && paste.KeepForUnitType != 0 {
- pasteExpirationString = paste.GetExpirationTime().Format("2006-01-02 @ 15:04:05") + " UTC"
- }
- pasteData["pasteExpiration"] = pasteExpirationString
-
- if paste.Private {
- pasteData["pasteType"] = "Private"
- pasteData["pasteTs"] = strconv.FormatInt(paste.CreatedAt.Unix(), 10) + "/"
- } else {
- pasteData["pasteType"] = "Public"
- pasteData["pasteTs"] = ""
- }
-
- // Highlight.
- // Get lexer.
- lexer := lexers.Get(paste.Language)
- if lexer == nil {
- lexer = lexers.Fallback
- }
- // Tokenize paste data.
- lexered, err3 := lexer.Tokenise(nil, paste.Data)
- if err3 != nil {
- c.Logger.Error().Msgf("Failed to tokenize paste data: %s", err3.Error())
- }
- // Get style for HTML output.
- style := styles.Get("monokai")
- if style == nil {
- style = styles.Fallback
- }
- // Get HTML formatter.
- formatter := chroma.Formatter(htmlfmt.New(htmlfmt.WithLineNumbers(), htmlfmt.LineNumbersInTable()))
- if formatter == nil {
- formatter = formatters.Fallback
- }
- // Create buffer and format into it.
- buf := new(bytes.Buffer)
- err4 := formatter.Format(buf, style, lexered)
- if err4 != nil {
- c.Logger.Error().Msgf("Failed to format paste data: %s", err4.Error())
- }
- pasteData["pastedata"] = buf.String()
-
- // Get template and format it.
- pasteHTML := templater.GetTemplate(ec, "paste.html", pasteData)
-
- return ec.HTML(http.StatusOK, pasteHTML)
-}
-
-// GET for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
-func pastePasswordedVerifyGet(ec echo.Context) error {
- // We should check if database connection available.
- dbConn := c.Database.GetDatabaseConnection()
- if c.Config.Database.Type != "flatfiles" && dbConn == nil {
- return ec.Redirect(http.StatusFound, "/database_not_available")
- }
-
- pasteIDRaw := ec.Param("id")
- timestampRaw := ec.Param("timestamp")
- // We already get numbers from string, so we will not check strconv.Atoi()
- // error.
- pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
-
- // Get paste.
- paste, err1 := c.Database.GetPaste(pasteID)
- if err1 != nil {
- c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
- errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDRaw+" not found")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
-
- // Check for auth cookie. If present - redirect to paste.
- cookie, err := ec.Cookie("PASTE-" + strconv.Itoa(pasteID))
- if err == nil {
- // No cookie, redirect to auth page.
- c.Logger.Debug().Msg("Paste cookie found, checking it...")
-
- // Generate cookie value to check.
- cookieValue := paste.GenerateCryptedCookieValue()
-
- if cookieValue == cookie.Value {
- c.Logger.Info().Msg("Valid cookie, redirecting to paste page...")
- return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp"))
- }
-
- c.Logger.Debug().Msg("Invalid cookie, showing auth page")
- }
-
- // HTML data.
- htmlData := make(map[string]string)
- htmlData["pasteID"] = strconv.Itoa(pasteID)
- htmlData["pasteTimestamp"] = timestampRaw
-
- verifyHTML := templater.GetTemplate(ec, "passworded_paste_verify.html", htmlData)
-
- return ec.HTML(http.StatusOK, verifyHTML)
-}
-
-// POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
-func pastePasswordedVerifyPost(ec echo.Context) error {
- // We should check if database connection available.
- dbConn := c.Database.GetDatabaseConnection()
- if c.Config.Database.Type != "flatfiles" && dbConn == nil {
- return ec.Redirect(http.StatusFound, "/database_not_available")
- }
-
- pasteIDRaw := ec.Param("id")
- timestampRaw := ec.Param("timestamp")
- // We already get numbers from string, so we will not check strconv.Atoi()
- // error.
- pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
- c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
-
- // Get paste.
- paste, err1 := c.Database.GetPaste(pasteID)
- if err1 != nil {
- c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
- errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
-
- params, err2 := ec.FormParams()
- if err2 != nil {
- c.Logger.Debug().Msg("No form parameters passed")
- errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
-
- if paste.VerifyPassword(params["paste-password"][0]) {
- // Set cookie that this paste's password is verified and paste
- // can be viewed.
- cookie := new(http.Cookie)
- cookie.Name = "PASTE-" + strconv.Itoa(pasteID)
- cookie.Value = paste.GenerateCryptedCookieValue()
- cookie.Expires = time.Now().Add(24 * time.Hour)
- ec.SetCookie(cookie)
-
- return ec.Redirect(http.StatusFound, "/paste/"+strconv.Itoa(pasteID)+"/"+timestampRaw)
- }
-
- errtpl := templater.GetErrorTemplate(ec, "Invalid password. Please, try again.")
- return ec.HTML(http.StatusBadRequest, string(errtpl))
-}
-
-// POST for "/paste/" which will create new paste and redirect to
-// "/pastes/CREATED_PASTE_ID".
-func pastePOST(ec echo.Context) error {
- // We should check if database connection available.
- dbConn := c.Database.GetDatabaseConnection()
- if c.Config.Database.Type != "flatfiles" && dbConn == nil {
- return ec.Redirect(http.StatusFound, "/database_not_available")
- }
-
- params, err := ec.FormParams()
- if err != nil {
- errtpl := templater.GetErrorTemplate(ec, "Cannot create empty paste")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
- c.Logger.Debug().Msgf("Received parameters: %+v", params)
-
- // Do nothing if paste contents is empty.
- if len(params["paste-contents"][0]) == 0 {
- c.Logger.Debug().Msg("Empty paste submitted, ignoring")
- errtpl := templater.GetErrorTemplate(ec, "Empty pastes aren't allowed.")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
-
- if !strings.ContainsAny(params["paste-keep-for"][0], "Mmhd") && params["paste-keep-for"][0] != "forever" {
- c.Logger.Debug().Msgf("'Keep paste for' field have invalid value: %s", params["paste-keep-for"][0])
- errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
-
- // Verify captcha.
- if !captcha.Verify(params["paste-captcha-id"][0], params["paste-captcha-solution"][0]) {
- c.Logger.Debug().Msgf("Invalid captcha solution for captcha ID '%s': %s", params["paste-captcha-id"][0], params["paste-captcha-solution"][0])
- errtpl := templater.GetErrorTemplate(ec, "Invalid captcha solution.")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
-
- paste := &structs.Paste{
- Title: params["paste-title"][0],
- Data: params["paste-contents"][0],
- Language: params["paste-language"][0],
- }
-
- // Paste creation time in UTC.
- createdAt := time.Now().UTC()
- paste.CreatedAt = &createdAt
-
- // Parse "keep for" field.
- // Defaulting to "forever".
- keepFor := 0
- keepForUnit := 0
- if params["paste-keep-for"][0] != "forever" {
- keepForUnitRegex := regexp.MustCompile("[Mmhd]")
-
- keepForRaw := regexInts.FindAllString(params["paste-keep-for"][0], 1)[0]
- var err error
- keepFor, err = strconv.Atoi(keepForRaw)
- if err != nil {
- if params["paste-keep-for"][0] == "forever" {
- c.Logger.Debug().Msg("Keeping paste forever!")
- keepFor = 0
- } else {
- c.Logger.Debug().Err(err).Msg("Failed to parse 'Keep for' integer")
- errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
- }
-
- keepForUnitRaw := keepForUnitRegex.FindAllString(params["paste-keep-for"][0], 1)[0]
- keepForUnit = structs.PASTE_KEEPS_CORELLATION[keepForUnitRaw]
- }
- paste.KeepFor = keepFor
- paste.KeepForUnitType = keepForUnit
-
- // Try to autodetect if it was selected.
- if params["paste-language"][0] == "autodetect" {
- lexer := lexers.Analyse(params["paste-language"][0])
- if lexer != nil {
- paste.Language = lexer.Config().Name
- } else {
- paste.Language = "text"
- }
- }
-
- // Private paste?
- paste.Private = false
- privateCheckbox, privateCheckboxFound := params["paste-private"]
- pastePassword, pastePasswordFound := params["paste-password"]
- if privateCheckboxFound && privateCheckbox[0] == "on" || pastePasswordFound && pastePassword[0] != "" {
- paste.Private = true
- }
-
- if pastePassword[0] != "" {
- paste.CreatePassword(pastePassword[0])
- }
-
- id, err2 := c.Database.SavePaste(paste)
- if err2 != nil {
- c.Logger.Error().Msgf("Failed to save paste: %s", err2.Error())
- errtpl := templater.GetErrorTemplate(ec, "Failed to save paste. Please, try again later.")
- return ec.HTML(http.StatusBadRequest, errtpl)
- }
-
- newPasteIDAsString := strconv.FormatInt(id, 10)
- c.Logger.Debug().Msgf("Paste saved, URL: /paste/" + newPasteIDAsString)
-
- // Private pastes have it's timestamp in URL.
- if paste.Private {
- return ec.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString+"/"+strconv.FormatInt(paste.CreatedAt.Unix(), 10))
- }
-
- return ec.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString)
-}
-
-// GET for "/pastes/:id/raw", raw paste output.
-func pasteRawGET(ec echo.Context) error {
- // We should check if database connection available.
- dbConn := c.Database.GetDatabaseConnection()
- if c.Config.Database.Type != "flatfiles" && dbConn == nil {
- return ec.Redirect(http.StatusFound, "/database_not_available/raw")
- }
-
- pasteIDRaw := ec.Param("id")
- // We already get numbers from string, so we will not check strconv.Atoi()
- // error.
- pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
- c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
-
- // Get paste.
- paste, err1 := c.Database.GetPaste(pasteID)
- if err1 != nil {
- c.Logger.Error().Msgf("Failed to get paste #%d from database: %s", pasteID, err1.Error())
- return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
- }
-
- if paste.IsExpired() {
- c.Logger.Error().Msgf("Paste #%d is expired", pasteID)
- return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
- }
-
- // Check if we have a private paste and it's parameters are correct.
- if paste.Private {
- tsProvidedStr := ec.Param("timestamp")
- tsProvided, err2 := strconv.ParseInt(tsProvidedStr, 10, 64)
- if err2 != nil {
- c.Logger.Error().Msgf("Invalid timestamp '%s' provided for getting private paste #%d: %s", tsProvidedStr, pasteID, err2.Error())
- return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
- }
- pasteTs := paste.CreatedAt.Unix()
- if tsProvided != pasteTs {
- c.Logger.Error().Msgf("Incorrect timestamp '%v' provided for private paste #%d, waiting for %v", tsProvidedStr, pasteID, strconv.FormatInt(pasteTs, 10))
- return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
- }
- }
-
- // ToDo: figure out how to handle passworded pastes here.
- // Return error for now.
- if paste.Password != "" {
- c.Logger.Error().Msgf("Cannot render paste #%d as raw: passworded paste. Patches welcome!", pasteID)
- return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
- }
-
- return ec.String(http.StatusOK, paste.Data)
-}
-
-// GET for "/pastes/", a list of publicly available pastes.
-func pastesGET(ec echo.Context) error {
- // We should check if database connection available.
- dbConn := c.Database.GetDatabaseConnection()
- if c.Config.Database.Type != "flatfiles" && dbConn == nil {
- return ec.Redirect(http.StatusFound, "/database_not_available")
- }
-
- pageFromParamRaw := ec.Param("page")
- var page = 1
- if pageFromParamRaw != "" {
- pageRaw := regexInts.FindAllString(pageFromParamRaw, 1)[0]
- page, _ = strconv.Atoi(pageRaw)
- }
-
- c.Logger.Debug().Msgf("Requested page #%d", page)
-
- // Get pastes IDs.
- pastes, err3 := c.Database.GetPagedPastes(page)
- c.Logger.Debug().Msgf("Got %d pastes", len(pastes))
-
- var pastesString = "No pastes to show."
-
- // Show "No pastes to show" on any error for now.
- if err3 != nil {
- c.Logger.Error().Msgf("Failed to get pastes list from database: %s", err3.Error())
- noPastesToShowTpl := templater.GetErrorTemplate(ec, "No pastes to show.")
- return ec.HTML(http.StatusOK, noPastesToShowTpl)
- }
-
- if len(pastes) > 0 {
- pastesString = ""
- for i := range pastes {
- pasteDataMap := make(map[string]string)
- pasteDataMap["pasteID"] = strconv.Itoa(pastes[i].ID)
- pasteDataMap["pasteTitle"] = pastes[i].Title
- pasteDataMap["pasteDate"] = pastes[i].CreatedAt.Format("2006-01-02 @ 15:04:05") + " UTC"
-
- // Get max 4 lines of each paste.
- pasteDataSplitted := strings.Split(pastes[i].Data, "\n")
- var pasteData = ""
- if len(pasteDataSplitted) < 4 {
- pasteData = pastes[i].Data
- } else {
- pasteData = strings.Join(pasteDataSplitted[0:4], "\n")
- }
-
- pasteDataMap["pasteData"] = pasteData
- pasteTpl := templater.GetRawTemplate(ec, "pastelist_paste.html", pasteDataMap)
-
- pastesString += pasteTpl
- }
- }
-
- // Pagination.
- pages := c.Database.GetPastesPages()
- c.Logger.Debug().Msgf("Total pages: %d, current: %d", pages, page)
- paginationHTML := pagination.CreateHTML(page, pages, "/pastes/")
-
- pasteListTpl := templater.GetTemplate(ec, "pastelist_list.html", map[string]string{"pastes": pastesString, "pagination": paginationHTML})
-
- return ec.HTML(http.StatusOK, string(pasteListTpl))
-}
diff --git a/domains/pastes/exported.go b/domains/pastes/exported.go
index c216253..37e5a72 100644
--- a/domains/pastes/exported.go
+++ b/domains/pastes/exported.go
@@ -25,10 +25,17 @@
package pastes
import (
+ // stdlib
+ "regexp"
+
// local
"gitlab.com/pztrn/fastpastebin/internal/context"
)
+var (
+ regexInts = regexp.MustCompile("[0-9]+")
+)
+
var (
c *context.Context
)
@@ -38,22 +45,22 @@ var (
func New(cc *context.Context) {
c = cc
+ ////////////////////////////////////////////////////////////
+ // HTTP endpoints.
+ ////////////////////////////////////////////////////////////
// New paste.
- c.Echo.POST("/paste/", pastePOST)
-
+ c.Echo.POST("/paste/", pastePOSTWebInterface)
// Show public paste.
- c.Echo.GET("/paste/:id", pasteGET)
+ c.Echo.GET("/paste/:id", pasteGETWebInterface)
// Show RAW representation of public paste.
- c.Echo.GET("/paste/:id/raw", pasteRawGET)
-
+ c.Echo.GET("/paste/:id/raw", pasteRawGETWebInterface)
// Show private paste.
- c.Echo.GET("/paste/:id/:timestamp", pasteGET)
+ c.Echo.GET("/paste/:id/:timestamp", pasteGETWebInterface)
// Show RAW representation of private paste.
- c.Echo.GET("/paste/:id/:timestamp/raw", pasteRawGET)
+ c.Echo.GET("/paste/:id/:timestamp/raw", pasteRawGETWebInterface)
// Verify access to passworded paste.
c.Echo.GET("/paste/:id/:timestamp/verify", pastePasswordedVerifyGet)
c.Echo.POST("/paste/:id/:timestamp/verify", pastePasswordedVerifyPost)
-
// Pastes list.
c.Echo.GET("/pastes/", pastesGET)
c.Echo.GET("/pastes/:page", pastesGET)
diff --git a/domains/pastes/paste_get.go b/domains/pastes/paste_get.go
new file mode 100644
index 0000000..1b0bdf7
--- /dev/null
+++ b/domains/pastes/paste_get.go
@@ -0,0 +1,329 @@
+package pastes
+
+import (
+ // stdlib
+ "bytes"
+ "net/http"
+ "strconv"
+ "time"
+
+ // local
+ "gitlab.com/pztrn/fastpastebin/internal/structs"
+ "gitlab.com/pztrn/fastpastebin/internal/templater"
+
+ // other
+ "github.com/alecthomas/chroma"
+ "github.com/alecthomas/chroma/formatters"
+ htmlfmt "github.com/alecthomas/chroma/formatters/html"
+ "github.com/alecthomas/chroma/lexers"
+ "github.com/alecthomas/chroma/styles"
+ "github.com/labstack/echo"
+)
+
+const (
+ pasteCookieInvalid = "PASTE_COOKIE_INVALID"
+ pasteExpired = "PASTE_EXPIRED"
+ pasteNotFound = "PASTE_NOT_FOUND"
+ pasteTimestampInvalid = "PASTE_TIMESTAMP_INVALID"
+)
+
+// Actual getting paste data and returns it's content without formatting.
+// This function will return paste's structure and optional error string
+// that defined in constants above.
+// Actually required only paste ID, all other parameters are optional
+// for some cases, e.g. public paste won't check for timestamp and cookie
+// value (they both will be ignored), but private will.
+func pasteGetData(pasteID int, timestamp int64, cookieValue string) (*structs.Paste, string) {
+ // We should check if database connection available.
+ //dbConn := c.Database.GetDatabaseConnection()
+ //if c.Config.Database.Type != "flatfiles" && dbConn == nil {
+ // return ec.Redirect(http.StatusFound, "/database_not_available")
+ //}
+
+ // Get paste.
+ paste, err1 := c.Database.GetPaste(pasteID)
+ if err1 != nil {
+ c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
+ return nil, pasteNotFound
+ }
+
+ // Check if paste is expired.
+ if paste.IsExpired() {
+ c.Logger.Error().Msgf("Paste #%d is expired", pasteID)
+ return nil, pasteExpired
+ }
+
+ // Check if we have a private paste and it's parameters are correct.
+ if paste.Private {
+ pasteTs := paste.CreatedAt.Unix()
+ if timestamp != pasteTs {
+ c.Logger.Error().Msgf("Incorrect timestamp '%d' provided for private paste #%d, waiting for %d", timestamp, pasteID, pasteTs)
+ return nil, pasteTimestampInvalid
+ }
+ }
+
+ // If we have a private paste requested and password for that paste
+ // was defined - check additional things that required to view this
+ // paste.
+ if paste.Private && paste.Password != "" {
+ // Generate cookie value to check.
+ pasteCookieValue := paste.GenerateCryptedCookieValue()
+
+ if cookieValue != pasteCookieValue {
+ return nil, pasteCookieInvalid
+ }
+ }
+
+ return paste, ""
+}
+
+// GET for "/api/paste/PASTE_ID" and "/api/paste/PASTE_ID/TIMESTAMP".
+func pasteGETApi(ec echo.Context) error {
+ return nil
+}
+
+// GET for "/paste/PASTE_ID" and "/paste/PASTE_ID/TIMESTAMP" (private pastes).
+// Web interface version.
+func pasteGETWebInterface(ec echo.Context) error {
+ pasteIDRaw := ec.Param("id")
+ // We already get numbers from string, so we will not check strconv.Atoi()
+ // error.
+ pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
+ pasteIDStr := strconv.Itoa(pasteID)
+ c.Logger.Debug().Msgf("Trying to get paste #%+v", pasteID)
+
+ // Check if we have timestamp passed.
+ // If passed timestamp is invalid (isn't a real UNIX timestamp) we
+ // will show 404 Not Found error and spam about that in logs.
+ var timestamp int64
+ tsProvidedStr := ec.Param("timestamp")
+ if tsProvidedStr != "" {
+ tsProvided, err := strconv.ParseInt(tsProvidedStr, 10, 64)
+ if err != nil {
+ c.Logger.Error().Err(err).Msgf("Invalid timestamp '%s' provided for getting private paste #%d", tsProvidedStr, pasteID)
+ errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDStr+" not found")
+ return ec.HTML(http.StatusBadRequest, errtpl)
+ } else {
+ timestamp = tsProvided
+ }
+ }
+
+ // Check if we have "PASTE-PASTEID" cookie defined. It is required
+ // for private pastes.
+ var cookieValue string
+ cookie, err1 := ec.Cookie("PASTE-" + pasteIDStr)
+ if err1 == nil {
+ cookieValue = cookie.Value
+ }
+
+ paste, error := pasteGetData(pasteID, timestamp, cookieValue)
+
+ // For these cases we should return 404 Not Found page.
+ if error == pasteExpired || error == pasteNotFound || error == pasteTimestampInvalid {
+ errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDRaw+" not found")
+ return ec.HTML(http.StatusNotFound, errtpl)
+ }
+
+ // If passed cookie value was invalid - go to paste authorization
+ // page.
+ if error == pasteCookieInvalid {
+ c.Logger.Info().Msg("Invalid cookie, redirecting to auth page...")
+ return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDStr+"/"+ec.Param("timestamp")+"/verify")
+ }
+
+ // Format paste data map.
+ pasteData := make(map[string]string)
+ pasteData["pasteTitle"] = paste.Title
+ pasteData["pasteID"] = strconv.Itoa(paste.ID)
+ pasteData["pasteDate"] = paste.CreatedAt.Format("2006-01-02 @ 15:04:05") + " UTC"
+ pasteData["pasteLanguage"] = paste.Language
+
+ pasteExpirationString := "Never"
+ if paste.KeepFor != 0 && paste.KeepForUnitType != 0 {
+ pasteExpirationString = paste.GetExpirationTime().Format("2006-01-02 @ 15:04:05") + " UTC"
+ }
+ pasteData["pasteExpiration"] = pasteExpirationString
+
+ if paste.Private {
+ pasteData["pasteType"] = "Private"
+ pasteData["pasteTs"] = strconv.FormatInt(paste.CreatedAt.Unix(), 10) + "/"
+ } else {
+ pasteData["pasteType"] = "Public"
+ pasteData["pasteTs"] = ""
+ }
+
+ // Highlight.
+ // Get lexer.
+ lexer := lexers.Get(paste.Language)
+ if lexer == nil {
+ lexer = lexers.Fallback
+ }
+ // Tokenize paste data.
+ lexered, err3 := lexer.Tokenise(nil, paste.Data)
+ if err3 != nil {
+ c.Logger.Error().Msgf("Failed to tokenize paste data: %s", err3.Error())
+ }
+ // Get style for HTML output.
+ style := styles.Get("monokai")
+ if style == nil {
+ style = styles.Fallback
+ }
+ // Get HTML formatter.
+ formatter := chroma.Formatter(htmlfmt.New(htmlfmt.WithLineNumbers(), htmlfmt.LineNumbersInTable()))
+ if formatter == nil {
+ formatter = formatters.Fallback
+ }
+ // Create buffer and format into it.
+ buf := new(bytes.Buffer)
+ err4 := formatter.Format(buf, style, lexered)
+ if err4 != nil {
+ c.Logger.Error().Msgf("Failed to format paste data: %s", err4.Error())
+ }
+ pasteData["pastedata"] = buf.String()
+
+ // Get template and format it.
+ pasteHTML := templater.GetTemplate(ec, "paste.html", pasteData)
+
+ return ec.HTML(http.StatusOK, pasteHTML)
+}
+
+// GET for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
+func pastePasswordedVerifyGet(ec echo.Context) error {
+ pasteIDRaw := ec.Param("id")
+ timestampRaw := ec.Param("timestamp")
+ // We already get numbers from string, so we will not check strconv.Atoi()
+ // error.
+ pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
+
+ // Get paste.
+ paste, err1 := c.Database.GetPaste(pasteID)
+ if err1 != nil {
+ c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
+ errtpl := templater.GetErrorTemplate(ec, "Paste #"+pasteIDRaw+" not found")
+ return ec.HTML(http.StatusBadRequest, errtpl)
+ }
+
+ // Check for auth cookie. If present - redirect to paste.
+ cookie, err := ec.Cookie("PASTE-" + strconv.Itoa(pasteID))
+ if err == nil {
+ // No cookie, redirect to auth page.
+ c.Logger.Debug().Msg("Paste cookie found, checking it...")
+
+ // Generate cookie value to check.
+ cookieValue := paste.GenerateCryptedCookieValue()
+
+ if cookieValue == cookie.Value {
+ c.Logger.Info().Msg("Valid cookie, redirecting to paste page...")
+ return ec.Redirect(http.StatusMovedPermanently, "/paste/"+pasteIDRaw+"/"+ec.Param("timestamp"))
+ }
+
+ c.Logger.Debug().Msg("Invalid cookie, showing auth page")
+ }
+
+ // HTML data.
+ htmlData := make(map[string]string)
+ htmlData["pasteID"] = strconv.Itoa(pasteID)
+ htmlData["pasteTimestamp"] = timestampRaw
+
+ verifyHTML := templater.GetTemplate(ec, "passworded_paste_verify.html", htmlData)
+
+ return ec.HTML(http.StatusOK, verifyHTML)
+}
+
+// POST for "/paste/PASTE_ID/TIMESTAMP/verify" - a password verify page.
+func pastePasswordedVerifyPost(ec echo.Context) error {
+ // We should check if database connection available.
+ dbConn := c.Database.GetDatabaseConnection()
+ if c.Config.Database.Type != "flatfiles" && dbConn == nil {
+ return ec.Redirect(http.StatusFound, "/database_not_available")
+ }
+
+ pasteIDRaw := ec.Param("id")
+ timestampRaw := ec.Param("timestamp")
+ // We already get numbers from string, so we will not check strconv.Atoi()
+ // error.
+ pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
+ c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
+
+ // Get paste.
+ paste, err1 := c.Database.GetPaste(pasteID)
+ if err1 != nil {
+ c.Logger.Error().Msgf("Failed to get paste #%d: %s", pasteID, err1.Error())
+ errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
+ return ec.HTML(http.StatusBadRequest, errtpl)
+ }
+
+ params, err2 := ec.FormParams()
+ if err2 != nil {
+ c.Logger.Debug().Msg("No form parameters passed")
+ errtpl := templater.GetErrorTemplate(ec, "Paste #"+strconv.Itoa(pasteID)+" not found")
+ return ec.HTML(http.StatusBadRequest, errtpl)
+ }
+
+ if paste.VerifyPassword(params["paste-password"][0]) {
+ // Set cookie that this paste's password is verified and paste
+ // can be viewed.
+ cookie := new(http.Cookie)
+ cookie.Name = "PASTE-" + strconv.Itoa(pasteID)
+ cookie.Value = paste.GenerateCryptedCookieValue()
+ cookie.Expires = time.Now().Add(24 * time.Hour)
+ ec.SetCookie(cookie)
+
+ return ec.Redirect(http.StatusFound, "/paste/"+strconv.Itoa(pasteID)+"/"+timestampRaw)
+ }
+
+ errtpl := templater.GetErrorTemplate(ec, "Invalid password. Please, try again.")
+ return ec.HTML(http.StatusBadRequest, string(errtpl))
+}
+
+// GET for "/pastes/:id/raw", raw paste output.
+// Web interface version.
+func pasteRawGETWebInterface(ec echo.Context) error {
+ // We should check if database connection available.
+ dbConn := c.Database.GetDatabaseConnection()
+ if c.Config.Database.Type != "flatfiles" && dbConn == nil {
+ return ec.Redirect(http.StatusFound, "/database_not_available/raw")
+ }
+
+ pasteIDRaw := ec.Param("id")
+ // We already get numbers from string, so we will not check strconv.Atoi()
+ // error.
+ pasteID, _ := strconv.Atoi(regexInts.FindAllString(pasteIDRaw, 1)[0])
+ c.Logger.Debug().Msgf("Requesting paste #%+v", pasteID)
+
+ // Get paste.
+ paste, err1 := c.Database.GetPaste(pasteID)
+ if err1 != nil {
+ c.Logger.Error().Msgf("Failed to get paste #%d from database: %s", pasteID, err1.Error())
+ return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
+ }
+
+ if paste.IsExpired() {
+ c.Logger.Error().Msgf("Paste #%d is expired", pasteID)
+ return ec.HTML(http.StatusBadRequest, "Paste #"+pasteIDRaw+" does not exist.")
+ }
+
+ // Check if we have a private paste and it's parameters are correct.
+ if paste.Private {
+ tsProvidedStr := ec.Param("timestamp")
+ tsProvided, err2 := strconv.ParseInt(tsProvidedStr, 10, 64)
+ if err2 != nil {
+ c.Logger.Error().Msgf("Invalid timestamp '%s' provided for getting private paste #%d: %s", tsProvidedStr, pasteID, err2.Error())
+ return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
+ }
+ pasteTs := paste.CreatedAt.Unix()
+ if tsProvided != pasteTs {
+ c.Logger.Error().Msgf("Incorrect timestamp '%v' provided for private paste #%d, waiting for %v", tsProvidedStr, pasteID, strconv.FormatInt(pasteTs, 10))
+ return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
+ }
+ }
+
+ // ToDo: figure out how to handle passworded pastes here.
+ // Return error for now.
+ if paste.Password != "" {
+ c.Logger.Error().Msgf("Cannot render paste #%d as raw: passworded paste. Patches welcome!", pasteID)
+ return ec.String(http.StatusBadRequest, "Paste #"+pasteIDRaw+" not found")
+ }
+
+ return ec.String(http.StatusOK, paste.Data)
+}
diff --git a/domains/pastes/paste_post.go b/domains/pastes/paste_post.go
new file mode 100644
index 0000000..a3870a3
--- /dev/null
+++ b/domains/pastes/paste_post.go
@@ -0,0 +1,133 @@
+package pastes
+
+import (
+ // stdlib
+ "net/http"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ // local
+ "gitlab.com/pztrn/fastpastebin/internal/captcha"
+ "gitlab.com/pztrn/fastpastebin/internal/structs"
+ "gitlab.com/pztrn/fastpastebin/internal/templater"
+
+ // other
+ "github.com/alecthomas/chroma/lexers"
+ "github.com/labstack/echo"
+)
+
+// POST for "/paste/" which will create new paste and redirect to
+// "/pastes/CREATED_PASTE_ID". This handler will do all the job for
+// requests comes from browsers via web interface.
+func pastePOSTWebInterface(ec echo.Context) error {
+ // We should check if database connection available.
+ dbConn := c.Database.GetDatabaseConnection()
+ if c.Config.Database.Type != "flatfiles" && dbConn == nil {
+ return ec.Redirect(http.StatusFound, "/database_not_available")
+ }
+
+ params, err := ec.FormParams()
+ if err != nil {
+ errtpl := templater.GetErrorTemplate(ec, "Cannot create empty paste")
+ return ec.HTML(http.StatusBadRequest, errtpl)
+ }
+ c.Logger.Debug().Msgf("Received parameters: %+v", params)
+
+ // Do nothing if paste contents is empty.
+ if len(params["paste-contents"][0]) == 0 {
+ c.Logger.Debug().Msg("Empty paste submitted, ignoring")
+ errtpl := templater.GetErrorTemplate(ec, "Empty pastes aren't allowed.")
+ return ec.HTML(http.StatusBadRequest, errtpl)
+ }
+
+ if !strings.ContainsAny(params["paste-keep-for"][0], "Mmhd") && params["paste-keep-for"][0] != "forever" {
+ c.Logger.Debug().Msgf("'Keep paste for' field have invalid value: %s", params["paste-keep-for"][0])
+ errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
+ return ec.HTML(http.StatusBadRequest, errtpl)
+ }
+
+ // Verify captcha.
+ if !captcha.Verify(params["paste-captcha-id"][0], params["paste-captcha-solution"][0]) {
+ c.Logger.Debug().Msgf("Invalid captcha solution for captcha ID '%s': %s", params["paste-captcha-id"][0], params["paste-captcha-solution"][0])
+ errtpl := templater.GetErrorTemplate(ec, "Invalid captcha solution.")
+ return ec.HTML(http.StatusBadRequest, errtpl)
+ }
+
+ paste := &structs.Paste{
+ Title: params["paste-title"][0],
+ Data: params["paste-contents"][0],
+ Language: params["paste-language"][0],
+ }
+
+ // Paste creation time in UTC.
+ createdAt := time.Now().UTC()
+ paste.CreatedAt = &createdAt
+
+ // Parse "keep for" field.
+ // Defaulting to "forever".
+ keepFor := 0
+ keepForUnit := 0
+ if params["paste-keep-for"][0] != "forever" {
+ keepForUnitRegex := regexp.MustCompile("[Mmhd]")
+
+ keepForRaw := regexInts.FindAllString(params["paste-keep-for"][0], 1)[0]
+ var err error
+ keepFor, err = strconv.Atoi(keepForRaw)
+ if err != nil {
+ if params["paste-keep-for"][0] == "forever" {
+ c.Logger.Debug().Msg("Keeping paste forever!")
+ keepFor = 0
+ } else {
+ c.Logger.Debug().Err(err).Msg("Failed to parse 'Keep for' integer")
+ errtpl := templater.GetErrorTemplate(ec, "Invalid 'Paste should be available for' parameter passed. Please do not try to hack us ;).")
+ return ec.HTML(http.StatusBadRequest, errtpl)
+ }
+ }
+
+ keepForUnitRaw := keepForUnitRegex.FindAllString(params["paste-keep-for"][0], 1)[0]
+ keepForUnit = structs.PASTE_KEEPS_CORELLATION[keepForUnitRaw]
+ }
+ paste.KeepFor = keepFor
+ paste.KeepForUnitType = keepForUnit
+
+ // Try to autodetect if it was selected.
+ if params["paste-language"][0] == "autodetect" {
+ lexer := lexers.Analyse(params["paste-language"][0])
+ if lexer != nil {
+ paste.Language = lexer.Config().Name
+ } else {
+ paste.Language = "text"
+ }
+ }
+
+ // Private paste?
+ paste.Private = false
+ privateCheckbox, privateCheckboxFound := params["paste-private"]
+ pastePassword, pastePasswordFound := params["paste-password"]
+ if privateCheckboxFound && privateCheckbox[0] == "on" || pastePasswordFound && pastePassword[0] != "" {
+ paste.Private = true
+ }
+
+ if pastePassword[0] != "" {
+ paste.CreatePassword(pastePassword[0])
+ }
+
+ id, err2 := c.Database.SavePaste(paste)
+ if err2 != nil {
+ c.Logger.Error().Msgf("Failed to save paste: %s", err2.Error())
+ errtpl := templater.GetErrorTemplate(ec, "Failed to save paste. Please, try again later.")
+ return ec.HTML(http.StatusBadRequest, errtpl)
+ }
+
+ newPasteIDAsString := strconv.FormatInt(id, 10)
+ c.Logger.Debug().Msgf("Paste saved, URL: /paste/" + newPasteIDAsString)
+
+ // Private pastes have it's timestamp in URL.
+ if paste.Private {
+ return ec.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString+"/"+strconv.FormatInt(paste.CreatedAt.Unix(), 10))
+ }
+
+ return ec.Redirect(http.StatusFound, "/paste/"+newPasteIDAsString)
+}
diff --git a/domains/pastes/pastes_get.go b/domains/pastes/pastes_get.go
new file mode 100644
index 0000000..a908b9b
--- /dev/null
+++ b/domains/pastes/pastes_get.go
@@ -0,0 +1,104 @@
+// Fast Paste Bin - uberfast and easy-to-use pastebin.
+//
+// Copyright (c) 2018, Stanislav N. aka pztrn and Fast Paste Bin
+// developers.
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject
+// to the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
+// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+package pastes
+
+import (
+ // stdlib
+ "net/http"
+ "strconv"
+ "strings"
+
+ // local
+ "gitlab.com/pztrn/fastpastebin/internal/pagination"
+ "gitlab.com/pztrn/fastpastebin/internal/templater"
+
+ // other
+ "github.com/labstack/echo"
+)
+
+// GET for "/pastes/", a list of publicly available pastes.
+// Web inteface version.
+func pastesGET(ec echo.Context) error {
+ // We should check if database connection available.
+ dbConn := c.Database.GetDatabaseConnection()
+ if c.Config.Database.Type != "flatfiles" && dbConn == nil {
+ return ec.Redirect(http.StatusFound, "/database_not_available")
+ }
+
+ pageFromParamRaw := ec.Param("page")
+ var page = 1
+ if pageFromParamRaw != "" {
+ pageRaw := regexInts.FindAllString(pageFromParamRaw, 1)[0]
+ page, _ = strconv.Atoi(pageRaw)
+ }
+
+ c.Logger.Debug().Msgf("Requested page #%d", page)
+
+ // Get pastes IDs.
+ pastes, err3 := c.Database.GetPagedPastes(page)
+ c.Logger.Debug().Msgf("Got %d pastes", len(pastes))
+
+ var pastesString = "No pastes to show."
+
+ // Show "No pastes to show" on any error for now.
+ if err3 != nil {
+ c.Logger.Error().Msgf("Failed to get pastes list from database: %s", err3.Error())
+ noPastesToShowTpl := templater.GetErrorTemplate(ec, "No pastes to show.")
+ return ec.HTML(http.StatusOK, noPastesToShowTpl)
+ }
+
+ if len(pastes) > 0 {
+ pastesString = ""
+ for i := range pastes {
+ pasteDataMap := make(map[string]string)
+ pasteDataMap["pasteID"] = strconv.Itoa(pastes[i].ID)
+ pasteDataMap["pasteTitle"] = pastes[i].Title
+ pasteDataMap["pasteDate"] = pastes[i].CreatedAt.Format("2006-01-02 @ 15:04:05") + " UTC"
+
+ // Get max 4 lines of each paste.
+ pasteDataSplitted := strings.Split(pastes[i].Data, "\n")
+ var pasteData = ""
+ if len(pasteDataSplitted) < 4 {
+ pasteData = pastes[i].Data
+ } else {
+ pasteData = strings.Join(pasteDataSplitted[0:4], "\n")
+ }
+
+ pasteDataMap["pasteData"] = pasteData
+ pasteTpl := templater.GetRawTemplate(ec, "pastelist_paste.html", pasteDataMap)
+
+ pastesString += pasteTpl
+ }
+ }
+
+ // Pagination.
+ pages := c.Database.GetPastesPages()
+ c.Logger.Debug().Msgf("Total pages: %d, current: %d", pages, page)
+ paginationHTML := pagination.CreateHTML(page, pages, "/pastes/")
+
+ pasteListTpl := templater.GetTemplate(ec, "pastelist_list.html", map[string]string{"pastes": pastesString, "pagination": paginationHTML})
+
+ return ec.HTML(http.StatusOK, string(pasteListTpl))
+}
diff --git a/internal/context/exported.go b/internal/context/exported.go
index 0fb13d7..9c3cf22 100644
--- a/internal/context/exported.go
+++ b/internal/context/exported.go
@@ -26,7 +26,7 @@ package context
const (
// Version .
- Version = "0.3.0"
+ Version = "0.3.1"
)
// New creates new context.
diff --git a/internal/structs/model.go b/internal/structs/paste.go
similarity index 100%
rename from internal/structs/model.go
rename to internal/structs/paste.go