Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions apps/rpc/src/bin/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,24 @@ controller.signal.addEventListener("abort", () => {
})

export async function createFastifyContext({ req }: CreateFastifyContextOptions) {
let rawToken: string | undefined

const bearer = req.headers.authorization
if (bearer !== undefined) {
const token = bearer.substring("Bearer ".length)
const principal = await dependencies.rpcJwtService.verify(token)
rawToken = bearer.substring("Bearer ".length)
} else if (typeof req.query === "object" && req.query !== null && "connectionParams" in req.query) {
try {
const params = JSON.parse(req.query.connectionParams as string)
if (typeof params.token === "string") {
rawToken = params.token
}
} catch {
// malformed connectionParams — treat as unauthenticated
}
}

if (rawToken !== undefined) {
const principal = await dependencies.rpcJwtService.verify(rawToken)
const subject = principal.payload.sub
if (subject === undefined) {
return createTrpcContext(null, serviceLayer)
Expand Down
18 changes: 17 additions & 1 deletion apps/rpc/src/modules/article/article-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { Pageable } from "@dotkomonline/utils"
import type { ArticleRepository } from "./article-repository"
import type { ArticleTagLinkRepository } from "./article-tag-link-repository"
import type { ArticleTagRepository } from "./article-tag-repository"
import type { NotificationService } from "../notification/notification-service"

export interface ArticleService {
create(handle: DBHandle, data: ArticleWrite): Promise<Article>
Expand Down Expand Up @@ -61,6 +62,7 @@ export function getArticleService(
articleRepository: ArticleRepository,
articleTagRepository: ArticleTagRepository,
articleTagLinkRepository: ArticleTagLinkRepository,
notificationService: NotificationService,
s3Client: S3Client,
s3BucketName: string
): ArticleService {
Expand All @@ -72,7 +74,21 @@ export function getArticleService(
throw new AlreadyExistsError(`Article(Slug=${data.slug}) already exists`)
}

return await articleRepository.create(handle, data)
const createdArticle = await articleRepository.create(handle, data)

const recipients = await notificationService.retrieveIntendedRecipientIds(handle, "NEW_ARTICLE")
await notificationService.create(
handle,
recipients,
"NEW_ARTICLE",
`Ny artikkel: ${data.title}`,
`En ny artikkel "${data.title}" har blitt publisert.`,
null,
"ARTICLE",
data.slug
)

return createdArticle
},

async update(handle, articleId, data) {
Expand Down
43 changes: 36 additions & 7 deletions apps/rpc/src/modules/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { getContestRepository } from "./contest/contest-repository"
import { getContestService } from "./contest/contest-service"
import { getNotificationRepository } from "./notification/notification-repository"
import { getNotificationService } from "./notification/notification-service"
import { getNotificationTaskService } from "./notification/notification-task-service"

export type ServiceLayer = Awaited<ReturnType<typeof createServiceLayer>>

Expand Down Expand Up @@ -191,6 +192,7 @@ export async function createServiceLayer(
const contestRepository = getContestRepository()
const notificationRepository = getNotificationRepository()

const notificationService = getNotificationService(notificationRepository, userRepository, attendanceRepository, eventEmitter)
const membershipService = getMembershipService()
const emailService = isAmazonSesEmailFeatureEnabled(configuration)
? getEmailService(clients.sesClient, clients.sqsClient, configuration)
Expand All @@ -203,15 +205,35 @@ export async function createServiceLayer(
clients.s3Client,
configuration.AWS_S3_BUCKET
)
const groupService = getGroupService(groupRepository, userService, clients.s3Client, configuration.AWS_S3_BUCKET)
const jobListingService = getJobListingService(jobListingRepository)
const notificationTaskService = getNotificationTaskService(notificationService, attendanceRepository, eventRepository)
const groupService = getGroupService(
groupRepository,
userService,
notificationService,
clients.s3Client,
configuration.AWS_S3_BUCKET
)
const jobListingService = getJobListingService(jobListingRepository, taskSchedulingService, notificationService)
const markService = getMarkService(markRepository)
const personalMarkService = getPersonalMarkService(personalMarkRepository, markService, userService, emailService)
const personalMarkService = getPersonalMarkService(
personalMarkRepository,
markService,
userService,
emailService,
notificationService
)
const paymentService = getPaymentService(clients.stripe)
const paymentProductsService = getPaymentProductsService(clients.stripe)
const paymentWebhookService = getPaymentWebhookService(clients.stripe)
const auditLogService = getAuditLogService(auditLogRepository)
const eventService = getEventService(eventRepository, clients.s3Client, configuration.AWS_S3_BUCKET)
const eventService = getEventService(
eventRepository,
attendanceRepository,
taskSchedulingService,
notificationService,
clients.s3Client,
configuration.AWS_S3_BUCKET
)
const feedbackFormService = getFeedbackFormService(
feedbackFormRepository,
feedbackFormAnswerRepository,
Expand All @@ -220,7 +242,6 @@ export async function createServiceLayer(
attendanceRepository
)
const feedbackFormAnswerService = getFeedbackFormAnswerService(feedbackFormAnswerRepository, feedbackFormService)
const notificationService = getNotificationService(notificationRepository, userRepository)
const taskDiscoveryService = getLocalTaskDiscoveryService(clients.prisma, taskService, recurringTaskService)
const attendanceService = getAttendanceService(
eventEmitter,
Expand All @@ -235,14 +256,21 @@ export async function createServiceLayer(
feedbackFormService,
feedbackFormAnswerService,
configuration,
emailService
emailService,
notificationService
)
const companyService = getCompanyService(companyRepository, clients.s3Client, configuration.AWS_S3_BUCKET)
const offlineService = getOfflineService(offlineRepository, clients.s3Client, configuration.AWS_S3_BUCKET)
const offlineService = getOfflineService(
offlineRepository,
notificationService,
clients.s3Client,
configuration.AWS_S3_BUCKET
)
const articleService = getArticleService(
articleRepository,
articleTagRepository,
articleTagLinkRepository,
notificationService,
clients.s3Client,
configuration.AWS_S3_BUCKET
)
Expand All @@ -261,6 +289,7 @@ export async function createServiceLayer(
taskDiscoveryService,
taskSchedulingService,
attendanceService,
notificationTaskService,
configuration
)

Expand Down
93 changes: 63 additions & 30 deletions apps/rpc/src/modules/event/attendance-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import type { UserService } from "../user/user-service"
import type { AttendanceRepository } from "./attendance-repository"
import type { EventService } from "./event-service"
import { validateTurnstileToken } from "../../turnstile"
import type { NotificationService } from "../notification/notification-service"

type EventRegistrationOptions = {
/** Should the user be registered regardless of if registration is closed? */
Expand Down Expand Up @@ -247,7 +248,7 @@ export interface AttendanceService {
handle: DBHandle,
task: InferTaskData<VerifyFeedbackAnsweredTaskDefinition>
): Promise<void>
executeSendFeedbackFormLinkEmails(handle: DBHandle): Promise<void>
executeSendFeedbackFormLinkEmailsAndNotifications(handle: DBHandle): Promise<void>
executeVerifyAttendeeAttendedTask(handle: DBHandle): Promise<void>

/**
Expand Down Expand Up @@ -281,7 +282,8 @@ export function getAttendanceService(
feedbackFormService: FeedbackFormService,
feedbackAnswerService: FeedbackFormAnswerService,
configuration: Configuration,
emailService: EmailService
emailService: EmailService,
notificationService: NotificationService
): AttendanceService {
const logger = getLogger("attendance-service")

Expand Down Expand Up @@ -427,7 +429,23 @@ export function getAttendanceService(
}
}

return await attendanceRepository.updateAttendanceById(handle, attendanceId, input)
const updatedAttendance = await attendanceRepository.updateAttendanceById(handle, attendanceId, input)

// Reschedule the registration notification if registerStart changed
if (data.registerStart !== undefined && input.registerStart.getTime() !== attendance.registerStart.getTime()) {
const existingTask = await taskSchedulingService.findEventRegistrationNotificationTask(handle, attendanceId)
if (existingTask) {
await taskSchedulingService.cancel(handle, existingTask.id)
}
await taskSchedulingService.scheduleAt(
handle,
tasks.SEND_NOTIFICATION_EVENT_REGISTRATION,
{ attendanceId },
new TZDate(input.registerStart)
)
}

return updatedAttendance
},

async createAttendancePool(handle, attendanceId, data) {
Expand Down Expand Up @@ -1438,7 +1456,7 @@ export function getAttendanceService(
await Promise.all([...personalMarkPromises])
},

async executeSendFeedbackFormLinkEmails(handle) {
async executeSendFeedbackFormLinkEmailsAndNotifications(handle) {
const eventsEndedYesterday = await eventService.findEvents(handle, {
byHasFeedbackForm: true,
byEndDate: {
Expand Down Expand Up @@ -1469,36 +1487,51 @@ export function getAttendanceService(
(attendee) => !answers.some((answer) => answer.attendeeId === attendee.id)
)
const bcc = attendeesWithoutAnswers.map((a) => a.user.email).filter((email) => email !== null)
const notificationRecipientIds = attendeesWithoutAnswers.map((a) => a.userId)

if (bcc.length === 0) {
return
if (notificationRecipientIds.length > 0) {
const formattedDeadline = feedbackForm.answerDeadline.toLocaleString("no-NO", {
dateStyle: "short",
timeStyle: "short",
})
await notificationService.create(
handle,
notificationRecipientIds,
"NEW_FEEDBACK_FORM",
"Nytt tilbakemeldingsskjema tilgjengelig",
`Tilbakemeldingsskjema for ${event.title} er nå tilgjengelig. Gi din tilbakemelding før ${formattedDeadline}.`,
`${configuration.WEB_PUBLIC_ORIGIN}/tilbakemelding/${event.id}`,
"URL"
)
Comment on lines +1497 to +1505
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguments are passed in the wrong order to notificationService.create(). The URL is being passed as actorGroupId (6th parameter), "URL" as payloadType (7th parameter), and the payload (8th parameter) is missing entirely.

The method signature is:

create(handle, recipientIds, notificationType, title, shortDescription, actorGroupId, payloadType, payload)

This should be:

await notificationService.create(
  handle,
  notificationRecipientIds,
  "NEW_FEEDBACK_FORM",
  "Nytt tilbakemeldingsskjema tilgjengelig",
  `Tilbakemeldingsskjema for ${event.title} er nå tilgjengelig. Gi din tilbakemelding før ${formattedDeadline}.`,
  null,  // actorGroupId
  "URL",  // payloadType
  `${configuration.WEB_PUBLIC_ORIGIN}/tilbakemelding/${event.id}`  // payload
)

This will cause the notification link to be stored incorrectly and users won't be able to navigate to the feedback form.

Suggested change
await notificationService.create(
handle,
notificationRecipientIds,
"NEW_FEEDBACK_FORM",
"Nytt tilbakemeldingsskjema tilgjengelig",
`Tilbakemeldingsskjema for ${event.title} er nå tilgjengelig. Gi din tilbakemelding før ${formattedDeadline}.`,
`${configuration.WEB_PUBLIC_ORIGIN}/tilbakemelding/${event.id}`,
"URL"
)
await notificationService.create(
handle,
notificationRecipientIds,
"NEW_FEEDBACK_FORM",
"Nytt tilbakemeldingsskjema tilgjengelig",
`Tilbakemeldingsskjema for ${event.title} er nå tilgjengelig. Gi din tilbakemelding før ${formattedDeadline}.`,
null,
"URL",
`${configuration.WEB_PUBLIC_ORIGIN}/tilbakemelding/${event.id}`
)

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

}

const hostingGroupEmail = findFirstHostingGroupEmail(event) ?? "bedkom@online.ntnu.no"
logger.info(
"Sending feedback form email for Event(ID=%s) to %d attendees from email %s",
event.id,
bcc.length,
hostingGroupEmail
)
if (bcc.length > 0) {
const hostingGroupEmail = findFirstHostingGroupEmail(event) ?? "bedkom@online.ntnu.no"
logger.info(
"Sending feedback form email for Event(ID=%s) to %d attendees from email %s",
event.id,
bcc.length,
hostingGroupEmail
)

await emailService.send(
hostingGroupEmail,
[],
[],
[],
bcc,
`Tilbakemelding på ${event.title}`,
emails.FEEDBACK_FORM_LINK,
{
eventName: event.title,
eventLink: `${configuration.WEB_PUBLIC_ORIGIN}/arrangementer/${slugify(event.title)}/${event.id}`,
feedbackLink: `${configuration.WEB_PUBLIC_ORIGIN}/tilbakemelding/${event.id}`,
eventStart: event.start.toISOString(),
feedbackDeadline: feedbackForm.answerDeadline.toISOString(),
organizerEmail: hostingGroupEmail,
}
)
await emailService.send(
hostingGroupEmail,
[],
[],
[],
bcc,
`Tilbakemelding på ${event.title}`,
emails.FEEDBACK_FORM_LINK,
{
eventName: event.title,
eventLink: `${configuration.WEB_PUBLIC_ORIGIN}/arrangementer/${slugify(event.title)}/${event.id}`,
feedbackLink: `${configuration.WEB_PUBLIC_ORIGIN}/tilbakemelding/${event.id}`,
eventStart: event.start.toISOString(),
feedbackDeadline: feedbackForm.answerDeadline.toISOString(),
organizerEmail: hostingGroupEmail,
}
)
}
})

await Promise.all(promises)
Expand Down
Loading
Loading