kjuulh
74ea9ddf79
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
174 lines
5.1 KiB
Rust
174 lines
5.1 KiB
Rust
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...")
|
|
}
|