Add session creation and lookup

This commit is contained in:
Savanni D'Gerinel 2024-12-27 14:02:43 -05:00
parent 1d400ce38b
commit d5f4b7cfa5
8 changed files with 339 additions and 104 deletions

View File

@ -1,11 +1,18 @@
CREATE TABLE users( CREATE TABLE users(
uuid TEXT PRIMARY KEY, uuid TEXT PRIMARY KEY,
name TEXT, name TEXT UNIQUE,
password TEXT, password TEXT,
admin BOOLEAN, admin BOOLEAN,
enabled BOOLEAN enabled BOOLEAN
); );
CREATE TABLE sessions(
id TEXT PRIMARY KEY,
user_id TEXT,
FOREIGN KEY(user_id) REFERENCES users(uuid)
);
CREATE TABLE games( CREATE TABLE games(
uuid TEXT PRIMARY KEY, uuid TEXT PRIMARY KEY,
name TEXT name TEXT
@ -28,5 +35,3 @@ CREATE TABLE roles(
FOREIGN KEY(game_id) REFERENCES games(uuid) FOREIGN KEY(game_id) REFERENCES games(uuid)
); );
INSERT INTO users VALUES ("admin", "admin", "", true, true);

View File

@ -10,7 +10,7 @@ use uuid::Uuid;
use crate::{ use crate::{
asset_db::{self, AssetId, Assets}, asset_db::{self, AssetId, Assets},
database::{CharacterId, Database, UserId}, database::{CharacterId, Database, SessionId, UserId},
types::{AppError, FatalError, Game, Message, Tabletop, User, RGB}, types::{AppError, FatalError, Game, Message, Tabletop, User, RGB},
}; };
@ -61,7 +61,7 @@ impl Core {
pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> { pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> {
let mut state = self.0.write().await; let mut state = self.0.write().await;
let admin_user = return_error!(match state.db.user(UserId::from("admin")).await { let admin_user = return_error!(match state.db.user(&UserId::from("admin")).await {
Ok(Some(admin_user)) => ok(admin_user), Ok(Some(admin_user)) => ok(admin_user),
Ok(None) => { Ok(None) => {
return ok(Status { return ok(Status {
@ -106,6 +106,15 @@ impl Core {
} }
} }
pub async fn user_by_username(&self, username: &str) -> ResultExt<Option<User>, AppError, FatalError> {
let state = self.0.read().await;
match state.db.user_by_username(username).await {
Ok(Some(user_row)) => ok(Some(User::from(user_row))),
Ok(None) => ok(None),
Err(err) => fatal(err),
}
}
pub async fn list_users(&self) -> ResultExt<Vec<User>, AppError, FatalError> { pub async fn list_users(&self) -> ResultExt<Vec<User>, AppError, FatalError> {
let users = self.0.write().await.db.users().await; let users = self.0.write().await.db.users().await;
match users { match users {
@ -206,7 +215,7 @@ impl Core {
password: String, password: String,
) -> ResultExt<(), AppError, FatalError> { ) -> ResultExt<(), AppError, FatalError> {
let mut state = self.0.write().await; let mut state = self.0.write().await;
let user = match state.db.user(uuid.clone()).await { let user = match state.db.user(&uuid).await {
Ok(Some(row)) => row, Ok(Some(row)) => row,
Ok(None) => return error(AppError::NotFound(uuid.as_str().to_owned())), Ok(None) => return error(AppError::NotFound(uuid.as_str().to_owned())),
Err(err) => return fatal(err), Err(err) => return fatal(err),
@ -221,10 +230,13 @@ impl Core {
} }
} }
pub async fn auth(&self, username: String, password: String) -> ResultExt<UserId, AppError, FatalError> { pub async fn auth(&self, username: &str, password: &str) -> ResultExt<SessionId, AppError, FatalError> {
let state = self.0.write().await; let state = self.0.write().await;
match state.db.user_by_username(username).await { match state.db.user_by_username(&username).await {
Ok(Some(row)) if (row.password == password) => ok(row.id), Ok(Some(row)) if (row.password == password) => {
let session_id = state.db.create_session(row.id).await.unwrap();
ok(session_id)
}
Ok(_) => error(AppError::AuthFailed), Ok(_) => error(AppError::AuthFailed),
Err(err) => fatal(err), Err(err) => fatal(err),
} }
@ -244,7 +256,7 @@ mod test {
database::{DbConn, DiskDb}, database::{DbConn, DiskDb},
}; };
fn test_core() -> Core { async fn test_core() -> Core {
let assets = MemoryAssets::new(vec![ let assets = MemoryAssets::new(vec![
( (
AssetId::from("asset_1"), AssetId::from("asset_1"),
@ -274,19 +286,21 @@ mod test {
]); ]);
let memory_db: Option<PathBuf> = None; let memory_db: Option<PathBuf> = None;
let conn = DbConn::new(memory_db); let conn = DbConn::new(memory_db);
conn.save_user(None, "admin", "aoeu", true, true).await.unwrap();
conn.save_user(None, "gm_1", "aoeu", false, true).await.unwrap();
Core::new(assets, conn) Core::new(assets, conn)
} }
#[tokio::test] #[tokio::test]
async fn it_lists_available_images() { async fn it_lists_available_images() {
let core = test_core(); let core = test_core().await;
let image_paths = core.available_images().await; let image_paths = core.available_images().await;
assert_eq!(image_paths.len(), 2); assert_eq!(image_paths.len(), 2);
} }
#[tokio::test] #[tokio::test]
async fn it_retrieves_an_asset() { async fn it_retrieves_an_asset() {
let core = test_core(); let core = test_core().await;
assert_matches!(core.get_asset(AssetId::from("asset_1")).await, ResultExt::Ok((mime, data)) => { assert_matches!(core.get_asset(AssetId::from("asset_1")).await, ResultExt::Ok((mime, data)) => {
assert_eq!(mime.type_(), mime::IMAGE); assert_eq!(mime.type_(), mime::IMAGE);
assert_eq!(data, "abcdefg".as_bytes()); assert_eq!(data, "abcdefg".as_bytes());
@ -295,7 +309,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn it_can_retrieve_the_default_tabletop() { async fn it_can_retrieve_the_default_tabletop() {
let core = test_core(); let core = test_core().await;
assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => { assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => {
assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR); assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
assert_eq!(background_image, None); assert_eq!(background_image, None);
@ -304,7 +318,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn it_can_change_the_tabletop_background() { async fn it_can_change_the_tabletop_background() {
let core = test_core(); let core = test_core().await;
assert_matches!( assert_matches!(
core.set_background_image(AssetId::from("asset_1")).await, core.set_background_image(AssetId::from("asset_1")).await,
ResultExt::Ok(()) ResultExt::Ok(())
@ -317,7 +331,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn it_sends_notices_to_clients_on_tabletop_change() { async fn it_sends_notices_to_clients_on_tabletop_change() {
let core = test_core(); let core = test_core().await;
let client_id = core.register_client().await; let client_id = core.register_client().await;
let mut receiver = core.connect_client(client_id).await; let mut receiver = core.connect_client(client_id).await;
@ -336,4 +350,21 @@ mod test {
None => panic!("receiver did not get a message"), None => panic!("receiver did not get a message"),
} }
} }
#[tokio::test]
async fn it_creates_a_sessionid_on_successful_auth() {
let core = test_core().await;
match core.auth("admin", "aoeu").await {
ResultExt::Ok(session_id) => {
let st = core.0.read().await;
match st.db.session(session_id).await {
Ok(Some(user_row)) => assert_eq!(user_row.name, "admin"),
Ok(None) => panic!("no matching user row for the session id"),
Err(err) => panic!("{}", err),
}
},
ResultExt::Err(err) => panic!("{}", err),
ResultExt::Fatal(err) => panic!("{}", err),
}
}
} }

View File

@ -24,11 +24,13 @@ lazy_static! {
#[derive(Debug)] #[derive(Debug)]
enum Request { enum Request {
Charsheet(CharacterId), Charsheet(CharacterId),
CreateSession(UserId),
Games, Games,
SaveUser(Option<UserId>, String, String, bool, bool),
Session(SessionId),
User(UserId), User(UserId),
UserByUsername(String), UserByUsername(String),
Users, Users,
SaveUser(Option<UserId>, String, String, bool, bool),
} }
#[derive(Debug)] #[derive(Debug)]
@ -40,10 +42,12 @@ struct DatabaseRequest {
#[derive(Debug)] #[derive(Debug)]
enum DatabaseResponse { enum DatabaseResponse {
Charsheet(Option<CharsheetRow>), Charsheet(Option<CharsheetRow>),
CreateSession(SessionId),
Games(Vec<GameRow>), Games(Vec<GameRow>),
SaveUser(UserId),
Session(Option<UserRow>),
User(Option<UserRow>), User(Option<UserRow>),
Users(Vec<UserRow>), Users(Vec<UserRow>),
SaveUser(UserId),
} }
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
@ -80,6 +84,40 @@ impl FromSql for UserId {
} }
} }
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct SessionId(String);
impl SessionId {
pub fn new() -> Self {
Self(format!("{}", Uuid::new_v4().hyphenated()))
}
pub fn as_str<'a>(&'a self) -> &'a str {
&self.0
}
}
impl From<&str> for SessionId {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<String> for SessionId {
fn from(s: String) -> Self {
Self(s)
}
}
impl FromSql for SessionId {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
match value {
ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())),
_ => unimplemented!(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct GameId(String); pub struct GameId(String);
@ -177,14 +215,20 @@ pub struct CharsheetRow {
pub data: serde_json::Value, pub data: serde_json::Value,
} }
#[derive(Clone, Debug)]
pub struct SessionRow {
id: SessionId,
user_id: SessionId,
}
#[async_trait] #[async_trait]
pub trait Database: Send + Sync { pub trait Database: Send + Sync {
async fn user(&mut self, _: UserId) -> Result<Option<UserRow>, FatalError>; async fn user(&mut self, _: &UserId) -> Result<Option<UserRow>, FatalError>;
async fn user_by_username(&self, _: String) -> Result<Option<UserRow>, FatalError>; async fn user_by_username(&self, _: &str) -> Result<Option<UserRow>, FatalError>;
async fn save_user( async fn save_user(
&mut self, &self,
user_id: Option<UserId>, user_id: Option<UserId>,
name: &str, name: &str,
password: &str, password: &str,
@ -197,6 +241,10 @@ pub trait Database: Send + Sync {
async fn games(&mut self) -> Result<Vec<GameRow>, FatalError>; async fn games(&mut self) -> Result<Vec<GameRow>, FatalError>;
async fn character(&mut self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError>; async fn character(&mut self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError>;
async fn session(&self, id: SessionId) -> Result<Option<UserRow>, FatalError>;
async fn create_session(&self, id: UserId) -> Result<SessionId, FatalError>;
} }
pub struct DiskDb { pub struct DiskDb {
@ -268,7 +316,7 @@ impl DiskDb {
Ok(DiskDb { conn }) Ok(DiskDb { conn })
} }
fn user(&self, id: UserId) -> Result<Option<UserRow>, FatalError> { fn user(&self, id: &UserId) -> Result<Option<UserRow>, FatalError> {
let mut stmt = self let mut stmt = self
.conn .conn
.prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE uuid=?") .prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE uuid=?")
@ -293,13 +341,13 @@ impl DiskDb {
} }
} }
fn user_by_username(&self, username: String) -> Result<Option<UserRow>, FatalError> { fn user_by_username(&self, username: &str) -> Result<Option<UserRow>, FatalError> {
let mut stmt = self let mut stmt = self
.conn .conn
.prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE name=?") .prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE name=?")
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
let items: Vec<UserRow> = stmt let items: Vec<UserRow> = stmt
.query_map([username.as_str()], |row| { .query_map([username], |row| {
Ok(UserRow { Ok(UserRow {
id: row.get(0).unwrap(), id: row.get(0).unwrap(),
name: row.get(1).unwrap(), name: row.get(1).unwrap(),
@ -361,9 +409,7 @@ impl DiskDb {
Some(user_id) => { Some(user_id) => {
let mut stmt = self let mut stmt = self
.conn .conn
.prepare( .prepare("UPDATE users SET name=?, password=?, admin=?, enabled=? WHERE uuid=?")
"UPDATE users SET name=?, password=?, admin=?, enabled=? WHERE uuid=?",
)
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
stmt.execute((name, password, admin, enabled, user_id.as_str())) stmt.execute((name, password, admin, enabled, user_id.as_str()))
.unwrap(); .unwrap();
@ -394,6 +440,51 @@ impl DiskDb {
} }
} }
fn session(&self, session_id: &SessionId) -> Result<Option<UserRow>, FatalError> {
let mut stmt = self.conn
.prepare("SELECT u.uuid, u.name, u.password, u.admin, u.enabled FROM sessions s INNER JOIN users u ON u.uuid = s.user_id WHERE s.id = ?")
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
let items: Vec<UserRow> = stmt
.query_map([session_id.as_str()], |row| {
Ok(UserRow {
id: row.get(0).unwrap(),
name: row.get(1).unwrap(),
password: row.get(2).unwrap(),
admin: row.get(3).unwrap(),
enabled: row.get(4).unwrap(),
})
})
.unwrap()
.collect::<Result<Vec<UserRow>, rusqlite::Error>>()
.unwrap();
match &items[..] {
[] => Ok(None),
[item] => Ok(Some(item.clone())),
_ => Err(FatalError::NonUniqueDatabaseKey(
session_id.as_str().to_owned(),
)),
}
}
fn create_session(&self, user_id: &UserId) -> Result<SessionId, FatalError> {
match self.user(user_id) {
Ok(Some(_)) => {
let mut stmt = self
.conn
.prepare("INSERT INTO sessions VALUES (?, ?)")
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
let session_id = SessionId::new();
stmt.execute((session_id.as_str(), user_id.as_str()))
.unwrap();
Ok(session_id)
}
Ok(None) => Err(FatalError::DatabaseKeyMissing),
Err(err) => Err(err),
}
}
fn character(&self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError> { fn character(&self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
let mut stmt = self let mut stmt = self
.conn .conn
@ -456,7 +547,6 @@ async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
match req { match req {
Request::Charsheet(id) => { Request::Charsheet(id) => {
let sheet = db.character(id); let sheet = db.character(id);
println!("sheet retrieved: {:?}", sheet);
match sheet { match sheet {
Ok(sheet) => { Ok(sheet) => {
tx.send(DatabaseResponse::Charsheet(sheet)).await.unwrap(); tx.send(DatabaseResponse::Charsheet(sheet)).await.unwrap();
@ -464,11 +554,17 @@ async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
_ => unimplemented!(), _ => unimplemented!(),
} }
} }
Request::CreateSession(id) => {
let session_id = db.create_session(&id).unwrap();
tx.send(DatabaseResponse::CreateSession(session_id))
.await
.unwrap();
}
Request::Games => { Request::Games => {
unimplemented!(); unimplemented!();
} }
Request::User(uid) => { Request::User(uid) => {
let user = db.user(uid); let user = db.user(&uid);
match user { match user {
Ok(user) => { Ok(user) => {
tx.send(DatabaseResponse::User(user)).await.unwrap(); tx.send(DatabaseResponse::User(user)).await.unwrap();
@ -477,14 +573,20 @@ async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
} }
} }
Request::UserByUsername(username) => { Request::UserByUsername(username) => {
let user = db.user_by_username(username); let user = db.user_by_username(&username);
match user { match user {
Ok(user) => tx.send(DatabaseResponse::User(user)).await.unwrap(), Ok(user) => tx.send(DatabaseResponse::User(user)).await.unwrap(),
err => panic!("{:?}", err), err => panic!("{:?}", err),
} }
} }
Request::SaveUser(user_id, username, password, admin, enabled) => { Request::SaveUser(user_id, username, password, admin, enabled) => {
let user_id = db.save_user(user_id, username.as_ref(), password.as_ref(), admin, enabled); let user_id = db.save_user(
user_id,
username.as_ref(),
password.as_ref(),
admin,
enabled,
);
match user_id { match user_id {
Ok(user_id) => { Ok(user_id) => {
tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap(); tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap();
@ -492,6 +594,13 @@ async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
err => panic!("{:?}", err), err => panic!("{:?}", err),
} }
} }
Request::Session(session_id) => {
let user = db.session(&session_id);
match user {
Ok(user) => tx.send(DatabaseResponse::Session(user)).await.unwrap(),
err => panic!("{:?}", err),
}
}
Request::Users => { Request::Users => {
let users = db.users(); let users = db.users();
match users { match users {
@ -529,12 +638,12 @@ impl DbConn {
#[async_trait] #[async_trait]
impl Database for DbConn { impl Database for DbConn {
async fn user(&mut self, uid: UserId) -> Result<Option<UserRow>, FatalError> { async fn user(&mut self, uid: &UserId) -> Result<Option<UserRow>, FatalError> {
let (tx, rx) = bounded::<DatabaseResponse>(1); let (tx, rx) = bounded::<DatabaseResponse>(1);
let request = DatabaseRequest { let request = DatabaseRequest {
tx, tx,
req: Request::User(uid), req: Request::User(uid.clone()),
}; };
match self.conn.send(request).await { match self.conn.send(request).await {
@ -549,12 +658,12 @@ impl Database for DbConn {
} }
} }
async fn user_by_username(&self, username: String) -> Result<Option<UserRow>, FatalError> { async fn user_by_username(&self, username: &str) -> Result<Option<UserRow>, FatalError> {
let (tx, rx) = bounded::<DatabaseResponse>(1); let (tx, rx) = bounded::<DatabaseResponse>(1);
let request = DatabaseRequest { let request = DatabaseRequest {
tx, tx,
req: Request::UserByUsername(username), req: Request::UserByUsername(username.to_owned()),
}; };
match self.conn.send(request).await { match self.conn.send(request).await {
@ -570,7 +679,7 @@ impl Database for DbConn {
} }
async fn save_user( async fn save_user(
&mut self, &self,
user_id: Option<UserId>, user_id: Option<UserId>,
name: &str, name: &str,
password: &str, password: &str,
@ -661,6 +770,46 @@ impl Database for DbConn {
Err(_) => Err(FatalError::DatabaseConnectionLost), Err(_) => Err(FatalError::DatabaseConnectionLost),
} }
} }
async fn session(&self, id: SessionId) -> Result<Option<UserRow>, FatalError> {
let (tx, rx) = bounded::<DatabaseResponse>(1);
let request = DatabaseRequest {
tx,
req: Request::Session(id),
};
match self.conn.send(request).await {
Ok(()) => (),
Err(_) => return Err(FatalError::DatabaseConnectionLost),
};
match rx.recv().await {
Ok(DatabaseResponse::Session(row)) => Ok(row),
Ok(_) => Err(FatalError::MessageMismatch),
Err(_) => Err(FatalError::DatabaseConnectionLost),
}
}
async fn create_session(&self, id: UserId) -> Result<SessionId, FatalError> {
let (tx, rx) = bounded::<DatabaseResponse>(1);
let request = DatabaseRequest {
tx,
req: Request::CreateSession(id),
};
match self.conn.send(request).await {
Ok(()) => (),
Err(_) => return Err(FatalError::DatabaseConnectionLost),
};
match rx.recv().await {
Ok(DatabaseResponse::CreateSession(session_id)) => Ok(session_id),
Ok(_) => Err(FatalError::MessageMismatch),
Err(_) => Err(FatalError::DatabaseConnectionLost),
}
}
} }
#[cfg(test)] #[cfg(test)]
@ -668,7 +817,6 @@ mod test {
use std::path::PathBuf; use std::path::PathBuf;
use cool_asserts::assert_matches; use cool_asserts::assert_matches;
use result_extended::ResultExt;
use super::*; use super::*;
@ -678,7 +826,7 @@ mod test {
let no_path: Option<PathBuf> = None; let no_path: Option<PathBuf> = None;
let db = DiskDb::new(no_path).unwrap(); let db = DiskDb::new(no_path).unwrap();
db.save_user(None, "admin", "abcdefg", true, true); db.save_user(None, "admin", "abcdefg", true, true).unwrap();
let game_id = db.save_game(None, "Candela").unwrap(); let game_id = db.save_game(None, "Candela").unwrap();
(db, game_id) (db, game_id)
} }
@ -699,9 +847,6 @@ mod test {
let memory_db: Option<PathBuf> = None; let memory_db: Option<PathBuf> = None;
let mut conn = DbConn::new(memory_db); let mut conn = DbConn::new(memory_db);
assert_matches!( assert_matches!(conn.character(CharacterId::from("1")).await, Ok(None));
conn.character(CharacterId::from("1")).await,
Ok(None)
);
} }
} }

View File

@ -9,7 +9,11 @@ use warp::{
}; };
use crate::{ use crate::{
asset_db::AssetId, core::Core, handlers::{handle_auth, handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_get_users, handle_register_client, handle_server_status, handle_set_admin_password, handle_set_background_image, handle_unregister_client, RegisterRequest} asset_db::AssetId,
core::Core,
handlers::{
handle_available_images, handle_check_password, handle_connect_websocket, handle_file, handle_get_charsheet, handle_register_client, handle_server_status, handle_set_background_image, handle_unregister_client, RegisterRequest
},
}; };
fn cors<H, M>(methods: Vec<M>, headers: Vec<H>) -> Builder fn cors<H, M>(methods: Vec<M>, headers: Vec<H>) -> Builder
@ -23,6 +27,13 @@ where
.allow_headers(headers) .allow_headers(headers)
} }
pub fn route_healthcheck() -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone
{
warp::path!("api" / "v1" / "healthcheck")
.and(warp::get())
.map(|| warp::reply::reply())
}
pub fn route_server_status( pub fn route_server_status(
core: Core, core: Core,
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone { ) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
@ -44,7 +55,6 @@ pub fn route_set_bg_image(
.with(cors::<HeaderName, Method>(vec![Method::PUT], vec![])) .with(cors::<HeaderName, Method>(vec![Method::PUT], vec![]))
} }
pub fn route_image( pub fn route_image(
core: Core, core: Core,
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone { ) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
@ -76,7 +86,6 @@ pub fn route_register_client(
}) })
} }
pub fn route_unregister_client( pub fn route_unregister_client(
core: Core, core: Core,
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone { ) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
@ -111,7 +120,7 @@ pub fn route_get_charsheet(
}) })
} }
pub fn route_check_password( pub fn route_authenticate(
core: Core, core: Core,
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone { ) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
warp::path!("api" / "v1" / "auth") warp::path!("api" / "v1" / "auth")
@ -119,7 +128,7 @@ pub fn route_check_password(
.and(warp::body::json()) .and(warp::body::json())
.then({ .then({
let core = core.clone(); let core = core.clone();
move |body| handle_auth(core.clone(), body) move |body| handle_check_password(core.clone(), body)
}) })
.with(cors::<HeaderName, Method>(vec![Method::PUT], vec![]))
} }

View File

@ -1,12 +1,12 @@
use warp::{ use warp::{
http::{header::CONTENT_TYPE, Method}, http::{header::CONTENT_TYPE, HeaderName, Method},
reply::Reply, reply::Reply,
Filter, Filter,
}; };
use crate::{ use crate::{
core::Core, core::Core,
handlers::{handle_get_users, handle_set_admin_password}, handlers::{handle_check_password, handle_get_users, handle_set_admin_password},
}; };
use super::cors; use super::cors;
@ -37,3 +37,71 @@ pub fn routes_user_management(
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone { ) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
route_get_users(core.clone()).or(route_set_admin_password(core.clone())) route_get_users(core.clone()).or(route_set_admin_password(core.clone()))
} }
pub fn route_check_password(
core: Core,
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
warp::path!("api" / "v1" / "auth")
.and(warp::put())
.and(warp::body::json())
.then({
let core = core.clone();
move |body| handle_check_password(core.clone(), body)
})
.with(cors::<HeaderName, Method>(vec![Method::PUT], vec![]))
}
#[cfg(test)]
mod test {
use std::{collections::HashMap, path::PathBuf};
use result_extended::ResultExt;
use crate::{asset_db::mocks::MemoryAssets, database::{Database, DbConn, UserId}};
use super::*;
async fn setup() -> Core {
let asset_store = MemoryAssets::new(vec![]);
let memory_file: Option<PathBuf> = None;
let db = DbConn::new(memory_file);
db.save_user(None, "admin", "", true, true).await.unwrap();
Core::new(asset_store, db)
}
#[tokio::test]
async fn handle_check_password_should_return_a_valid_token() {
let core = setup().await;
match core.list_users().await {
ResultExt::Ok(users) => println!("{:?}", users),
ResultExt::Err(err) => panic!("{}", err),
ResultExt::Fatal(err) => panic!("{}", err),
}
match core.user_by_username("admin").await {
ResultExt::Ok(Some(user)) => {
let _ = core.set_password(UserId::from(user.id), "aoeu".to_owned()).await;
},
ResultExt::Ok(None) => panic!("expected user wasn't found"),
ResultExt::Err(err) => panic!("{}", err),
ResultExt::Fatal(err) => panic!("{}", err),
}
let filter = route_check_password(core);
let params: HashMap<String, String> = vec![
("username".to_owned(), "admin".to_owned()),
("password".to_owned(), "aoeu".to_owned()),
]
.into_iter()
.collect();
let resp = warp::test::request()
.method("PUT")
.path("/api/v1/auth")
.json(&params)
.reply(&filter)
.await;
println!("response: {}", resp.status());
assert!(resp.status().is_success());
println!("resp.body(): {}", String::from_utf8(resp.body().to_vec()).unwrap());
serde_json::from_slice::<String>(resp.body()).unwrap();
}
}

View File

@ -47,10 +47,13 @@ where
.status(StatusCode::NOT_FOUND) .status(StatusCode::NOT_FOUND)
.body(vec![]) .body(vec![])
.unwrap(), .unwrap(),
ResultExt::Err(_) => Response::builder() ResultExt::Err(err) => {
.status(StatusCode::INTERNAL_SERVER_ERROR) println!("request error: {:?}", err);
.body(vec![]) Response::builder()
.unwrap(), .status(StatusCode::INTERNAL_SERVER_ERROR)
.body(vec![])
.unwrap()
}
ResultExt::Fatal(err) => { ResultExt::Fatal(err) => {
panic!("Shutting down with fatal error: {:?}", err); panic!("Shutting down with fatal error: {:?}", err);
} }
@ -262,35 +265,20 @@ pub async fn handle_set_admin_password(core: Core, password: String) -> impl Rep
pub struct AuthRequest { pub struct AuthRequest {
username: String, username: String,
password: String, password: String,
} }
pub async fn handle_auth(core: Core, auth_request: AuthRequest) -> impl Reply { pub async fn handle_check_password(core: Core, auth_request: AuthRequest) -> impl Reply {
handler(async move { handler(async move {
let userid = return_error!(core.auth(auth_request.username, auth_request.password).await); let session_id = return_error!(
core.auth(&auth_request.username, &auth_request.password)
.await
);
println!("handle_check_password: {:?}", session_id);
ok(Response::builder() ok(Response::builder()
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "*")
.header("Access-Control-Allow-Headers", "content-type")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(serde_json::to_vec(&userid).unwrap()) .body(serde_json::to_vec(&session_id).unwrap())
.unwrap()) .unwrap())
}).await })
} .await
#[cfg(test)]
mod test {
use std::path::PathBuf;
use crate::{asset_db::mocks::MemoryAssets, database::DbConn};
use super::*;
fn setup() -> Core {
let asset_store = MemoryAssets::new(vec![]);
let memory_file: Option<PathBuf> = None;
let db = DbConn::new(memory_file);
Core::new(asset_store, db)
}
} }

View File

@ -7,13 +7,7 @@ use std::{
use asset_db::{AssetId, FsAssets}; use asset_db::{AssetId, FsAssets};
use authdb::AuthError; use authdb::AuthError;
use database::DbConn; use database::DbConn;
use filters::{route_available_images, route_check_password, route_get_charsheet, route_image, route_register_client, route_server_status, route_set_bg_image, route_unregister_client, route_websocket, routes_user_management}; use filters::{route_authenticate, route_available_images, route_get_charsheet, route_healthcheck, route_image, route_register_client, route_server_status, route_set_bg_image, route_unregister_client, route_websocket, routes_user_management};
use handlers::{
handle_auth, handle_available_images, handle_connect_websocket, handle_file,
handle_get_charsheet, handle_get_users, handle_register_client, handle_server_status,
handle_set_admin_password, handle_set_background_image, handle_unregister_client,
RegisterRequest,
};
use warp::{ use warp::{
// header, // header,
filters::{method, path}, filters::{method, path},
@ -111,21 +105,13 @@ pub async fn main() {
let core = core::Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn); let core = core::Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
let unauthenticated_endpoints = route_healthcheck().or(route_authenticate(core.clone()));
let authenticated_endpoints = route_image(core.clone());
let filter = route_server_status(core.clone()) let server = warp::serve(unauthenticated_endpoints
.or(route_register_client(core.clone())) .or(authenticated_endpoints)
.or(route_unregister_client(core.clone())) .with(warp::log("visions"))
.or(route_websocket(core.clone())) .recover(handle_rejection));
.or(route_image(core.clone()))
.or(route_available_images(core.clone()))
.or(route_set_bg_image(core.clone()))
.or(routes_user_management(core.clone()))
.or(route_get_charsheet(core.clone()))
.or(route_check_password(core.clone()))
.with(warp::log("visions"))
.recover(handle_rejection);
let server = warp::serve(filter);
server server
.run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8001)) .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8001))
.await; .await;

View File

@ -7,20 +7,23 @@ use crate::{asset_db::AssetId, database::UserRow};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum FatalError { pub enum FatalError {
#[error("Non-unique database key {0}")]
NonUniqueDatabaseKey(String),
#[error("Database migrations failed {0}")]
DatabaseMigrationFailure(String),
#[error("Failed to construct a query")] #[error("Failed to construct a query")]
ConstructQueryFailure(String), ConstructQueryFailure(String),
#[error("Database connection lost")] #[error("Database connection lost")]
DatabaseConnectionLost, DatabaseConnectionLost,
#[error("Expected database key is missing")]
DatabaseKeyMissing,
#[error("Database migrations failed {0}")]
DatabaseMigrationFailure(String),
#[error("Unexpected response for message")] #[error("Unexpected response for message")]
MessageMismatch, MessageMismatch,
#[error("Non-unique database key {0}")]
NonUniqueDatabaseKey(String),
} }
impl result_extended::FatalError for FatalError {} impl result_extended::FatalError for FatalError {}