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" }
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"

View File

@ -192,7 +192,7 @@ impl Core {
id: CharacterId,
) -> ResultExt<Option<serde_json::Value>, 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<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)]
@ -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),

View File

@ -58,11 +58,11 @@ pub trait Database: Send + Sync {
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 {
@ -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<Option<CharsheetRow>, FatalError> {
send_request!(self, Request::Charsheet(id), DatabaseResponse::Charsheet(row) => Ok(row))
async fn character(&mut self, id: &CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
send_request!(self, Request::Charsheet(id.to_owned()), DatabaseResponse::Charsheet(row) => Ok(row))
}
async fn session(&self, id: SessionId) -> Result<Option<UserRow>, FatalError> {
send_request!(self, Request::Session(id), DatabaseResponse::Session(row) => Ok(row))
async fn session(&self, id: &SessionId) -> Result<Option<UserRow>, FatalError> {
send_request!(self, Request::Session(id.to_owned()), DatabaseResponse::Session(row) => Ok(row))
}
async fn create_session(&self, id: UserId) -> Result<SessionId, FatalError> {
send_request!(self, Request::CreateSession(id), DatabaseResponse::CreateSession(session_id) => Ok(session_id))
async fn create_session(&self, id: &UserId) -> Result<SessionId, FatalError> {
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 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 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);

View File

@ -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<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)]
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<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;
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<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(
auth_ctx: &AuthDB,

View File

@ -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<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)]
@ -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<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!();
}
}

View File

@ -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<UserRow> 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,