From c3139ef1d2a07c1ab3120b4eae5f0fcdfb9b8c9f Mon Sep 17 00:00:00 2001 From: trisua Date: Fri, 13 Jun 2025 12:49:09 -0400 Subject: [PATCH] add: grant scopes for all community endpoints --- crates/app/src/macros.rs | 43 +++++-- crates/app/src/routes/api/v1/auth/profile.rs | 34 ++++-- .../routes/api/v1/communities/communities.rs | 80 +++++++++--- .../src/routes/api/v1/communities/drafts.rs | 87 ++++++++++++- .../src/routes/api/v1/communities/emojis.rs | 9 +- .../src/routes/api/v1/communities/images.rs | 6 +- .../src/routes/api/v1/communities/posts.rs | 19 +-- .../routes/api/v1/communities/questions.rs | 10 +- crates/app/src/routes/api/v1/mod.rs | 14 +++ crates/core/src/model/oauth.rs | 115 ++++++++++++++---- 10 files changed, 342 insertions(+), 75 deletions(-) diff --git a/crates/app/src/macros.rs b/crates/app/src/macros.rs index 7c4025e..b91fbbb 100644 --- a/crates/app/src/macros.rs +++ b/crates/app/src/macros.rs @@ -77,17 +77,15 @@ macro_rules! create_dir_if_not_exists { #[macro_export] macro_rules! get_user_from_token { ($jar:ident, $db:expr) => {{ - if let Some(token) = $jar.get("Atto-Grant") { - // this allows us to ALSO authenticate with a grant token... - // TODO: require macro to pass a required AppScope to check permission - // TODO: check token verifier + // pages; regular token only + if let Some(token) = $jar.get("__Secure-atto-token") { match $db - .get_user_by_grant_token(&tetratto_shared::hash::hash( - token.to_string().replace("Atto-Grant=", ""), + .get_user_by_token(&tetratto_shared::hash::hash( + token.to_string().replace("__Secure-atto-token=", ""), )) .await { - Ok((_, ua)) => { + Ok(ua) => { if ua.permissions.check_banned() { Some(tetratto_core::model::auth::User::banned()) } else { @@ -96,7 +94,38 @@ macro_rules! get_user_from_token { } Err(_) => None, } + } else { + None + } + }}; + + ($jar:ident, $db:expr, $grant_scope:expr) => {{ + if let Some(token) = $jar.get("Atto-Grant") + && let Some(verifier) = $jar.get("Atto-Grant-Verifier") + { + // grant token + let verifier = verifier.to_string().replace("Atto-Grant-Verifier=", ""); + match $db + .get_user_by_grant_token(&token.to_string().replace("Atto-Grant=", "")) + .await + { + Ok((grant, ua)) => { + if grant.scopes.contains(&$grant_scope) + && grant.check_verifier(&verifier).is_ok() + { + if ua.permissions.check_banned() { + Some(tetratto_core::model::auth::User::banned()) + } else { + Some(ua) + } + } else { + None + } + } + Err(_) => None, + } } else if let Some(token) = $jar.get("__Secure-atto-token") { + // regular token match $db .get_user_by_token(&tetratto_shared::hash::hash( token.to_string().replace("__Secure-atto-token=", ""), diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs index 91e3908..987536f 100644 --- a/crates/app/src/routes/api/v1/auth/profile.rs +++ b/crates/app/src/routes/api/v1/auth/profile.rs @@ -22,6 +22,7 @@ use tetratto_core::{ cache::Cache, model::{ auth::{Token, UserSettings}, + oauth, permissions::FinePermission, socket::{PacketType, SocketMessage, SocketMethod}, }, @@ -72,7 +73,7 @@ pub async fn redirect_from_ip( pub async fn me_request(jar: CookieJar, Extension(data): Extension) -> 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::UserReadProfile) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -92,7 +93,7 @@ pub async fn update_user_settings_request( Json(mut 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::UserManageProfile) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -145,7 +146,7 @@ pub async fn append_associations_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let mut user = match get_user_from_token!(jar, data) { + let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProfile) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -188,6 +189,8 @@ pub async fn append_associations_request( } /// Update the password of the given user. +/// +/// Does not support third-party grants. pub async fn update_user_password_request( jar: CookieJar, Path(id): Path, @@ -218,6 +221,9 @@ pub async fn update_user_password_request( } } +/// Update a user's username. +/// +/// Does not support third-party grants. pub async fn update_user_username_request( jar: CookieJar, Path(id): Path, @@ -249,6 +255,8 @@ pub async fn update_user_username_request( } /// Update the tokens of the given user. +/// +/// Does not support third-party grants. pub async fn update_user_tokens_request( jar: CookieJar, Path(id): Path, @@ -276,6 +284,8 @@ pub async fn update_user_tokens_request( } /// Update the verification status of the given user. +/// +/// Does not support third-party grants. pub async fn update_user_is_verified_request( jar: CookieJar, Path(id): Path, @@ -302,6 +312,8 @@ pub async fn update_user_is_verified_request( } /// Update the role of the given user. +/// +/// Does not support third-party grants. pub async fn update_user_role_request( jar: CookieJar, Path(id): Path, @@ -327,7 +339,7 @@ pub async fn update_user_role_request( /// Update the current user's last seen value. pub async fn seen_request(jar: CookieJar, Extension(data): Extension) -> 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::UserManageProfile) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -343,6 +355,8 @@ pub async fn seen_request(jar: CookieJar, Extension(data): Extension) -> } /// Delete the given user. +/// +/// Does not support third-party grants. pub async fn delete_user_request( jar: CookieJar, Path(id): Path, @@ -373,6 +387,8 @@ pub async fn delete_user_request( } /// Enable TOTP for a user. +/// +/// Does not support third-party grants. pub async fn enable_totp_request( jar: CookieJar, Path(id): Path, @@ -395,6 +411,8 @@ pub async fn enable_totp_request( } /// Disable TOTP for a user. +/// +/// Does not support third-party grants. pub async fn disable_totp_request( jar: CookieJar, Path(id): Path, @@ -433,6 +451,8 @@ pub async fn disable_totp_request( } /// Refresh TOTP recovery codes for a user. +/// +/// Does not support third-party grants. pub async fn refresh_totp_codes_request( jar: CookieJar, Path(id): Path, @@ -498,7 +518,7 @@ pub async fn subscription_handler( Path((user_id, id)): Path<(String, String)>, ) -> 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::UserReadSockets) { Some(ua) => ua, None => return Err("Socket refused"), }; @@ -624,7 +644,7 @@ pub async fn post_to_socket_request( Json(msg): 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::UserReadSockets) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -654,7 +674,7 @@ pub async fn get_user_gpa_request( Extension(data): Extension, ) -> 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::UserReadProfile) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; diff --git a/crates/app/src/routes/api/v1/communities/communities.rs b/crates/app/src/routes/api/v1/communities/communities.rs index adda953..e2c72f1 100644 --- a/crates/app/src/routes/api/v1/communities/communities.rs +++ b/crates/app/src/routes/api/v1/communities/communities.rs @@ -5,10 +5,10 @@ use axum::{ }; use axum_extra::extract::CookieJar; use tetratto_core::model::{ - ApiReturn, Error, auth::Notification, communities::{Community, CommunityMembership}, communities_permissions::CommunityPermission, + oauth, ApiReturn, Error, }; use crate::{ @@ -44,7 +44,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::UserCreateCommunities) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -73,7 +73,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::CommunityDelete) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -95,7 +95,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::CommunityManage) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -117,7 +117,7 @@ pub async fn update_context_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::CommunityManage) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -149,7 +149,7 @@ pub async fn update_read_access_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::CommunityManage) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -174,7 +174,7 @@ pub async fn update_write_access_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::CommunityManage) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -199,7 +199,7 @@ pub async fn update_join_access_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::CommunityManage) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -224,7 +224,7 @@ pub async fn update_owner_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::CommunityTransferOwnership) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -255,7 +255,7 @@ pub async fn get_membership( Path((cid, uid)): Path<(usize, usize)>, ) -> 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::CommunityReadMemberships) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -286,7 +286,7 @@ pub async fn create_membership( 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::UserJoinCommunities) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -314,7 +314,7 @@ pub async fn delete_membership( Path((cid, uid)): Path<(usize, usize)>, ) -> 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::UserManageMemberships) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -341,7 +341,7 @@ pub async fn update_membership_role( 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::UserManageMemberships) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -449,7 +449,7 @@ pub async fn supports_titles_request( Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - if get_user_from_token!(jar, data).is_none() { + if get_user_from_token!(jar, data, oauth::AppScope::UserReadCommunities).is_none() { return Json(Error::NotAllowed.into()); } @@ -468,3 +468,55 @@ pub async fn supports_titles_request( payload: (), }) } + +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::UserReadCommunities).is_none() { + return Json(Error::NotAllowed.into()); + } + + match data.get_community_by_id_no_void(id).await { + Ok(c) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(c), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn get_communities_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::UserReadCommunities) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + let memberships = match data.get_memberships_by_owner(user.id).await { + Ok(p) => p, + Err(e) => return Json(e.into()), + }; + + let mut communities: Vec = Vec::new(); + for membership in memberships { + let community = match data.get_community_by_id_no_void(membership.community).await { + Ok(p) => p, + Err(e) => return Json(e.into()), + }; + + communities.push(community) + } + + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(communities), + }) +} diff --git a/crates/app/src/routes/api/v1/communities/drafts.rs b/crates/app/src/routes/api/v1/communities/drafts.rs index f181f05..a6de4c9 100644 --- a/crates/app/src/routes/api/v1/communities/drafts.rs +++ b/crates/app/src/routes/api/v1/communities/drafts.rs @@ -1,9 +1,16 @@ -use axum::{extract::Path, response::IntoResponse, Extension, Json}; +use axum::{ + extract::{Path, Query}, + response::IntoResponse, + Extension, Json, +}; use axum_extra::extract::CookieJar; -use tetratto_core::model::{communities::PostDraft, ApiReturn, Error}; +use tetratto_core::model::{communities::PostDraft, oauth, ApiReturn, Error}; use crate::{ get_user_from_token, - routes::api::v1::{CreatePostDraft, UpdatePostContent}, + routes::{ + api::v1::{CreatePostDraft, UpdatePostContent}, + pages::PaginatedQuery, + }, State, }; @@ -13,7 +20,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::UserCreateDrafts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -37,7 +44,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::UserDeleteDrafts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -59,7 +66,7 @@ pub async fn update_content_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::UserEditDrafts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -73,3 +80,71 @@ pub async fn update_content_request( 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; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadDrafts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_draft_by_id(id).await { + Ok(d) => { + if d.owner != user.id { + return Json(Error::NotAllowed.into()); + } + + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(d), + }) + } + Err(e) => Json(e.into()), + } +} + +pub async fn get_drafts_request( + jar: CookieJar, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadDrafts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_drafts_by_user(user.id, 12, props.page).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn get_all_drafts_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::UserReadDrafts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_drafts_by_user_all(user.id).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} diff --git a/crates/app/src/routes/api/v1/communities/emojis.rs b/crates/app/src/routes/api/v1/communities/emojis.rs index fd45c21..6f0d037 100644 --- a/crates/app/src/routes/api/v1/communities/emojis.rs +++ b/crates/app/src/routes/api/v1/communities/emojis.rs @@ -9,6 +9,7 @@ use crate::{ use axum::{body::Body, extract::Path, response::IntoResponse, Extension, Json}; use axum_extra::extract::CookieJar; use tetratto_core::model::{ + oauth, uploads::{CustomEmoji, MediaType, MediaUpload}, ApiReturn, Error, }; @@ -88,7 +89,7 @@ pub async fn create_request( img: Image, ) -> 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::CommunityCreateEmojis) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -174,7 +175,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::CommunityManageEmojis) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -195,7 +196,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::CommunityManageEmojis) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -215,7 +216,7 @@ pub async fn get_my_request( Extension(data): Extension, ) -> 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::UserReadEmojis) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; diff --git a/crates/app/src/routes/api/v1/communities/images.rs b/crates/app/src/routes/api/v1/communities/images.rs index 25a1d1f..464dede 100644 --- a/crates/app/src/routes/api/v1/communities/images.rs +++ b/crates/app/src/routes/api/v1/communities/images.rs @@ -2,7 +2,7 @@ use axum::{Extension, Json, body::Body, extract::Path, response::IntoResponse}; use axum_extra::extract::CookieJar; use pathbufd::{PathBufD, pathd}; use std::fs::exists; -use tetratto_core::model::{ApiReturn, Error, permissions::FinePermission}; +use tetratto_core::model::{ApiReturn, Error, permissions::FinePermission, oauth}; use crate::{ State, @@ -110,7 +110,7 @@ pub async fn upload_avatar_request( ) -> impl IntoResponse { // get user from token let data = &(data.read().await).0; - let auth_user = match get_user_from_token!(jar, data) { + let auth_user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManage) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -165,7 +165,7 @@ pub async fn upload_banner_request( ) -> impl IntoResponse { // get user from token let data = &(data.read().await).0; - let auth_user = match get_user_from_token!(jar, data) { + let auth_user = match get_user_from_token!(jar, data, oauth::AppScope::CommunityManage) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs index f9139ba..dc3292e 100644 --- a/crates/app/src/routes/api/v1/communities/posts.rs +++ b/crates/app/src/routes/api/v1/communities/posts.rs @@ -8,6 +8,7 @@ use axum_extra::extract::CookieJar; use tetratto_core::model::{ addr::RemoteAddr, communities::{Poll, PollVote, Post}, + oauth, permissions::FinePermission, uploads::{MediaType, MediaUpload}, ApiReturn, Error, @@ -32,7 +33,7 @@ pub async fn create_request( JsonMultipart(images, req): JsonMultipart, ) -> 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::UserCreatePosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -188,7 +189,7 @@ pub async fn create_repost_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::UserCreatePosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -220,7 +221,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::UserDeletePosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -241,7 +242,7 @@ pub async fn purge_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::ModPurgePosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -266,7 +267,7 @@ pub async fn restore_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::ModDeletePosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -292,7 +293,7 @@ pub async fn update_content_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::UserEditPosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -314,7 +315,7 @@ pub async fn update_context_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::UserEditPosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -346,7 +347,7 @@ pub async fn vote_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::UserVote) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -394,7 +395,7 @@ pub async fn update_is_open_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::UserEditPosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; diff --git a/crates/app/src/routes/api/v1/communities/questions.rs b/crates/app/src/routes/api/v1/communities/questions.rs index d6ca0a4..0b85bff 100644 --- a/crates/app/src/routes/api/v1/communities/questions.rs +++ b/crates/app/src/routes/api/v1/communities/questions.rs @@ -5,7 +5,9 @@ use axum::{ Extension, Json, }; use axum_extra::extract::CookieJar; -use tetratto_core::model::{addr::RemoteAddr, auth::IpBlock, communities::Question, ApiReturn, Error}; +use tetratto_core::model::{ + addr::RemoteAddr, auth::IpBlock, communities::Question, ApiReturn, Error, oauth, +}; use crate::{get_user_from_token, routes::api::v1::CreateQuestion, State}; pub async fn create_request( @@ -15,7 +17,7 @@ pub async fn create_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = get_user_from_token!(jar, data); + let user = get_user_from_token!(jar, data, oauth::AppScope::UserCreateQuestions); if req.is_global && user.is_none() { return Json(Error::NotAllowed.into()); @@ -75,7 +77,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::UserDeleteQuestions) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -96,7 +98,7 @@ pub async fn ip_block_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::UserCreateIpBlock) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index c455e25..714f9ab 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -46,6 +46,14 @@ pub fn routes() -> Router { "/communities", post(communities::communities::create_request), ) + .route( + "/communities/my/all", + post(communities::communities::get_communities_request), + ) + .route( + "/communities/{id}", + get(communities::communities::get_request), + ) .route( "/communities/{id}", delete(communities::communities::delete_request), @@ -127,6 +135,12 @@ pub fn routes() -> Router { ) // drafts .route("/drafts", post(communities::drafts::create_request)) + .route("/drafts/my", get(communities::drafts::get_drafts_request)) + .route( + "/drafts/my/all", + get(communities::drafts::get_all_drafts_request), + ) + .route("/drafts/{id}", get(communities::drafts::get_request)) .route("/drafts/{id}", delete(communities::drafts::delete_request)) .route( "/drafts/{id}/content", diff --git a/crates/core/src/model/oauth.rs b/crates/core/src/model/oauth.rs index 4f08cd6..19e716b 100644 --- a/crates/core/src/model/oauth.rs +++ b/crates/core/src/model/oauth.rs @@ -46,20 +46,68 @@ pub enum AppScope { UserReadPosts, /// Read messages as the user. UserReadMessages, + /// Read drafts as the user. + UserReadDrafts, + /// Read the user's communities. + UserReadCommunities, + /// Connect to sockets on the user's behalf. + UserReadSockets, /// Create posts as the user. UserCreatePosts, /// Create messages as the user. UserCreateMessages, + /// Ask questions as the user. + UserCreateQuestions, + /// Create IP blocks as the user. + UserCreateIpBlock, + /// Create drafts on behalf of the user. + UserCreateDrafts, + /// Create communities on behalf of the user. + UserCreateCommunities, /// Delete posts owned by the user. UserDeletePosts, /// Delete messages owned by the user. UserDeleteMessages, + /// Delete questions as the user. + UserDeleteQuestions, + /// Delete drafts as the user. + UserDeleteDrafts, + /// Edit the user's settings and upload avatars/banners on behalf of the user. + UserManageProfile, /// Manage stacks owned by the user. UserManageStacks, /// Manage the user's following/unfollowing. UserManageRelationships, - /// Manage the user's settings. - UserManageSettings, + /// Manage the user's community memberships. + /// + /// Also includes managing the membership of users in the user's communities. + UserManageMemberships, + /// Edit posts created by the user. + UserEditPosts, + /// Edit drafts created by the user. + UserEditDrafts, + /// Vote in polls as the user. + UserVote, + /// Join communities on behalf of the user. + UserJoinCommunities, + /// Permanently delete posts. + ModPurgePosts, + /// Restore deleted posts. + ModDeletePosts, + /// Get a list of all emojis available to the user. + UserReadEmojis, + /// Create emojis on behalf of the user. + CommunityCreateEmojis, + /// Manage emojis on behalf of the user. + CommunityManageEmojis, + /// Delete communities on behalf of the user. + CommunityDelete, + /// Manage communities on behalf of the user. + CommunityManage, + /// Transfer ownership of communities on behalf of the user. + CommunityTransferOwnership, + /// Read the membership of users in communities owned by the current user. + CommunityReadMemberships, } impl AppScope { @@ -73,13 +121,36 @@ impl AppScope { "user-read-sessions" => Self::UserReadSessions, "user-read-posts" => Self::UserReadPosts, "user-read-messages" => Self::UserReadMessages, + "user-read-drafts" => Self::UserReadDrafts, + "user-read-communities" => Self::UserReadCommunities, + "user-read-sockets" => Self::UserReadSockets, "user-create-posts" => Self::UserCreatePosts, "user-create-messages" => Self::UserCreateMessages, + "user-create-questions" => Self::UserCreateQuestions, + "user-create-ip-blocks" => Self::UserCreateIpBlock, + "user-create-drafts" => Self::UserCreateDrafts, + "user-create-communities" => Self::UserCreateCommunities, "user-delete-posts" => Self::UserDeletePosts, "user-delete-messages" => Self::UserDeleteMessages, + "user-delete-questions" => Self::UserDeleteQuestions, + "user-delete-drafts" => Self::UserDeleteDrafts, + "user-manage-profile" => Self::UserManageProfile, "user-manage-stacks" => Self::UserManageStacks, "user-manage-relationships" => Self::UserManageRelationships, - "user-manage-settings" => Self::UserManageSettings, + "user-manage-memberships" => Self::UserManageMemberships, + "user-edit-posts" => Self::UserEditPosts, + "user-edit-drafts" => Self::UserEditDrafts, + "user-vote" => Self::UserVote, + "user-join-communities" => Self::UserJoinCommunities, + "mod-purge-posts" => Self::ModPurgePosts, + "mod-delete-posts" => Self::ModDeletePosts, + "user-read-emojis" => Self::UserReadEmojis, + "community-create-emojis" => Self::CommunityCreateEmojis, + "community-manage-emojis" => Self::CommunityManageEmojis, + "community-delete" => Self::CommunityDelete, + "community-manage" => Self::CommunityManage, + "community-transfer-ownership" => Self::CommunityTransferOwnership, + "community-read-memberships" => Self::CommunityReadMemberships, _ => continue, }) } @@ -87,23 +158,25 @@ impl AppScope { } } -/// Check a verifier against the stored challenge (using the given [`PkceChallengeMethod`]). -pub fn check_verifier(verifier: &str, challenge: &str, method: PkceChallengeMethod) -> Result<()> { - if method != PkceChallengeMethod::S256 { - return Err(Error::MiscError("only S256 is supported".to_string())); +impl AuthGrant { + /// Check a verifier against the stored challenge (using the given [`PkceChallengeMethod`]). + pub fn check_verifier(&self, verifier: &str) -> Result<()> { + if self.method != PkceChallengeMethod::S256 { + return Err(Error::MiscError("only S256 is supported".to_string())); + } + + let decoded = match base64url.decode(self.challenge.as_bytes()) { + Ok(hash) => hash, + Err(e) => return Err(Error::MiscError(e.to_string())), + }; + + let hash = hash(verifier.to_string()); + + if hash.as_bytes() != decoded { + // the verifier we received does not match the verifier from the stored challenge + return Err(Error::NotAllowed); + } + + Ok(()) } - - let decoded = match base64url.decode(challenge.as_bytes()) { - Ok(hash) => hash, - Err(e) => return Err(Error::MiscError(e.to_string())), - }; - - let hash = hash(verifier.to_string()); - - if hash.as_bytes() != decoded { - // the verifier we received does not match the verifier from the stored challenge - return Err(Error::NotAllowed); - } - - Ok(()) }