authenticate an endpoint for getting the user profile

This commit is contained in:
Savanni D'Gerinel 2024-12-31 14:09:21 -05:00
parent a0f1a0b81c
commit 822dfe2a13
7 changed files with 130 additions and 22 deletions

View File

@ -10,7 +10,6 @@ async-std = { version = "1.13.0" }
async-trait = { version = "0.1.83" } async-trait = { version = "0.1.83" }
authdb = { path = "../../authdb/" } authdb = { path = "../../authdb/" }
axum = { version = "0.7.9" } axum = { version = "0.7.9" }
axum-test = "16.4.1"
futures = { version = "0.3.31" } futures = { version = "0.3.31" }
include_dir = { version = "0.7.4" } include_dir = { version = "0.7.4" }
lazy_static = { version = "1.5.0" } lazy_static = { version = "1.5.0" }
@ -31,3 +30,4 @@ uuid = { version = "1.11.0", features = ["v4"] }
[dev-dependencies] [dev-dependencies]
cool_asserts = "2.0.3" cool_asserts = "2.0.3"
axum-test = "16.4.1"

View File

@ -192,7 +192,7 @@ impl Core {
id: CharacterId, id: CharacterId,
) -> ResultExt<Option<serde_json::Value>, AppError, FatalError> { ) -> ResultExt<Option<serde_json::Value>, AppError, FatalError> {
let mut state = self.0.write().await; let mut state = self.0.write().await;
let cr = state.db.character(id).await; let cr = state.db.character(&id).await;
match cr { match cr {
Ok(Some(row)) => ok(Some(row.data)), Ok(Some(row)) => ok(Some(row.data)),
Ok(None) => ok(None), Ok(None) => ok(None),
@ -258,13 +258,22 @@ impl Core {
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(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(session_id)
} }
Ok(_) => error(AppError::AuthFailed), Ok(_) => error(AppError::AuthFailed),
Err(err) => fatal(err), Err(err) => fatal(err),
} }
} }
pub async fn session(&self, session_id: &SessionId) -> ResultExt<Option<User>, 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)] #[cfg(test)]
@ -382,7 +391,7 @@ mod test {
match core.auth("admin", "aoeu").await { match core.auth("admin", "aoeu").await {
ResultExt::Ok(session_id) => { ResultExt::Ok(session_id) => {
let st = core.0.read().await; 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(Some(user_row)) => assert_eq!(user_row.name, "admin"),
Ok(None) => panic!("no matching user row for the session id"), Ok(None) => panic!("no matching user row for the session id"),
Err(err) => panic!("{}", err), Err(err) => panic!("{}", err),

View File

@ -58,11 +58,11 @@ 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 session(&self, id: &SessionId) -> Result<Option<UserRow>, FatalError>;
async fn create_session(&self, id: UserId) -> Result<SessionId, FatalError>; async fn create_session(&self, id: &UserId) -> Result<SessionId, FatalError>;
} }
pub struct DbConn { pub struct DbConn {
@ -144,16 +144,16 @@ impl Database for DbConn {
send_request!(self, Request::Games, DatabaseResponse::Games(lst) => Ok(lst)) send_request!(self, Request::Games, DatabaseResponse::Games(lst) => Ok(lst))
} }
async fn character(&mut self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError> { async fn character(&mut self, id: &CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
send_request!(self, Request::Charsheet(id), DatabaseResponse::Charsheet(row) => Ok(row)) send_request!(self, Request::Charsheet(id.to_owned()), DatabaseResponse::Charsheet(row) => Ok(row))
} }
async fn session(&self, id: SessionId) -> Result<Option<UserRow>, FatalError> { async fn session(&self, id: &SessionId) -> Result<Option<UserRow>, FatalError> {
send_request!(self, Request::Session(id), DatabaseResponse::Session(row) => Ok(row)) send_request!(self, Request::Session(id.to_owned()), DatabaseResponse::Session(row) => Ok(row))
} }
async fn create_session(&self, id: UserId) -> Result<SessionId, FatalError> { async fn create_session(&self, id: &UserId) -> Result<SessionId, FatalError> {
send_request!(self, Request::CreateSession(id), DatabaseResponse::CreateSession(session_id) => Ok(session_id)) 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<PathBuf> = None; let memory_db: Option<PathBuf> = None;
let mut conn = DbConn::new(memory_db); 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));
} }
} }

View File

@ -1,3 +1,5 @@
use std::fmt;
use rusqlite::types::{FromSql, FromSqlResult, ValueRef}; use rusqlite::types::{FromSql, FromSqlResult, ValueRef};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; 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)] #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct GameId(String); pub struct GameId(String);

View File

