Skip to content

Commit b51f554

Browse files
committed
feat: support token validation for buckal cli
1 parent cd72234 commit b51f554

File tree

8 files changed

+79
-29
lines changed

8 files changed

+79
-29
lines changed

jupiter/src/storage/base_storage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ pub trait StorageConnector {
9292
}
9393
}
9494

95-
#[derive(Clone)]
95+
#[derive(Debug, Clone)]
9696
pub struct BaseStorage {
9797
pub connection: Arc<DatabaseConnection>,
9898
}

jupiter/src/storage/user_storage.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use common::{errors::MegaError, utils::generate_id};
1010

1111
use crate::storage::base_storage::{BaseStorage, StorageConnector};
1212

13-
#[derive(Clone)]
13+
#[derive(Debug, Clone)]
1414
pub struct UserStorage {
1515
pub base: BaseStorage,
1616
}
@@ -23,6 +23,11 @@ impl Deref for UserStorage {
2323
}
2424

2525
impl UserStorage {
26+
pub fn mock() -> Self {
27+
let mock = BaseStorage::mock();
28+
Self { base: mock }
29+
}
30+
2631
pub async fn save_ssh_key(
2732
&self,
2833
username: String,

mono/src/api/oauth/campsite_store.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::sync::Arc;
33
use anyhow::Context;
44
use async_trait::async_trait;
55
use http::header::COOKIE;
6+
use jupiter::storage::user_storage::UserStorage;
67
use reqwest::Client;
78
use reqwest::Url;
89
use tower_sessions::{
@@ -20,6 +21,7 @@ pub struct CampsiteApiStore {
2021
client: Arc<Client>,
2122
// cookie_store: Arc<Jar>,
2223
api_base_url: String,
24+
user_storage: UserStorage,
2325
}
2426

2527
#[async_trait]
@@ -42,14 +44,15 @@ impl SessionStore for CampsiteApiStore {
4244
}
4345

4446
impl CampsiteApiStore {
45-
pub fn new(api_base_url: String) -> Self {
47+
pub fn new(api_base_url: String, user_storage: UserStorage) -> Self {
4648
let client = Client::builder()
4749
.no_proxy()
4850
.build()
4951
.expect("Failed to build client");
5052
Self {
5153
client: Arc::new(client),
5254
api_base_url,
55+
user_storage,
5356
}
5457
}
5558

@@ -82,4 +85,15 @@ impl CampsiteApiStore {
8285
Ok(None)
8386
}
8487
}
88+
89+
pub async fn load_user_from_token(&self, token: String) -> anyhow::Result<Option<LoginUser>> {
90+
if let Some(username) = self.user_storage.find_user_by_token(&token).await? {
91+
let user = LoginUser {
92+
username,
93+
..Default::default()
94+
};
95+
return Ok(Some(user));
96+
}
97+
Ok(None)
98+
}
8599
}

mono/src/api/oauth/mod.rs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ use axum::{
66
response::{IntoResponse, Redirect, Response},
77
routing::get,
88
};
9-
use axum_extra::{TypedHeader, headers, typed_header::TypedHeaderRejectionReason};
9+
use axum_extra::{
10+
TypedHeader,
11+
headers::{self, Authorization, authorization::Bearer},
12+
typed_header::TypedHeaderRejectionReason,
13+
};
1014
use chrono::{Duration, Utc};
1115
use http::request::Parts;
1216
use oauth2::{
@@ -203,6 +207,25 @@ where
203207
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
204208
let store = CampsiteApiStore::from_ref(state);
205209

210+
// Try Bearer Token for CLI authentication
211+
if let Ok(TypedHeader(Authorization(bearer))) =
212+
parts.extract::<TypedHeader<Authorization<Bearer>>>().await
213+
{
214+
let token = bearer.token().to_string();
215+
match store.load_user_from_token(token).await {
216+
Ok(Some(user)) => return Ok(user),
217+
Ok(None) => {
218+
tracing::error!("Invalid or expired bearer token");
219+
return Err(AuthRedirect);
220+
}
221+
Err(e) => {
222+
tracing::error!("Error validating bearer token: {:?}", e);
223+
return Err(AuthRedirect);
224+
}
225+
}
226+
}
227+
228+
// Fall back to Cookie Session (UI)
206229
let cookies = parts
207230
.extract::<TypedHeader<headers::Cookie>>()
208231
.await
@@ -217,15 +240,16 @@ where
217240
let session_cookie = cookies.get(CAMPSITE_API_COOKIE).ok_or(AuthRedirect)?;
218241

219242
// Load user from external API
220-
let user = store
221-
.load_user_from_api(session_cookie.to_string())
222-
.await
223-
.map_err(|e| {
224-
tracing::error!("load_user_from_api error: {:?}", e);
225-
AuthRedirect
226-
})?
227-
.ok_or(AuthRedirect)?;
228-
229-
Ok(user)
243+
match store.load_user_from_api(session_cookie.to_string()).await {
244+
Ok(Some(user)) => Ok(user),
245+
Ok(None) => {
246+
tracing::error!("Invalid or expired session cookie");
247+
Err(AuthRedirect)
248+
}
249+
Err(e) => {
250+
tracing::error!("Error loading user from cookie session: {:?}", e);
251+
Err(AuthRedirect)
252+
}
253+
}
230254
}
231255
}

mono/src/api/oauth/model.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use chrono::{DateTime, NaiveDateTime, Utc};
21
use serde::{Deserialize, Serialize};
32

43
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -30,7 +29,6 @@ impl From<GitHubUserJson> for LoginUser {
3029
email: value.email.unwrap_or_default(),
3130
avatar_url: value.avatar_url,
3231
campsite_user_id: String::new(),
33-
created_at: Utc::now().naive_utc(),
3432
}
3533
}
3634
}
@@ -41,7 +39,6 @@ pub struct CampsiteUserJson {
4139
pub id: String,
4240
pub avatar_url: String,
4341
pub email: Option<String>,
44-
pub created_at: DateTime<Utc>,
4542
}
4643

