diff --git a/Cargo.lock b/Cargo.lock index 15c72cc0..842e75f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,7 @@ dependencies = [ "dotenv", "gotrue", "gotrue-entity", + "redis", "reqwest", "serde", "serde_json", diff --git a/admin_frontend/Cargo.toml b/admin_frontend/Cargo.toml index 1022cecb..45975d6f 100644 --- a/admin_frontend/Cargo.toml +++ b/admin_frontend/Cargo.toml @@ -13,6 +13,7 @@ askama = "0.12.1" axum-extra = { version = "0.8.0", features = ["cookie"] } serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" +redis = { version = "0.23.3", features = [ "aio", "tokio-comp", "connection-manager"] } # local dependencies gotrue = { path = "../libs/gotrue" } diff --git a/admin_frontend/dev.env b/admin_frontend/dev.env index 7e13181f..4e6f3d37 100644 --- a/admin_frontend/dev.env +++ b/admin_frontend/dev.env @@ -1 +1,2 @@ GOTRUE_URL=http://localhost:9998 +REDIS_URL=redis://localhost:6380 diff --git a/admin_frontend/src/main.rs b/admin_frontend/src/main.rs index 421b44eb..be88a0b2 100644 --- a/admin_frontend/src/main.rs +++ b/admin_frontend/src/main.rs @@ -2,6 +2,7 @@ mod access_token; mod error; mod models; mod response; +mod session; mod templates; mod web_api; mod web_app; @@ -17,8 +18,18 @@ async fn main() { reqwest::Client::new(), &std::env::var("GOTRUE_URL").unwrap_or("http://gotrue:9999".to_string()), ); + let redis_client = + redis::Client::open(std::env::var("REDIS_URL").unwrap_or("redis://redis:6379".to_string())) + .unwrap() + .get_tokio_connection_manager() + .await + .unwrap(); + let session_store = session::SessionStorage::new(redis_client); - let state = AppState { gotrue_client }; + let state = AppState { + gotrue_client, + session_store, + }; let web_app_router = web_app::router().with_state(state.clone()); let web_api_router = web_api::router().with_state(state); @@ -37,4 +48,5 @@ async fn main() { #[derive(Clone)] pub struct AppState { pub gotrue_client: gotrue::api::Client, + pub session_store: session::SessionStorage, } diff --git a/admin_frontend/src/session.rs b/admin_frontend/src/session.rs new file mode 100644 index 00000000..2e5a7ccb --- /dev/null +++ b/admin_frontend/src/session.rs @@ -0,0 +1,104 @@ +use redis::{aio::ConnectionManager, AsyncCommands, FromRedisValue, ToRedisArgs}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +static SESSION_EXPIRATION: usize = 60 * 60 * 24; // 1 day + +#[derive(Clone)] +pub struct SessionStorage { + redis_client: ConnectionManager, +} + +fn session_id_key(session_id: &str) -> String { + format!("web::session::{}", session_id) +} + +impl SessionStorage { + pub fn new(redis_client: ConnectionManager) -> Self { + Self { redis_client } + } + + pub async fn get_user_session(&self, session_id: &str) -> Option { + let key = session_id_key(session_id); + let s: Result = self.redis_client.clone().get(&key).await; + match s { + Ok(s) => Some(s), + Err(e) => { + println!("redis error: {:?}", e); + None + }, + } + } + + pub async fn put_user_session(&self, user_session: UserSession) -> redis::RedisResult<()> { + let key = session_id_key(&user_session.session_id); + self + .redis_client + .clone() + .set_options( + key, + user_session, + redis::SetOptions::default().with_expiration(redis::SetExpiry::EX(SESSION_EXPIRATION)), + ) + .await + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserSession { + session_id: String, + access_token: String, + refresh_token: String, +} + +impl UserSession { + pub fn new(session_id: String, access_token: String, refresh_token: String) -> Self { + Self { + session_id, + access_token, + refresh_token, + } + } +} + +impl ToRedisArgs for UserSession { + fn write_redis_args(&self, out: &mut W) + where + W: ?Sized + redis::RedisWrite, + { + let s = serde_json::to_string(self).unwrap(); + out.write_arg(s.as_bytes()); + } +} + +impl FromRedisValue for UserSession { + fn from_redis_value(v: &redis::Value) -> redis::RedisResult { + let bytes = expect_redis_value_data(v)?; + expect_redis_json_bytes(bytes) + } +} + +fn expect_redis_json_bytes(v: &[u8]) -> redis::RedisResult +where + T: DeserializeOwned, +{ + let res: Result = serde_json::from_slice(v); + match res { + Ok(v) => Ok(v), + Err(e) => Err(redis::RedisError::from(( + redis::ErrorKind::TypeError, + "redis data json deserialization failed!", + e.to_string(), + ))), + } +} + +fn expect_redis_value_data(v: &redis::Value) -> redis::RedisResult<&[u8]> { + match v { + redis::Value::Data(ref bytes) => Ok(bytes), + x => Err(redis::RedisError::from(( + redis::ErrorKind::TypeError, + "unexpected value from redis", + format!("redis value is not data: {:?}", x), + ))), + } +} diff --git a/admin_frontend/templates/users.html b/admin_frontend/templates/users.html index d08a40e2..c0039666 100644 --- a/admin_frontend/templates/users.html +++ b/admin_frontend/templates/users.html @@ -2,6 +2,10 @@

User List

+ + + +
    @@ -14,7 +18,6 @@ {% for user in users %} - @@ -22,6 +25,32 @@ {% endfor %}
    {{ user.id|escape }} {{ user.email|escape }} {{ user.created_at|escape }} Go
    - +