mirror of
https://github.com/AppFlowy-IO/AppFlowy-Cloud.git
synced 2025-04-19 03:24:42 -04:00
feat: add endpoints to allow workspace owner to approve web page view request
This commit is contained in:
parent
b023e2f511
commit
813fa29253
23 changed files with 753 additions and 1 deletions
14
.sqlx/query-4476f271f4ea8c83428b4178c43ee2894e380a7c3ae3cbc782f438fabc45de8b.json
generated
Normal file
14
.sqlx/query-4476f271f4ea8c83428b4178c43ee2894e380a7c3ae3cbc782f438fabc45de8b.json
generated
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM af_access_request\n WHERE request_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4476f271f4ea8c83428b4178c43ee2894e380a7c3ae3cbc782f438fabc45de8b"
|
||||
}
|
25
.sqlx/query-598e731078fc6417039cc16772eb5bc6c74d24c1a8018a981d2175a483dc699c.json
generated
Normal file
25
.sqlx/query-598e731078fc6417039cc16772eb5bc6c74d24c1a8018a981d2175a483dc699c.json
generated
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO af_access_request (\n workspace_id,\n view_id,\n uid,\n status\n )\n VALUES ($1, $2, $3, $4)\n RETURNING request_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "request_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Int8",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "598e731078fc6417039cc16772eb5bc6c74d24c1a8018a981d2175a483dc699c"
|
||||
}
|
52
.sqlx/query-6317de690a65f0cb63e2f9d4889fc929f05df7ccfc0b7cb955787d0f88f91c45.json
generated
Normal file
52
.sqlx/query-6317de690a65f0cb63e2f9d4889fc929f05df7ccfc0b7cb955787d0f88f91c45.json
generated
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n request_id,\n view_id,\n (\n workspace_id,\n af_workspace.database_storage_id,\n af_workspace.owner_uid,\n owner_profile.name,\n af_workspace.created_at,\n af_workspace.workspace_type,\n af_workspace.deleted_at,\n af_workspace.workspace_name,\n af_workspace.icon\n ) AS \"workspace!: AFWorkspaceRow\",\n (\n af_user.uuid,\n af_user.email,\n af_user.name,\n af_user.metadata ->> 'avatar'\n ) AS \"requester!: AFAccessRequesterColumn\",\n status AS \"status: AFAccessRequestStatusColumn\",\n af_access_request.created_at AS created_at\n FROM af_access_request\n JOIN af_user USING (uid)\n JOIN af_workspace USING (workspace_id)\n JOIN af_user AS owner_profile ON af_workspace.owner_uid = owner_profile.uid\n WHERE request_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "request_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "view_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "workspace!: AFWorkspaceRow",
|
||||
"type_info": "Record"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "requester!: AFAccessRequesterColumn",
|
||||
"type_info": "Record"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "status: AFAccessRequestStatusColumn",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6317de690a65f0cb63e2f9d4889fc929f05df7ccfc0b7cb955787d0f88f91c45"
|
||||
}
|
15
.sqlx/query-95b1b405028c45c074121110d046f42f8229f150c2384671802ee7c1ef9e376d.json
generated
Normal file
15
.sqlx/query-95b1b405028c45c074121110d046f42f8229f150c2384671802ee7c1ef9e376d.json
generated
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE af_access_request\n SET status = $2\n WHERE request_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "95b1b405028c45c074121110d046f42f8229f150c2384671802ee7c1ef9e376d"
|
||||
}
|
|
@ -11,6 +11,7 @@ use appflowy_ai_client::error::AIError;
|
|||
use reqwest::StatusCode;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Error, Default)]
|
||||
pub enum AppError {
|
||||
|
@ -145,6 +146,12 @@ pub enum AppError {
|
|||
|
||||
#[error("{0}")]
|
||||
NotInviteeOfWorkspaceInvitation(String),
|
||||
|
||||
#[error("{0}")]
|
||||
MissingView(String),
|
||||
|
||||
#[error("There is existing access request for workspace {workspace_id} and view {view_id}")]
|
||||
AccessRequestAlreadyExists { workspace_id: Uuid, view_id: Uuid },
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
|
@ -212,6 +219,8 @@ impl AppError {
|
|||
AppError::InvalidPublishedOutline(_) => ErrorCode::InvalidPublishedOutline,
|
||||
AppError::InvalidFolderView(_) => ErrorCode::InvalidFolderView,
|
||||
AppError::NotInviteeOfWorkspaceInvitation(_) => ErrorCode::NotInviteeOfWorkspaceInvitation,
|
||||
AppError::MissingView(_) => ErrorCode::MissingView,
|
||||
AppError::AccessRequestAlreadyExists { .. } => ErrorCode::AccessRequestAlreadyExists,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -339,6 +348,8 @@ pub enum ErrorCode {
|
|||
InvalidPublishedOutline = 1039,
|
||||
InvalidFolderView = 1040,
|
||||
NotInviteeOfWorkspaceInvitation = 1041,
|
||||
MissingView = 1042,
|
||||
AccessRequestAlreadyExists = 1043,
|
||||
}
|
||||
|
||||
impl ErrorCode {
|
||||
|
|
76
libs/client-api/src/http_access_request.rs
Normal file
76
libs/client-api/src/http_access_request.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use client_api_entity::{
|
||||
access_request_dto::AccessRequest, AccessRequestMinimal, ApproveAccessRequestParams,
|
||||
CreateAccessRequestParams,
|
||||
};
|
||||
use reqwest::Method;
|
||||
use shared_entity::response::{AppResponse, AppResponseError};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::Client;
|
||||
|
||||
impl Client {
|
||||
pub async fn get_access_request(
|
||||
&self,
|
||||
access_request_id: Uuid,
|
||||
) -> Result<AccessRequest, AppResponseError> {
|
||||
let url = format!("{}/api/access-request/{}", self.base_url, access_request_id);
|
||||
let resp = self
|
||||
.http_client_with_auth(Method::GET, &url)
|
||||
.await?
|
||||
.send()
|
||||
.await?;
|
||||
AppResponse::<AccessRequest>::from_response(resp)
|
||||
.await?
|
||||
.into_data()
|
||||
}
|
||||
|
||||
pub async fn create_access_request(
|
||||
&self,
|
||||
data: CreateAccessRequestParams,
|
||||
) -> Result<AccessRequestMinimal, AppResponseError> {
|
||||
let url = format!("{}/api/access-request", self.base_url);
|
||||
let resp = self
|
||||
.http_client_with_auth(Method::POST, &url)
|
||||
.await?
|
||||
.json(&data)
|
||||
.send()
|
||||
.await?;
|
||||
AppResponse::<AccessRequestMinimal>::from_response(resp)
|
||||
.await?
|
||||
.into_data()
|
||||
}
|
||||
|
||||
pub async fn approve_access_request(
|
||||
&self,
|
||||
access_request_id: Uuid,
|
||||
) -> Result<(), AppResponseError> {
|
||||
let url = format!(
|
||||
"{}/api/access-request/{}/approve",
|
||||
self.base_url, access_request_id
|
||||
);
|
||||
let resp = self
|
||||
.http_client_with_auth(Method::POST, &url)
|
||||
.await?
|
||||
.json(&ApproveAccessRequestParams { is_approved: true })
|
||||
.send()
|
||||
.await?;
|
||||
AppResponse::<()>::from_response(resp).await?.into_error()
|
||||
}
|
||||
|
||||
pub async fn reject_access_request(
|
||||
&self,
|
||||
access_request_id: Uuid,
|
||||
) -> Result<(), AppResponseError> {
|
||||
let url = format!(
|
||||
"{}/api/access-request/{}/approve",
|
||||
self.base_url, access_request_id
|
||||
);
|
||||
let resp = self
|
||||
.http_client_with_auth(Method::POST, &url)
|
||||
.await?
|
||||
.json(&ApproveAccessRequestParams { is_approved: false })
|
||||
.send()
|
||||
.await?;
|
||||
AppResponse::<()>::from_response(resp).await?.into_error()
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ mod http;
|
|||
mod http_ai;
|
||||
mod http_billing;
|
||||
|
||||
mod http_access_request;
|
||||
mod http_blob;
|
||||
mod http_collab;
|
||||
mod http_history;
|
||||
|
|
|
@ -1238,6 +1238,51 @@ pub struct AvatarImageSource {
|
|||
pub file_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Copy, Clone)]
|
||||
#[repr(i32)]
|
||||
pub enum AccessRequestStatus {
|
||||
Pending = 0,
|
||||
Approved = 1,
|
||||
Rejected = 2,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AccessRequestWithViewId {
|
||||
pub request_id: Uuid,
|
||||
pub workspace: AFWorkspace,
|
||||
pub requester: AccessRequesterInfo,
|
||||
pub view_id: Uuid,
|
||||
pub status: AccessRequestStatus,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AccessRequesterInfo {
|
||||
pub uuid: Uuid,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AccessRequestMinimal {
|
||||
pub request_id: Uuid,
|
||||
pub workspace_id: Uuid,
|
||||
pub requester_id: Uuid,
|
||||
pub view_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CreateAccessRequestParams {
|
||||
pub workspace_id: Uuid,
|
||||
pub view_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ApproveAccessRequestParams {
|
||||
pub is_approved: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::dto::{CollabParams, CollabParamsV0};
|
||||
|
|
127
libs/database/src/access_request.rs
Normal file
127
libs/database/src/access_request.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
use crate::pg_row::{
|
||||
AFAccessRequestStatusColumn, AFAccessRequestWithViewIdColumn, AFAccessRequesterColumn,
|
||||
AFWorkspaceRow,
|
||||
};
|
||||
use app_error::AppError;
|
||||
use database_entity::dto::AccessRequestWithViewId;
|
||||
use sqlx::{Executor, Postgres};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn insert_new_access_request<'a, E: Executor<'a, Database = Postgres>>(
|
||||
executor: E,
|
||||
workspace_id: Uuid,
|
||||
view_id: Uuid,
|
||||
uid: i64,
|
||||
) -> Result<Uuid, AppError> {
|
||||
let request_id_result = sqlx::query_scalar!(
|
||||
r#"
|
||||
INSERT INTO af_access_request (
|
||||
workspace_id,
|
||||
view_id,
|
||||
uid,
|
||||
status
|
||||
)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING request_id
|
||||
"#,
|
||||
workspace_id,
|
||||
view_id,
|
||||
uid,
|
||||
AFAccessRequestStatusColumn::Pending as _,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await;
|
||||
match request_id_result {
|
||||
Err(e)
|
||||
if e
|
||||
.as_database_error()
|
||||
.map_or(false, |e| e.constraint().is_some()) =>
|
||||
{
|
||||
Err(AppError::AccessRequestAlreadyExists {
|
||||
workspace_id,
|
||||
view_id,
|
||||
})
|
||||
},
|
||||
Err(e) => Err(e.into()),
|
||||
Ok(request_id) => Ok(request_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn select_access_request_by_request_id<'a, E: Executor<'a, Database = Postgres>>(
|
||||
executor: E,
|
||||
request_id: Uuid,
|
||||
) -> Result<AccessRequestWithViewId, AppError> {
|
||||
let access_request = sqlx::query_as!(
|
||||
AFAccessRequestWithViewIdColumn,
|
||||
r#"
|
||||
SELECT
|
||||
request_id,
|
||||
view_id,
|
||||
(
|
||||
workspace_id,
|
||||
af_workspace.database_storage_id,
|
||||
af_workspace.owner_uid,
|
||||
owner_profile.name,
|
||||
af_workspace.created_at,
|
||||
af_workspace.workspace_type,
|
||||
af_workspace.deleted_at,
|
||||
af_workspace.workspace_name,
|
||||
af_workspace.icon
|
||||
) AS "workspace!: AFWorkspaceRow",
|
||||
(
|
||||
af_user.uuid,
|
||||
af_user.email,
|
||||
af_user.name,
|
||||
af_user.metadata ->> 'avatar'
|
||||
) AS "requester!: AFAccessRequesterColumn",
|
||||
status AS "status: AFAccessRequestStatusColumn",
|
||||
af_access_request.created_at AS created_at
|
||||
FROM af_access_request
|
||||
JOIN af_user USING (uid)
|
||||
JOIN af_workspace USING (workspace_id)
|
||||
JOIN af_user AS owner_profile ON af_workspace.owner_uid = owner_profile.uid
|
||||
WHERE request_id = $1
|
||||
"#,
|
||||
request_id,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
let access_request: AccessRequestWithViewId = access_request.try_into()?;
|
||||
Ok(access_request)
|
||||
}
|
||||
|
||||
pub async fn update_access_request_status<'a, E: Executor<'a, Database = Postgres>>(
|
||||
executor: E,
|
||||
request_id: Uuid,
|
||||
status: AFAccessRequestStatusColumn,
|
||||
) -> Result<(), AppError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE af_access_request
|
||||
SET status = $2
|
||||
WHERE request_id = $1
|
||||
"#,
|
||||
request_id,
|
||||
status as _,
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_access_request<'a, E: Executor<'a, Database = Postgres>>(
|
||||
executor: E,
|
||||
request_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM af_access_request
|
||||
WHERE request_id = $1
|
||||
"#,
|
||||
request_id,
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod access_request;
|
||||
pub mod chat;
|
||||
pub mod collab;
|
||||
pub mod file;
|
||||
|
|
|
@ -4,6 +4,7 @@ use chrono::{DateTime, Utc};
|
|||
|
||||
use database_entity::dto::{
|
||||
AFAccessLevel, AFRole, AFUserProfile, AFWebUser, AFWorkspace, AFWorkspaceInvitationStatus,
|
||||
AccessRequestMinimal, AccessRequestStatus, AccessRequestWithViewId, AccessRequesterInfo,
|
||||
AccountLink, GlobalComment, Reaction, Template, TemplateCategory, TemplateCategoryMinimal,
|
||||
TemplateCategoryType, TemplateCreator, TemplateCreatorMinimal, TemplateGroup, TemplateMinimal,
|
||||
};
|
||||
|
@ -12,7 +13,7 @@ use sqlx::FromRow;
|
|||
use uuid::Uuid;
|
||||
|
||||
/// Represent the row of the af_workspace table
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, sqlx::Type)]
|
||||
pub struct AFWorkspaceRow {
|
||||
pub workspace_id: Uuid,
|
||||
pub database_storage_id: Option<Uuid>,
|
||||
|
@ -502,3 +503,84 @@ impl From<AFTemplateGroupRow> for TemplateGroup {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Deserialize, Debug)]
|
||||
#[repr(i32)]
|
||||
pub enum AFAccessRequestStatusColumn {
|
||||
Pending = 0,
|
||||
Approved = 1,
|
||||
Rejected = 2,
|
||||
}
|
||||
|
||||
impl From<AFAccessRequestStatusColumn> for AccessRequestStatus {
|
||||
fn from(value: AFAccessRequestStatusColumn) -> Self {
|
||||
match value {
|
||||
AFAccessRequestStatusColumn::Pending => AccessRequestStatus::Pending,
|
||||
AFAccessRequestStatusColumn::Approved => AccessRequestStatus::Approved,
|
||||
AFAccessRequestStatusColumn::Rejected => AccessRequestStatus::Rejected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Debug)]
|
||||
pub struct AFAccessRequesterColumn {
|
||||
pub uuid: Uuid,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
impl From<AFAccessRequesterColumn> for AccessRequesterInfo {
|
||||
fn from(value: AFAccessRequesterColumn) -> Self {
|
||||
Self {
|
||||
uuid: value.uuid,
|
||||
name: value.name,
|
||||
email: value.email,
|
||||
avatar_url: value.avatar_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Debug)]
|
||||
pub struct AFAccessRequestMinimalColumn {
|
||||
pub request_id: Uuid,
|
||||
pub workspace_id: Uuid,
|
||||
pub requester_id: Uuid,
|
||||
pub view_id: Uuid,
|
||||
}
|
||||
|
||||
impl From<AFAccessRequestMinimalColumn> for AccessRequestMinimal {
|
||||
fn from(value: AFAccessRequestMinimalColumn) -> Self {
|
||||
Self {
|
||||
request_id: value.request_id,
|
||||
workspace_id: value.workspace_id,
|
||||
requester_id: value.requester_id,
|
||||
view_id: value.view_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AFAccessRequestWithViewIdColumn {
|
||||
pub request_id: Uuid,
|
||||
pub workspace: AFWorkspaceRow,
|
||||
pub requester: AccessRequesterInfo,
|
||||
pub view_id: Uuid,
|
||||
pub status: AFAccessRequestStatusColumn,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl TryFrom<AFAccessRequestWithViewIdColumn> for AccessRequestWithViewId {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: AFAccessRequestWithViewIdColumn) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
request_id: value.request_id,
|
||||
workspace: value.workspace.try_into()?,
|
||||
requester: value.requester,
|
||||
view_id: value.view_id,
|
||||
status: value.status.into(),
|
||||
created_at: value.created_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
23
libs/shared-entity/src/dto/access_request_dto.rs
Normal file
23
libs/shared-entity/src/dto/access_request_dto.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use database_entity::dto::{AFWorkspace, AccessRequestStatus, AccessRequesterInfo};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::workspace_dto::{ViewIcon, ViewLayout};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AccessRequestView {
|
||||
pub view_id: String,
|
||||
pub name: String,
|
||||
pub icon: Option<ViewIcon>,
|
||||
pub layout: ViewLayout,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AccessRequest {
|
||||
pub request_id: Uuid,
|
||||
pub workspace: AFWorkspace,
|
||||
pub requester: AccessRequesterInfo,
|
||||
pub view: AccessRequestView,
|
||||
pub status: AccessRequestStatus,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod access_request_dto;
|
||||
pub mod ai_dto;
|
||||
pub mod auth_dto;
|
||||
pub mod billing_dto;
|
||||
|
|
|
@ -158,6 +158,14 @@ pub struct FolderView {
|
|||
pub children: Vec<FolderView>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FolderViewMinimal {
|
||||
pub view_id: String,
|
||||
pub name: String,
|
||||
pub icon: Option<ViewIcon>,
|
||||
pub layout: ViewLayout,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SectionItems {
|
||||
pub views: Vec<FolderView>,
|
||||
|
|
13
migrations/20240924045045_access_request.sql
Normal file
13
migrations/20240924045045_access_request.sql
Normal file
|
@ -0,0 +1,13 @@
|
|||
CREATE TABLE IF NOT EXISTS af_access_request (
|
||||
request_id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
uid BIGINT NOT NULL REFERENCES af_user(uid) ON DELETE CASCADE,
|
||||
workspace_id UUID NOT NULL,
|
||||
view_id UUID NOT NULL,
|
||||
status INT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (uid, workspace_id, view_id),
|
||||
PRIMARY KEY(request_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_id_on_af_access_request ON af_access_request(workspace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_uid_on_af_access_request ON af_access_request(uid);
|
77
src/api/access_request.rs
Normal file
77
src/api/access_request.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use actix_web::{
|
||||
web::{self, Data, Json},
|
||||
Result, Scope,
|
||||
};
|
||||
use authentication::jwt::UserUuid;
|
||||
use database_entity::dto::{
|
||||
AccessRequestMinimal, ApproveAccessRequestParams, CreateAccessRequestParams,
|
||||
};
|
||||
use shared_entity::{
|
||||
dto::access_request_dto::AccessRequest,
|
||||
response::{AppResponse, JsonAppResponse},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
biz::access_request::ops::{
|
||||
approve_or_reject_access_request, create_access_request, get_access_request,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub fn access_request_scope() -> Scope {
|
||||
web::scope("/api/access-request")
|
||||
.service(web::resource("").route(web::post().to(post_access_request_handler)))
|
||||
.service(web::resource("/{request_id}").route(web::get().to(get_access_request_handler)))
|
||||
.service(
|
||||
web::resource("/{request_id}/approve")
|
||||
.route(web::post().to(post_approve_access_request_handler)),
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_access_request_handler(
|
||||
_uuid: UserUuid,
|
||||
access_request_id: web::Path<Uuid>,
|
||||
state: Data<AppState>,
|
||||
) -> Result<JsonAppResponse<AccessRequest>> {
|
||||
let access_request_id = access_request_id.into_inner();
|
||||
let access_request = get_access_request(
|
||||
&state.pg_pool,
|
||||
state.collab_access_control_storage.clone(),
|
||||
access_request_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(AppResponse::Ok().with_data(access_request)))
|
||||
}
|
||||
|
||||
async fn post_access_request_handler(
|
||||
uuid: UserUuid,
|
||||
create_access_request_params: Json<CreateAccessRequestParams>,
|
||||
state: Data<AppState>,
|
||||
) -> Result<JsonAppResponse<AccessRequestMinimal>> {
|
||||
let uid = state.user_cache.get_user_uid(&uuid).await?;
|
||||
let workspace_id = create_access_request_params.workspace_id;
|
||||
let view_id = create_access_request_params.view_id;
|
||||
let request_id = create_access_request(&state.pg_pool, workspace_id, view_id, uid).await?;
|
||||
let access_request = AccessRequestMinimal {
|
||||
request_id,
|
||||
workspace_id,
|
||||
requester_id: *uuid,
|
||||
view_id,
|
||||
};
|
||||
Ok(Json(AppResponse::Ok().with_data(access_request)))
|
||||
}
|
||||
|
||||
async fn post_approve_access_request_handler(
|
||||
uuid: UserUuid,
|
||||
access_request_id: web::Path<Uuid>,
|
||||
approve_access_request_params: Json<ApproveAccessRequestParams>,
|
||||
state: Data<AppState>,
|
||||
) -> Result<JsonAppResponse<()>> {
|
||||
let uid = state.user_cache.get_user_uid(&uuid).await?;
|
||||
let access_request_id = access_request_id.into_inner();
|
||||
let is_approved = approve_access_request_params.is_approved;
|
||||
approve_or_reject_access_request(&state.pg_pool, access_request_id, uid, *uuid, is_approved)
|
||||
.await?;
|
||||
Ok(Json(AppResponse::Ok()))
|
||||
}
|
|
@ -2,6 +2,7 @@ pub mod ai;
|
|||
pub mod chat;
|
||||
pub mod file_storage;
|
||||
|
||||
pub mod access_request;
|
||||
pub mod history;
|
||||
pub mod metrics;
|
||||
pub mod search;
|
||||
|
|
|
@ -42,6 +42,7 @@ use snowflake::Snowflake;
|
|||
use tonic_proto::history::history_client::HistoryClient;
|
||||
use workspace_access::WorkspaceAccessControlImpl;
|
||||
|
||||
use crate::api::access_request::access_request_scope;
|
||||
use crate::api::ai::ai_completion_scope;
|
||||
use crate::api::chat::chat_scope;
|
||||
use crate::api::file_storage::file_storage_scope;
|
||||
|
@ -169,6 +170,7 @@ pub async fn run_actix_server(
|
|||
.service(metrics_scope())
|
||||
.service(search_scope())
|
||||
.service(template_scope())
|
||||
.service(access_request_scope())
|
||||
.app_data(Data::new(state.metrics.registry.clone()))
|
||||
.app_data(Data::new(state.metrics.request_metrics.clone()))
|
||||
.app_data(Data::new(state.metrics.realtime_metrics.clone()))
|
||||
|
|
1
src/biz/access_request/mod.rs
Normal file
1
src/biz/access_request/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod ops;
|
105
src/biz/access_request/ops.rs
Normal file
105
src/biz/access_request/ops.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use std::{ops::DerefMut, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
use app_error::AppError;
|
||||
use appflowy_collaborate::collab::storage::CollabAccessControlStorage;
|
||||
use database::{
|
||||
access_request::{
|
||||
insert_new_access_request, select_access_request_by_request_id, update_access_request_status,
|
||||
},
|
||||
collab::GetCollabOrigin,
|
||||
pg_row::AFAccessRequestStatusColumn,
|
||||
workspace::upsert_workspace_member_with_txn,
|
||||
};
|
||||
use database_entity::dto::AFRole;
|
||||
use shared_entity::dto::access_request_dto::{AccessRequest, AccessRequestView};
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::biz::collab::{
|
||||
folder_view::{to_dto_view_icon, to_view_layout},
|
||||
ops::get_latest_collab_folder,
|
||||
};
|
||||
|
||||
pub async fn create_access_request(
|
||||
pg_pool: &PgPool,
|
||||
workspace_id: Uuid,
|
||||
view_id: Uuid,
|
||||
uid: i64,
|
||||
) -> Result<Uuid, AppError> {
|
||||
let request_id = insert_new_access_request(pg_pool, workspace_id, view_id, uid).await?;
|
||||
Ok(request_id)
|
||||
}
|
||||
|
||||
pub async fn get_access_request(
|
||||
pg_pool: &PgPool,
|
||||
collab_storage: Arc<CollabAccessControlStorage>,
|
||||
access_request_id: Uuid,
|
||||
) -> Result<AccessRequest, AppError> {
|
||||
let access_request_with_view_id =
|
||||
select_access_request_by_request_id(pg_pool, access_request_id).await?;
|
||||
let folder = get_latest_collab_folder(
|
||||
collab_storage,
|
||||
GetCollabOrigin::Server,
|
||||
&access_request_with_view_id
|
||||
.workspace
|
||||
.workspace_id
|
||||
.to_string(),
|
||||
)
|
||||
.await?;
|
||||
let view = folder.get_view(&access_request_with_view_id.view_id.to_string());
|
||||
let access_request_view = view
|
||||
.map(|v| AccessRequestView {
|
||||
view_id: v.id.clone(),
|
||||
name: v.name.clone(),
|
||||
icon: v.icon.as_ref().map(|icon| to_dto_view_icon(icon.clone())),
|
||||
layout: to_view_layout(&v.layout),
|
||||
})
|
||||
.ok_or(AppError::MissingView(format!(
|
||||
"the view {} is missing",
|
||||
access_request_with_view_id.view_id
|
||||
)))?;
|
||||
let access_request = AccessRequest {
|
||||
request_id: access_request_with_view_id.request_id,
|
||||
workspace: access_request_with_view_id.workspace,
|
||||
requester: access_request_with_view_id.requester,
|
||||
view: access_request_view,
|
||||
status: access_request_with_view_id.status,
|
||||
created_at: access_request_with_view_id.created_at,
|
||||
};
|
||||
Ok(access_request)
|
||||
}
|
||||
|
||||
pub async fn approve_or_reject_access_request(
|
||||
pg_pool: &PgPool,
|
||||
request_id: Uuid,
|
||||
uid: i64,
|
||||
user_uuid: Uuid,
|
||||
is_approved: bool,
|
||||
) -> Result<(), AppError> {
|
||||
let access_request = select_access_request_by_request_id(pg_pool, request_id).await?;
|
||||
if access_request.workspace.owner_uid != uid {
|
||||
return Err(AppError::NotEnoughPermissions {
|
||||
user: user_uuid.to_string(),
|
||||
action: "approve access request".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut txn = pg_pool.begin().await.context("approving request")?;
|
||||
let role = AFRole::Member;
|
||||
upsert_workspace_member_with_txn(
|
||||
&mut txn,
|
||||
&access_request.workspace.workspace_id,
|
||||
&access_request.requester.email,
|
||||
role,
|
||||
)
|
||||
.await?;
|
||||
let status = if is_approved {
|
||||
AFAccessRequestStatusColumn::Approved
|
||||
} else {
|
||||
AFAccessRequestStatusColumn::Rejected
|
||||
};
|
||||
update_access_request_status(txn.deref_mut(), request_id, status).await?;
|
||||
txn.commit().await.context("committing transaction")?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod access_request;
|
||||
pub mod chat;
|
||||
pub mod collab;
|
||||
pub mod pg_listener;
|
||||
|
|
70
tests/workspace/access_request.rs
Normal file
70
tests/workspace/access_request.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use app_error::ErrorCode;
|
||||
use client_api::entity::CreateAccessRequestParams;
|
||||
use client_api_test::generate_unique_registered_user_client;
|
||||
use shared_entity::dto::workspace_dto::ViewLayout;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn access_request_test() {
|
||||
let (owner_client, _) = generate_unique_registered_user_client().await;
|
||||
let workspaces = owner_client.get_workspaces().await.unwrap();
|
||||
let workspace_id = workspaces[0].workspace_id;
|
||||
let folder_view = owner_client
|
||||
.get_workspace_folder(&workspace_id.to_string(), Some(2), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let view_id = folder_view
|
||||
.children
|
||||
.into_iter()
|
||||
.find(|v| v.name == "General")
|
||||
.unwrap()
|
||||
.children
|
||||
.iter()
|
||||
.find(|v| v.name == "To-dos")
|
||||
.unwrap()
|
||||
.view_id
|
||||
.clone();
|
||||
let view_id = Uuid::parse_str(&view_id).unwrap();
|
||||
let data = CreateAccessRequestParams {
|
||||
workspace_id,
|
||||
view_id,
|
||||
};
|
||||
let (requester_client, requester) = generate_unique_registered_user_client().await;
|
||||
let access_request = requester_client
|
||||
.create_access_request(data.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let resp = requester_client.create_access_request(data).await;
|
||||
assert!(resp.is_err());
|
||||
assert_eq!(
|
||||
resp.unwrap_err().code,
|
||||
ErrorCode::AccessRequestAlreadyExists
|
||||
);
|
||||
let access_request_id = access_request.request_id;
|
||||
let access_request_to_be_approved = owner_client
|
||||
.get_access_request(access_request_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
access_request_to_be_approved.requester.email,
|
||||
requester.email
|
||||
);
|
||||
assert_eq!(
|
||||
access_request_to_be_approved.view.view_id,
|
||||
view_id.to_string()
|
||||
);
|
||||
assert_eq!(access_request_to_be_approved.view.layout, ViewLayout::Board);
|
||||
assert_eq!(
|
||||
access_request_to_be_approved.workspace.workspace_id,
|
||||
workspace_id
|
||||
);
|
||||
owner_client
|
||||
.approve_access_request(access_request_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let workspace_members = owner_client
|
||||
.get_workspace_members(workspace_id.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(workspace_members.iter().any(|m| m.email == requester.email));
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
mod access_request;
|
||||
mod default_user_workspace;
|
||||
mod edit_workspace;
|
||||
mod invitation_crud;
|
||||
|
|
Loading…
Add table
Reference in a new issue