mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-08-18 10:12:38 +00:00
Impl private rooms
This commit is contained in:
parent
9ced78d13d
commit
501b3e8db4
5 changed files with 159 additions and 64 deletions
123
src/main.rs
123
src/main.rs
|
@ -6,16 +6,17 @@ use std::sync::{Arc, Mutex};
|
|||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{ensure, Context, Result};
|
||||
use axum::extract::{FromRequest, Path, Query, Request, State};
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::extract::{FromRequest, FromRequestParts, Path, Query, Request, State};
|
||||
use axum::http::{header, request, StatusCode};
|
||||
use axum::response::{sse, IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{async_trait, Json, Router};
|
||||
use blah::types::{
|
||||
ChatItem, ChatPayload, CreateRoomPayload, RoomPermission, ServerPermission, Signee, WithSig,
|
||||
AuthPayload, ChatItem, ChatPayload, CreateRoomPayload, RoomAttrs, RoomPermission,
|
||||
ServerPermission, Signee, UserKey, WithSig,
|
||||
};
|
||||
use ed25519_dalek::SIGNATURE_LENGTH;
|
||||
use rusqlite::{named_params, params, OptionalExtension};
|
||||
use rusqlite::{named_params, params, OptionalExtension, Row};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::broadcast;
|
||||
|
@ -198,9 +199,11 @@ async fn room_get_item(
|
|||
st: ArcState,
|
||||
Path(ruuid): Path<Uuid>,
|
||||
params: Query<GetRoomItemParams>,
|
||||
OptionalAuth(user): OptionalAuth,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let (room_meta, items) =
|
||||
query_room_items(&st.conn.lock().unwrap(), ruuid, ¶ms).map_err(from_db_error)?;
|
||||
query_room_items(&st.conn.lock().unwrap(), ruuid, user.as_ref(), ¶ms)
|
||||
.map_err(from_db_error)?;
|
||||
|
||||
// TODO: This format is to-be-decided. Or do we even need this interface other than
|
||||
// `feed.json`?
|
||||
|
@ -213,7 +216,7 @@ async fn room_get_feed(
|
|||
params: Query<GetRoomItemParams>,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let (room_meta, items) =
|
||||
query_room_items(&st.conn.lock().unwrap(), ruuid, ¶ms).map_err(from_db_error)?;
|
||||
query_room_items(&st.conn.lock().unwrap(), ruuid, None, ¶ms).map_err(from_db_error)?;
|
||||
|
||||
let items = items
|
||||
.into_iter()
|
||||
|
@ -292,27 +295,51 @@ struct FeedItemExtra {
|
|||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RoomMetadata {
|
||||
pub title: String,
|
||||
pub attrs: RoomAttrs,
|
||||
}
|
||||
|
||||
fn get_room_if_readable<T>(
|
||||
conn: &rusqlite::Connection,
|
||||
ruuid: Uuid,
|
||||
user: Option<&UserKey>,
|
||||
f: impl FnOnce(&Row<'_>) -> rusqlite::Result<T>,
|
||||
) -> rusqlite::Result<T> {
|
||||
conn.query_row(
|
||||
r"
|
||||
SELECT `rid`, `title`, `attrs`
|
||||
FROM `room`
|
||||
WHERE `ruuid` = :ruuid AND
|
||||
((`attrs` & :perm) = :perm OR
|
||||
EXISTS(SELECT 1
|
||||
FROM `room_member`
|
||||
JOIN `user` USING (`uid`)
|
||||
WHERE `room_member`.`rid` = `room`.`rid` AND
|
||||
`userkey` = :userkey))
|
||||
",
|
||||
named_params! {
|
||||
":perm": RoomAttrs::PUBLIC_READABLE,
|
||||
":ruuid": ruuid,
|
||||
":userkey": user,
|
||||
},
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
fn query_room_items(
|
||||
conn: &rusqlite::Connection,
|
||||
ruuid: Uuid,
|
||||
user: Option<&UserKey>,
|
||||
params: &GetRoomItemParams,
|
||||
) -> rusqlite::Result<(RoomMetadata, Vec<(u64, ChatItem)>)> {
|
||||
let (rid, title) = conn.query_row(
|
||||
r"
|
||||
SELECT `rid`, `title`
|
||||
FROM `room`
|
||||
WHERE `ruuid` = ?
|
||||
",
|
||||
params![ruuid],
|
||||
|row| {
|
||||
let rid = row.get::<_, u64>("rid")?;
|
||||
let title = row.get::<_, String>("title")?;
|
||||
Ok((rid, title))
|
||||
},
|
||||
)?;
|
||||
let room_meta = RoomMetadata { title };
|
||||
let (rid, title, attrs) = get_room_if_readable(conn, ruuid, user, |row| {
|
||||
Ok((
|
||||
row.get::<_, u64>("rid")?,
|
||||
row.get::<_, String>("title")?,
|
||||
row.get::<_, RoomAttrs>("attrs")?,
|
||||
))
|
||||
})?;
|
||||
|
||||
let room_meta = RoomMetadata { title, attrs };
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
r"
|
||||
|
@ -374,6 +401,38 @@ impl<S: Send + Sync, T: Serialize + DeserializeOwned> FromRequest<S> for SignedJ
|
|||
}
|
||||
}
|
||||
|
||||
/// Extractor for optional verified JSON authorization header.
|
||||
#[derive(Debug)]
|
||||
struct OptionalAuth(Option<UserKey>);
|
||||
|
||||
#[async_trait]
|
||||
impl<S: Send + Sync> FromRequestParts<S> for OptionalAuth {
|
||||
type Rejection = StatusCode;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut request::Parts,
|
||||
_state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let Some(auth) = parts.headers.get(header::AUTHORIZATION) else {
|
||||
return Ok(Self(None));
|
||||
};
|
||||
|
||||
let ret = serde_json::from_slice::<WithSig<AuthPayload>>(auth.as_bytes())
|
||||
.context("invalid JSON")
|
||||
.and_then(|data| {
|
||||
data.verify()?;
|
||||
Ok(data.signee.user)
|
||||
});
|
||||
match ret {
|
||||
Ok(user) => Ok(Self(Some(user))),
|
||||
Err(err) => {
|
||||
tracing::debug!(%err, "invalid authorization");
|
||||
Err(StatusCode::BAD_REQUEST)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn room_post_item(
|
||||
st: ArcState,
|
||||
Path(ruuid): Path<Uuid>,
|
||||
|
@ -443,23 +502,17 @@ async fn room_post_item(
|
|||
}
|
||||
|
||||
async fn room_event(
|
||||
Path(ruuid): Path<Uuid>,
|
||||
st: ArcState,
|
||||
Path(ruuid): Path<Uuid>,
|
||||
// TODO: There is actually no way to add headers via `EventSource` in client side.
|
||||
// But this API is kinda temporary and need a better replacement anyway.
|
||||
// So just only support public room for now.
|
||||
OptionalAuth(user): OptionalAuth,
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let rid = st
|
||||
.conn
|
||||
.lock()
|
||||
.unwrap()
|
||||
.query_row(
|
||||
r"
|
||||
SELECT `rid`
|
||||
FROM `room`
|
||||
WHERE `ruuid` = ?
|
||||
",
|
||||
params![ruuid],
|
||||
|row| row.get::<_, u64>(0),
|
||||
)
|
||||
.map_err(from_db_error)?;
|
||||
let rid = get_room_if_readable(&st.conn.lock().unwrap(), ruuid, user.as_ref(), |row| {
|
||||
row.get::<_, u64>(0)
|
||||
})
|
||||
.map_err(from_db_error)?;
|
||||
|
||||
let rx = match st.room_listeners.lock().unwrap().entry(rid) {
|
||||
Entry::Occupied(ent) => ent.get().subscribe(),
|
||||
|
|
17
src/types.rs
17
src/types.rs
|
@ -92,8 +92,16 @@ pub type ChatItem = WithSig<ChatPayload>;
|
|||
#[serde(tag = "typ", rename = "create_room")]
|
||||
pub struct CreateRoomPayload {
|
||||
pub title: String,
|
||||
pub attrs: RoomAttrs,
|
||||
}
|
||||
|
||||
/// Proof of room membership for read-access.
|
||||
///
|
||||
/// TODO: Should we use JWT here instead?
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "typ", rename = "auth")]
|
||||
pub struct AuthPayload {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields, tag = "typ", rename_all = "snake_case")]
|
||||
pub enum RoomAdminPayload {
|
||||
|
@ -121,6 +129,13 @@ bitflags! {
|
|||
|
||||
const ALL = !0;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct RoomAttrs: u64 {
|
||||
const PUBLIC_READABLE = 1 << 0;
|
||||
|
||||
const _ = !0;
|
||||
}
|
||||
}
|
||||
|
||||
mod sql_impl {
|
||||
|
@ -165,5 +180,5 @@ mod sql_impl {
|
|||
};
|
||||
}
|
||||
|
||||
impl_u64_flag!(ServerPermission, RoomPermission);
|
||||
impl_u64_flag!(ServerPermission, RoomPermission, RoomAttrs);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue