aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-03-28 06:20:36 +0000
committerFuwn <[email protected]>2026-03-28 06:20:36 +0000
commit78bf2502cbf1e26980abf6c0ffb7f0c74b917a3b (patch)
tree9f4a84c90fa7fcfa945f1c9e0e637ba80b9cca79
parentfix(cache): preserve hydrated client state (diff)
downloaddue.moe-78bf2502cbf1e26980abf6c0ffb7f0c74b917a3b.tar.xz
due.moe-78bf2502cbf1e26980abf6c0ffb7f0c74b917a3b.zip
fix(notifications): support per-device push subscriptions
-rw-r--r--src/lib/Database/SB/User/notifications.ts41
-rw-r--r--src/lib/Utility/fingerprint.ts2
-rw-r--r--supabase/notifications.sql9
-rw-r--r--supabase/schema.sql5
4 files changed, 50 insertions, 7 deletions
diff --git a/src/lib/Database/SB/User/notifications.ts b/src/lib/Database/SB/User/notifications.ts
index 058171a9..21ad827b 100644
--- a/src/lib/Database/SB/User/notifications.ts
+++ b/src/lib/Database/SB/User/notifications.ts
@@ -8,6 +8,16 @@ export interface UserNotifications {
fingerprint: string;
}
+const subscriptionEndpoint = (subscription: JSON) => {
+ if (typeof subscription !== "object" || subscription === null) return null;
+
+ return "endpoint" in subscription &&
+ typeof subscription.endpoint === "string" &&
+ subscription.endpoint.length
+ ? subscription.endpoint
+ : null;
+};
+
export const getUserSubscription = async (userId: number) =>
await sb.from("user_notifications").select("*").eq("user_id", userId);
@@ -33,13 +43,38 @@ export const setUserSubscription = async (
userId: number,
subscription: JSON,
fingerprint: string,
-) =>
- await sb.from("user_notifications").upsert(
+) => {
+ const endpoint = subscriptionEndpoint(subscription);
+
+ if (endpoint) {
+ const { data } = await sb
+ .from("user_notifications")
+ .select("fingerprint, subscription")
+ .eq("user_id", userId);
+
+ const staleFingerprints =
+ data
+ ?.filter(
+ (existing) =>
+ existing.fingerprint !== fingerprint &&
+ subscriptionEndpoint(existing.subscription as JSON) === endpoint,
+ )
+ .map((existing) => existing.fingerprint) ?? [];
+
+ await Promise.all(
+ staleFingerprints.map(async (staleFingerprint) => {
+ await deleteUserSubscription(userId, staleFingerprint);
+ }),
+ );
+ }
+
+ return await sb.from("user_notifications").upsert(
{
user_id: userId,
updated_at: new Date().toISOString(),
subscription: subscription,
fingerprint,
},
- { onConflict: "user_id" },
+ { onConflict: "user_id,fingerprint" },
);
+};
diff --git a/src/lib/Utility/fingerprint.ts b/src/lib/Utility/fingerprint.ts
index 73a9a978..5af258d0 100644
--- a/src/lib/Utility/fingerprint.ts
+++ b/src/lib/Utility/fingerprint.ts
@@ -14,5 +14,5 @@ export const getFingerprint = () =>
navigator === null || navigator === void 0
? void 0
: navigator.hardwareConcurrency
- }-${new Date().getTimezoneOffset()}`,
+ }`,
);
diff --git a/supabase/notifications.sql b/supabase/notifications.sql
new file mode 100644
index 00000000..81905a29
--- /dev/null
+++ b/supabase/notifications.sql
@@ -0,0 +1,9 @@
+ALTER TABLE public.user_notifications
+ DROP CONSTRAINT IF EXISTS user_notifications_user_id_key;
+
+ALTER TABLE public.user_notifications
+ DROP CONSTRAINT IF EXISTS user_notifications_user_id_fingerprint_key;
+
+ALTER TABLE public.user_notifications
+ ADD CONSTRAINT user_notifications_user_id_fingerprint_key
+ UNIQUE (user_id, fingerprint);
diff --git a/supabase/schema.sql b/supabase/schema.sql
index 7648b7e4..9dc0934a 100644
--- a/supabase/schema.sql
+++ b/supabase/schema.sql
@@ -489,11 +489,11 @@ ALTER TABLE ONLY "public"."user_notifications"
--
--- Name: user_notifications user_notifications_user_id_key; Type: CONSTRAINT; Schema: public; Owner: postgres
+-- Name: user_notifications user_notifications_user_id_fingerprint_key; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY "public"."user_notifications"
- ADD CONSTRAINT "user_notifications_user_id_key" UNIQUE ("user_id");
+ ADD CONSTRAINT "user_notifications_user_id_fingerprint_key" UNIQUE ("user_id", "fingerprint");
--
@@ -1481,4 +1481,3 @@ ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT SELECT,INS
--
-- \unrestrict ayCRchpk9QYAkH0FR7MzsPfEkgyq5Y02kQfarpa01RE4AwEybP0seWKhOVsHiST
-