diff --git a/visions/server/Cargo.toml b/visions/server/Cargo.toml index 8d1d27e..1e9f78d 100644 --- a/visions/server/Cargo.toml +++ b/visions/server/Cargo.toml @@ -10,7 +10,6 @@ async-std = { version = "1.13.0" } async-trait = { version = "0.1.83" } authdb = { path = "../../authdb/" } axum = { version = "0.7.9" } -axum-test = "16.4.1" futures = { version = "0.3.31" } include_dir = { version = "0.7.4" } lazy_static = { version = "1.5.0" } @@ -31,3 +30,4 @@ uuid = { version = "1.11.0", features = ["v4"] } [dev-dependencies] cool_asserts = "2.0.3" +axum-test = "16.4.1" diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index 2d72ac1..8cecb45 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -192,7 +192,7 @@ impl Core { id: CharacterId, ) -> ResultExt, AppError, FatalError> { let mut state = self.0.write().await; - let cr = state.db.character(id).await; + let cr = state.db.character(&id).await; match cr { Ok(Some(row)) => ok(Some(row.data)), Ok(None) => ok(None), @@ -258,13 +258,22 @@ impl Core { let state = self.0.write().await; match state.db.user_by_username(username).await { Ok(Some(row)) if (row.password == password) => { - let session_id = state.db.create_session(row.id).await.unwrap(); + let session_id = state.db.create_session(&row.id).await.unwrap(); ok(session_id) } Ok(_) => error(AppError::AuthFailed), Err(err) => fatal(err), } } + + pub async fn session(&self, session_id: &SessionId) -> ResultExt, AppError, FatalError> { + let state = self.0.read().await; + match state.db.session(session_id).await { + Ok(Some(user_row)) => ok(Some(User::from(user_row))), + Ok(None) => ok(None), + Err(fatal_error) => fatal(fatal_error), + } + } } #[cfg(test)] @@ -382,7 +391,7 @@ mod test { match core.auth("admin", "aoeu").await { ResultExt::Ok(session_id) => { let st = core.0.read().await; - match st.db.session(session_id).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), diff --git a/visions/server/src/database/mod.rs b/visions/server/src/database/mod.rs index 8352e88..39211ce 100644 --- a/visions/server/src/database/mod.rs +++ b/visions/server/src/database/mod.rs @@ -58,11 +58,11 @@ pub trait Database: Send + Sync { async fn games(&mut self) -> Result, FatalError>; - async fn character(&mut self, id: CharacterId) -> Result, FatalError>; + async fn character(&mut self, id: &CharacterId) -> Result, FatalError>; - async fn session(&self, id: SessionId) -> Result, FatalError>; + async fn session(&self, id: &SessionId) -> Result, FatalError>; - async fn create_session(&self, id: UserId) -> Result; + async fn create_session(&self, id: &UserId) -> Result; } pub struct DbConn { @@ -144,16 +144,16 @@ impl Database for DbConn { send_request!(self, Request::Games, DatabaseResponse::Games(lst) => Ok(lst)) } - async fn character(&mut self, id: CharacterId) -> Result, FatalError> { - send_request!(self, Request::Charsheet(id), DatabaseResponse::Charsheet(row) => Ok(row)) + async fn character(&mut self, id: &CharacterId) -> Result, FatalError> { + send_request!(self, Request::Charsheet(id.to_owned()), DatabaseResponse::Charsheet(row) => Ok(row)) } - async fn session(&self, id: SessionId) -> Result, FatalError> { - send_request!(self, Request::Session(id), DatabaseResponse::Session(row) => Ok(row)) + async fn session(&self, id: &SessionId) -> Result, FatalError> { + send_request!(self, Request::Session(id.to_owned()), DatabaseResponse::Session(row) => Ok(row)) } - async fn create_session(&self, id: UserId) -> Result { - send_request!(self, Request::CreateSession(id), DatabaseResponse::CreateSession(session_id) => Ok(session_id)) + async fn create_session(&self, id: &UserId) -> Result { + send_request!(self, Request::CreateSession(id.to_owned()), DatabaseResponse::CreateSession(session_id) => Ok(session_id)) } } @@ -194,6 +194,6 @@ mod test { let memory_db: Option = None; let mut conn = DbConn::new(memory_db); - assert_matches!(conn.character(CharacterId::from("1")).await, Ok(None)); + assert_matches!(conn.character(&CharacterId::from("1")).await, Ok(None)); } } diff --git a/visions/server/src/database/types.rs b/visions/server/src/database/types.rs index 65d2e35..9bb9283 100644 --- a/visions/server/src/database/types.rs +++ b/visions/server/src/database/types.rs @@ -1,3 +1,5 @@ +use std::fmt; + use rusqlite::types::{FromSql, FromSqlResult, ValueRef}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -72,6 +74,12 @@ impl FromSql for SessionId { } } +impl fmt::Display for SessionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str(&self.0) + } +} + #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct GameId(String); diff --git a/visions/server/src/handlers/mod.rs b/visions/server/src/handlers/mod.rs index b6d05e5..c742b9d 100644 --- a/visions/server/src/handlers/mod.rs +++ b/visions/server/src/handlers/mod.rs @@ -1,18 +1,44 @@ use std::future::Future; +use axum::{ + http::{HeaderMap, StatusCode}, + Json, +}; use futures::{SinkExt, StreamExt}; use result_extended::{error, ok, return_error, ResultExt}; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use axum::{http::StatusCode, Json}; use crate::{ asset_db::AssetId, core::Core, - database::{CharacterId, UserId, SessionId}, - types::{AppError, FatalError}, + database::{CharacterId, SessionId, UserId}, + types::{AppError, FatalError, User}, }; +async fn check_session( + core: &Core, + headers: HeaderMap, +) -> ResultExt, AppError, FatalError> { + println!("headers: {:?}", headers); + println!("auth_header: {:?}", headers.get("Authorization")); + match headers.get("Authorization") { + Some(token) => { + match token + .to_str() + .unwrap() + .split(" ") + .collect::>() + .as_slice() + { + [_schema, token] => core.session(&SessionId::from(token.to_owned())).await, + _ => error(AppError::BadRequest), + } + } + None => ok(None), + } +} + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct HealthCheck { pub ok: bool, @@ -36,7 +62,10 @@ pub struct AuthRequest { pub password: String, } -pub async fn check_password(core: Core, req: Json) -> (StatusCode, Json>) { +pub async fn check_password( + core: Core, + req: Json, +) -> (StatusCode, Json>) { let Json(AuthRequest { username, password }) = req; match core.auth(&username, &password).await { ResultExt::Ok(session_id) => (StatusCode::OK, Json(Some(session_id))), @@ -45,6 +74,24 @@ pub async fn check_password(core: Core, req: Json) -> (StatusCode, } } +#[derive(Deserialize, Serialize)] +#[typeshare] +pub struct UserProfile { + pub userid: UserId, + pub username: String, +} + +pub async fn get_user(core: Core, headers: HeaderMap) -> (StatusCode, Json>) { + match check_session(&core, headers).await { + ResultExt::Ok(Some(user)) => { + (StatusCode::OK, Json(Some(UserProfile{userid: UserId::from(user.id), username: user.name} ))) + } + ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)), + ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), + ResultExt::Fatal(err) => panic!("{}", err), + } +} + /* pub async fn handle_auth( auth_ctx: &AuthDB, diff --git a/visions/server/src/routes.rs b/visions/server/src/routes.rs index d99e229..2337e19 100644 --- a/visions/server/src/routes.rs +++ b/visions/server/src/routes.rs @@ -1,11 +1,14 @@ +use std::fmt; + use axum::{ + http::{HeaderMap, StatusCode}, routing::{get, post}, Json, Router, }; use crate::{ core::Core, - handlers::{check_password, healthcheck, AuthRequest}, + handlers::{check_password, get_user, healthcheck, AuthRequest}, }; pub fn routes(core: Core) -> Router { @@ -24,6 +27,14 @@ pub fn routes(core: Core) -> Router { move |req: Json| check_password(core, req) }), ) + .route( + // By default, just get the self user. + "/api/v1/user", + get({ + let core = core.clone(); + move |headers: HeaderMap| get_user(core, headers) + }) + ) } #[cfg(test)] @@ -40,6 +51,7 @@ mod test { asset_db::FsAssets, core::Core, database::{Database, DbConn, SessionId, UserId}, + handlers::UserProfile, }; fn setup_without_admin() -> (Core, TestServer) { @@ -120,6 +132,35 @@ mod test { #[tokio::test] async fn it_returns_user_profile() { + let (_core, server) = setup_admin_enabled().await; + + let response = server.get("/api/v1/user").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(); + let session_id = session_id.unwrap(); + + let response = server + .get("/api/v1/user") + .add_header("Authorization", format!("Bearer {}", session_id)) + .await; + response.assert_status_ok(); + let profile: Option = response.json(); + let profile = profile.unwrap(); + assert_eq!(profile.userid, UserId::from("admin")); + assert_eq!(profile.username, "admin"); + } + + #[tokio::test] + async fn an_admin_can_create_a_user() { unimplemented!(); } } diff --git a/visions/server/src/types.rs b/visions/server/src/types.rs index 7ea62b4..6e8fc4c 100644 --- a/visions/server/src/types.rs +++ b/visions/server/src/types.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use typeshare::typeshare; -use crate::{asset_db::AssetId, database::UserRow}; +use crate::{asset_db::AssetId, database::{UserId, UserRow}}; #[derive(Debug, Error)] pub enum FatalError { @@ -30,6 +30,9 @@ impl result_extended::FatalError for FatalError {} #[derive(Debug, Error)] pub enum AppError { + #[error("invalid request")] + BadRequest, + #[error("something wasn't found {0}")] NotFound(String), @@ -62,7 +65,7 @@ pub struct Rgb { #[serde(rename_all = "camelCase")] #[typeshare] pub struct User { - pub id: String, + pub id: UserId, pub name: String, pub password: String, pub admin: bool, @@ -72,7 +75,7 @@ pub struct User { impl From for User { fn from(row: UserRow) -> Self { Self { - id: row.id.as_str().to_owned(), + id: row.id, name: row.name.to_owned(), password: row.password.to_owned(), admin: row.admin,