From b29760d7ec6eccec829ad0898a7e07458da1c435 Mon Sep 17 00:00:00 2001 From: trisua Date: Fri, 13 Jun 2025 22:07:36 -0400 Subject: [PATCH] add: channels/messages scopes and api endpoints --- crates/app/src/public/html/chats/app.lisp | 1 - .../app/src/public/html/chats/channels.lisp | 2 +- .../src/routes/api/v1/channels/channels.rs | 86 ++++++++++++++-- .../src/routes/api/v1/channels/messages.rs | 99 ++++++++++++------- crates/app/src/routes/api/v1/mod.rs | 13 +++ crates/app/src/routes/api/v1/reactions.rs | 8 +- crates/app/src/routes/api/v1/stacks.rs | 18 ++-- crates/app/src/routes/api/v1/uploads.rs | 4 +- crates/app/src/routes/pages/chats.rs | 13 ++- crates/core/src/model/oauth.rs | 14 +++ 10 files changed, 195 insertions(+), 63 deletions(-) diff --git a/crates/app/src/public/html/chats/app.lisp b/crates/app/src/public/html/chats/app.lisp index 97bb440..e7cc4ec 100644 --- a/crates/app/src/public/html/chats/app.lisp +++ b/crates/app/src/public/html/chats/app.lisp @@ -536,7 +536,6 @@ method: \"Headers\", data: JSON.stringify({ // SocketHeaders - user: \"{{ user.id }}\", is_channel: window.SUBSCRIBE_CHANNEL, }), }), diff --git a/crates/app/src/public/html/chats/channels.lisp b/crates/app/src/public/html/chats/channels.lisp index e8498b2..a87dbeb 100644 --- a/crates/app/src/public/html/chats/channels.lisp +++ b/crates/app/src/public/html/chats/channels.lisp @@ -44,7 +44,7 @@ (text "{{ icon \"trash\" }}") (span (text "{{ text \"general:action.delete\" }}"))) - (text "{% else %}") + (text "{% elif selected_community == 0 %}") (button ("onclick" "kick_member('{{ channel.id }}', '{{ user.id }}')") ("class" "red") diff --git a/crates/app/src/routes/api/v1/channels/channels.rs b/crates/app/src/routes/api/v1/channels/channels.rs index 584cc78..e3ead5a 100644 --- a/crates/app/src/routes/api/v1/channels/channels.rs +++ b/crates/app/src/routes/api/v1/channels/channels.rs @@ -1,6 +1,6 @@ use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum_extra::extract::CookieJar; -use tetratto_core::model::{channels::Channel, ApiReturn, Error}; +use tetratto_core::model::{oauth, channels::Channel, ApiReturn, Error}; use crate::{ get_user_from_token, routes::api::v1::{ @@ -15,7 +15,7 @@ pub async fn create_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityCreateChannels) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -47,7 +47,7 @@ pub async fn create_group_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -111,7 +111,7 @@ pub async fn delete_request( Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -133,7 +133,7 @@ pub async fn update_title_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -155,7 +155,7 @@ pub async fn update_position_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -177,7 +177,7 @@ pub async fn add_member_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -199,7 +199,7 @@ pub async fn kick_member_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -223,3 +223,73 @@ pub async fn kick_member_request( Err(e) => Json(e.into()), } } + +pub async fn get_dm_channels_request( + jar: CookieJar, + Extension(data): Extension, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_channels_by_user(user.id).await { + Ok(c) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(c), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn get_community_channels_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if data + .get_membership_by_owner_community_no_void(user.id, id) + .await + .is_err() + { + // must be a member of the community to request channels + return Json(Error::NotAllowed.into()); + } + + match data.get_channels_by_community(id).await { + Ok(c) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(c), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn get_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, +) -> impl IntoResponse { + let data = &(data.read().await).0; + if get_user_from_token!(jar, data, oauth::AppScope::CommunityManageChannels).is_none() { + return Json(Error::NotAllowed.into()); + } + + match data.get_channel_by_id(id).await { + Ok(c) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(c), + }), + Err(e) => Json(e.into()), + } +} diff --git a/crates/app/src/routes/api/v1/channels/messages.rs b/crates/app/src/routes/api/v1/channels/messages.rs index 1cee397..e88138e 100644 --- a/crates/app/src/routes/api/v1/channels/messages.rs +++ b/crates/app/src/routes/api/v1/channels/messages.rs @@ -2,15 +2,16 @@ use std::{collections::HashMap, time::Duration}; use axum::{ extract::{ ws::{Message as WsMessage, WebSocket, WebSocketUpgrade}, - Path, + Path, Query, }, - response::{IntoResponse, Response}, + response::IntoResponse, Extension, Json, }; use axum_extra::extract::CookieJar; use tetratto_core::{ cache::{Cache, redis::Commands}, model::{ + oauth, auth::User, channels::Message, socket::{PacketType, SocketMessage, SocketMethod}, @@ -18,36 +19,42 @@ use tetratto_core::{ }, DataManager, }; -use crate::{get_user_from_token, routes::api::v1::CreateMessage, State}; +use crate::{ + get_user_from_token, + routes::{api::v1::CreateMessage, pages::PaginatedQuery}, + State, +}; use serde::Deserialize; use futures_util::{sink::SinkExt, stream::StreamExt}; #[derive(Clone, Deserialize)] pub struct SocketHeaders { - pub user: String, pub is_channel: bool, } /// Handle a subscription to the websocket. pub async fn subscription_handler( + jar: CookieJar, ws: WebSocketUpgrade, Extension(data): Extension, Path(id): Path, -) -> Response { - let data = &(data.read().await); - let data = data.0.clone(); +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadSockets) { + Some(ua) => ua, + None => return Err(Error::NotAllowed.to_string()), + }; - ws.on_upgrade(|socket| async move { + let data = data.clone(); + Ok(ws.on_upgrade(|socket| async move { tokio::spawn(async move { - handle_socket(socket, data, id).await; + handle_socket(socket, data, id, user).await; }); - }) + })) } -pub async fn handle_socket(socket: WebSocket, db: DataManager, community_id: String) { +pub async fn handle_socket(socket: WebSocket, db: DataManager, community_id: String, user: User) { let (mut sink, mut stream) = socket.split(); - - let mut user: Option = None; let mut headers: Option = None; let channel_id = format!("chats/{community_id}"); @@ -63,7 +70,7 @@ pub async fn handle_socket(socket: WebSocket, db: DataManager, community_id: Str } }; - if data.method != SocketMethod::Headers && user.is_none() && headers.is_none() { + if data.method != SocketMethod::Headers && headers.is_none() { // we've sent something else before authenticating... that's not right let _ = sink.close().await; return; @@ -74,24 +81,6 @@ pub async fn handle_socket(socket: WebSocket, db: DataManager, community_id: Str let data: SocketHeaders = data.data(); headers = Some(data.clone()); - user = Some( - match dbc - .get_user_by_id(match data.user.parse::() { - Ok(c) => c, - Err(_) => { - let _ = sink.close().await; - return; - } - }) - .await - { - Ok(ua) => ua, - Err(_) => { - let _ = sink.close().await; - return; - } - }, - ); if data.is_channel { // verify permissions for single channel @@ -112,8 +101,6 @@ pub async fn handle_socket(socket: WebSocket, db: DataManager, community_id: Str } }; - let user = user.as_ref().unwrap(); - let membership = match dbc .get_membership_by_owner_community(user.id, channel.id) .await @@ -142,7 +129,6 @@ pub async fn handle_socket(socket: WebSocket, db: DataManager, community_id: Str } // get channel permissions - let user = user.unwrap(); let headers = headers.unwrap(); let mut channel_read_statuses: HashMap = HashMap::new(); @@ -280,7 +266,7 @@ pub async fn create_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateMessages) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -311,7 +297,7 @@ pub async fn delete_request( Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserDeleteMessages) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -325,3 +311,42 @@ pub async fn delete_request( Err(e) => Json(e.into()), } } + +pub async fn from_channel_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateMessages) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + let channel = match data.get_channel_by_id(id).await { + Ok(c) => c, + Err(e) => return Json(e.into()), + }; + + let membership = match data + .get_membership_by_owner_community(user.id, channel.community) + .await + { + Ok(m) => m, + Err(e) => return Json(e.into()), + }; + + if !channel.check_read(user.id, Some(membership.role)) { + return Json(Error::NotAllowed.into()); + } + + match data.get_messages_by_channel(id, 24, props.page).await { + Ok(m) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(m), + }), + Err(e) => Json(e.into()), + } +} diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index b912c04..242dceb 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -433,6 +433,15 @@ pub fn routes() -> Router { "/channels/{id}/kick", post(channels::channels::kick_member_request), ) + .route("/channels/{id}", get(channels::channels::get_request)) + .route( + "/channels/community/{id}", + get(channels::channels::get_community_channels_request), + ) + .route( + "/channels/dms", + get(channels::channels::get_dm_channels_request), + ) // messages .route( "/_connect/{id}", @@ -440,6 +449,10 @@ pub fn routes() -> Router { ) .route("/messages", post(channels::messages::create_request)) .route("/messages/{id}", delete(channels::messages::delete_request)) + .route( + "/messages/from_channel/{id}", + get(channels::messages::from_channel_request), + ) // emojis .route( "/lookup_emoji", diff --git a/crates/app/src/routes/api/v1/reactions.rs b/crates/app/src/routes/api/v1/reactions.rs index 3b3529a..b3efe52 100644 --- a/crates/app/src/routes/api/v1/reactions.rs +++ b/crates/app/src/routes/api/v1/reactions.rs @@ -1,7 +1,7 @@ use crate::{State, get_user_from_token, routes::api::v1::CreateReaction}; use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum_extra::extract::CookieJar; -use tetratto_core::model::{ApiReturn, Error, reactions::Reaction}; +use tetratto_core::model::{oauth, ApiReturn, Error, reactions::Reaction}; pub async fn get_request( jar: CookieJar, @@ -9,7 +9,7 @@ pub async fn get_request( Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReact) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -30,7 +30,7 @@ pub async fn create_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReact) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -81,7 +81,7 @@ pub async fn delete_request( Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReact) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; diff --git a/crates/app/src/routes/api/v1/stacks.rs b/crates/app/src/routes/api/v1/stacks.rs index 85ebd5a..8faab1f 100644 --- a/crates/app/src/routes/api/v1/stacks.rs +++ b/crates/app/src/routes/api/v1/stacks.rs @@ -1,7 +1,7 @@ use crate::{State, get_user_from_token}; use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum_extra::extract::CookieJar; -use tetratto_core::model::{stacks::UserStack, ApiReturn, Error}; +use tetratto_core::model::{oauth, stacks::UserStack, ApiReturn, Error}; use super::{ AddOrRemoveStackUser, CreateStack, UpdateStackMode, UpdateStackName, UpdateStackPrivacy, UpdateStackSort, @@ -13,7 +13,7 @@ pub async fn create_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateStacks) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -38,7 +38,7 @@ pub async fn update_name_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageStacks) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -60,7 +60,7 @@ pub async fn update_privacy_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageStacks) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -82,7 +82,7 @@ pub async fn update_mode_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageStacks) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -104,7 +104,7 @@ pub async fn update_sort_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageStacks) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -126,7 +126,7 @@ pub async fn add_user_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageStacks) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -169,7 +169,7 @@ pub async fn remove_user_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageStacks) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -207,7 +207,7 @@ pub async fn delete_request( Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageStacks) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; diff --git a/crates/app/src/routes/api/v1/uploads.rs b/crates/app/src/routes/api/v1/uploads.rs index 9d587c2..c90c427 100644 --- a/crates/app/src/routes/api/v1/uploads.rs +++ b/crates/app/src/routes/api/v1/uploads.rs @@ -4,7 +4,7 @@ use axum_extra::extract::CookieJar; use pathbufd::PathBufD; use crate::{get_user_from_token, State}; use super::auth::images::read_image; -use tetratto_core::model::{ApiReturn, Error}; +use tetratto_core::model::{oauth, ApiReturn, Error}; pub async fn get_request( Path(id): Path, @@ -38,7 +38,7 @@ pub async fn delete_request( Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageUploads) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; diff --git a/crates/app/src/routes/pages/chats.rs b/crates/app/src/routes/pages/chats.rs index 0c78039..e6ef791 100644 --- a/crates/app/src/routes/pages/chats.rs +++ b/crates/app/src/routes/pages/chats.rs @@ -159,6 +159,11 @@ pub async fn stream_request( let ignore_users = crate::ignore_users_gen!(user!, data); + let channel = match data.0.get_channel_by_id(channel).await { + Ok(c) => c, + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + }; + let membership = match data .0 .get_membership_by_owner_community(user.id, community) @@ -168,13 +173,19 @@ pub async fn stream_request( Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), }; + if !channel.check_read(user.id, Some(membership.role)) { + return Err(Html( + render_error(Error::NotAllowed, &jar, &data, &Some(user)).await, + )); + } + let can_manage_messages = membership.role.check(CommunityPermission::MANAGE_MESSAGES) | user.permissions.check(FinePermission::MANAGE_MESSAGES); let messages = if props.message == 0 { match data .0 - .get_messages_by_channel(channel, 24, props.page) + .get_messages_by_channel(channel.id, 24, props.page) .await { Ok(p) => match data.0.fill_messages(p, &ignore_users).await { diff --git a/crates/core/src/model/oauth.rs b/crates/core/src/model/oauth.rs index 006690f..6ac3e83 100644 --- a/crates/core/src/model/oauth.rs +++ b/crates/core/src/model/oauth.rs @@ -70,6 +70,8 @@ pub enum AppScope { UserCreateDrafts, /// Create communities on behalf of the user. UserCreateCommunities, + /// Create stacks on behalf of the user. + UserCreateStacks, /// Delete posts owned by the user. UserDeletePosts, /// Delete messages owned by the user. @@ -98,12 +100,16 @@ pub enum AppScope { UserManageNotifications, /// Manage the user's requests. UserManageRequests, + /// Manage the user's uploads. + UserManageUploads, /// Edit posts created by the user. UserEditPosts, /// Edit drafts created by the user. UserEditDrafts, /// Vote in polls as the user. UserVote, + /// React to posts on behalf of the user. Also allows the removal of reactions. + UserReact, /// Join communities on behalf of the user. UserJoinCommunities, /// Permanently delete posts. @@ -126,6 +132,10 @@ pub enum AppScope { CommunityTransferOwnership, /// Read the membership of users in communities owned by the current user. CommunityReadMemberships, + /// Create channels in the user's communities. + CommunityCreateChannels, + /// Manage channels in the user's communities. + CommunityManageChannels, } impl AppScope { @@ -164,9 +174,11 @@ impl AppScope { "user-manage-blocks" => Self::UserManageBlocks, "user-manage-notifications" => Self::UserManageNotifications, "user-manage-requests" => Self::UserManageRequests, + "user-manage-uploads" => Self::UserManageUploads, "user-edit-posts" => Self::UserEditPosts, "user-edit-drafts" => Self::UserEditDrafts, "user-vote" => Self::UserVote, + "user-react" => Self::UserReact, "user-join-communities" => Self::UserJoinCommunities, "mod-purge-posts" => Self::ModPurgePosts, "mod-delete-posts" => Self::ModDeletePosts, @@ -178,6 +190,8 @@ impl AppScope { "community-manage" => Self::CommunityManage, "community-transfer-ownership" => Self::CommunityTransferOwnership, "community-read-memberships" => Self::CommunityReadMemberships, + "community-create-channels" => Self::CommunityCreateChannels, + "community-manage-channels" => Self::CommunityManageChannels, _ => continue, }) }