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
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ jobs:
FLICKR_SECRET=${{ secrets.FLICKR_SECRET }}
REFUND_PERIOD=${{ secrets.REFUND_PERIOD }}
CONFERENCE=${{ matrix.conference }}
JWT_SECRET=${{ secrets.JWT_SECRET }}

- name: Update GitHub Deployment
uses: bobheadxi/deployments@v1
Expand Down
20 changes: 19 additions & 1 deletion Package.resolved

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

2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ let package = Package(
.package(url: "https://github.com/vapor/apns.git", from: "5.0.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
.package(url: "https://github.com/handya/markdown.git", branch: "fix/xcode-16"),
.package(url: "https://github.com/vapor/jwt.git", from: "5.0.0"),

// This package is used by AWSSDKSwiftCore on Linux only. We add it here (but don't utilise it) in order to
// add it to the Package.resolved file. This ensures that when Docker resolves this project, it will not ignore
Expand All @@ -35,6 +36,7 @@ let package = Package(
.product(name: "VaporAPNS", package: "apns"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "JWT", package: "jwt"),
],
swiftSettings: swiftSettings
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ struct ActivityRouteController: RouteCollection {
let activity = try await request.parameters.get("id").map { Activity.find($0, on: request.db) }?.get()
try await activity?.$event.load(on: request.db)

let context = try await buildContext(from: request.db, activity: activity)
let context = try await buildContext(req: request, activity: activity)
return try await request.view.render("Admin/Form/activity_form", context)
}

private func buildContext(from db: any Database, activity: Activity?) async throws -> ActivityContext {
let events = try await Event.query(on: db).sort(\.$date).all()
private func buildContext(req: Request, activity: Activity?) async throws -> ActivityContext {
let events = try await Event.query(on: req.db)
.filter(\.$conference == req.application.conference.rawValue)
.sort(\.$date)
.all()
return ActivityContext(activity: activity, events: events)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Fluent
import Vapor

struct AppLoginRouteController: RouteCollection {
func boot(routes: any RoutesBuilder) throws {
routes.post("ticket", use: login)

routes.group(AppBearerMiddleware()) { builder in
builder.get("ticket", use: ticket)
}
}

@Sendable private func login(request: Request) async throws -> String {
let payload = try request.content.decode(AppLoginRequest.self, as: .json)

var eventQuery = Event.query(on: request.db)
.filter(\.$conference == request.application.conference.rawValue)

if let eventId = payload.event {
eventQuery = eventQuery.filter(\.$id == eventId)
} else {
eventQuery = eventQuery.filter(\.$isCurrent == true)
}

guard let event = try await eventQuery.first() else {
throw Abort(.notFound, reason: "failed to identify event")
}

guard let titoEvent = event.titoEvent else {
throw Abort(.internalServerError, reason: "login has not been setup for this event")
}

let lookup = TicketLoginPayload(email: payload.email, ticket: payload.ticket)
guard let ticket = try await TitoService(event: titoEvent).ticket(payload: lookup, req: request) else {
throw Abort(.forbidden)
}

guard let email = ticket.email else {
// An email is `nil` when the ticket has not been assigned
// This an impossible flow though as to get past the ticket lookup you must have matched the input email
// therefore ticket.email will always equal payload.email (± formatting)
throw Abort(.internalServerError, reason: "requested unassigned ticket")
}

return try await request.jwt.sign(AppTicketJWTPayload(
sub: .init(value: email),
iat: .init(value: .now),
slug: ticket.slug,
reference: ticket.reference,
event: event.requireID(),
ticketType: ticket.release.title
))
}

@Sendable private func ticket(request: Request) async throws -> TitoTicketResponse {
guard let ticket = request.storage.get(TicketStorage.self) else {
throw Abort(.unauthorized, reason: "Ticket not present in session storage")
}

return TitoTicketResponse(ticket: ticket)
}
}

struct TitoTicketResponse: Content {
static let defaultContentType: HTTPMediaType = .json
let ticket: TitoTicket
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Fluent
import Vapor

struct AppBearerMiddleware: AsyncMiddleware {
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
guard let token = request.headers.bearerAuthorization?.token else {
throw Abort(.unauthorized)
}

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

guard let currentEvent = try await Event.query(on: request.db).filter(\.$id == payload.event).first() else {
throw Abort(.internalServerError)
}

try await currentEvent.$days.load(on: request.db)

guard let titoEvent = currentEvent.titoEvent else {
throw Abort(.badRequest, reason: "unable to identify tito project")
}

await request.storage.setWithAsyncShutdown(CurrentEventKey.self, to: currentEvent)

guard let ticket = try await TitoService(event: titoEvent).ticket(stub: payload.slug, req: request) else {
throw Abort(.unauthorized)
}

await request.storage.setWithAsyncShutdown(TicketStorage.self, to: ticket)
return try await next.respond(to: request)
}
}
7 changes: 7 additions & 0 deletions Sources/App/Features/App Login/Models/AppLoginRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Vapor

struct AppLoginRequest: Content {
let event: UUID?
let email: String
let ticket: String
}
13 changes: 13 additions & 0 deletions Sources/App/Features/App Login/Models/AppTicketResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation
import JWT

struct AppTicketJWTPayload: JWTPayload {
let sub: SubjectClaim
let iat: IssuedAtClaim
let slug: String
let reference: String
let event: UUID
let ticketType: String

func verify(using _: some JWTKit.JWTAlgorithm) async throws {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct CheckInAPIController: RouteCollection {
}

// If it is, then return Event.checkinKey or `CHECKIN_TAG`
let event = try await Event.getCurrent(on: request.db)
let event = try await Event.getCurrent(req: request)
return CheckIn(tag: event.checkinKey ?? Environment.get("CHECKIN_TAG") ?? "")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ struct DropInRouteController: RouteCollection {
.first()
}?.get()

let events = try await Event.query(on: request.db).sort(\.$date).all()
let events = try await Event.query(on: request.db)
.filter(\.$conference == request.application.conference.rawValue)
.sort(\.$date)
.all()
let context = SessionContext(
session: session,
events: events,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ struct EventDayRouteController: RouteCollection {
let day = try await request.parameters.get("id").map { EventDay.find($0, on: request.db) }?.get()
try await day?.$event.load(on: request.db)

let events = try await Event.query(on: request.db).all()
let events = try await Event
.query(on: request.db)
.filter(\.$conference == request.application.conference.rawValue)
.all()

let context = EventDayContext(day: day, events: events)
return try await request.view.render("Admin/Form/event_day_form", context)
Expand Down
13 changes: 11 additions & 2 deletions Sources/App/Features/Events/Models/Event.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,24 @@ final class Event: Model, Content, @unchecked Sendable {
}

extension Event {
static func getCurrent(on db: any Database) async throws -> Event {
guard let event = try await Event.query(on: db).filter(\.$isCurrent == true).first() else {
static func getCurrent(req: Request) async throws -> Event {
guard let event = try await Event.query(on: req.db)
.filter(\.$isCurrent == true)
.filter(\.$conference == req.application.conference.rawValue)
.first()
else {
throw Abort(.notFound, reason: "could not locate current event")
}

return event
}

func shouldBeReturned(by request: Request) -> Bool {
// prevent being able to specify an ID of a conference on a different deployment
guard request.application.conference.rawValue == conference else {
return false
}

// if the request has a query parameter of 'event' (the event ID)
// then only return 'true' if the ID provided matches this event
if let targetEvent: String = try? request.query.get(String.self, at: "event") {
Expand Down
4 changes: 2 additions & 2 deletions Sources/App/Features/Home/HomeRouteController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,11 @@ struct HomeRouteController: RouteCollection {
private func getEvent(for req: Request) async throws -> Event? {
if let yearParameterItem = req.parameters.get("year") {
return try await Event.query(on: req.db)
.filter("name", .equal, "SwiftLeeds \(yearParameterItem)")
.filter("name", .custom("ilike"), "\(req.application.conference.rawValue) \(yearParameterItem)")
.first()
}

return try await Event.getCurrent(on: req.db)
return try await Event.getCurrent(req: req)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ struct PresentationRouteController: RouteCollection {
@Sendable private func onRead(request: Request) async throws -> View {
let presentation = try await request.parameters.get("id").map { Presentation.find($0, on: request.db) }?.get()
let speakers = try await Speaker.query(on: request.db).sort(\.$name).all()
let events = try await Event.query(on: request.db).sort(\.$date).all()
let events = try await Event
.query(on: request.db)
.filter(\.$conference == request.application.conference.rawValue)
.sort(\.$date)
.all()
let context = PresentationContext(presentation: presentation, speakers: speakers, events: events, hasSecondSpeaker: presentation?.$secondSpeaker.id != nil)

return try await request.view.render("Admin/Form/presentation_form", context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ struct PurchaseRouteController: RouteCollection {
}

@Sendable func get(req: Request) async throws -> View {
let event = try await Event.getCurrent(on: req.db)
let event = try await Event.getCurrent(req: req)
return try await req.view.render("Purchase/index", HomeContext(event: EventContext(event: event)))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ struct SponsorRouteController: RouteCollection {

@Sendable private func onRead(request: Request) async throws -> View {
let sponsor = try await request.parameters.get("id").map { Sponsor.find($0, on: request.db) }?.get()
let events = try await Event.query(on: request.db).sort(\.$date).all()
let events = try await Event.query(on: request.db)
.filter(\.$conference == request.application.conference.rawValue)
.sort(\.$date)
.all()
let context = SponsorContext(sponsor: sponsor, sponsorLevels: sponsorLevels, events: events)

return try await request.view.render("Admin/Form/sponsor_form", context)
Expand Down
12 changes: 6 additions & 6 deletions Sources/App/Features/Talks/TalkRouteController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ struct TalkRouteController: RouteCollection {
title: "ICYMI: Enums Are...",
description: """
2 facts about me: I am obsessed with enums, and I am a professional drummer and percussionist 🎵

For this talk, I'd like to marry these great loves by

1. sharing my curiosity about enums with the SwiftLeeds community
2. leveraging my musical skills as the backdrop for a sample app called PocketPerc: Beat Generator

In the app, a user would be able to toggle on any number of instrument samples to build a unique groove all their own, with samples largely recorded by me 🥁

To build up to the beat part (obviously the Grand Finale), I’d like to showcase enums in all their glory, aka possibly/hopefully using them way too much and inspiring some wild ideas about what they can do 🤗 The static data we would need is a perfect opportunity to eclipse structs and classes as the go-to data types, and instead rely heavily on enums to build views and stay categorized! To make it extra fun, I plan to limit myself as much as possible to only that data type, see what boundaries we bump up against, and discuss where and why we might want to pivot to another structure for production.

Here’s a non-exhaustive list of the power moves I’ve been exploring:

Protocol conformance CaseIterable, Equatable and Comparable conformances Associated values (with and without default values) Pattern matching Screen management Static properties Computed properties Interaction with ObservableObjects Custom raw types
""".replacingOccurrences(of: "\n", with: "<br />"),
author: "Jessie Linden",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ struct TicketHubRouteController: RouteCollection {
func boot(routes: any RoutesBuilder) throws {
routes.grouped(ValidTicketMiddleware()).group("ticket") { builder in
builder.get { req -> View in
// guard let currentEvent = try await Event.query(on: req.db).filter(\.$name == "SwiftLeeds 2023").first() else {
guard let currentEvent = req.storage.get(CurrentEventKey.self) else {
throw Abort(.badRequest, reason: "unable to identify current event")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct TicketLoginController: RouteCollection {
}

routes.post("ticketLogin", "validate") { req async throws -> Response in
guard let currentTitoEvent = try await Event.query(on: req.db).filter(\.$isCurrent == true).first()?.titoEvent else {
guard let currentTitoEvent = try await Event.getCurrent(req: req).titoEvent else {
throw Abort(.badRequest, reason: "unable to identify current event")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import Vapor

struct ValidTicketMiddleware: AsyncMiddleware {
func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
guard let currentEvent = try await Event.query(on: request.db).filter(\.$isCurrent == true).first() else {
throw Abort(.badRequest, reason: "unable to identify current event")
}

let currentEvent = try await Event.getCurrent(req: request)
try await currentEvent.$days.load(on: request.db)

guard let titoEvent = currentEvent.titoEvent else {
Expand Down
14 changes: 6 additions & 8 deletions Sources/App/configure.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import JWT
import Leaf
import Vapor
import VaporAPNS
Expand Down Expand Up @@ -51,15 +52,12 @@ public func configure(_ app: Application) async throws {

// Routes
app.routes.defaultMaxBodySize = "10mb"
try routes(app)

switch app.conference {
case .kotlinleeds:
app.get { req in
req.view.render("Kotlin/home")
}

case .swiftleeds:
try routes(app)
// JWT
if let secret = Environment.get("JWT_SECRET") {
await app.jwt.keys.add(hmac: HMACKey(from: secret), digestAlgorithm: .sha256)
app.logger.info("Setup JWT successfully")
}

// APNS
Expand Down
Loading