Refactor: split monolith into cmd/palette + internal/{store,api,web,lang} (#35)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
)
|
||||
|
||||
// genDeletionToken returns a 32-char url-safe random token
|
||||
func genDeletionToken() string {
|
||||
b := make([]byte, 24)
|
||||
cryptoRead(b)
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
// TimeNow is overridable in tests to inject the clock.
|
||||
var TimeNow = time.Now
|
||||
|
||||
// registerRead applies the burn-after-read budget for one view (#49).
|
||||
// For pastes with reads_limit set: the viewer's paste_views row is checked;
|
||||
// a view within the burn viewer window of the viewer's last view is deduped
|
||||
// (count=false). Otherwise reads_used is incremented, and the paste is
|
||||
// soft-deleted (burned) once reads_used reaches reads_limit. Viewers without
|
||||
// a cookie (plain API clients) count as their own viewer id "".
|
||||
// For legacy plain burn_after_read pastes (no reads_limit), any read burns.
|
||||
// Returns the number of reads remaining (0 when burned), or nil when no
|
||||
// budget is set. view_count is tracked separately and unaffected.
|
||||
func (s *Store) RegisterRead(row *PasteRow, viewerID string, burnWindowMinutes int) (remaining *int, count bool) {
|
||||
if !row.ReadsLimit.Valid {
|
||||
if row.BurnAfterRead {
|
||||
s.SoftDelete(row.ID)
|
||||
r := 0
|
||||
return &r, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
now := TimeNow().Unix()
|
||||
var last sql.NullInt64
|
||||
s.db.QueryRow(`SELECT last_viewed FROM paste_views WHERE paste_id=? AND viewer_id=?`,
|
||||
row.ID, viewerID).Scan(&last)
|
||||
if last.Valid && now-last.Int64 < int64(burnWindowMinutes)*60 {
|
||||
r := int(row.ReadsLimit.Int64) - row.ReadsUsed
|
||||
if r < 0 {
|
||||
r = 0
|
||||
}
|
||||
return &r, false
|
||||
}
|
||||
s.db.Exec(`INSERT INTO paste_views (paste_id, viewer_id, last_viewed) VALUES (?,?,?)
|
||||
ON CONFLICT(paste_id, viewer_id) DO UPDATE SET last_viewed = excluded.last_viewed`,
|
||||
row.ID, viewerID, now)
|
||||
used := row.ReadsUsed + 1
|
||||
s.db.Exec(`UPDATE pastes SET reads_used=? WHERE id=?`, used, row.ID)
|
||||
if int64(used) >= row.ReadsLimit.Int64 {
|
||||
s.SoftDelete(row.ID)
|
||||
}
|
||||
r := int(row.ReadsLimit.Int64) - int(used)
|
||||
if r < 0 {
|
||||
r = 0
|
||||
}
|
||||
return &r, true
|
||||
}
|
||||
|
||||
// Burned reports whether a read-limited paste has exhausted its budget.
|
||||
func (row *PasteRow) Burned() bool {
|
||||
return row.ReadsLimit.Valid && int64(row.ReadsUsed) >= row.ReadsLimit.Int64
|
||||
}
|
||||
|
||||
func DeletionTokenEqual(stored, given string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(stored), []byte(given)) == 1
|
||||
}
|
||||
Reference in New Issue
Block a user