Add oauth/auth endpoints #1

Open
marvin wants to merge 1 commits from feat/auth into master
6 changed files with 310 additions and 21 deletions

52
examples/oauth_login.rs Normal file
View File

@@ -0,0 +1,52 @@
use immich_sdk::{Client, Config};
use std::time::Duration;
use immich_sdk::models::{OAuthConfigDto, OAuthCallbackDto};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = Config::new("http://localhost:2283")
.with_api_key("your-api-key")
.with_timeout(Duration::from_secs(30));
let client = Client::new(config)?;
println!("Starting OAuth authorization process...");
// 1. Start OAuth authorization
// In a real scenario, this URL would be opened in a browser.
let auth_config = OAuthConfigDto {
redirect_uri: "http://localhost:8080/callback".to_string(),
code_challenge: None,
state: Some("random_state_string".to_string()),
};
let auth_response = client
.oauth()
.authorize(auth_config)
.await?;
let auth_url = auth_response.url;
println!("Please visit this URL to authorize: {}", auth_url);
// 2. Simulate the callback from the OAuth provider
// In a real scenario, your web server would receive this POST request.
let callback_data = OAuthCallbackDto {
url: "http://localhost:8080/callback".to_string(),
state: Some("random_state_string".to_string()),
code_verifier: Some("some_verifier".to_string()),
};
println!("Simulating OAuth callback with: {:?}", callback_data);
// 3. Finish OAuth process by exchanging the code for a session token
let login_response = client
.oauth()
.finish_oauth(callback_data)
.await?;
println!("Successfully logged in!");
println!("Access Token: {}", login_response.access_token);
println!("User ID: {}", login_response.user_id);
println!("User Email: {}", login_response.user_email);
Ok(())
}

55
src/apis/auth.rs Normal file
View File

@@ -0,0 +1,55 @@
//! Authentication API
use crate::{
Client,
error::Result,
models::{
AuthStatusResponseDto, LoginCredentialDto, LoginResponseDto, LogoutResponseDto,
ValidateAccessTokenResponseDto,
},
};
/// API for authentication
#[derive(Debug, Clone)]
pub struct AuthApi {
client: Client,
}
impl AuthApi {
/// Create a new auth API instance
pub const fn new(client: Client) -> Self {
Self { client }
}
/// Login with username and password
pub async fn login(&self, credentials: LoginCredentialDto) -> Result<LoginResponseDto> {
let req = self.client.post("/auth/login").json(&credentials);
let response = self.client.execute(req.build()?).await?;
let login_response: LoginResponseDto = response.json().await?;
Ok(login_response)
}
/// Logout the current user and invalidate the session token
pub async fn logout(&self) -> Result<LogoutResponseDto> {
let req = self.client.post("/auth/logout");
let response = self.client.execute(req.build()?).await?;
let logout_response: LogoutResponseDto = response.json().await?;
Ok(logout_response)
}
/// Get information about the current session
pub async fn get_auth_status(&self) -> Result<AuthStatusResponseDto> {
let req = self.client.get("/auth/status");
let response = self.client.execute(req.build()?).await?;
let auth_status: AuthStatusResponseDto = response.json().await?;
Ok(auth_status)
}
/// Validate the current authorization method is still valid
pub async fn validate_access_token(&self) -> Result<ValidateAccessTokenResponseDto> {
let req = self.client.post("/auth/validateToken");
let response = self.client.execute(req.build()?).await?;
let validate_response: ValidateAccessTokenResponseDto = response.json().await?;
Ok(validate_response)
}
}

View File

@@ -5,6 +5,8 @@ pub mod assets;
pub mod search; pub mod search;
pub mod server; pub mod server;
pub mod timeline; pub mod timeline;
pub mod auth;
pub mod oauth;
// Re-export main API modules // Re-export main API modules
pub use albums::AlbumsApi; pub use albums::AlbumsApi;
@@ -12,3 +14,5 @@ pub use assets::AssetsApi;
pub use search::SearchApi; pub use search::SearchApi;
pub use server::ServerApi; pub use server::ServerApi;
pub use timeline::TimelineApi; pub use timeline::TimelineApi;
pub use auth::AuthApi;
pub use oauth::OAuthApi;

61
src/apis/oauth.rs Normal file
View File

