mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-04-30 16:21:10 +00:00
refactor(webapi): split /room/:rid/admin
endpoint and deprecate
Since we alraedy use `/room/:rid/member`, member CRUD can use this path for better semantics. The `admin` endpoint will be removed later.
This commit is contained in:
parent
b8921a5485
commit
d1dfda51db
4 changed files with 235 additions and 97 deletions
|
@ -339,24 +339,36 @@ pub struct RoomMember {
|
|||
#[serde(tag = "typ", rename = "auth")]
|
||||
pub struct AuthPayload {}
|
||||
|
||||
// FIXME: Remove this.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
// `typ` is provided by `RoomAdminOp`.
|
||||
pub struct RoomAdminPayload {
|
||||
pub room: Id,
|
||||
#[serde(flatten)]
|
||||
pub op: RoomAdminOp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "typ", rename_all = "snake_case", rename = "remove_member")]
|
||||
pub struct RemoveMemberPayload {
|
||||
pub room: Id,
|
||||
// TODO: This field name collide with `Signee::user`.
|
||||
pub user: PubKey,
|
||||
}
|
||||
|
||||
// TODO: Maybe disallow adding other user without consent?
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "typ", rename_all = "snake_case")]
|
||||
pub struct AddMemberPayload {
|
||||
pub room: Id,
|
||||
#[serde(flatten)]
|
||||
pub member: RoomMember,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum RoomAdminOp {
|
||||
AddMember {
|
||||
permission: MemberPermission,
|
||||
user: PubKey,
|
||||
},
|
||||
RemoveMember {
|
||||
user: PubKey,
|
||||
},
|
||||
AddMember(AddMemberPayload),
|
||||
RemoveMember(RemoveMemberPayload),
|
||||
// TODO: RU
|
||||
}
|
||||
|
||||
|
@ -479,6 +491,7 @@ mod sql_impl {
|
|||
mod tests {
|
||||
use ed25519_dalek::{SigningKey, PUBLIC_KEY_LENGTH};
|
||||
use expect_test::expect;
|
||||
use AddMemberPayload;
|
||||
|
||||
use crate::SignExt;
|
||||
|
||||
|
@ -547,11 +560,13 @@ mod tests {
|
|||
#[test]
|
||||
fn room_admin_serde() {
|
||||
let data = RoomAdminPayload {
|
||||
room: Id(42),
|
||||
op: RoomAdminOp::AddMember {
|
||||
permission: MemberPermission::POST_CHAT,
|
||||
user: PubKey([0x42; PUBLIC_KEY_LENGTH]),
|
||||
},
|
||||
op: RoomAdminOp::AddMember(AddMemberPayload {
|
||||
room: Id(42),
|
||||
member: RoomMember {
|
||||
permission: MemberPermission::POST_CHAT,
|
||||
user: PubKey([0x42; PUBLIC_KEY_LENGTH]),
|
||||
},
|
||||
}),
|
||||
};
|
||||
let raw = serde_jcs::to_string(&data).unwrap();
|
||||
|
||||
|
|
115
blahd/src/lib.rs
115
blahd/src/lib.rs
|
@ -1,3 +1,4 @@
|
|||
use std::marker::PhantomData;
|
||||
use std::num::NonZero;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
@ -7,24 +8,24 @@ use axum::body::Bytes;
|
|||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::{header, HeaderName, HeaderValue, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use axum_extra::extract::WithRejection as R;
|
||||
use blah_types::msg::{
|
||||
ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, DeleteRoomPayload,
|
||||
MemberPermission, RoomAdminOp, RoomAdminPayload, RoomAttrs, ServerPermission,
|
||||
SignedChatMsgWithId, WithMsgId,
|
||||
AddMemberPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload,
|
||||
DeleteRoomPayload, MemberPermission, RemoveMemberPayload, RoomAdminOp, RoomAdminPayload,
|
||||
RoomAttrs, ServerPermission, SignedChatMsgWithId, WithMsgId,
|
||||
};
|
||||
use blah_types::server::{
|
||||
ErrorResponseWithChallenge, RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs,
|
||||
ServerCapabilities, ServerMetadata,
|
||||
};
|
||||
use blah_types::{get_timestamp, Id, Signed, UserKey};
|
||||
use blah_types::{get_timestamp, Id, PubKey, Signed, UserKey};
|
||||
use data_encoding::BASE64_NOPAD;
|
||||
use database::{Transaction, TransactionOps};
|
||||
use id::IdExt;
|
||||
use middleware::{Auth, ETag, MaybeAuth, ResultExt as _, SignedJson};
|
||||
use parking_lot::Mutex;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_inline_default::serde_inline_default;
|
||||
use sha2::Digest;
|
||||
|
@ -148,7 +149,9 @@ impl AppState {
|
|||
type ArcState = State<Arc<AppState>>;
|
||||
|
||||
pub fn router(st: Arc<AppState>) -> Router {
|
||||
// NB. User consistent handler naming: `<method>_<path>[_<details>]`.
|
||||
use axum::routing::{delete, get, post};
|
||||
|
||||
// NB. Use consistent handler naming: `<method>_<path>[_<details>]`.
|
||||
// Use prefix `list` for GET with pagination.
|
||||
//
|
||||
// One route per line.
|
||||
|
@ -165,8 +168,11 @@ pub fn router(st: Arc<AppState>) -> Router {
|
|||
.route("/room/:rid/feed.atom", get(feed::get_room_feed::<feed::AtomFeed>))
|
||||
.route("/room/:rid/msg", get(list_room_msg).post(post_room_msg))
|
||||
.route("/room/:rid/msg/:cid/seen", post(post_room_msg_seen))
|
||||
// TODO!: remove this.
|
||||
.route("/room/:rid/admin", post(post_room_admin))
|
||||
.route("/room/:rid/member", get(list_room_member));
|
||||
.route("/room/:rid/member", get(list_room_member).post(post_room_member))
|
||||
.route("/room/:rid/member/:uid", delete(delete_room_member))
|
||||
;
|
||||
|
||||
let router = router
|
||||
.layer(tower_http::limit::RequestBodyLimitLayer::new(
|
||||
|
@ -470,58 +476,81 @@ async fn post_room_admin(
|
|||
R(Path(rid), _): RE<Path<Id>>,
|
||||
SignedJson(op): SignedJson<RoomAdminPayload>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
api_ensure!(rid == op.signee.payload.room, "room id mismatch with URI");
|
||||
api_ensure!(!rid.is_peer_chat(), "cannot operate on a peer chat room");
|
||||
|
||||
match op.signee.payload.op {
|
||||
RoomAdminOp::AddMember { user, permission } => {
|
||||
api_ensure!(
|
||||
user == op.signee.user.id_key,
|
||||
ApiError::NotImplemented("only self-adding is implemented yet"),
|
||||
);
|
||||
api_ensure!(
|
||||
MemberPermission::MAX_SELF_ADD.contains(permission),
|
||||
"invalid initial permission",
|
||||
);
|
||||
room_join(&st, rid, &op.signee.user, permission).await?;
|
||||
}
|
||||
RoomAdminOp::RemoveMember { user } => {
|
||||
api_ensure!(
|
||||
user == op.signee.user.id_key,
|
||||
ApiError::NotImplemented("only self-removal is implemented yet"),
|
||||
);
|
||||
room_leave(&st, rid, &op.signee.user).await?;
|
||||
}
|
||||
// TODO: This is a temporary endpoint so just reserialize them.
|
||||
fn transcode<T: Serialize, U: DeserializeOwned>(v: &T) -> SignedJson<U> {
|
||||
let v = serde_json::to_value(v).expect("serialization cannot fail");
|
||||
SignedJson(serde_json::from_value(v).expect("format should be compatible"))
|
||||
}
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
match op.signee.payload.op {
|
||||
RoomAdminOp::AddMember(_) => {
|
||||
post_room_member(st, R(Path(rid), PhantomData), transcode(&op)).await
|
||||
}
|
||||
RoomAdminOp::RemoveMember(_) => {
|
||||
delete_room_member(
|
||||
st,
|
||||
R(Path((rid, op.signee.user.id_key.clone())), PhantomData),
|
||||
transcode(&op),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn room_join(
|
||||
st: &AppState,
|
||||
rid: Id,
|
||||
user: &UserKey,
|
||||
permission: MemberPermission,
|
||||
) -> Result<(), ApiError> {
|
||||
async fn post_room_member(
|
||||
st: ArcState,
|
||||
R(Path(rid), _): RE<Path<Id>>,
|
||||
SignedJson(op): SignedJson<AddMemberPayload>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
api_ensure!(rid == op.signee.payload.room, "room id mismatch with URI");
|
||||
api_ensure!(
|
||||
!rid.is_peer_chat(),
|
||||
"cannot add members to a peer chat room"
|
||||
);
|
||||
let member = op.signee.payload.member;
|
||||
api_ensure!(
|
||||
member.user == op.signee.user.id_key,
|
||||
ApiError::NotImplemented("only self-adding is implemented yet"),
|
||||
);
|
||||
api_ensure!(
|
||||
MemberPermission::MAX_SELF_ADD.contains(member.permission),
|
||||
"invalid member permission",
|
||||
);
|
||||
|
||||
st.db.with_write(|txn| {
|
||||
let (uid, _perm) = txn.get_user(user)?;
|
||||
let (uid, _perm) = txn.get_user(&op.signee.user)?;
|
||||
let (attrs, _) = txn.get_room_having(rid, RoomAttrs::PUBLIC_JOINABLE)?;
|
||||
// Sanity check.
|
||||
assert!(!attrs.contains(RoomAttrs::PEER_CHAT));
|
||||
txn.add_room_member(rid, uid, permission)?;
|
||||
Ok(())
|
||||
txn.add_room_member(rid, uid, member.permission)?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
})
|
||||
}
|
||||
|
||||
async fn room_leave(st: &AppState, rid: Id, user: &UserKey) -> Result<(), ApiError> {
|
||||
async fn delete_room_member(
|
||||
st: ArcState,
|
||||
R(Path((rid, id_key)), _): RE<Path<(Id, PubKey)>>,
|
||||
SignedJson(op): SignedJson<RemoveMemberPayload>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
api_ensure!(rid == op.signee.payload.room, "room id mismatch with URI");
|
||||
api_ensure!(
|
||||
!rid.is_peer_chat(),
|
||||
"cannot remove members from a peer chat room"
|
||||
);
|
||||
let tgt_user = op.signee.payload.user;
|
||||
api_ensure!(id_key == tgt_user, "user id mismatch with URI");
|
||||
api_ensure!(
|
||||
op.signee.user.id_key == tgt_user,
|
||||
ApiError::NotImplemented("only self-removal is implemented yet"),
|
||||
);
|
||||
|
||||
st.db.with_write(|txn| {
|
||||
api_ensure!(!rid.is_peer_chat(), "cannot leave a peer chat room");
|
||||
let (uid, ..) = txn.get_room_member(rid, user)?;
|
||||
let (uid, ..) = txn.get_room_member(rid, &op.signee.user)?;
|
||||
let (attrs, _) = txn.get_room_having(rid, RoomAttrs::empty())?;
|
||||
// Sanity check.
|
||||
assert!(!attrs.contains(RoomAttrs::PEER_CHAT));
|
||||
txn.remove_room_member(rid, uid)?;
|
||||
Ok(())
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -10,10 +10,10 @@ use std::time::{Duration, Instant};
|
|||
use anyhow::{Context, Result};
|
||||
use blah_types::identity::{IdUrl, UserActKeyDesc, UserIdentityDesc, UserProfile};
|
||||
use blah_types::msg::{
|
||||
AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, DeleteRoomPayload,
|
||||
MemberPermission, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, ServerPermission,
|
||||
SignedChatMsg, SignedChatMsgWithId, UserRegisterChallengeResponse, UserRegisterPayload,
|
||||
WithMsgId,
|
||||
self, AddMemberPayload, AuthPayload, ChatPayload, CreateGroup, CreatePeerChat,
|
||||
CreateRoomPayload, DeleteRoomPayload, MemberPermission, RemoveMemberPayload, RichText,
|
||||
RoomAttrs, ServerPermission, SignedChatMsg, SignedChatMsgWithId, UserRegisterChallengeResponse,
|
||||
UserRegisterPayload, WithMsgId,
|
||||
};
|
||||
use blah_types::server::{
|
||||
RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs, ServerEvent, ServerMetadata,
|
||||
|
@ -302,30 +302,39 @@ impl Server {
|
|||
) -> impl Future<Output = Result<()>> + use<'_> {
|
||||
let req = self.sign(
|
||||
user,
|
||||
RoomAdminPayload {
|
||||
AddMemberPayload {
|
||||
room: rid,
|
||||
op: RoomAdminOp::AddMember {
|
||||
member: msg::RoomMember {
|
||||
permission,
|
||||
user: user.pubkeys.id_key.clone(),
|
||||
},
|
||||
},
|
||||
);
|
||||
self.request::<_, NoContent>(Method::POST, &format!("/room/{rid}/admin"), None, Some(req))
|
||||
.map_ok(|None| {})
|
||||
self.request::<_, NoContent>(
|
||||
Method::POST,
|
||||
&format!("/room/{rid}/member"),
|
||||
None,
|
||||
Some(req),
|
||||
)
|
||||
.map_ok(|None| {})
|
||||
}
|
||||
|
||||
fn leave_room(&self, rid: Id, user: &User) -> impl Future<Output = Result<()>> + use<'_> {
|
||||
let user_id = user.pubkeys.id_key.clone();
|
||||
let req = self.sign(
|
||||
user,
|
||||
RoomAdminPayload {
|
||||
RemoveMemberPayload {
|
||||
room: rid,
|
||||
op: RoomAdminOp::RemoveMember {
|
||||
user: user.pubkeys.id_key.clone(),
|
||||
},
|
||||
user: user_id.clone(),
|
||||
},
|
||||
);
|
||||
self.request::<_, NoContent>(Method::POST, &format!("/room/{rid}/admin"), None, Some(req))
|
||||
.map_ok(|None| {})
|
||||
self.request::<_, NoContent>(
|
||||
Method::DELETE,
|
||||
&format!("/room/{rid}/member/{user_id}"),
|
||||
None,
|
||||
Some(req),
|
||||
)
|
||||
.map_ok(|None| {})
|
||||
}
|
||||
|
||||
fn post_chat(
|
||||
|
@ -585,7 +594,7 @@ async fn room_join_leave(server: Server) {
|
|||
server
|
||||
.join_room(rid_priv, &BOB, MemberPermission::ALL)
|
||||
.await
|
||||
.expect_invalid_request("invalid initial permission");
|
||||
.expect_invalid_request("invalid member permission");
|
||||
|
||||
// Bob is joined now.
|
||||
assert_eq!(
|
||||
|
|
137
docs/webapi.yaml
137
docs/webapi.yaml
|
@ -327,7 +327,11 @@ paths:
|
|||
|
||||
/_blah/room/{rid}/admin:
|
||||
post:
|
||||
summary: Room management
|
||||
summary: Room management (legacy)
|
||||
deprecated: true
|
||||
description: |
|
||||
Use POST `/_blah/room/{rid}/member` or
|
||||
DELETE `/_blah/room/{rid}/member/{member_id_key}` instead.
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
|
@ -561,6 +565,67 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
|
||||
post:
|
||||
summary: Join a room
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Signed-AddMember'
|
||||
|
||||
responses:
|
||||
204:
|
||||
description: Operation completed.
|
||||
|
||||
404:
|
||||
description: |
|
||||
Room does not exist or the user does not have permission for the
|
||||
operation.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
|
||||
409:
|
||||
description:
|
||||
The user is already a room member.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
|
||||
/_blah/room/{rid}/member/{target_id_key}:
|
||||
delete:
|
||||
summary: Remove a room member.
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Signed-RemoveMember'
|
||||
|
||||
responses:
|
||||
204:
|
||||
description: Operation completed.
|
||||
|
||||
403:
|
||||
description: |
|
||||
The user does not have permission to remove the operand member.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
|
||||
404:
|
||||
description: |
|
||||
Room does not exist, the user does not have permission for the
|
||||
operation, or the operand user is not a room member.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
|
||||
|
||||
# Ideally we should generate these from src, but we need to
|
||||
# WAIT: https://github.com/juhaku/utoipa/pull/1034
|
||||
components:
|
||||
|
@ -775,6 +840,11 @@ components:
|
|||
const: 'auth'
|
||||
|
||||
Signed-RoomAdmin:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Signed-AddMember'
|
||||
- $ref: '#/components/schemas/Signed-RemoveMember'
|
||||
|
||||
Signed-AddMember:
|
||||
type: object
|
||||
properties:
|
||||
sig:
|
||||
|
@ -793,32 +863,47 @@ components:
|
|||
act_key:
|
||||
type: string
|
||||
payload:
|
||||
oneOf:
|
||||
type: object
|
||||
properties:
|
||||
typ:
|
||||
type: string
|
||||
const: 'add_member'
|
||||
room:
|
||||
type: string
|
||||
permission:
|
||||
type: integer
|
||||
format: int32
|
||||
user:
|
||||
type: string
|
||||
|
||||
- description: Add member to the room.
|
||||
type: object
|
||||
properties:
|
||||
typ:
|
||||
type: string
|
||||
const: 'add_member'
|
||||
room:
|
||||
type: string
|
||||
permission:
|
||||
type: integer
|
||||
format: int32
|
||||
user:
|
||||
type: string
|
||||
|
||||
- description: Remove member from the room.
|
||||
type: object
|
||||
properties:
|
||||
typ:
|
||||
type: string
|
||||
const: 'remove_member'
|
||||
room:
|
||||
type: string
|
||||
user:
|
||||
type: string
|
||||
Signed-RemoveMember:
|
||||
type: object
|
||||
properties:
|
||||
sig:
|
||||
type: string
|
||||
signee:
|
||||
type: object
|
||||
properties:
|
||||
nonce:
|
||||
type: integer
|
||||
format: uint32
|
||||
timestamp:
|
||||
type: integer
|
||||
format: uint64
|
||||
id_key:
|
||||
type: string
|
||||
act_key:
|
||||
type: string
|
||||
payload:
|
||||
type: object
|
||||
properties:
|
||||
typ:
|
||||
type: string
|
||||
const: 'remove_member'
|
||||
room:
|
||||
type: string
|
||||
user:
|
||||
type: string
|
||||
|
||||
Signed-Chat:
|
||||
type: object
|
||||
|
|
Loading…
Add table
Reference in a new issue