@frigear-nu/events 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/module.d.mts +105 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +40 -31
  4. package/dist/runtime/app/components/EventAddonGroupSlideover.d.vue.ts +18 -0
  5. package/dist/runtime/app/components/EventAddonGroupSlideover.vue +66 -0
  6. package/dist/runtime/app/components/EventAddonGroupSlideover.vue.d.ts +18 -0
  7. package/dist/runtime/app/components/EventAddonSlideover.d.vue.ts +20 -0
  8. package/dist/runtime/app/components/EventAddonSlideover.vue +103 -0
  9. package/dist/runtime/app/components/EventAddonSlideover.vue.d.ts +20 -0
  10. package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +1 -9
  11. package/dist/runtime/app/components/EventAdminAddonGroups.vue +6 -48
  12. package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +1 -9
  13. package/dist/runtime/app/components/EventFormSlideover.vue +45 -23
  14. package/dist/runtime/app/components/EventTicketCard.vue +59 -62
  15. package/dist/runtime/app/components/EventTicketTypeSlideover.d.vue.ts +20 -0
  16. package/dist/runtime/app/components/EventTicketTypeSlideover.vue +176 -0
  17. package/dist/runtime/app/components/EventTicketTypeSlideover.vue.d.ts +20 -0
  18. package/dist/runtime/app/composables/index.d.ts +2 -0
  19. package/dist/runtime/app/composables/index.js +1 -0
  20. package/dist/runtime/app/composables/useEventActions.d.ts +3 -3
  21. package/dist/runtime/app/composables/useEventActions.js +9 -4
  22. package/dist/runtime/app/composables/useEventCatalogActions.d.ts +33 -0
  23. package/dist/runtime/app/composables/useEventCatalogActions.js +107 -0
  24. package/dist/runtime/app/composables/useEvents.d.ts +7 -6
  25. package/dist/runtime/app/composables/useEvents.js +17 -10
  26. package/dist/runtime/app/pages/admin-event.vue +71 -136
  27. package/dist/runtime/app/pages/admin.vue +91 -29
  28. package/dist/runtime/app/pages/event.vue +8 -4
  29. package/dist/runtime/app/pages/tickets.vue +24 -2
  30. package/dist/runtime/app/tabs/Attendees.vue +78 -39
  31. package/dist/runtime/app/tabs/Details.d.vue.ts +1 -1
  32. package/dist/runtime/app/tabs/Details.vue +112 -30
  33. package/dist/runtime/app/tabs/Details.vue.d.ts +1 -1
  34. package/dist/runtime/app/tabs/Tickets.vue +105 -132
  35. package/dist/runtime/app/utils/admin-event.d.ts +53 -38
  36. package/dist/runtime/app/utils/admin-event.js +14 -0
  37. package/dist/runtime/app/widgets/UpcomingEvents.vue +2 -1
  38. package/dist/runtime/app/widgets/UpcomingEventsStat.vue +2 -1
  39. package/dist/runtime/i18n/lang/da.json +24 -3
  40. package/dist/runtime/i18n/lang/en.json +24 -3
  41. package/dist/runtime/server/bootstrap/my-tickets.d.ts +7 -0
  42. package/dist/runtime/server/{routes/public/tickets-me.get.js → bootstrap/my-tickets.js} +2 -2
  43. package/dist/runtime/server/routes/admin/addon-group.delete.js +6 -3
  44. package/dist/runtime/server/routes/admin/addon-group.patch.js +6 -4
  45. package/dist/runtime/server/routes/admin/addon-groups.post.js +8 -6
  46. package/dist/runtime/server/routes/admin/addon.delete.js +6 -3
  47. package/dist/runtime/server/routes/admin/addon.patch.js +8 -5
  48. package/dist/runtime/server/routes/admin/addons.post.js +8 -6
  49. package/dist/runtime/server/routes/admin/attendees.get.js +38 -8
  50. package/dist/runtime/server/routes/{public → admin}/check-in.post.js +4 -3
  51. package/dist/runtime/server/routes/admin/event-clone.post.js +37 -19
  52. package/dist/runtime/server/routes/admin/event.delete.js +7 -4
  53. package/dist/runtime/server/routes/admin/event.get.js +4 -2
  54. package/dist/runtime/server/routes/admin/event.patch.js +4 -2
  55. package/dist/runtime/server/routes/admin/events.get.js +36 -3
  56. package/dist/runtime/server/routes/admin/events.post.js +3 -4
  57. package/dist/runtime/server/routes/admin/membership-options.get.js +3 -2
  58. package/dist/runtime/server/routes/admin/ticket-cancel.post.js +7 -8
  59. package/dist/runtime/server/routes/admin/ticket-refund.post.js +7 -5
  60. package/dist/runtime/server/routes/admin/ticket-type.delete.js +7 -4
  61. package/dist/runtime/server/routes/admin/ticket-type.patch.js +4 -2
  62. package/dist/runtime/server/routes/admin/ticket-types.post.js +10 -7
  63. package/dist/runtime/server/routes/admin/upcoming.get.js +7 -3
  64. package/dist/runtime/server/routes/me/ticket-pay.post.js +13 -0
  65. package/dist/runtime/server/routes/me/ticket.get.js +6 -0
  66. package/dist/runtime/server/routes/me/tickets.get.js +2 -0
  67. package/dist/runtime/server/routes/public/event.get.js +5 -2
  68. package/dist/runtime/server/routes/public/purchase.post.js +3 -1
  69. package/dist/runtime/server/utils/admin-dto.d.ts +30 -0
  70. package/dist/runtime/server/utils/admin-dto.js +101 -0
  71. package/dist/runtime/server/utils/admin-schemas.js +2 -1
  72. package/dist/runtime/server/utils/admin.d.ts +3 -1
  73. package/dist/runtime/server/utils/admin.js +20 -6
  74. package/dist/runtime/server/utils/auth.d.ts +5 -4
  75. package/dist/runtime/server/utils/auth.js +6 -7
  76. package/dist/runtime/server/utils/tickets.js +6 -11
  77. package/dist/runtime/utils/api.d.ts +48 -0
  78. package/dist/runtime/utils/api.js +51 -0
  79. package/dist/runtime/utils/index.d.ts +2 -1
  80. package/dist/runtime/utils/index.js +1 -0
  81. package/dist/types.d.mts +1 -1
  82. package/package.json +21 -16
  83. package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +0 -11
  84. package/dist/runtime/app/components/EventAdminAddonDialog.vue +0 -112
  85. package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +0 -11
  86. package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +0 -11
  87. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +0 -78
  88. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +0 -11
  89. package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +0 -20
  90. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +0 -174
  91. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +0 -20
  92. package/dist/runtime/server/routes/public/ticket-pay.post.js +0 -10
  93. package/dist/runtime/server/routes/public/ticket.get.js +0 -4
  94. /package/dist/runtime/server/routes/{public → admin}/check-in.post.d.ts +0 -0
  95. /package/dist/runtime/server/routes/{public → me}/ticket-pay.post.d.ts +0 -0
  96. /package/dist/runtime/server/routes/{public → me}/ticket.get.d.ts +0 -0
  97. /package/dist/runtime/server/routes/{public/tickets-me.get.d.ts → me/tickets.get.d.ts} +0 -0
