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-message"),
54    ];
55
56    CorsLayer::new()
57        .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
58        .allow_headers(AllowHeaders::list(allow_headers))
59        .expose_headers(ExposeHeaders::list(expose_headers))
60        .allow_origin(tower_http::cors::Any)
61        .allow_headers(tower_http::cors::Any)
62}
63
64impl<G: core_traits::Global> scuffle_bootstrap::Service<G> for CoreSvc<G> {
65    async fn run(self, global: Arc<G>, ctx: scuffle_context::Context) -> anyhow::Result<()> {
66        // REST
67        let organization_invitations_svc_tinc =
68            pb::scufflecloud::core::v1::organization_invitations_service_tinc::OrganizationInvitationsServiceTinc::new(
69                CoreSvc::<G>::default(),
70            );
71        let organizations_svc_tinc =
72            pb::scufflecloud::core::v1::organizations_service_tinc::OrganizationsServiceTinc::new(CoreSvc::<G>::default());
73        let sessions_svc_tinc =
74            pb::scufflecloud::core::v1::sessions_service_tinc::SessionsServiceTinc::new(CoreSvc::<G>::default());
75        let users_svc_tinc = pb::scufflecloud::core::v1::users_service_tinc::UsersServiceTinc::new(CoreSvc::<G>::default());
76
77        let mut openapi_schema = organization_invitations_svc_tinc.openapi_schema();
78        openapi_schema.merge(organizations_svc_tinc.openapi_schema());
79        openapi_schema.merge(sessions_svc_tinc.openapi_schema());
80        openapi_schema.merge(users_svc_tinc.openapi_schema());
81        openapi_schema.info.title = "Scuffle Cloud Core API".to_string();
82        openapi_schema.info.version = "v1".to_string();
83        openapi_schema.servers = Some(vec![Server::new("/v1")]);
84
85        let v1_rest_router = axum::Router::new()
86            .route("/openapi.json", axum::routing::get(Json(openapi_schema)))
87            .merge(organization_invitations_svc_tinc.into_router())
88            .merge(organizations_svc_tinc.into_router())
89            .merge(sessions_svc_tinc.into_router())
90            .merge(users_svc_tinc.into_router())
91            .layer(rest_cors_layer());
92
93        // gRPC
94        let organization_invitations_svc =
95            pb::scufflecloud::core::v1::organization_invitations_service_server::OrganizationInvitationsServiceServer::new(
96                CoreSvc::<G>::default(),
97            );
98        let organizations_svc = pb::scufflecloud::core::v1::organizations_service_server::OrganizationsServiceServer::new(
99            CoreSvc::<G>::default(),
100        );
101        let sessions_svc =
102            pb::scufflecloud::core::v1::sessions_service_server::SessionsServiceServer::new(CoreSvc::<G>::default());
103        let users_svc = pb::scufflecloud::core::v1::users_service_server::UsersServiceServer::new(CoreSvc::<G>::default());
104
105        let reflection_v1_svc = tonic_reflection::server::Builder::configure()
106            .register_encoded_file_descriptor_set(pb::ANNOTATIONS_PB)
107            .build_v1()?;
108        let reflection_v1alpha_svc = tonic_reflection::server::Builder::configure()
109            .register_encoded_file_descriptor_set(pb::ANNOTATIONS_PB)
110            .build_v1alpha()?;
111
112        let mut builder = tonic::service::Routes::builder();
113        builder.add_service(organization_invitations_svc);
114        builder.add_service(organizations_svc);
115        builder.add_service(sessions_svc);
116        builder.add_service(users_svc);
117        builder.add_service(reflection_v1_svc);
118        builder.add_service(reflection_v1alpha_svc);
119
120        let grpc_router = builder
121            .routes()
122            .prepare()
123            .into_axum_router()
124            .layer(tonic_web::GrpcWebLayer::new())
125            .layer(grpc_web_cors_layer());
126
127        let mut router = axum::Router::new()
128            .nest("/v1", v1_rest_router)
129            .merge(grpc_router)
130            .route_layer(axum::middleware::from_fn(crate::middleware::auth::<G>))
131            .layer(geo_ip::middleware::middleware::<G>())
132            .layer(TraceLayer::new_for_http())
133            .layer(Extension(Arc::clone(&global)))
134            .fallback(StatusCode::NOT_FOUND);
135
136        if global.swagger_ui_enabled() {
137            router = router.merge(swagger_ui_dist::generate_routes(swagger_ui_dist::ApiDefinition {
138                uri_prefix: "/v1/docs",
139                api_definition: swagger_ui_dist::OpenApiSource::Uri("/v1/openapi.json"),
140                title: Some("Scuffle Core v1 Api Docs"),
141            }));
142        }
143
144        scuffle_http::HttpServer::builder()
145            .tower_make_service_with_addr(router.into_make_service_with_connect_info::<SocketAddr>())
146            .bind(global.service_bind())
147            .ctx(ctx)
148            .build()
149            .run()
150            .await?;
151
152        Ok(())
153    }
154}