Skip to content

Commit

Permalink
feat(auth): add some rudimentary session management
Browse files Browse the repository at this point in the history
  • Loading branch information
H1ghBre4k3r committed Aug 29, 2023
1 parent f8c1f13 commit eed3610
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 28 deletions.
18 changes: 13 additions & 5 deletions src/contexts/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}
}

Expand All @@ -15,7 +15,7 @@ pub struct AuthContext {
pub login: Action<Login, Result<(), ServerFnError>>,
pub logout: Action<Logout, Result<(), ServerFnError>>,
pub register: Action<Register, Result<RegistrationResult, ServerFnError>>,
pub user: Resource<(usize, usize, usize), Result<String, ServerFnError>>,
pub user: Resource<(usize, usize, usize), Result<Option<String>, ServerFnError>>,
}

impl AuthContext {
Expand Down Expand Up @@ -46,14 +46,22 @@ impl AuthContext {
}

#[server(GetUserId, "/api")]
async fn get_user_id(cx: Scope) -> Result<String, ServerFnError> {
async fn get_user_id(cx: Scope) -> Result<Option<String>, 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.
Expand Down
15 changes: 11 additions & 4 deletions src/functions/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}
}

Expand Down Expand Up @@ -85,15 +85,19 @@ pub async fn login(cx: Scope, username: String, password: String) -> Result<(),

let user: Option<User> = 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(());
Expand All @@ -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(())
Expand Down
10 changes: 7 additions & 3 deletions src/hooks/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ use surrealdb::{
Surreal,
};

pub async fn use_database(db: impl ToString) -> Surreal<Client> {
const NS_NAME: &str = "aoc-website";

const DB_NAME: &str = "aoc-website";

pub async fn use_database() -> Surreal<Client> {
let connection =
Surreal::new::<Ws>(env::var("SURREAL_URL").expect("no surreal url db user given"))
.await
Expand All @@ -22,8 +26,8 @@ pub async fn use_database(db: impl ToString) -> Surreal<Client> {
.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");

Expand Down
2 changes: 2 additions & 0 deletions src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
}
}
97 changes: 97 additions & 0 deletions src/model/session.rs
Original file line number Diff line number Diff line change
@@ -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<Thing>,
}

impl Session {
pub async fn find_by_id(id: &str) -> Option<Session> {
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<User> {
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<Option<LoggedInModel>, 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<Session> {
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:?}");
};
}
}
44 changes: 43 additions & 1 deletion src/model/user.rs
Original file line number Diff line number Diff line change
@@ -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<Session>,
}

impl User {
Expand All @@ -23,6 +29,28 @@ impl User {
Ok(())
}

pub async fn get_by_id(id: impl ToString) -> Option<User> {
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<User> {
let Some(user) = UserRepository::get_by_username(username)
.await
Expand All @@ -45,6 +73,20 @@ impl User {
username,
password,
email,
sessions: vec![],
})
}

pub async fn login(&mut self) -> Option<String> {
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)
}
}
2 changes: 2 additions & 0 deletions src/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
}
}
43 changes: 43 additions & 0 deletions src/repository/session.rs
Original file line number Diff line number Diff line change
@@ -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<Thing>,
}

impl SessionRepository {
const TABLE: &str = "session";

pub fn id(&self) -> Option<String> {
self.id.as_ref().map(|id| format!("{}:{}", id.tb, id.id))
}

pub async fn create() -> Result<Option<SessionRepository>, surrealdb::Error> {
let db = use_database().await;
let result: Vec<SessionRepository> = 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<SessionRepository> = db.delete((tb, id)).await?;
Ok(())
}
}
18 changes: 7 additions & 11 deletions src/repository/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,23 @@ impl UserRepository {
}

pub async fn get_all() -> Result<Vec<UserRepository>, 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<Option<UserRepository>, 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<UserRepository> = db.select((Self::TABLE, id.to_string())).await?;

result.take(0)
Ok(result)
}

pub async fn get_by_username(
username: impl ToString,
) -> Result<Option<UserRepository>, 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;")
Expand All @@ -56,9 +52,9 @@ impl UserRepository {
password: String,
email: String,
) -> Result<Option<UserRepository>, surrealdb::Error> {
let db = use_database("aoc-website").await;
let db = use_database().await;
let result: Vec<UserRepository> = db
.create("user")
.create(Self::TABLE)
.content(UserRepository {
username,
password,
Expand Down
Loading

0 comments on commit eed3610

Please sign in to comment.