diff --git a/visions/server/migrations/01-initial-db/up.sql b/visions/server/migrations/01-initial-db/up.sql index 29463cd..8f2ad6b 100644 --- a/visions/server/migrations/01-initial-db/up.sql +++ b/visions/server/migrations/01-initial-db/up.sql @@ -35,3 +35,4 @@ CREATE TABLE roles( FOREIGN KEY(game_id) REFERENCES games(uuid) ); +INSERT INTO users VALUES ('admin', 'admin', '', true, true); diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index 03937de..2d72ac1 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -11,7 +11,7 @@ use uuid::Uuid; use crate::{ asset_db::{self, AssetId, Assets}, database::{CharacterId, Database, SessionId, UserId}, - types::{AppError, FatalError, Game, Message, Tabletop, User, Rgb}, + types::{AppError, FatalError, Game, Message, Rgb, Tabletop, User}, }; const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb { @@ -154,9 +154,7 @@ impl Core { asset_db::Error::Inaccessible => { AppError::Inaccessible(format!("{}", asset_id)) } - asset_db::Error::Unexpected(err) => { - AppError::Inaccessible(format!("{}", err)) - } + asset_db::Error::Unexpected(err) => AppError::Inaccessible(format!("{}", err)), }), ) } @@ -212,6 +210,25 @@ impl Core { }); } + pub async fn save_user( + &self, + uuid: Option, + username: &str, + password: &str, + admin: bool, + enabled: bool, + ) -> ResultExt { + let state = self.0.read().await; + match state + .db + .save_user(uuid, username, password, admin, enabled) + .await + { + Ok(uuid) => ok(uuid), + Err(err) => fatal(err), + } + } + pub async fn set_password( &self, uuid: UserId, @@ -290,7 +307,7 @@ mod test { ]); let memory_db: Option = None; let conn = DbConn::new(memory_db); - conn.save_user(None, "admin", "aoeu", true, true) + conn.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true) .await .unwrap(); conn.save_user(None, "gm_1", "aoeu", false, true) diff --git a/visions/server/src/database/disk_db.rs b/visions/server/src/database/disk_db.rs index 5b41a28..bc8dd6c 100644 --- a/visions/server/src/database/disk_db.rs +++ b/visions/server/src/database/disk_db.rs @@ -100,7 +100,7 @@ impl DiskDb { ) -> Result { match user_id { None => { - let user_id = UserId::new(); + let user_id = UserId::default(); let mut stmt = self .conn .prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)") diff --git a/visions/server/src/database/mod.rs b/visions/server/src/database/mod.rs index 3743d54..8352e88 100644 --- a/visions/server/src/database/mod.rs +++ b/visions/server/src/database/mod.rs @@ -173,7 +173,7 @@ mod test { let no_path: Option = None; let db = DiskDb::new(no_path).unwrap(); - db.save_user(None, "admin", "abcdefg", true, true).unwrap(); + db.save_user(Some(UserId::from("admin")), "admin", "abcdefg", true, true).unwrap(); let game_id = db.save_game(None, "Candela").unwrap(); (db, game_id) } diff --git a/visions/server/src/database/types.rs b/visions/server/src/database/types.rs index 55b3490..65d2e35 100644 --- a/visions/server/src/database/types.rs +++ b/visions/server/src/database/types.rs @@ -6,15 +6,17 @@ use uuid::Uuid; pub struct UserId(String); impl UserId { - pub fn new() -> Self { - Self(format!("{}", Uuid::new_v4().hyphenated())) - } - pub fn as_str(&self) -> &str { &self.0 } } +impl Default for UserId { + fn default() -> Self { + Self(format!("{}", Uuid::new_v4().hyphenated())) + } +} + impl From<&str> for UserId { fn from(s: &str) -> Self { Self(s.to_owned()) diff --git a/visions/server/src/handlers/mod.rs b/visions/server/src/handlers/mod.rs index 6d33a58..b6d05e5 100644 --- a/visions/server/src/handlers/mod.rs +++ b/visions/server/src/handlers/mod.rs @@ -32,8 +32,8 @@ pub async fn healthcheck(core: Core) -> Vec { #[derive(Clone, Debug, Deserialize, Serialize)] #[typeshare] pub struct AuthRequest { - username: String, - password: String, + pub username: String, + pub password: String, } pub async fn check_password(core: Core, req: Json) -> (StatusCode, Json>) { diff --git a/visions/server/src/routes.rs b/visions/server/src/routes.rs index c644c58..d99e229 100644 --- a/visions/server/src/routes.rs +++ b/visions/server/src/routes.rs @@ -1,4 +1,7 @@ -use axum::{routing::{get, post}, Json, Router}; +use axum::{ + routing::{get, post}, + Json, Router, +}; use crate::{ core::Core, @@ -27,12 +30,19 @@ pub fn routes(core: Core) -> Router { mod test { use std::path::PathBuf; + use axum::http::StatusCode; use axum_test::TestServer; + use cool_asserts::assert_matches; + use result_extended::ResultExt; - use crate::{asset_db::FsAssets, core::Core, database::DbConn}; use super::*; + use crate::{ + asset_db::FsAssets, + core::Core, + database::{Database, DbConn, SessionId, UserId}, + }; - fn setup() -> (Core, TestServer) { + fn setup_without_admin() -> (Core, TestServer) { let memory_db: Option = None; let conn = DbConn::new(memory_db); let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn); @@ -41,14 +51,75 @@ mod test { (core, server) } + async fn setup_admin_enabled() -> (Core, TestServer) { + let memory_db: Option = None; + let conn = DbConn::new(memory_db); + conn.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true) + .await + .unwrap(); + let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn); + let app = routes(core.clone()); + let server = TestServer::new(app).unwrap(); + (core, server) + } + #[tokio::test] async fn it_returns_a_healthcheck() { - let (_core, server) = setup(); + let (core, server) = setup_without_admin(); + let response = server.get("/api/v1/health").await; response.assert_status_ok(); - let b: crate::handlers::HealthCheck = response.json(); - assert_eq!(b, crate::handlers::HealthCheck { ok: false }); + + assert_matches!( + core.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true) + .await, + ResultExt::Ok(_) + ); + + let response = server.get("/api/v1/health").await; + response.assert_status_ok(); + let b: crate::handlers::HealthCheck = response.json(); + assert_eq!(b, crate::handlers::HealthCheck { ok: true }); + } + + #[tokio::test] + async fn it_authenticates_a_user() { + let (_core, server) = setup_admin_enabled().await; + + let response = server + .post("/api/v1/auth") + .json(&AuthRequest { + username: "admin".to_owned(), + password: "wrong".to_owned(), + }) + .await; + response.assert_status(StatusCode::UNAUTHORIZED); + + let response = server + .post("/api/v1/auth") + .json(&AuthRequest { + username: "unknown".to_owned(), + password: "wrong".to_owned(), + }) + .await; + response.assert_status(StatusCode::UNAUTHORIZED); + + let response = server + .post("/api/v1/auth") + .json(&AuthRequest { + username: "admin".to_owned(), + password: "aoeu".to_owned(), + }) + .await; + response.assert_status_ok(); + let session_id: Option = response.json(); + assert!(session_id.is_some()); + } + + #[tokio::test] + async fn it_returns_user_profile() { + unimplemented!(); } }