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
2 changes: 2 additions & 0 deletions apps/rpc/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export const createConfiguration = () =>

STRIPE_SECRET_KEY: config(process.env.STRIPE_SECRET_KEY),

TURNSTILE_SECRET_KEY: config(process.env.TURNSTILE_SECRET_KEY),

googleWorkspace: {
serviceAccount: config(process.env.WORKSPACE_SERVICE_ACCOUNT, null),
userAccountEmail: config(process.env.WORKSPACE_USER_ACCOUNT_EMAIL, null),
Expand Down
9 changes: 9 additions & 0 deletions apps/rpc/src/modules/event/attendance-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const attendanceRouter = t.router({
.input(
z.object({
attendanceId: AttendanceSchema.shape.id,
turnstileToken: z.string(),
})
)
.mutation(async ({ input, ctx }) =>
Expand All @@ -179,9 +180,17 @@ export const attendanceRouter = t.router({
ignoreRegisteredToParent: false,
}
)

if (!result.success) {
throw new FailedPreconditionError(`Failed to register: ${result.cause}`)
}

const isTurnstileValid = await ctx.attendanceService.validateTurnstileToken(input.turnstileToken)

if (!isTurnstileValid) {
throw new FailedPreconditionError("Failed to validate turnstile token")
}

return await ctx.attendanceService.registerAttendee(handle, result)
})
),
Expand Down
21 changes: 19 additions & 2 deletions apps/rpc/src/modules/event/attendance-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ import {
type UserId,
findActiveMembership,
getMembershipGrade,
hasAttendeePaid,
isAttendable,
} from "@dotkomonline/types"
import { createAbsoluteEventPageUrl, createPoolName, getCurrentUTC, ogJoin, slugify } from "@dotkomonline/utils"
import type { TurnstileServerValidationResponse } from "@marsidev/react-turnstile"
import {
addDays,
addHours,
Expand Down Expand Up @@ -192,6 +192,7 @@ export interface AttendanceService {
user: UserId,
options: EventRegistrationOptions
): Promise<RegistrationAvailabilityResult>
validateTurnstileToken(turnstileToken: string): Promise<boolean>
/**
* Attempt to register an attendee for an event.
*
Expand Down Expand Up @@ -524,6 +525,19 @@ export function getAttendanceService(
success: true,
}
},
async validateTurnstileToken(turnstileToken: string): Promise<boolean> {
const res = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
body: `secret=${encodeURIComponent(configuration.TURNSTILE_SECRET_KEY)}&response=${encodeURIComponent(turnstileToken)}`,
headers: {
"content-type": "application/x-www-form-urlencoded",
},
})

const data = (await res.json()) as TurnstileServerValidationResponse

return data.success
},
async registerAttendee(
handle,
{ user, event, attendance, pool, reservationActiveAt, bypassedChecks, membership, options, success }
Expand Down Expand Up @@ -699,7 +713,10 @@ export function getAttendanceService(
throw new NotFoundError(`Attendee(ID=${attendeeId}) not found in Attendance(ID=${attendance.id})`)
}

const hasPaid = hasAttendeePaid(attendance, attendee)
const hasPaid =
attendance.paymentPrice &&
attendance.paymentPrice !== 0 &&
Boolean(attendee.paymentChargedAt || (attendee.paymentRefundedAt && !attendee.paymentDeadline))

if (hasPaid) {
throw new FailedPreconditionError(
Expand Down
4 changes: 2 additions & 2 deletions apps/rpc/src/modules/user/user-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as crypto from "node:crypto"
import type { S3Client } from "@aws-sdk/client-s3"
import { type PresignedPost, createPresignedPost } from "@aws-sdk/s3-presigned-post"
import { TZDate } from "@date-fns/tz"
import type { DBHandle } from "@dotkomonline/db"
import { getLogger } from "@dotkomonline/logger"
import {
Expand Down Expand Up @@ -29,7 +30,6 @@ import type { NTNUStudyPlanRepository, StudyplanCourse } from "../ntnu-study-pla
import type { NotificationPermissionsRepository } from "./notification-permissions-repository"
import type { PrivacyPermissionsRepository } from "./privacy-permissions-repository"
import type { UserRepository } from "./user-repository"
import { TZDate } from "@date-fns/tz"

export interface UserService {
/**
Expand Down Expand Up @@ -121,7 +121,7 @@ export function getUserService(
// NOTE: We grant memberships for at most one year at a time. If you are granted membership after new-years, you
// will only keep the membership until the start of the next school year.
const now = getCurrentUTC()
const firstAugust = new TZDate(getYear(now), 7, 1, 'Europe/Oslo')
const firstAugust = new TZDate(getYear(now), 7, 1, "Europe/Oslo")
const isDueThisYear = isBefore(now, firstAugust)
const endDate = isDueThisYear ? firstAugust : addYears(firstAugust, 1)

Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@dotkomonline/ui": "workspace:*",
"@dotkomonline/utils": "workspace:*",
"@hookform/resolvers": "^4.0.0",
"@marsidev/react-turnstile": "^1.3.1",
"@next/env": "^15.3.5",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"use client"

import { useTRPCSSERegisterChangeConnectionState } from "@/utils/trpc/QueryProvider"
import { useTRPC } from "@/utils/trpc/client"
import { useFullPathname } from "@/utils/use-full-pathname"
Expand All @@ -13,6 +12,7 @@ import {
} from "@dotkomonline/types"
import { Text, Title, cn } from "@dotkomonline/ui"
import { createAuthorizeUrl, getCurrentUTC } from "@dotkomonline/utils"
import { Turnstile } from "@marsidev/react-turnstile"
import { IconEdit } from "@tabler/icons-react"
import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query"
import { useSubscription } from "@trpc/tanstack-react-query"
Expand Down Expand Up @@ -54,7 +54,14 @@ export const AttendanceCard = ({
const { setTRPCSSERegisterChangeConnectionState } = useTRPCSSERegisterChangeConnectionState()

const fullPathname = useFullPathname()
const authorizeUrl = createAuthorizeUrl({ connection: "FEIDE", redirectAfter: fullPathname })
const authorizeUrl = createAuthorizeUrl({
connection: "FEIDE",
redirectAfter: fullPathname,
})

const [turnstileToken, setTurnstileToken] = useState<string | null>(null)
const [turnstileError, setTurnstileError] = useState<string | null>(null)
const [turnstileVisible, setTurnstileVisible] = useState(false)

const [closeToEvent, setCloseToEvent] = useState(false)
const [attendanceStatus, setAttendanceStatus] = useState(getAttendanceStatus(initialAttendance))
Expand Down Expand Up @@ -102,7 +109,9 @@ export const AttendanceCard = ({
onData: ({ status, attendee }) => {
// If the attendee is not the current user, we can update the state
queryClient.setQueryData(
trpc.event.attendance.getAttendance.queryOptions({ id: attendance?.id }).queryKey,
trpc.event.attendance.getAttendance.queryOptions({
id: attendance?.id,
}).queryKey,
(oldData) => {
if (!oldData) {
return oldData
Expand Down Expand Up @@ -171,10 +180,24 @@ export const AttendanceCard = ({
}

const registerForAttendance = () => {
registerMutation.mutate({ attendanceId: attendance.id })
if (!turnstileToken) {
setTurnstileError("CAPTCHA ble ikke godkjent. Refresh siden og prøv igjen.")
return
}

setTurnstileError(null)

registerMutation.mutate({
attendanceId: attendance.id,
turnstileToken,
})
}

const deregisterForAttendance = (deregisterReason: DeregisterReasonFormResult) => {
deregisterMutation.mutate({ attendanceId: attendance.id, deregisterReason })
deregisterMutation.mutate({
attendanceId: attendance.id,
deregisterReason,
})
}

const isLoading = attendanceLoading || punishmentLoading || deregisterMutation.isPending || registerMutation.isPending
Expand All @@ -192,18 +215,14 @@ export const AttendanceCard = ({
<Title element="h2" size="lg">
Påmelding
</Title>

<AttendanceDateInfo attendance={attendance} attendee={attendee} chargeScheduleDate={chargeScheduleDate} />

{punishment && hasPunishment && !attendee && <PunishmentBox punishment={punishment} />}

<MainPoolCard
attendance={attendance}
user={user}
authorizeUrl={authorizeUrl}
chargeScheduleDate={chargeScheduleDate}
/>

{attendee?.reserved && attendance.selections.length > 0 && (
<div className="flex flex-col gap-2">
<Title element="p" size="sm" className="text-base">
Expand All @@ -218,9 +237,7 @@ export const AttendanceCard = ({
/>
</div>
)}

<NonAttendablePoolsBox attendance={attendance} user={user} />

<div className="flex flex-col gap-4 sm:flex-row">
{attendee?.reserved && <TicketButton attendee={attendee} />}

Expand All @@ -232,6 +249,16 @@ export const AttendanceCard = ({
/>
</div>

<Turnstile
style={{ display: turnstileVisible ? "flex" : "hidden" }}
siteKey={"0x4AAAAAABu6CgmuG63-w5SP"}
options={{ appearance: "interaction-only", size: "flexible" }}
onSuccess={(token) => setTurnstileToken(token)}
onExpire={() => setTurnstileToken(null)}
onBeforeInteractive={() => setTurnstileVisible(true)}
onAfterInteractive={() => setTurnstileVisible(false)}
/>

<RegistrationButton
registerForAttendance={registerForAttendance}
unregisterForAttendance={deregisterForAttendance}
Expand All @@ -244,6 +271,8 @@ export const AttendanceCard = ({
chargeScheduleDate={chargeScheduleDate ?? null}
/>

{turnstileError && <Text className="text-red-600">{turnstileError}</Text>}

<div className="flex flex-row flex-wrap gap-4">
<EventRules className="text-slate-800 hover:text-black dark:text-stone-400 dark:hover:text-stone-100 transition-colors" />

Expand Down
1 change: 1 addition & 0 deletions apps/web/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ export const env = defineConfiguration({
stg: "https://cdn.staging.online.ntnu.no",
dev: "https://cdn.staging.online.ntnu.no",
}),
TURNSTILE_SITE_KEY: config(process.env.TURNSTILE_SITE_KEY),
})
36 changes: 20 additions & 16 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading