mod auth; mod graphql; use std::{io, net::SocketAddr, sync::Arc}; use async_graphql::{ extensions::{Logger, Tracing}, http::{playground_source, GraphQLPlaygroundConfig}, Request, Response, Schema, }; use async_graphql_axum::GraphQLSubscription; use async_session::{async_trait, MemoryStore, SessionStore}; use auth::{authorized, gitea}; use axum::{ extract::{rejection::TypedHeaderRejectionReason, FromRequest, RequestParts}, headers, http::{header, Method}, response::{Html, IntoResponse, Redirect}, routing::{self, get_service}, Extension, Json, Router, TypedHeader, }; use graphql::{ mutation::MutationRoot, query::QueryRoot, schema::ScelSchema, subscription::SubscriptionRoot, }; use reqwest::StatusCode; use scel_core::App; use serde::{Deserialize, Serialize}; use tower_http::{ cors::CorsLayer, services::ServeDir, trace::{DefaultMakeSpan, TraceLayer}, }; async fn graphql_playground() -> impl IntoResponse { Html(playground_source( GraphQLPlaygroundConfig::new("/graphql").subscription_endpoint("/ws"), )) } async fn graphql_handler( schema: Extension, req: Json, _: User, ) -> Json { schema.execute(req.0).await.into() } pub struct Server { app: Router, addr: SocketAddr, } impl Server { pub fn new(app: Arc) -> Server { let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) .extension(Tracing) .extension(Logger) .data(app) .finish(); let cors = vec![ "http://localhost:3000" .parse() .expect("Could not parse url"), "https://scel.front.kjuulh.io" .parse() .expect("Could not parse url"), ]; let api_router = Router::new() .route( "/graphql", routing::get(graphql_playground).post(graphql_handler), ) .route("/ws", GraphQLSubscription::new(schema.clone())) .route("/auth/gitea", routing::get(gitea)) .route("/auth/authorized", routing::get(authorized)) // .merge(axum_extra::routing::SpaRouter::new( // "/assets", // "src/web/dist/assets", // )) .fallback(get_service(ServeDir::new("./src/web/dist/")).handle_error(handle_error)) .layer(Extension(schema)) .layer(Extension(MemoryStore::new())) .layer(Extension(auth::oauth_client())) .layer( CorsLayer::new() .allow_origin(cors) .allow_headers([axum::http::header::CONTENT_TYPE]) .allow_methods([Method::GET, Method::POST, Method::OPTIONS]), ) .layer(TraceLayer::new_for_http().make_span_with(DefaultMakeSpan::default())); let app = Router::new().nest("/api", api_router); let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); Server { app, addr } } pub async fn start(self) -> anyhow::Result<()> { tracing::info!("listening on {}", self.addr); match axum::Server::bind(&self.addr) .serve(self.app.into_make_service()) .await { Ok(_) => Ok(()), Err(e) => Err(e.into()), } } } #[derive(Debug, Serialize, Deserialize)] struct User { #[serde(alias = "sub")] id: String, #[serde(alias = "picture")] avatar: Option, #[serde(alias = "email")] email: String, #[serde(alias = "preferred_username")] username: String, } struct AuthRedirect; impl IntoResponse for AuthRedirect { fn into_response(self) -> axum::response::Response { Redirect::temporary("/auth/gitea").into_response() } } const COOKIE_NAME: &str = "auth"; #[async_trait] impl FromRequest for User where B: Send, { type Rejection = AuthRedirect; async fn from_request(req: &mut RequestParts) -> Result { let Extension(store) = Extension::::from_request(req) .await .expect("MemoryStore extension is missing"); let cookies = TypedHeader::::from_request(req) .await .map_err(|e| match *e.name() { header::COOKIE => match e.reason() { TypedHeaderRejectionReason::Missing => AuthRedirect, _ => panic!("unexpected error getting Cookie header(s): {}", e), }, _ => panic!("unexpected error getting cookies: {}", e), })?; let session_cookie = cookies.get(COOKIE_NAME).ok_or(AuthRedirect)?; let session = store .load_session(session_cookie.to_string()) .await .expect("could not load session") .ok_or(AuthRedirect)?; let user = session.get::("user").ok_or(AuthRedirect)?; Ok(user) } } async fn handle_error(_err: io::Error) -> impl IntoResponse { (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong...") }