Skip to content

Commit 64b2f36

Browse files
authored
Improve routing API (#20)
* Improve routing API * Rename generic types * Update README for v0.7.0 * Update SPI links
1 parent 900653c commit 64b2f36

File tree

21 files changed

+518
-855
lines changed

21 files changed

+518
-855
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
Use the SPM string to easily include the dependendency in your `Package.swift` file.
2424

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

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

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

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

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

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

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

Sources/VaporWallet/Testing/SecretMiddleware.swift

Lines changed: 0 additions & 16 deletions
This file was deleted.

Sources/VaporWallet/VaporWallet.docc/VaporWallet.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The `VaporWallet` framework provides a set of tools shared by the `VaporWalletPa
1919
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.
2020
It also provides a way to update passes after they have been distributed, using APNs, and models to store pass and device data.
2121

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

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

@@ -28,6 +28,6 @@ For information on Apple Wallet passes, see the [Apple Developer Documentation](
2828
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.
2929
It also provides a way to update orders after they have been distributed, using APNs, and models to store order and device data.
3030

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

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

Sources/VaporWalletOrders/Middleware/AppleOrderMiddleware.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import FluentKit
22
import FluentWalletOrders
33
import Vapor
44

5-
struct AppleOrderMiddleware<O: OrderModel>: AsyncMiddleware {
5+
struct AppleOrderMiddleware<OrderType: OrderModel>: AsyncMiddleware {
66
func respond(
77
to request: Request, chainingTo next: any AsyncResponder
88
) async throws -> Response {
99
guard
1010
let id = request.parameters.get("orderIdentifier", as: UUID.self),
1111
let authToken = request.headers["Authorization"].first?.replacingOccurrences(of: "AppleOrder ", with: ""),
12-
(try await O.query(on: request.db)
12+
(try await OrderType.query(on: request.db)
1313
.filter(\._$id == id)
1414
.filter(\._$authenticationToken == authToken)
1515
.first()) != nil

Sources/VaporWalletOrders/Middleware/OrdersService+AsyncModelMiddleware.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ import FluentWalletOrders
33
import Foundation
44

55
extension OrdersService: AsyncModelMiddleware {
6-
public func create(model: OD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
6+
public func create(model: OrderDataType, on db: any Database, next: any AnyAsyncModelResponder) async throws {
77
let order = Order(
8-
typeIdentifier: OD.typeIdentifier,
8+
typeIdentifier: OrderDataType.typeIdentifier,
99
authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()
1010
)
1111
try await order.save(on: db)
1212
model._$order.id = try order.requireID()
1313
try await next.create(model, on: db)
1414
}
1515

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

2525
extension OrdersServiceCustom: AsyncModelMiddleware {
26-
public func create(model: OD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
27-
let order = O(
28-
typeIdentifier: OD.typeIdentifier,
26+
public func create(model: OrderDataType, on db: any Database, next: any AnyAsyncModelResponder) async throws {
27+
let order = OrderType(
28+
typeIdentifier: OrderDataType.typeIdentifier,
2929
authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()
3030
)
3131
try await order.save(on: db)
3232
model._$order.id = try order.requireID()
3333
try await next.create(model, on: db)
3434
}
3535

36-
public func update(model: OD, on db: any Database, next: any AnyAsyncModelResponder) async throws {
36+
public func update(model: OrderDataType, on db: any Database, next: any AnyAsyncModelResponder) async throws {
3737
let order = try await model._$order.get(on: db)
3838
order.updatedAt = Date.now
3939
try await order.save(on: db)

Sources/VaporWalletOrders/OrdersService.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,20 @@ import FluentWalletOrders
33
import Vapor
44

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

99
/// Initializes the service and registers all the routes required for Apple Wallet to work.
1010
///
1111
/// - Parameters:
1212
/// - app: The `Vapor.Application` to use in route handlers and APNs.
13-
/// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered.
14-
/// - logger: The `Logger` to use.
1513
/// - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format.
1614
/// - pemCertificate: The PEM Certificate for signing orders.
1715
/// - pemPrivateKey: The PEM Certificate's private key for signing orders.
1816
/// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`.
1917
/// - openSSLPath: The location of the `openssl` command as a file path.
2018
public init(
2119
app: Application,
22-
pushRoutesMiddleware: (any Middleware)? = nil,
23-
logger: Logger? = nil,
2420
pemWWDRCertificate: String,
2521
pemCertificate: String,
2622
pemPrivateKey: String,
@@ -29,8 +25,6 @@ public final class OrdersService<OD: OrderDataModel>: Sendable where Order == OD
2925
) throws {
3026
self.service = try .init(
3127
app: app,
32-
pushRoutesMiddleware: pushRoutesMiddleware,
33-
logger: logger,
3428
pemWWDRCertificate: pemWWDRCertificate,
3529
pemCertificate: pemCertificate,
3630
pemPrivateKey: pemPrivateKey,
@@ -46,7 +40,7 @@ public final class OrdersService<OD: OrderDataModel>: Sendable where Order == OD
4640
/// - db: The `Database` to use.
4741
///
4842
/// - Returns: The generated order content.
49-
public func build(order: OD, on db: any Database) async throws -> Data {
43+
public func build(order: OrderDataType, on db: any Database) async throws -> Data {
5044
try await service.build(order: order, on: db)
5145
}
5246

@@ -64,7 +58,13 @@ public final class OrdersService<OD: OrderDataModel>: Sendable where Order == OD
6458
/// - Parameters:
6559
/// - order: The order to send the notifications for.
6660
/// - db: The `Database` to use.
67-
public func sendPushNotifications(for order: OD, on db: any Database) async throws {
61+
public func sendPushNotifications(for order: OrderDataType, on db: any Database) async throws {
6862
try await service.sendPushNotifications(for: order, on: db)
6963
}
7064
}
65+
66+
extension OrdersService: RouteCollection {
67+
public func boot(routes: any RoutesBuilder) throws {
68+
try service.boot(routes: routes)
69+
}
70+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import Fluent
2+
import FluentWalletOrders
3+
import Vapor
4+
import VaporWallet
5+
6+
extension OrdersServiceCustom: RouteCollection {
7+
public func boot(routes: any RoutesBuilder) throws {
8+
let orderTypeIdentifier = PathComponent(stringLiteral: OrderDataType.typeIdentifier)
9+
10+
let v1 = routes.grouped("v1")
11+
v1.get("devices", ":deviceIdentifier", "registrations", orderTypeIdentifier, use: self.ordersForDevice)
12+
v1.post("log", use: self.logMessage)
13+
14+
let v1auth = v1.grouped(AppleOrderMiddleware<OrderType>())
15+
v1auth.post("devices", ":deviceIdentifier", "registrations", orderTypeIdentifier, ":orderIdentifier", use: self.registerDevice)
16+
v1auth.get("orders", orderTypeIdentifier, ":orderIdentifier", use: self.latestVersionOfOrder)
17+
v1auth.delete("devices", ":deviceIdentifier", "registrations", orderTypeIdentifier, ":orderIdentifier", use: self.unregisterDevice)
18+
}
19+
20+
private func latestVersionOfOrder(req: Request) async throws -> Response {
21+
req.logger.debug("Called latestVersionOfOrder")
22+
23+
var ifModifiedSince: TimeInterval = 0
24+
if let header = req.headers[.ifModifiedSince].first, let ims = TimeInterval(header) {
25+
ifModifiedSince = ims
26+
}
27+
28+
guard let id = req.parameters.get("orderIdentifier", as: UUID.self) else {
29+
throw Abort(.badRequest)
30+
}
31+
guard
32+
let order = try await OrderType.query(on: req.db)
33+
.filter(\._$id == id)
34+
.filter(\._$typeIdentifier == OrderDataType.typeIdentifier)
35+
.first()
36+
else {
37+
throw Abort(.notFound)
38+
}
39+
40+
guard ifModifiedSince < order.updatedAt?.timeIntervalSince1970 ?? 0 else {
41+
throw Abort(.notModified)
42+
}
43+
44+
guard
45+
let orderData = try await OrderDataType.query(on: req.db)
46+
.filter(\._$order.$id == id)
47+
.first()
48+
else {
49+
throw Abort(.notFound)
50+
}
51+
52+
var headers = HTTPHeaders()
53+
headers.add(name: .contentType, value: "application/vnd.apple.order")
54+
headers.lastModified = HTTPHeaders.LastModified(order.updatedAt ?? Date.distantPast)
55+
headers.add(name: .contentTransferEncoding, value: "binary")
56+
return try await Response(
57+
status: .ok,
58+
headers: headers,
59+
body: Response.Body(data: self.build(order: orderData, on: req.db))
60+
)
61+
}
62+
63+
private func registerDevice(req: Request) async throws -> HTTPStatus {
64+
req.logger.debug("Called register device")
65+
66+
let pushToken: String
67+
do {
68+
pushToken = try req.content.decode(PushTokenDTO.self).pushToken
69+
} catch {
70+
throw Abort(.badRequest)
71+
}
72+
73+
guard let orderIdentifier = req.parameters.get("orderIdentifier", as: UUID.self) else {
74+
throw Abort(.badRequest)
75+
}
76+
let deviceIdentifier = req.parameters.get("deviceIdentifier")!
77+
guard
78+
let order = try await OrderType.query(on: req.db)
79+
.filter(\._$id == orderIdentifier)
80+
.filter(\._$typeIdentifier == OrderDataType.typeIdentifier)
81+
.first()
82+
else {
83+
throw Abort(.notFound)
84+
}
85+
86+
let device = try await DeviceType.query(on: req.db)
87+
.filter(\._$libraryIdentifier == deviceIdentifier)
88+
.filter(\._$pushToken == pushToken)
89+
.first()
90+
if let device = device {
91+
return try await Self.createRegistration(device: device, order: order, db: req.db)
92+
} else {
93+
let newDevice = DeviceType(libraryIdentifier: deviceIdentifier, pushToken: pushToken)
94+
try await newDevice.create(on: req.db)
95+
return try await Self.createRegistration(device: newDevice, order: order, db: req.db)
96+
}
97+
}
98+
99+
private static func createRegistration(device: DeviceType, order: OrderType, db: any Database) async throws -> HTTPStatus {
100+
let r = try await OrdersRegistrationType.for(
101+
deviceLibraryIdentifier: device.libraryIdentifier,
102+
typeIdentifier: order.typeIdentifier,
103+
on: db
104+
)
105+
.filter(OrderType.self, \._$id == order.requireID())
106+
.first()
107+
// If the registration already exists, docs say to return 200 OK
108+
if r != nil { return .ok }
109+
110+
let registration = OrdersRegistrationType()
111+
registration._$order.id = try order.requireID()
112+
registration._$device.id = try device.requireID()
113+
try await registration.create(on: db)
114+
return .created
115+
}
116+
117+
private func ordersForDevice(req: Request) async throws -> OrderIdentifiersDTO {
118+
req.logger.debug("Called ordersForDevice")
119+
120+
let deviceIdentifier = req.parameters.get("deviceIdentifier")!
121+
122+
var query = OrdersRegistrationType.for(
123+
deviceLibraryIdentifier: deviceIdentifier,
124+
typeIdentifier: OrderDataType.typeIdentifier,
125+
on: req.db
126+
)
127+
if let since: TimeInterval = req.query["ordersModifiedSince"] {
128+
let when = Date(timeIntervalSince1970: since)
129+
query = query.filter(OrderType.self, \._$updatedAt > when)
130+
}
131+
132+
let registrations = try await query.all()
133+
guard !registrations.isEmpty else {
134+
throw Abort(.noContent)
135+
}
136+
137+
var orderIdentifiers: [String] = []
138+
var maxDate = Date.distantPast
139+
for registration in registrations {
140+
let order = try await registration._$order.get(on: req.db)
141+
try orderIdentifiers.append(order.requireID().uuidString)
142+
if let updatedAt = order.updatedAt, updatedAt > maxDate {
143+
maxDate = updatedAt
144+
}
145+
}
146+
147+
return OrderIdentifiersDTO(with: orderIdentifiers, maxDate: maxDate)
148+
}
149+
150+
private func logMessage(req: Request) async throws -> HTTPStatus {
151+
let entries = try req.content.decode(LogEntriesDTO.self)
152+
153+
for log in entries.logs {
154+
req.logger.notice("VaporWalletOrders: \(log)")
155+
}
156+
157+
return .ok
158+
}
159+
160+
private func unregisterDevice(req: Request) async throws -> HTTPStatus {
161+
req.logger.debug("Called unregisterDevice")
162+
163+
guard let orderIdentifier = req.parameters.get("orderIdentifier", as: UUID.self) else {
164+
throw Abort(.badRequest)
165+
}
166+
let deviceIdentifier = req.parameters.get("deviceIdentifier")!
167+
168+
guard
169+
let r = try await OrdersRegistrationType.for(
170+
deviceLibraryIdentifier: deviceIdentifier,
171+
typeIdentifier: OrderDataType.typeIdentifier,
172+
on: req.db
173+
)
174+
.filter(OrderType.self, \._$id == orderIdentifier)
175+
.first()
176+
else {
177+
throw Abort(.notFound)
178+
}
179+
try await r.delete(on: req.db)
180+
return .ok
181+
}
182+
}

0 commit comments

Comments
 (0)