feat(api): expand API endpoints for repo, PR, user, workspace management

- Add git operation endpoints: archive, compare branches, diff, tree,
  repository extras
- Add repo endpoints: contributors, delete fork, get branch/commit
  status/deploy key/invitation/member/release/tag/webhook, topics,
  release assets, webhook deliveries/retry
- Add PR endpoints: review requests, templates
- Add user endpoints: block/unblock, follow/unfollow, presence,
  personal access tokens, account restore
- Add workspace endpoints: billing history, approvals, domains,
  integrations, invitations, members, webhooks, restore
- Add internal API, notification API, IM API modules
- Update route configuration and OpenAPI spec
This commit is contained in:
zhenyi
2026-06-10 18:49:27 +08:00
parent 4586b79cb8
commit cec6dce955
161 changed files with 7522 additions and 349 deletions
+29
View File
@@ -0,0 +1,29 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
/// Clear all notifications (dismiss all)
#[utoipa::path(
delete,
path = "/api/v1/notifications",
tag = "Notifications",
operation_id = "notificationClearAll",
responses(
(status = 200, description = "All notifications cleared", body = ApiResponse<i64>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn clear_all_notifications(
service: web::Data<AppService>,
session: Session,
) -> Result<HttpResponse, AppError> {
let result = service.notify.clear_all_notifications(&session).await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+40
View File
@@ -0,0 +1,40 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationBlock;
use crate::service::AppService;
use crate::service::notify::blocks::CreateBlockParams;
use crate::session::Session;
/// Create a notification block
#[utoipa::path(
post,
path = "/api/v1/notifications/blocks",
tag = "Notifications",
operation_id = "notificationCreateBlock",
request_body(
content = CreateBlockParams,
description = "Block creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Block created", body = ApiResponse<NotificationBlock>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn create_block(
service: web::Data<AppService>,
session: Session,
params: web::Json<CreateBlockParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.create_block(&session, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+40
View File
@@ -0,0 +1,40 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationSubscription;
use crate::service::AppService;
use crate::service::notify::subscriptions::CreateSubscriptionParams;
use crate::session::Session;
/// Create a notification subscription
#[utoipa::path(
post,
path = "/api/v1/notifications/subscriptions",
tag = "Notifications",
operation_id = "notificationCreateSubscription",
request_body(
content = CreateSubscriptionParams,
description = "Subscription creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Subscription created", body = ApiResponse<NotificationSubscription>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn create_subscription(
service: web::Data<AppService>,
session: Session,
params: web::Json<CreateSubscriptionParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.create_subscription(&session, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+41
View File
@@ -0,0 +1,41 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationTemplate;
use crate::service::AppService;
use crate::service::notify::templates::CreateTemplateParams;
use crate::session::Session;
/// Create a notification template (requires system admin)
#[utoipa::path(
post,
path = "/api/v1/notifications/templates",
tag = "Notifications",
operation_id = "notificationCreateTemplate",
request_body(
content = CreateTemplateParams,
description = "Template creation parameters",
content_type = "application/json"
),
responses(
(status = 201, description = "Template created", body = ApiResponse<NotificationTemplate>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "System admin access required", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn create_template(
service: web::Data<AppService>,
session: Session,
params: web::Json<CreateTemplateParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.create_template(&session, params.into_inner())
.await?;
Ok(HttpResponse::Created().json(ApiResponse::new(result)))
}
+39
View File
@@ -0,0 +1,39 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub block_id: uuid::Uuid,
}
/// Delete a notification block
#[utoipa::path(
delete,
path = "/api/v1/notifications/blocks/{block_id}",
tag = "Notifications",
operation_id = "notificationDeleteBlock",
params(PathParams),
responses(
(status = 200, description = "Block deleted", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Block not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn delete_block(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service.notify.delete_block(&session, path.block_id).await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Block deleted".to_string())))
}
+42
View File
@@ -0,0 +1,42 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub notification_id: uuid::Uuid,
}
/// Delete a notification
#[utoipa::path(
delete,
path = "/api/v1/notifications/{notification_id}",
tag = "Notifications",
operation_id = "notificationDelete",
params(PathParams),
responses(
(status = 200, description = "Notification deleted", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Notification not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn delete_notification(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.notify
.delete_notification(&session, path.notification_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Notification deleted".to_string())))
}
+42
View File
@@ -0,0 +1,42 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub subscription_id: uuid::Uuid,
}
/// Delete a notification subscription
#[utoipa::path(
delete,
path = "/api/v1/notifications/subscriptions/{subscription_id}",
tag = "Notifications",
operation_id = "notificationDeleteSubscription",
params(PathParams),
responses(
(status = 200, description = "Subscription deleted", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Subscription not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn delete_subscription(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.notify
.delete_subscription(&session, path.subscription_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Subscription deleted".to_string())))
}
+43
View File
@@ -0,0 +1,43 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub template_id: uuid::Uuid,
}
/// Delete a notification template (requires system admin)
#[utoipa::path(
delete,
path = "/api/v1/notifications/templates/{template_id}",
tag = "Notifications",
operation_id = "notificationDeleteTemplate",
params(PathParams),
responses(
(status = 200, description = "Template deleted", body = ApiResponse<String>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "System admin access required", body = ApiErrorResponse),
(status = 404, description = "Template not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn delete_template(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
service
.notify
.delete_template(&session, path.template_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new("Template deleted".to_string())))
}
+66
View File
@@ -0,0 +1,66 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::base_info;
use crate::models::notifications::NotificationDetail;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub notification_id: uuid::Uuid,
}
/// Dismiss a notification
#[utoipa::path(
post,
path = "/api/v1/notifications/{notification_id}/dismiss",
tag = "Notifications",
operation_id = "notificationDismiss",
params(PathParams),
responses(
(status = 200, description = "Notification dismissed", body = ApiResponse<NotificationDetail>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Notification not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn dismiss_notification(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let notification = service
.notify
.dismiss_notification(&session, path.notification_id)
.await?;
let actor = match notification.actor_id {
Some(id) => base_info::resolve_users(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
let workspace = match notification.workspace_id {
Some(id) => base_info::resolve_workspaces(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
let repo = match notification.repo_id {
Some(id) => base_info::resolve_repos(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
Ok(HttpResponse::Ok().json(ApiResponse::new(
notification.into_detail(actor, workspace, repo),
)))
}
+44
View File
@@ -0,0 +1,44 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationTemplate;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub template_id: uuid::Uuid,
}
/// Get a notification template by ID (requires system admin)
#[utoipa::path(
get,
path = "/api/v1/notifications/templates/{template_id}",
tag = "Notifications",
operation_id = "notificationGetTemplate",
params(PathParams),
responses(
(status = 200, description = "Template retrieved", body = ApiResponse<NotificationTemplate>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "System admin access required", body = ApiErrorResponse),
(status = 404, description = "Template not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn get_template(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.get_template(&session, path.template_id)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+29
View File
@@ -0,0 +1,29 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
/// Get unread notification count for the current user
#[utoipa::path(
get,
path = "/api/v1/notifications/unread-count",
tag = "Notifications",
operation_id = "notificationUnreadCount",
responses(
(status = 200, description = "Unread count returned successfully", body = ApiResponse<i64>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn get_unread_count(
service: web::Data<AppService>,
session: Session,
) -> Result<HttpResponse, AppError> {
let result = service.notify.count_unread(&session).await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+47
View File
@@ -0,0 +1,47 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationBlock;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List notification blocks for the current user
#[utoipa::path(
get,
path = "/api/v1/notifications/blocks",
tag = "Notifications",
operation_id = "notificationListBlocks",
params(QueryParams),
responses(
(status = 200, description = "Blocks listed successfully", body = ApiResponse<Vec<NotificationBlock>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_blocks(
service: web::Data<AppService>,
session: Session,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.list_blocks(
&session,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+47
View File
@@ -0,0 +1,47 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationDelivery;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List notification deliveries for the current user
#[utoipa::path(
get,
path = "/api/v1/notifications/deliveries",
tag = "Notifications",
operation_id = "notificationListDeliveries",
params(QueryParams),
responses(
(status = 200, description = "Deliveries listed successfully", body = ApiResponse<Vec<NotificationDelivery>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_deliveries(
service: web::Data<AppService>,
session: Session,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.list_deliveries(
&session,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
@@ -0,0 +1,55 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationDelivery;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub notification_id: uuid::Uuid,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List deliveries for a specific notification
#[utoipa::path(
get,
path = "/api/v1/notifications/{notification_id}/deliveries",
tag = "Notifications",
operation_id = "notificationListDeliveriesForNotification",
params(PathParams, QueryParams),
responses(
(status = 200, description = "Deliveries listed successfully", body = ApiResponse<Vec<NotificationDelivery>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Notification not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_deliveries_for_notification(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.list_deliveries_for_notification(
&session,
path.notification_id,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+72
View File
@@ -0,0 +1,72 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::base_info;
use crate::models::notifications::NotificationDetail;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub unread_only: Option<bool>,
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List notifications for the current user
#[utoipa::path(
get,
path = "/api/v1/notifications",
tag = "Notifications",
operation_id = "notificationList",
params(QueryParams),
responses(
(status = 200, description = "Notifications listed successfully", body = ApiResponse<Vec<NotificationDetail>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_notifications(
service: web::Data<AppService>,
session: Session,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let notifications = service
.notify
.list_notifications(
&session,
query.unread_only.unwrap_or(false),
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
let actor_ids: Vec<_> = notifications.iter().filter_map(|n| n.actor_id).collect();
let workspace_ids: Vec<_> = notifications
.iter()
.filter_map(|n| n.workspace_id)
.collect();
let repo_ids: Vec<_> = notifications.iter().filter_map(|n| n.repo_id).collect();
let actors = base_info::resolve_users(&service.ctx.db, &actor_ids).await?;
let workspaces = base_info::resolve_workspaces(&service.ctx.db, &workspace_ids).await?;
let repos = base_info::resolve_repos(&service.ctx.db, &repo_ids).await?;
let details: Vec<NotificationDetail> = notifications
.into_iter()
.map(|n| {
let actor = n.actor_id.and_then(|id| actors.get(&id).cloned());
let workspace = n.workspace_id.and_then(|id| workspaces.get(&id).cloned());
let repo = n.repo_id.and_then(|id| repos.get(&id).cloned());
n.into_detail(actor, workspace, repo)
})
.collect();
Ok(HttpResponse::Ok().json(ApiResponse::new(details)))
}
+47
View File
@@ -0,0 +1,47 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationSubscription;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List notification subscriptions for the current user
#[utoipa::path(
get,
path = "/api/v1/notifications/subscriptions",
tag = "Notifications",
operation_id = "notificationListSubscriptions",
params(QueryParams),
responses(
(status = 200, description = "Subscriptions listed successfully", body = ApiResponse<Vec<NotificationSubscription>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_subscriptions(
service: web::Data<AppService>,
session: Session,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.list_subscriptions(
&session,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+48
View File
@@ -0,0 +1,48 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationTemplate;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct QueryParams {
pub limit: Option<i64>,
pub offset: Option<i64>,
}
/// List notification templates (requires system admin)
#[utoipa::path(
get,
path = "/api/v1/notifications/templates",
tag = "Notifications",
operation_id = "notificationListTemplates",
params(QueryParams),
responses(
(status = 200, description = "Templates listed successfully", body = ApiResponse<Vec<NotificationTemplate>>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "System admin access required", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn list_templates(
service: web::Data<AppService>,
session: Session,
query: web::Query<QueryParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.list_templates(
&session,
query.limit.unwrap_or(50),
query.offset.unwrap_or(0),
)
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+29
View File
@@ -0,0 +1,29 @@
use actix_web::{HttpResponse, web};
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::service::AppService;
use crate::session::Session;
/// Mark all notifications as read
#[utoipa::path(
put,
path = "/api/v1/notifications/read-all",
tag = "Notifications",
operation_id = "notificationMarkAllAsRead",
responses(
(status = 200, description = "All notifications marked as read", body = ApiResponse<i64>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn mark_all_as_read(
service: web::Data<AppService>,
session: Session,
) -> Result<HttpResponse, AppError> {
let result = service.notify.mark_all_as_read(&session).await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+66
View File
@@ -0,0 +1,66 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::base_info;
use crate::models::notifications::NotificationDetail;
use crate::service::AppService;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub notification_id: uuid::Uuid,
}
/// Mark a notification as read
#[utoipa::path(
put,
path = "/api/v1/notifications/{notification_id}/read",
tag = "Notifications",
operation_id = "notificationMarkAsRead",
params(PathParams),
responses(
(status = 200, description = "Notification marked as read", body = ApiResponse<NotificationDetail>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Notification not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn mark_as_read(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
) -> Result<HttpResponse, AppError> {
let notification = service
.notify
.mark_as_read(&session, path.notification_id)
.await?;
let actor = match notification.actor_id {
Some(id) => base_info::resolve_users(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
let workspace = match notification.workspace_id {
Some(id) => base_info::resolve_workspaces(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
let repo = match notification.repo_id {
Some(id) => base_info::resolve_repos(&service.ctx.db, &[id])
.await?
.remove(&id),
None => None,
};
Ok(HttpResponse::Ok().json(ApiResponse::new(
notification.into_detail(actor, workspace, repo),
)))
}
+108
View File
@@ -0,0 +1,108 @@
pub mod clear_all_notifications;
pub mod create_block;
pub mod create_subscription;
pub mod create_template;
pub mod delete_block;
pub mod delete_notification;
pub mod delete_subscription;
pub mod delete_template;
pub mod dismiss_notification;
pub mod get_template;
pub mod get_unread_count;
pub mod list_blocks;
pub mod list_deliveries;
pub mod list_deliveries_for_notification;
pub mod list_notifications;
pub mod list_subscriptions;
pub mod list_templates;
pub mod mark_all_as_read;
pub mod mark_as_read;
pub mod update_subscription;
pub mod update_template;
use actix_web::web;
/// Configure notification routes under `/api/v1/notifications`
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/notifications")
// Non-parameterized paths first
.route("", web::get().to(list_notifications::list_notifications))
.route(
"",
web::delete().to(clear_all_notifications::clear_all_notifications),
)
.route(
"/unread-count",
web::get().to(get_unread_count::get_unread_count),
)
.route(
"/read-all",
web::put().to(mark_all_as_read::mark_all_as_read),
)
// Parameterized notification paths
.route(
"/{notification_id}",
web::delete().to(delete_notification::delete_notification),
)
.route(
"/{notification_id}/read",
web::put().to(mark_as_read::mark_as_read),
)
.route(
"/{notification_id}/dismiss",
web::post().to(dismiss_notification::dismiss_notification),
)
.route(
"/{notification_id}/deliveries",
web::get().to(list_deliveries_for_notification::list_deliveries_for_notification),
)
// Subscriptions
.route(
"/subscriptions",
web::get().to(list_subscriptions::list_subscriptions),
)
.route(
"/subscriptions",
web::post().to(create_subscription::create_subscription),
)
.route(
"/subscriptions/{subscription_id}",
web::put().to(update_subscription::update_subscription),
)
.route(
"/subscriptions/{subscription_id}",
web::delete().to(delete_subscription::delete_subscription),
)
// Blocks
.route("/blocks", web::get().to(list_blocks::list_blocks))
.route("/blocks", web::post().to(create_block::create_block))
.route(
"/blocks/{block_id}",
web::delete().to(delete_block::delete_block),
)
// Deliveries
.route(
"/deliveries",
web::get().to(list_deliveries::list_deliveries),
)
// Templates
.route("/templates", web::get().to(list_templates::list_templates))
.route(
"/templates",
web::post().to(create_template::create_template),
)
.route(
"/templates/{template_id}",
web::get().to(get_template::get_template),
)
.route(
"/templates/{template_id}",
web::put().to(update_template::update_template),
)
.route(
"/templates/{template_id}",
web::delete().to(delete_template::delete_template),
),
);
}
+50
View File
@@ -0,0 +1,50 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationSubscription;
use crate::service::AppService;
use crate::service::notify::subscriptions::UpdateSubscriptionParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub subscription_id: uuid::Uuid,
}
/// Update a notification subscription
#[utoipa::path(
put,
path = "/api/v1/notifications/subscriptions/{subscription_id}",
tag = "Notifications",
operation_id = "notificationUpdateSubscription",
params(PathParams),
request_body(
content = UpdateSubscriptionParams,
description = "Subscription update parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Subscription updated", body = ApiResponse<NotificationSubscription>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 404, description = "Subscription not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn update_subscription(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateSubscriptionParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.update_subscription(&session, path.subscription_id, params.into_inner())
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}
+51
View File
@@ -0,0 +1,51 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::api::response::{ApiErrorResponse, ApiResponse};
use crate::error::AppError;
use crate::models::notifications::NotificationTemplate;
use crate::service::AppService;
use crate::service::notify::templates::UpdateTemplateParams;
use crate::session::Session;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PathParams {
pub template_id: uuid::Uuid,
}
/// Update a notification template (requires system admin)
#[utoipa::path(
put,
path = "/api/v1/notifications/templates/{template_id}",
tag = "Notifications",
operation_id = "notificationUpdateTemplate",
params(PathParams),
request_body(
content = UpdateTemplateParams,
description = "Template update parameters",
content_type = "application/json"
),
responses(
(status = 200, description = "Template updated", body = ApiResponse<NotificationTemplate>),
(status = 401, description = "Authentication required or session expired", body = ApiErrorResponse),
(status = 403, description = "System admin access required", body = ApiErrorResponse),
(status = 404, description = "Template not found", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
),
security(
("session_cookie" = [])
)
)]
pub async fn update_template(
service: web::Data<AppService>,
session: Session,
path: web::Path<PathParams>,
params: web::Json<UpdateTemplateParams>,
) -> Result<HttpResponse, AppError> {
let result = service
.notify
.update_template(&session, path.template_id, params.into_inner())
.await?;
Ok(HttpResponse::Ok().json(ApiResponse::new(result)))
}