mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 00:31:09 +00:00
feat(webapi): impl identity description retrieval
This commit is contained in:
parent
d5cc097e7a
commit
c3842a6d3b
6 changed files with 126 additions and 6 deletions
|
@ -5,6 +5,7 @@ use std::fmt;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::identity::UserIdentityDesc;
|
||||||
use crate::msg::{Id, MemberPermission, RoomAttrs, SignedChatMsgWithId};
|
use crate::msg::{Id, MemberPermission, RoomAttrs, SignedChatMsgWithId};
|
||||||
use crate::PubKey;
|
use crate::PubKey;
|
||||||
|
|
||||||
|
@ -172,6 +173,15 @@ pub struct RoomMember {
|
||||||
pub last_seen_cid: Option<Id>,
|
pub last_seen_cid: Option<Id>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Server cached user identity description.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
pub struct UserIdentityDescResponse<I = UserIdentityDesc> {
|
||||||
|
/// The identity description of the requested user.
|
||||||
|
#[cfg_attr(feature = "schemars", schemars(with = "UserIdentityDesc"))]
|
||||||
|
pub identity: I,
|
||||||
|
}
|
||||||
|
|
||||||
/// A server-to-client event.
|
/// A server-to-client event.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
|
|
@ -30,7 +30,7 @@ serde = { version = "1", features = ["derive"] }
|
||||||
serde-constant = "0.1"
|
serde-constant = "0.1"
|
||||||
serde-inline-default = "0.2"
|
serde-inline-default = "0.2"
|
||||||
serde_jcs = "0.1"
|
serde_jcs = "0.1"
|
||||||
serde_json = "1"
|
serde_json = { version = "1", features = ["raw_value"] }
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "sync", "time"] }
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "sync", "time"] }
|
||||||
|
|
|
@ -10,8 +10,10 @@ use blah_types::msg::{
|
||||||
use blah_types::server::RoomMetadata;
|
use blah_types::server::RoomMetadata;
|
||||||
use blah_types::{Id, PubKey, Signee, UserKey};
|
use blah_types::{Id, PubKey, Signee, UserKey};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use rusqlite::types::FromSqlError;
|
||||||
use rusqlite::{named_params, params, prepare_cached_and_bind, Connection, OpenFlags, Row};
|
use rusqlite::{named_params, params, prepare_cached_and_bind, Connection, OpenFlags, Row};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde_json::value::RawValue as JsonRawValue;
|
||||||
|
|
||||||
use crate::middleware::ApiError;
|
use crate::middleware::ApiError;
|
||||||
|
|
||||||
|
@ -170,6 +172,27 @@ fn parse_room_metadata(row: &Row<'_>) -> Result<RoomMetadata> {
|
||||||
pub trait TransactionOps {
|
pub trait TransactionOps {
|
||||||
fn conn(&self) -> &Connection;
|
fn conn(&self) -> &Connection;
|
||||||
|
|
||||||
|
fn get_user_id_desc_by_uid(&self, uid: i64) -> Result<Box<JsonRawValue>> {
|
||||||
|
prepare_cached_and_bind!(
|
||||||
|
self.conn(),
|
||||||
|
r"
|
||||||
|
SELECT `id_desc`
|
||||||
|
FROM `user`
|
||||||
|
WHERE `uid` = :uid
|
||||||
|
"
|
||||||
|
)
|
||||||
|
.raw_query()
|
||||||
|
.and_then(|row| {
|
||||||
|
let json = JsonRawValue::from_string(row.get(0)?).map_err(|err| {
|
||||||
|
FromSqlError::Other(format!("invalid id_desc in database: {err}").into())
|
||||||
|
})?;
|
||||||
|
Ok::<_, rusqlite::Error>(json)
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.ok_or(ApiError::UserNotFound)?
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_user(&self, UserKey { id_key, act_key }: &UserKey) -> Result<(i64, ServerPermission)> {
|
fn get_user(&self, UserKey { id_key, act_key }: &UserKey) -> Result<(i64, ServerPermission)> {
|
||||||
prepare_cached_and_bind!(
|
prepare_cached_and_bind!(
|
||||||
self.conn(),
|
self.conn(),
|
||||||
|
|
|
@ -18,7 +18,7 @@ use blah_types::msg::{
|
||||||
};
|
};
|
||||||
use blah_types::server::{
|
use blah_types::server::{
|
||||||
ErrorResponseWithChallenge, RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs,
|
ErrorResponseWithChallenge, RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs,
|
||||||
ServerCapabilities, ServerMetadata,
|
ServerCapabilities, ServerMetadata, UserIdentityDescResponse,
|
||||||
};
|
};
|
||||||
use blah_types::{get_timestamp, Id, PubKey, Signed, UserKey};
|
use blah_types::{get_timestamp, Id, PubKey, Signed, UserKey};
|
||||||
use data_encoding::BASE64_NOPAD;
|
use data_encoding::BASE64_NOPAD;
|
||||||
|
@ -29,6 +29,7 @@ use parking_lot::Mutex;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use serde_inline_default::serde_inline_default;
|
use serde_inline_default::serde_inline_default;
|
||||||
|
use serde_json::value::RawValue as JsonRawValue;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use utils::ExpiringSet;
|
use utils::ExpiringSet;
|
||||||
|
@ -172,7 +173,8 @@ pub fn router(st: Arc<AppState>) -> Router {
|
||||||
// TODO!: remove this.
|
// TODO!: remove this.
|
||||||
.route("/room/:rid/admin", r().post(post_room_admin))
|
.route("/room/:rid/admin", r().post(post_room_admin))
|
||||||
.route("/room/:rid/member", r().get(list_room_member).post(post_room_member))
|
.route("/room/:rid/member", r().get(list_room_member).post(post_room_member))
|
||||||
.route("/room/:rid/member/:uid", r().get(get_room_member).delete(delete_room_member).patch(patch_room_member))
|
.route("/room/:rid/member/:idkey", r().get(get_room_member).delete(delete_room_member).patch(patch_room_member))
|
||||||
|
.route("/room/:rid/member/:idkey/identity", r().get(get_room_member_identity))
|
||||||
.fallback(fallback_route)
|
.fallback(fallback_route)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -620,6 +622,20 @@ async fn patch_room_member(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_room_member_identity(
|
||||||
|
st: ArcState,
|
||||||
|
R(Path((rid, id_key)), _): RE<Path<(Id, PubKey)>>,
|
||||||
|
Auth(user): Auth,
|
||||||
|
) -> Result<Json<UserIdentityDescResponse<Box<JsonRawValue>>>, ApiError> {
|
||||||
|
st.db.with_read(|txn| {
|
||||||
|
// Check membership.
|
||||||
|
let _ = txn.get_room_member(rid, &user)?;
|
||||||
|
let (uid, ..) = txn.get_room_member_by_id_key(rid, &id_key)?;
|
||||||
|
let identity = txn.get_user_id_desc_by_uid(uid)?;
|
||||||
|
Ok(Json(UserIdentityDescResponse { identity }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn delete_room(
|
async fn delete_room(
|
||||||
st: ArcState,
|
st: ArcState,
|
||||||
R(Path(rid), _): RE<Path<Id>>,
|
R(Path(rid), _): RE<Path<Id>>,
|
||||||
|
|
|
@ -17,7 +17,7 @@ use blah_types::msg::{
|
||||||
};
|
};
|
||||||
use blah_types::server::{
|
use blah_types::server::{
|
||||||
RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs, ServerEvent, ServerMetadata,
|
RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs, ServerEvent, ServerMetadata,
|
||||||
UserRegisterChallenge,
|
UserIdentityDescResponse, UserRegisterChallenge,
|
||||||
};
|
};
|
||||||
use blah_types::{Id, SignExt, Signed, UserKey};
|
use blah_types::{Id, SignExt, Signed, UserKey};
|
||||||
use blahd::{AppState, Database};
|
use blahd::{AppState, Database};
|
||||||
|
@ -31,6 +31,7 @@ use rstest::{fixture, rstest};
|
||||||
use rusqlite::{params, Connection};
|
use rusqlite::{params, Connection};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::json;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
@ -68,6 +69,7 @@ nonce_rotate_secs = 60
|
||||||
};
|
};
|
||||||
|
|
||||||
struct User {
|
struct User {
|
||||||
|
name: u8,
|
||||||
pubkeys: UserKey,
|
pubkeys: UserKey,
|
||||||
id_priv: SigningKey,
|
id_priv: SigningKey,
|
||||||
act_priv: SigningKey,
|
act_priv: SigningKey,
|
||||||
|
@ -79,6 +81,7 @@ impl User {
|
||||||
let id_priv = SigningKey::from_bytes(&[b; 32]);
|
let id_priv = SigningKey::from_bytes(&[b; 32]);
|
||||||
let act_priv = SigningKey::from_bytes(&[b.to_ascii_lowercase(); 32]);
|
let act_priv = SigningKey::from_bytes(&[b.to_ascii_lowercase(); 32]);
|
||||||
Self {
|
Self {
|
||||||
|
name: b,
|
||||||
pubkeys: UserKey {
|
pubkeys: UserKey {
|
||||||
id_key: id_priv.verifying_key().into(),
|
id_key: id_priv.verifying_key().into(),
|
||||||
act_key: act_priv.verifying_key().into(),
|
act_key: act_priv.verifying_key().into(),
|
||||||
|
@ -433,7 +436,7 @@ fn server() -> Server {
|
||||||
.prepare(
|
.prepare(
|
||||||
r"
|
r"
|
||||||
INSERT INTO `user` (`id_key`, `permission`, `last_fetch_time`, `id_desc`)
|
INSERT INTO `user` (`id_key`, `permission`, `last_fetch_time`, `id_desc`)
|
||||||
VALUES (?, ?, 0, '{}')
|
VALUES (?, ?, 0, ?)
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -449,8 +452,10 @@ fn server() -> Server {
|
||||||
(&*ALICE, ServerPermission::ALL),
|
(&*ALICE, ServerPermission::ALL),
|
||||||
(&BOB, ServerPermission::empty()),
|
(&BOB, ServerPermission::empty()),
|
||||||
] {
|
] {
|
||||||
|
// Fake value.
|
||||||
|
let id_desc = json!({"user": user.name as char }).to_string();
|
||||||
add_user
|
add_user
|
||||||
.execute(params![user.pubkeys.id_key, perm])
|
.execute(params![user.pubkeys.id_key, perm, id_desc])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uid = conn.last_insert_rowid();
|
let uid = conn.last_insert_rowid();
|
||||||
add_act_key
|
add_act_key
|
||||||
|
@ -1793,3 +1798,44 @@ async fn room_mgmt_perm(server: Server) {
|
||||||
// Bob can chat again.
|
// Bob can chat again.
|
||||||
server.post_chat(rid, &BOB, "yay").await.unwrap();
|
server.post_chat(rid, &BOB, "yay").await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_member_id_desc(server: Server) {
|
||||||
|
let rid = server
|
||||||
|
.create_room(&ALICE, RoomAttrs::PUBLIC_JOINABLE, "public")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let get_id_desc = |src_user: &User, tgt_user: &User| {
|
||||||
|
server
|
||||||
|
.get::<UserIdentityDescResponse<Box<serde_json::value::Value>>>(
|
||||||
|
&format!("/room/{rid}/member/{}/identity", tgt_user.pubkeys.id_key),
|
||||||
|
Some(&auth(src_user)),
|
||||||
|
)
|
||||||
|
.map_ok(|desc| desc.identity["user"].as_str().unwrap().to_owned())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Current user not in the room.
|
||||||
|
get_id_desc(&BOB, &ALICE)
|
||||||
|
.await
|
||||||
|
.expect_api_err(StatusCode::NOT_FOUND, "room_not_found");
|
||||||
|
|
||||||
|
// Target user not in the room.
|
||||||
|
get_id_desc(&ALICE, &BOB)
|
||||||
|
.await
|
||||||
|
.expect_api_err(StatusCode::NOT_FOUND, "member_not_found");
|
||||||
|
|
||||||
|
// OK, get self.
|
||||||
|
let desc = get_id_desc(&ALICE, &ALICE).await.unwrap();
|
||||||
|
assert_eq!(desc, "A");
|
||||||
|
|
||||||
|
server
|
||||||
|
.join_room(rid, &BOB, MemberPermission::MAX_SELF_ADD)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ok, get member.
|
||||||
|
let desc = get_id_desc(&ALICE, &BOB).await.unwrap();
|
||||||
|
assert_eq!(desc, "B");
|
||||||
|
}
|
||||||
|
|
|
@ -688,6 +688,31 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiError'
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
|
||||||
|
/_blah/room/{rid}/member/{member_id_key}/identity:
|
||||||
|
get:
|
||||||
|
summary: Get identity description of a room member
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
- name: Authorization
|
||||||
|
in: header
|
||||||
|
description: User authentication token.
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Signed-Auth'
|
||||||
|
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserIdentityDescResponse'
|
||||||
|
|
||||||
|
404:
|
||||||
|
description: |
|
||||||
|
Room does not exist, or either 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
|
# Ideally we should generate these from src, but we need to
|
||||||
# WAIT: https://github.com/juhaku/utoipa/pull/1034
|
# WAIT: https://github.com/juhaku/utoipa/pull/1034
|
||||||
|
|
Loading…
Add table
Reference in a new issue