Skip to main content

pebble_cms/services/
preview.rs

1//! Draft preview service — generates signed, time-limited preview tokens
2//! so authors can share links to unpublished content.
3
4use crate::Database;
5use anyhow::Result;
6use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
7use rand::RngCore;
8
9/// Duration in seconds that a preview token remains valid (1 hour).
10const PREVIEW_TOKEN_LIFETIME_SECS: i64 = 3600;
11
12/// Generate a signed preview token for a content item.
13/// The token encodes the content ID and an expiration time.
14pub fn generate_preview_token(db: &Database, content_id: i64) -> Result<String> {
15    let mut random_bytes = [0u8; 16];
16    rand::rngs::OsRng.fill_bytes(&mut random_bytes);
17    let token = URL_SAFE_NO_PAD.encode(random_bytes);
18
19    let conn = db.get()?;
20    conn.execute(
21        "INSERT INTO preview_tokens (token, content_id, expires_at) VALUES (?, ?, datetime('now', ?||' seconds'))",
22        (&token, content_id, PREVIEW_TOKEN_LIFETIME_SECS),
23    )?;
24
25    Ok(token)
26}
27
28/// Validate a preview token and return the associated content_id if valid.
29pub fn validate_preview_token(db: &Database, token: &str) -> Result<Option<i64>> {
30    let conn = db.get()?;
31    let content_id: Option<i64> = conn
32        .query_row(
33            "SELECT content_id FROM preview_tokens WHERE token = ? AND expires_at > datetime('now')",
34            [token],
35            |row| row.get(0),
36        )
37        .ok();
38    Ok(content_id)
39}
40
41/// Remove expired preview tokens.
42pub fn cleanup_expired_tokens(db: &Database) -> Result<usize> {
43    let conn = db.get()?;
44    let count = conn.execute(
45        "DELETE FROM preview_tokens WHERE expires_at <= datetime('now')",
46        [],
47    )?;
48    Ok(count)
49}