@@ -1,14 +1,13 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { and, eq, inArray } from "drizzle-orm";
3
+ import { conflict, idParams, notFound } from "@frigear-nu/module-kit/server";
3
4
  export default defineEventHandler(async (event) => {
4
- await requireEventsManager(event);
5
- const id = getRouterParam(event, "id");
5
+ await eventsStaff.require(event);
6
+ const { id } = await getValidatedRouterParams(event, idParams().parse);
6
7
  const [ticket] = await db.select().from(schema.eventTickets).where(eq(schema.eventTickets.id, id)).limit(1);
7
- if (!ticket) throw createError({ statusCode: 404, statusMessage: "Ticket not found." });
8
- if (ticket.status === "paid" && ticket.amount > 0) {
9
- throw createError({ statusCode: 409, statusMessage: "This ticket was paid for. Refund it instead." });
10
- }
8
+ if (!ticket) throw notFound("Ticket not found.");
9
+ if (ticket.status === "paid" && ticket.amount > 0) throw conflict("This ticket was paid for. Refund it instead.");
11
10
  const [updated] = await db.update(schema.eventTickets).set({ status: "cancelled" }).where(and(eq(schema.eventTickets.id, id), inArray(schema.eventTickets.status, ["pending", "paid"]))).returning();
12
- if (!updated) throw createError({ statusCode: 409, statusMessage: `Ticket is already ${ticket.status}.` });
13
- return { ok: true };
11
+ if (!updated) throw conflict(`Ticket is already ${ticket.status}.`, { status: ticket.status });
12
+ return toAttendeeDto(updated, await ticketHolder(updated.userId));
14
13
  });
@@ -1,10 +1,12 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { eq } from "drizzle-orm";
3
+ import { conflict, idParams, notFound } from "@frigear-nu/module-kit/server";
3
4
  export default defineEventHandler(async (event) => {
4
- await requireEventsManager(event);
5
- const [ticket] = await db.select().from(schema.eventTickets).where(eq(schema.eventTickets.id, getRouterParam(event, "id"))).limit(1);
6
- if (!ticket) throw createError({ statusCode: 404, statusMessage: "Ticket not found." });
7
- if (ticket.status !== "paid" || !ticket.paymentId) throw createError({ statusCode: 409, statusMessage: "Only paid tickets with a payment can be refunded." });
5
+ await eventsStaff.require(event, { sessionOnly: true });
6
+ const { id } = await getValidatedRouterParams(event, idParams().parse);
7
+ const [ticket] = await db.select().from(schema.eventTickets).where(eq(schema.eventTickets.id, id)).limit(1);
8
+ if (!ticket) throw notFound("Ticket not found.");
9
+ if (ticket.status !== "paid" || !ticket.paymentId) throw conflict("Only paid tickets with a payment can be refunded.", { status: ticket.status });
8
10
  await refundPayment(ticket.paymentId);
9
- return { ok: true };
11
+ return toAttendeeDto(ticket, await ticketHolder(ticket.userId));
10
12
  });
@@ -1,10 +1,13 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { and, eq, inArray, sql } from "drizzle-orm";
3
+ import { conflict, idParams, noContent, notFound } from "@frigear-nu/module-kit/server";
3
4
  export default defineEventHandler(async (event) => {
4
- await requireEventsManager(event);
5
- const id = getRouterParam(event, "id");
5
+ await eventsStaff.require(event);
6
+ const { id } = await getValidatedRouterParams(event, idParams().parse);
7
+ const [existing] = await db.select({ id: schema.eventTicketTypes.id }).from(schema.eventTicketTypes).where(eq(schema.eventTicketTypes.id, id)).limit(1);
8
+ if (!existing) throw notFound("Ticket type not found.");
6
9
  const [sold] = await db.select({ count: sql`count(*)` }).from(schema.eventTickets).where(and(eq(schema.eventTickets.ticketTypeId, id), inArray(schema.eventTickets.status, ["paid", "pending"])));
7
- if (Number(sold?.count ?? 0) > 0) throw createError({ statusCode: 409, statusMessage: "This ticket type has tickets. Cancel them first." });
10
+ if (Number(sold?.count ?? 0) > 0) throw conflict("This ticket type has tickets. Cancel them first.");
8
11
  await db.delete(schema.eventTicketTypes).where(eq(schema.eventTicketTypes.id, id));
9
- return { ok: true };
12
+ return noContent(event);
10
13
  });
