Skip to content
Closed
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
41 changes: 24 additions & 17 deletions services/api/src/routes/host/_host-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import {
} from "../../services/host/registration-wiring.js"
import type { PageService } from "../../services/host/page-service.js"

const ONE_MINUTE = "1 minute"

/** A cheap bound on the raw param; the request schema validates the slug itself. */
const PAGE_SLUG_PARAM_MAX = 120

export const CleanupIdParamsSchema = z.object({ id: IdSchema }).strict()

export const TicketTypeParamsSchema = z.object({ id: IdSchema, ticketTypeId: IdSchema }).strict()
Expand All @@ -26,24 +31,26 @@ export const WaitlistParamsSchema = z.object({ id: IdSchema, waitlistId: IdSchem

export const SeatParamsSchema = z.object({ id: IdSchema, seatId: IdSchema }).strict()

export const PageSlugParamsSchema = z.object({ slug: z.string().min(1).max(120) }).strict()
export const PageSlugParamsSchema = z
.object({ slug: z.string().min(1).max(PAGE_SLUG_PARAM_MAX) })
.strict()

export const TICKET_TYPE_READ_RATE_LIMIT = perHost({ max: 120, timeWindow: "1 minute" })
export const TICKET_TYPE_WRITE_RATE_LIMIT = perIdentity({ max: 30, timeWindow: "1 minute" })
export const QUESTION_WRITE_RATE_LIMIT = perIdentity({ max: 30, timeWindow: "1 minute" })
export const REGISTER_RATE_LIMIT = perIdentity({ max: 10, timeWindow: "1 minute" })
export const ROSTER_READ_RATE_LIMIT = perIdentity({ max: 60, timeWindow: "1 minute" })
export const WALKUP_RATE_LIMIT = perIdentity({ max: 60, timeWindow: "1 minute" })
export const REGISTRATION_WRITE_RATE_LIMIT = perIdentity({ max: 30, timeWindow: "1 minute" })
export const WAITLIST_WRITE_RATE_LIMIT = perIdentity({ max: 20, timeWindow: "1 minute" })
export const WAITLIST_READ_RATE_LIMIT = perIdentity({ max: 60, timeWindow: "1 minute" })
export const PAGE_READ_RATE_LIMIT = perIdentity({ max: 60, timeWindow: "1 minute" })
export const PAGE_WRITE_RATE_LIMIT = perIdentity({ max: 30, timeWindow: "1 minute" })
export const PUBLIC_PAGE_RATE_LIMIT = perHost({ max: 120, timeWindow: "1 minute" })
export const SCAN_RATE_LIMIT = perIdentity({ max: 300, timeWindow: "1 minute" })
export const CHECKIN_RATE_LIMIT = perIdentity({ max: 120, timeWindow: "1 minute" })
export const TICKET_READ_RATE_LIMIT = perIdentity({ max: 60, timeWindow: "1 minute" })
export const GUEST_TICKET_RATE_LIMIT = perHost({ max: 20, timeWindow: "1 minute" })
export const TICKET_TYPE_READ_RATE_LIMIT = perHost({ max: 120, timeWindow: ONE_MINUTE })
export const TICKET_TYPE_WRITE_RATE_LIMIT = perIdentity({ max: 30, timeWindow: ONE_MINUTE })
export const QUESTION_WRITE_RATE_LIMIT = perIdentity({ max: 30, timeWindow: ONE_MINUTE })
export const REGISTER_RATE_LIMIT = perIdentity({ max: 10, timeWindow: ONE_MINUTE })
export const ROSTER_READ_RATE_LIMIT = perIdentity({ max: 60, timeWindow: ONE_MINUTE })
export const WALKUP_RATE_LIMIT = perIdentity({ max: 60, timeWindow: ONE_MINUTE })
export const REGISTRATION_WRITE_RATE_LIMIT = perIdentity({ max: 30, timeWindow: ONE_MINUTE })
export const WAITLIST_WRITE_RATE_LIMIT = perIdentity({ max: 20, timeWindow: ONE_MINUTE })
export const WAITLIST_READ_RATE_LIMIT = perIdentity({ max: 60, timeWindow: ONE_MINUTE })
export const PAGE_READ_RATE_LIMIT = perIdentity({ max: 60, timeWindow: ONE_MINUTE })
export const PAGE_WRITE_RATE_LIMIT = perIdentity({ max: 30, timeWindow: ONE_MINUTE })
export const PUBLIC_PAGE_RATE_LIMIT = perHost({ max: 120, timeWindow: ONE_MINUTE })
export const SCAN_RATE_LIMIT = perIdentity({ max: 300, timeWindow: ONE_MINUTE })
export const CHECKIN_RATE_LIMIT = perIdentity({ max: 120, timeWindow: ONE_MINUTE })
export const TICKET_READ_RATE_LIMIT = perIdentity({ max: 60, timeWindow: ONE_MINUTE })
export const GUEST_TICKET_RATE_LIMIT = perHost({ max: 20, timeWindow: ONE_MINUTE })

declare module "fastify" {
interface FastifyInstance {
Expand Down
13 changes: 8 additions & 5 deletions services/api/src/routes/host/orgs.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ import { makeDrizzleOrganizationRepository } from "../../services/host/organizat
import type { OrganizationRepository } from "../../services/host/organization-repository.types.js"
import { webBaseUrlOf } from "../../lib/base-url.js"

const ONE_MINUTE = "1 minute"
const ONE_HOUR = "1 hour"

export interface OrganizationOverrides {
repo: OrganizationRepository
counters?: OrganizationServiceDeps["counters"]
Expand Down Expand Up @@ -91,15 +94,15 @@ const OrgEventsQuerySchema = z
})
.strict()

export const CREATE_ORG_RATE_LIMIT = perIdentity({ max: 5, timeWindow: "1 hour" })
export const CREATE_ORG_RATE_LIMIT = perIdentity({ max: 5, timeWindow: ONE_HOUR })

export const ORG_MUTATION_RATE_LIMIT = perIdentity({ max: 30, timeWindow: "1 minute" })
export const ORG_MUTATION_RATE_LIMIT = perIdentity({ max: 30, timeWindow: ONE_MINUTE })

export const ORG_INVITE_RATE_LIMIT = perIdentity({ max: 20, timeWindow: "1 hour" })
export const ORG_INVITE_RATE_LIMIT = perIdentity({ max: 20, timeWindow: ONE_HOUR })

export const ORG_VERIFICATION_RATE_LIMIT = perIdentity({ max: 3, timeWindow: "1 hour" })
export const ORG_VERIFICATION_RATE_LIMIT = perIdentity({ max: 3, timeWindow: ONE_HOUR })

export const PUBLIC_ORG_READ_RATE_LIMIT = perHost({ max: 120, timeWindow: "1 minute" })
export const PUBLIC_ORG_READ_RATE_LIMIT = perHost({ max: 120, timeWindow: ONE_MINUTE })

const CreateOrganizationBodySchema = trimTextFields(
CreateOrganizationRequestSchema,
Expand Down
4 changes: 3 additions & 1 deletion services/api/src/routes/host/page-views.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { parse } from "../_validate.js"
import { makeCommsRuntime } from "../../services/host/comms-wiring.js"
import type { CommsRuntime } from "../../services/host/comms-wiring.js"

export const PAGE_VIEW_RATE_LIMIT = perHost({ max: 120, timeWindow: "1 minute" })
const ONE_MINUTE = "1 minute"

export const PAGE_VIEW_RATE_LIMIT = perHost({ max: 120, timeWindow: ONE_MINUTE })

function headerOf(request: FastifyRequest, name: string): string | undefined {
const value = request.headers[name]
Expand Down
4 changes: 3 additions & 1 deletion services/api/src/routes/host/portfolio.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
} from "../../services/host/host-portfolio-service.js"
import { hostedEventCounts } from "../../services/host/portfolio-counts.js"

const ONE_MINUTE = "1 minute"

export interface HostPortfolioOverrides {
repo: HostPortfolioServiceDeps["repo"]
counts?: HostPortfolioServiceDeps["counts"]
Expand All @@ -42,7 +44,7 @@ const HostedEventsQuerySchema = z
})
.strict()

export const HOSTED_EVENTS_RATE_LIMIT = perIdentity({ max: 60, timeWindow: "1 minute" })
export const HOSTED_EVENTS_RATE_LIMIT = perIdentity({ max: 60, timeWindow: ONE_MINUTE })

export async function registerHostPortfolioRoutes(
app: FastifyInstance,
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/routes/host/registrations.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
type HostRouteContext,
} from "./_host-routes.js"

export const WalkupBodySchema = trimTextFields(CreateWalkupRegistrationRequestSchema, "name")
const WalkupBodySchema = trimTextFields(CreateWalkupRegistrationRequestSchema, "name")

function projectionFor(standing: HostStanding): RegistrationProjection {
return {
Expand Down
14 changes: 8 additions & 6 deletions services/api/src/routes/host/team.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import { requireAuth } from "../../auth/context.js"
import { parse } from "../_validate.js"
import { perIdentity } from "../../plugins/rate-limit.js"
import { route } from "../../versioning/route.js"
import { requireCapability } from "../../services/host/authz.js"
import {
makeHostTeamService,
makeSqlTeamStanding,
MY_EVENT_INVITES_DEFAULT_LIMIT,
type HostTeamService,
type HostTeamServiceDeps,
Expand All @@ -36,6 +36,9 @@ import { makeRouteNotificationService } from "../../services/route-notifier.js"
import { makeRouteCleanupReader } from "../../services/route-cleanup-reader.js"
import { webBaseUrlOf } from "../../lib/base-url.js"

const ONE_MINUTE = "1 minute"
const ONE_HOUR = "1 hour"

export interface HostTeamOverrides {
repo: HostTeamRepository
standing: HostTeamServiceDeps["standing"]
Expand Down Expand Up @@ -66,11 +69,11 @@ const MyInvitesQuerySchema = z
.object({ cursor: z.string().optional(), limit: z.string().optional() })
.strict()

export const TEAM_INVITE_RATE_LIMIT = perIdentity({ max: 20, timeWindow: "1 hour" })
export const TEAM_INVITE_RATE_LIMIT = perIdentity({ max: 20, timeWindow: ONE_HOUR })

export const TEAM_MUTATION_RATE_LIMIT = perIdentity({ max: 30, timeWindow: "1 minute" })
export const TEAM_MUTATION_RATE_LIMIT = perIdentity({ max: 30, timeWindow: ONE_MINUTE })

export const MY_EVENT_INVITES_READ_RATE_LIMIT = perIdentity({ max: 60, timeWindow: "1 minute" })
export const MY_EVENT_INVITES_READ_RATE_LIMIT = perIdentity({ max: 60, timeWindow: ONE_MINUTE })

export async function registerHostTeamRoutes(
app: FastifyInstance,
Expand Down Expand Up @@ -102,8 +105,7 @@ export async function registerHostTeamRoutes(
const sql = container.getDb().sql
return makeHostTeamService({
repo: makeDrizzleHostTeamRepository(sql),
standing: (cleanupId, userId, capability) =>
requireCapability(sql, cleanupId, userId, capability),
standing: makeSqlTeamStanding(sql),
loadEvent: makeRouteCleanupReader(container, app.log),
counters: container.getCounterStore(),
mailer: container.mailer,
Expand Down
7 changes: 3 additions & 4 deletions services/api/src/routes/host/tickets.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
} from "@civfix/shared"
import type { FastifyInstance } from "fastify"
import type { Container } from "../../di.js"
import { requireAuth } from "../../auth/context.js"
import { resolveAuthContext } from "../../auth/context.js"
import { requireAuth, resolveAuthContext } from "../../auth/context.js"
import { route } from "../../versioning/route.js"
import { parse } from "../_validate.js"
import {
Expand All @@ -28,10 +27,10 @@ import {

export function registerHostTicketRoutes(
app: FastifyInstance,
_container: Container,
container: Container,
ctx: HostRouteContext,
): void {
const csrfProtect = _container.csrf.protect
const csrfProtect = container.csrf.protect

route(
app,
Expand Down
4 changes: 1 addition & 3 deletions services/api/src/routes/legal.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ export const LEGAL_VERSIONS_RATE_LIMIT = perHost({ max: 120, timeWindow: "1 minu

export async function registerLegalRoutes(
app: FastifyInstance,
container: Container,
_container: Container,
): Promise<void> {
void container

route(
app,
"getLegalVersions",
Expand Down
4 changes: 2 additions & 2 deletions services/api/src/routes/notifications.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const MARK_NOTIFICATIONS_READ_MAX_IDS = 200

export const PUSH_TOKEN_RATE_LIMIT = perIdentity({ max: 10, timeWindow: "1 minute" })

const DeviceIdSchema = z.string().uuid()

export const MarkNotificationsReadBodySchema = MarkReadRequestSchema.extend({
ids: MarkReadRequestSchema.shape.ids.max(MARK_NOTIFICATIONS_READ_MAX_IDS),
})
Expand Down Expand Up @@ -163,5 +165,3 @@ export function normalizeDeviceId(raw: string | undefined): string | undefined {
const parsed = DeviceIdSchema.safeParse(raw.trim().toLowerCase())
return parsed.success ? parsed.data : undefined
}

const DeviceIdSchema = z.string().uuid()
44 changes: 20 additions & 24 deletions services/api/src/routes/social.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import { requireAuth } from "../auth/context.js"
import { makeContainerSuggestionsCache } from "../services/social-suggestions-wiring.js"
import {
makeSocialService,
PERSON_NOT_FOUND_MESSAGE,
type SocialNotifier,
type SocialRepository,
type SocialService,
type SocialServiceDeps,
type SocialViewer,
} from "../services/social-service.js"
import { makeDrizzleSocialRepository } from "../services/social-repository.drizzle.js"
Expand Down Expand Up @@ -75,34 +77,28 @@ export async function registerSocialRoutes(

const suggestionsCache = makeContainerSuggestionsCache(container)

function containerDeps(): Omit<SocialServiceDeps, "repo" | "logger" | "notifier"> {
return {
affiliations: container.getAffiliationLoader(),
...(suggestionsCache !== undefined ? { suggestionsCache } : {}),
presignAvatar: (k: string) => container.storage.presignGet(k, MEDIA_GET_URL_TTL_SEC),
volunteerHoursTotalFor: (userId: string) =>
container.getVolunteerHoursRepo().totalHoursFor(userId),
isBlockedEitherWay: (viewerId: string, targetId: string) =>
container.getBlocksRepo().isBlockedEitherWay(viewerId, targetId),
blockState: (viewerId: string, targetId: string) =>
container.getBlocksRepo().blockState(viewerId, targetId),
}
}

function service(withNotifier = false): SocialService {
const n = withNotifier ? notifier() : undefined
const cache = app.socialOverrides ? undefined : suggestionsCache
const presignAvatar = app.socialOverrides
? undefined
: (k: string) => container.storage.presignGet(k, MEDIA_GET_URL_TTL_SEC)
const volunteerHoursTotalFor = app.socialOverrides
? undefined
: (userId: string) => container.getVolunteerHoursRepo().totalHoursFor(userId)
const isBlockedEitherWay = app.socialOverrides
? undefined
: (viewerId: string, targetId: string) =>
container.getBlocksRepo().isBlockedEitherWay(viewerId, targetId)
const blockState = app.socialOverrides
? undefined
: (viewerId: string, targetId: string) =>
container.getBlocksRepo().blockState(viewerId, targetId)
const affiliations = app.socialOverrides ? undefined : container.getAffiliationLoader()
const wired = app.socialOverrides ? {} : containerDeps()
return makeSocialService({
repo: repo(),
logger: app.log,
...(affiliations !== undefined ? { affiliations } : {}),
...(cache !== undefined ? { suggestionsCache: cache } : {}),
...wired,
...(n !== undefined ? { notifier: n } : {}),
...(presignAvatar !== undefined ? { presignAvatar } : {}),
...(volunteerHoursTotalFor !== undefined ? { volunteerHoursTotalFor } : {}),
...(isBlockedEitherWay !== undefined ? { isBlockedEitherWay } : {}),
...(blockState !== undefined ? { blockState } : {}),
})
}

Expand All @@ -116,10 +112,10 @@ export async function registerSocialRoutes(
async function resolvePersonId(ref: string): Promise<string> {
if (UUID_RE.test(ref)) {
const person = await repo().findPersonById(ref)
if (person === null) throw AppError.notFound("Person not found")
if (person === null) throw AppError.notFound(PERSON_NOT_FOUND_MESSAGE)
return ref
}
if (ref.length > PERSON_REF_MAX) throw AppError.notFound("Person not found")
if (ref.length > PERSON_REF_MAX) throw AppError.notFound(PERSON_NOT_FOUND_MESSAGE)
return service().resolveHandleToId(ref)
}

Expand Down
Loading
Loading