From eed36107d7ee035a0f06758a4b96de96d6183834 Mon Sep 17 00:00:00 2001 From: H1ghBre4k3r Date: Wed, 30 Aug 2023 01:11:44 +0200 Subject: [PATCH] feat(auth): add some rudimentary session management --- src/contexts/auth.rs | 18 ++++++-- src/functions/auth.rs | 15 ++++-- src/hooks/database.rs | 10 ++-- src/model/mod.rs | 2 + src/model/session.rs | 97 +++++++++++++++++++++++++++++++++++++++ src/model/user.rs | 44 +++++++++++++++++- src/repository/mod.rs | 2 + src/repository/session.rs | 43 +++++++++++++++++ src/repository/user.rs | 18 +++----- src/utils/password.rs | 8 ++-- 10 files changed, 229 insertions(+), 28 deletions(-) create mode 100644 src/model/session.rs create mode 100644 src/repository/session.rs diff --git a/src/contexts/auth.rs b/src/contexts/auth.rs index 43ca29e..0da4add 100644 --- a/src/contexts/auth.rs +++ b/src/contexts/auth.rs @@ -6,7 +6,7 @@ use crate::functions::{Login, Logout, Register, RegistrationResult}; cfg_if! { if #[cfg(feature = "ssr")] { - use crate::hooks::use_identity; + use crate::{hooks::use_identity, model::Session}; } } @@ -15,7 +15,7 @@ pub struct AuthContext { pub login: Action>, pub logout: Action>, pub register: Action>, - pub user: Resource<(usize, usize, usize), Result>, + pub user: Resource<(usize, usize, usize), Result, ServerFnError>>, } impl AuthContext { @@ -46,14 +46,22 @@ impl AuthContext { } #[server(GetUserId, "/api")] -async fn get_user_id(cx: Scope) -> Result { +async fn get_user_id(cx: Scope) -> Result, ServerFnError> { let identity = use_identity(cx)?; - let id = identity + let session_id = identity .id() .map_err(|_| ServerFnError::ServerError("User Not Found!".to_string()))?; - Ok(id) + match Session::find_user_via_session(&session_id).await { + Some(user) => { + return Ok(Some(user.username)); + } + None => { + identity.logout(); + return Err(ServerFnError::ServerError("Inactive session!".to_string())); + } + } } /// Provide an AuthContext for use in child components. diff --git a/src/functions/auth.rs b/src/functions/auth.rs index 895ab5e..47858dd 100644 --- a/src/functions/auth.rs +++ b/src/functions/auth.rs @@ -13,7 +13,7 @@ if #[cfg(feature = "ssr")] { }; use crate::hooks::use_identity; use crate::utils::password::{verify_password, hash_password}; - use crate::model::User; + use crate::model::{User, Session}; } } @@ -85,15 +85,19 @@ pub async fn login(cx: Scope, username: String, password: String) -> Result<(), let user: Option = User::get_by_username(&username).await; - let Some(user) = user else { + let Some(mut user) = user else { return Err(ServerFnError::ServerError("User not found".into())); }; - let Ok(true) = verify_password(password, user.password) else { + let Ok(true) = verify_password(&password, &user.password) else { return Err(ServerFnError::ServerError("User not found".into())); }; - Identity::login(&req.extensions(), username.clone()).unwrap(); + let Some(session_id) = user.login().await else { + return Err(ServerFnError::ServerError("Some Error".into())); + }; + + Identity::login(&req.extensions(), session_id).unwrap(); leptos_actix::redirect(cx, "/"); return Ok(()); @@ -105,6 +109,9 @@ pub async fn logout(cx: Scope) -> Result<(), ServerFnError> { return Ok(()); }; + let session_id = identity.id().expect("session did not have an error"); + Session::destroy(&session_id).await; + identity.logout(); Ok(()) diff --git a/src/hooks/database.rs b/src/hooks/database.rs index 3bbb039..6aca96d 100644 --- a/src/hooks/database.rs +++ b/src/hooks/database.rs @@ -6,7 +6,11 @@ use surrealdb::{ Surreal, }; -pub async fn use_database(db: impl ToString) -> Surreal { +const NS_NAME: &str = "aoc-website"; + +const DB_NAME: &str = "aoc-website"; + +pub async fn use_database() -> Surreal { let connection = Surreal::new::(env::var("SURREAL_URL").expect("no surreal url db user given")) .await @@ -22,8 +26,8 @@ pub async fn use_database(db: impl ToString) -> Surreal { .expect("could not login to database"); connection - .use_ns("aoc-website") - .use_db(db.to_string()) + .use_ns(NS_NAME) + .use_db(DB_NAME) .await .expect("could not switch to correct namespace"); diff --git a/src/model/mod.rs b/src/model/mod.rs index 84be66f..965fc47 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -3,7 +3,9 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { mod user; + mod session; pub use self::user::*; + pub use self::session::*; } } diff --git a/src/model/session.rs b/src/model/session.rs new file mode 100644 index 0000000..9511086 --- /dev/null +++ b/src/model/session.rs @@ -0,0 +1,97 @@ +use leptos::*; +use serde::{Deserialize, Serialize}; +use surrealdb::sql::Thing; + +use crate::{hooks::use_database, repository::SessionRepository}; + +use super::User; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + pub id: String, + pub user_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct LoggenInRelation { + user_id: String, + session_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct LoggedInModel { + id: Thing, + users: Vec, +} + +impl Session { + pub async fn find_by_id(id: &str) -> Option { + let Some(user) = Self::find_user_via_session(id).await else { + return None; + }; + + Some(Session { + id: id.to_string(), + user_id: user.id, + }) + } + + pub async fn find_user_via_session(session_id: &str) -> Option { + let db = use_database().await; + + let Ok(mut response) = db + .query(format!( + "select id, <-logged_in<-user as users from {session_id};" + )) + .await + else { + return None; + }; + + let Ok(Some(result)): Result, surrealdb::Error> = response.take(0) + else { + return None; + }; + + let Some(user) = result.users.get(0) else { + return None; + }; + + User::get_by_id(user.id.clone()).await + } + + pub async fn new(user: &User) -> Option { + let Some(session) = SessionRepository::create().await.ok().flatten() else { + return None; + }; + + let session_id = session.id().expect("session from DB should have ID"); + + let db = use_database().await; + + if let Err(e) = db + .query(format!("RELATE {}->logged_in->{}", user.id, session_id)) + .await + { + error!( + "Error creating a relation between user ({}) and session ({}): {e:?}", + user.id, session_id + ); + if let Err(e) = SessionRepository::delete(&session_id).await { + error!("Error deleting session ({session_id}): {e:?}"); + }; + return None; + }; + + Some(Session { + id: session_id, + user_id: user.id.clone(), + }) + } + + pub async fn destroy(session_id: &str) { + if let Err(e) = SessionRepository::delete(&session_id).await { + error!("Error deleting session ({session_id}): {e:?}"); + }; + } +} diff --git a/src/model/user.rs b/src/model/user.rs index 303d26d..eb9d49a 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -1,13 +1,19 @@ use std::error::Error; +use leptos::error; +use serde::{Deserialize, Serialize}; + use crate::repository::UserRepository; -#[derive(Debug, Clone, Default)] +use super::Session; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct User { pub id: String, pub username: String, pub email: String, pub password: String, + pub sessions: Vec, } impl User { @@ -23,6 +29,28 @@ impl User { Ok(()) } + pub async fn get_by_id(id: impl ToString) -> Option { + let Some(user) = UserRepository::get_by_id(id).await.ok().flatten() else { + return None; + }; + + let id = user.id().expect("user from database should have id"); + let UserRepository { + username, + password, + email, + .. + } = user; + + Some(Self { + id, + username, + password, + email, + sessions: vec![], + }) + } + pub async fn get_by_username(username: impl ToString) -> Option { let Some(user) = UserRepository::get_by_username(username) .await @@ -45,6 +73,20 @@ impl User { username, password, email, + sessions: vec![], }) } + + pub async fn login(&mut self) -> Option { + let Some(session) = Session::new(self).await else { + error!("failed to login user ({})", self.id); + return None; + }; + + let id = session.id.clone(); + + self.sessions.push(session); + + Some(id) + } } diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 84be66f..965fc47 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -3,7 +3,9 @@ use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { mod user; + mod session; pub use self::user::*; + pub use self::session::*; } } diff --git a/src/repository/session.rs b/src/repository/session.rs new file mode 100644 index 0000000..4e16ded --- /dev/null +++ b/src/repository/session.rs @@ -0,0 +1,43 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use surrealdb::sql::Thing; + +use crate::hooks::use_database; + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct SessionRepository { + #[serde(skip_serializing)] + id: Option, +} + +impl SessionRepository { + const TABLE: &str = "session"; + + pub fn id(&self) -> Option { + self.id.as_ref().map(|id| format!("{}:{}", id.tb, id.id)) + } + + pub async fn create() -> Result, surrealdb::Error> { + let db = use_database().await; + let result: Vec = db + .create(Self::TABLE) + .content(SessionRepository { + ..Default::default() + }) + .await?; + + Ok(result.get(0).cloned()) + } + + pub async fn delete(id: &str) -> Result<(), surrealdb::Error> { + let Ok(Thing { tb, id }) = Thing::from_str(id) else { + return Ok(()); + }; + + let db = use_database().await; + + let _: Option = db.delete((tb, id)).await?; + Ok(()) + } +} diff --git a/src/repository/user.rs b/src/repository/user.rs index 038fe06..71b738e 100644 --- a/src/repository/user.rs +++ b/src/repository/user.rs @@ -20,27 +20,23 @@ impl UserRepository { } pub async fn get_all() -> Result, surrealdb::Error> { - let db = use_database("aoc-website").await; + let db = use_database().await; db.select(Self::TABLE).await } pub async fn get_by_id(id: impl ToString) -> Result, surrealdb::Error> { - let db = use_database("aoc-website").await; + let db = use_database().await; - let mut result = db - .query("SELECT * FROM type::table($table) where id = $id;") - .bind(("table", Self::TABLE)) - .bind(("id", id.to_string())) - .await?; + let result: Option = db.select((Self::TABLE, id.to_string())).await?; - result.take(0) + Ok(result) } pub async fn get_by_username( username: impl ToString, ) -> Result, surrealdb::Error> { - let db = use_database("aoc-website").await; + let db = use_database().await; let mut result = db .query("SELECT * FROM type::table($table) where username = $username;") @@ -56,9 +52,9 @@ impl UserRepository { password: String, email: String, ) -> Result, surrealdb::Error> { - let db = use_database("aoc-website").await; + let db = use_database().await; let result: Vec = db - .create("user") + .create(Self::TABLE) .content(UserRepository { username, password, diff --git a/src/utils/password.rs b/src/utils/password.rs index b821637..79d8115 100644 --- a/src/utils/password.rs +++ b/src/utils/password.rs @@ -18,8 +18,8 @@ pub fn hash_password(password: String) -> Result { Ok(password_hash) } -pub fn verify_password(password: String, hash: String) -> Result { - let parsed_hash = PasswordHash::new(&hash)?; +pub fn verify_password(password: &String, hash: &String) -> Result { + let parsed_hash = PasswordHash::new(hash)?; Ok(Argon2::default() .verify_password(password.as_bytes(), &parsed_hash) @@ -40,7 +40,7 @@ mod tests { let password = "some_password".to_string(); let hash = hash_password(password.clone()).unwrap(); - assert_eq!(verify_password(password, hash), Ok(true)); + assert_eq!(verify_password(&password, &hash), Ok(true)); } #[test] @@ -49,6 +49,6 @@ mod tests { let hash = hash_password(password.clone()).unwrap(); let password = "other_password".to_string(); - assert_eq!(verify_password(password, hash), Ok(false)); + assert_eq!(verify_password(&password, &hash), Ok(false)); } }