@@ -1,6 +1,8 @@
1
+ import { idParams } from "@frigear-nu/module-kit/server";
1
2
  import { patchTicketTypeRow, ticketTypePatchSchema } from "../../utils/admin.js";
2
3
  export default defineEventHandler(async (event) => {
3
- await requireEventsManager(event);
4
+ await eventsStaff.require(event);
5
+ const { id } = await getValidatedRouterParams(event, idParams().parse);
4
6
  const patch = await readValidatedBody(event, ticketTypePatchSchema.parse);
5
- return patchTicketTypeRow(getRouterParam(event, "id"), patch);
7
+ return toAdminTicketTypeDto(await patchTicketTypeRow(id, patch));
6
8
  });
@@ -1,12 +1,15 @@
1
- import { assertMembershipAvailable, insertTicketType } from "../../utils/admin.js";
1
+ import { db, schema } from "@nuxthub/db";
2
+ import { eq } from "drizzle-orm";
3
+ import { created, idParams, notFound } from "@frigear-nu/module-kit/server";
4
+ import { assertMembershipAvailable, assertTicketTypeKeyFree, insertTicketType } from "../../utils/admin.js";
2
5
  import { ticketTypeInput } from "../../utils/admin-schemas.js";
3
6
  export default defineEventHandler(async (event) => {
4
- await requireEventsManager(event);
7
+ await eventsStaff.require(event);
8
+ const { id } = await getValidatedRouterParams(event, idParams().parse);
5
9
  const input = await readValidatedBody(event, ticketTypeInput.parse);
10
+ const [parent] = await db.select({ id: schema.events.id }).from(schema.events).where(eq(schema.events.id, id)).limit(1);
11
+ if (!parent) throw notFound("Event not found.");
6
12
  await assertMembershipAvailable(input);
7
- const row = await insertTicketType(getRouterParam(event, "id"), input).catch((error) => {
8
- throw createError({ statusCode: 409, statusMessage: "Could not create the ticket type (unknown event, or the key is taken).", cause: error });
9
- });
10
- setResponseStatus(event, 201);
11
- return row;
13
+ await assertTicketTypeKeyFree(id, input.key);
14
+ return created(event, toAdminTicketTypeDto(await insertTicketType(id, input)));
12
15
  });
@@ -1,10 +1,11 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { and, asc, count, eq, gte } from "drizzle-orm";
3
+ import { isoDate } from "@frigear-nu/module-kit/server";
3
4
  const LIMIT = 5;
