Compare commits
3 Commits
feat/auth
...
bcf28b35c7
| Author | SHA1 | Date | |
|---|---|---|---|
| bcf28b35c7 | |||
| 2c30e59ac5 | |||
| a2095b7daa |
2
.github/workflows/integration-test.yml
vendored
2
.github/workflows/integration-test.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-action@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
- name: Install Podman
|
- name: Install Podman
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
//! 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,6 @@ 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;
|
||||||
@@ -14,5 +12,3 @@ 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;
|
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
//! 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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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, AuthApi, OAuthApi};
|
use crate::apis::{AlbumsApi, AssetsApi, SearchApi, ServerApi, TimelineApi};
|
||||||
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,16 +215,6 @@ 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)]
|
||||||
|
|||||||
@@ -574,131 +574,24 @@ pub struct SearchResponse {
|
|||||||
pub assets: SearchAssetResult,
|
pub assets: SearchAssetResult,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[cfg(test)]
|
||||||
#[serde(rename_all = "camelCase")]
|
mod tests {
|
||||||
pub struct LoginCredentialDto {
|
use super::*;
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
#[test]
|
||||||
|
fn test_asset_type_serialization() {
|
||||||
|
let asset_type = AssetType::Image;
|
||||||
|
let json = serde_json::to_string(&asset_type).unwrap();
|
||||||
|
assert_eq!(json, r#""IMAGE""#);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[test]
|
||||||
#[serde(rename_all = "camelCase")]
|
fn test_server_version_display() {
|
||||||
pub struct LoginResponseDto {
|
let version = ServerVersion {
|
||||||
pub access_token: String,
|
major: 1,
|
||||||
pub is_admin: bool,
|
minor: 137,
|
||||||
pub is_onboarded: bool,
|
patch: 0,
|
||||||
pub name: String,
|
};
|
||||||
pub profile_image_path: String,
|
assert_eq!(version.to_string(), "1.137.0");
|
||||||
pub should_change_password: bool,
|
|
||||||
pub user_email: String,
|
|
||||||
pub user_id: UserId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user