use crate::extract::{Extractor, SESSION_COOKIE};
use crate::RouterApi;
use axum::body::Body;
use axum::extract::Query;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::{Extension, Json, Router};
use axum_extra::extract::cookie::Cookie;
use axum_extra::extract::CookieJar;
use cookie::time::OffsetDateTime;
use cookie::Expiration;
use etwin_core::auth::{AuthContext, RawUserCredentials};
use etwin_core::dinoparc::DinoparcCredentials;
use etwin_core::hammerfest::HammerfestCredentials;
use etwin_core::twinoid::TwinoidCredentials;
use etwin_core::types::AnyError;
use etwin_core::user::User;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
use thiserror::Error;
pub fn router() -> Router<(), Body> {
Router::new().route("/self", get(get_self).put(set_self).delete(delete_self))
}
async fn get_self(acx: Extractor<AuthContext>) -> Json<AuthContext> {
Json(acx.value())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
struct SetSelfQuery {
method: Option<SetSelfMethod>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
enum SetSelfMethod {
Dinoparc,
Etwin,
Hammerfest,
Twinoid,
}
#[derive(Debug, Error)]
enum SetSelfError {
#[error("bad request")]
BadRequest(#[from] serde_json::Error),
#[error("internal error")]
Internal(#[from] AnyError),
}
impl IntoResponse for SetSelfError {
fn into_response(self) -> Response {
let status = match &self {
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(json!({ "error": self.to_string() }))).into_response()
}
}
async fn set_self(
Extension(api): Extension<RouterApi>,
Query(query): Query<SetSelfQuery>,
jar: CookieJar,
Json(body): Json<JsonValue>,
) -> Result<(CookieJar, Json<User>), SetSelfError> {
let user_and_session = match query.method {
Some(SetSelfMethod::Dinoparc) => {
let credentials: DinoparcCredentials = serde_json::from_value(body)?;
api.auth.register_or_login_with_dinoparc(&credentials).await?
}
None | Some(SetSelfMethod::Etwin) => {
let credentials: RawUserCredentials = serde_json::from_value(body)?;
api.auth.raw_login_with_credentials(&credentials).await?
}
Some(SetSelfMethod::Hammerfest) => {
let credentials: HammerfestCredentials = serde_json::from_value(body)?;
api.auth.register_or_login_with_hammerfest(&credentials).await?
}
Some(SetSelfMethod::Twinoid) => {
let credentials: TwinoidCredentials = serde_json::from_value(body)?;
api.auth.register_or_login_with_twinoid(&credentials).await?
}
};
let cookie = Cookie::build(SESSION_COOKIE, user_and_session.session.id.to_string())
.path("/")
.http_only(true)
.finish();
Ok((jar.add(cookie), Json(user_and_session.user)))
}
async fn delete_self(jar: CookieJar) -> (CookieJar, Json<AuthContext>) {
let cookie = Cookie::build(SESSION_COOKIE, "".to_string())
.path("/")
.http_only(true)
.expires(Expiration::DateTime(OffsetDateTime::UNIX_EPOCH))
.finish();
(jar.add(cookie), Json(AuthContext::guest()))
}
#[cfg(test)]
mod tests {
use crate::app;
use crate::clock::RestClock;
use crate::dev::hammerfest_client::UpsertHammerfestUserBody;
use crate::extract::AuthLogger;
use crate::test::{create_api, RouterExt};
use axum::http::StatusCode;
use axum::Extension;
use etwin_core::auth::{AuthContext, RawUserCredentials, RegisterWithUsernameOptions};
use etwin_core::core::Instant;
use etwin_core::hammerfest::{HammerfestCredentials, HammerfestServer};
use etwin_core::link::VersionedLinks;
use etwin_core::password::Password;
use etwin_core::user::{User, UserDisplayNameVersion, UserDisplayNameVersions};
use etwin_log::NoopLogger;
use std::sync::Arc;
#[tokio::test]
async fn test_register_a_user_and_authenticate_back() {
let api = create_api();
let logger = AuthLogger(Arc::new(NoopLogger));
let router = app(api).layer(Extension(logger));
let mut client = router.client();
{
let req = axum::http::Request::builder()
.method("PUT")
.uri("/api/v1/clock")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(
serde_json::to_string(&RestClock {
time: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
})
.unwrap(),
))
.unwrap();
let res = client.send(req).await;
let actual_status = res.status();
assert_eq!(actual_status, StatusCode::OK);
}
let alice = {
let req = axum::http::Request::builder()
.method("POST")
.uri("/api/v1/users")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(
serde_json::to_string(&RegisterWithUsernameOptions {
username: "alice".parse().unwrap(),
display_name: "Alice".parse().unwrap(),
password: Password::from("aaaaaaaaaa"),
})
.unwrap(),
))
.unwrap();
let res = client.send(req).await;
assert_eq!(res.status(), StatusCode::OK);
let body = hyper::body::to_bytes(res.into_body())
.await
.expect("read body")
.to_vec();
let body: &str = std::str::from_utf8(body.as_slice()).unwrap();
serde_json::from_str::<User>(body).unwrap()
};
let mut client = router.client();
{
let req = axum::http::Request::builder()
.method("GET")
.uri("/api/v1/auth/self")
.header("Content-Type", "application/json")
.body(axum::body::Body::empty())
.unwrap();
let res = client.send(req).await;
assert_eq!(res.status(), StatusCode::OK);
let body = hyper::body::to_bytes(res.into_body())
.await
.expect("read body")
.to_vec();
let body: &str = std::str::from_utf8(body.as_slice()).unwrap();
let actual: AuthContext = serde_json::from_str(body).unwrap();
let expected = AuthContext::guest();
assert_eq!(actual, expected);
}
{
let req = axum::http::Request::builder()
.method("PUT")
.uri("/api/v1/auth/self?method=Etwin")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(
serde_json::to_string(&RawUserCredentials {
login: "alice".to_string(),
password: b"aaaaaaaaaa".as_slice().into(),
})
.unwrap(),
))
.unwrap();
let res = client.send(req).await;
assert_eq!(res.status(), StatusCode::OK);
let body = hyper::body::to_bytes(res.into_body())
.await
.expect("read body")
.to_vec();
let body: &str = std::str::from_utf8(body.as_slice()).unwrap();
let actual: User = serde_json::from_str(body).unwrap();
let expected = User {
id: alice.id,
created_at: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
display_name: UserDisplayNameVersions {
current: UserDisplayNameVersion {
value: "Alice".parse().unwrap(),
},
},
is_administrator: true,
links: VersionedLinks::default(),
};
assert_eq!(actual, expected);
}
}
#[tokio::test]
async fn test_register_with_hammerfest() {
let api = create_api();
let logger = AuthLogger(Arc::new(NoopLogger));
let router = app(api).layer(Extension(logger));
let mut client = router.client();
{
let req = axum::http::Request::builder()
.method("PUT")
.uri("/api/v1/hammerfest_client/users")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(
serde_json::to_string(&UpsertHammerfestUserBody {
server: HammerfestServer::HammerfestFr,
id: "123".parse().unwrap(),
username: "alicehf".parse().unwrap(),
password: "alicehf".parse().unwrap(),
})
.unwrap(),
))
.unwrap();
let res = client.send(req).await;
let actual_status = res.status();
assert_eq!(actual_status, StatusCode::OK);
}
{
let req = axum::http::Request::builder()
.method("PUT")
.uri("/api/v1/clock")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(
serde_json::to_string(&RestClock {
time: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
})
.unwrap(),
))
.unwrap();
let res = client.send(req).await;
let actual_status = res.status();
assert_eq!(actual_status, StatusCode::OK);
}
let mut client = router.client();
{
let req = axum::http::Request::builder()
.method("PUT")
.uri("/api/v1/auth/self?method=Hammerfest")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(
serde_json::to_string(&HammerfestCredentials {
server: HammerfestServer::HammerfestFr,
username: "alicehf".parse().unwrap(),
password: "alicehf".parse().unwrap(),
})
.unwrap(),
))
.unwrap();
let res = client.send(req).await;
assert_eq!(res.status(), StatusCode::OK);
let body = hyper::body::to_bytes(res.into_body())
.await
.expect("read body")
.to_vec();
let body: &str = std::str::from_utf8(body.as_slice()).unwrap();
let actual: User = serde_json::from_str(body).unwrap();
let expected = User {
id: actual.id,
created_at: Instant::ymd_hms(2021, 1, 1, 0, 0, 0),
display_name: UserDisplayNameVersions {
current: UserDisplayNameVersion {
value: "alicehf".parse().unwrap(),
},
},
is_administrator: true,
links: VersionedLinks {
dinoparc_com: Default::default(),
en_dinoparc_com: Default::default(),
hammerfest_es: Default::default(),
hammerfest_fr: actual.links.hammerfest_fr.clone(),
hfest_net: Default::default(),
sp_dinoparc_com: Default::default(),
twinoid: Default::default(),
},
};
assert_eq!(actual, expected);
}
}
}