feat: api to get invitation code info

This commit is contained in:
khorshuheng 2025-04-15 20:38:35 +08:00
parent 9ea3c46c26
commit 553d7101bd
10 changed files with 193 additions and 3 deletions

View file

@ -0,0 +1,59 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH invited_workspace_member AS (\n SELECT\n invite_code,\n COUNT(*) AS member_count,\n COUNT(CASE WHEN uid = $2 THEN uid END) > 0 AS is_member\n FROM af_workspace_invite_code\n JOIN af_workspace_member USING (workspace_id)\n WHERE invite_code = $1\n AND (expires_at IS NULL OR expires_at < NOW())\n GROUP BY invite_code\n )\n SELECT\n workspace_id,\n owner_profile.name AS \"owner_name!\",\n owner_profile.metadata ->> 'icon_url' AS owner_avatar,\n af_workspace.workspace_name AS \"workspace_name!\",\n af_workspace.icon AS workspace_icon_url,\n invited_workspace_member.member_count AS \"member_count!\",\n invited_workspace_member.is_member AS \"is_member!\"\n FROM af_workspace_invite_code\n JOIN af_workspace USING (workspace_id)\n JOIN af_user AS owner_profile ON af_workspace.owner_uid = owner_profile.uid\n JOIN invited_workspace_member USING (invite_code)\n WHERE invite_code = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "owner_name!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "owner_avatar",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "workspace_name!",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "workspace_icon_url",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "member_count!",
"type_info": "Int8"
},
{
"ordinal": 6,
"name": "is_member!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Int8"
]
},
"nullable": [
false,
false,
null,
true,
false,
null,
null
]
},
"hash": "7c2d481530566aec6a6acf3f705d2a0ac6cd94c013c1639aa23f6401281b3fd9"
}

View file

