mirror of
https://github.com/v1k45/pastepass.git
synced 2025-12-23 06:15:48 +00:00
Add ability to configure the application; reset db; better logging
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
bin/
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
8
config/config.go
Normal file
8
config/config.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
var (
|
||||
ServerAddr = ":8080"
|
||||
AppName = "PastePass"
|
||||
DBPath = "pastes.boltdb"
|
||||
ResetDB = false
|
||||
)
|
||||
17
db/db.go
17
db/db.go
@@ -3,7 +3,7 @@ package db
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"encoding/json"
|
||||
@@ -26,15 +26,6 @@ type DB struct {
|
||||
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 {
|
||||
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) {
|
||||
// delete paste if expired
|
||||
if _, err := d.Get(id); err == ErrPasteExpired {
|
||||
if _, err := d.Get(id); errors.Is(err, ErrPasteExpired) {
|
||||
return "", d.Delete(id)
|
||||
}
|
||||
|
||||
@@ -186,7 +177,7 @@ func (d *DB) DeleteExpired() error {
|
||||
|
||||
for _, id := range expiredPastes {
|
||||
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 {
|
||||
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
36
db/utils.go
Normal 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
15
main.go
@@ -1,7 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/v1k45/pastepass/config"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -10,14 +13,22 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
boltdb, err := db.NewDB("pastes.boltdb")
|
||||
boltdb, err := db.NewDB(config.DBPath, config.ResetDB)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
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
|
||||
handler := web.NewHandler(boltdb)
|
||||
http.ListenAndServe(":8080", handler.Router())
|
||||
http.ListenAndServe(config.ServerAddr, handler.Router())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package views
|
||||
|
||||
import "github.com/v1k45/pastepass/config"
|
||||
|
||||
templ base() {
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
@@ -8,14 +10,14 @@ templ base() {
|
||||
<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>
|
||||
<title>{ config.AppName } - secure, one-time paste bin.</title>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">PastePass</a> — secure one-time paste bin.
|
||||
<a href="/">{ config.AppName }</a> — secure one-time paste bin.
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -25,7 +27,7 @@ templ base() {
|
||||
<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>.
|
||||
<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/>
|
||||
|
||||
@@ -10,6 +10,8 @@ import "context"
|
||||
import "io"
|
||||
import "bytes"
|
||||
|
||||
import "github.com/v1k45/pastepass/config"
|
||||
|
||||
func base() templ.Component {
|
||||
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)
|
||||
@@ -23,7 +25,33 @@ func base() templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
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> — 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> — secure one-time paste bin.</li></ul></nav><hr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -31,7 +59,7 @@ func base() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ templ Decrypt(text string) {
|
||||
</p>
|
||||
</hgroup>
|
||||
<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>
|
||||
<button onclick="copyText(this, '#pastedContent')" data-tooltip="Click to copy">Copy content</button>
|
||||
</div>
|
||||
|
||||
@@ -29,14 +29,14 @@ func Decrypt(text string) templ.Component {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||
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))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -5,8 +5,8 @@ templ Index() {
|
||||
<form method="post">
|
||||
<textarea
|
||||
name="text"
|
||||
placeholder="Paste your secret here, select expiration time and click 'Submit'"
|
||||
aria-label="Paste your secret here"
|
||||
placeholder="Paste your secret here, select expiration time and click 'Paste'"
|
||||
aria-label="Paste your secret here, select expiration time and click 'Paste'"
|
||||
rows="10"
|
||||
required
|
||||
autofocus
|
||||
|
||||
@@ -29,7 +29,7 @@ func Index() templ.Component {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
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 'Submit'\" 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 'Paste'\" aria-label=\"Paste your secret here, select expiration time and click 'Paste'\" 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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package web
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"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) {
|
||||
pastedText := r.FormValue("text")
|
||||
if pastedText == "" {
|
||||
slog.Error("validation_error", "error", "paste content is required")
|
||||
errorResponse(w, http.StatusBadRequest, "Invalid Data", "Paste content is required.")
|
||||
return
|
||||
}
|
||||
|
||||
expiresAt, err := getExpiresAt(r.FormValue("expiration"))
|
||||
if err != nil {
|
||||
slog.Error("validation_error", "error", err)
|
||||
errorResponse(w, http.StatusBadRequest, "Invalid Data", "Invalid expiration time.")
|
||||
return
|
||||
}
|
||||
|
||||
paste, err := h.DB.NewPaste(pastedText, expiresAt)
|
||||
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.")
|
||||
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) {
|
||||
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.")
|
||||
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) {
|
||||
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 {
|
||||
slog.Error("cannot_decrypt_paste", "error", err, "id", id)
|
||||
errorResponse(
|
||||
w, http.StatusInternalServerError,
|
||||
"Internal Server Error", "The paste you are looking for is either expired, corrputed or does not exist.")
|
||||
|
||||
Reference in New Issue
Block a user