feat(contacts): add API to address-books, contacts, groups
* full gateway to /carddav * add system readonly address-book issued from OxiCloud users
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
use crate::domain::entities::contact::{Address, Contact, ContactGroup, Email, Phone};
|
use crate::domain::entities::contact::{Address, Contact, ContactGroup, Email, Phone};
|
||||||
use chrono::{DateTime, NaiveDate, Utc};
|
use chrono::{DateTime, NaiveDate, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct EmailDto {
|
pub struct EmailDto {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
@@ -19,7 +20,7 @@ impl From<Email> for EmailDto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct PhoneDto {
|
pub struct PhoneDto {
|
||||||
pub number: String,
|
pub number: String,
|
||||||
pub r#type: String,
|
pub r#type: String,
|
||||||
@@ -36,7 +37,7 @@ impl From<Phone> for PhoneDto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct AddressDto {
|
pub struct AddressDto {
|
||||||
pub street: Option<String>,
|
pub street: Option<String>,
|
||||||
pub city: Option<String>,
|
pub city: Option<String>,
|
||||||
@@ -61,7 +62,7 @@ impl From<Address> for AddressDto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct ContactDto {
|
pub struct ContactDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub address_book_id: String,
|
pub address_book_id: String,
|
||||||
@@ -181,7 +182,7 @@ pub struct CreateContactVCardDto {
|
|||||||
pub user_id: String, // User creating the contact
|
pub user_id: String, // User creating the contact
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct ContactGroupDto {
|
pub struct ContactGroupDto {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub address_book_id: String,
|
pub address_book_id: String,
|
||||||
|
|||||||
@@ -42,10 +42,11 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::database_error(format!("Failed to create address book: {}", e)))?;
|
.map_err(|e| DomainError::database_error(format!("Failed to create address book: {}", e)))?;
|
||||||
|
|
||||||
|
let owner_id: Uuid = row.get("owner_id");
|
||||||
Ok(AddressBook::from_raw(
|
Ok(AddressBook::from_raw(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
row.get("name"),
|
row.get("name"),
|
||||||
row.get("owner_id"),
|
owner_id.to_string(),
|
||||||
row.get("description"),
|
row.get("description"),
|
||||||
row.get("color"),
|
row.get("color"),
|
||||||
row.get("is_public"),
|
row.get("is_public"),
|
||||||
@@ -79,10 +80,11 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
DomainError::database_error(format!("Failed to update address book: {}", e))
|
DomainError::database_error(format!("Failed to update address book: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let owner_id: Uuid = row.get("owner_id");
|
||||||
Ok(AddressBook::from_raw(
|
Ok(AddressBook::from_raw(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
row.get("name"),
|
row.get("name"),
|
||||||
row.get("owner_id"),
|
owner_id.to_string(),
|
||||||
row.get("description"),
|
row.get("description"),
|
||||||
row.get("color"),
|
row.get("color"),
|
||||||
row.get("is_public"),
|
row.get("is_public"),
|
||||||
@@ -127,10 +129,11 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let result = maybe_row.map(|row| {
|
let result = maybe_row.map(|row| {
|
||||||
|
let owner_id: Uuid = row.get("owner_id");
|
||||||
AddressBook::from_raw(
|
AddressBook::from_raw(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
row.get("name"),
|
row.get("name"),
|
||||||
row.get("owner_id"),
|
owner_id.to_string(),
|
||||||
row.get("description"),
|
row.get("description"),
|
||||||
row.get("color"),
|
row.get("color"),
|
||||||
row.get("is_public"),
|
row.get("is_public"),
|
||||||
@@ -164,10 +167,11 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
let result = rows
|
let result = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
|
let owner_id: Uuid = row.get("owner_id");
|
||||||
AddressBook::from_raw(
|
AddressBook::from_raw(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
row.get("name"),
|
row.get("name"),
|
||||||
row.get("owner_id"),
|
owner_id.to_string(),
|
||||||
row.get("description"),
|
row.get("description"),
|
||||||
row.get("color"),
|
row.get("color"),
|
||||||
row.get("is_public"),
|
row.get("is_public"),
|
||||||
@@ -201,10 +205,11 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
let result = rows
|
let result = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
|
let owner_id: Uuid = row.get("owner_id");
|
||||||
AddressBook::from_raw(
|
AddressBook::from_raw(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
row.get("name"),
|
row.get("name"),
|
||||||
row.get("owner_id"),
|
owner_id.to_string(),
|
||||||
row.get("description"),
|
row.get("description"),
|
||||||
row.get("color"),
|
row.get("color"),
|
||||||
row.get("is_public"),
|
row.get("is_public"),
|
||||||
@@ -235,10 +240,11 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
let result = rows
|
let result = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
|
let owner_id: Uuid = row.get("owner_id");
|
||||||
AddressBook::from_raw(
|
AddressBook::from_raw(
|
||||||
row.get("id"),
|
row.get("id"),
|
||||||
row.get("name"),
|
row.get("name"),
|
||||||
row.get("owner_id"),
|
owner_id.to_string(),
|
||||||
row.get("description"),
|
row.get("description"),
|
||||||
row.get("color"),
|
row.get("color"),
|
||||||
row.get("is_public"),
|
row.get("is_public"),
|
||||||
@@ -317,7 +323,10 @@ impl AddressBookRepository for AddressBookPgRepository {
|
|||||||
|
|
||||||
let result = rows
|
let result = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| (row.get("user_id"), row.get("can_write")))
|
.map(|row| {
|
||||||
|
let user_id: Uuid = row.get("user_id");
|
||||||
|
(user_id.to_string(), row.get("can_write"))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ pub mod batch_handler;
|
|||||||
pub mod caldav_handler;
|
pub mod caldav_handler;
|
||||||
pub mod carddav_handler;
|
pub mod carddav_handler;
|
||||||
pub mod chunked_upload_handler;
|
pub mod chunked_upload_handler;
|
||||||
|
pub mod contacts_handler;
|
||||||
pub mod dedup_handler;
|
pub mod dedup_handler;
|
||||||
pub mod device_auth_handler;
|
pub mod device_auth_handler;
|
||||||
pub mod favorites_handler;
|
pub mod favorites_handler;
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ pub use routes::create_public_api_routes;
|
|||||||
|
|
||||||
use utoipa::OpenApi;
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
use crate::application::dtos::contact_dto::{
|
||||||
|
AddressDto, ContactDto, ContactGroupDto, EmailDto, PhoneDto,
|
||||||
|
};
|
||||||
use crate::application::dtos::favorites_dto::{
|
use crate::application::dtos::favorites_dto::{
|
||||||
BatchFavoritesResult, BatchFavoritesStats, FavoriteItemDto,
|
BatchFavoritesResult, BatchFavoritesStats, FavoriteItemDto,
|
||||||
};
|
};
|
||||||
@@ -41,6 +44,10 @@ use crate::application::ports::chunked_upload_ports::{
|
|||||||
use crate::interfaces::api::handlers::chunked_upload_handler::{
|
use crate::interfaces::api::handlers::chunked_upload_handler::{
|
||||||
CompleteUploadResponse, CreateUploadRequest,
|
CompleteUploadResponse, CreateUploadRequest,
|
||||||
};
|
};
|
||||||
|
use crate::interfaces::api::handlers::contacts_handler::{
|
||||||
|
AddMemberRequest, AddressBookResponse, CreateAddressBookRequest, CreateContactRequest,
|
||||||
|
GroupNameRequest, UpdateAddressBookRequest, UpdateContactRequest,
|
||||||
|
};
|
||||||
use crate::interfaces::api::handlers::dedup_handler::{
|
use crate::interfaces::api::handlers::dedup_handler::{
|
||||||
DedupUploadResponse, HashCheckResponse, StatsResponse,
|
DedupUploadResponse, HashCheckResponse, StatsResponse,
|
||||||
};
|
};
|
||||||
@@ -147,6 +154,24 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
|||||||
handlers::music_handler::remove_share,
|
handlers::music_handler::remove_share,
|
||||||
handlers::music_handler::get_playlist_shares,
|
handlers::music_handler::get_playlist_shares,
|
||||||
handlers::music_handler::get_audio_metadata,
|
handlers::music_handler::get_audio_metadata,
|
||||||
|
// Contacts / address-book handlers (free functions)
|
||||||
|
handlers::contacts_handler::list_address_books,
|
||||||
|
handlers::contacts_handler::create_address_book,
|
||||||
|
handlers::contacts_handler::update_address_book,
|
||||||
|
handlers::contacts_handler::delete_address_book,
|
||||||
|
handlers::contacts_handler::list_contacts,
|
||||||
|
handlers::contacts_handler::create_contact,
|
||||||
|
handlers::contacts_handler::get_contact,
|
||||||
|
handlers::contacts_handler::update_contact,
|
||||||
|
handlers::contacts_handler::delete_contact,
|
||||||
|
handlers::contacts_handler::list_groups,
|
||||||
|
handlers::contacts_handler::create_group,
|
||||||
|
handlers::contacts_handler::get_group,
|
||||||
|
handlers::contacts_handler::update_group,
|
||||||
|
handlers::contacts_handler::delete_group,
|
||||||
|
handlers::contacts_handler::list_contacts_in_group,
|
||||||
|
handlers::contacts_handler::add_contact_to_group,
|
||||||
|
handlers::contacts_handler::remove_contact_from_group,
|
||||||
// Admin handlers (pub free functions)
|
// Admin handlers (pub free functions)
|
||||||
handlers::admin_handler::get_dashboard_stats,
|
handlers::admin_handler::get_dashboard_stats,
|
||||||
handlers::admin_handler::list_users,
|
handlers::admin_handler::list_users,
|
||||||
@@ -231,6 +256,19 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
|||||||
HashCheckResponse,
|
HashCheckResponse,
|
||||||
DedupUploadResponse,
|
DedupUploadResponse,
|
||||||
StatsResponse,
|
StatsResponse,
|
||||||
|
// Contacts / address-book schemas
|
||||||
|
AddressBookResponse,
|
||||||
|
CreateAddressBookRequest,
|
||||||
|
UpdateAddressBookRequest,
|
||||||
|
ContactDto,
|
||||||
|
ContactGroupDto,
|
||||||
|
EmailDto,
|
||||||
|
PhoneDto,
|
||||||
|
AddressDto,
|
||||||
|
CreateContactRequest,
|
||||||
|
UpdateContactRequest,
|
||||||
|
GroupNameRequest,
|
||||||
|
AddMemberRequest,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
tags(
|
tags(
|
||||||
@@ -247,6 +285,7 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
|||||||
(name = "dedup", description = "Content deduplication endpoints"),
|
(name = "dedup", description = "Content deduplication endpoints"),
|
||||||
(name = "batch", description = "Batch operation endpoints"),
|
(name = "batch", description = "Batch operation endpoints"),
|
||||||
(name = "playlists", description = "Music playlist endpoints"),
|
(name = "playlists", description = "Music playlist endpoints"),
|
||||||
|
(name = "contacts", description = "Address books, contacts, and groups endpoints"),
|
||||||
(name = "admin", description = "Admin management endpoints"),
|
(name = "admin", description = "Admin management endpoints"),
|
||||||
),
|
),
|
||||||
info(
|
info(
|
||||||
|
|||||||
@@ -392,6 +392,67 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
|||||||
tracing::info!("Music routes initialized");
|
tracing::info!("Music routes initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REST browse API for CardDAV contacts, groups, and OxiCloud users.
|
||||||
|
// Write operations and protocol sync remain on the /carddav endpoint.
|
||||||
|
if let Some(contact_service) = app_state.contact_use_case.clone() {
|
||||||
|
use crate::interfaces::api::handlers::contacts_handler::{self, ContactsApiState};
|
||||||
|
|
||||||
|
let auth_svc = app_state
|
||||||
|
.auth_service
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.auth_application_service.clone());
|
||||||
|
|
||||||
|
let contacts_state = ContactsApiState {
|
||||||
|
contact_service,
|
||||||
|
auth_service: auth_svc,
|
||||||
|
};
|
||||||
|
|
||||||
|
let contacts_router = Router::new()
|
||||||
|
.route(
|
||||||
|
"/",
|
||||||
|
get(contacts_handler::list_address_books)
|
||||||
|
.post(contacts_handler::create_address_book),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{book_id}",
|
||||||
|
put(contacts_handler::update_address_book)
|
||||||
|
.delete(contacts_handler::delete_address_book),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{book_id}/contacts",
|
||||||
|
get(contacts_handler::list_contacts).post(contacts_handler::create_contact),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{book_id}/contacts/{contact_id}",
|
||||||
|
get(contacts_handler::get_contact)
|
||||||
|
.put(contacts_handler::update_contact)
|
||||||
|
.delete(contacts_handler::delete_contact),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{book_id}/groups",
|
||||||
|
get(contacts_handler::list_groups).post(contacts_handler::create_group),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{book_id}/groups/{group_id}",
|
||||||
|
get(contacts_handler::get_group)
|
||||||
|
.put(contacts_handler::update_group)
|
||||||
|
.delete(contacts_handler::delete_group),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{book_id}/groups/{group_id}/contacts",
|
||||||
|
get(contacts_handler::list_contacts_in_group)
|
||||||
|
.post(contacts_handler::add_contact_to_group),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{book_id}/groups/{group_id}/contacts/{contact_id}",
|
||||||
|
delete(contacts_handler::remove_contact_from_group),
|
||||||
|
)
|
||||||
|
.with_state(contacts_state);
|
||||||
|
|
||||||
|
router = router.nest("/address-books", contacts_router);
|
||||||
|
tracing::info!("Contacts REST API routes initialized");
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: WebDAV routes are mounted at top-level (/webdav) in main.rs
|
// NOTE: WebDAV routes are mounted at top-level (/webdav) in main.rs
|
||||||
// for client compatibility, NOT under /api.
|
// for client compatibility, NOT under /api.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user