initial commit; working state

This commit is contained in:
Vikas
2024-06-02 10:33:54 +05:30
commit a124c1b0be
24 changed files with 1082 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
# database
*.boltdb
+67
View File
@@ -0,0 +1,67 @@
package db
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"fmt"
"io"
random "math/rand"
)
func encrypt(text string, key string) ([]byte, error) {
c, err := aes.NewCipher([]byte(key))
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, []byte(text), nil), nil
}
func decrypt(ciphertext []byte, key string) (string, error) {
c, err := aes.NewCipher([]byte(key))
if err != nil {
return "", fmt.Errorf("failed to create cipher: %w", err)
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return "", fmt.Errorf("failed to create GCM: %w", err)
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return "", errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", fmt.Errorf("failed to decrypt: %w", err)
}
return string(plaintext), nil
}
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
const keyLength = 32
func randomKey() string {
b := make([]rune, keyLength)
for i := range b {
b[i] = letterRunes[random.Intn(len(letterRunes))]
}
return string(b)
}
+205
View File
@@ -0,0 +1,205 @@
package db
import (
"errors"
"fmt"
"log"
"time"
"encoding/json"
"github.com/boltdb/bolt"
)
var (
pastesBucketName = []byte("pastes")
metadataBucketName = []byte("metadata")
)
var (
ErrPasteNotFound = errors.New("paste not found")
ErrBucketNotFound = errors.New("bucket not found")
ErrPasteExpired = errors.New("paste expired")
)
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()
}
func (d *DB) NewPaste(text string, expiresAt time.Time) (*Paste, error) {
paste, err := NewEncryptedPaste(text, expiresAt)
if err != nil {
return nil, err
}
return paste, d.save(paste)
}
func (d *DB) save(paste *Paste) error {
return d.boltDB.Update(func(tx *bolt.Tx) error {
// Save encrypted paste
pasteBucket, err := tx.CreateBucketIfNotExists(pastesBucketName)
if err != nil {
return err
}
if err = pasteBucket.Put([]byte(paste.ID), paste.EncryptedBytes); err != nil {
return err
}
// Save metadata to check expiration
pasteJson, err := json.Marshal(paste)
if err != nil {
return err
}
metadataBucket, err := tx.CreateBucketIfNotExists(metadataBucketName)
if err != nil {
return err
}
return metadataBucket.Put([]byte(paste.ID), pasteJson)
})
}
func (d *DB) Get(id string) (*Paste, error) {
var paste Paste
err := d.boltDB.View(func(tx *bolt.Tx) error {
// get metadata
bucket := tx.Bucket(metadataBucketName)
if bucket == nil {
return ErrBucketNotFound
}
// unmarshal metadata
jsonPaste := bucket.Get([]byte(id))
if jsonPaste == nil {
return ErrPasteNotFound
}
if err := json.Unmarshal(jsonPaste, &paste); err != nil {
return err
}
// ensure paste is not expired
if time.Now().After(paste.ExpiresAt) {
return ErrPasteExpired
}
return nil
})
return &paste, err
}
func (d *DB) Decrypt(id string, key string) (string, error) {
// delete paste if expired
if _, err := d.Get(id); err == ErrPasteExpired {
return "", d.Delete(id)
}
var decryptedText string
err := d.boltDB.Update(func(tx *bolt.Tx) error {
pasteBucket := tx.Bucket(pastesBucketName)
if pasteBucket == nil {
return ErrBucketNotFound
}
encryptedPaste := pasteBucket.Get([]byte(id))
if encryptedPaste == nil {
return ErrPasteNotFound
}
var err error
decryptedText, err = decrypt(encryptedPaste, key)
if err != nil {
return err
}
return nil
})
if err != nil {
return "", err
}
return decryptedText, d.Delete(id)
}
func (d *DB) Delete(id string) error {
return d.boltDB.Update(func(tx *bolt.Tx) error {
pasteBucket := tx.Bucket(pastesBucketName)
if pasteBucket == nil {
return ErrBucketNotFound
}
if err := pasteBucket.Delete([]byte(id)); err != nil {
return err
}
metadataBucket := tx.Bucket(metadataBucketName)
if metadataBucket == nil {
return ErrBucketNotFound
}
return metadataBucket.Delete([]byte(id))
})
}
func (d *DB) DeleteExpired() error {
var expiredPastes []string
err := d.boltDB.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(metadataBucketName)
if bucket == nil {
return ErrBucketNotFound
}
return bucket.ForEach(func(k, v []byte) error {
var paste Paste
if err := json.Unmarshal(v, &paste); err != nil {
return err
}
if time.Now().After(paste.ExpiresAt) {
expiredPastes = append(expiredPastes, string(k))
}
return nil
})
})
if err != nil {
return fmt.Errorf("error getting expired pastes: %v", err)
}
for _, id := range expiredPastes {
if err := d.Delete(id); err != nil {
log.Println(fmt.Errorf("error deleting expired paste %s: %v", id, err))
}
}
return nil
}
func (d *DB) DeleteExpiredPeriodically(interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
if err := d.DeleteExpired(); err != nil {
log.Println(fmt.Errorf("error deleting expired pastes: %v", err))
}
}
}
+31
View File
@@ -0,0 +1,31 @@
package db
import (
"time"
)
type Paste struct {
ID string `json:"id"`
Text string `json:"-"`
EncryptedBytes []byte `json:"-"`
Key string `json:"-"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
func NewEncryptedPaste(text string, expiresAt time.Time) (*Paste, error) {
key := randomKey()
encryptedText, err := encrypt(text, key)
if err != nil {
return nil, err
}
return &Paste{
ID: randomKey(),
Text: text,
EncryptedBytes: encryptedText,
Key: key,
CreatedAt: time.Now(),
ExpiresAt: expiresAt,
}, nil
}
+10
View File
@@ -0,0 +1,10 @@
module github.com/v1k45/paste
go 1.22.3
require (
github.com/a-h/templ v0.2.707
github.com/boltdb/bolt v1.3.1
)
require golang.org/x/sys v0.19.0 // indirect
+8
View File
@@ -0,0 +1,8 @@
github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"log"
"net/http"
"time"
"github.com/v1k45/paste/db"
"github.com/v1k45/paste/web"
)
func main() {
// Open the database
boltdb, err := db.NewDB("pastes.boltdb")
if err != nil {
log.Fatalf("failed to open database: %v", err)
}
go boltdb.DeleteExpiredPeriodically(time.Minute * 5)
// Start the web server
handler := web.NewHandler(boltdb)
http.ListenAndServe(":8080", handler.Router())
}
+38
View File
@@ -0,0 +1,38 @@
package views
templ base() {
<!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="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"/>
<title>Paste</title>
</head>
<body>
<main class="container">
<nav>
<ul>
<li>
<a href="/">Paste</a> &mdash; secure one-time paste bin.
</li>
</ul>
</nav>
<hr />
{ children... }
<hr />
<footer>
<small>
<p style="color: #8891A4;">
Paste is open-source and free to use. <a href="https://github.com/v1k45/paste">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>
</body>
</html>
}
+43
View File
@@ -0,0 +1,43 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
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)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
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=\"https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css\"><title>Paste</title></head><body><main class=\"container\"><nav><ul><li><a href=\"/\">Paste</a> &mdash; secure one-time paste bin.</li></ul></nav><hr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<hr><footer><small><p style=\"color: #8891A4;\">Paste is open-source and free to use. <a href=\"https://github.com/v1k45/paste\">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></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
+25
View File
@@ -0,0 +1,25 @@
package views
templ Decrypt(text string) {
@base() {
<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;">{text}</pre>
<div>
<button onclick="copyText(this, '#pastedContent')" data-tooltip="Click to copy">Copy content</button>
</div>
</div>
@copyTextScript()
</div>
}
}
+71
View File
@@ -0,0 +1,71 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func Decrypt(text string) 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)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := 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)
if !templ_7745c5c3_IsBuffer {
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;\">")
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}
}
_, 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("</pre><div><button onclick=\"copyText(this, &#39;#pastedContent&#39;)\" data-tooltip=\"Click to copy\">Copy content</button></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = copyTextScript().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
+14
View File
@@ -0,0 +1,14 @@
package views
templ Error(title, message string) {
@base() {
<div style="padding-bottom: 5rem;">
<hgroup>
<h3>{title}</h3>
<small style="color: #8891A4;">
{message}
</small>
</hgroup>
</div>
}
}
+76
View File
@@ -0,0 +1,76 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func Error(title, message string) 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)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := 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)
if !templ_7745c5c3_IsBuffer {
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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/error.templ`, Line: 7, Col: 22}
}
_, 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("</h3><small style=\"color: #8891A4;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(message)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/error.templ`, Line: 9, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</small></hgroup></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
+32
View File
@@ -0,0 +1,32 @@
package views
templ Index() {
@base() {
<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="width: 33.33%">
<label for="expiration">Expires In</label>
<select id="expiration" expired 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="width: 33.33%">
<button type="submit">Paste</button>
</div>
</div>
</form>
}
}
+50
View File
@@ -0,0 +1,50 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func Index() 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)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := 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)
if !templ_7745c5c3_IsBuffer {
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 &#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=\"width: 33.33%\"><label for=\"expiration\">Expires In</label> <select id=\"expiration\" expired 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=\"width: 33.33%\"><button type=\"submit\">Paste</button></div></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
+24
View File
@@ -0,0 +1,24 @@
package views
templ PasteSuccess(url string) {
@base() {
<div style="padding-bottom: 5rem;">
<hgroup>
<h3>Paste Created</h3>
<p>
<small style="color: #8891A4;">
Your secret paste has been created. Share the following link with the recipient:
</small>
</p>
</hgroup>
<div>
<pre id="url" style="padding: 1rem; min-height: 3rem;">{url}</pre>
<div>
<button onclick="copyText(this, '#url')" data-tooltip="Click to copy">Copy content</button>
<a href={ templ.SafeURL(url) } style="margin-left: 1rem;">View Paste</a>
</div>
</div>
@copyTextScript()
</div>
}
}
+80
View File
@@ -0,0 +1,80 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func PasteSuccess(url string) 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)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := 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)
if !templ_7745c5c3_IsBuffer {
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>Paste Created</h3><p><small style=\"color: #8891A4;\">Your secret paste has been created. Share the following link with the recipient:</small></p></hgroup><div><pre id=\"url\" style=\"padding: 1rem; min-height: 3rem;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(url)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/paste.templ`, Line: 15, Col: 71}
}
_, 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("</pre><div><button onclick=\"copyText(this, &#39;#url&#39;)\" data-tooltip=\"Click to copy\">Copy content</button> <a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL = templ.SafeURL(url)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" style=\"margin-left: 1rem;\">View Paste</a></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = copyTextScript().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
+28
View File
@@ -0,0 +1,28 @@
package views
templ copyTextScript() {
<script>
function copyText(event, selector) {
var pastedContent = document.querySelector(selector);
// Create a range and select the text
var range = document.createRange();
range.selectNode(pastedContent);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
// Copy the selected text
if (navigator.clipboard) {
navigator.clipboard.writeText(pastedContent.innerText).then(function() {
event.dataset.tooltip = 'Copied!';
event.innerText = 'Copied!';
setTimeout(function() {
event.innerText = 'Copy content';
event.dataset.tooltip = 'Click to copy';
event.blur();
}, 1000);
});
}
}
</script>
}
+35
View File
@@ -0,0 +1,35 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func copyTextScript() 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)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<script>\n function copyText(event, selector) {\n var pastedContent = document.querySelector(selector);\n\n // Create a range and select the text\n var range = document.createRange();\n range.selectNode(pastedContent);\n window.getSelection().removeAllRanges();\n window.getSelection().addRange(range);\n\n // Copy the selected text\n if (navigator.clipboard) {\n navigator.clipboard.writeText(pastedContent.innerText).then(function() {\n event.dataset.tooltip = 'Copied!';\n event.innerText = 'Copied!';\n setTimeout(function() {\n event.innerText = 'Copy content';\n event.dataset.tooltip = 'Click to copy';\n event.blur();\n }, 1000);\n });\n }\n }\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
+19
View File
@@ -0,0 +1,19 @@
package views
templ View() {
@base() {
<div style="padding-bottom: 5rem;">
<hgroup>
<h3>View Paste</h3>
<p>
<small style="color: #8891A4;">
You can only view this paste once. Make sure to copy it before you close this page.
</small>
</p>
</hgroup>
<form style="width: 33.33%;" method="post">
<button type="submit">Show Paste</button>
</form>
</div>
}
}
+50
View File
@@ -0,0 +1,50 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func View() 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)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := 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)
if !templ_7745c5c3_IsBuffer {
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;\">You can only view this paste once. Make sure to copy it before you close this page.</small></p></hgroup><form style=\"width: 33.33%;\" method=\"post\"><button type=\"submit\">Show Paste</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
+78
View File
@@ -0,0 +1,78 @@
package web
import (
"context"
"fmt"
"net/http"
"github.com/v1k45/paste/db"
"github.com/v1k45/paste/views"
)
type Handler struct {
DB *db.DB
}
func NewHandler(db *db.DB) *Handler {
return &Handler{DB: db}
}
func (h *Handler) Index(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
component := views.Index()
component.Render(context.Background(), w)
}
func (h *Handler) Paste(w http.ResponseWriter, r *http.Request) {
pastedText := r.FormValue("text")
if pastedText == "" {
errorResponse(w, http.StatusBadRequest, "Invalid Data", "Paste content is required.")
return
}
expiresAt, err := getExpiresAt(r.FormValue("expiration"))
if err != nil {
errorResponse(w, http.StatusBadRequest, "Invalid Data", "Invalid expiration time.")
return
}
paste, err := h.DB.NewPaste(pastedText, expiresAt)
if err != nil {
errorResponse(w, http.StatusInternalServerError, "Internal Server Error", "Failed to create paste, please try again later.")
return
}
var scheme string
if r.TLS == nil {
scheme = "http"
} else {
scheme = "https"
}
url := fmt.Sprintf("%s://%s/p/%s/%s", scheme, r.Host, paste.ID, paste.Key)
component := views.PasteSuccess(url)
component.Render(context.Background(), w)
}
func (h *Handler) View(w http.ResponseWriter, r *http.Request) {
if _, err := h.DB.Get(r.PathValue("id")); err != nil {
errorResponse(w, http.StatusNotFound, "Not Found", "The paste you are looking for is either expired or does not exist.")
return
}
component := views.View()
component.Render(context.Background(), w)
}
func (h *Handler) Decrypt(w http.ResponseWriter, r *http.Request) {
decryptedText, err := h.DB.Decrypt(r.PathValue("id"), r.PathValue("key"))
if err != nil {
errorResponse(
w, http.StatusInternalServerError,
"Internal Server Error", "The paste you are looking for is either expired, corrputed or does not exist.")
return
}
component := views.Decrypt(decryptedText)
component.Render(context.Background(), w)
}
+12
View File
@@ -0,0 +1,12 @@
package web
import "net/http"
func (h *Handler) Router() http.Handler {
router := http.NewServeMux()
router.HandleFunc("GET /", h.Index)
router.HandleFunc("POST /", h.Paste)
router.HandleFunc("GET /p/{id}/{key}", h.View)
router.HandleFunc("POST /p/{id}/{key}", h.Decrypt)
return router
}
+35
View File
@@ -0,0 +1,35 @@
package web
import (
"context"
"errors"
"net/http"
"time"
"github.com/v1k45/paste/views"
)
var (
expirationTimes = map[string]time.Duration{
"1h": time.Hour,
"1d": 24 * time.Hour,
"1w": 7 * 24 * time.Hour,
"2w": 2 * 7 * 24 * time.Hour,
"4w": 4 * 7 * 24 * time.Hour,
}
)
func getExpiresAt(expiresAt string) (time.Time, error) {
expiresDuration, found := expirationTimes[expiresAt]
if !found {
return time.Time{}, errors.New("invalid expiration time")
}
return time.Now().Add(expiresDuration), nil
}
func errorResponse(w http.ResponseWriter, status int, title, message string) {
w.WriteHeader(status)
component := views.Error(title, message)
component.Render(context.Background(), w)
}