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β
Field | Type | Required | Description |
---|---|---|---|
enabled | boolean | No | Master toggle for all notifications |
channels | object | No | Notification channel preferences |
channels.inApp | boolean | No | In-app notifications |
channels.email | boolean | No | Email notifications |
channels.sms | boolean | No | SMS notifications |
channels.push | boolean | No | Push notifications |
categories | object | No | Notification category preferences |
frequency | object | No | Notification frequency settings |
quietHours | object | No | Quiet hours configuration |
language | string | No | Preferred 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:β
- Authentication Check: Verifies user session
- Preference Retrieval: Fetches user preferences from database
- Default Handling: Returns default preferences if none exist
- Response: Returns preference object
PUT Request Flow:β
- Authentication Check: Verifies user session
- Input Validation: Validates preference data structure
- Database Update: Updates or creates user preferences
- 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)
})
})
Related Endpointsβ
GET /api/notifications
- List user notificationsPOST /api/notifications/[id]/read
- Mark specific notification as readPOST /api/notifications/read-all
- Mark all notifications as read