Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Vapor

struct SponsorScanController: RouteCollection {
func boot(routes: any RoutesBuilder) throws {
let protected = routes.grouped(AppBearerMiddleware(), SponsorScanMiddleware())
protected.post("scan", use: onScan)
}

/// Scan an attendee's QR code and return their details.
/// - Parameter request: The incoming request containing the ticket stub
/// - Returns: Attendee details (name, company, email, custom questions)
@Sendable private func onScan(request: Request) async throws -> SponsorScanResponse {
let payload = try request.content.decode(SponsorScanRequest.self)

guard let currentEvent = request.storage.get(CurrentEventKey.self),
let titoEvent = currentEvent.titoEvent else {

Check warning on line 16 in Sources/App/Features/Sponsors/Controllers/SponsorScanController.swift

View workflow job for this annotation

GitHub Actions / lint

Wrap the opening brace of multiline statements. (wrapMultilineStatementBraces)

Check warning on line 16 in Sources/App/Features/Sponsors/Controllers/SponsorScanController.swift

View workflow job for this annotation

GitHub Actions / lint

Place else, catch or while keyword in accordance with current style (same or next line). (elseOnSameLine)
throw Abort(.internalServerError, reason: "Unable to identify event")
}

guard let attendeeTicket = try await TitoService(event: titoEvent).ticket(stub: payload.stub, req: request) else {
throw Abort(.notFound, reason: "Ticket not found")
}

return SponsorScanResponse(ticket: attendeeTicket)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Vapor

/// Middleware that validates the authenticated user has a sponsor ticket type.
/// This middleware must be used after `AppBearerMiddleware` which validates the JWT
/// and stores the ticket in request storage.
struct SponsorScanMiddleware: AsyncMiddleware {
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
guard let token = request.headers.bearerAuthorization?.token else {
throw Abort(.unauthorized, reason: "No authorization token provided")
}

let payload = try await request.jwt.verify(token, as: AppTicketJWTPayload.self)

// Check if the ticket type contains "sponsor"
guard payload.ticketType.lowercased().contains("sponsor") else {
throw Abort(.forbidden, reason: "Sponsor ticket required to access this endpoint")
}

return try await next.respond(to: request)
}
}
19 changes: 19 additions & 0 deletions Sources/App/Features/Sponsors/Models/SponsorScanResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Vapor

struct SponsorScanResponse: Content {
let name: String
let company: String?
let email: String
let responses: [String: String]

init(ticket: TitoTicket) {
self.name = ticket.fullName

Check warning on line 10 in Sources/App/Features/Sponsors/Models/SponsorScanResponse.swift

View workflow job for this annotation

GitHub Actions / lint

Insert/remove explicit self where applicable. (redundantSelf)
self.company = ticket.company_name

Check warning on line 11 in Sources/App/Features/Sponsors/Models/SponsorScanResponse.swift

View workflow job for this annotation

GitHub Actions / lint

Insert/remove explicit self where applicable. (redundantSelf)
self.email = ticket.email

Check warning on line 12 in Sources/App/Features/Sponsors/Models/SponsorScanResponse.swift

View workflow job for this annotation

GitHub Actions / lint

Insert/remove explicit self where applicable. (redundantSelf)
self.responses = ticket.responses

Check warning on line 13 in Sources/App/Features/Sponsors/Models/SponsorScanResponse.swift

View workflow job for this annotation

GitHub Actions / lint

Insert/remove explicit self where applicable. (redundantSelf)
}
}

struct SponsorScanRequest: Content {
let stub: String
}
1 change: 1 addition & 0 deletions Sources/App/Features/Tickets/Models/TitoTicket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct TitoTicket: Codable {
let avatar_url: URL?
let responses: [String: String]
let release: Release?
let release_title: String?
let email: String
let reference: String
let qr_url: String?
Expand Down
1 change: 1 addition & 0 deletions Sources/App/routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func routes(_ app: Application) throws {

let apiRoutes = app.grouped("api", "v1")
try apiRoutes.grouped("sponsors").register(collection: SponsorAPIController())
try apiRoutes.grouped("sponsors").register(collection: SponsorScanController())
try apiRoutes.grouped("schedule").register(collection: ScheduleAPIController())
try apiRoutes.grouped("local").register(collection: LocalAPIController())
try apiRoutes.grouped("tickets").register(collection: TicketsAPIController())
Expand Down
Loading