forge_core_server/routes/
auth.rs

1use 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
32/// POST /auth/github/device/start
33async 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
57/// POST /auth/github/device/poll
58async 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    // Save to config
76    {
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; // Also acknowledge the GitHub login step
83        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
98/// GET /auth/github/check
99async 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
120/// Middleware to set Sentry user context for every request
121pub 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}