Add ability to configure the application; reset db; better logging

This commit is contained in:
Vikas
2024-06-02 19:42:26 +05:30
parent bc11d72720
commit 0f23e11414
12 changed files with 113 additions and 28 deletions

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
bin/
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

8
config/config.go Normal file
View File

@@ -0,0 +1,8 @@
package config
var (
ServerAddr = ":8080"
AppName = "PastePass"
DBPath = "pastes.boltdb"
ResetDB = false
)

View File

@@ -3,7 +3,7 @@ package db
import ( import (
"errors" "errors"
"fmt" "fmt"
"log" "log/slog"
"time" "time"
"encoding/json" "encoding/json"
@@ -26,15 +26,6 @@ type DB struct {
boltDB *bolt.DB boltDB *bolt.DB
} }
func NewDB(name string) (*DB, error) {
boltDB, err := bolt.Open(name, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return nil, err
}
return &DB{boltDB: boltDB}, nil
}
func (d *DB) Close() error { func (d *DB) Close() error {
return d.boltDB.Close() return d.boltDB.Close()
} }
@@ -106,7 +97,7 @@ func (d *DB) Get(id string) (*Paste, error) {
func (d *DB) Decrypt(id string, key string) (string, error) { func (d *DB) Decrypt(id string, key string) (string, error) {
// delete paste if expired // delete paste if expired
if _, err := d.Get(id); err == ErrPasteExpired { if _, err := d.Get(id); errors.Is(err, ErrPasteExpired) {
return "", d.Delete(id) return "", d.Delete(id)
} }
@@ -186,7 +177,7 @@ func (d *DB) DeleteExpired() error {
for _, id := range expiredPastes { for _, id := range expiredPastes {
if err := d.Delete(id); err != nil { if err := d.Delete(id); err != nil {
log.Println(fmt.Errorf("error deleting expired paste %s: %v", id, err)) slog.Error("error_deleting_expired_paste", "id", id, "error", err)
} }
} }
@@ -199,7 +190,7 @@ func (d *DB) DeleteExpiredPeriodically(interval time.Duration) {
for range ticker.C { for range ticker.C {
if err := d.DeleteExpired(); err != nil { if err := d.DeleteExpired(); err != nil {
log.Println(fmt.Errorf("error deleting expired pastes: %v", err)) slog.Error("error_starting_expired_paste_job", "error", err)
} }
} }
} }

36
db/utils.go Normal file
View File

@@ -0,0 +1,36 @@
package db
import (
"github.com/boltdb/bolt"
"log/slog"
"os"
"time"
)
func NewDB(path string, reset bool) (*DB, error) {
if reset {
removeDB(path)
}
boltDB, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return nil, err
}
return &DB{boltDB: boltDB}, nil
}
func removeDB(path string) {
slog.Info("resetting_db", "path", path)
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
slog.Error("db_does_not_exist", "path", path, "error", err)
return
}
if err := os.Remove(path); err != nil {
slog.Error("error_removing_db", "path", path, "error", err)
return
}
slog.Info("db_removed", "path", path)
}

15
main.go
View File

@@ -1,7 +1,10 @@
package main package main
import ( import (
"flag"
"github.com/v1k45/pastepass/config"
"log" "log"
"log/slog"
"net/http" "net/http"
"time" "time"
@@ -10,14 +13,22 @@ import (
) )
func main() { func main() {
flag.StringVar(&config.ServerAddr, "server-addr", config.ServerAddr, "The server address to listen on")
flag.StringVar(&config.AppName, "app-name", config.AppName, "The name of the application (e.g. ACME PastePass)")
flag.StringVar(&config.DBPath, "db-path", config.DBPath, "The path to the database file")
flag.BoolVar(&config.ResetDB, "reset-db", config.ResetDB, "Reset the database on startup")
flag.Parse()
// Open the database // Open the database
boltdb, err := db.NewDB("pastes.boltdb") boltdb, err := db.NewDB(config.DBPath, config.ResetDB)
if err != nil { if err != nil {
log.Fatalf("failed to open database: %v", err) log.Fatalf("failed to open database: %v", err)
} }
go boltdb.DeleteExpiredPeriodically(time.Minute * 5) go boltdb.DeleteExpiredPeriodically(time.Minute * 5)
slog.Info("starting_server", "server_addr", config.ServerAddr, "app_name", config.AppName, "db_name", config.DBPath)
// Start the web server // Start the web server
handler := web.NewHandler(boltdb) handler := web.NewHandler(boltdb)
http.ListenAndServe(":8080", handler.Router()) http.ListenAndServe(config.ServerAddr, handler.Router())
} }

View File

@@ -1,5 +1,7 @@
package views package views
import "github.com/v1k45/pastepass/config"
templ base() { templ base() {
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
@@ -8,14 +10,14 @@ templ base() {
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
<link rel="stylesheet" href="/static/pico.min.css"/> <link rel="stylesheet" href="/static/pico.min.css"/>
<title>Paste</title> <title>{ config.AppName } - secure, one-time paste bin.</title>
</head> </head>
<body> <body>
<main class="container"> <main class="container">
<nav> <nav>
<ul> <ul>
<li> <li>
<a href="/">PastePass</a> &mdash; secure one-time paste bin. <a href="/">{ config.AppName }</a> &mdash; secure one-time paste bin.
</li> </li>
</ul> </ul>
</nav> </nav>
@@ -25,7 +27,7 @@ templ base() {
<footer> <footer>
<small> <small>
<p style="color: #8891A4;"> <p style="color: #8891A4;">
PastePass is open-source and free to use. <a href="https://github.com/v1k45/pastepass">View source on github</a>. <a href="https://github.com/v1k45/pastepass">PastePass</a> is open-source and free to use. Created by <a href="https://github.com/v1k45/pastepass">v1k45</a>.
</p> </p>
<p style="color: #8891A4;"> <p style="color: #8891A4;">
Pasted content is encrypted and stored with an expiration time. Once the content is read, it is deleted from the server. <br/> Pasted content is encrypted and stored with an expiration time. Once the content is read, it is deleted from the server. <br/>

View File

@@ -10,6 +10,8 @@ import "context"
import "io" import "io"
import "bytes" import "bytes"
import "github.com/v1k45/pastepass/config"
func base() templ.Component { func base() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
@@ -23,7 +25,33 @@ func base() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"color-scheme\" content=\"light dark\"><link rel=\"stylesheet\" href=\"/static/pico.min.css\"><title>Paste</title></head><body><main class=\"container\"><nav><ul><li><a href=\"/\">PastePass</a> &mdash; secure one-time paste bin.</li></ul></nav><hr>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"color-scheme\" content=\"light dark\"><link rel=\"stylesheet\" href=\"/static/pico.min.css\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(config.AppName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/base.templ`, Line: 13, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" - secure, one-time paste bin.</title></head><body><main class=\"container\"><nav><ul><li><a href=\"/\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(config.AppName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/base.templ`, Line: 20, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> &mdash; secure one-time paste bin.</li></ul></nav><hr>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -31,7 +59,7 @@ func base() templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<hr><footer><small><p style=\"color: #8891A4;\">PastePass is open-source and free to use. <a href=\"https://github.com/v1k45/pastepass\">View source on github</a>.</p><p style=\"color: #8891A4;\">Pasted content is encrypted and stored with an expiration time. Once the content is read, it is deleted from the server. <br></p></small></footer></main><script src=\"/static/pastepass.js\"></script></body></html>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<hr><footer><small><p style=\"color: #8891A4;\"><a href=\"https://github.com/v1k45/pastepass\">PastePass</a> is open-source and free to use. Created by <a href=\"https://github.com/v1k45/pastepass\">v1k45</a>.</p><p style=\"color: #8891A4;\">Pasted content is encrypted and stored with an expiration time. Once the content is read, it is deleted from the server. <br></p></small></footer></main><script src=\"/static/pastepass.js\"></script></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -13,7 +13,7 @@ templ Decrypt(text string) {
</p> </p>
</hgroup> </hgroup>
<div> <div>
<pre id="pastedContent" style="padding: 1rem; min-height: 10rem;">{text}</pre> <pre id="pastedContent" style="padding: 1rem; min-height: 10rem; max-height: 30rem;">{text}</pre>
<div> <div>
<button onclick="copyText(this, '#pastedContent')" data-tooltip="Click to copy">Copy content</button> <button onclick="copyText(this, '#pastedContent')" data-tooltip="Click to copy">Copy content</button>
</div> </div>

View File

@@ -29,14 +29,14 @@ func Decrypt(text string) templ.Component {
templ_7745c5c3_Buffer = templ.GetBuffer() templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div style=\"padding-bottom: 5rem;\"><hgroup><h3>View Paste</h3><p><small style=\"color: #8891A4;\">Please make sure to save the content before closing this page. This paste has been deleted and will no longer be available for viewing again.</small></p></hgroup><div><pre id=\"pastedContent\" style=\"padding: 1rem; min-height: 10rem;\">") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div style=\"padding-bottom: 5rem;\"><hgroup><h3>View Paste</h3><p><small style=\"color: #8891A4;\">Please make sure to save the content before closing this page. This paste has been deleted and will no longer be available for viewing again.</small></p></hgroup><div><pre id=\"pastedContent\" style=\"padding: 1rem; min-height: 10rem; max-height: 30rem;\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(text) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/decrypt.templ`, Line: 16, Col: 87} return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/decrypt.templ`, Line: 16, Col: 106}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {

View File

@@ -5,8 +5,8 @@ templ Index() {
<form method="post"> <form method="post">
<textarea <textarea
name="text" name="text"
placeholder="Paste your secret here, select expiration time and click 'Submit'" placeholder="Paste your secret here, select expiration time and click 'Paste'"
aria-label="Paste your secret here" aria-label="Paste your secret here, select expiration time and click 'Paste'"
rows="10" rows="10"
required required
autofocus autofocus

View File

@@ -29,7 +29,7 @@ func Index() templ.Component {
templ_7745c5c3_Buffer = templ.GetBuffer() templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form method=\"post\"><textarea name=\"text\" placeholder=\"Paste your secret here, select expiration time and click &#39;Submit&#39;\" aria-label=\"Paste your secret here\" rows=\"10\" required autofocus></textarea><div style=\"display: flex; align-items: end; justify-content: space-between;\"><div style=\"min-width: 33.33%\"><label for=\"expiration\">Expires In</label> <select id=\"expiration\" name=\"expiration\" aria-label=\"Expires In\"><option value=\"1h\" selected>1 Hour</option> <option value=\"1d\">1 Day</option> <option value=\"1w\">1 Week</option> <option value=\"2w\">2 weeks</option> <option value=\"4w\">4 weeks</option></select></div><div style=\"min-width: 33.33%\"><button type=\"submit\">Paste</button></div></div></form>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form method=\"post\"><textarea name=\"text\" placeholder=\"Paste your secret here, select expiration time and click &#39;Paste&#39;\" aria-label=\"Paste your secret here, select expiration time and click &#39;Paste&#39;\" rows=\"10\" required autofocus></textarea><div style=\"display: flex; align-items: end; justify-content: space-between;\"><div style=\"min-width: 33.33%\"><label for=\"expiration\">Expires In</label> <select id=\"expiration\" name=\"expiration\" aria-label=\"Expires In\"><option value=\"1h\" selected>1 Hour</option> <option value=\"1d\">1 Day</option> <option value=\"1w\">1 Week</option> <option value=\"2w\">2 weeks</option> <option value=\"4w\">4 weeks</option></select></div><div style=\"min-width: 33.33%\"><button type=\"submit\">Paste</button></div></div></form>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -3,6 +3,7 @@ package web
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"github.com/v1k45/pastepass/db" "github.com/v1k45/pastepass/db"
@@ -26,18 +27,21 @@ func (h *Handler) Index(w http.ResponseWriter, r *http.Request) {
func (h *Handler) Paste(w http.ResponseWriter, r *http.Request) { func (h *Handler) Paste(w http.ResponseWriter, r *http.Request) {
pastedText := r.FormValue("text") pastedText := r.FormValue("text")
if pastedText == "" { if pastedText == "" {
slog.Error("validation_error", "error", "paste content is required")
errorResponse(w, http.StatusBadRequest, "Invalid Data", "Paste content is required.") errorResponse(w, http.StatusBadRequest, "Invalid Data", "Paste content is required.")
return return
} }
expiresAt, err := getExpiresAt(r.FormValue("expiration")) expiresAt, err := getExpiresAt(r.FormValue("expiration"))
if err != nil { if err != nil {
slog.Error("validation_error", "error", err)
errorResponse(w, http.StatusBadRequest, "Invalid Data", "Invalid expiration time.") errorResponse(w, http.StatusBadRequest, "Invalid Data", "Invalid expiration time.")
return return
} }
paste, err := h.DB.NewPaste(pastedText, expiresAt) paste, err := h.DB.NewPaste(pastedText, expiresAt)
if err != nil { if err != nil {
slog.Error("cannot_create_paste", "error", err)
errorResponse(w, http.StatusInternalServerError, "Internal Server Error", "Failed to create paste, please try again later.") errorResponse(w, http.StatusInternalServerError, "Internal Server Error", "Failed to create paste, please try again later.")
return return
} }
@@ -55,7 +59,9 @@ func (h *Handler) Paste(w http.ResponseWriter, r *http.Request) {
} }
func (h *Handler) View(w http.ResponseWriter, r *http.Request) { func (h *Handler) View(w http.ResponseWriter, r *http.Request) {
if _, err := h.DB.Get(r.PathValue("id")); err != nil { id := r.PathValue("id")
if _, err := h.DB.Get(id); err != nil {
slog.Error("cannot_view_paste", "error", err, "id", id)
errorResponse(w, http.StatusNotFound, "Not Found", "The paste you are looking for is either expired or does not exist.") errorResponse(w, http.StatusNotFound, "Not Found", "The paste you are looking for is either expired or does not exist.")
return return
} }
@@ -65,8 +71,10 @@ func (h *Handler) View(w http.ResponseWriter, r *http.Request) {
} }
func (h *Handler) Decrypt(w http.ResponseWriter, r *http.Request) { func (h *Handler) Decrypt(w http.ResponseWriter, r *http.Request) {
decryptedText, err := h.DB.Decrypt(r.PathValue("id"), r.PathValue("key")) id, key := r.PathValue("id"), r.PathValue("key")
decryptedText, err := h.DB.Decrypt(id, key)
if err != nil { if err != nil {
slog.Error("cannot_decrypt_paste", "error", err, "id", id)
errorResponse( errorResponse(
w, http.StatusInternalServerError, w, http.StatusInternalServerError,
"Internal Server Error", "The paste you are looking for is either expired, corrputed or does not exist.") "Internal Server Error", "The paste you are looking for is either expired, corrputed or does not exist.")