Add session creation and lookup
This commit is contained in:
parent
1d400ce38b
commit
d5f4b7cfa5
@ -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);
|
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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![]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(¶ms)
|
||||||
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 {}
|
||||||
|
Loading…
Reference in New Issue
Block a user