scufflecloud_core/
services.rs1use std::net::SocketAddr;
2use std::sync::Arc;
3
4use axum::http::{HeaderName, StatusCode};
5use axum::{Extension, Json};
6use geo_ip::maxminddb;
7use geo_ip::middleware::IpAddressInfo;
8use reqwest::header::CONTENT_TYPE;
9use scuffle_http::http::Method;
10use tinc::TincService;
11use tinc::openapi::Server;
12use tower_http::cors::{AllowHeaders, CorsLayer, ExposeHeaders};
13use tower_http::trace::TraceLayer;
14
15use crate::middleware;
16
17mod organization_invitations;
18mod organizations;
19mod sessions;
20mod users;
21
22#[derive(Debug)]
23pub struct CoreSvc<G> {
24 _phantom: std::marker::PhantomData<G>,
25}
26
27impl<G> Default for CoreSvc<G> {
28 fn default() -> Self {
29 Self {
30 _phantom: std::marker::PhantomData,
31 }
32 }
33}
34
35fn rest_cors_layer() -> CorsLayer {
36 CorsLayer::new()
37 .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
38 .allow_origin(tower_http::cors::Any)
39 .allow_headers(tower_http::cors::Any)
40}
41
42fn grpc_web_cors_layer() -> CorsLayer {
43 let allow_headers = [
45 CONTENT_TYPE,
46 HeaderName::from_static("x-grpc-web"),
47 HeaderName::from_static("grpc-timeout"),
48 ]
49 .into_iter()
50 .chain(middleware::auth_headers());
51
52 let expose_headers = [
53 HeaderName::from_static("grpc-encoding"),
54 HeaderName::from_static("grpc-status"),
55 HeaderName::from_static("grpc-message"),
56 ];
57
58 CorsLayer::new()
59 .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
60 .allow_headers(AllowHeaders::list(allow_headers))
61 .expose_headers(ExposeHeaders::list(expose_headers))
62 .allow_origin(tower_http::cors::Any)
63 .allow_headers(tower_http::cors::Any)
64}
65
66#[derive(serde::Serialize)]
67struct RootGeoResponse {
68 city: Option<String>,
69 country_code: Option<String>,
70}
71
72#[derive(serde::Serialize)]
73struct RootResponse {
74 version: &'static str,
75 ip_address: std::net::IpAddr,
76 geo_location: RootGeoResponse,
77 branch: Option<&'static str>,
78 commit: Option<&'static str>,
79}
80
81async fn root<G: geo_ip::GeoIpInterface>(
82 Extension(global): Extension<Arc<G>>,
83 Extension(ip_address_info): Extension<IpAddressInfo>,
84) -> Json<RootResponse> {
85 let geo_city = global
86 .geo_ip_resolver()
87 .lookup::<maxminddb::geoip2::City>(ip_address_info.ip_address)
88 .ok()
89 .flatten();
90
91 let city = geo_city
92 .as_ref()
93 .and_then(|c| c.city.as_ref())
94 .and_then(|c| c.names.as_ref())
95 .and_then(|c| c.get("en").map(|s| s.to_string()));
96
97 let country_code = geo_city
98 .as_ref()
99 .and_then(|c| c.country.as_ref())
100 .and_then(|c| c.iso_code.map(|s| s.to_string()));
101
102 let resp = RootResponse {
103 version: env!("CARGO_PKG_VERSION"),
104 ip_address: ip_address_info.ip_address,
105 geo_location: RootGeoResponse { city, country_code },
106 branch: option_env!("GIT_BRANCH"),
107 commit: option_env!("COMMIT_SHA"),
108 };
109
110 Json(resp)
111}
112
113impl<G: core_traits::Global> scuffle_bootstrap::Service<G> for CoreSvc<G> {
114 async fn run(self, global: Arc<G>, ctx: scuffle_context::Context) -> anyhow::Result<()> {
115 let organization_invitations_svc_tinc =
117 pb::scufflecloud::core::v1::organization_invitations_service_tinc::OrganizationInvitationsServiceTinc::new(
118 CoreSvc::<G>::default(),
119 );
120 let organizations_svc_tinc =
121 pb::scufflecloud::core::v1::organizations_service_tinc::OrganizationsServiceTinc::new(CoreSvc::<G>::default());
122 let sessions_svc_tinc =
123 pb::scufflecloud::core::v1::sessions_service_tinc::SessionsServiceTinc::new(CoreSvc::<G>::default());
124 let users_svc_tinc = pb::scufflecloud::core::v1::users_service_tinc::UsersServiceTinc::new(CoreSvc::<G>::default());
125
126 let mut openapi_schema = organization_invitations_svc_tinc.openapi_schema();
127 openapi_schema.merge(organizations_svc_tinc.openapi_schema());
128 openapi_schema.merge(sessions_svc_tinc.openapi_schema());
129 openapi_schema.merge(users_svc_tinc.openapi_schema());
130 openapi_schema.info.title = "Scuffle Cloud Core API".to_string();
131 openapi_schema.info.version = "v1".to_string();
132 openapi_schema.servers = Some(vec![Server::new("/v1")]);
133
134 let v1_rest_router = axum::Router::new()
135 .route("/openapi.json", axum::routing::get(Json(openapi_schema)))
136 .merge(organization_invitations_svc_tinc.into_router())
137 .merge(organizations_svc_tinc.into_router())
138 .merge(sessions_svc_tinc.into_router())
139 .merge(users_svc_tinc.into_router())
140 .layer(rest_cors_layer());
141
142 let organization_invitations_svc =
144 pb::scufflecloud::core::v1::organization_invitations_service_server::OrganizationInvitationsServiceServer::new(
145 CoreSvc::<G>::default(),
146 );
147 let organizations_svc = pb::scufflecloud::core::v1::organizations_service_server::OrganizationsServiceServer::new(
148 CoreSvc::<G>::default(),
149 );
150 let sessions_svc =
151 pb::scufflecloud::core::v1::sessions_service_server::SessionsServiceServer::new(CoreSvc::<G>::default());
152 let users_svc = pb::scufflecloud::core::v1::users_service_server::UsersServiceServer::new(CoreSvc::<G>::default());
153
154 let reflection_v1_svc = tonic_reflection::server::Builder::configure()
155 .register_encoded_file_descriptor_set(pb::ANNOTATIONS_PB)
156 .build_v1()?;
157 let reflection_v1alpha_svc = tonic_reflection::server::Builder::configure()
158 .register_encoded_file_descriptor_set(pb::ANNOTATIONS_PB)
159 .build_v1alpha()?;
160
161 let mut builder = tonic::service::Routes::builder();
162 builder.add_service(organization_invitations_svc);
163 builder.add_service(organizations_svc);
164 builder.add_service(sessions_svc);
165 builder.add_service(users_svc);
166 builder.add_service(reflection_v1_svc);
167 builder.add_service(reflection_v1alpha_svc);
168
169 let grpc_router = builder
170 .routes()
171 .prepare()
172 .into_axum_router()
173 .layer(tonic_web::GrpcWebLayer::new())
174 .layer(grpc_web_cors_layer());
175
176 let mut router = axum::Router::new()
177 .route("/", axum::routing::get(root::<G>))
178 .nest("/v1", v1_rest_router)
179 .merge(grpc_router)
180 .route_layer(axum::middleware::from_fn(crate::middleware::auth::<G>))
181 .layer(geo_ip::middleware::middleware::<G>())
182 .layer(TraceLayer::new_for_http())
183 .layer(Extension(Arc::clone(&global)))
184 .fallback(StatusCode::NOT_FOUND);
185
186 if global.swagger_ui_enabled() {
187 router = router.merge(swagger_ui_dist::generate_routes(swagger_ui_dist::ApiDefinition {
188 uri_prefix: "/v1/docs",
189 api_definition: swagger_ui_dist::OpenApiSource::Uri("/v1/openapi.json"),
190 title: Some("Scuffle Core v1 Api Docs"),
191 }));
192 }
193
194 scuffle_http::HttpServer::builder()
195 .tower_make_service_with_addr(router.into_make_service_with_connect_info::<SocketAddr>())
196 .bind(global.service_bind())
197 .ctx(ctx)
198 .build()
199 .run()
200 .await?;
201
202 Ok(())
203 }
204}