security-update #23
+11
-1
@@ -673,11 +673,21 @@ const openApiSpec = {
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/events/next': {
|
||||
get: {
|
||||
tags: ['Events'],
|
||||
summary: 'Get next upcoming event (chronological)',
|
||||
description: 'Get the single earliest upcoming published event, ignoring featured promotion.',
|
||||
responses: {
|
||||
200: { description: 'Next event or null' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/events/next/upcoming': {
|
||||
get: {
|
||||
tags: ['Events'],
|
||||
summary: 'Get next upcoming event',
|
||||
description: 'Get the single next upcoming published event.',
|
||||
description: 'Get the featured event if valid, otherwise the single next upcoming published event.',
|
||||
responses: {
|
||||
200: { description: 'Next event or null' },
|
||||
},
|
||||
|
||||
@@ -294,6 +294,45 @@ async function getEventTicketCount(eventId: string): Promise<number> {
|
||||
return ticketCount?.count || 0;
|
||||
}
|
||||
|
||||
// Get the earliest upcoming published event with ticket counts (ignores featured promotion)
|
||||
async function getNextChronologicalUpcoming(): Promise<any | null> {
|
||||
const now = getNow();
|
||||
const event = await dbGet<any>(
|
||||
(db as any)
|
||||
.select()
|
||||
.from(events)
|
||||
.where(
|
||||
and(
|
||||
eq((events as any).status, 'published'),
|
||||
gte((events as any).startDatetime, now)
|
||||
)
|
||||
)
|
||||
.orderBy((events as any).startDatetime)
|
||||
.limit(1)
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bookedCount = await getEventTicketCount(event.id);
|
||||
const normalized = normalizeEvent(event);
|
||||
return {
|
||||
...normalized,
|
||||
bookedCount,
|
||||
availableSeats: calculateAvailableSeats(normalized.capacity, bookedCount),
|
||||
};
|
||||
}
|
||||
|
||||
// Get next upcoming event (public) - earliest upcoming published event, ignores featured promotion
|
||||
eventsRouter.get('/next', async (c) => {
|
||||
const event = await getNextChronologicalUpcoming();
|
||||
if (!event) {
|
||||
return c.json({ event: null });
|
||||
}
|
||||
return c.json({ event: { ...event, isFeatured: false } });
|
||||
});
|
||||
|
||||
// Get next upcoming event (public) - returns featured event if valid, otherwise next upcoming
|
||||
eventsRouter.get('/next/upcoming', async (c) => {
|
||||
const now = getNow();
|
||||
@@ -357,34 +396,13 @@ eventsRouter.get('/next/upcoming', async (c) => {
|
||||
}
|
||||
|
||||
// Fallback: get the next upcoming published event
|
||||
const event = await dbGet<any>(
|
||||
(db as any)
|
||||
.select()
|
||||
.from(events)
|
||||
.where(
|
||||
and(
|
||||
eq((events as any).status, 'published'),
|
||||
gte((events as any).startDatetime, now)
|
||||
)
|
||||
)
|
||||
.orderBy((events as any).startDatetime)
|
||||
.limit(1)
|
||||
);
|
||||
|
||||
const event = await getNextChronologicalUpcoming();
|
||||
|
||||
if (!event) {
|
||||
return c.json({ event: null });
|
||||
}
|
||||
|
||||
const bookedCount = await getEventTicketCount(event.id);
|
||||
const normalized = normalizeEvent(event);
|
||||
return c.json({
|
||||
event: {
|
||||
...normalized,
|
||||
bookedCount,
|
||||
availableSeats: calculateAvailableSeats(normalized.capacity, bookedCount),
|
||||
isFeatured: false,
|
||||
},
|
||||
});
|
||||
|
||||
return c.json({ event: { ...event, isFeatured: false } });
|
||||
});
|
||||
|
||||
// Create event (admin/organizer only)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
|
||||
async function getFeaturedEventSlug(): Promise<string | null> {
|
||||
try {
|
||||
const revalidateSeconds =
|
||||
parseInt(process.env.NEXT_EVENT_REVALIDATE_SECONDS || '3600', 10) || 3600;
|
||||
const response = await fetch(`${apiUrl}/api/events/next/upcoming`, {
|
||||
next: { tags: ['next-event'], revalidate: revalidateSeconds },
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
return data.event?.slug || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function FeaturedEventPage() {
|
||||
const slug = await getFeaturedEventSlug();
|
||||
redirect(slug ? `/events/${slug}` : '/events');
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
|
||||
async function getNextEventSlug(): Promise<string | null> {
|
||||
try {
|
||||
const revalidateSeconds =
|
||||
parseInt(process.env.NEXT_EVENT_REVALIDATE_SECONDS || '3600', 10) || 3600;
|
||||
const response = await fetch(`${apiUrl}/api/events/next`, {
|
||||
next: { tags: ['next-event'], revalidate: revalidateSeconds },
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
return data.event?.slug || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function NextEventPage() {
|
||||
const slug = await getNextEventSlug();
|
||||
redirect(slug ? `/events/${slug}` : '/events');
|
||||
}
|
||||
@@ -58,6 +58,18 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
changeFrequency: 'daily',
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${siteUrl}/next`,
|
||||
lastModified: now,
|
||||
changeFrequency: 'daily',
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${siteUrl}/featured`,
|
||||
lastModified: now,
|
||||
changeFrequency: 'daily',
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${siteUrl}/community`,
|
||||
lastModified: now,
|
||||
|
||||
@@ -11,6 +11,8 @@ export const eventsApi = {
|
||||
|
||||
getById: (id: string) => fetchApi<{ event: Event }>(`/api/events/${id}`),
|
||||
|
||||
getNext: () => fetchApi<{ event: Event | null }>('/api/events/next'),
|
||||
|
||||
getNextUpcoming: () => fetchApi<{ event: Event | null }>('/api/events/next/upcoming'),
|
||||
|
||||
create: (data: Partial<Event>) =>
|
||||
|
||||
Reference in New Issue
Block a user