working push notifiations
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { users } from '../db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { z } from 'zod';
|
||||
import { sendPushNotification, isExpoPushToken, createTaskReminderPayload } from '../services/pushNotifications';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const registerPushTokenSchema = z.object({
|
||||
pushToken: z.string().min(1),
|
||||
});
|
||||
|
||||
router.post('/push-token', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!req.user) {
|
||||
throw new AppError('UNAUTHORIZED', 'Authentication required', 401);
|
||||
}
|
||||
|
||||
const data = registerPushTokenSchema.parse(req.body);
|
||||
|
||||
if (!isExpoPushToken(data.pushToken)) {
|
||||
throw new AppError('INVALID_PUSH_TOKEN', 'Invalid Expo push token format', 400);
|
||||
}
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
expoPushToken: data.pushToken,
|
||||
pushTokenUpdatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(users.id, req.user.userId));
|
||||
|
||||
res.json({ success: true });
|
||||
}));
|
||||
|
||||
router.delete('/push-token', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!req.user) {
|
||||
throw new AppError('UNAUTHORIZED', 'Authentication required', 401);
|
||||
}
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
expoPushToken: null,
|
||||
pushTokenUpdatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(users.id, req.user.userId));
|
||||
|
||||
res.json({ success: true });
|
||||
}));
|
||||
|
||||
router.post('/test', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!req.user) {
|
||||
throw new AppError('UNAUTHORIZED', 'Authentication required', 401);
|
||||
}
|
||||
|
||||
const user = await db.select().from(users).where(eq(users.id, req.user.userId)).limit(1);
|
||||
if (user.length === 0 || !user[0].expoPushToken) {
|
||||
throw new AppError('NO_PUSH_TOKEN', 'No push token registered', 400);
|
||||
}
|
||||
|
||||
const ticket = await sendPushNotification({
|
||||
to: user[0].expoPushToken!,
|
||||
title: 'Test Notification',
|
||||
body: 'Push notifications are working!',
|
||||
data: { type: 'test' },
|
||||
sound: 'default',
|
||||
});
|
||||
|
||||
res.json({ success: true, ticket });
|
||||
}));
|
||||
|
||||
router.post('/send-reminder', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!req.user) {
|
||||
throw new AppError('UNAUTHORIZED', 'Authentication required', 401);
|
||||
}
|
||||
|
||||
const { taskId, taskTitle, dueTime, repeat } = req.body;
|
||||
if (!taskId || !taskTitle || !dueTime) {
|
||||
throw new AppError('INVALID_PAYLOAD', 'Missing required fields', 400);
|
||||
}
|
||||
|
||||
const user = await db.select().from(users).where(eq(users.id, req.user.userId)).limit(1);
|
||||
if (user.length === 0 || !user[0].expoPushToken) {
|
||||
throw new AppError('NO_PUSH_TOKEN', 'No push token registered', 400);
|
||||
}
|
||||
|
||||
const ticket = await sendPushNotification(
|
||||
createTaskReminderPayload(user[0].expoPushToken!, taskTitle, dueTime, taskId, repeat)
|
||||
);
|
||||
|
||||
res.json({ success: true, ticket });
|
||||
}));
|
||||
|
||||
export default router;
|
||||
@@ -83,6 +83,7 @@ router.post('/task/:taskId', asyncHandler(async (req: Request, res: Response) =>
|
||||
taskId: req.params.taskId,
|
||||
parentSubtaskId,
|
||||
categoryId: data.categoryId ?? null,
|
||||
tags: data.tags ?? '',
|
||||
title: data.title,
|
||||
description: data.description ?? '',
|
||||
priority: data.priority ?? 'none',
|
||||
|
||||
+34
-46
@@ -17,60 +17,47 @@ router.get('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
|
||||
const sinceDate = since;
|
||||
|
||||
// Fetch categories changed since timestamp
|
||||
const changedCategories = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.userId, userId), gte(categories.updatedAt, sinceDate)))
|
||||
.orderBy(asc(categories.updatedAt));
|
||||
// Fetch all changed entities in parallel
|
||||
const [changedCategories, changedTasks, changedSubtasks, changedRepeatProfiles, changedFriendships, changedTombstones] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.userId, userId), gte(categories.updatedAt, sinceDate)))
|
||||
.orderBy(asc(categories.updatedAt)),
|
||||
|
||||
// Fetch tasks changed since timestamp
|
||||
const changedTasks = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.userId, userId), gte(tasks.updatedAt, sinceDate)))
|
||||
.orderBy(asc(tasks.updatedAt));
|
||||
db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.userId, userId), gte(tasks.updatedAt, sinceDate)))
|
||||
.orderBy(asc(tasks.updatedAt)),
|
||||
|
||||
// Fetch subtasks changed since timestamp
|
||||
let changedSubtasks: any[] = [];
|
||||
if (changedTasks.length > 0) {
|
||||
changedSubtasks = await db
|
||||
db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.userId, userId), gte(subtasks.updatedAt, sinceDate)))
|
||||
.orderBy(asc(subtasks.updatedAt));
|
||||
} else {
|
||||
// Also fetch subtasks for tasks that might have been deleted
|
||||
changedSubtasks = await db
|
||||
.orderBy(asc(subtasks.updatedAt)),
|
||||
|
||||
db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.userId, userId), gte(subtasks.updatedAt, sinceDate)))
|
||||
.orderBy(asc(subtasks.updatedAt));
|
||||
}
|
||||
.from(repeatProfiles)
|
||||
.where(and(eq(repeatProfiles.userId, userId), gte(repeatProfiles.updatedAt, sinceDate)))
|
||||
.orderBy(asc(repeatProfiles.updatedAt)),
|
||||
|
||||
// Fetch repeat profiles changed since timestamp
|
||||
const changedRepeatProfiles = await db
|
||||
.select()
|
||||
.from(repeatProfiles)
|
||||
.where(and(eq(repeatProfiles.userId, userId), gte(repeatProfiles.updatedAt, sinceDate)))
|
||||
.orderBy(asc(repeatProfiles.updatedAt));
|
||||
db
|
||||
.select()
|
||||
.from(friendships)
|
||||
.where(and(
|
||||
or(eq(friendships.userId, userId), eq(friendships.friendId, userId)),
|
||||
gte(friendships.updatedAt, sinceDate)
|
||||
))
|
||||
.orderBy(asc(friendships.updatedAt)),
|
||||
|
||||
// Fetch friendships changed since timestamp
|
||||
const changedFriendships = await db
|
||||
.select()
|
||||
.from(friendships)
|
||||
.where(and(
|
||||
or(eq(friendships.userId, userId), eq(friendships.friendId, userId)),
|
||||
gte(friendships.updatedAt, sinceDate)
|
||||
))
|
||||
.orderBy(asc(friendships.updatedAt));
|
||||
|
||||
// Fetch tombstones (deletions) changed since timestamp
|
||||
const changedTombstones = await db
|
||||
.select()
|
||||
.from(tombstones)
|
||||
.where(and(eq(tombstones.userId, userId), gte(tombstones.updatedAt, sinceDate)))
|
||||
.orderBy(asc(tombstones.updatedAt));
|
||||
db
|
||||
.select()
|
||||
.from(tombstones)
|
||||
.where(and(eq(tombstones.userId, userId), gte(tombstones.updatedAt, sinceDate)))
|
||||
.orderBy(asc(tombstones.updatedAt)),
|
||||
]);
|
||||
|
||||
const timestamp = Date.now();
|
||||
|
||||
@@ -292,6 +279,7 @@ router.post('/push', asyncHandler(async (req: Request, res: Response) => {
|
||||
taskId: sub.taskId,
|
||||
parentSubtaskId: sub.parentSubtaskId ?? null,
|
||||
categoryId: sub.categoryId ?? null,
|
||||
tags: sub.tags ?? '',
|
||||
title: sub.title,
|
||||
description: sub.description ?? '',
|
||||
priority: sub.priority ?? 'none',
|
||||
|
||||
@@ -161,6 +161,7 @@ router.post('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
taskId,
|
||||
title: st.title,
|
||||
categoryId: (st as any).categoryId ?? null,
|
||||
tags: (st as any).tags ?? '',
|
||||
completed: false,
|
||||
order: index,
|
||||
createdAt: now,
|
||||
|
||||
Reference in New Issue
Block a user