1use std::ops::Deref;
11use std::sync::Arc;
12
13use axum::extract::{FromRequestParts, OptionalFromRequestParts};
14use axum::http::StatusCode;
15use axum::http::request::Parts;
16use axum::response::{IntoResponse, Response};
17use serde::Serialize;
18
19use super::tokens::sha256_hex;
20use crate::Result;
21use crate::crypto::{constant_time_eq, random_token};
22use crate::db::{DateTime, Db, now};
23
24#[derive(Debug, Clone, Serialize)]
26#[non_exhaustive]
27pub struct DeviceToken {
28 pub id: i64,
30 pub device: String,
32 pub name: String,
34 pub abilities: Option<Vec<String>>,
36 pub last_used_at: Option<DateTime>,
38 pub expires_at: Option<DateTime>,
40 pub created_at: Option<DateTime>,
42}
43
44#[derive(Debug, Clone, Serialize)]
47#[non_exhaustive]
48pub struct NewDeviceToken {
49 pub token: DeviceToken,
51 pub plain: String,
53}
54
55const COLUMNS: &str = "id, device, name, abilities, last_used_at, expires_at, created_at";
56
57fn from_row(row: &crate::db::Row) -> std::result::Result<DeviceToken, crate::db::DbError> {
58 let abilities: Option<String> = row.try_get("abilities")?;
59 Ok(DeviceToken {
60 id: row.try_get("id")?,
61 device: row.try_get("device")?,
62 name: row.try_get("name")?,
63 abilities: abilities.and_then(|json| serde_json::from_str(&json).ok()),
64 last_used_at: row.try_get("last_used_at")?,
65 expires_at: row.try_get("expires_at")?,
66 created_at: row.try_get("created_at")?,
67 })
68}
69
70impl DeviceToken {
71 pub async fn create(
84 db: &Db,
85 device: &str,
86 name: &str,
87 abilities: Option<&[&str]>,
88 expires_at: Option<DateTime>,
89 ) -> Result<NewDeviceToken> {
90 let secret = random_token();
91 let created = now();
92 let abilities = abilities.map(|list| serde_json::json!(list).to_string());
93 let row = crate::db::sql(format!(
94 "INSERT INTO device_tokens (device, name, abilities, token, expires_at, created_at, updated_at) \
95 VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING {COLUMNS}"
96 ))
97 .bind(device)
98 .bind(name)
99 .bind(abilities)
100 .bind(sha256_hex(&secret))
101 .bind(expires_at)
102 .bind(created)
103 .bind(created)
104 .fetch_one(db)
105 .await?;
106 let token = from_row(&row)?;
107 Ok(NewDeviceToken {
108 plain: format!("d{}|{secret}", token.id),
109 token,
110 })
111 }
112
113 pub async fn for_device(db: &Db, device: &str) -> Result<Vec<DeviceToken>> {
115 let rows = crate::db::sql(format!(
116 "SELECT {COLUMNS} FROM device_tokens WHERE device = ? ORDER BY id DESC"
117 ))
118 .bind(device)
119 .fetch_all(db)
120 .await?;
121 Ok(rows
122 .iter()
123 .map(from_row)
124 .collect::<std::result::Result<_, _>>()?)
125 }
126
127 pub async fn revoke(db: &Db, device: &str, token_id: i64) -> Result<bool> {
129 let done = crate::db::sql("DELETE FROM device_tokens WHERE id = ? AND device = ?")
130 .bind(token_id)
131 .bind(device)
132 .execute(db)
133 .await?;
134 Ok(done > 0)
135 }
136
137 pub async fn revoke_all(db: &Db, device: &str) -> Result<u64> {
140 Ok(crate::db::sql("DELETE FROM device_tokens WHERE device = ?")
141 .bind(device)
142 .execute(db)
143 .await?)
144 }
145
146 pub async fn prune_expired(db: &Db, grace: std::time::Duration) -> Result<u64> {
149 let before = now() - chrono::Duration::from_std(grace).unwrap_or_default();
150 Ok(
151 crate::db::sql("DELETE FROM device_tokens WHERE expires_at < ?")
152 .bind(before)
153 .execute(db)
154 .await?,
155 )
156 }
157}
158
159#[derive(Debug, Clone)]
175#[non_exhaustive]
176pub struct Device {
177 key: String,
178 token_id: i64,
179 name: String,
180 abilities: Option<Arc<Vec<String>>>,
181}
182
183impl Device {
184 pub fn key(&self) -> &str {
186 &self.key
187 }
188
189 pub fn token_id(&self) -> i64 {
191 self.token_id
192 }
193
194 pub fn name(&self) -> &str {
196 &self.name
197 }
198
199 pub fn can(&self, ability: &str) -> bool {
202 self.abilities
203 .as_ref()
204 .is_none_or(|list| list.iter().any(|a| a == ability || a == "*"))
205 }
206
207 pub fn id_of(&self, kind: &str) -> Option<&str> {
210 self.key.strip_prefix(kind)?.strip_prefix(':')
211 }
212}
213
214impl Deref for Device {
215 type Target = str;
216
217 fn deref(&self) -> &str {
218 &self.key
219 }
220}
221
222fn unauthenticated() -> Response {
223 let body = serde_json::json!({ "message": "Unauthenticated." });
224 (StatusCode::UNAUTHORIZED, axum::Json(body)).into_response()
225}
226
227pub(super) fn current(extensions: &axum::http::Extensions) -> Option<Device> {
228 extensions
229 .get::<super::CurrentUser>()?
230 .device
231 .as_deref()
232 .cloned()
233}
234
235impl<S: Send + Sync> FromRequestParts<S> for Device {
236 type Rejection = Response;
237
238 async fn from_request_parts(parts: &mut Parts, _: &S) -> std::result::Result<Self, Response> {
239 current(&parts.extensions).ok_or_else(unauthenticated)
240 }
241}
242
243impl<S: Send + Sync> OptionalFromRequestParts<S> for Device {
244 type Rejection = std::convert::Infallible;
245
246 async fn from_request_parts(
247 parts: &mut Parts,
248 _: &S,
249 ) -> std::result::Result<Option<Self>, std::convert::Infallible> {
250 Ok(current(&parts.extensions))
251 }
252}
253
254pub(super) async fn authenticate(db: &Db, bearer: &str) -> Result<Option<Device>> {
257 let Some((id, secret)) = bearer.strip_prefix('d').and_then(|b| b.split_once('|')) else {
258 return Ok(None);
259 };
260 let Ok(id) = id.parse::<i64>() else {
261 return Ok(None);
262 };
263 let Some(row) = crate::db::sql(
264 "SELECT device, name, token, abilities, expires_at FROM device_tokens WHERE id = ?",
265 )
266 .bind(id)
267 .fetch_optional(db)
268 .await?
269 else {
270 return Ok(None);
271 };
272 let hash: String = row.try_get("token")?;
273 let expires_at: Option<DateTime> = row.try_get("expires_at")?;
274 if !constant_time_eq(&hash, &sha256_hex(secret)) || expires_at.is_some_and(|at| at <= now()) {
275 return Ok(None);
276 }
277 crate::db::sql("UPDATE device_tokens SET last_used_at = ? WHERE id = ?")
278 .bind(now())
279 .bind(id)
280 .execute(db)
281 .await?;
282 let abilities: Option<String> = row.try_get("abilities")?;
283 Ok(Some(Device {
284 key: row.try_get("device")?,
285 token_id: id,
286 name: row.try_get("name")?,
287 abilities: abilities
288 .and_then(|json| serde_json::from_str::<Vec<String>>(&json).ok())
289 .map(Arc::new),
290 }))
291}
292
293pub(crate) async fn require(
296 ability: Option<Arc<String>>,
297 req: axum::extract::Request,
298 next: axum::middleware::Next,
299) -> Response {
300 let Some(device) = current(req.extensions()) else {
301 return unauthenticated();
302 };
303 if ability.is_some_and(|a| !device.can(&a)) {
304 return crate::Error::Forbidden.into_response();
305 }
306 next.run(req).await
307}