feat(notify): add notif to internal users when granted
- add coalesced protection to avoid mail bombing if an invited goes many grant in a short period
- add resentd method in share menu item (work for both internal and external users)
- user can disable email notification via his properties
- add env variable from admin to disable notifications
This commit is contained in:
@@ -761,6 +761,20 @@ pub struct MagicLinkConfig {
|
||||
/// may enforce MFA we shouldn't bypass. See
|
||||
/// `magic_link_eligibility()` for the precedence ladder.
|
||||
pub open_to_password_users: bool,
|
||||
/// Operator-level kill switch for plain-notification emails to
|
||||
/// internal users (PR N1). When `true` (default), users who can't
|
||||
/// receive a magic link (password users, OIDC users) get a "Hey,
|
||||
/// you got a new grant" mail with a `/login` deep link on every
|
||||
/// share. When `false`, the plain-notification arm is suppressed
|
||||
/// entirely — internal users discover shares only on next login.
|
||||
///
|
||||
/// This is a coarser knob than the per-user
|
||||
/// `auth.users.notify_on_share` column: when this is `false`, the
|
||||
/// user-level opt-in does not matter. External-user magic-link
|
||||
/// invitations are NOT affected by this flag — those always send,
|
||||
/// because the link is the only way the recipient can claim the
|
||||
/// share for the first time.
|
||||
pub notify_internal_users_on_share: bool,
|
||||
}
|
||||
|
||||
impl Default for MagicLinkConfig {
|
||||
@@ -774,6 +788,7 @@ impl Default for MagicLinkConfig {
|
||||
send_per_email_per_hour: 5,
|
||||
send_per_ip_per_hour: 200,
|
||||
open_to_password_users: false,
|
||||
notify_internal_users_on_share: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1475,6 +1490,9 @@ impl AppConfig {
|
||||
if let Ok(v) = env::var("OXICLOUD_MAGIC_LINK_OPEN_TO_PASSWORD_USERS") {
|
||||
config.magic_link.open_to_password_users = v == "true" || v == "1";
|
||||
}
|
||||
if let Ok(v) = env::var("OXICLOUD_NOTIFY_INTERNAL_USERS_ON_SHARE") {
|
||||
config.magic_link.notify_internal_users_on_share = v == "true" || v == "1";
|
||||
}
|
||||
|
||||
if let Ok(v) = env::var("OXICLOUD_DEFAULT_LOCALE") {
|
||||
let trimmed = v.trim();
|
||||
|
||||
+39
-5
@@ -948,9 +948,10 @@ impl AppServiceFactory {
|
||||
),
|
||||
),
|
||||
)),
|
||||
email_sender: None, // populated below
|
||||
mock_email_sender: None, // populated below
|
||||
magic_link_invite_service: None, // populated below
|
||||
email_sender: None, // populated below
|
||||
mock_email_sender: None, // populated below
|
||||
magic_link_invite_service: None, // populated below
|
||||
recipient_notification_service: None, // populated below alongside magic_link_invite_service
|
||||
// 60 lookups / minute / caller; cap at 50 000 tracked
|
||||
// callers to bound memory. The same limiter instance is
|
||||
// shared by every clone of AppState since it lives in an
|
||||
@@ -1013,9 +1014,9 @@ impl AppServiceFactory {
|
||||
);
|
||||
app_state.magic_link_invite_service = Some(Arc::new(
|
||||
crate::application::services::magic_link_invite_service::MagicLinkInviteService::new(
|
||||
invite_user_storage,
|
||||
invite_user_storage.clone(),
|
||||
invite_magic_link_repo,
|
||||
email_sender,
|
||||
email_sender.clone(),
|
||||
lifecycle,
|
||||
app_state.applications.i18n_service.clone(),
|
||||
app_state.locale_registry.clone(),
|
||||
@@ -1023,6 +1024,30 @@ impl AppServiceFactory {
|
||||
self.config.base_url(),
|
||||
),
|
||||
));
|
||||
|
||||
// PR N1: wire the unified RecipientNotificationService.
|
||||
// Only constructed when MagicLinkInviteService is also
|
||||
// available — the magic-link path delegates to it.
|
||||
// SubjectGroupService is built earlier in this factory; the
|
||||
// notification service needs it for the Group subject arm.
|
||||
if let (Some(magic_link_svc), Some(subject_groups)) = (
|
||||
app_state.magic_link_invite_service.clone(),
|
||||
app_state.subject_group_service.clone(),
|
||||
) {
|
||||
app_state.recipient_notification_service = Some(Arc::new(
|
||||
crate::application::services::recipient_notification_service::RecipientNotificationService::new(
|
||||
invite_user_storage,
|
||||
magic_link_svc,
|
||||
email_sender,
|
||||
app_state.applications.i18n_service.clone(),
|
||||
app_state.locale_registry.clone(),
|
||||
subject_groups,
|
||||
app_state.magic_link_send_per_email_rate_limiter.clone(),
|
||||
self.config.magic_link.clone(),
|
||||
self.config.base_url(),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 9b. Wire admin settings service when auth is available
|
||||
@@ -1374,6 +1399,15 @@ pub struct AppState {
|
||||
pub magic_link_invite_service: Option<
|
||||
Arc<crate::application::services::magic_link_invite_service::MagicLinkInviteService>,
|
||||
>,
|
||||
/// Unified share-notification dispatcher (PR N1) — used by both
|
||||
/// `create_grant` and the future `POST /api/grants/{id}/notify` to
|
||||
/// route share emails through coalesce + rate-limit + per-recipient
|
||||
/// dispatch. `None` when SMTP / magic-link / subject-group services
|
||||
/// aren't all configured; callers degrade to silent no-op in that
|
||||
/// case (no mail sent, grant still created).
|
||||
pub recipient_notification_service: Option<
|
||||
Arc<crate::application::services::recipient_notification_service::RecipientNotificationService>,
|
||||
>,
|
||||
/// Per-caller sliding-window limiter for `GET /api/users/{id}`. The
|
||||
/// endpoint's primary defense is the visibility check, but a stale
|
||||
/// JWT could in theory iterate UUIDs against the related-by-grant
|
||||
|
||||
Reference in New Issue
Block a user