Add pocketbase and other changes
This commit is contained in:
parent
a9717d570d
commit
229150b641
14 changed files with 1178 additions and 91 deletions
145
server-rs/src/auth.rs
Normal file
145
server-rs/src/auth.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use axum::extract::Request;
|
||||
use axum::middleware::Next;
|
||||
use axum::response::Response;
|
||||
use parking_lot::RwLock;
|
||||
use reqwest::Client;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::warn;
|
||||
|
||||
const TOKEN_TTL_SECS: u64 = 60;
|
||||
const MAX_CACHE_ENTRIES: usize = 1000;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PocketBaseUser {
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub avatar: String,
|
||||
#[serde(default)]
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OptionalUser(pub Option<PocketBaseUser>);
|
||||
|
||||
pub struct TokenCache {
|
||||
entries: RwLock<FxHashMap<String, (PocketBaseUser, Instant)>>,
|
||||
}
|
||||
|
||||
impl TokenCache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: RwLock::new(FxHashMap::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, token: &str) -> Option<PocketBaseUser> {
|
||||
let map = self.entries.read();
|
||||
if let Some((user, created)) = map.get(token) {
|
||||
if created.elapsed().as_secs() < TOKEN_TTL_SECS {
|
||||
return Some(user.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn insert(&self, token: String, user: PocketBaseUser) {
|
||||
let mut map = self.entries.write();
|
||||
if map.len() >= MAX_CACHE_ENTRIES {
|
||||
// Evict expired entries first
|
||||
let now = Instant::now();
|
||||
map.retain(|_, (_, created)| now.duration_since(*created).as_secs() < TOKEN_TTL_SECS);
|
||||
// If still too many, clear all
|
||||
if map.len() >= MAX_CACHE_ENTRIES {
|
||||
map.clear();
|
||||
}
|
||||
}
|
||||
map.insert(token, (user, Instant::now()));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthRefreshResponse {
|
||||
record: PocketBaseUser,
|
||||
}
|
||||
|
||||
async fn validate_token(
|
||||
client: &Client,
|
||||
pocketbase_url: &str,
|
||||
token: &str,
|
||||
) -> Option<PocketBaseUser> {
|
||||
let url = format!(
|
||||
"{}/api/collections/users/auth-refresh",
|
||||
pocketbase_url.trim_end_matches('/')
|
||||
);
|
||||
let res = client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.send()
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
if !res.status().is_success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let body: AuthRefreshResponse = res.json().await.ok()?;
|
||||
Some(body.record)
|
||||
}
|
||||
|
||||
pub async fn auth_middleware(req: Request, next: Next) -> Response {
|
||||
let pocketbase_url = req
|
||||
.extensions()
|
||||
.get::<Arc<crate::state::AppState>>()
|
||||
.and_then(|st| st.pocketbase_url.as_deref())
|
||||
.map(String::from);
|
||||
|
||||
let token_cache = req
|
||||
.extensions()
|
||||
.get::<Arc<crate::state::AppState>>()
|
||||
.map(|st| st.token_cache.clone());
|
||||
|
||||
let http_client = req
|
||||
.extensions()
|
||||
.get::<Arc<crate::state::AppState>>()
|
||||
.map(|st| st.http_client.clone());
|
||||
|
||||
let token = req
|
||||
.headers()
|
||||
.get("authorization")
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.and_then(|hv| hv.strip_prefix("Bearer "))
|
||||
.map(String::from);
|
||||
|
||||
let user = match (&pocketbase_url, &token, &token_cache, &http_client) {
|
||||
(Some(pb_url), Some(tk), Some(cache), Some(client)) => {
|
||||
if let Some(cached) = cache.get(tk) {
|
||||
Some(cached)
|
||||
} else {
|
||||
match validate_token(client, pb_url, tk).await {
|
||||
Some(user) => {
|
||||
cache.insert(tk.clone(), user.clone());
|
||||
Some(user)
|
||||
}
|
||||
None => {
|
||||
warn!("Invalid auth token");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let (mut parts, body) = req.into_parts();
|
||||
parts.extensions.insert(OptionalUser(user));
|
||||
let req = Request::from_parts(parts, body);
|
||||
|
||||
next.run(req).await
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue