Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e661abd24 | ||
|
|
1f59916efa | ||
|
|
e728a06b9a | ||
|
|
1d9b87500d | ||
|
|
97faff1c89 | ||
|
|
5b2183677d | ||
|
|
af6dffe534 |
@@ -7,6 +7,26 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.10.5] - 2026-10-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Events that have already started but not yet finished now show on the home page and the Events page. Both pages were hiding an event as soon as its start time passed; they now keep it listed until its end time, which matches what `GET /api/events` already returns.
|
||||
|
||||
## [1.10.4] - 2026-09-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- "Add to calendar" (`GET /api/events/:id/ics`) always failed with a 400 in production: the `ics` library's `url` field requires a scheme (`https://...`), but `FRONTEND_URL`/`APP_BASE_URL` was configured without one, so `createEvent()` rejected it before generating any output. A missing scheme is now assumed to be `https://` instead of failing.
|
||||
|
||||
## [1.10.3] - 2026-08-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- Creating an event was never recorded in the admin audit log — 1.10.0 added `event_created` as a logged action on the frontend's filter list, but the backend's `createEvent` never actually called `logAdminAction`, only `updateEvent`/`deleteEvent` did. Event creation is now logged the same way.
|
||||
- Staff cancelling a registration via `PUT /api/registrations/:id` (the staff status-change endpoint, separate from the owner-facing `DELETE /:id` cancel route) wasn't logged at all — only the `DELETE` path logged `registration_cancelled`. Both paths now log it, verified against all six audit categories promised in 1.10.0 (refunds, donation assign/unassign, manual registrations, staff-initiated cancellations, event create/update/delete, settings changes) with no other gaps found.
|
||||
- Added a "Back to dashboard" link to Admin → Audit log, matching the back-link pattern already used on the Cashup page.
|
||||
|
||||
## [1.10.2] - 2026-08-28
|
||||
|
||||
### Fixed
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "event-management-backend",
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "event-management-backend",
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.5",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.4.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "event-management-backend",
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.5",
|
||||
"description": "Event Management System Backend",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -68,8 +68,19 @@ const createEvent = async (req, res) => {
|
||||
location: location || null,
|
||||
};
|
||||
|
||||
const logEventCreate = (createdEvent) => logAdminAction({
|
||||
actorId: req.user?.id,
|
||||
actorRole: req.user?.role,
|
||||
action: 'event_created',
|
||||
targetType: 'Event',
|
||||
targetId: createdEvent.id,
|
||||
metadata: { title: createdEvent.title },
|
||||
ip: getClientIp(req),
|
||||
});
|
||||
|
||||
try {
|
||||
const event = await prisma.event.create({ data });
|
||||
logEventCreate(event);
|
||||
|
||||
// Automatically create a main ticket (event option) with the event price — contact-only
|
||||
// events have no bookable options, so there's nothing to auto-create for them.
|
||||
@@ -133,24 +144,28 @@ const createEvent = async (req, res) => {
|
||||
// @ts-ignore delete field and retry
|
||||
delete data.registrationDeadline;
|
||||
const event = await prisma.event.create({ data });
|
||||
logEventCreate(event);
|
||||
return res.status(201).json(event);
|
||||
}
|
||||
if (msg.includes('Unknown argument `goLiveAt`')) {
|
||||
// @ts-ignore delete field and retry
|
||||
delete data.goLiveAt;
|
||||
const event = await prisma.event.create({ data });
|
||||
logEventCreate(event);
|
||||
return res.status(201).json(event);
|
||||
}
|
||||
if (msg.includes('Unknown argument `createdById`')) {
|
||||
// @ts-ignore delete field and retry
|
||||
delete data.createdById;
|
||||
const event = await prisma.event.create({ data });
|
||||
logEventCreate(event);
|
||||
return res.status(201).json(event);
|
||||
}
|
||||
if (msg.includes('Unknown argument `redirectUrl`')) {
|
||||
// @ts-ignore delete field and retry
|
||||
delete data.redirectUrl;
|
||||
const event = await prisma.event.create({ data });
|
||||
logEventCreate(event);
|
||||
return res.status(201).json(event);
|
||||
}
|
||||
throw err;
|
||||
@@ -478,7 +493,12 @@ const getEventIcs = async (req, res) => {
|
||||
}
|
||||
|
||||
const { buildEventIcs } = require('../utils/icsUtils');
|
||||
const frontendUrl = (process.env.FRONTEND_URL || process.env.APP_BASE_URL || 'http://localhost:3001').replace(/\/$/, '');
|
||||
let frontendUrl = (process.env.FRONTEND_URL || process.env.APP_BASE_URL || 'http://localhost:3001').replace(/\/$/, '');
|
||||
// The ics library's url validator requires a scheme; guard against FRONTEND_URL/APP_BASE_URL
|
||||
// being configured without one (e.g. "example.com" instead of "https://example.com").
|
||||
if (!/^[a-z0-9+.-]+:\/\//i.test(frontendUrl)) {
|
||||
frontendUrl = `https://${frontendUrl}`;
|
||||
}
|
||||
const ics = buildEventIcs(event, `${frontendUrl}/events/${event.id}`);
|
||||
|
||||
res.setHeader('Content-Type', 'text/calendar; charset=utf-8');
|
||||
|
||||
@@ -626,6 +626,22 @@ const updateRegistrationStatus = async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// This is the staff-only status-change endpoint (separate from the owner-facing
|
||||
// DELETE /:id cancel route), so any transition into 'cancelled' here is always a
|
||||
// staff-initiated cancellation — log it the same way DELETE /:id does, so both
|
||||
// paths land under the one 'registration_cancelled' filter in the audit log.
|
||||
if (status === 'cancelled' && registration.status !== 'cancelled') {
|
||||
logAdminAction({
|
||||
actorId: req.user.id,
|
||||
actorRole: req.user.role,
|
||||
action: 'registration_cancelled',
|
||||
targetType: 'Registration',
|
||||
targetId: req.params.id,
|
||||
metadata: { registrationOwnerId: registration.userId, previousStatus: registration.status },
|
||||
ip: getClientIp(req),
|
||||
});
|
||||
}
|
||||
|
||||
// Generate tickets and email them when status is manually set to 'paid' by staff
|
||||
if (status === 'paid') {
|
||||
(async () => {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hope-events-frontend",
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hope-events-frontend",
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.5",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hope-events-frontend",
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
|
||||
@@ -113,7 +113,8 @@ export default function AdminAuditLogPage() {
|
||||
<History className="w-5 h-5 text-brand-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Admin Audit Log</h1>
|
||||
<button className="text-xs text-brand-600 hover:underline" onClick={() => router.push("/dashboard/admin")}>← Back to dashboard</button>
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mt-0.5">Admin Audit Log</h1>
|
||||
<p className="text-sm text-gray-500">{total} action{total !== 1 ? "s" : ""} recorded</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default async function EventsPage() {
|
||||
const now = Date.now();
|
||||
const upcoming = (events || []).filter(e => {
|
||||
if (e.cashupStatus === "closed") return false;
|
||||
const t = new Date(e.startDate).getTime();
|
||||
const t = new Date(e.endDate || e.startDate).getTime();
|
||||
return !isNaN(t) && t > now;
|
||||
});
|
||||
const sorted = [...upcoming].sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime());
|
||||
|
||||
@@ -45,7 +45,7 @@ export default async function HomePage() {
|
||||
]);
|
||||
const now = Date.now();
|
||||
const upcoming = (events || []).filter(e => {
|
||||
const t = new Date(e.startDate).getTime();
|
||||
const t = new Date(e.endDate || e.startDate).getTime();
|
||||
return !isNaN(t) && t > now;
|
||||
});
|
||||
const sorted = [...upcoming].sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime());
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hope-events",
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hope-events",
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.5",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.2.1"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hope-events",
|
||||
"version": "1.10.2",
|
||||
"version": "1.10.5",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev:backend": "cd backend && npm run dev",
|
||||
|
||||
Reference in New Issue
Block a user