Skip to main content

Notification Preferences API

Overview​

The Notification Preferences API allows authenticated users to view and manage their notification preferences. This endpoint supports both retrieving current preferences and updating them with new settings across multiple notification channels.

Endpoint Details​

  • URL: /api/notifications/preferences
  • Methods: GET, PUT
  • Authentication: Required (NextAuth.js session)
  • Content-Type: application/json

Authentication​

This endpoint requires user authentication via NextAuth.js session:

import { getSession } from 'next-auth/react'

const session = await getSession()
if (!session) {
// Handle unauthenticated state
}

GET - Retrieve Preferences​

Request​

No request body required for GET requests.

Response Format​

Success Response (200)​

{
"enabled": true,
"channels": {
"inApp": true,
"email": true,
"sms": false,
"push": false
},
"categories": {
"messages": true,
"opportunities": true,
"entities": false,
"system": true,
"marketing": false
},
"frequency": {
"immediate": true,
"daily": false,
"weekly": false
},
"quietHours": {
"enabled": true,
"start": "22:00",
"end": "08:00",
"timezone": "UTC"
},
"language": "en",
"updatedAt": "2024-01-15T10:30:00Z"
}

Default Preferences (200)​

If no preferences exist, default values are returned:

{
"enabled": true,
"channels": {
"inApp": true,
"email": true,
"sms": false,
"push": false
},
"language": "en",
"updatedAt": "2024-01-15T10:30:00Z"
}

PUT - Update Preferences​

Request Body​

FieldTypeRequiredDescription
enabledbooleanNoMaster toggle for all notifications
channelsobjectNoNotification channel preferences
channels.inAppbooleanNoIn-app notifications
channels.emailbooleanNoEmail notifications
channels.smsbooleanNoSMS notifications
channels.pushbooleanNoPush notifications
categoriesobjectNoNotification category preferences
frequencyobjectNoNotification frequency settings
quietHoursobjectNoQuiet hours configuration
languagestringNoPreferred language (en, uk)

Example Request​

{
"enabled": true,
"channels": {
"inApp": true,
"email": true,
"sms": false,
"push": true
},
"categories": {
"messages": true,
"opportunities": true,
"entities": false,
"system": true,
"marketing": false
},
"frequency": {
"immediate": false,
"daily": true,
"weekly": false
},
"quietHours": {
"enabled": true,
"start": "23:00",
"end": "07:00",
"timezone": "Europe/Kiev"
},
"language": "uk"
}

Response Format​

Success Response (200)​

{
"success": true,
"message": "Notification preferences updated successfully"
}

Error Responses​

Unauthorized (401)​

{
"error": "User not authenticated"
}

Bad Request (400)​

{
"error": "Invalid preferences data provided."
}

Internal Server Error (500)​

{
"error": "Failed to fetch notification preferences. Please try again later."
}
{
"error": "Failed to update notification preferences. Please try again later."
}

Implementation Examples​

React Component for Preferences Management​

import { useState, useEffect } from 'react'
import { useSession } from 'next-auth/react'

interface NotificationPreferences {
enabled: boolean
channels: {
inApp: boolean
email: boolean
sms: boolean
push: boolean
}
categories: {
messages: boolean
opportunities: boolean
entities: boolean
system: boolean
marketing: boolean
}
frequency: {
immediate: boolean
daily: boolean
weekly: boolean
}
quietHours: {
enabled: boolean
start: string
end: string
timezone: string
}
language: string
}

function NotificationPreferences() {
const { data: session } = useSession()
const [preferences, setPreferences] = useState<NotificationPreferences | null>(null)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)

// Fetch preferences on component mount
useEffect(() => {
if (!session) return

const fetchPreferences = async () => {
try {
const response = await fetch('/api/notifications/preferences')

if (!response.ok) {
throw new Error('Failed to fetch preferences')
}

const data = await response.json()
setPreferences(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setLoading(false)
}
}

fetchPreferences()
}, [session])

const handleSave = async () => {
if (!session || !preferences) return

setSaving(true)
setError(null)
setSuccess(null)

try {
const response = await fetch('/api/notifications/preferences', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(preferences),
})

if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to save preferences')
}

setSuccess('Preferences saved successfully!')
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setSaving(false)
}
}

