mirror of
https://github.com/modrinth/code.git
synced 2026-09-03 13:36:48 +00:00
feat: use discord_id from discord sso for role grant (#6326)
* feat: properly use labrinth's linked discord accts sso for discord bot * Update email for fixture user in SQL insert Signed-off-by: Calum H. <calum@modrinth.com> * fix: rev changes * fix: copy on email * fix: lint * fix: rev * fix: lint --------- Signed-off-by: Calum H. <calum@modrinth.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::ids::DBUserId;
|
||||
use crate::database::models::ids::{DBNotificationId, DBUserId};
|
||||
use crate::database::models::notification_item::DBNotification;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::user_item::DBUser;
|
||||
@@ -64,34 +64,12 @@ pub async fn create(
|
||||
.insert_many(user_ids, &mut txn, &redis)
|
||||
.await?;
|
||||
|
||||
let notifications = DBNotification::get_many(¬ification_ids, &mut txn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Notification::from)
|
||||
.collect::<Vec<_>>();
|
||||
let notifications =
|
||||
get_site_exposed_notifications(¬ification_ids, &mut txn).await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
for notification in notifications {
|
||||
let notification_id = notification.id;
|
||||
let to_user = notification.user_id;
|
||||
if let Err(error) = broadcast_friends_message(
|
||||
&redis,
|
||||
RedisFriendsMessage::Notification {
|
||||
to_user,
|
||||
notification,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
?error,
|
||||
?notification_id,
|
||||
?to_user,
|
||||
"failed to broadcast realtime notification"
|
||||
);
|
||||
}
|
||||
}
|
||||
broadcast_notifications(&redis, notifications).await;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
@@ -155,37 +133,12 @@ pub async fn create_email_sync(
|
||||
.insert_many_without_delivery(notification_user_ids, &mut txn, &redis)
|
||||
.await?;
|
||||
|
||||
let notifications = DBNotification::get_many(¬ification_ids, &mut txn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Notification::from)
|
||||
.collect::<Vec<_>>();
|
||||
let notifications =
|
||||
get_site_exposed_notifications(¬ification_ids, &mut txn).await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
for notification in notifications {
|
||||
let Notification {
|
||||
user_id: to_user,
|
||||
id: notification_id,
|
||||
..
|
||||
} = notification;
|
||||
if let Err(error) = broadcast_friends_message(
|
||||
&redis,
|
||||
RedisFriendsMessage::Notification {
|
||||
to_user,
|
||||
notification,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
?error,
|
||||
?notification_id,
|
||||
?to_user,
|
||||
"failed to broadcast realtime notification"
|
||||
);
|
||||
}
|
||||
}
|
||||
broadcast_notifications(&redis, notifications).await;
|
||||
|
||||
let mut email_txn = pool.begin().await?;
|
||||
|
||||
@@ -332,3 +285,57 @@ pub async fn send_custom_email(
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
async fn get_site_exposed_notifications(
|
||||
notification_ids: &[DBNotificationId],
|
||||
txn: &mut crate::database::PgTransaction<'_>,
|
||||
) -> Result<Vec<Notification>, ApiError> {
|
||||
let raw_ids = notification_ids.iter().map(|x| x.0).collect::<Vec<_>>();
|
||||
let exposed_ids = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT n.id AS "id!"
|
||||
FROM notifications n
|
||||
INNER JOIN notifications_types nt ON nt.name = n.body ->> 'type'
|
||||
WHERE n.id = ANY($1::BIGINT[])
|
||||
AND nt.expose_in_site_notifications = TRUE
|
||||
"#,
|
||||
&raw_ids[..],
|
||||
)
|
||||
.fetch_all(&mut *txn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(DBNotificationId)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(DBNotification::get_many(&exposed_ids, txn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Notification::from)
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn broadcast_notifications(
|
||||
redis: &RedisPool,
|
||||
notifications: Vec<Notification>,
|
||||
) {
|
||||
for notification in notifications {
|
||||
let notification_id = notification.id;
|
||||
let to_user = notification.user_id;
|
||||
if let Err(error) = broadcast_friends_message(
|
||||
redis,
|
||||
RedisFriendsMessage::Notification {
|
||||
to_user,
|
||||
notification,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
?error,
|
||||
?notification_id,
|
||||
?to_user,
|
||||
"failed to broadcast realtime notification"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,18 @@ use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use ariadne::ids::base62_impl::{parse_base62, to_base62};
|
||||
use ariadne::ids::random_base62_rng;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use chrono::{Duration, Utc};
|
||||
use eyre::eyre;
|
||||
use hmac::{Hmac, Mac};
|
||||
use lettre::message::Mailbox;
|
||||
use rand::Rng;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -61,7 +66,8 @@ pub fn config(cfg: &mut utoipa_actix_web::service_config::ServiceConfig) {
|
||||
.service(set_email)
|
||||
.service(verify_email)
|
||||
.service(subscribe_newsletter)
|
||||
.service(get_newsletter_subscription_status),
|
||||
.service(get_newsletter_subscription_status)
|
||||
.service(discord_community_link),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1369,6 +1375,97 @@ pub struct DeleteAuthProvider {
|
||||
pub provider: AuthProvider,
|
||||
}
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct DiscordCommunityLinkResponse {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DiscordCommunityHandoffPayload {
|
||||
v: u8,
|
||||
modrinth_user_id: String,
|
||||
discord_user_id: String,
|
||||
iat: i64,
|
||||
exp: i64,
|
||||
nonce: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
operation_id = "discordCommunityLink",
|
||||
responses(
|
||||
(status = 200, description = "Discord community bot handoff URL", body = DiscordCommunityLinkResponse),
|
||||
(status = 400, description = "Discord provider not linked"),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
security(("bearer_auth" = ["SESSION_ACCESS"]))
|
||||
)]
|
||||
#[post("/discord-community-link")]
|
||||
pub async fn discord_community_link(
|
||||
req: HttpRequest,
|
||||
client: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<web::Json<DiscordCommunityLinkResponse>, ApiError> {
|
||||
if ENV.DISCORD_COMMUNITY_LINK_SECRET.is_empty()
|
||||
|| ENV.DISCORD_COMMUNITY_BOT_HANDOFF_URL.is_empty()
|
||||
{
|
||||
return Err(ApiError::Internal(eyre!(
|
||||
"discord community linking is not configured"
|
||||
)));
|
||||
}
|
||||
|
||||
let db_user = get_full_user_from_headers(
|
||||
&req,
|
||||
&**client,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let Some(discord_id) = db_user.discord_id else {
|
||||
return Err(ApiError::Request(eyre!("discord account is not linked")));
|
||||
};
|
||||
|
||||
let now = Utc::now().timestamp();
|
||||
let nonce = ChaCha20Rng::from_entropy()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(32)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
|
||||
let payload = DiscordCommunityHandoffPayload {
|
||||
v: 1,
|
||||
modrinth_user_id: ariadne::ids::UserId::from(db_user.id).to_string(),
|
||||
discord_user_id: discord_id.to_string(),
|
||||
iat: now,
|
||||
exp: now + 600,
|
||||
nonce,
|
||||
};
|
||||
|
||||
let payload_json = serde_json::to_vec(&payload).wrap_internal_err(
|
||||
"failed to serialize discord community handoff payload",
|
||||
)?;
|
||||
let payload_b64 = URL_SAFE_NO_PAD.encode(payload_json);
|
||||
|
||||
let mut mac = HmacSha256::new_from_slice(
|
||||
ENV.DISCORD_COMMUNITY_LINK_SECRET.as_bytes(),
|
||||
)
|
||||
.wrap_internal_err("failed to initialize discord community link hmac")?;
|
||||
mac.update(payload_b64.as_bytes());
|
||||
let sig = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
|
||||
|
||||
let url = format!(
|
||||
"{}?payload={}&sig={}",
|
||||
ENV.DISCORD_COMMUNITY_BOT_HANDOFF_URL, payload_b64, sig,
|
||||
);
|
||||
|
||||
Ok(web::Json(DiscordCommunityLinkResponse { url }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
operation_id = "deleteAuthProvider",
|
||||
|
||||
@@ -54,7 +54,8 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
.service(flows::set_email)
|
||||
.service(flows::verify_email)
|
||||
.service(flows::subscribe_newsletter)
|
||||
.service(flows::get_newsletter_subscription_status),
|
||||
.service(flows::get_newsletter_subscription_status)
|
||||
.service(flows::discord_community_link),
|
||||
);
|
||||
cfg.service(pats::get_pats);
|
||||
cfg.service(pats::create_pat);
|
||||
|
||||
Reference in New Issue
Block a user