Add pocketbase and other changes

This commit is contained in:
Andras Schmelczer 2026-02-07 19:20:22 +00:00
parent a9717d570d
commit 229150b641
14 changed files with 1178 additions and 91 deletions

145
server-rs/src/auth.rs Normal file
View 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
}