const updatePreference = (path: string, value: any) => {
if (!preferences) return

const keys = path.split('.')
const newPreferences = { ...preferences }
let current: any = newPreferences

for (let i = 0; i < keys.length - 1; i++) {
current[keys[i]] = { ...current[keys[i]] }
current = current[keys[i]]
}

current[keys[keys.length - 1]] = value
setPreferences(newPreferences)
}

if (loading) {
return <div className="flex justify-center p-4">Loading preferences...</div>
}

if (!preferences) {
return <div className="text-red-500 p-4">Failed to load preferences</div>
}

return (
<div className="max-w-2xl mx-auto p-6 space-y-6">
<h2 className="text-2xl font-bold">Notification Preferences</h2>

{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}

{success && (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
{success}
</div>
)}

{/* Master Toggle */}
<div className="border rounded-lg p-4">
<label className="flex items-center space-x-3">
<input
type="checkbox"
checked={preferences.enabled}
onChange={(e) => updatePreference('enabled', e.target.checked)}
className="w-4 h-4 text-blue-600"
/>
<span className="text-lg font-semibold">Enable Notifications</span>
</label>
</div>

{/* Channels */}
<div className="border rounded-lg p-4 space-y-3">
<h3 className="text-lg font-semibold">Notification Channels</h3>

{Object.entries(preferences.channels).map(([channel, enabled]) => (
<label key={channel} className="flex items-center space-x-3">
<input
type="checkbox"
checked={enabled}
onChange={(e) => updatePreference(`channels.${channel}`, e.target.checked)}
disabled={!preferences.enabled}
className="w-4 h-4 text-blue-600"
/>
<span className="capitalize">{channel.replace(/([A-Z])/g, ' $1').trim()}</span>
</label>
))}
</div>

{/* Categories */}
<div className="border rounded-lg p-4 space-y-3">
<h3 className="text-lg font-semibold">Notification Categories</h3>

{preferences.categories && Object.entries(preferences.categories).map(([category, enabled]) => (
<label key={category} className="flex items-center space-x-3">
<input
type="checkbox"
checked={enabled}
onChange={(e) => updatePreference(`categories.${category}`, e.target.checked)}
disabled={!preferences.enabled}
className="w-4 h-4 text-blue-600"
/>
<span className="capitalize">{category}</span>
</label>
))}
</div>

{/* Save Button */}
<button
onClick={handleSave}
disabled={saving || !preferences.enabled}
className="w-full py-2 px-4 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save Preferences'}
</button>
</div>
)
}

Custom Hook for Preferences​

import { useState, useEffect, useCallback } from 'react'
import { useSession } from 'next-auth/react'

export function useNotificationPreferences() {
const { data: session } = useSession()
const [preferences, setPreferences] = useState<NotificationPreferences | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)

const fetchPreferences = useCallback(async () => {
if (!session) return

setLoading(true)
setError(null)

try {
const response = await fetch('/api/notifications/preferences')

if (!response.ok) {
throw new Error('Failed to fetch preferences')
}

const data = await response.json()
setPreferences(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setLoading(false)
}
}, [session])

const updatePreferences = useCallback(async (newPreferences: Partial<NotificationPreferences>) => {
if (!session) return false

try {
const response = await fetch('/api/notifications/preferences', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPreferences),
})

if (!response.ok) {
throw new Error('Failed to update preferences')
}

// Refetch preferences after update
await fetchPreferences()
return true
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
return false
}
}, [session, fetchPreferences])

useEffect(() => {
fetchPreferences()
}, [fetchPreferences])

return {
preferences,
loading,
error,
fetchPreferences,
updatePreferences
}
}

Server Action (Next.js 13+)​

'use server'

import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'

