feat: update notification preferences to include granular security alerts

This commit is contained in:
Usman Baig
2026-02-28 21:18:57 +01:00
parent 15f82eee00
commit 5ef6eafc63
5 changed files with 28 additions and 22 deletions

View File

@@ -30,7 +30,7 @@ function BellIcon({ className }: { className?: string }) {
// --- Types --- // --- Types ---
type ProfileSubTab = 'profile' | 'security' | 'preferences' type ProfileSubTab = 'profile' | 'security' | 'preferences'
type NotificationSubTab = 'email' | 'center' type NotificationSubTab = 'security' | 'center'
type ActiveSelection = type ActiveSelection =
| { section: 'profile'; subTab: ProfileSubTab } | { section: 'profile'; subTab: ProfileSubTab }
@@ -139,12 +139,14 @@ function ExpandableSubItems({ expanded, children }: { expanded: boolean; childre
// --- Content Components --- // --- Content Components ---
// Email Notification Preferences Card // Security Alerts Card (granular security toggles)
const PULSE_NOTIFICATION_OPTIONS = [ const SECURITY_ALERT_OPTIONS = [
{ key: 'security_alerts', label: 'Security Alerts', description: 'Important security events like new logins, password changes, and 2FA updates.' }, { key: 'login_alerts', label: 'Login Activity', description: 'New device sign-ins and suspicious login attempts.' },
{ key: 'password_alerts', label: 'Password Changes', description: 'Password changes and session revocations.' },
{ key: 'two_factor_alerts', label: 'Two-Factor Authentication', description: '2FA enabled/disabled and recovery code changes.' },
] ]
function EmailNotificationPreferencesCard() { function SecurityAlertsCard() {
const { user } = useAuth() const { user } = useAuth()
const [emailNotifications, setEmailNotifications] = useState<Record<string, boolean>>({}) const [emailNotifications, setEmailNotifications] = useState<Record<string, boolean>>({})
@@ -152,7 +154,7 @@ function EmailNotificationPreferencesCard() {
if (user?.preferences?.email_notifications) { if (user?.preferences?.email_notifications) {
setEmailNotifications(user.preferences.email_notifications) setEmailNotifications(user.preferences.email_notifications)
} else { } else {
const defaults = PULSE_NOTIFICATION_OPTIONS.reduce((acc, option) => ({ const defaults = SECURITY_ALERT_OPTIONS.reduce((acc, option) => ({
...acc, ...acc,
[option.key]: true [option.key]: true
}), {} as Record<string, boolean>) }), {} as Record<string, boolean>)
@@ -168,7 +170,7 @@ function EmailNotificationPreferencesCard() {
setEmailNotifications(newState) setEmailNotifications(newState)
try { try {
await updateUserPreferences({ await updateUserPreferences({
email_notifications: newState as { new_file_received: boolean; file_downloaded: boolean; security_alerts: boolean } email_notifications: newState as { new_file_received: boolean; file_downloaded: boolean; login_alerts: boolean; password_alerts: boolean; two_factor_alerts: boolean }
}) })
} catch { } catch {
setEmailNotifications(prev => ({ setEmailNotifications(prev => ({
@@ -185,13 +187,13 @@ function EmailNotificationPreferencesCard() {
<BellIcon className="w-5 h-5 text-brand-orange" /> <BellIcon className="w-5 h-5 text-brand-orange" />
</div> </div>
<div> <div>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-white">Email Notifications</h2> <h2 className="text-lg font-semibold text-neutral-900 dark:text-white">Security Alerts</h2>
<p className="text-sm text-neutral-500">Choose which email notifications you receive</p> <p className="text-sm text-neutral-500">Choose which security events trigger email alerts</p>
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{PULSE_NOTIFICATION_OPTIONS.map((item) => ( {SECURITY_ALERT_OPTIONS.map((item) => (
<div <div
key={item.key} key={item.key}
className={`flex items-center justify-between p-4 border rounded-xl transition-all duration-200 ${ className={`flex items-center justify-between p-4 border rounded-xl transition-all duration-200 ${
@@ -328,7 +330,7 @@ function AppSettingsSection() {
case 'profile': case 'profile':
return <ProfileSettings activeTab={active.subTab} /> return <ProfileSettings activeTab={active.subTab} />
case 'notifications': case 'notifications':
if (active.subTab === 'email') return <EmailNotificationPreferencesCard /> if (active.subTab === 'security') return <SecurityAlertsCard />
if (active.subTab === 'center') return ( if (active.subTab === 'center') return (
<div className="bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800 p-8 shadow-sm"> <div className="bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800 p-8 shadow-sm">
<div className="text-center max-w-md mx-auto"> <div className="text-center max-w-md mx-auto">
@@ -410,7 +412,7 @@ function AppSettingsSection() {
onToggle={() => { onToggle={() => {
toggleSection('notifications') toggleSection('notifications')
if (!expanded.has('notifications')) { if (!expanded.has('notifications')) {
selectSubTab({ section: 'notifications', subTab: 'email' }) selectSubTab({ section: 'notifications', subTab: 'security' })
} }
}} }}
icon={BellIcon} icon={BellIcon}
@@ -419,9 +421,9 @@ function AppSettingsSection() {
/> />
<ExpandableSubItems expanded={expanded.has('notifications')}> <ExpandableSubItems expanded={expanded.has('notifications')}>
<SubItem <SubItem
active={active.section === 'notifications' && active.subTab === 'email'} active={active.section === 'notifications' && active.subTab === 'security'}
onClick={() => selectSubTab({ section: 'notifications', subTab: 'email' })} onClick={() => selectSubTab({ section: 'notifications', subTab: 'security' })}
label="Email Preferences" label="Security Alerts"
/> />
<SubItem <SubItem
active={active.section === 'notifications' && active.subTab === 'center'} active={active.section === 'notifications' && active.subTab === 'center'}

View File

@@ -48,7 +48,9 @@ export interface UserPreferences {
email_notifications: { email_notifications: {
new_file_received: boolean new_file_received: boolean
file_downloaded: boolean file_downloaded: boolean
security_alerts: boolean login_alerts: boolean
password_alerts: boolean
two_factor_alerts: boolean
} }
} }

View File

@@ -19,7 +19,9 @@ interface User {
email_notifications?: { email_notifications?: {
new_file_received: boolean new_file_received: boolean
file_downloaded: boolean file_downloaded: boolean
security_alerts: boolean login_alerts: boolean
password_alerts: boolean
two_factor_alerts: boolean
} }
} }
} }

8
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "pulse-frontend", "name": "pulse-frontend",
"version": "0.11.1-alpha", "version": "0.11.1-alpha",
"dependencies": { "dependencies": {
"@ciphera-net/ui": "^0.0.76", "@ciphera-net/ui": "^0.0.77",
"@ducanh2912/next-pwa": "^10.2.9", "@ducanh2912/next-pwa": "^10.2.9",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/browser": "^13.2.2",
@@ -1543,9 +1543,9 @@
} }
}, },
"node_modules/@ciphera-net/ui": { "node_modules/@ciphera-net/ui": {
"version": "0.0.76", "version": "0.0.77",
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.76/f584def8ea9ac4bccc52abdd281212f2e28959c0", "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.77/c243fce3b29ee4b3d90dd5b6b5a7d93ff3a955b9",
"integrity": "sha512-Bw7KOSUXQajfMAmgU39XiJGFudxx7Gj7Tnjr2tHHL+2oLOoBuDCNRRIC+A6Uo5WmxDBitw6voipoOfRcFo5XAA==", "integrity": "sha512-Z+aLef5843t9TIvaSV+ecC4sQi2VzX+hE6/7A/4/Y49CT91vg0x9QuQuQiV7B4j93Ui1yv+aZyWN21NY3mhKbA==",
"dependencies": { "dependencies": {
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",

View File

@@ -10,7 +10,7 @@
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@ciphera-net/ui": "^0.0.76", "@ciphera-net/ui": "^0.0.77",
"@ducanh2912/next-pwa": "^10.2.9", "@ducanh2912/next-pwa": "^10.2.9",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/browser": "^13.2.2",