4744
impl From<CampsiteUserJson> for LoginUser {
@@ -51,16 +48,14 @@ impl From<CampsiteUserJson> for LoginUser {
5148
email: value.email.unwrap_or_default(),
5249
avatar_url: value.avatar_url,
5350
campsite_user_id: value.id,
54-
created_at: value.created_at.naive_utc(),
5551
}
5652
}
5753
}
5854

59-
#[derive(Serialize, Deserialize, Clone, Debug)]
55+
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
6056
pub struct LoginUser {
6157
pub campsite_user_id: String,
6258
pub username: String,
6359
pub avatar_url: String,
6460
pub email: String,
65-
pub created_at: NaiveDateTime,
6661
}

mono/src/server/http_server.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ pub async fn app(storage: Storage, host: String, port: u16) -> Router {
110110
let api_state = MonoApiServiceState {
111111
storage: storage.clone(),
112112
oauth_client: Some(oauth_client(oauth_config.clone()).unwrap()),
113-
session_store: Some(CampsiteApiStore::new(oauth_config.campsite_api_domain)),
113+
session_store: Some(CampsiteApiStore::new(
114+
oauth_config.campsite_api_domain,
115+
storage.user_storage(),
116+
)),
114117
listen_addr: format!("http://{host}:{port}"),
115118
entity_store: EntityStore::new(),
116119
};

mono/tests/campsite_api_store_tests.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! These tests cover the CampsiteApiStore's ability to load user information from an external API.
44
55
use axum::Router;
6+
use jupiter::storage::user_storage::UserStorage;
67
use mono::api::oauth::campsite_store::CampsiteApiStore;
78
use serde_json::json;
89
use std::net::SocketAddr;
@@ -53,7 +54,7 @@ async fn test_load_user_from_api_success() {
5354
let (addr, _handle) = create_mock_campsite_server().await;
5455
let api_url = format!("http://{}", addr);
5556

56-
let store = CampsiteApiStore::new(api_url);
57+
let store = CampsiteApiStore::new(api_url, UserStorage::mock());
5758

5859
// Test with a valid cookie
5960
let result = store
@@ -78,7 +79,8 @@ async fn test_load_user_from_api_invalid_cookie() {
7879

7980
// Test with an invalid cookie that causes a 401 response
8081
// We'll simulate this by using a non-existent endpoint
81-
let invalid_store = CampsiteApiStore::new(format!("http://{}/nonexistent", addr));
82+
let invalid_store =
83+
CampsiteApiStore::new(format!("http://{}/nonexistent", addr), UserStorage::mock());
8284

8385
let result = invalid_store
8486
.load_user_from_api("invalid_session_cookie".to_string())
@@ -100,7 +102,7 @@ async fn test_load_user_from_api_server_error() {
100102
let (addr, _handle) = create_mock_campsite_server().await;
101103
let api_url = format!("http://{}", addr);
102104

103-
let store = CampsiteApiStore::new(format!("{}/v1/users/error", api_url));
105+
let store = CampsiteApiStore::new(format!("{}/v1/users/error", api_url), UserStorage::mock());
104106

105107
let result = store.load_user_from_api("any_cookie".to_string()).await;
106108

@@ -118,7 +120,10 @@ async fn test_load_user_from_api_server_error() {
118120
#[tokio::test]
119121
async fn test_load_user_from_api_network_error() {
120122
// Test with an invalid URL that will cause a network error
121-
let store = CampsiteApiStore::new("http://invalid.domain.localhost:12345".to_string());
123+
let store = CampsiteApiStore::new(
124+
"http://invalid.domain.localhost:12345".to_string(),
125+
UserStorage::mock(),
126+
);
122127

123128
let result = store.load_user_from_api("any_cookie".to_string()).await;
124129

mono/tests/login_user_extractor_tests.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! These tests cover the LoginUser extractor's ability to extract user information from requests.
44
//! Since the extractor relies on CampsiteApiStore, we focus on testing the underlying functionality.
55
6+
use jupiter::storage::user_storage::UserStorage;
67
use mono::api::oauth::campsite_store::CampsiteApiStore;
78
use serde_json::json;
89
use std::net::SocketAddr;
@@ -44,7 +45,7 @@ async fn test_login_user_extractor_success() {
4445
let _api_url = format!("http://{}", addr);
4546

4647
// Create a mock store
47-
let store = CampsiteApiStore::new(format!("http://{}", addr));
48+
let store = CampsiteApiStore::new(format!("http://{}", addr), UserStorage::mock());
4849

4950
// Test the load_user_from_api method directly
5051
let result = store
@@ -68,7 +69,7 @@ async fn test_login_user_extractor_invalid_cookie() {
6869
let _api_url = format!("http://{}", addr);
6970

7071
// Create a mock store with a non-existent endpoint to simulate an invalid cookie
71-
let store = CampsiteApiStore::new(format!("http://{}/nonexistent", addr));
72+
let store = CampsiteApiStore::new(format!("http://{}/nonexistent", addr), UserStorage::mock());
7273

7374
// Test the load_user_from_api method directly
7475
let result = store
@@ -90,7 +91,7 @@ async fn test_login_user_extractor_missing_cookie() {
9091
let _api_url = format!("http://{}", addr);
9192

9293
// Create a mock store
93-
let store = CampsiteApiStore::new(format!("http://{}", addr));
94+
let store = CampsiteApiStore::new(format!("http://{}", addr), UserStorage::mock());
9495

9596
// Test with an empty cookie string to simulate missing cookie
9697
let result = store.load_user_from_api("".to_string()).await;
@@ -106,7 +107,10 @@ async fn test_login_user_extractor_missing_cookie() {
106107
#[tokio::test]
107108
async fn test_login_user_extractor_network_error() {
108109
// Test with an invalid URL that will cause a network error
109-
let store = CampsiteApiStore::new("http://invalid.domain.localhost:12345".to_string());
110+
let store = CampsiteApiStore::new(
111+
"http://invalid.domain.localhost:12345".to_string(),
112+
UserStorage::mock(),
113+
);
110114

111115
let result = store.load_user_from_api("any_cookie".to_string()).await;
112116

0 commit comments

Comments
 (0)