diff --git a/crates/ely_app/src/shell/internal_pages/site_permissions_settings.rs b/crates/ely_app/src/shell/internal_pages/site_permissions_settings.rs index 09fd167..09aac29 100644 --- a/crates/ely_app/src/shell/internal_pages/site_permissions_settings.rs +++ b/crates/ely_app/src/shell/internal_pages/site_permissions_settings.rs @@ -25,6 +25,12 @@ impl ElyShell { .gap_5() .child(render_site_permissions_header(snapshot)) .child(render_site_permissions_summary(snapshot)) + .child(render_site_permissions_controls( + snapshot, + self.site_permissions_clear_confirmation.as_ref() + == Some(&snapshot.active_profile_id), + cx, + )) .child(render_site_permissions_list(snapshot, cx)), ) } @@ -102,6 +108,102 @@ fn site_permission_metric(label: &'static str, value: usize) -> AnyElement { .into_any_element() } +fn render_site_permissions_controls( + snapshot: &BrowserSnapshot, + confirming_clear: bool, + cx: &mut Context, +) -> AnyElement { + if snapshot.site_permissions.is_empty() { + return div().into_any_element(); + } + + if confirming_clear { + return render_clear_confirmation(cx); + } + + div() + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .text_sm() + .text_color(rgb(colors::MUTED)) + .child("Clear all configured permissions for this Profile."), + ) + .child( + Button::new("request-clear-site-permissions") + .danger() + .xsmall() + .label("Clear Permissions") + .tooltip("Clear Profile Permissions") + .on_click(cx.listener(|shell, _, _, cx| { + shell.request_clear_site_permissions_confirmation(cx); + })), + ) + .into_any_element() +} + +fn render_clear_confirmation(cx: &mut Context) -> AnyElement { + div() + .rounded_md() + .border_1() + .border_color(rgb(colors::ERROR)) + .bg(rgb(colors::CANVAS_SOFT)) + .px_4() + .py_3() + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .text_color(rgb(colors::INK)) + .child("Confirm permission clearing"), + ) + .child( + div() + .text_xs() + .text_color(rgb(colors::MUTED)) + .child("Each cleared permission records a local audit event."), + ), + ) + .child( + div() + .flex() + .items_center() + .gap_2() + .child( + Button::new("cancel-clear-site-permissions") + .ghost() + .xsmall() + .label("Cancel") + .on_click(cx.listener(|shell, _, _, cx| { + shell.cancel_clear_site_permissions_confirmation(cx); + })), + ) + .child( + Button::new("confirm-clear-site-permissions") + .danger() + .xsmall() + .label("Clear") + .on_click(cx.listener(|shell, _, _, cx| { + shell.clear_active_profile_site_permissions(cx); + })), + ), + ) + .into_any_element() +} + fn render_site_permissions_list( snapshot: &BrowserSnapshot, cx: &mut Context, diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index c718ab4..d7de6d3 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -36,6 +36,7 @@ pub struct ElyShell { download_action_error: Option, download_clear_confirmation: bool, download_security_confirmation: Option, + site_permissions_clear_confirmation: Option, plugin_install_error: Option, pending_plugin_install: Option, pending_plugin_uninstall: Option, @@ -97,6 +98,7 @@ impl ElyShell { download_action_error: None, download_clear_confirmation: false, download_security_confirmation: None, + site_permissions_clear_confirmation: None, plugin_install_error: None, pending_plugin_install: None, pending_plugin_uninstall: None, diff --git a/crates/ely_app/src/shell/site_permissions.rs b/crates/ely_app/src/shell/site_permissions.rs index 2b72853..d1f967b 100644 --- a/crates/ely_app/src/shell/site_permissions.rs +++ b/crates/ely_app/src/shell/site_permissions.rs @@ -30,4 +30,30 @@ impl ElyShell { cx.notify(); } } + + pub(super) fn request_clear_site_permissions_confirmation(&mut self, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && let Ok(snapshot) = core.snapshot() + { + self.site_permissions_clear_confirmation = Some(snapshot.active_profile_id); + cx.notify(); + } + } + + pub(super) fn cancel_clear_site_permissions_confirmation(&mut self, cx: &mut Context) { + self.site_permissions_clear_confirmation = None; + cx.notify(); + } + + pub(super) fn clear_active_profile_site_permissions(&mut self, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && let Ok(snapshot) = core.snapshot() + && self.site_permissions_clear_confirmation.as_ref() + == Some(&snapshot.active_profile_id) + && core.clear_active_profile_site_permissions().is_ok() + { + self.site_permissions_clear_confirmation = None; + cx.notify(); + } + } } diff --git a/crates/ely_browser_core/src/state/site_permissions.rs b/crates/ely_browser_core/src/state/site_permissions.rs index 49e5aba..084ff2e 100644 --- a/crates/ely_browser_core/src/state/site_permissions.rs +++ b/crates/ely_browser_core/src/state/site_permissions.rs @@ -29,6 +29,11 @@ impl BrowserCore { self.revoke_site_permission_for_profile(&profile_id, origin, feature) } + pub fn clear_active_profile_site_permissions(&mut self) -> Result { + let profile_id = self.active_profile_id.clone(); + self.clear_site_permissions_for_profile(&profile_id) + } + pub fn active_tab_site_settings_url(&self) -> Result, CoreError> { let active_tab = self.active_tab()?; let Some(origin) = SiteOrigin::from_url(active_tab.url())? else { @@ -106,6 +111,35 @@ impl BrowserCore { Ok(()) } + fn clear_site_permissions_for_profile( + &mut self, + profile_id: &ProfileId, + ) -> Result { + self.require_profile(profile_id)?; + let mut revoked_permissions = Vec::new(); + + self.site_permissions.retain(|entry| { + if entry.profile_id() == profile_id { + revoked_permissions.push((entry.origin().clone(), entry.feature())); + false + } else { + true + } + }); + + let revoked_count = revoked_permissions.len(); + for (origin, feature) in revoked_permissions { + self.record_site_permission_audit_event( + profile_id.clone(), + origin, + feature, + SitePermissionAuditAction::Revoked, + ); + } + + Ok(revoked_count) + } + fn require_profile(&self, profile_id: &ProfileId) -> Result<(), CoreError> { if self.profiles.iter().any(|profile| profile.id() == profile_id) { return Ok(()); diff --git a/crates/ely_browser_core/tests/site_permissions.rs b/crates/ely_browser_core/tests/site_permissions.rs index b416303..e3a1556 100644 --- a/crates/ely_browser_core/tests/site_permissions.rs +++ b/crates/ely_browser_core/tests/site_permissions.rs @@ -90,6 +90,56 @@ fn revoke_site_permission_removes_entry_and_records_audit() -> Result<(), Box Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let default_profile_id = core.snapshot()?.active_profile_id; + let personal_profile_id = core.create_profile("Personal", 0xf54e00, ProfileKind::Standard)?; + let default_origin = SiteOrigin::parse("https://example.com")?; + let personal_origin = SiteOrigin::parse("https://personal.example")?; + + core.set_site_permission( + personal_origin, + SitePermissionFeature::Notifications, + SitePermissionDecision::DenyAlways, + )?; + core.select_profile(&default_profile_id)?; + core.set_site_permission( + default_origin, + SitePermissionFeature::Camera, + SitePermissionDecision::AllowAlways, + )?; + + let removed_count = core.clear_active_profile_site_permissions()?; + let default_snapshot = core.snapshot()?; + assert_eq!(removed_count, 1); + assert!(default_snapshot.site_permissions.is_empty()); + assert_eq!(default_snapshot.site_permission_audit_events.len(), 2); + assert_eq!( + default_snapshot.site_permission_audit_events[1].action(), + &SitePermissionAuditAction::Revoked, + ); + + core.select_profile(&personal_profile_id)?; + let personal_snapshot = core.snapshot()?; + assert_eq!(personal_snapshot.site_permissions.len(), 1); + assert_eq!(personal_snapshot.site_permission_audit_events.len(), 1); + Ok(()) +} + +#[test] +fn clear_site_permissions_without_entries_is_empty_change() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + + let removed_count = core.clear_active_profile_site_permissions()?; + let snapshot = core.snapshot()?; + + assert_eq!(removed_count, 0); + assert!(snapshot.site_permissions.is_empty()); + assert!(snapshot.site_permission_audit_events.is_empty()); + Ok(()) +} + #[test] fn site_settings_command_opens_active_origin() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;