Compare commits

1 Commits

Author SHA1 Message Date
075bc855fd Add auth and oauth endpoints
Some checks failed
Integration Tests / integration-test (pull_request) Failing after 1s
2026-05-05 06:08:00 +00:00
11 changed files with 333 additions and 311 deletions

View File

@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-action@stable
- name: Install Podman
run: |

1
Cargo.lock generated
View File

@@ -1016,7 +1016,6 @@ dependencies = [
"bytes",
"chrono",
"image",
"mime",
"reqwest",
"serde",
"serde_json",

View File

@@ -24,7 +24,6 @@ image = "0.25"
url = "2.5"
bytes = "1.10"
async-trait = "0.1"
mime = "0.3.17"
[dev-dependencies]
tokio-test = "0.4"

249
TODO.md
View File

@@ -1,249 +0,0 @@
# Immich SDK Implementation Progress
| Method | Path | Status |
| :--- | :--- | :--- |
| DELETE | /activities/{id} | Not Implemented |
| DELETE | /admin/database-backups | Not Implemented |
| DELETE | /admin/users/{id} | Not Implemented |
| DELETE | /albums/{id} | Implemented |
| DELETE | /albums/{id}/assets | Implemented |
| DELETE | /albums/{id}/user/{userId} | Not Implemented |
| DELETE | /api-keys/{id} | Not Implemented |
| DELETE | /assets | Implemented |
| DELETE | /assets/metadata | Not Implemented |
| DELETE | /assets/{id}/edits | Not Implemented |
| DELETE | /assets/{id}/metadata/{key} | Not Implemented |
| DELETE | /auth/pin-code | Not Implemented |
| DELETE | /duplicates | Not Implemented |
| DELETE | /duplicates/{id} | Not Implemented |
| DELETE | /faces/{id} | Not Implemented |
| DELETE | /libraries/{id} | Not Implemented |
| DELETE | /memories/{id} | Not Implemented |
| DELETE | /memories/{id}/assets | Not Implemented |
| DELETE | /notifications | Not Implemented |
| DELETE | /notifications/{id} | Not Implemented |
| DELETE | /partners/{id} | Not Implemented |
| DELETE | /people | Not Implemented |
| DELETE | /people/{id} | Not Implemented |
| DELETE | /queues/{name}/jobs | Not Implemented |
| DELETE | /server/license | Not Implemented |
| DELETE | /sessions | Not Implemented |
| DELETE | /sessions/{id} | Not Implemented |
| DELETE | /shared-links/{id} | Not Implemented |
| DELETE | /shared-links/{id}/assets | Not Implemented |
| DELETE | /stacks | Not Implemented |
| DELETE | /stacks/{id} | Not Implemented |
| DELETE | /stacks/{id}/assets/{assetId} | Not Implemented |
| DELETE | /sync/ack | Not Implemented |
| DELETE | /tags/{id} | Not Implemented |
| DELETE | /tags/{id}/assets | Not Implemented |
| DELETE | /users/me/license | Not Implemented |
| DELETE | /users/me/onboarding | Not Implemented |
| DELETE | /users/profile-image | Not Implemented |
| DELETE | /workflows/{id} | Not Implemented |
| GET | /activities | Not Implemented |
| GET | /activities/statistics | Not Implemented |
| GET | /admin/database-backups | Not Implemented |
| GET | /admin/database-backups/{filename} | Not Implemented |
| GET | /admin/maintenance/detect-install | Not Implemented |
| GET | /admin/maintenance/status | Not Implemented |
| GET | /admin/users | Not Implemented |
| GET | /admin/users/{id} | Not Implemented |
| GET | /admin/users/{id}/preferences | Not Implemented |
| GET | /admin/users/{id}/sessions | Not Implemented |
| GET | /admin/users/{id}/statistics | Not Implemented |
| GET | /albums | Implemented |
| GET | /albums/statistics | Not Implemented |
| GET | /albums/{id} | Implemented |
| GET | /api-keys | Not Implemented |
| GET | /api-keys/me | Not Implemented |
| GET | /api-keys/{id} | Not Implemented |
| GET | /assets/device/{deviceId} | Not Implemented |
| GET | /assets/random | Not Implemented |
| GET | /assets/statistics | Not Implemented |
| GET | /assets/{id} | Not Implemented |
| GET | /assets/{id}/edits | Not Implemented |
| GET | /assets/{id}/metadata | Not Implemented |
| GET | /assets/{id}/metadata/{key} | Not Implemented |
| GET | /assets/{id}/ocr | Not Implemented |
| GET | /assets/{id}/original | Not Implemented |
| GET | /assets/{id}/thumbnail | Not Implemented |
| GET | /assets/{id}/video/playback | Not Implemented |
| GET | /auth/status | Not Implemented |
| GET | /duplicates | Not Implemented |
| GET | /faces | Not Implemented |
| GET | /jobs | Not Implemented |
| GET | /libraries | Not Implemented |
| GET | /libraries/{id} | Not Implemented |
| GET | /libraries/{id}/statistics | Not Implemented |
| GET | /map/markers | Not Implemented |
| GET | /map/reverse-geocode | Not Implemented |
| GET | /memories | Not Implemented |
| GET | /memories/statistics | Not Implemented |
| GET | /memories/{id} | Not Implemented |
| GET | /notifications | Not Implemented |
| GET | /notifications/{id} | Not Implemented |
| GET | /oauth/mobile-redirect | Not Implemented |
| GET | /partners | Not Implemented |
| GET | /people | Not Implemented |
| GET | /people/{id} | Not Implemented |
| GET | /people/{id}/statistics | Not Implemented |
| GET | /people/{id}/thumbnail | Not Implemented |
| GET | /plugins | Not Implemented |
| GET | /plugins/triggers | Not Implemented |
| GET | /plugins/{id} | Not Implemented |
| GET | /queues | Not Implemented |
| GET | /queues/{name} | Not Implemented |
| GET | /queues/{name}/jobs | Not Implemented |
| GET | /search/cities | Not Implemented |
| GET | /search/explore | Not Implemented |
| GET | /search/person | Not Implemented |
| GET | /search/places | Not Implemented |
| GET | /search/suggestions | Not Implemented |
| GET | /server/about | Implemented |
| GET | /server/apk-links | Implemented |
| GET | /server/config | Implemented |
| GET | /server/features | Implemented |
| GET | /server/license | Implemented |
| GET | /server/media-types | Implemented |
| GET | /server/ping | Implemented |
| GET | /server/statistics | Implemented |
| GET | /server/storage | Implemented |
| GET | /server/theme | Implemented |
| GET | /server/version | Implemented |
| GET | /server/version-check | Implemented |
| GET | /server/version-history | Implemented |
| GET | /sessions | Not Implemented |
| GET | /shared-links | Not Implemented |
| GET | /shared-links/me | Not Implemented |
| GET | /shared-links/{id} | Not Implemented |
| GET | /stacks | Not Implemented |
| GET | /stacks/{id} | Not Implemented |
| GET | /sync/ack | Not Implemented |
| GET | /system-config | Not Implemented |
| GET | /system-config/defaults | Not Implemented |
| GET | /system-config/storage-template-options | Not Implemented |
| GET | /system-metadata/admin-onboarding | Not Implemented |
| GET | /system-metadata/reverse-geocoding-state | Not Implemented |
| GET | /system-metadata/version-check-state | Not Implemented |
| GET | /tags | Not Implemented |
| GET | /tags/{id} | Not Implemented |
| GET | /timeline/bucket | Implemented (Candidate) |
| GET | /timeline/buckets | Implemented (Candidate) |
| GET | /users | Not Implemented |
| GET | /users/me | Not Implemented |
| GET | /users/me/license | Not Implemented |
| GET | /users/me/onboarding | Not Implemented |
| GET | /users/me/preferences | Not Implemented |
| GET | /users/{id} | Not Implemented |
| GET | /users/{id}/profile-image | Not Implemented |
| GET | /view/folder | Not Implemented |
| GET | /view/folder/unique-paths | Not Implemented |
| GET | /workflows | Not Implemented |
| GET | /workflows/{id} | Not Implemented |
| PATCH | /albums/{id} | Implemented |
| PATCH | /shared-links/{id} | Not Implemented |
| POST | /activities | Not Implemented |
| POST | /admin/auth/unlink-all | Not Implemented |
| POST | /admin/database-backups/start-restore | Not Implemented |
| POST | /admin/database-backups/upload | Not Implemented |
| POST | /admin/maintenance | Not Implemented |
| POST | /admin/maintenance/login | Not Implemented |
| POST | /admin/notifications | Not Implemented |
| POST | /admin/notifications/templates/{name} | Not Implemented |
| POST | /admin/notifications/test-email | Not Implemented |
| POST | /admin/users | Not Implemented |
| POST | /admin/users/{id}/restore | Not Implemented |
| POST | /albums | Implemented |
| POST | /api-keys | Not Implemented |
| POST | /assets | Implemented |
| POST | /assets/bulk-upload-check | Not Implemented |
| POST | /assets/exist | Not Implemented |
| POST | /assets/jobs | Not Implemented |
| POST | /auth/admin-sign-up | Not Implemented |
| POST | /auth/change-password | Not Implemented |
| POST | /auth/login | Not Implemented |
| POST | /auth/logout | Not Implemented |
| POST | /auth/pin-code | Not Implemented |
| POST | /auth/session/lock | Not Implemented |
| POST | /auth/session/unlock | Not Implemented |
| POST | /auth/validateToken | Not Implemented |
| POST | /download/archive | Not Implemented |
| POST | /download/info | Not Implemented |
| POST | /duplicates/resolve | Not Implemented |
| POST | /faces | Not Implemented |
| POST | /jobs | Not Implemented |
| POST | /libraries | Not Implemented |
| POST | /libraries/{id}/scan | Not Implemented |
| POST | /libraries/{id}/validate | Not Implemented |
| POST | /memories | Not Implemented |
| POST | /oauth/authorize | Not Implemented |
| POST | /oauth/callback | Not Implemented |
| POST | /oauth/link | Not Implemented |
| POST | /oauth/unlink | Not Implemented |
| POST | /partners | Not Implemented |
| POST | /partners/{id} | Not Implemented |
| POST | /people | Not Implemented |
| POST | /people/{id}/merge | Not Implemented |
| POST | /search/large-assets | Not Implemented |
| POST | /search/metadata | Not Implemented |
| POST | /search/random | Not Implemented |
| POST | /search/smart | Not Implemented |
| POST | /search/statistics | Not Implemented |
| POST | /sessions | Not Implemented |
| POST | /sessions/{id}/lock | Not Implemented |
| POST | /shared-links | Not Implemented |
| POST | /shared-links/login | Not Implemented |
| POST | /stacks | Not Implemented |
| POST | /sync/ack | Not Implemented |
| POST | /sync/delta-sync | Not Implemented |
| POST | /sync/full-sync | Not Implemented |
| POST | /sync/stream | Not Implemented |
| POST | /system-metadata/admin-onboarding | Not Implemented |
| POST | /tags | Not Implemented |
| POST | /trash/empty | Not Implemented |
| POST | /trash/restore | Not Implemented |
| POST | /trash/restore/assets | Not Implemented |
| POST | /users/profile-image | Not Implemented |
| POST | /workflows | Not Implemented |
| PUT | /admin/users/{id} | Not Implemented |
| PUT | /admin/users/{id}/preferences | Not Implemented |
| PUT | /albums/assets | Implemented |
| PUT | /albums/{id}/assets | Implemented |
| PUT | /albums/{id}/user/{userId} | Not Implemented |
| PUT | /albums/{id}/users | Not Implemented |
| PUT | /api-keys/{id} | Not Implemented |
| PUT | /assets | Implemented |
| PUT | /assets/copy | Not Implemented |
| PUT | /assets/metadata | Not Implemented |
| PUT | /assets/{id} | Implemented (Candidate) |
| PUT | /assets/{id}/edits | Not Implemented |
| PUT | /assets/{id}/metadata | Not Implemented |
| PUT | /assets/{id}/original | Not Implemented |
| PUT | /auth/pin-code | Not Implemented |
| PUT | /faces/{id} | Not Implemented |
| PUT | /jobs/{name} | Not Implemented |
| PUT | /libraries/{id} | Not Implemented |
| PUT | /memories/{id} | Not Implemented |
| PUT | /memories/{id}/assets | Not Implemented |
| PUT | /notifications | Not Implemented |
| PUT | /notifications/{id} | Not Implemented |
| PUT | /partners/{id} | Not Implemented |
| PUT | /people | Not Implemented |
| PUT | /people/{id} | Not Implemented |
| PUT | /people/{id}/reassign | Not Implemented |
| PUT | /queues/{name} | Not Implemented |
| PUT | /server/license | Implemented |
| PUT | /sessions/{id} | Not Implemented |
| PUT | /shared-links/{id}/assets | Not Implemented |
| PUT | /stacks/{id} | Not Implemented |
| PUT | /system-config | Not Implemented |
| PUT | /tags | Not Implemented |
| PUT | /tags/assets | Not Implemented |
| PUT | /tags/{id} | Not Implemented |
| PUT | /tags/{id}/assets | Not Implemented |
| PUT | /users/me | Not Implemented |
| PUT | /users/me/license | Not Implemented |
| PUT | /users/me/onboarding | Not Implemented |
| PUT | /users/me/preferences | Not Implemented |
| PUT | /workflows/{id} | Not Implemented |

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(())
}

