Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
173
crates/scel_api/src/lib.rs
Normal file
173
crates/scel_api/src/lib.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
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<ScelSchema>,
|
||||
req: Json<Request>,
|
||||
_: User,
|
||||
) -> Json<Response> {
|
||||
schema.execute(req.0).await.into()
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
app: Router,
|
||||
addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(app: Arc<App>) -> 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<String>,
|
||||
#[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<B> FromRequest<B> for User
|
||||
where
|
||||
B: Send,
|
||||
{
|
||||
type Rejection = AuthRedirect;
|
||||
|
||||
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
|
||||
let Extension(store) = Extension::<MemoryStore>::from_request(req)
|
||||
.await
|
||||
.expect("MemoryStore extension is missing");
|
||||
|
||||
let cookies = TypedHeader::<headers::Cookie>::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>("user").ok_or(AuthRedirect)?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_error(_err: io::Error) -> impl IntoResponse {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong...")
|
||||
}
|
Reference in New Issue
Block a user