scufflecloud_core/
services.rs

1use std::net::SocketAddr;
2use std::sync::Arc;
3
4use axum::http::{HeaderName, StatusCode};
5use axum::{Extension, Json};
6use reqwest::header::CONTENT_TYPE;
7use scuffle_http::http::Method;
8use tinc::TincService;
9use tinc::openapi::Server;
10use tower_http::cors::{AllowHeaders, CorsLayer, ExposeHeaders};
11use tower_http::trace::TraceLayer;
12
13use crate::middleware;
14
15mod organization_invitations;
16mod organizations;
17mod sessions;
18mod users;
19
20#[derive(Debug)]
21pub struct CoreSvc<G> {
22    _phantom: std::marker::PhantomData<G>,
23}
24
25impl<G> Default for CoreSvc<G> {
26    fn default() -> Self {
27        Self {
28            _phantom: std::marker::PhantomData,
29        }
30    }
31}
32
33fn rest_cors_layer() -> CorsLayer {
34    CorsLayer::new()
35        .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
36        .allow_origin(tower_http::cors::Any)
37        .allow_headers(tower_http::cors::Any)
38}
39
40fn grpc_web_cors_layer() -> CorsLayer {
41    // https://github.com/timostamm/protobuf-ts/blob/main/MANUAL.md#grpc-web-transport
42    let allow_headers = [
43        CONTENT_TYPE,
44        HeaderName::from_static("x-grpc-web"),
45        HeaderName::from_static("grpc-timeout"),
46    ]
47    .into_iter()
48    .chain(middleware::auth_headers());
49
50    let expose_headers = [
51        HeaderName::from_static("grpc-encoding"),
52        HeaderName::from_static("grpc-status"),
53        HeaderName::from_static("grpc-status-details-bin"),
54        HeaderName::from_static("grpc-message"),
55    ];
56
57    CorsLayer::new()
58        .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
59        .allow_headers(AllowHeaders::list(allow_headers))
60        .expose_headers(ExposeHeaders::list(expose_headers))
61        .allow_origin(tower_http::cors::Any)
62        .allow_headers(tower_http::cors::Any)
63}
64
65impl<G: core_traits::Global> scuffle_bootstrap::Service<G> for CoreSvc<G> {
66    async fn run(self, global: Arc<G>, ctx: scuffle_context::Context) -> anyhow::Result<()> {
67        // REST
68        let organization_invitations_svc_tinc =
69            pb::scufflecloud::core::v1::organization_invitations_service_tinc::OrganizationInvitationsServiceTinc::new(
70                CoreSvc::<G>::default(),
71            );
72        let organizations_svc_tinc =
73            pb::scufflecloud::core::v1::organizations_service_tinc::OrganizationsServiceTinc::new(CoreSvc::<G>::default());
74        let sessions_svc_tinc =
75            pb::scufflecloud::core::v1::sessions_service_tinc::SessionsServiceTinc::new(CoreSvc::<G>::default());
76        let users_svc_tinc = pb::scufflecloud::core::v1::users_service_tinc::UsersServiceTinc::new(CoreSvc::<G>::default());
77
78        let mut openapi_schema = organization_invitations_svc_tinc.openapi_schema();
79        openapi_schema.merge(organizations_svc_tinc.openapi_schema());
80        openapi_schema.merge(sessions_svc_tinc.openapi_schema());
81        openapi_schema.merge(users_svc_tinc.openapi_schema());
82        openapi_schema.info.title = "Scuffle Cloud Core API".to_string();
83        openapi_schema.info.version = "v1".to_string();
84        openapi_schema.servers = Some(vec![Server::new("/v1")]);
85
86        let v1_rest_router = axum::Router::new()
87            .route("/openapi.json", axum::routing::get(Json(openapi_schema)))
88            .merge(organization_invitations_svc_tinc.into_router())
89            .merge(organizations_svc_tinc.into_router())
90            .merge(sessions_svc_tinc.into_router())
91            .merge(users_svc_tinc.into_router())
92            .layer(rest_cors_layer());
93
94        // gRPC
95        let organization_invitations_svc =
96            pb::scufflecloud::core::v1::organization_invitations_service_server::OrganizationInvitationsServiceServer::new(
97                CoreSvc::<G>::default(),
98            );
99        let organizations_svc = pb::scufflecloud::core::v1::organizations_service_server::OrganizationsServiceServer::new(
100            CoreSvc::<G>::default(),
101        );
102        let sessions_svc =
103            pb::scufflecloud::core::v1::sessions_service_server::SessionsServiceServer::new(CoreSvc::<G>::default());
104        let users_svc = pb::scufflecloud::core::v1::users_service_server::UsersServiceServer::new(CoreSvc::<G>::default());
105
106        let reflection_v1_svc = tonic_reflection::server::Builder::configure()
107            .register_encoded_file_descriptor_set(pb::ANNOTATIONS_PB)
108            .build_v1()?;
109        let reflection_v1alpha_svc = tonic_reflection::server::Builder::configure()
110            .register_encoded_file_descriptor_set(pb::ANNOTATIONS_PB)
111            .build_v1alpha()?;
112
113        let mut builder = tonic::service::Routes::builder();
114        builder.add_service(organization_invitations_svc);
115        builder.add_service(organizations_svc);
116        builder.add_service(sessions_svc);
117        builder.add_service(users_svc);
118        builder.add_service(reflection_v1_svc);
119        builder.add_service(reflection_v1alpha_svc);
120
121        let grpc_router = builder
122            .routes()
123            .prepare()
124            .into_axum_router()
125            .layer(tonic_web::GrpcWebLayer::new())
126            .layer(grpc_web_cors_layer());
127
128        let mut router = axum::Router::new()
129            .nest("/v1", v1_rest_router)
130            .merge(grpc_router)
131            .route_layer(axum::middleware::from_fn(crate::middleware::auth::<G>))
132            .layer(geo_ip::middleware::middleware::<G>())
133            .layer(TraceLayer::new_for_http())
134            .layer(Extension(Arc::clone(&global)))
135            .fallback(StatusCode::NOT_FOUND);
136
137        if global.swagger_ui_enabled() {
138            router = router.merge(swagger_ui_dist::generate_routes(swagger_ui_dist::ApiDefinition {
139                uri_prefix: "/v1/docs",
140                api_definition: swagger_ui_dist::OpenApiSource::Uri("/v1/openapi.json"),
141                title: Some("Scuffle Core v1 Api Docs"),
142            }));
143        }
144
145        scuffle_http::HttpServer::builder()
146            .tower_make_service_with_addr(router.into_make_service_with_connect_info::<SocketAddr>())
147            .bind(global.service_bind())
148            .ctx(ctx)
149            .build()
150            .run()
151            .await?;
152
153        Ok(())
154    }
155}