forge_core_server/routes/
auth.rs1use axum::{
2 Router,
3 extract::{Request, State},
4 http::StatusCode,
5 middleware::{Next, from_fn_with_state},
6 response::{Json as ResponseJson, Response},
7 routing::{get, post},
8};
9use forge_core_deployment::Deployment;
10use forge_core_services::services::{
11 auth::{AuthError, DeviceFlowStartResponse},
12 config::save_config_to_file,
13 github_service::{GitHubService, GitHubServiceError},
14};
15use forge_core_utils::response::ApiResponse;
16use octocrab::auth::Continue;
17use serde::{Deserialize, Serialize};
18
19use crate::{DeploymentImpl, error::ApiError};
20
21pub fn router(deployment: &DeploymentImpl) -> Router<DeploymentImpl> {
22 Router::new()
23 .route("/auth/github/device/start", post(device_start))
24 .route("/auth/github/device/poll", post(device_poll))
25 .route("/auth/github/check", get(github_check_token))
26 .layer(from_fn_with_state(
27 deployment.clone(),
28 sentry_user_context_middleware,
29 ))
30}
31
32async fn device_start(
34 State(deployment): State<DeploymentImpl>,
35) -> Result<ResponseJson<ApiResponse<DeviceFlowStartResponse>>, ApiError> {
36 let device_start_response = deployment.auth().device_start().await?;
37 Ok(ResponseJson(ApiResponse::success(device_start_response)))
38}
39
40#[derive(Serialize, Deserialize, ts_rs_forge::TS)]
41#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
42#[ts(use_ts_enum)]
43pub enum DevicePollStatus {
44 SlowDown,
45 AuthorizationPending,
46 Success,
47}
48
49#[derive(Serialize, Deserialize, ts_rs_forge::TS)]
50#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
51#[ts(use_ts_enum)]
52pub enum CheckTokenResponse {
53 Valid,
54 Invalid,
55}
56
57async fn device_poll(
59 State(deployment): State<DeploymentImpl>,
60) -> Result<ResponseJson<ApiResponse<DevicePollStatus>>, ApiError> {
61 let user_info = match deployment.auth().device_poll().await {
62 Ok(info) => info,
63 Err(AuthError::Pending(Continue::SlowDown)) => {
64 return Ok(ResponseJson(ApiResponse::success(
65 DevicePollStatus::SlowDown,
66 )));
67 }
68 Err(AuthError::Pending(Continue::AuthorizationPending)) => {
69 return Ok(ResponseJson(ApiResponse::success(
70 DevicePollStatus::AuthorizationPending,
71 )));
72 }
73 Err(e) => return Err(e.into()),
74 };
75 {
77 let config_path = forge_core_utils::assets::config_path();
78 let mut config = deployment.config().write().await;
79 config.github.username = Some(user_info.username.clone());
80 config.github.primary_email = user_info.primary_email.clone();
81 config.github.oauth_token = Some(user_info.token.to_string());
82 config.github_login_acknowledged = true; save_config_to_file(&config.clone(), &config_path).await?;
84 }
85 let _ = deployment.update_sentry_scope().await;
86 let props = serde_json::json!({
87 "username": user_info.username,
88 "email": user_info.primary_email,
89 });
90 deployment
91 .track_if_analytics_allowed("$identify", props)
92 .await;
93 Ok(ResponseJson(ApiResponse::success(
94 DevicePollStatus::Success,
95 )))
96}
97
98async fn github_check_token(
100 State(deployment): State<DeploymentImpl>,
101) -> Result<ResponseJson<ApiResponse<CheckTokenResponse>>, ApiError> {
102 let gh_config = deployment.config().read().await.github.clone();
103 let Some(token) = gh_config.token() else {
104 return Ok(ResponseJson(ApiResponse::success(
105 CheckTokenResponse::Invalid,
106 )));
107 };
108 let gh = GitHubService::new(&token)?;
109 match gh.check_token().await {
110 Ok(()) => Ok(ResponseJson(ApiResponse::success(
111 CheckTokenResponse::Valid,
112 ))),
113 Err(GitHubServiceError::TokenInvalid) => Ok(ResponseJson(ApiResponse::success(
114 CheckTokenResponse::Invalid,
115 ))),
116 Err(e) => Err(e.into()),
117 }
118}
119
120pub async fn sentry_user_context_middleware(
122 State(deployment): State<DeploymentImpl>,
123 req: Request,
124 next: Next,
125) -> Result<Response, StatusCode> {
126 let _ = deployment.update_sentry_scope().await;
127 Ok(next.run(req).await)
128}