11"""Notification-related API routes."""
22
3- from dataclasses import dataclass
4- from datetime import datetime
5- from typing import Any
6- from fastapi import APIRouter , Request , HTTPException
7- from sqlalchemy import select , desc , delete
3+ import asyncio
4+ from fastapi import APIRouter , HTTPException
5+ from sqlalchemy import select
86
9- from celine .webapp .api .deps import UserDep , DbDep
7+ from celine .webapp .api .deps import NudgingDep , UserDep , DbDep
108from celine .webapp .api .schemas import (
119 NotificationItem ,
10+ PushSubscriptionPayload ,
11+ PushSubscriptionUnsubscribePayload ,
1212 VapidKeyResponse ,
13- WebPushUnsubscribeRequest ,
1413 SuccessResponse ,
1514)
16- from celine .webapp .db import WebPushSubscription
17- from celine .webapp .settings import settings as app_settings
15+ from celine .webapp .db .user_settings import update_user_settings
1816
17+ from celine .sdk .openapi .nudging .models import (
18+ SubscribeRequest ,
19+ WebPushSubscriptionIn ,
20+ WebPushKeysIn ,
21+ UnsubscribeRequest ,
22+ )
1923
2024router = APIRouter (prefix = "/api/notifications" , tags = ["notifications" ])
2125
2226
2327@router .get ("" , response_model = list [NotificationItem ])
2428async def list_notifications (
25- user : UserDep ,
26- db : DbDep ,
29+ user : UserDep , db : DbDep , nudging_client : NudgingDep
2730) -> list [NotificationItem ]:
2831 """List user notifications."""
2932
30- # TODO
31- @dataclass (frozen = True )
32- class TestNotification :
33- id : str
34- title : str
35- body : str
36- severity : str
37- created_at : datetime
38- read_at : datetime | None
39-
40- notifications : list [TestNotification ] = [
41- TestNotification (
42- id = "0000001" ,
43- created_at = datetime .now (),
44- title = "Welcome to the REC Webapp (example)" ,
45- body = "Learn more about the app from your energy community manager" ,
46- severity = "info" ,
47- read_at = None ,
48- )
49- ]
33+ res = await nudging_client .list_notifications (token = user .token )
5034
5135 return [
5236 NotificationItem (
@@ -60,8 +44,9 @@ class TestNotification:
6044 else "warning" if n .severity == "warning" else "info"
6145 ),
6246 read_at = n .read_at .isoformat () if n .read_at else None ,
47+ deleted_at = n .deleted_at .isoformat () if n .deleted_at else None ,
6348 )
64- for n in notifications
49+ for n in res
6550 ]
6651
6752
@@ -70,76 +55,113 @@ async def enable_notifications(
7055 user : UserDep ,
7156 db : DbDep ,
7257) -> SuccessResponse :
73- """Enable notifications for user."""
58+ # TODO
59+ await update_user_settings (user_id = user .sub , db = db , email_notifications = True )
60+ return SuccessResponse ()
61+
62+
63+ @router .post ("/disable" , response_model = SuccessResponse )
64+ async def disable_notifications (
65+ user : UserDep ,
66+ db : DbDep ,
67+ ) -> SuccessResponse :
68+ # TODO
69+ await update_user_settings (user_id = user .sub , db = db , email_notifications = False )
70+ return SuccessResponse ()
71+
72+
73+ # NOTE: /read-all must be registered before /{id}/read so FastAPI does not
74+ # capture the literal string "read-all" as a notification id.
75+ @router .post ("/read-all" , response_model = SuccessResponse )
76+ async def mark_all_notifications_read (
77+ user : UserDep ,
78+ nudging_client : NudgingDep ,
79+ ) -> SuccessResponse :
80+ """Mark every unread notification as read for the current user.
81+
82+ The nudging service has no bulk-mark-read endpoint, so we fetch the
83+ unread list and fan out individual mark_read calls concurrently.
84+ """
85+ unread = await nudging_client .list_notifications (unread_only = True , token = user .token )
86+
87+ if unread :
88+ await asyncio .gather (
89+ * [nudging_client .mark_read (n .id , token = user .token ) for n in unread ]
90+ )
91+
92+ return SuccessResponse ()
93+
7494
75- # Idempotent placeholder: in a real implementation this might
76- # register the user in the nudging tool
95+ @router .post ("/{notification_id}/read" , response_model = SuccessResponse )
96+ async def mark_notification_read (
97+ notification_id : str ,
98+ user : UserDep ,
99+ nudging_client : NudgingDep ,
100+ ) -> SuccessResponse :
101+ """Mark a single notification as read. Idempotent."""
102+ result = await nudging_client .mark_read (notification_id , token = user .token )
103+ if result is None :
104+ raise HTTPException (status_code = 404 , detail = "Notification not found" )
77105 return SuccessResponse ()
78106
79107
80108@router .get ("/webpush/vapid-public-key" , response_model = VapidKeyResponse )
81109async def vapid_public_key (
82- user : UserDep ,
83- db : DbDep ,
110+ nudging_client : NudgingDep ,
84111) -> VapidKeyResponse :
85112 """Get VAPID public key for web push."""
86- return VapidKeyResponse (public_key = app_settings .vapid_public_key )
113+ res = await nudging_client .get_vapid_public_key ()
114+ if res is None :
115+ raise HTTPException (500 , "Failed to fetch VAPID public key" )
116+ return VapidKeyResponse (public_key = res .public_key )
87117
88118
89119@router .post ("/webpush/subscribe" , response_model = SuccessResponse )
90120async def webpush_subscribe (
91- request : Request ,
92121 user : UserDep ,
93122 db : DbDep ,
123+ nudging_client : NudgingDep ,
124+ payload : PushSubscriptionPayload ,
94125) -> SuccessResponse :
95- """Subscribe to web push notifications."""
96-
97- data = await request .json ()
98- endpoint = data .get ("endpoint" )
99-
100- if not endpoint :
101- raise HTTPException (status_code = 400 , detail = "subscription endpoint missing" )
126+ """Enable notifications for user."""
127+ await nudging_client .subscribe (
128+ body = SubscribeRequest (
129+ subscription = WebPushSubscriptionIn (
130+ endpoint = payload .endpoint ,
131+ keys = WebPushKeysIn (
132+ p256dh = payload .p256dh ,
133+ auth = payload .auth ,
134+ ),
135+ )
136+ ),
137+ token = user .token ,
138+ )
102139
103- # Check if subscription already exists
104- result = await db .execute (
105- select (WebPushSubscription ).filter (
106- WebPushSubscription .user_id == user .sub ,
107- WebPushSubscription .endpoint == endpoint ,
108- )
140+ await update_user_settings (
141+ user_id = user .sub ,
142+ db = db ,
143+ webpush_enabled = True ,
109144 )
110- existing = result .scalar_one_or_none ()
111-
112- if existing :
113- # Update existing subscription
114- existing .subscription_json = data
115- await db .commit ()
116- else :
117- # Create new subscription
118- subscription = WebPushSubscription (
119- user_id = user .sub ,
120- endpoint = endpoint ,
121- subscription_json = data ,
122- )
123- db .add (subscription )
124- await db .commit ()
125145
126146 return SuccessResponse ()
127147
128148
129149@router .post ("/webpush/unsubscribe" , response_model = SuccessResponse )
130150async def webpush_unsubscribe (
131- body : WebPushUnsubscribeRequest ,
132151 user : UserDep ,
133152 db : DbDep ,
153+ nudging_client : NudgingDep ,
154+ payload : PushSubscriptionUnsubscribePayload ,
134155) -> SuccessResponse :
135- """Unsubscribe from web push notifications."""
156+ await nudging_client .unsubscribe (
157+ body = UnsubscribeRequest (endpoint = payload .endpoint ),
158+ token = user .token ,
159+ )
136160
137- await db .execute (
138- delete (WebPushSubscription ).filter (
139- WebPushSubscription .user_id == user .sub ,
140- WebPushSubscription .endpoint == body .endpoint ,
141- )
161+ await update_user_settings (
162+ user_id = user .sub ,
163+ db = db ,
164+ webpush_enabled = False ,
142165 )
143- await db .commit ()
144166
145167 return SuccessResponse ()
0 commit comments