Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve routing API #20

Merged
merged 4 commits into from
Jan 17, 2025
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
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
Loading