Skip to content

Commit

Permalink
Improve routing API (#20)
Browse files Browse the repository at this point in the history
* Improve routing API

* Rename generic types

* Update README for v0.7.0

* Update SPI links
  • Loading branch information
fpseverino authored Jan 17, 2025
1 parent 900653c commit 64b2f36
Show file tree
Hide file tree
Showing 21 changed files with 518 additions and 855 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
Use the SPM string to easily include the dependendency in your `Package.swift` file.

```swift
.package(url: "https://github.com/vapor-community/wallet.git", from: "0.6.0")
.package(url: "https://github.com/vapor-community/wallet.git", from: "0.7.0")
```

> Note: This package is made for Vapor 4.
Expand All @@ -39,7 +39,7 @@ Add the `VaporWalletPasses` product to your target's dependencies:
.product(name: "VaporWalletPasses", package: "wallet")
```

See the framework's [documentation](https://swiftpackageindex.com/vapor-community/wallet/documentation/passes) for information and guides on how to use it.
See the framework's [documentation](https://swiftpackageindex.com/vapor-community/wallet/documentation/vaporwalletpasses) for information and guides on how to use it.

For information on Apple Wallet passes, see the [Apple Developer Documentation](https://developer.apple.com/documentation/walletpasses).

Expand All @@ -54,6 +54,6 @@ Add the `VaporWalletOrders` product to your target's dependencies:
.product(name: "VaporWalletOrders", package: "wallet")
```

See the framework's [documentation](https://swiftpackageindex.com/vapor-community/wallet/documentation/orders) for information and guides on how to use it.
See the framework's [documentation](https://swiftpackageindex.com/vapor-community/wallet/documentation/vaporwalletorders) for information and guides on how to use it.

For information on Apple Wallet orders, see the [Apple Developer Documentation](https://developer.apple.com/documentation/walletorders).
16 changes: 0 additions & 16 deletions Sources/VaporWallet/Testing/SecretMiddleware.swift

This file was deleted.

4 changes: 2 additions & 2 deletions Sources/VaporWallet/VaporWallet.docc/VaporWallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The `VaporWallet` framework provides a set of tools shared by the `VaporWalletPa
The `VaporWalletPasses` framework provides a set of tools to help you create, build, and distribute digital passes for the Apple Wallet app using a Vapor server.
It also provides a way to update passes after they have been distributed, using APNs, and models to store pass and device data.

See the framework's [documentation](https://swiftpackageindex.com/vapor-community/wallet/documentation/passes) for information and guides on how to use it.
See the framework's [documentation](https://swiftpackageindex.com/vapor-community/wallet/documentation/vaporwalletpasses) for information and guides on how to use it.

For information on Apple Wallet passes, see the [Apple Developer Documentation](https://developer.apple.com/documentation/walletpasses).

Expand All @@ -28,6 +28,6 @@ For information on Apple Wallet passes, see the [Apple Developer Documentation](
The `VaporWalletOrders` framework provides a set of tools to help you create, build, and distribute orders that users can track and manage in Apple Wallet using a Vapor server.
It also provides a way to update orders after they have been distributed, using APNs, and models to store order and device data.

See the framework's [documentation](https://swiftpackageindex.com/vapor-community/wallet/documentation/orders) for information and guides on how to use it.
See the framework's [documentation](https://swiftpackageindex.com/vapor-community/wallet/documentation/vaporwalletorders) for information and guides on how to use it.

For information on Apple Wallet orders, see the [Apple Developer Documentation](https://developer.apple.com/documentation/walletorders).
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import FluentKit
import FluentWalletOrders
import Vapor

struct AppleOrderMiddleware<O: OrderModel>: AsyncMiddleware {
struct AppleOrderMiddleware<OrderType: OrderModel>: AsyncMiddleware {
func respond(
to request: Request, chainingTo next: any AsyncResponder
) async throws -> Response {
guard
let id = request.parameters.get("orderIdentifier", as: UUID.self),
let authToken = request.headers["Authorization"].first?.replacingOccurrences(of: "AppleOrder ", with: ""),
(try await O.query(on: request.db)
(try await OrderType.query(on: request.db)
.filter(\._$id == id)
.filter(\._$authenticationToken == authToken)
.first()) != nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import FluentWalletOrders
import Foundation

extension OrdersService: AsyncModelMiddleware {
public func create(model: OD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
public func create(model: OrderDataType, on db: any Database, next: any AnyAsyncModelResponder) async throws {
let order = Order(
typeIdentifier: OD.typeIdentifier,
typeIdentifier: OrderDataType.typeIdentifier,
authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()
)
try await order.save(on: db)
model._$order.id = try order.requireID()
try await next.create(model, on: db)
}

public func update(model: OD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
public func update(model: OrderDataType, on db: any Database, next: any AnyAsyncModelResponder) async throws {
let order = try await model._$order.get(on: db)
order.updatedAt = Date.now
try await order.save(on: db)
Expand All @@ -23,17 +23,17 @@ extension OrdersService: AsyncModelMiddleware {
}

extension OrdersServiceCustom: AsyncModelMiddleware {
public func create(model: OD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
let order = O(
typeIdentifier: OD.typeIdentifier,
public func create(model: OrderDataType, on db: any Database, next: any AnyAsyncModelResponder) async throws {
let order = OrderType(
typeIdentifier: OrderDataType.typeIdentifier,
authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()
)
try await order.save(on: db)
model._$order.id = try order.requireID()
try await next.create(model, on: db)
}

public func update(model: OD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
public func update(model: OrderDataType, on db: any Database, next: any AnyAsyncModelResponder) async throws {
let order = try await model._$order.get(on: db)
order.updatedAt = Date.now
try await order.save(on: db)
Expand Down
20 changes: 10 additions & 10 deletions Sources/VaporWalletOrders/OrdersService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,20 @@ import FluentWalletOrders
import Vapor

/// The main class that handles Wallet orders.
public final class OrdersService<OD: OrderDataModel>: Sendable where Order == OD.OrderType {
private let service: OrdersServiceCustom<OD, Order, OrdersDevice, OrdersRegistration>
public final class OrdersService<OrderDataType: OrderDataModel>: Sendable where Order == OrderDataType.OrderType {
private let service: OrdersServiceCustom<OrderDataType, Order, OrdersDevice, OrdersRegistration>

/// Initializes the service and registers all the routes required for Apple Wallet to work.
///
/// - Parameters:
/// - app: The `Vapor.Application` to use in route handlers and APNs.
/// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered.
/// - logger: The `Logger` to use.
/// - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format.
/// - pemCertificate: The PEM Certificate for signing orders.
/// - pemPrivateKey: The PEM Certificate's private key for signing orders.
/// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`.
/// - openSSLPath: The location of the `openssl` command as a file path.
public init(
app: Application,
pushRoutesMiddleware: (any Middleware)? = nil,
logger: Logger? = nil,
pemWWDRCertificate: String,
pemCertificate: String,
pemPrivateKey: String,
Expand All @@ -29,8 +25,6 @@ public final class OrdersService<OD: OrderDataModel>: Sendable where Order == OD
) throws {
self.service = try .init(
app: app,
pushRoutesMiddleware: pushRoutesMiddleware,
logger: logger,
pemWWDRCertificate: pemWWDRCertificate,
pemCertificate: pemCertificate,
pemPrivateKey: pemPrivateKey,
Expand All @@ -46,7 +40,7 @@ public final class OrdersService<OD: OrderDataModel>: Sendable where Order == OD
/// - db: The `Database` to use.
///
/// - Returns: The generated order content.
public func build(order: OD, on db: any Database) async throws -> Data {
public func build(order: OrderDataType, on db: any Database) async throws -> Data {
try await service.build(order: order, on: db)
}

Expand All @@ -64,7 +58,13 @@ public final class OrdersService<OD: OrderDataModel>: Sendable where Order == OD
/// - Parameters:
/// - order: The order to send the notifications for.
/// - db: The `Database` to use.
public func sendPushNotifications(for order: OD, on db: any Database) async throws {
public func sendPushNotifications(for order: OrderDataType, on db: any Database) async throws {
try await service.sendPushNotifications(for: order, on: db)
}
}

extension OrdersService: RouteCollection {
public func boot(routes: any RoutesBuilder) throws {
try service.boot(routes: routes)
}
}
182 changes: 182 additions & 0 deletions Sources/VaporWalletOrders/OrdersServiceCustom+RouteCollection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import Fluent
import FluentWalletOrders
import Vapor
import VaporWallet

extension OrdersServiceCustom: RouteCollection {
public func boot(routes: any RoutesBuilder) throws {
let orderTypeIdentifier = PathComponent(stringLiteral: OrderDataType.typeIdentifier)

let v1 = routes.grouped("v1")
v1.get("devices", ":deviceIdentifier", "registrations", orderTypeIdentifier, use: self.ordersForDevice)
v1.post("log", use: self.logMessage)

let v1auth = v1.grouped(AppleOrderMiddleware<OrderType>())
v1auth.post("devices", ":deviceIdentifier", "registrations", orderTypeIdentifier, ":orderIdentifier", use: self.registerDevice)
v1auth.get("orders", orderTypeIdentifier, ":orderIdentifier", use: self.latestVersionOfOrder)
v1auth.delete("devices", ":deviceIdentifier", "registrations", orderTypeIdentifier, ":orderIdentifier", use: self.unregisterDevice)
}

private func latestVersionOfOrder(req: Request) async throws -> Response {
req.logger.debug("Called latestVersionOfOrder")

var ifModifiedSince: TimeInterval = 0
if let header = req.headers[.ifModifiedSince].first, let ims = TimeInterval(header) {
ifModifiedSince = ims
}

guard let id = req.parameters.get("orderIdentifier", as: UUID.self) else {
throw Abort(.badRequest)
}
guard
let order = try await OrderType.query(on: req.db)
.filter(\._$id == id)
.filter(\._$typeIdentifier == OrderDataType.typeIdentifier)
.first()
else {
throw Abort(.notFound)
}

guard ifModifiedSince < order.updatedAt?.timeIntervalSince1970 ?? 0 else {
throw Abort(.notModified)
}

guard
let orderData = try await OrderDataType.query(on: req.db)
.filter(\._$order.$id == id)
.first()
else {
throw Abort(.notFound)
}

var headers = HTTPHeaders()
headers.add(name: .contentType, value: "application/vnd.apple.order")
headers.lastModified = HTTPHeaders.LastModified(order.updatedAt ?? Date.distantPast)
headers.add(name: .contentTransferEncoding, value: "binary")
return try await Response(
status: .ok,
headers: headers,
body: Response.Body(data: self.build(order: orderData, on: req.db))
)
}

private func registerDevice(req: Request) async throws -> HTTPStatus {
req.logger.debug("Called register device")

let pushToken: String
do {
pushToken = try req.content.decode(PushTokenDTO.self).pushToken
} catch {
throw Abort(.badRequest)
}

guard let orderIdentifier = req.parameters.get("orderIdentifier", as: UUID.self) else {
throw Abort(.badRequest)
}
let deviceIdentifier = req.parameters.get("deviceIdentifier")!
guard
let order = try await OrderType.query(on: req.db)
.filter(\._$id == orderIdentifier)
.filter(\._$typeIdentifier == OrderDataType.typeIdentifier)
.first()
else {
throw Abort(.notFound)
}

let device = try await DeviceType.query(on: req.db)
.filter(\._$libraryIdentifier == deviceIdentifier)
.filter(\._$pushToken == pushToken)
.first()
if let device = device {
return try await Self.createRegistration(device: device, order: order, db: req.db)
} else {
let newDevice = DeviceType(libraryIdentifier: deviceIdentifier, pushToken: pushToken)
try await newDevice.create(on: req.db)
return try await Self.createRegistration(device: newDevice, order: order, db: req.db)
}
}

private static func createRegistration(device: DeviceType, order: OrderType, db: any Database) async throws -> HTTPStatus {
let r = try await OrdersRegistrationType.for(
deviceLibraryIdentifier: device.libraryIdentifier,
typeIdentifier: order.typeIdentifier,
on: db
)
.filter(OrderType.self, \._$id == order.requireID())
.first()
// If the registration already exists, docs say to return 200 OK
if r != nil { return .ok }

let registration = OrdersRegistrationType()
registration._$order.id = try order.requireID()
registration._$device.id = try device.requireID()
try await registration.create(on: db)
return .created
}

private func ordersForDevice(req: Request) async throws -> OrderIdentifiersDTO {
req.logger.debug("Called ordersForDevice")

let deviceIdentifier = req.parameters.get("deviceIdentifier")!

var query = OrdersRegistrationType.for(
deviceLibraryIdentifier: deviceIdentifier,
typeIdentifier: OrderDataType.typeIdentifier,
on: req.db
)
if let since: TimeInterval = req.query["ordersModifiedSince"] {
let when = Date(timeIntervalSince1970: since)
query = query.filter(OrderType.self, \._$updatedAt > when)
}

let registrations = try await query.all()
guard !registrations.isEmpty else {
throw Abort(.noContent)
}

var orderIdentifiers: [String] = []
var maxDate = Date.distantPast
for registration in registrations {
let order = try await registration._$order.get(on: req.db)
try orderIdentifiers.append(order.requireID().uuidString)
if let updatedAt = order.updatedAt, updatedAt > maxDate {
maxDate = updatedAt
}
}

return OrderIdentifiersDTO(with: orderIdentifiers, maxDate: maxDate)
}

private func logMessage(req: Request) async throws -> HTTPStatus {
let entries = try req.content.decode(LogEntriesDTO.self)

for log in entries.logs {
req.logger.notice("VaporWalletOrders: \(log)")
}

return .ok
}

private func unregisterDevice(req: Request) async throws -> HTTPStatus {
req.logger.debug("Called unregisterDevice")

guard let orderIdentifier = req.parameters.get("orderIdentifier", as: UUID.self) else {
throw Abort(.badRequest)
}
let deviceIdentifier = req.parameters.get("deviceIdentifier")!

guard
let r = try await OrdersRegistrationType.for(
deviceLibraryIdentifier: deviceIdentifier,
typeIdentifier: OrderDataType.typeIdentifier,
on: req.db
)
.filter(OrderType.self, \._$id == orderIdentifier)
.first()
else {
throw Abort(.notFound)
}
try await r.delete(on: req.db)
return .ok
}
}
Loading

0 comments on commit 64b2f36

Please sign in to comment.