@ -193,6 +193,9 @@ pub enum AppError {
#[error("{0}")]
FeatureNotAvailable(String),
#[error("unable to find invitation code")]
InvalidInvitationCode,
}
impl AppError {
@ -276,6 +279,7 @@ impl AppError {
AppError::ActionTimeout(_) => ErrorCode::ActionTimeout,
AppError::InvalidBlock(_) => ErrorCode::InvalidBlock,
AppError::FeatureNotAvailable(_) => ErrorCode::FeatureNotAvailable,
AppError::InvalidInvitationCode => ErrorCode::InvalidInvitationCode,
}
}
}
@ -453,6 +457,7 @@ pub enum ErrorCode {
RequestTimeout = 1065,
AIResponseError = 1066,
FeatureNotAvailable = 1067,
InvalidInvitationCode = 1068,
}
impl ErrorCode {

View file

@ -9,6 +9,8 @@ use client_api_entity::workspace_dto::TrashSectionItems;
use client_api_entity::workspace_dto::{FolderView, QueryWorkspaceFolder, QueryWorkspaceParam};
use client_api_entity::AuthProvider;
use client_api_entity::CollabType;
use client_api_entity::GetInvitationCodeInfoQuery;
use client_api_entity::InvitationCodeInfo;
use client_api_entity::InvitedWorkspace;
use client_api_entity::JoinWorkspaceByInviteCodeParams;
use client_api_entity::WorkspaceInviteCodeParams;
@ -814,6 +816,22 @@ impl Client {
process_response_data::<InvitedWorkspace>(resp).await
}
pub async fn get_invitation_code_info(
&self,
invitation_code: &str,
) -> Result<InvitationCodeInfo, AppResponseError> {
let url = format!("{}/api/invite-code-info", self.base_url);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.query(&GetInvitationCodeInfoQuery {
code: invitation_code.to_string(),
})
.send()
.await?;
process_response_data::<InvitationCodeInfo>(resp).await
}
pub async fn create_workspace_invitation_code(
&self,
workspace_id: &Uuid,

View file

@ -1247,6 +1247,22 @@ pub struct InvitedWorkspace {
pub workspace_id: Uuid,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetInvitationCodeInfoQuery {
pub code: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct InvitationCodeInfo {
pub workspace_id: Uuid,
pub workspace_name: String,
pub owner_avatar: Option<String>,
pub owner_name: String,
pub workspace_icon_url: Option<String>,
pub is_member: Option<bool>,
pub member_count: i64,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct JoinWorkspaceByInviteCodeParams {
pub code: String,

View file

@ -1,7 +1,7 @@
use chrono::{DateTime, Utc};
use database_entity::dto::{
AFRole, AFWorkspaceInvitation, AFWorkspaceInvitationStatus, AFWorkspaceSettings, GlobalComment,
Reaction,
InvitationCodeInfo, Reaction,
};
use futures_util::stream::BoxStream;
use sqlx::{types::uuid, Executor, PgPool, Postgres, Transaction};
@ -1530,6 +1530,48 @@ pub async fn select_invited_workspace_id(
Ok(res)
}
pub async fn select_invitation_code_info<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
invite_code: &str,
uid: i64,
) -> Result<Vec<InvitationCodeInfo>, AppError> {
let info_list = sqlx::query_as!(
InvitationCodeInfo,
r#"
WITH invited_workspace_member AS (
SELECT
invite_code,
COUNT(*) AS member_count,
COUNT(CASE WHEN uid = $2 THEN uid END) > 0 AS is_member
FROM af_workspace_invite_code
JOIN af_workspace_member USING (workspace_id)
WHERE invite_code = $1
AND (expires_at IS NULL OR expires_at < NOW())
GROUP BY invite_code
)
SELECT
workspace_id,
owner_profile.name AS "owner_name!",
owner_profile.metadata ->> 'icon_url' AS owner_avatar,
af_workspace.workspace_name AS "workspace_name!",
af_workspace.icon AS workspace_icon_url,
invited_workspace_member.member_count AS "member_count!",
invited_workspace_member.is_member AS "is_member!"
FROM af_workspace_invite_code
JOIN af_workspace USING (workspace_id)
JOIN af_user AS owner_profile ON af_workspace.owner_uid = owner_profile.uid
JOIN invited_workspace_member USING (invite_code)
WHERE invite_code = $1
"#,
invite_code,
uid
)
.fetch_all(executor)
.await?;
Ok(info_list)
}
pub async fn upsert_workspace_member_uid<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
workspace_id: &Uuid,

24
src/api/invite_code.rs Normal file
View file

@ -0,0 +1,24 @@
use actix_web::{
web::{self, Data, Json},
Result, Scope,
};
use authentication::jwt::UserUuid;
use database_entity::dto::{GetInvitationCodeInfoQuery, InvitationCodeInfo};
use shared_entity::response::{AppResponse, JsonAppResponse};
use crate::{biz::workspace::invite::get_invitation_code_info, state::AppState};
pub fn invite_code_scope() -> Scope {
web::scope("/api/invite-code-info")
.service(web::resource("").route(web::get().to(get_invite_code_info_handler)))
}
async fn get_invite_code_info_handler(
user_uuid: UserUuid,
query: web::Query<GetInvitationCodeInfoQuery>,
state: Data<AppState>,
) -> Result<JsonAppResponse<InvitationCodeInfo>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let info = get_invitation_code_info(&state.pg_pool, &query.code, uid).await?;
Ok(Json(AppResponse::Ok().with_data(info)))
}

View file

@ -3,6 +3,7 @@ pub mod ai;
pub mod chat;
pub mod data_import;
pub mod file_storage;
pub mod invite_code;
pub mod metrics;
pub mod search;
pub mod server_info;

View file

@ -55,6 +55,7 @@ use crate::api::ai::ai_completion_scope;
use crate::api::chat::chat_scope;
use crate::api::data_import::data_import_scope;
use crate::api::file_storage::file_storage_scope;
use crate::api::invite_code::invite_code_scope;
use crate::api::metrics::metrics_scope;
use crate::api::search::search_scope;
use crate::api::server_info::server_info_scope;
@ -152,6 +153,7 @@ pub async fn run_actix_server(
.service(server_info_scope())
.service(user_scope())
.service(workspace_scope())
.service(invite_code_scope())
.service(collab_scope())
.service(ws_scope())
.service(file_storage_scope())

View file

@ -1,12 +1,13 @@
use app_error::AppError;
use database::workspace::{
insert_workspace_invite_code, select_invited_workspace_id, upsert_workspace_member_uid,
insert_workspace_invite_code, select_invitation_code_info, select_invited_workspace_id,
upsert_workspace_member_uid,
};
use rand::{distributions::Alphanumeric, Rng};
use sqlx::PgPool;
use uuid::Uuid;
use database_entity::dto::{AFRole, WorkspaceInviteToken};
use database_entity::dto::{AFRole, InvitationCodeInfo, WorkspaceInviteToken};
const INVITE_LINK_CODE_LENGTH: usize = 16;
@ -40,3 +41,15 @@ pub async fn join_workspace_invite_by_code(
upsert_workspace_member_uid(pg_pool, &invited_workspace_id, uid, AFRole::Member).await?;
Ok(invited_workspace_id)
}
pub async fn get_invitation_code_info(
pg_pool: &PgPool,
invitation_code: &str,
uid: i64,
) -> Result<InvitationCodeInfo, AppError> {
let info_list = select_invitation_code_info(pg_pool, invitation_code, uid).await?;
info_list
.into_iter()
.next()
.ok_or(AppError::InvalidInvitationCode)
}

View file

@ -17,6 +17,16 @@ async fn join_workspace_by_invite_code() {
.await
.unwrap()
.code;
let invitation_code_info = invitee_client
.get_invitation_code_info(&invitation_code)
.await
.unwrap();
assert_eq!(invitation_code_info.is_member, Some(false));
assert_eq!(invitation_code_info.member_count, 1);
assert_eq!(
invitation_code_info.workspace_name,
workspaces[0].workspace_name
);
let invited_workspace_id = invitee_client
.join_workspace_by_invitation_code(&invitation_code)
.await