- Add bearer plugin to BetterAuth for mobile auth - Auto-sync birthday/anniversary events on client create/update - Add /api/events/sync-all endpoint for bulk sync - Add test user seed (test@test.com / test) - Expose set-auth-token header in CORS
349 lines
9.6 KiB
TypeScript
349 lines
9.6 KiB
TypeScript
import { Elysia, t } from 'elysia';
|
|
import { db } from '../db';
|
|
import { events, clients } from '../db/schema';
|
|
import { eq, and, gte, lte, sql } from 'drizzle-orm';
|
|
import type { User } from '../lib/auth';
|
|
|
|
export const eventRoutes = new Elysia({ prefix: '/events' })
|
|
// List events with optional filters
|
|
.get('/', async ({ query, user }: {
|
|
query: {
|
|
clientId?: string;
|
|
type?: string;
|
|
upcoming?: string; // days ahead
|
|
};
|
|
user: User;
|
|
}) => {
|
|
let conditions = [eq(events.userId, user.id)];
|
|
|
|
if (query.clientId) {
|
|
conditions.push(eq(events.clientId, query.clientId));
|
|
}
|
|
|
|
if (query.type) {
|
|
conditions.push(eq(events.type, query.type));
|
|
}
|
|
|
|
let results = await db.select({
|
|
event: events,
|
|
client: {
|
|
id: clients.id,
|
|
firstName: clients.firstName,
|
|
lastName: clients.lastName,
|
|
},
|
|
})
|
|
.from(events)
|
|
.innerJoin(clients, eq(events.clientId, clients.id))
|
|
.where(and(...conditions))
|
|
.orderBy(events.date);
|
|
|
|
// Filter upcoming events if requested
|
|
if (query.upcoming) {
|
|
const daysAhead = parseInt(query.upcoming) || 7;
|
|
const now = new Date();
|
|
const future = new Date();
|
|
future.setDate(future.getDate() + daysAhead);
|
|
|
|
results = results.filter(r => {
|
|
const eventDate = new Date(r.event.date);
|
|
// For recurring events, check if the month/day falls within range
|
|
if (r.event.recurring) {
|
|
const thisYear = new Date(
|
|
now.getFullYear(),
|
|
eventDate.getMonth(),
|
|
eventDate.getDate()
|
|
);
|
|
const nextYear = new Date(
|
|
now.getFullYear() + 1,
|
|
eventDate.getMonth(),
|
|
eventDate.getDate()
|
|
);
|
|
return (thisYear >= now && thisYear <= future) ||
|
|
(nextYear >= now && nextYear <= future);
|
|
}
|
|
return eventDate >= now && eventDate <= future;
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}, {
|
|
query: t.Object({
|
|
clientId: t.Optional(t.String({ format: 'uuid' })),
|
|
type: t.Optional(t.String()),
|
|
upcoming: t.Optional(t.String()),
|
|
}),
|
|
})
|
|
|
|
// Get single event
|
|
.get('/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
|
const [event] = await db.select({
|
|
event: events,
|
|
client: {
|
|
id: clients.id,
|
|
firstName: clients.firstName,
|
|
lastName: clients.lastName,
|
|
},
|
|
})
|
|
.from(events)
|
|
.innerJoin(clients, eq(events.clientId, clients.id))
|
|
.where(and(eq(events.id, params.id), eq(events.userId, user.id)))
|
|
.limit(1);
|
|
|
|
if (!event) {
|
|
throw new Error('Event not found');
|
|
}
|
|
|
|
return event;
|
|
}, {
|
|
params: t.Object({
|
|
id: t.String({ format: 'uuid' }),
|
|
}),
|
|
})
|
|
|
|
// Create event
|
|
.post('/', async ({ body, user }: {
|
|
body: {
|
|
clientId: string;
|
|
type: string;
|
|
title: string;
|
|
date: string;
|
|
recurring?: boolean;
|
|
reminderDays?: number;
|
|
};
|
|
user: User;
|
|
}) => {
|
|
// Verify client belongs to user
|
|
const [client] = await db.select()
|
|
.from(clients)
|
|
.where(and(eq(clients.id, body.clientId), eq(clients.userId, user.id)))
|
|
.limit(1);
|
|
|
|
if (!client) {
|
|
throw new Error('Client not found');
|
|
}
|
|
|
|
const [event] = await db.insert(events)
|
|
.values({
|
|
userId: user.id,
|
|
clientId: body.clientId,
|
|
type: body.type,
|
|
title: body.title,
|
|
date: new Date(body.date),
|
|
recurring: body.recurring ?? false,
|
|
reminderDays: body.reminderDays ?? 7,
|
|
})
|
|
.returning();
|
|
|
|
return event;
|
|
}, {
|
|
body: t.Object({
|
|
clientId: t.String({ format: 'uuid' }),
|
|
type: t.String({ minLength: 1 }),
|
|
title: t.String({ minLength: 1 }),
|
|
date: t.String(), // ISO date
|
|
recurring: t.Optional(t.Boolean()),
|
|
reminderDays: t.Optional(t.Number({ minimum: 0 })),
|
|
}),
|
|
})
|
|
|
|
// Update event
|
|
.put('/:id', async ({ params, body, user }: {
|
|
params: { id: string };
|
|
body: {
|
|
type?: string;
|
|
title?: string;
|
|
date?: string;
|
|
recurring?: boolean;
|
|
reminderDays?: number;
|
|
};
|
|
user: User;
|
|
}) => {
|
|
const updateData: Record<string, unknown> = {};
|
|
|
|
if (body.type !== undefined) updateData.type = body.type;
|
|
if (body.title !== undefined) updateData.title = body.title;
|
|
if (body.date !== undefined) updateData.date = new Date(body.date);
|
|
if (body.recurring !== undefined) updateData.recurring = body.recurring;
|
|
if (body.reminderDays !== undefined) updateData.reminderDays = body.reminderDays;
|
|
|
|
const [event] = await db.update(events)
|
|
.set(updateData)
|
|
.where(and(eq(events.id, params.id), eq(events.userId, user.id)))
|
|
.returning();
|
|
|
|
if (!event) {
|
|
throw new Error('Event not found');
|
|
}
|
|
|
|
return event;
|
|
}, {
|
|
params: t.Object({
|
|
id: t.String({ format: 'uuid' }),
|
|
}),
|
|
body: t.Object({
|
|
type: t.Optional(t.String()),
|
|
title: t.Optional(t.String()),
|
|
date: t.Optional(t.String()),
|
|
recurring: t.Optional(t.Boolean()),
|
|
reminderDays: t.Optional(t.Number()),
|
|
}),
|
|
})
|
|
|
|
// Delete event
|
|
.delete('/:id', async ({ params, user }: { params: { id: string }; user: User }) => {
|
|
const [deleted] = await db.delete(events)
|
|
.where(and(eq(events.id, params.id), eq(events.userId, user.id)))
|
|
.returning({ id: events.id });
|
|
|
|
if (!deleted) {
|
|
throw new Error('Event not found');
|
|
}
|
|
|
|
return { success: true, id: deleted.id };
|
|
}, {
|
|
params: t.Object({
|
|
id: t.String({ format: 'uuid' }),
|
|
}),
|
|
})
|
|
|
|
// Sync ALL events from all clients' birthdays/anniversaries
|
|
.post('/sync-all', async ({ user }: { user: User }) => {
|
|
// Get all clients for this user
|
|
const userClients = await db.select()
|
|
.from(clients)
|
|
.where(eq(clients.userId, user.id));
|
|
|
|
let created = 0;
|
|
let updated = 0;
|
|
|
|
for (const client of userClients) {
|
|
// Sync birthday
|
|
if (client.birthday) {
|
|
const [existing] = await db.select()
|
|
.from(events)
|
|
.where(and(eq(events.clientId, client.id), eq(events.type, 'birthday')))
|
|
.limit(1);
|
|
|
|
if (!existing) {
|
|
await db.insert(events).values({
|
|
userId: user.id,
|
|
clientId: client.id,
|
|
type: 'birthday',
|
|
title: `${client.firstName}'s Birthday`,
|
|
date: client.birthday,
|
|
recurring: true,
|
|
reminderDays: 7,
|
|
});
|
|
created++;
|
|
} else {
|
|
await db.update(events)
|
|
.set({ date: client.birthday, title: `${client.firstName}'s Birthday` })
|
|
.where(eq(events.id, existing.id));
|
|
updated++;
|
|
}
|
|
}
|
|
|
|
// Sync anniversary
|
|
if (client.anniversary) {
|
|
const [existing] = await db.select()
|
|
.from(events)
|
|
.where(and(eq(events.clientId, client.id), eq(events.type, 'anniversary')))
|
|
.limit(1);
|
|
|
|
if (!existing) {
|
|
await db.insert(events).values({
|
|
userId: user.id,
|
|
clientId: client.id,
|
|
type: 'anniversary',
|
|
title: `${client.firstName}'s Anniversary`,
|
|
date: client.anniversary,
|
|
recurring: true,
|
|
reminderDays: 7,
|
|
});
|
|
created++;
|
|
} else {
|
|
await db.update(events)
|
|
.set({ date: client.anniversary, title: `${client.firstName}'s Anniversary` })
|
|
.where(eq(events.id, existing.id));
|
|
updated++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return { success: true, created, updated, clientsProcessed: userClients.length };
|
|
})
|
|
|
|
// Sync events from client birthdays/anniversaries
|
|
.post('/sync/:clientId', async ({ params, user }: { params: { clientId: string }; user: User }) => {
|
|
// Get client
|
|
const [client] = await db.select()
|
|
.from(clients)
|
|
.where(and(eq(clients.id, params.clientId), eq(clients.userId, user.id)))
|
|
.limit(1);
|
|
|
|
if (!client) {
|
|
throw new Error('Client not found');
|
|
}
|
|
|
|
const created = [];
|
|
|
|
// Create birthday event if client has birthday
|
|
if (client.birthday) {
|
|
// Check if birthday event already exists
|
|
const [existing] = await db.select()
|
|
.from(events)
|
|
.where(and(
|
|
eq(events.clientId, client.id),
|
|
eq(events.type, 'birthday')
|
|
))
|
|
.limit(1);
|
|
|
|
if (!existing) {
|
|
const [event] = await db.insert(events)
|
|
.values({
|
|
userId: user.id,
|
|
clientId: client.id,
|
|
type: 'birthday',
|
|
title: `${client.firstName}'s Birthday`,
|
|
date: client.birthday,
|
|
recurring: true,
|
|
reminderDays: 7,
|
|
})
|
|
.returning();
|
|
created.push(event);
|
|
}
|
|
}
|
|
|
|
// Create anniversary event if client has anniversary
|
|
if (client.anniversary) {
|
|
const [existing] = await db.select()
|
|
.from(events)
|
|
.where(and(
|
|
eq(events.clientId, client.id),
|
|
eq(events.type, 'anniversary')
|
|
))
|
|
.limit(1);
|
|
|
|
if (!existing) {
|
|
const [event] = await db.insert(events)
|
|
.values({
|
|
userId: user.id,
|
|
clientId: client.id,
|
|
type: 'anniversary',
|
|
title: `${client.firstName}'s Anniversary`,
|
|
date: client.anniversary,
|
|
recurring: true,
|
|
reminderDays: 7,
|
|
})
|
|
.returning();
|
|
created.push(event);
|
|
}
|
|
}
|
|
|
|
return { created };
|
|
}, {
|
|
params: t.Object({
|
|
clientId: t.String({ format: 'uuid' }),
|
|
}),
|
|
});
|