4
5
  export default defineEventHandler(async (event) => {
5
- await requireEventsManager(event);
6
+ await eventsStaff.require(event);
6
7
  const upcoming = and(eq(schema.events.status, "published"), gte(schema.events.startsAt, /* @__PURE__ */ new Date()));
7
- const [[total], items] = await Promise.all([
8
+ const [[total], rows] = await Promise.all([
8
9
  db.select({ value: count() }).from(schema.events).where(upcoming),
9
10
  db.select({
10
11
  id: schema.events.id,
@@ -13,5 +14,8 @@ export default defineEventHandler(async (event) => {
13
14
  startsAt: schema.events.startsAt
14
15
  }).from(schema.events).where(upcoming).orderBy(asc(schema.events.startsAt)).limit(LIMIT)
15
16
  ]);
16
- return { total: total?.value ?? 0, items };
17
+ return {
18
+ total: total?.value ?? 0,
19
+ items: rows.map((row) => ({ id: row.id, name: row.name, location: row.location ?? null, startsAt: isoDate(row.startsAt) }))
20
+ };
17
21
  });
@@ -0,0 +1,13 @@
1
+ import { db, schema } from "@nuxthub/db";
2
+ import { and, eq } from "drizzle-orm";
3
+ import { conflict, idParams, notFound } from "@frigear-nu/module-kit/server";
4
+ export default defineEventHandler(async (event) => {
5
+ const user = await requireUser(event, { scopes: ["tickets:write"] });
6
+ const { id } = await getValidatedRouterParams(event, idParams().parse);
7
+ const [ticket] = await db.select().from(schema.eventTickets).where(and(eq(schema.eventTickets.id, id), eq(schema.eventTickets.userId, user.id))).limit(1);
8
+ if (!ticket) throw notFound("Ticket not found.");
9
+ if (ticket.status === "paid") throw conflict("This ticket is already paid.", { status: ticket.status });
10
+ if (ticket.status !== "pending" || !ticket.paymentId) throw conflict("This ticket can no longer be paid. Buy a new one.", { status: ticket.status });
11
+ const { url } = await resumePayment(ticket.paymentId, user.id, event);
12
+ return { url };
13
+ });
@@ -0,0 +1,6 @@
1
+ import { idParams } from "@frigear-nu/module-kit/server";
2
+ export default defineEventHandler(async (event) => {
3
+ const user = await requireUser(event, { scopes: ["tickets:read"] });
4
+ const { id } = await getValidatedRouterParams(event, idParams().parse);
5
+ return getUserTicket(user.id, id);
6
+ });
@@ -0,0 +1,2 @@
1
+ import myTickets from "../../bootstrap/my-tickets.js";
2
+ export default defineEventHandler(myTickets);
@@ -1,7 +1,10 @@
1
+ import { z } from "zod";
2
+ import { notFound } from "@frigear-nu/module-kit/server";
3
+ const paramsSchema = z.object({ slug: z.string().trim().min(1).max(128) });
1
4
  export default defineEventHandler(async (event) => {
2
- const slug = getRouterParam(event, "slug");
5
+ const { slug } = await getValidatedRouterParams(event, paramsSchema.parse);
3
6
  const row = await getPublishedEventRow(slug);
4
- if (!row) throw createError({ statusCode: 404, statusMessage: "Event not found." });
7
+ if (!row) throw notFound("Event not found.");
5
8
  const user = await getUser(event, { scopes: ["tickets:read"] });
6
9
  return getEventDetail(row, user?.id);
7
10
  });
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ const paramsSchema = z.object({ slug: z.string().trim().min(1).max(128) });
2
3
  const bodySchema = z.object({
3
4
  ticketTypeId: z.string().min(1),
4
5
  addonIds: z.array(z.string().min(1)).max(50).optional(),
@@ -8,6 +9,7 @@ const bodySchema = z.object({
8
9
  });
9
10
  export default defineEventHandler(async (event) => {
10
11
  const user = await requireUser(event, { scopes: ["tickets:write"] });
12
+ const { slug } = await getValidatedRouterParams(event, paramsSchema.parse);
11
13
  const body = await readValidatedBody(event, bodySchema.parse);
12
- return purchaseTicket(event, user.id, getRouterParam(event, "slug"), body);
14
+ return purchaseTicket(event, user.id, slug, body);
13
15
  });
@@ -0,0 +1,30 @@
1
+ import { schema } from '@nuxthub/db';
2
+ import type { AdminAddonDto, AdminAddonGroupDto, AdminEventDetailDto, AdminEventDto, AdminTicketTypeDto, AttendeeDto } from '../../../types.js';
3
+ type EventRow = typeof schema.events.$inferSelect;
4
+ type TicketTypeRow = typeof schema.eventTicketTypes.$inferSelect;
5
+ type AddonGroupRow = typeof schema.eventAddonGroups.$inferSelect;
6
+ type AddonRow = typeof schema.eventAddons.$inferSelect;
7
+ type TicketRow = typeof schema.eventTickets.$inferSelect;
8
+ export declare function toAdminEventDto(row: EventRow): AdminEventDto;
9
+ export declare function toAdminTicketTypeDto(row: TicketTypeRow): AdminTicketTypeDto;
10
+ export declare function toAdminAddonGroupDto(row: AddonGroupRow): AdminAddonGroupDto;
11
+ export declare function toAdminAddonDto(row: AddonRow): AdminAddonDto;
12
+ /** The result of `getEventWithTicketTypes` as a DTO. */
13
+ export declare function toAdminEventDetailDto(row: EventRow & {
14
+ ticketTypes: Array<TicketTypeRow & {
15
+ addonGroups: Array<AddonGroupRow & {
16
+ addons: AddonRow[];
17
+ }>;
18
+ }>;
19
+ }): AdminEventDetailDto;
20
+ /** A ticket with its holder, for staff. Keeps the QR `code` (staff check tickets in with it). */
21
+ export declare function toAttendeeDto(row: TicketRow, holder: {
22
+ name: string | null;
23
+ email: string | null;
24
+ }): AttendeeDto;
25
+ /** The name and email of a ticket's holder, for `toAttendeeDto`. */
26
+ export declare function ticketHolder(userId: number): Promise<{
27
+ name: string | null;
28
+ email: string | null;
29
+ }>;
30
+ export {};
@@ -0,0 +1,101 @@
1
+ import { db, schema } from "@nuxthub/db";
2
+ import { eq } from "drizzle-orm";
3
+ import { isoDate } from "@frigear-nu/module-kit/server";
4
+ import { normalizeMembership } from "../../utils/index.js";
5
+ export function toAdminEventDto(row) {
6
+ return {
7
+ id: row.id,
8
+ slug: row.slug,
9
+ name: row.name,
10
+ description: row.description ?? null,
11
+ body: row.body ?? null,
12
+ image: row.image ?? null,
13
+ location: row.location ?? null,
14
+ locationUrl: row.locationUrl ?? null,
15
+ startsAt: isoDate(row.startsAt),
16
+ endsAt: isoDate(row.endsAt),
17
+ status: row.status,
18
+ requiresMembership: normalizeMembership(row.requiresMembership ?? void 0),
19
+ createdAt: isoDate(row.createdAt),
20
+ updatedAt: isoDate(row.updatedAt)
21
+ };
22
+ }
23
+ export function toAdminTicketTypeDto(row) {
24
+ return {
25
+ id: row.id,
26
+ eventId: row.eventId,
27
+ key: row.key,
28
+ name: row.name,
29
+ description: row.description ?? null,
30
+ price: row.price,
31
+ currency: row.currency,
32
+ hidePrice: row.hidePrice,
33
+ capacity: row.capacity ?? null,
34
+ saleStartsAt: isoDate(row.saleStartsAt),
35
+ saleEndsAt: isoDate(row.saleEndsAt),
36
+ saleSchedule: row.saleSchedule ?? null,
37
+ maxPerUser: row.maxPerUser,
38
+ requiresMembership: normalizeMembership(row.requiresMembership ?? void 0),
39
+ excludesMembership: normalizeMembership(row.excludesMembership ?? void 0),
40
+ isDefault: row.isDefault,
41
+ sortOrder: row.sortOrder,
42
+ createdAt: isoDate(row.createdAt),
43
+ updatedAt: isoDate(row.updatedAt)
44
+ };
45
+ }
46
+ export function toAdminAddonGroupDto(row) {
47
+ return {
48
+ id: row.id,
49
+ ticketTypeId: row.ticketTypeId,
50
+ title: row.title,
51
+ required: row.required,
52
+ multiple: row.multiple,
53
+ sortOrder: row.sortOrder
54
+ };
55
+ }
56
+ export function toAdminAddonDto(row) {
57
+ return {
58
+ id: row.id,
59
+ groupId: row.groupId,
60
+ label: row.label,
61
+ description: row.description ?? null,
62
+ price: row.price,
63
+ capacity: row.capacity ?? null,
64
+ sortOrder: row.sortOrder
65
+ };
66
+ }
67
+ export function toAdminEventDetailDto(row) {
68
+ return {
69
+ ...toAdminEventDto(row),
70
+ ticketTypes: row.ticketTypes.map((type) => ({
71
+ ...toAdminTicketTypeDto(type),
72
+ addonGroups: type.addonGroups.map((group) => ({
73
+ ...toAdminAddonGroupDto(group),
74
+ addons: group.addons.map(toAdminAddonDto)
75
+ }))
76
+ }))
77
+ };
78
+ }
79
+ export function toAttendeeDto(row, holder) {
80
+ return {
81
+ id: row.id,
82
+ eventId: row.eventId,
83
+ ticketTypeId: row.ticketTypeId,
84
+ userId: row.userId,
85
+ status: row.status,
86
+ addons: row.addons,
87
+ amount: row.amount,
88
+ currency: row.currency,
89
+ paymentId: row.paymentId ?? null,
90
+ code: row.code,
91
+ locale: row.locale ?? null,
92
+ checkedInAt: isoDate(row.checkedInAt),
93
+ createdAt: isoDate(row.createdAt),
94
+ userName: holder.name,
95
+ userEmail: holder.email
96
+ };
97
+ }
98
+ export async function ticketHolder(userId) {
99
+ const [user] = await db.select({ name: schema.users.name, email: schema.users.email }).from(schema.users).where(eq(schema.users.id, userId)).limit(1);
100
+ return { name: user?.name ?? null, email: user?.email ?? null };
101
+ }
@@ -39,7 +39,8 @@ export const ticketTypeInput = z.object({
39
39
  addonGroups: z.array(addonGroupInput).optional()
40
40
  });
41
41
  export const eventInput = z.object({
42
- slug: z.string().min(1).max(128).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use lowercase letters, numbers and dashes."),
42
+ // `me` is the static segment of `/api/events/me/...`: h3's router never falls back to `:slug` past it.
43
+ slug: z.string().min(1).max(128).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use lowercase letters, numbers and dashes.").refine((slug) => slug !== "me", "This URL name is reserved."),
43
44
  name: translated,
44
45
  description: translated.nullish(),
45
46
  body: translated.nullish(),
@@ -4,6 +4,8 @@ import type { addonGroupInput } from './admin-schemas.js';
4
4
  type TicketTypeInput = z.output<typeof ticketTypeInput>;
5
5
  type AddonGroupInput = z.output<typeof addonGroupInput>;
6
6
  export declare function insertAddonGroup(ticketTypeId: string, input: AddonGroupInput): Promise<any>;
7
+ /** 409 when another ticket type of `eventId` already uses `key` (keys are unique per event). */
8
+ export declare function assertTicketTypeKeyFree(eventId: string, key: string, exceptId?: string): Promise<void>;
7
9
  export declare function insertTicketType(eventId: string, input: TicketTypeInput): Promise<any>;
8
10
  /**
9
11
  * Membership rules only make sense with a membership provider (`@frigear-nu/subscriptions`): without one a
@@ -37,8 +39,8 @@ export declare const eventPatchSchema: z.ZodObject<{
37
39
  location: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodString>, z.ZodTransform<{
38
40
  [k: string]: string;
39
41
  }, Record<string, string>>>]>>>>;
40
- endsAt: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
41
42
  locationUrl: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
43
+ endsAt: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
42
44
  requiresMembership: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodLiteral<"any">, z.ZodArray<z.ZodString>]>>>>;
43
45
  }, z.core.$strip>;
44
46
  export declare function patchEventRow(id: string, patch: z.output<typeof eventPatchSchema>): Promise<any>;
@@ -1,6 +1,7 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { newId } from "../../utils/index.js";
3
- import { asc, eq } from "drizzle-orm";
3
+ import { and, asc, eq } from "drizzle-orm";
4
+ import { badRequest, conflict, notFound } from "@frigear-nu/module-kit/server";
4
5
  import { loadCatalog } from "./catalog.js";
5
6
  import { eventInput, ticketTypeInput } from "./admin-schemas.js";
6
7
  export async function insertAddonGroup(ticketTypeId, input) {
@@ -11,6 +12,10 @@ export async function insertAddonGroup(ticketTypeId, input) {
11
12
  }
12
13
  return row;
13
14
  }
15
+ export async function assertTicketTypeKeyFree(eventId, key, exceptId) {
16
+ const [taken] = await db.select({ id: schema.eventTicketTypes.id }).from(schema.eventTicketTypes).where(and(eq(schema.eventTicketTypes.eventId, eventId), eq(schema.eventTicketTypes.key, key))).limit(1);
17
+ if (taken && taken.id !== exceptId) throw conflict("A ticket type with this key already exists on the event.", { field: "key" });
18
+ }
14
19
  export async function insertTicketType(eventId, input) {
15
20
  const { addonGroups, ...type } = input;
16
21
  const [row] = await db.insert(schema.eventTicketTypes).values({
@@ -30,13 +35,13 @@ export async function insertTicketType(eventId, input) {
30
35
  export async function assertMembershipAvailable(...inputs) {
31
36
  if (!inputs.some((input) => input?.requiresMembership || input?.excludesMembership)) return;
32
37
  if ((await getMembershipOptions()).available) return;
33
- throw createError({ statusCode: 400, statusMessage: "Membership rules need a membership provider such as @frigear-nu/subscriptions." });
38
+ throw badRequest("Membership rules need a membership provider such as @frigear-nu/subscriptions.");
34
39
  }
35
40
  export async function createEventWithCatalog(raw) {
36
41
  const { ticketTypes, ...input } = raw;
37
42
  await assertMembershipAvailable(input, ...ticketTypes ?? []);
38
43
  const [taken] = await db.select({ id: schema.events.id }).from(schema.events).where(eq(schema.events.slug, input.slug)).limit(1);
39
- if (taken) throw createError({ statusCode: 409, statusMessage: "An event with this slug already exists." });
44
+ if (taken) throw conflict("An event with this slug already exists.", { field: "slug" });
40
45
  const [row] = await db.insert(schema.events).values({
41
46
  id: newId("evn"),
42
47
  ...input,
@@ -48,20 +53,29 @@ export async function createEventWithCatalog(raw) {
48
53
  export const eventPatchSchema = eventInput.omit({ ticketTypes: true }).partial();
49
54
  export async function patchEventRow(id, patch) {
50
55
  await assertMembershipAvailable(patch);
56
+ if (patch.slug) {
57
+ const [taken] = await db.select({ id: schema.events.id }).from(schema.events).where(eq(schema.events.slug, patch.slug)).limit(1);
58
+ if (taken && taken.id !== id) throw conflict("An event with this slug already exists.", { field: "slug" });
59
+ }
51
60
  const [row] = await db.update(schema.events).set(patch).where(eq(schema.events.id, id)).returning();
52
- if (!row) throw createError({ statusCode: 404, statusMessage: "Event not found." });
61
+ if (!row) throw notFound("Event not found.");
53
62
  return row;
54
63
  }
55
64
  export const ticketTypePatchSchema = ticketTypeInput.omit({ addonGroups: true }).partial();
56
65
  export async function patchTicketTypeRow(id, patch) {
57
66
  await assertMembershipAvailable(patch);
67
+ if (patch.key) {
68
+ const [current] = await db.select({ eventId: schema.eventTicketTypes.eventId }).from(schema.eventTicketTypes).where(eq(schema.eventTicketTypes.id, id)).limit(1);
69
+ if (!current) throw notFound("Ticket type not found.");
70
+ await assertTicketTypeKeyFree(current.eventId, patch.key, id);
71
+ }
58
72
  const [row] = await db.update(schema.eventTicketTypes).set(patch).where(eq(schema.eventTicketTypes.id, id)).returning();
59
- if (!row) throw createError({ statusCode: 404, statusMessage: "Ticket type not found." });
73
+ if (!row) throw notFound("Ticket type not found.");
60
74
  return row;
61
75
  }
62
76
  export async function getEventWithTicketTypes(id) {
63
77
  const [row] = await db.select().from(schema.events).where(eq(schema.events.id, id)).limit(1);
64
- if (!row) throw createError({ statusCode: 404, statusMessage: "Event not found." });
78
+ if (!row) throw notFound("Event not found.");
65
79
  const ticketTypes = await db.select().from(schema.eventTicketTypes).where(eq(schema.eventTicketTypes.eventId, id)).orderBy(asc(schema.eventTicketTypes.sortOrder));
66
80
  const { groups, addons } = await loadCatalog(id);
67
81
  return {
@@ -1,8 +1,9 @@
1
1
  import type { H3Event } from 'h3';
2
- /** Gate for staff routes (event/ticket management, check-in). */
3
- export declare function requireEventsManager(event: H3Event): Promise<{
4
- userId: number;
5
- }>;
2
+ /**
3
+ * The staff gate of every `/api/admin/events` route (event and ticket management, check-in). The host decides who is
4
+ * staff through the `events:authorize` Nitro hook; denied by default. Bearer tokens need the `events:manage` scope.
5
+ */
6
+ export declare const eventsStaff: any;
6
7
  /** Non-throwing form of the `events:authorize` policy for the caller of an already-authenticated request (MCP). */
7
8
  export declare const canManageEvents: (event: H3Event, principal: {
8
9
  user: {
@@ -1,7 +1,6 @@
1
- const gate = definePolicyGate("events:authorize");
2
- export async function requireEventsManager(event) {
3
- const user = await requireUser(event, { scopes: ["events:manage"] });
4
- await gate.assert(event, user.id, "You are not allowed to manage events.");
5
- return { userId: user.id };
6
- }
7
- export const canManageEvents = (event, principal) => gate.isAllowed(event, principal.user.id);
1
+ export const eventsStaff = defineStaffGuard({
2
+ hook: "events:authorize",
3
+ scope: "events:manage",
4
+ message: "You are not allowed to manage events."
5
+ });
6
+ export const canManageEvents = (event, principal) => eventsStaff.can(event, principal.user.id);
@@ -1,23 +1,18 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { and, eq, sql } from "drizzle-orm";
3
- import { resolveSameOriginUrl } from "@frigear-nu/stripe/utils";
3
+ import { apiError, resolveSameOriginUrl } from "@frigear-nu/module-kit/server";
4
4
  import { newId, newTicketCode, normalizeMembership, translate } from "../../utils/index.js";
5
5
  import { countAddonSeats, countSeats, getPublishedEventRow, holdsSeat, loadCatalog, toTicketDtos } from "./catalog.js";
6
6
  import { meetsMembership, passesExclusion } from "./membership.js";
7
- const fail = (statusCode, statusMessage) => createError({ statusCode, statusMessage });
7
+ const CODES = { 400: "bad_request", 403: "forbidden", 404: "not_found", 409: "conflict" };
8
+ const fail = (statusCode, message) => apiError(statusCode, CODES[statusCode], message);
8
9
  function returnUrls(event, slug, ticketId, body) {
9
- const origin = getRequestURL(event).origin;
10
10
  const pages = useRuntimeConfig().public.events?.pages ?? {};
11
11
  const eventPath = (pages.event || "/events/:slug").replace(":slug", encodeURIComponent(slug));
12
- const resolve = (input, status, name) => {
13
- if (input) {
14
- const safe = resolveSameOriginUrl(input, origin);
15
- if (!safe) throw fail(400, `${name} must be a relative path or an URL on the same origin.`);
16
- return safe;
17
- }
18
- return `${origin}${eventPath}?ticket=${ticketId}&payment=${status}`;
12
+ return {
13
+ successUrl: resolveSameOriginUrl(event, body.successUrl, { fallback: `${eventPath}?ticket=${ticketId}&payment=success`, name: "successUrl" }),
14
+ cancelUrl: resolveSameOriginUrl(event, body.cancelUrl, { fallback: `${eventPath}?ticket=${ticketId}&payment=cancel`, name: "cancelUrl" })
19
15
  };
20
- return { successUrl: resolve(body.successUrl, "success", "successUrl"), cancelUrl: resolve(body.cancelUrl, "cancel", "cancelUrl") };
21
16
  }
22
17
  async function release(ticketId, status = "cancelled") {
23
18
  await db.update(schema.eventTickets).set({ status }).where(eq(schema.eventTickets.id, ticketId));
@@ -0,0 +1,48 @@
1
+ export declare const eventsApi: {
2
+ /** `GET` the published events. */
3
+ readonly list: "/api/events";
4
+ /** `GET` one published event with its ticket types (personalised when signed in). */
5
+ readonly event: (slug: string) => string;
6
+ /** `POST` a ticket purchase. */
7
+ readonly purchase: (slug: string) => string;
8
+ readonly me: {
9
+ /** `GET` the caller's tickets (also the account bootstrap's `events:tickets:mine`). */
10
+ readonly tickets: "/api/events/me/tickets";
11
+ /** `GET` one of the caller's tickets. */
12
+ readonly ticket: (id: string) => string;
13
+ /** `POST` to resume paying for one of the caller's pending tickets. */
14
+ readonly pay: (id: string) => string;
15
+ };
16
+ readonly admin: {
17
+ /** `GET` (paginated) / `POST` events. */
18
+ readonly events: "/api/admin/events";
19
+ /** `GET` / `PATCH` / `DELETE` one event. */
20
+ readonly event: (id: string) => string;
21
+ /** `POST` a copy of an event as a new draft. */
22
+ readonly clone: (id: string) => string;
23
+ /** `GET` an event's tickets with their holders (paginated). */
24
+ readonly attendees: (id: string) => string;
25
+ /** `POST` a ticket type to an event. */
26
+ readonly ticketTypes: (eventId: string) => string;
27
+ /** `PATCH` / `DELETE` one ticket type. */
28
+ readonly ticketType: (id: string) => string;
29
+ /** `POST` an add-on group to a ticket type. */
30
+ readonly addonGroups: (ticketTypeId: string) => string;
31
+ /** `PATCH` / `DELETE` one add-on group. */
32
+ readonly addonGroup: (id: string) => string;
33
+ /** `POST` an add-on to a group. */
34
+ readonly addons: (groupId: string) => string;
35
+ /** `PATCH` / `DELETE` one add-on. */
36
+ readonly addon: (id: string) => string;
37
+ /** `POST` to cancel an unpaid or free ticket. */
38
+ readonly cancelTicket: (id: string) => string;
39
+ /** `POST` to refund a paid ticket (session only). */
40
+ readonly refundTicket: (id: string) => string;
41
+ /** `GET` the memberships a ticket type can require or exclude. */
42
+ readonly membershipOptions: "/api/admin/events/membership-options";
43
+ /** `GET` the next published events and how many there are (admin dashboard). */
44
+ readonly upcoming: "/api/admin/events/upcoming";
45
+ /** `POST` a scanned ticket code. */
46
+ readonly checkIn: "/api/admin/events/check-in";
47
+ };
48
+ };
@@ -0,0 +1,51 @@
1
+ const enc = encodeURIComponent;
2
+ const PUBLIC_BASE = "/api/events";
3
+ const ADMIN_BASE = "/api/admin/events";
4
+ export const eventsApi = {
5
+ /** `GET` the published events. */
6
+ list: PUBLIC_BASE,
7
+ /** `GET` one published event with its ticket types (personalised when signed in). */
8
+ event: (slug) => `${PUBLIC_BASE}/${enc(slug)}`,
9
+ /** `POST` a ticket purchase. */
10
+ purchase: (slug) => `${PUBLIC_BASE}/${enc(slug)}/purchase`,
11
+ me: {
12
+ /** `GET` the caller's tickets (also the account bootstrap's `events:tickets:mine`). */
13
+ tickets: `${PUBLIC_BASE}/me/tickets`,
14
+ /** `GET` one of the caller's tickets. */
15
+ ticket: (id) => `${PUBLIC_BASE}/me/tickets/${enc(id)}`,
16
+ /** `POST` to resume paying for one of the caller's pending tickets. */
17
+ pay: (id) => `${PUBLIC_BASE}/me/tickets/${enc(id)}/pay`
18
+ },
19
+ admin: {
20
+ /** `GET` (paginated) / `POST` events. */
21
+ events: ADMIN_BASE,
22
+ /** `GET` / `PATCH` / `DELETE` one event. */
23
+ event: (id) => `${ADMIN_BASE}/${enc(id)}`,
24
+ /** `POST` a copy of an event as a new draft. */
25
+ clone: (id) => `${ADMIN_BASE}/${enc(id)}/clone`,
26
+ /** `GET` an event's tickets with their holders (paginated). */
27
+ attendees: (id) => `${ADMIN_BASE}/${enc(id)}/tickets`,
28
+ /** `POST` a ticket type to an event. */
29
+ ticketTypes: (eventId) => `${ADMIN_BASE}/${enc(eventId)}/ticket-types`,
30
+ /** `PATCH` / `DELETE` one ticket type. */
31
+ ticketType: (id) => `${ADMIN_BASE}/ticket-types/${enc(id)}`,
32
+ /** `POST` an add-on group to a ticket type. */
33
+ addonGroups: (ticketTypeId) => `${ADMIN_BASE}/ticket-types/${enc(ticketTypeId)}/addon-groups`,
34
+ /** `PATCH` / `DELETE` one add-on group. */
35
+ addonGroup: (id) => `${ADMIN_BASE}/addon-groups/${enc(id)}`,
36
+ /** `POST` an add-on to a group. */
37
+ addons: (groupId) => `${ADMIN_BASE}/addon-groups/${enc(groupId)}/addons`,
38
+ /** `PATCH` / `DELETE` one add-on. */
39
+ addon: (id) => `${ADMIN_BASE}/addons/${enc(id)}`,
40
+ /** `POST` to cancel an unpaid or free ticket. */
41
+ cancelTicket: (id) => `${ADMIN_BASE}/tickets/${enc(id)}/cancel`,
42
+ /** `POST` to refund a paid ticket (session only). */
43
+ refundTicket: (id) => `${ADMIN_BASE}/tickets/${enc(id)}/refund`,
44
+ /** `GET` the memberships a ticket type can require or exclude. */
45
+ membershipOptions: `${ADMIN_BASE}/membership-options`,
46
+ /** `GET` the next published events and how many there are (admin dashboard). */
47
+ upcoming: `${ADMIN_BASE}/upcoming`,
48
+ /** `POST` a scanned ticket code. */
49
+ checkIn: `${ADMIN_BASE}/check-in`
50
+ }
51
+ };
@@ -2,7 +2,8 @@ import { translate } from '@frigear-nu/ui/translated';
2
2
  import type { MembershipRequirement } from '../types.js';
3
3
  export { translate };
4
4
  export * from './links.js';
5
- export type { AddonDto, AddonGroupDto, AuthorizeContext, CheckInResponse, EventDetailDto, EventStatus, EventSummaryDto, MembershipRequirement, ModuleOptions, PurchaseBeforeContext, PurchaseBody, PurchaseResponse, TicketDto, TicketHookContext, TicketStatus, TicketTypeDto, Translated, } from '../types.js';
5
+ export * from './api.js';
6
+ export type { AddonDto, AdminAddonDto, AdminAddonGroupDto, AdminEventDetailDto, AdminEventDto, AdminTicketTypeDto, AttendeeDto, AddonGroupDto, AuthorizeContext, CheckInResponse, EventDetailDto, EventStatus, EventSummaryDto, MembershipOptionsDto, MembershipRequirement, ModuleOptions, PurchaseBeforeContext, PurchaseBody, PurchaseResponse, TicketDto, TicketHookContext, TicketStatus, TicketTypeDto, Translated, UpcomingEventsDto, } from '../types.js';
6
7
  /** The currency tickets and add-ons are sold in unless they say otherwise (the column default). */
7
8
  export declare const DEFAULT_CURRENCY = "DKK";
8
9
  /** Normalises a stored requirement: empty plan lists mean "any active subscription". */
@@ -2,6 +2,7 @@ import { translate } from "@frigear-nu/ui/translated";
2
2
  import { formatMoney as formatPrice } from "@frigear-nu/ui/money";
3
3
  export { translate };
4
4
  export * from "./links.js";
5
+ export * from "./api.js";
5
6
  export const DEFAULT_CURRENCY = "DKK";
6
7
  export function normalizeMembership(value) {
7
8
  if (value === "any") return "any";
package/dist/types.d.mts CHANGED
@@ -2,4 +2,4 @@ export { type Translated } from '@frigear-nu/ui/translated'
2
2
 
3
3
  export { default } from './module.mjs'
4
4
 
5
- export { type AddonDto, type AddonGroupDto, type AuthorizeContext, type CheckInResponse, type EventDetailDto, type EventStatus, type EventSummaryDto, type EventsRuntimeConfig, type MembershipCheckContext, type MembershipOption, type MembershipOptionsContext, type MembershipRequirement, type ModuleOptions, type PublicModuleOptions, type PurchaseBeforeContext, type PurchaseBody, type PurchaseResponse, type TicketDto, type TicketHookContext, type TicketStatus, type TicketTypeDto } from './module.mjs'
5
+ export { type AddonDto, type AddonGroupDto, type AdminAddonDto, type AdminAddonGroupDto, type AdminEventDetailDto, type AdminEventDto, type AdminTicketTypeDto, type AttendeeDto, type AuthorizeContext, type CheckInResponse, type EventDetailDto, type EventStatus, type EventSummaryDto, type EventsRuntimeConfig, type MembershipCheckContext, type MembershipOption, type MembershipOptionsContext, type MembershipOptionsDto, type MembershipRequirement, type ModuleOptions, type PublicModuleOptions, type PurchaseBeforeContext, type PurchaseBody, type PurchaseResponse, type TicketDto, type TicketHookContext, type TicketStatus, type TicketTypeDto, type UpcomingEventsDto } from './module.mjs'