1use anyhow::{Context, Result};
2pub use greentic_session::{SessionData, SessionKey};
3use greentic_session::{
4 SessionStore, inmemory::InMemorySessionStore, redis_store::RedisSessionStore,
5};
6use greentic_types::{TenantCtx, UserId};
7use redis::Client;
8use std::sync::Arc;
9use tokio::task;
10
11const DEFAULT_NAMESPACE: &str = "greentic:session";
12const SESSION_NAMESPACE_ENV: &str = "SESSION_NAMESPACE";
13const SESSION_REDIS_URL_ENV: &str = "SESSION_REDIS_URL";
14
15#[derive(Clone)]
17pub struct SharedSessionStore {
18 inner: Arc<SessionBackend>,
19}
20
21enum SessionBackend {
22 InMemory(Arc<InMemorySessionStore>),
23 Redis(Arc<RedisSessionStore>),
24}
25
26pub async fn store_from_env() -> Result<SharedSessionStore> {
28 match std::env::var(SESSION_REDIS_URL_ENV) {
29 Ok(url) => {
30 let namespace =
31 std::env::var(SESSION_NAMESPACE_ENV).unwrap_or_else(|_| DEFAULT_NAMESPACE.into());
32 build_redis_store(&url, &namespace)
33 }
34 Err(_) => Ok(shared_memory_store()),
35 }
36}
37
38pub fn shared_memory_store() -> SharedSessionStore {
40 SharedSessionStore {
41 inner: Arc::new(SessionBackend::InMemory(Arc::new(
42 InMemorySessionStore::new(),
43 ))),
44 }
45}
46
47fn build_redis_store(url: &str, namespace: &str) -> Result<SharedSessionStore> {
48 let client = Client::open(url).context("invalid SESSION_REDIS_URL")?;
49 let store = RedisSessionStore::with_namespace(client, namespace);
50 Ok(SharedSessionStore {
51 inner: Arc::new(SessionBackend::Redis(Arc::new(store))),
52 })
53}
54
55impl SharedSessionStore {
56 pub async fn find_by_user(
58 &self,
59 ctx: &TenantCtx,
60 user: &UserId,
61 ) -> Result<Option<(SessionKey, SessionData)>> {
62 match self.inner.as_ref() {
63 SessionBackend::InMemory(store) => store.find_by_user(ctx, user).map_err(Into::into),
64 SessionBackend::Redis(store) => {
65 let store = Arc::clone(store);
66 let ctx = ctx.clone();
67 let user = user.clone();
68 blocking_call(move || store.find_by_user(&ctx, &user)).await
69 }
70 }
71 }
72
73 pub async fn create_session(&self, ctx: &TenantCtx, data: SessionData) -> Result<SessionKey> {
75 match self.inner.as_ref() {
76 SessionBackend::InMemory(store) => store.create_session(ctx, data).map_err(Into::into),
77 SessionBackend::Redis(store) => {
78 let store = Arc::clone(store);
79 let ctx = ctx.clone();
80 blocking_call(move || store.create_session(&ctx, data)).await
81 }
82 }
83 }
84
85 pub async fn update_session(&self, key: &SessionKey, data: SessionData) -> Result<()> {
87 match self.inner.as_ref() {
88 SessionBackend::InMemory(store) => store.update_session(key, data).map_err(Into::into),
89 SessionBackend::Redis(store) => {
90 let store = Arc::clone(store);
91 let key = key.clone();
92 blocking_call(move || store.update_session(&key, data)).await
93 }
94 }
95 }
96}
97
98async fn blocking_call<T, F>(f: F) -> Result<T>
99where
100 T: Send + 'static,
101 F: FnOnce() -> greentic_types::GResult<T> + Send + 'static,
102{
103 task::spawn_blocking(move || f().map_err(anyhow::Error::from))
104 .await
105 .context("session store operation failed")?
106}