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': {
|
'/api/events/next/upcoming': {
|
||||||
get: {
|
get: {
|
||||||
tags: ['Events'],
|
tags: ['Events'],
|
||||||
summary: 'Get next upcoming event',
|
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: {
|
responses: {
|
||||||
200: { description: 'Next event or null' },
|
200: { description: 'Next event or null' },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -294,6 +294,45 @@ async function getEventTicketCount(eventId: string): Promise<number> {
|
|||||||
return ticketCount?.count || 0;
|
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
|
// Get next upcoming event (public) - returns featured event if valid, otherwise next upcoming
|
||||||
eventsRouter.get('/next/upcoming', async (c) => {
|
eventsRouter.get('/next/upcoming', async (c) => {
|
||||||
const now = getNow();
|
const now = getNow();
|
||||||
@@ -357,34 +396,13 @@ eventsRouter.get('/next/upcoming', async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: get the next upcoming published event
|
// Fallback: get the next upcoming published event
|
||||||
const event = await dbGet<any>(
|
const event = await getNextChronologicalUpcoming();
|
||||||
(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) {
|
if (!event) {
|
||||||
return c.json({ event: null });
|
return c.json({ event: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookedCount = await getEventTicketCount(event.id);
|
return c.json({ event: { ...event, isFeatured: false } });
|
||||||
const normalized = normalizeEvent(event);
|
|
||||||
return c.json({
|
|
||||||
event: {
|
|
||||||
...normalized,
|
|
||||||
bookedCount,
|
|
||||||
availableSeats: calculateAvailableSeats(normalized.capacity, bookedCount),
|
|
||||||
isFeatured: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create event (admin/organizer only)
|
// 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',
|
changeFrequency: 'daily',
|
||||||
priority: 0.9,
|
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`,
|
url: `${siteUrl}/community`,
|
||||||
lastModified: now,
|
lastModified: now,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export const eventsApi = {
|
|||||||
|
|
||||||
getById: (id: string) => fetchApi<{ event: Event }>(`/api/events/${id}`),
|
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'),
|
getNextUpcoming: () => fetchApi<{ event: Event | null }>('/api/events/next/upcoming'),
|
||||||
|
|
||||||
create: (data: Partial<Event>) =>
|
create: (data: Partial<Event>) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user