security-update #23

Merged
Michilis merged 4 commits from security-update into main 2026-06-29 06:47:19 +00:00
6 changed files with 114 additions and 26 deletions
Showing only changes of commit f5fe87613f - Show all commits
+11 -1
View File
@@ -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' },
},
+43 -25
View File
@@ -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');
}
+23
View File
@@ -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');
}
+12
View File
@@ -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,
+2
View File
@@ -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>) =>