@@ -0,0 +1,61 @@
//! OAuth API
use crate::{
Client,
error::Result,
models::{
OAuthAuthorizeResponseDto, OAuthCallbackDto, OAuthConfigDto, UserAdminResponseDto,
LoginResponseDto,
},
};
/// API for OAuth
#[derive(Debug, Clone)]
pub struct OAuthApi {
client: Client,
}
impl OAuthApi {
/// Create a new oauth API instance
pub const fn new(client: Client) -> Self {
Self { client }
}
/// Start OAuth authorization
pub async fn authorize(&self, config: OAuthConfigDto) -> Result<OAuthAuthorizeResponseDto> {
let req = self.client.post("/oauth/authorize").json(&config);
let response = self.client.execute(req.build()?).await?;
let auth_response: OAuthAuthorizeResponseDto = response.json().await?;
Ok(auth_response)
}
/// Finish OAuth authorization
pub async fn finish_oauth(&self, callback_data: OAuthCallbackDto) -> Result<LoginResponseDto> {
let req = self.client.post("/oauth/callback").json(&callback_data);
let response = self.client.execute(req.build()?).await?;
let login_response: LoginResponseDto = response.json().await?;
Ok(login_response)
}
/// Link an OAuth account
pub async fn link_oauth_account(&self, callback_data: OAuthCallbackDto) -> Result<UserAdminResponseDto> {
let req = self.client.post("/oauth/link").json(&callback_data);
let response = self.client.execute(req.build()?).await?;
let user_admin_response: UserAdminResponseDto = response.json().await?;
Ok(user_admin_response)
}
/// Redirect OAuth to mobile
pub async fn redirect_oauth_to_mobile(&self) -> Result<()> {
let req = self.client.get("/oauth/mobile-redirect");
self.client.execute(req.build()?).await?;
Ok(())
}
/// Unlink an OAuth account
pub async fn unlink_oauth_account(&self) -> Result<()> {
let req = self.client.post("/oauth/unlink");
self.client.execute(req.build()?).await?;
Ok(())
}
}

View File

@@ -1,6 +1,6 @@
//! Client for interacting with the Immich API //! Client for interacting with the Immich API
use crate::apis::{AlbumsApi, AssetsApi, SearchApi, ServerApi, TimelineApi}; use crate::apis::{AlbumsApi, AssetsApi, SearchApi, ServerApi, TimelineApi, AuthApi, OAuthApi};
use crate::error::{ImmichError, Result}; use crate::error::{ImmichError, Result};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -215,6 +215,16 @@ impl Client {
pub fn timeline(&self) -> TimelineApi { pub fn timeline(&self) -> TimelineApi {
TimelineApi::new(self.clone()) TimelineApi::new(self.clone())
} }
/// Access the auth API
pub fn auth(&self) -> AuthApi {
AuthApi::new(self.clone())
}
/// Access the oauth API
pub fn oauth(&self) -> OAuthApi {
OAuthApi::new(self.clone())
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -574,24 +574,131 @@ pub struct SearchResponse {
pub assets: SearchAssetResult, pub assets: SearchAssetResult,
} }
#[cfg(test)] #[derive(Debug, Clone, Serialize, Deserialize)]
mod tests { #[serde(rename_all = "camelCase")]
use super::*; pub struct LoginCredentialDto {
pub email: String,
#[test] pub password: String,
fn test_asset_type_serialization() { }
let asset_type = AssetType::Image;
let json = serde_json::to_string(&asset_type).unwrap(); #[derive(Debug, Clone, Serialize, Deserialize)]
assert_eq!(json, r#""IMAGE""#); #[serde(rename_all = "camelCase")]
} pub struct LoginResponseDto {
pub access_token: String,
#[test] pub is_admin: bool,
fn test_server_version_display() { pub is_onboarded: bool,
let version = ServerVersion { pub name: String,
major: 1, pub profile_image_path: String,
minor: 137, pub should_change_password: bool,
patch: 0, pub user_email: String,
}; pub user_id: UserId,
assert_eq!(version.to_string(), "1.137.0"); }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogoutResponseDto {
pub redirect_uri: String,
pub successful: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthStatusResponseDto {
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
pub is_elevated: bool,
pub password: bool,
pub pin_code: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub pin_expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidateAccessTokenResponseDto {
pub auth_status: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OAuthConfigDto {
#[serde(skip_serializing_if = "Option::is_none")]
pub code_challenge: Option<String>,
pub redirect_uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OAuthAuthorizeResponseDto {
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OAuthCallbackDto {
#[serde(skip_serializing_if = "Option::is_none")]
pub code_verifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserAdminResponseDto {
pub avatar_color: UserAvatarColor,
pub created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<DateTime<Utc>>,
pub email: String,
pub id: UserId,
pub is_admin: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<UserLicense>,
pub name: String,
pub oauth_id: String,
pub profile_changed_at: DateTime<Utc>,
pub profile_image_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub quota_size_in_bytes: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quota_usage_in_bytes: Option<i64>,
pub should_change_password: bool,
pub status: UserStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage_label: Option<String>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum UserAvatarColor {
Primary,
Pink,
Red,
Yellow,
Blue,
Green,
Purple,
Orange,
Gray,
Amber,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserLicense {
pub activated_at: DateTime<Utc>,
pub activation_key: String,
pub license_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum UserStatus {
Active,
Removing,
Deleted,
} }