@ -1,18 +1,44 @@
use std::future::Future; use std::future::Future;
use axum::{
http::{HeaderMap, StatusCode},
Json,
};
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use result_extended::{error, ok, return_error, ResultExt}; use result_extended::{error, ok, return_error, ResultExt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typeshare::typeshare; use typeshare::typeshare;
use axum::{http::StatusCode, Json};
use crate::{ use crate::{
asset_db::AssetId, asset_db::AssetId,
core::Core, core::Core,
database::{CharacterId, UserId, SessionId}, database::{CharacterId, SessionId, UserId},
types::{AppError, FatalError}, types::{AppError, FatalError, User},
}; };
async fn check_session(
core: &Core,
headers: HeaderMap,
) -> ResultExt<Option<User>, AppError, FatalError> {
println!("headers: {:?}", headers);
println!("auth_header: {:?}", headers.get("Authorization"));
match headers.get("Authorization") {
Some(token) => {
match token
.to_str()
.unwrap()
.split(" ")
.collect::<Vec<&str>>()
.as_slice()
{
[_schema, token] => core.session(&SessionId::from(token.to_owned())).await,
_ => error(AppError::BadRequest),
}
}
None => ok(None),
}
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct HealthCheck { pub struct HealthCheck {
pub ok: bool, pub ok: bool,
@ -36,7 +62,10 @@ pub struct AuthRequest {
pub password: String, pub password: String,
} }
pub async fn check_password(core: Core, req: Json<AuthRequest>) -> (StatusCode, Json<Option<SessionId>>) { pub async fn check_password(
core: Core,
req: Json<AuthRequest>,
) -> (StatusCode, Json<Option<SessionId>>) {
let Json(AuthRequest { username, password }) = req; let Json(AuthRequest { username, password }) = req;
match core.auth(&username, &password).await { match core.auth(&username, &password).await {
ResultExt::Ok(session_id) => (StatusCode::OK, Json(Some(session_id))), ResultExt::Ok(session_id) => (StatusCode::OK, Json(Some(session_id))),
@ -45,6 +74,24 @@ pub async fn check_password(core: Core, req: Json<AuthRequest>) -> (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<Option<UserProfile>>) {
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( pub async fn handle_auth(
auth_ctx: &AuthDB, auth_ctx: &AuthDB,

View File

@ -1,11 +1,14 @@
use std::fmt;
use axum::{ use axum::{
http::{HeaderMap, StatusCode},
routing::{get, post}, routing::{get, post},
Json, Router, Json, Router,
}; };
use crate::{ use crate::{
core::Core, core::Core,
handlers::{check_password, healthcheck, AuthRequest}, handlers::{check_password, get_user, healthcheck, AuthRequest},
}; };
pub fn routes(core: Core) -> Router { pub fn routes(core: Core) -> Router {
@ -24,6 +27,14 @@ pub fn routes(core: Core) -> Router {
move |req: Json<AuthRequest>| check_password(core, req) move |req: Json<AuthRequest>| 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)] #[cfg(test)]
@ -40,6 +51,7 @@ mod test {
asset_db::FsAssets, asset_db::FsAssets,
core::Core, core::Core,
database::{Database, DbConn, SessionId, UserId}, database::{Database, DbConn, SessionId, UserId},
handlers::UserProfile,
}; };
fn setup_without_admin() -> (Core, TestServer) { fn setup_without_admin() -> (Core, TestServer) {
@ -120,6 +132,35 @@ mod test {
#[tokio::test] #[tokio::test]
async fn it_returns_user_profile() { 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<SessionId> = 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<UserProfile> = 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!(); unimplemented!();
} }
} }

View File

@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use typeshare::typeshare; use typeshare::typeshare;
use crate::{asset_db::AssetId, database::UserRow}; use crate::{asset_db::AssetId, database::{UserId, UserRow}};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum FatalError { pub enum FatalError {
@ -30,6 +30,9 @@ impl result_extended::FatalError for FatalError {}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum AppError { pub enum AppError {
#[error("invalid request")]
BadRequest,
#[error("something wasn't found {0}")] #[error("something wasn't found {0}")]
NotFound(String), NotFound(String),
@ -62,7 +65,7 @@ pub struct Rgb {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[typeshare] #[typeshare]
pub struct User { pub struct User {
pub id: String, pub id: UserId,
pub name: String, pub name: String,
pub password: String, pub password: String,
pub admin: bool, pub admin: bool,
@ -72,7 +75,7 @@ pub struct User {
impl From<UserRow> for User { impl From<UserRow> for User {
fn from(row: UserRow) -> Self { fn from(row: UserRow) -> Self {
Self { Self {
id: row.id.as_str().to_owned(), id: row.id,
name: row.name.to_owned(), name: row.name.to_owned(),
password: row.password.to_owned(), password: row.password.to_owned(),
admin: row.admin, admin: row.admin,