Skip to content
Merged
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: 1 addition & 1 deletion Sources/App/Features/Auth/Models/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class User: Authenticatable, ModelAuthenticatable, Content, ModelSessionAu
}

/// Unique identifier for this user.
@ID()
@ID
var id: UUID?

/// The user's name.
Expand Down
2 changes: 1 addition & 1 deletion Sources/App/Features/Auth/Models/UserToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class UserToken: Model, Content, ModelTokenAuthenticatable, Codable, @unch
return timestamp > Date()
}

@ID()
@ID
var id: UUID?

@Field(key: "value")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 {
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)
}
}
23 changes: 23 additions & 0 deletions Sources/App/Features/Sponsors/Models/SponsorScanResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Vapor

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

init(ticket: TitoTicket) {
name = ticket.fullName
firstName = ticket.first_name
lastName = ticket.last_name
company = ticket.company_name
email = ticket.email
responses = ticket.responses
}
}

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