pebble_cms/services/
api_token.rs1use crate::models::ApiToken;
2use crate::Database;
3use anyhow::Result;
4use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
5use rand::Rng;
6use sha2::{Digest, Sha256};
7
8const TOKEN_PREFIX: &str = "pb_";
9const TOKEN_BYTE_LENGTH: usize = 32;
10
11fn generate_raw_token() -> String {
13 let mut bytes = [0u8; TOKEN_BYTE_LENGTH];
14 rand::thread_rng().fill(&mut bytes);
15 format!("{}{}", TOKEN_PREFIX, URL_SAFE_NO_PAD.encode(bytes))
16}
17
18fn hash_token(raw: &str) -> String {
20 let digest = Sha256::digest(raw.as_bytes());
21 hex::encode(digest)
22}
23
24fn extract_prefix(raw: &str) -> String {
26 let without_prefix = raw.strip_prefix(TOKEN_PREFIX).unwrap_or(raw);
27 let end = without_prefix.len().min(8);
28 format!("{}{}...", TOKEN_PREFIX, &without_prefix[..end])
29}
30
31pub fn create_token(
33 db: &Database,
34 name: &str,
35 permissions: &str,
36 created_by: Option<i64>,
37 expires_at: Option<&str>,
38) -> Result<(String, ApiToken)> {
39 let raw_token = generate_raw_token();
40 let token_hash = hash_token(&raw_token);
41 let prefix = extract_prefix(&raw_token);
42
43 let conn = db.get()?;
44 conn.execute(
45 "INSERT INTO api_tokens (name, token_hash, prefix, permissions, created_by, expires_at)
46 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
47 rusqlite::params![name, token_hash, prefix, permissions, created_by, expires_at],
48 )?;
49
50 let id = conn.last_insert_rowid();
51 let token = conn.query_row(
52 "SELECT id, name, prefix, permissions, created_by, last_used_at, expires_at, created_at
53 FROM api_tokens WHERE id = ?",
54 [id],
55 row_to_token,
56 )?;
57
58 Ok((raw_token, token))
59}
60
61pub fn validate_token(db: &Database, raw_token: &str) -> Result<Option<ApiToken>> {
63 if !raw_token.starts_with(TOKEN_PREFIX) {
64 return Ok(None);
65 }
66
67 let token_hash = hash_token(raw_token);
68 let conn = db.get()?;
69
70 let token = conn
71 .query_row(
72 "SELECT id, name, prefix, permissions, created_by, last_used_at, expires_at, created_at
73 FROM api_tokens WHERE token_hash = ?",
74 [&token_hash],
75 row_to_token,
76 )
77 .ok();
78
79 let token = match token {
80 Some(t) => t,
81 None => return Ok(None),
82 };
83
84 if let Some(ref expires) = token.expires_at {
86 let now = chrono::Utc::now().to_rfc3339();
87 if *expires < now {
88 return Ok(None);
89 }
90 }
91
92 conn.execute(
94 "UPDATE api_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE id = ?",
95 [token.id],
96 )?;
97
98 Ok(Some(token))
99}
100
101pub fn list_tokens(db: &Database) -> Result<Vec<ApiToken>> {
103 let conn = db.get()?;
104 let mut stmt = conn.prepare(
105 "SELECT id, name, prefix, permissions, created_by, last_used_at, expires_at, created_at
106 FROM api_tokens ORDER BY created_at DESC",
107 )?;
108
109 let tokens = stmt
110 .query_map([], row_to_token)?
111 .filter_map(|r| r.ok())
112 .collect();
113
114 Ok(tokens)
115}
116
117pub fn revoke_token(db: &Database, id: i64) -> Result<()> {
119 let conn = db.get()?;
120 conn.execute("DELETE FROM api_tokens WHERE id = ?", [id])?;
121 Ok(())
122}
123
124fn row_to_token(row: &rusqlite::Row<'_>) -> rusqlite::Result<ApiToken> {
125 Ok(ApiToken {
126 id: row.get(0)?,
127 name: row.get(1)?,
128 prefix: row.get(2)?,
129 permissions: row.get(3)?,
130 created_by: row.get(4)?,
131 last_used_at: row.get(5)?,
132 expires_at: row.get(6)?,
133 created_at: row.get(7)?,
134 })
135}