export async function updateNotificationPreferencesAction(formData: FormData) {
const session = await auth()

if (!session) {
redirect('/auth/signin')
}

const preferences = {
enabled: formData.get('enabled') === 'true',
channels: {
inApp: formData.get('channels.inApp') === 'true',
email: formData.get('channels.email') === 'true',
sms: formData.get('channels.sms') === 'true',
push: formData.get('channels.push') === 'true',
},
language: formData.get('language') as string || 'en'
}

try {
const response = await fetch(`${process.env.NEXTAUTH_URL}/api/notifications/preferences`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Cookie': request.headers.get('cookie') || '',
},
body: JSON.stringify(preferences),
})

if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to update preferences')
}

revalidatePath('/settings/notifications')
redirect('/settings/notifications?success=preferences-updated')

} catch (error) {
console.error('Server action error:', error)
throw error
}
}

cURL Examples​

Get Preferences​

curl -X GET https://ring.ck.ua/api/notifications/preferences \
-H "Content-Type: application/json" \
-H "Cookie: next-auth.session-token=your-session-token"

Update Preferences​

curl -X PUT https://ring.ck.ua/api/notifications/preferences \
-H "Content-Type: application/json" \
-H "Cookie: next-auth.session-token=your-session-token" \
-d '{
"enabled": true,
"channels": {
"inApp": true,
"email": true,
"sms": false,
"push": true
},
"language": "en"
}'

Business Logic​

GET Request Flow:​

  1. Authentication Check: Verifies user session
  2. Preference Retrieval: Fetches user preferences from database
  3. Default Handling: Returns default preferences if none exist
  4. Response: Returns preference object

PUT Request Flow:​

  1. Authentication Check: Verifies user session
  2. Input Validation: Validates preference data structure
  3. Database Update: Updates or creates user preferences
  4. Response: Returns success confirmation

Database Schema​

Preferences Document Structure​

{
"userId": "user123",
"enabled": true,
"channels": {
"inApp": true,
"email": true,
"sms": false,
"push": false
},
"categories": {
"messages": true,
"opportunities": true,
"entities": false,
"system": true,
"marketing": false
},
"frequency": {
"immediate": true,
"daily": false,
"weekly": false
},
"quietHours": {
"enabled": true,
"start": "22:00",
"end": "08:00",
"timezone": "UTC"
},
"language": "en",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T14:22:00Z"
}

Security Considerations​

  • Authentication Required: Only authenticated users can access preferences
  • User Isolation: Users can only access their own preferences
  • Input Validation: All preference values are validated before storage
  • Safe Defaults: Secure default values when preferences don't exist

Performance Notes​

  • Single Query: Efficient retrieval with one database query
  • Partial Updates: Only modified fields are updated in database
  • Caching: Preferences are cached at application level for performance

Rate Limiting​

  • User-based: 20 requests per minute per authenticated user
  • Global: 500 requests per minute across all users

Testing​

Unit Test Example​

import { GET, PUT } from '@/app/api/notifications/preferences/route'
import { NextRequest } from 'next/server'

jest.mock('@/auth')
jest.mock('@/services/notifications/notification-service')

describe('/api/notifications/preferences', () => {
it('should fetch preferences successfully', async () => {
(getServerAuthSession as jest.Mock).mockResolvedValue({
user: { id: 'user123', role: 'member' }
})

const request = new NextRequest('http://localhost:3000/api/notifications/preferences')
const response = await GET(request)
const data = await response.json()

expect(response.status).toBe(200)
expect(data.enabled).toBeDefined()
expect(data.channels).toBeDefined()
})

it('should update preferences successfully', async () => {
const preferences = {
enabled: true,
channels: { inApp: true, email: false },
language: 'en'
}

const request = new NextRequest('http://localhost:3000/api/notifications/preferences', {
method: 'PUT',
body: JSON.stringify(preferences)
})

const response = await PUT(request)
const data = await response.json()

expect(response.status).toBe(200)
expect(data.success).toBe(true)
})
})