1use serde::Serialize;
2
3use super::User;
4use crate::Result;
5use crate::crypto::{constant_time_eq, random_token};
6use crate::db::{DateTime, Db, Model, now};
7
8#[derive(Debug, Clone, Serialize)]
10#[non_exhaustive]
11pub struct AccessToken {
12 pub id: i64,
14 pub user_id: i64,
16 pub name: String,
18 pub abilities: Option<Vec<String>>,
20 pub last_used_at: Option<DateTime>,
22 pub expires_at: Option<DateTime>,
24 pub created_at: Option<DateTime>,
26}
27
28#[derive(Debug, Clone, Serialize)]
31#[non_exhaustive]
32pub struct NewToken {
33 pub token: AccessToken,
35 pub plain: String,
37}
38
39pub(crate) fn sha256_hex(value: &str) -> String {
40 use sha2::{Digest, Sha256};
41 Sha256::digest(value.as_bytes())
42 .iter()
43 .map(|b| format!("{b:02x}"))
44 .collect()
45}
46
47fn from_row(row: &crate::db::Row) -> std::result::Result<AccessToken, crate::db::DbError> {
48 Ok(AccessToken {
49 id: row.try_get("id")?,
50 user_id: row.try_get("user_id")?,
51 name: row.try_get("name")?,
52 abilities: parse_abilities(row.try_get("abilities")?),
53 last_used_at: row.try_get("last_used_at")?,
54 expires_at: row.try_get("expires_at")?,
55 created_at: row.try_get("created_at")?,
56 })
57}
58
59const COLUMNS: &str = "id, user_id, name, abilities, last_used_at, expires_at, created_at";
60
61fn parse_abilities(json: Option<String>) -> Option<Vec<String>> {
62 json.and_then(|json| serde_json::from_str(&json).ok())
63}
64
65impl User {
66 pub async fn create_token(
69 &self,
70 db: &Db,
71 name: &str,
72 expires_at: Option<DateTime>,
73 ) -> Result<NewToken> {
74 self.insert_token(db, name, None, expires_at).await
75 }
76
77 pub async fn create_token_with(
82 &self,
83 db: &Db,
84 name: &str,
85 abilities: &[&str],
86 expires_at: Option<DateTime>,
87 ) -> Result<NewToken> {
88 self.insert_token(db, name, Some(abilities), expires_at)
89 .await
90 }
91
92 async fn insert_token(
93 &self,
94 db: &Db,
95 name: &str,
96 abilities: Option<&[&str]>,
97 expires_at: Option<DateTime>,
98 ) -> Result<NewToken> {
99 let secret = random_token();
100 let created = now();
101 let abilities = abilities.map(|list| serde_json::json!(list).to_string());
102 let row = crate::db::sql(format!(
103 "INSERT INTO personal_access_tokens (user_id, name, abilities, token, expires_at, created_at, updated_at) \
104 VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING {COLUMNS}"
105 ))
106 .bind(self.id)
107 .bind(name)
108 .bind(abilities)
109 .bind(sha256_hex(&secret))
110 .bind(expires_at)
111 .bind(created)
112 .bind(created)
113 .fetch_one(db)
114 .await?;
115 let token = from_row(&row)?;
116 Ok(NewToken {
117 plain: format!("{}|{secret}", token.id),
118 token,
119 })
120 }
121
122 pub async fn tokens(&self, db: &Db) -> Result<Vec<AccessToken>> {
124 let rows = crate::db::sql(format!(
125 "SELECT {COLUMNS} FROM personal_access_tokens WHERE user_id = ? ORDER BY id DESC"
126 ))
127 .bind(self.id)
128 .fetch_all(db)
129 .await?;
130 Ok(rows
131 .iter()
132 .map(from_row)
133 .collect::<std::result::Result<_, _>>()?)
134 }
135
136 pub async fn revoke_token(&self, db: &Db, token_id: i64) -> Result<bool> {
138 let done =
139 crate::db::sql("DELETE FROM personal_access_tokens WHERE id = ? AND user_id = ?")
140 .bind(token_id)
141 .bind(self.id)
142 .execute(db)
143 .await?;
144 Ok(done > 0)
145 }
146
147 pub async fn revoke_tokens(&self, db: &Db) -> Result<u64> {
149 let done = crate::db::sql("DELETE FROM personal_access_tokens WHERE user_id = ?")
150 .bind(self.id)
151 .execute(db)
152 .await?;
153 Ok(done)
154 }
155}
156
157pub async fn prune_expired_tokens(db: &Db, grace: std::time::Duration) -> Result<u64> {
160 let before = now() - chrono::Duration::from_std(grace).unwrap_or_default();
161 Ok(
162 crate::db::sql("DELETE FROM personal_access_tokens WHERE expires_at < ?")
163 .bind(before)
164 .execute(db)
165 .await?,
166 )
167}
168
169pub(crate) type TokenGrant = (i64, Option<Vec<String>>);
171
172pub(crate) async fn authenticate(db: &Db, bearer: &str) -> Result<Option<(User, TokenGrant)>> {
175 let Some((id, secret)) = bearer.split_once('|') else {
176 return Ok(None);
177 };
178 let Ok(id) = id.parse::<i64>() else {
179 return Ok(None);
180 };
181 let Some(row) = crate::db::sql(
182 "SELECT user_id, token, abilities, expires_at FROM personal_access_tokens WHERE id = ?",
183 )
184 .bind(id)
185 .fetch_optional(db)
186 .await?
187 else {
188 return Ok(None);
189 };
190 let hash: String = row.try_get("token")?;
191 let expires_at: Option<DateTime> = row.try_get("expires_at")?;
192 if !constant_time_eq(&hash, &sha256_hex(secret)) || expires_at.is_some_and(|at| at <= now()) {
193 return Ok(None);
194 }
195 crate::db::sql("UPDATE personal_access_tokens SET last_used_at = ? WHERE id = ?")
196 .bind(now())
197 .bind(id)
198 .execute(db)
199 .await?;
200 let abilities = parse_abilities(row.try_get("abilities")?);
201 Ok(User::find(db, row.try_get("user_id")?)
202 .await?
203 .map(|user| (user, (id, abilities))))
204}