Burn after read + deletion tokens: single-read pastes, hard-delete via token redeem
This commit is contained in:
@@ -48,6 +48,7 @@ type Paste struct {
|
||||
DeletedAt *int64 `json:"deleted_at,omitempty"`
|
||||
ExpiresAt *int64 `json:"expires_at,omitempty"`
|
||||
ViewCount int `json:"view_count"`
|
||||
DeletionToken string `json:"-"`
|
||||
}
|
||||
|
||||
type PasteRow struct {
|
||||
@@ -65,6 +66,7 @@ type PasteRow struct {
|
||||
CreatedAt int64
|
||||
DeletedAt sql.NullInt64
|
||||
ViewCount int
|
||||
DeletionToken sql.NullString
|
||||
}
|
||||
|
||||
type CanRow struct {
|
||||
@@ -109,7 +111,8 @@ func (s *Store) migrate() error {
|
||||
can_id TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
deleted_at INTEGER,
|
||||
view_count INTEGER NOT NULL DEFAULT 0
|
||||
view_count INTEGER NOT NULL DEFAULT 0,
|
||||
deletion_token TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_pastes_visibility_created ON pastes(visibility, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_pastes_expires ON pastes(expires_at) WHERE expires_at IS NOT NULL;
|
||||
@@ -125,6 +128,7 @@ func (s *Store) migrate() error {
|
||||
expires_at INTEGER
|
||||
);
|
||||
`)
|
||||
s.db.Exec(`ALTER TABLE pastes ADD COLUMN deletion_token TEXT`) // ignore if exists
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -199,10 +203,11 @@ func (s *Store) CreatePaste(p *Paste) (*Paste, error) {
|
||||
if p.CustomSlug != nil && *p.CustomSlug != "" {
|
||||
slugVal = p.CustomSlug
|
||||
}
|
||||
p.DeletionToken = genDeletionToken()
|
||||
_, err := s.db.Exec(`INSERT INTO pastes
|
||||
(id, custom_slug, content, content_type, language, title, password_hash, expires_at, burn_after_read, visibility, created_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
id, slugVal, p.Content, contentType, p.Language, p.Title, pwHash, expiresAt, boolToInt(p.BurnAfterRead), visibility, now)
|
||||
(id, custom_slug, content, content_type, language, title, password_hash, expires_at, burn_after_read, visibility, created_at, deletion_token)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
id, slugVal, p.Content, contentType, p.Language, p.Title, pwHash, expiresAt, boolToInt(p.BurnAfterRead), visibility, now, p.DeletionToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -214,10 +219,10 @@ func (s *Store) CreatePaste(p *Paste) (*Paste, error) {
|
||||
}
|
||||
|
||||
func (s *Store) GetPaste(idOrSlug string) (*PasteRow, error) {
|
||||
row := s.db.QueryRow(`SELECT id, custom_slug, content, content_type, language, title, password_hash, expires_at, burn_after_read, visibility, can_id, created_at, deleted_at, view_count
|
||||
row := s.db.QueryRow(`SELECT id, custom_slug, content, content_type, language, title, password_hash, expires_at, burn_after_read, visibility, can_id, created_at, deleted_at, view_count, deletion_token
|
||||
FROM pastes WHERE (id = ? OR custom_slug = ?) AND deleted_at IS NULL`, idOrSlug, idOrSlug)
|
||||
var r PasteRow
|
||||
err := row.Scan(&r.ID, &r.CustomSlug, &r.Content, &r.ContentType, &r.Language, &r.Title, &r.PasswordHash, &r.ExpiresAt, &r.BurnAfterRead, &r.Visibility, &r.CanID, &r.CreatedAt, &r.DeletedAt, &r.ViewCount)
|
||||
err := row.Scan(&r.ID, &r.CustomSlug, &r.Content, &r.ContentType, &r.Language, &r.Title, &r.PasswordHash, &r.ExpiresAt, &r.BurnAfterRead, &r.Visibility, &r.CanID, &r.CreatedAt, &r.DeletedAt, &r.ViewCount, &r.DeletionToken)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -319,6 +324,7 @@ func (a *apiServer) routes() http.Handler {
|
||||
r.Post("/pastes", a.handleCreatePaste)
|
||||
r.Get("/pastes/{id}", a.handleGetPaste)
|
||||
r.Delete("/pastes/{id}", a.handleDeletePaste)
|
||||
r.Delete("/pastes/{id}/redeem", a.handleRedeemDeletion)
|
||||
r.Get("/public", a.handleListPublic)
|
||||
r.Post("/pastes/can", a.handleCreateCan)
|
||||
r.Get("/cans/{id}", a.handleGetCan)
|
||||
@@ -362,6 +368,7 @@ func (a *apiServer) handleCreatePaste(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
writeJSON(w, 201, map[string]any{
|
||||
"id": created.ID,
|
||||
"deletion_token": created.DeletionToken,
|
||||
"url": "/" + created.ID,
|
||||
"raw_url": "/raw/" + created.ID,
|
||||
"api_url": "/api/pastes/" + created.ID,
|
||||
@@ -402,6 +409,7 @@ func (a *apiServer) handleGetPaste(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
a.store.maybeBurn(row)
|
||||
writeJSON(w, 200, map[string]any{
|
||||
"id": row.ID, "content": row.Content, "content_type": row.ContentType,
|
||||
"language": nullPtr(row.Language), "title": nullPtr(row.Title), "created_at": row.CreatedAt,
|
||||
|
||||
Reference in New Issue
Block a user