use std::net::SocketAddr;
use std::string::String;
use axum08::extract::rejection::JsonRejection;
use axum08::extract::{ConnectInfo, FromRef, FromRequest, FromRequestParts, Path, Request, State};
use axum08::http::request::Parts;
use axum08::http::{HeaderMap, StatusCode, header};
use axum08::response::{IntoResponse, Response};
use axum08::routing::{get, post};
use axum08::{Json, Router};
use serde::de::DeserializeOwned;
use super::{Admin, SignedIn};
use crate::Auth;
use crate::core::{Caller, Error, SessionCookie};
#[cfg(doc)]
use crate::types::{AuthConfig, CreatedUser, Link, RegisterInfo, UserEntry, UserInfo};
use crate::types::{
AuthError, AuthRequest, Invalid, LoginRequest, NewUser, PasswordChange, RegisterRequest,
UserChange, UserId,
};
pub fn routes<S>() -> Router<S>
where
Auth: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
let router = Router::new()
.route("/api/auth/config", get(config))
.route("/api/auth/me", get(me))
.route("/api/auth/login", post(login))
.route("/api/auth/logout", post(logout))
.route(
"/api/auth/register/{token}",
get(register_info).post(register),
)
.route("/api/auth/account", axum08::routing::patch(update_account))
.route("/api/auth/account/password", post(change_password))
.route("/api/auth/users", get(users).post(create_user))
.route(
"/api/auth/users/{id}",
axum08::routing::patch(update_user).delete(delete_user),
)
.route(
"/api/auth/users/{id}/link",
post(issue_link).delete(revoke_link),
)
.route("/api/auth/users/{id}/sign-out", post(sign_out_everywhere))
.route("/api/auth/users/{id}/revoke", post(revoke_access))
.route("/api/auth/users/{id}/impersonate", post(impersonate))
.route("/api/auth/impersonate/stop", post(stop_impersonating));
#[cfg(feature = "cloudflare")]
let router = router.route(
"/api/auth/cloudflare",
post(cloudflare)
.get(cloudflare_settings)
.put(set_cloudflare_settings),
);
#[cfg(feature = "google")]
let router = router.merge(super::google::routes());
router
}
struct Body<T>(T);
impl<T, S> FromRequest<S> for Body<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = Error;
async fn from_request(request: Request, state: &S) -> Result<Self, Self::Rejection> {
match Json::<T>::from_request(request, state).await {
Ok(Json(value)) => Ok(Body(value)),
Err(rejection) => Err(invalid_body(rejection).into()),
}
}
}
fn invalid_body(rejection: JsonRejection) -> AuthError {
tracing::debug!("auth: malformed request: {}", rejection.body_text());
AuthError::Invalid(Invalid::MalformedRequest)
}
struct Id(UserId);
impl<S> FromRequestParts<S> for Id
where
S: Send + Sync,
{
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
match Path::<u64>::from_request_parts(parts, state).await {
Ok(Path(id)) => Ok(Id(UserId(id))),
Err(_) => Err(AuthError::NotFound.into()),
}
}
}
pub(super) struct Token(pub(super) String);
impl<S> FromRequestParts<S> for Token
where
S: Send + Sync,
{
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
match Path::<String>::from_request_parts(parts, state).await {
Ok(Path(token)) => Ok(Token(token)),
Err(_) => Err(AuthError::NotFound.into()),
}
}
}
struct Peer(Option<SocketAddr>);
impl<S> FromRequestParts<S> for Peer
where
S: Send + Sync,
{
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let peer = ConnectInfo::<SocketAddr>::from_request_parts(parts, state)
.await
.ok()
.map(|ConnectInfo(addr)| addr);
Ok(Peer(peer))
}
}
async fn answer(auth: &Auth, caller: Caller<'_>, request: AuthRequest) -> Result<Response, Error> {
let dispatched = auth.dispatch(&caller, request).await?;
let mut response = dispatched.response.into_response();
if let Some(cookie) = dispatched.cookie {
let empty = HeaderMap::new();
let headers = caller.http_headers().unwrap_or(&empty);
let value = match cookie {
SessionCookie::Set(session) => auth.set_cookie(headers, &session.id),
SessionCookie::Clear => auth.clear_cookie(headers),
};
response.headers_mut().insert(header::SET_COOKIE, value);
}
Ok(response)
}
fn signed_in(SignedIn { user, session }: &SignedIn) -> Caller<'_> {
Caller::new().signed_in(user, session)
}
async fn config(State(auth): State<Auth>, headers: HeaderMap) -> Result<Response, Error> {
let caller = Caller::new().headers(&headers);
answer(&auth, caller, AuthRequest::Config).await
}
#[cfg(feature = "cloudflare")]
async fn cloudflare(State(auth): State<Auth>, headers: HeaderMap) -> Result<Response, Error> {
let caller = Caller::new().headers(&headers);
answer(&auth, caller, AuthRequest::CloudflareSignIn).await
}
#[cfg(feature = "cloudflare")]
async fn cloudflare_settings(
State(auth): State<Auth>,
Admin(admin): Admin,
) -> Result<Response, Error> {
answer(&auth, signed_in(&admin), AuthRequest::Cloudflare).await
}
#[cfg(feature = "cloudflare")]
async fn set_cloudflare_settings(
State(auth): State<Auth>,
Admin(admin): Admin,
Body(config): Body<crate::types::CloudflareConfig>,
) -> Result<Response, Error> {
answer(&auth, signed_in(&admin), AuthRequest::SetCloudflare(config)).await
}
async fn me(State(auth): State<Auth>, user: SignedIn) -> Result<Response, Error> {
answer(&auth, signed_in(&user), AuthRequest::Me).await
}
async fn login(
State(auth): State<Auth>,
Peer(peer): Peer,
headers: HeaderMap,
Body(request): Body<LoginRequest>,
) -> Result<Response, Error> {
let Some(peer) = peer else {
tracing::error!(
"auth: sign-in needs the client's address; serve the app with \
`into_make_service_with_connect_info::<SocketAddr>()`"
);
return Ok((StatusCode::INTERNAL_SERVER_ERROR, "internal error").into_response());
};
let address = auth.client_address(peer.ip(), &headers);
let caller = Caller::new().headers(&headers).address(address);
answer(&auth, caller, AuthRequest::Login(request)).await
}
async fn logout(State(auth): State<Auth>, headers: HeaderMap) -> Result<Response, Error> {
let caller = Caller::new().headers(&headers);
answer(&auth, caller, AuthRequest::Logout).await
}
async fn register_info(State(auth): State<Auth>, Token(token): Token) -> Result<Response, Error> {
answer(&auth, Caller::new(), AuthRequest::RegisterInfo { token }).await
}
async fn register(
State(auth): State<Auth>,
Token(token): Token,
headers: HeaderMap,
Body(request): Body<RegisterRequest>,
) -> Result<Response, Error> {
let caller = Caller::new().headers(&headers);
answer(&auth, caller, AuthRequest::Register { token, request }).await
}
async fn change_password(
State(auth): State<Auth>,
user: SignedIn,
Body(change): Body<PasswordChange>,
) -> Result<Response, Error> {
answer(&auth, signed_in(&user), AuthRequest::ChangePassword(change)).await
}
async fn update_account(
State(auth): State<Auth>,
user: SignedIn,
Body(change): Body<UserChange>,
) -> Result<Response, Error> {
answer(&auth, signed_in(&user), AuthRequest::UpdateAccount(change)).await
}
async fn users(State(auth): State<Auth>, Admin(admin): Admin) -> Result<Response, Error> {
answer(&auth, signed_in(&admin), AuthRequest::Users).await
}
async fn create_user(
State(auth): State<Auth>,
Admin(admin): Admin,
Body(new): Body<NewUser>,
) -> Result<Response, Error> {
answer(&auth, signed_in(&admin), AuthRequest::CreateUser(new)).await
}
async fn update_user(
State(auth): State<Auth>,
Admin(admin): Admin,
Id(id): Id,
Body(change): Body<UserChange>,
) -> Result<Response, Error> {
answer(
&auth,
signed_in(&admin),
AuthRequest::UpdateUser { id, change },
)
.await
}
async fn delete_user(
State(auth): State<Auth>,
Admin(admin): Admin,
Id(id): Id,
) -> Result<Response, Error> {
answer(&auth, signed_in(&admin), AuthRequest::DeleteUser { id }).await
}
async fn issue_link(
State(auth): State<Auth>,
Admin(admin): Admin,
Id(id): Id,
) -> Result<Response, Error> {
answer(&auth, signed_in(&admin), AuthRequest::IssueLink { id }).await
}
async fn revoke_link(
State(auth): State<Auth>,
Admin(admin): Admin,
Id(id): Id,
) -> Result<Response, Error> {
answer(&auth, signed_in(&admin), AuthRequest::RevokeLink { id }).await
}
async fn sign_out_everywhere(
State(auth): State<Auth>,
Admin(admin): Admin,
Id(id): Id,
) -> Result<Response, Error> {
answer(&auth, signed_in(&admin), AuthRequest::SignOutUser { id }).await
}
async fn revoke_access(
State(auth): State<Auth>,
Admin(admin): Admin,
Id(id): Id,
) -> Result<Response, Error> {
answer(&auth, signed_in(&admin), AuthRequest::RevokeAccess { id }).await
}
async fn impersonate(
State(auth): State<Auth>,
Admin(admin): Admin,
Id(id): Id,
headers: HeaderMap,
) -> Result<Response, Error> {
let caller = signed_in(&admin).headers(&headers);
answer(&auth, caller, AuthRequest::Impersonate { id }).await
}
async fn stop_impersonating(
State(auth): State<Auth>,
user: SignedIn,
headers: HeaderMap,
) -> Result<Response, Error> {
let caller = signed_in(&user).headers(&headers);
answer(&auth, caller, AuthRequest::StopImpersonating).await
}