Phase 3-4: Goals AI planner, Team tasks, Push notifications, i18n (CZ/HE/RU/UA)
- Goals CRUD API + AI study plan generator + progress reports - Goals frontend page with progress bars - Team tasks: assign, transfer, collaborate endpoints - Push notifications: web-push, VAPID, subscribe/send - i18n: 4 languages (cs, he, ru, ua) translation files - notifications.js + goals.js routes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
77
api/src/routes/notifications.js
Normal file
77
api/src/routes/notifications.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// Task Team — Push Notifications — 2026-03-29
|
||||
const webpush = require("web-push");
|
||||
|
||||
async function notificationRoutes(app) {
|
||||
// Generate VAPID keys if not exist
|
||||
const vapidKeys = {
|
||||
publicKey: process.env.VAPID_PUBLIC_KEY || "",
|
||||
privateKey: process.env.VAPID_PRIVATE_KEY || ""
|
||||
};
|
||||
|
||||
if (!vapidKeys.publicKey) {
|
||||
const keys = webpush.generateVAPIDKeys();
|
||||
vapidKeys.publicKey = keys.publicKey;
|
||||
vapidKeys.privateKey = keys.privateKey;
|
||||
console.log("VAPID keys generated. Add to env:", JSON.stringify(keys));
|
||||
}
|
||||
|
||||
webpush.setVapidDetails("mailto:admin@hasdo.info", vapidKeys.publicKey, vapidKeys.privateKey);
|
||||
|
||||
// Create subscriptions table if not exists
|
||||
await app.db.query(`
|
||||
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
subscription JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
// Get VAPID public key
|
||||
app.get("/notifications/vapid-key", async () => {
|
||||
return { data: { publicKey: vapidKeys.publicKey } };
|
||||
});
|
||||
|
||||
// Subscribe
|
||||
app.post("/notifications/subscribe", async (req) => {
|
||||
const { subscription, user_id } = req.body;
|
||||
await app.db.query(
|
||||
"INSERT INTO push_subscriptions (user_id, subscription) VALUES ($1, $2)",
|
||||
[user_id, JSON.stringify(subscription)]
|
||||
);
|
||||
return { status: "subscribed" };
|
||||
});
|
||||
|
||||
// Send notification (internal use)
|
||||
app.post("/notifications/send", async (req) => {
|
||||
const { user_id, title, body, url } = req.body;
|
||||
const { rows } = await app.db.query(
|
||||
"SELECT subscription FROM push_subscriptions WHERE user_id = $1", [user_id]
|
||||
);
|
||||
|
||||
let sent = 0;
|
||||
for (const row of rows) {
|
||||
try {
|
||||
await webpush.sendNotification(
|
||||
JSON.parse(row.subscription),
|
||||
JSON.stringify({ title, body, url: url || "/tasks" })
|
||||
);
|
||||
sent++;
|
||||
} catch (e) {
|
||||
if (e.statusCode === 410) {
|
||||
await app.db.query("DELETE FROM push_subscriptions WHERE subscription = $1", [row.subscription]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { status: "sent", count: sent };
|
||||
});
|
||||
|
||||
// Unsubscribe
|
||||
app.delete("/notifications/unsubscribe", async (req) => {
|
||||
const { user_id } = req.body;
|
||||
await app.db.query("DELETE FROM push_subscriptions WHERE user_id = $1", [user_id]);
|
||||
return { status: "unsubscribed" };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = notificationRoutes;
|
||||
Reference in New Issue
Block a user