Set up session creation code

This commit is contained in:
2025-12-11 12:40:55 -05:00
parent 4a88bbb997
commit 684c0d544e
3 changed files with 165 additions and 56 deletions

View File

@@ -16,7 +16,7 @@ use tokio::sync::{RwLock, mpsc::UnboundedSender};
use visions_types::*;
use crate::{
database::{CharacterFilter, Database, Fatal},
database::{self, CharacterFilter, CreateSessionError, Database},
types::{Game, Scene, Tabletop, User},
};
@@ -38,8 +38,24 @@ pub enum Error {
#[error("Scene not found")]
SceneNotFound(SceneId),
#[error("A fatal error has occured. The application should have shut down.")]
Fatal(String),
#[error("User not found")]
UserNotFound(UserId),
#[error("Cannot open database")]
CannotOpen,
#[error("Cannot write to the database")]
CannotWrite,
}
#[derive(Debug, thiserror::Error)]
#[error("A fatal error has occured. The application should have shut down.")]
pub struct Fatal(String);
impl From<database::Fatal> for Fatal {
fn from(value: database::Fatal) -> Self {
Self(format!("{}", value))
}
}
#[derive(Clone)]
@@ -136,14 +152,23 @@ impl App {
}
}
pub async fn create_session(&self, id: &UserId) -> SessionId {
// eprintln!("create_session: {:?}", id);
// let state = self.inner.write().await;
// match state.database.new_session(id.clone()).await {
// ResultExt::Ok(id) => id,
// _ => panic!(),
// }
todo!()
pub async fn create_session(&self, id: &UserId) -> ResultExt<SessionId, Error, Fatal> {
let state = self.inner.write().await;
match state.database.new_session(id) {
ResultExt::Ok(id) => ResultExt::Ok(id),
ResultExt::Err(CreateSessionError::CannotFindUser) => {
ResultExt::Err(Error::UserNotFound(id.clone()))
}
ResultExt::Err(CreateSessionError::CannotOpen(err)) => {
eprintln!("Error opening database: {:?}", err);
ResultExt::Err(Error::CannotOpen)
}
ResultExt::Err(CreateSessionError::WriteError(err)) => {
eprintln!("Error writing to the database: {:?}", err);
ResultExt::Err(Error::CannotWrite)
}
ResultExt::Fatal(err) => ResultExt::Fatal(err.into()),
}
}
pub async fn delete_session(&self, session_id: &SessionId) {
@@ -152,18 +177,16 @@ impl App {
todo!()
}
pub async fn user_from_session(&self, session_id: &SessionId) -> Result<Option<User>, Error> {
// let state = self.inner.read().await;
// match state
// .database
// .session_info(session_id.clone())
// .await
// .expect("")
// {
// Some(user_id) => Ok(state.database.user(&user_id).await.expect("")),
// None => Ok(None),
// }
todo!()
pub async fn user_from_session(
&self,
session_id: &SessionId,
) -> ResultExt<Option<User>, Error, Fatal> {
let state = self.inner.read().await;
match state.database.user_by_session(session_id) {
ResultExt::Ok(user) => ResultExt::Ok(user),
ResultExt::Err(_err) => ResultExt::Err(Error::CannotOpen),
ResultExt::Fatal(err) => ResultExt::Fatal(err.into()),
}
}
#[allow(dead_code)]

View File

@@ -487,7 +487,6 @@ async fn app_can_validate_passwords() {
assert_matches!(app.check_password(VAKARIAN_USERNAME, "").await, Ok(None));
}
/*
/// SCENARIO: The app can create a session for a user
/// GIVEN: The app has already been populated with data
/// GIVEN: We already have a valid user id
@@ -504,15 +503,19 @@ async fn can_create_sessions() {
let session = app.create_session(&UserId::from(VAKARIAN_ID)).await;
// THEN: The session can be created, and the user can be retrieved from the session ID
assert_matches!(session, Ok(session_id) => {
assert_matches!(app.user_from_session(&session_id).await, Ok(Some(user)) => {
assert_matches!(session, ResultExt::Ok(session_id) => {
assert_matches!(app.user_from_session(&session_id).await, ResultExt::Ok(Some(user)) => {
assert_eq!(user.id, UserId::from(VAKARIAN_ID));
})
});
assert_matches!(app.user_from_session(&SessionId::default()).await, Ok(None));
assert_matches!(
app.user_from_session(&SessionId::default()).await,
ResultExt::Ok(None)
);
}
/*
/// SCENARIO: If a session has been deleted, the data for it is no longer available
/// GIVEN: The app has already been populated with data
/// GIVEN: We have a valid session

View File

@@ -170,12 +170,15 @@ impl From<rusqlite::Error> for CannotOpen {
}
#[derive(Debug, Error)]
pub enum CannotFind {
#[error("Cannot find object")]
CannotFind,
pub enum CreateSessionError {
#[error("Cannot find user")]
CannotFindUser,
#[error("Cannot open database")]
CannotOpen(#[from] CannotOpen),
#[error("Cannot write to database")]
WriteError(#[from] rusqlite::Error),
}
#[derive(Debug, Error)]
@@ -332,10 +335,9 @@ impl Database {
Connection::open(self.path.clone()).map_err(|err| CannotOpen(format!("{:?}", err)))
}
/*
/// Create a new session for this user, setting the expiration time to now + session_duration.
pub fn new_session(&self, user_id: &UserId) -> Result<SessionId, Fatal> {
pub fn new_session(&self, user_id: &UserId) -> ResultExt<SessionId, CreateSessionError, Fatal> {
fail_point!("sqldatabase-new-session", |_| {
Result::Err(Fatal::Unhandled(
"FAILPOINT: sqldatabase-new-session".to_owned(),
@@ -350,15 +352,50 @@ impl Database {
.as_secs()
.to_string();
self.connection
.execute(
"INSERT INTO sessions (id, user_id, expiration) VALUES (?1, ?2, ?3)",
rusqlite::params![session_id.as_str(), user_id.as_str(), expiration],
)
.map(|_| session_id)
.map_err(|err| err.into())
let mut connection = return_error!(ResultExt::from(self.open()).map_err(|err| err.into()));
let tx = return_error!(ResultExt::promote_fatal(
connection.transaction().map_err(|err| err.into())
));
let result = {
// The nesting is necessary in order to ensure that user_query goes out of scope and
// drops the implicit reference against tx.
let mut user_query = return_error!(ResultExt::promote_fatal(
tx.prepare_cached("SELECT id FROM users WHERE id = ?1")
.map_err(|err| err.into())
));
match user_query.query_one(rusqlite::params![user_id.as_str()], |_| Ok(())) {
Ok(_) => {
let res = tx.execute(
"INSERT INTO sessions (id, user_id, expiration) VALUES (?1, ?2, ?3)",
rusqlite::params![session_id.as_str(), user_id.as_str(), expiration],
);
match res {
Ok(_) => ResultExt::Ok(session_id),
Err(err) => ResultExt::Err(CreateSessionError::WriteError(err)),
}
}
Err(rusqlite::Error::QueryReturnedNoRows) => {
ResultExt::Err(CreateSessionError::CannotFindUser)
}
Err(err) => ResultExt::Fatal(err.into()),
}
};
match result {
ResultExt::Ok(_) => {
return_error!(ResultExt::promote_fatal(
tx.commit().map_err(|err| err.into())
))
}
_ => {}
}
result
}
/*
/// Create a new session for this user, setting the expiration time to now + session_duration.
#[cfg(test)]
pub fn new_session_with_timeout(
@@ -727,25 +764,27 @@ impl Database {
Result::Ok(Some(tabletop))
}
fn user(&mut self, user_id: &UserId) -> Result<Option<User>, Fatal> {
let tx = self.connection.transaction()?;
let mut stmt = tx.prepare_cached(
"SELECT id, email, name, admin, status, password FROM users WHERE id = ?1",
)?;
let mut rows = stmt.query(rusqlite::params![user_id.as_str()])?;
if let Some(row) = rows.next()? {
let user = user_from_row(row)?;
return Ok(Some(user));
}
Ok(None)
}
*/
pub fn user_by_email(&self, email: &str) -> ResultExt<Option<User>, CannotFind, Fatal> {
fn user(&mut self, user_id: &UserId) -> ResultExt<Option<User>, CannotOpen, Fatal> {
let mut connection = return_error!(ResultExt::from(self.open()));
let tx = return_error!(ResultExt::promote_fatal(
connection.transaction().map_err(|err| err.into())
));
let row = return_error!(ResultExt::promote_fatal(
user_by_id(&tx, user_id).map_err(|err| err.into())
));
let user: Result<Option<User>, Fatal> = match row {
Some(row) => User::try_from(row).map(Some),
None => Ok(None),
};
ResultExt::promote_fatal(user)
}
pub fn user_by_email(&self, email: &str) -> ResultExt<Option<User>, CannotOpen, Fatal> {
let mut connection = return_error!(ResultExt::from(self.open()).map_err(|err| err.into()));
let tx = return_error!(ResultExt::promote_fatal(
connection.transaction().map_err(|err| err.into())
@@ -763,6 +802,27 @@ impl Database {
ResultExt::promote_fatal(user)
}
pub fn user_by_session(
&self,
session_id: &SessionId,
) -> ResultExt<Option<User>, CannotOpen, Fatal> {
let mut connection = return_error!(ResultExt::from(self.open()));
let tx = return_error!(ResultExt::promote_fatal(
connection.transaction().map_err(|err| err.into())
));
let row = return_error!(ResultExt::promote_fatal(
user_by_session(&tx, session_id).map_err(|err| err.into())
));
let user: Result<Option<User>, Fatal> = match row {
Some(row) => User::try_from(row).map(Some),
None => Ok(None),
};
ResultExt::promote_fatal(user)
}
/*
fn users(&mut self) -> Result<Vec<User>, Fatal> {
let tx = self.connection.transaction()?;
@@ -907,6 +967,20 @@ impl Database {
*/
}
fn user_by_session(
tx: &Transaction,
session_id: &SessionId,
) -> Result<Option<UserRow>, rusqlite::Error> {
let mut stmt = tx.prepare_cached(
"SELECT users.id, users.email, users.name, users.admin, users.status, users.password
FROM users INNER JOIN sessions ON sessions.user_id = users.id
WHERE sessions.id = ?1",
)?;
stmt.query_one(rusqlite::params![session_id.as_str()], user_row)
.optional()
}
fn user_by_email(tx: &Transaction, email: &str) -> Result<Option<UserRow>, rusqlite::Error> {
let mut stmt = tx.prepare_cached(
"SELECT id, email, name, admin, status, password FROM users WHERE email = ?1",
@@ -916,6 +990,15 @@ fn user_by_email(tx: &Transaction, email: &str) -> Result<Option<UserRow>, rusql
.optional()
}
fn user_by_id(tx: &Transaction, id: &UserId) -> Result<Option<UserRow>, rusqlite::Error> {
let mut stmt = tx.prepare_cached(
"SELECT id, email, name, admin, status, password FROM users WHERE id = ?1",
)?;
stmt.query_one(rusqlite::params![id.as_str()], user_row)
.optional()
}
fn read_game(tx: &Transaction, game_id: &GameId) -> Result<Option<Game>, rusqlite::Error> {
let mut game_query =
tx.prepare_cached("SELECT id, name, system, gm, background FROM games WHERE id = ?1")?;