View File

@@ -1,10 +1,7 @@
//! Assets API - Manage photos and videos
use std::io::Cursor;
use std::path::Path;
use std::{io::Cursor, str::FromStr};
use mime::Mime;
use reqwest::header::CONTENT_TYPE;
use crate::{
Client,
@@ -17,17 +14,17 @@ use crate::{
/// Response from downloading a thumbnail containing image data and metadata
#[derive(Debug, Clone)]
pub struct AssetDownload {
/// The asset data as bytes
pub struct ThumbnailResponse {
/// The image data as bytes
pub data: bytes::Bytes,
/// The MIME type of the asset (e.g., "image/jpeg", "image/png", "image/webp")
pub mime: Mime,
/// The MIME type of the image (e.g., "image/jpeg", "image/png", "image/webp")
pub content_type: String,
}
impl AssetDownload {
impl ThumbnailResponse {
/// Get the file extension based on content type
pub fn extension(&self) -> Option<&str> {
match self.mime.essence_str() {
match self.content_type.as_str() {
"image/jpeg" | "image/jpg" => Some("jpg"),
"image/png" => Some("png"),
"image/webp" => Some("webp"),
@@ -38,8 +35,8 @@ impl AssetDownload {
}
/// Get the content type
pub fn content_type(&self) -> &Mime {
&self.mime
pub fn content_type(&self) -> &str {
&self.content_type
}
/// Get the data
@@ -55,7 +52,7 @@ impl AssetDownload {
/// Decode the image data into a DynamicImage
///
/// # Errors
/// Returns an error if the asset is not an image, content type is not supported, or if decoding fails
/// Returns an error if the content type is not supported or if decoding fails
///
/// # Example
/// ```rust,ignore
@@ -72,8 +69,8 @@ impl AssetDownload {
/// ```
pub fn decode(&self) -> Result<image::DynamicImage> {
// Get image format from content type
let format = image::ImageFormat::from_mime_type(&self.mime).ok_or_else(|| {
ImmichError::Image(format!("Unsupported content type: {}", self.mime))
let format = image::ImageFormat::from_mime_type(&self.content_type).ok_or_else(|| {
ImmichError::Image(format!("Unsupported content type: {}", self.content_type))
})?;
// Create reader with the format and decode
@@ -306,10 +303,7 @@ impl UploadAssetBuilder {
// Add file timestamps (required by Immich API v2)
let now = chrono::Utc::now().to_rfc3339();
form = form.text(
"fileCreatedAt",
self.file_created_at.unwrap_or_else(|| now.clone()),
);
form = form.text("fileCreatedAt", self.file_created_at.unwrap_or_else(|| now.clone()));
form = form.text("fileModifiedAt", self.file_modified_at.unwrap_or(now));
let req = self.client.post("/assets").multipart(form);
@@ -392,7 +386,7 @@ impl DownloadAssetBuilder {
}
/// Execute the download
pub async fn execute(self) -> Result<AssetDownload> {
pub async fn execute(self) -> Result<bytes::Bytes> {
let path = format!("/assets/{}/original", self.id);
let mut req = self.client.get(&path);
@@ -401,18 +395,8 @@ impl DownloadAssetBuilder {
}
let response = self.client.execute(req.build()?).await?;
// Get content type from response headers
let mime = response
.headers()
.get(CONTENT_TYPE)
.and_then(|ct| ct.to_str().ok())
.and_then(|ct| Mime::from_str(ct).ok())
.ok_or(ImmichError::Image("Missing or invalid content-type".into()))?;
let data = response.bytes().await?;
Ok(AssetDownload { data, mime })
let bytes = response.bytes().await?;
Ok(bytes)
}
}
@@ -449,7 +433,7 @@ impl ThumbnailBuilder {
}
/// Execute the request
pub async fn execute(self) -> Result<AssetDownload> {
pub async fn execute(self) -> Result<ThumbnailResponse> {
let path = format!("/assets/{}/thumbnail", self.id);
let mut req = self.client.get(&path);
@@ -470,15 +454,15 @@ impl ThumbnailBuilder {
let response = self.client.execute(req.build()?).await?;
// Get content type from response headers
let mime = response
let content_type = response
.headers()
.get(CONTENT_TYPE)
.get("content-type")
.and_then(|ct| ct.to_str().ok())
.and_then(|ct| Mime::from_str(ct).ok())
.ok_or(ImmichError::Image("Missing or invalid content-type".into()))?;
.map(String::from)
.unwrap_or_else(|| "application/octet-stream".to_string());
let data = response.bytes().await?;
Ok(AssetDownload { data, mime })
Ok(ThumbnailResponse { data, content_type })
}
}

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 server;
pub mod timeline;
pub mod auth;
pub mod oauth;
// Re-export main API modules
pub use albums::AlbumsApi;
@@ -12,3 +14,5 @@ pub use assets::AssetsApi;
pub use search::SearchApi;
pub use server::ServerApi;
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
use crate::apis::{AlbumsApi, AssetsApi, SearchApi, ServerApi, TimelineApi};
use crate::apis::{AlbumsApi, AssetsApi, SearchApi, ServerApi, TimelineApi, AuthApi, OAuthApi};
use crate::error::{ImmichError, Result};
use std::sync::Arc;
use std::time::Duration;
@@ -215,6 +215,16 @@ impl Client {
pub fn timeline(&self) -> TimelineApi {
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)]

View File

@@ -574,24 +574,131 @@ pub struct SearchResponse {
pub assets: SearchAssetResult,
}
#[cfg(test)]
mod tests {
use super::*;
#[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)]
#[serde(rename_all = "camelCase")]
pub struct LoginCredentialDto {
pub email: String,
pub password: String,
}
#[test]
fn test_server_version_display() {
let version = ServerVersion {
major: 1,
minor: 137,
patch: 0,
};
assert_eq!(version.to_string(), "1.137.0");
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoginResponseDto {
pub access_token: String,
pub is_admin: bool,
pub is_onboarded: bool,
pub name: String,
pub profile_image_path: String,
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,
}