Set up session creation code
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
Reference in New Issue
Block a user