From 75735b9894ae3f1218f46480df710dd15a8f8937 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 30 Jun 2024 23:28:38 +0200 Subject: [PATCH] Start work on the Orders target --- Package.swift | 8 +++ Sources/Orders/DTOs/OrdersForDeviceDTO.swift | 18 ++++++ .../Middleware/AppleOrderMiddleware.swift | 22 +++++++ .../Orders/Models/Concrete Models/Order.swift | 57 +++++++++++++++++ .../Models/Concrete Models/OrdersDevice.swift | 53 ++++++++++++++++ .../Concrete Models/OrdersErrorLog.swift | 52 ++++++++++++++++ .../Concrete Models/OrdersRegistration.swift | 49 +++++++++++++++ Sources/Orders/Models/OrderDataModel.swift | 27 ++++++++ Sources/Orders/Models/OrderModel.swift | 61 +++++++++++++++++++ .../Models/OrdersRegistrationModel.swift | 51 ++++++++++++++++ Sources/Orders/OrdersError.swift | 26 ++++++++ 11 files changed, 424 insertions(+) create mode 100644 Sources/Orders/DTOs/OrdersForDeviceDTO.swift create mode 100644 Sources/Orders/Middleware/AppleOrderMiddleware.swift create mode 100644 Sources/Orders/Models/Concrete Models/Order.swift create mode 100644 Sources/Orders/Models/Concrete Models/OrdersDevice.swift create mode 100644 Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift create mode 100644 Sources/Orders/Models/Concrete Models/OrdersRegistration.swift create mode 100644 Sources/Orders/Models/OrderDataModel.swift create mode 100644 Sources/Orders/Models/OrderModel.swift create mode 100644 Sources/Orders/Models/OrdersRegistrationModel.swift create mode 100644 Sources/Orders/OrdersError.swift diff --git a/Package.swift b/Package.swift index 34c0659..d1e8051 100644 --- a/Package.swift +++ b/Package.swift @@ -32,11 +32,19 @@ let package = Package( ], swiftSettings: swiftSettings ), + .target( + name: "Orders", + dependencies: [ + .target(name: "PassKit"), + ], + swiftSettings: swiftSettings + ), .testTarget( name: "PassKitTests", dependencies: [ .target(name: "PassKit"), .target(name: "Passes"), + .target(name: "Orders"), .product(name: "XCTVapor", package: "vapor"), ], swiftSettings: swiftSettings diff --git a/Sources/Orders/DTOs/OrdersForDeviceDTO.swift b/Sources/Orders/DTOs/OrdersForDeviceDTO.swift new file mode 100644 index 0000000..921da6b --- /dev/null +++ b/Sources/Orders/DTOs/OrdersForDeviceDTO.swift @@ -0,0 +1,18 @@ +// +// OrdersForDeviceDTO.swift +// PassKit +// +// Created by Francesco Paolo Severino on 30/06/24. +// + +import Vapor + +struct OrdersForDeviceDTO: Content { + let orderIdentifiers: [String] + let lastModified: String + + init(with orderIdentifiers: [String], maxDate: Date) { + self.orderIdentifiers = orderIdentifiers + lastModified = String(maxDate.timeIntervalSince1970) + } +} \ No newline at end of file diff --git a/Sources/Orders/Middleware/AppleOrderMiddleware.swift b/Sources/Orders/Middleware/AppleOrderMiddleware.swift new file mode 100644 index 0000000..517fe9d --- /dev/null +++ b/Sources/Orders/Middleware/AppleOrderMiddleware.swift @@ -0,0 +1,22 @@ +// +// AppleOrderMiddleware.swift +// PassKit +// +// Created by Francesco Paolo Severino on 30/06/24. +// + +import Vapor +import FluentKit + +struct AppleOrderMiddleware: AsyncMiddleware { + func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response { + guard let auth = request.headers["Authorization"].first?.replacingOccurrences(of: "AppleOrder ", with: ""), + let _ = try await O.query(on: request.db) + .filter(\._$authenticationToken == auth) + .first() + else { + throw Abort(.unauthorized) + } + return try await next.respond(to: request) + } +} \ No newline at end of file diff --git a/Sources/Orders/Models/Concrete Models/Order.swift b/Sources/Orders/Models/Concrete Models/Order.swift new file mode 100644 index 0000000..d625205 --- /dev/null +++ b/Sources/Orders/Models/Concrete Models/Order.swift @@ -0,0 +1,57 @@ +// +// Order.swift +// PassKit +// +// Created by Francesco Paolo Severino on 30/06/24. +// + +import Foundation +import FluentKit + +/// The `Model` that stores Wallet orders. +open class Order: OrderModel, @unchecked Sendable { + public static let schema = Order.FieldKeys.schemaName + + @ID + public var id: UUID? + + @Timestamp(key: Order.FieldKeys.updatedAt, on: .update) + public var updatedAt: Date? + + @Field(key: Order.FieldKeys.orderTypeIdentifier) + public var orderTypeIdentifier: String + + @Field(key: Order.FieldKeys.authenticationToken) + public var authenticationToken: String + + public required init() { } + + public required init(orderTypeIdentifier: String, authenticationToken: String) { + self.orderTypeIdentifier = orderTypeIdentifier + self.authenticationToken = authenticationToken + } +} + +extension Order: AsyncMigration { + public func prepare(on database: any Database) async throws { + try await database.schema(Self.schema) + .id() + .field(Order.FieldKeys.updatedAt, .datetime, .required) + .field(Order.FieldKeys.orderTypeIdentifier, .string, .required) + .field(Order.FieldKeys.authenticationToken, .string, .required) + .create() + } + + public func revert(on database: any Database) async throws { + try await database.schema(Self.schema).delete() + } +} + +extension Order { + enum FieldKeys { + static let schemaName = "orders" + static let updatedAt = FieldKey(stringLiteral: "updated_at") + static let orderTypeIdentifier = FieldKey(stringLiteral: "order_type_identifier") + static let authenticationToken = FieldKey(stringLiteral: "authentication_token") + } +} \ No newline at end of file diff --git a/Sources/Orders/Models/Concrete Models/OrdersDevice.swift b/Sources/Orders/Models/Concrete Models/OrdersDevice.swift new file mode 100644 index 0000000..b2519ea --- /dev/null +++ b/Sources/Orders/Models/Concrete Models/OrdersDevice.swift @@ -0,0 +1,53 @@ +// +// OrdersDevice.swift +// PassKit +// +// Created by Francesco Paolo Severino on 30/06/24. +// + +import FluentKit +import PassKit + +/// The `Model` that stores Wallet orders devices. +final public class OrdersDevice: DeviceModel, @unchecked Sendable { + public static let schema = OrdersDevice.FieldKeys.schemaName + + @ID(custom: .id) + public var id: Int? + + @Field(key: OrdersDevice.FieldKeys.pushToken) + public var pushToken: String + + @Field(key: OrdersDevice.FieldKeys.deviceLibraryIdentifier) + public var deviceLibraryIdentifier: String + + public init(deviceLibraryIdentifier: String, pushToken: String) { + self.deviceLibraryIdentifier = deviceLibraryIdentifier + self.pushToken = pushToken + } + + public init() {} +} + +extension OrdersDevice: AsyncMigration { + public func prepare(on database: any Database) async throws { + try await database.schema(Self.schema) + .field(.id, .int, .identifier(auto: true)) + .field(OrdersDevice.FieldKeys.pushToken, .string, .required) + .field(OrdersDevice.FieldKeys.deviceLibraryIdentifier, .string, .required) + .unique(on: OrdersDevice.FieldKeys.pushToken, OrdersDevice.FieldKeys.deviceLibraryIdentifier) + .create() + } + + public func revert(on database: any Database) async throws { + try await database.schema(Self.schema).delete() + } +} + +extension OrdersDevice { + enum FieldKeys { + static let schemaName = "orders_devices" + static let pushToken = FieldKey(stringLiteral: "push_token") + static let deviceLibraryIdentifier = FieldKey(stringLiteral: "device_library_identifier") + } +} \ No newline at end of file diff --git a/Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift b/Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift new file mode 100644 index 0000000..173e340 --- /dev/null +++ b/Sources/Orders/Models/Concrete Models/OrdersErrorLog.swift @@ -0,0 +1,52 @@ +// +// OrdersErrorLog.swift +// PassKit +// +// Created by Francesco Paolo Severino on 30/06/24. +// + +import struct Foundation.Date +import FluentKit +import PassKit + +/// The `Model` that stores Wallet orders error logs. +final public class OrdersErrorLog: ErrorLogModel, @unchecked Sendable { + public static let schema = OrdersErrorLog.FieldKeys.schemaName + + @ID(custom: .id) + public var id: Int? + + @Timestamp(key: OrdersErrorLog.FieldKeys.createdAt, on: .create) + public var createdAt: Date? + + @Field(key: OrdersErrorLog.FieldKeys.message) + public var message: String + + public init(message: String) { + self.message = message + } + + public init() {} +} + +extension OrdersErrorLog: AsyncMigration { + public func prepare(on database: any Database) async throws { + try await database.schema(Self.schema) + .field(.id, .int, .identifier(auto: true)) + .field(OrdersErrorLog.FieldKeys.createdAt, .datetime, .required) + .field(OrdersErrorLog.FieldKeys.message, .string, .required) + .create() + } + + public func revert(on database: any Database) async throws { + try await database.schema(Self.schema).delete() + } +} + +extension OrdersErrorLog { + enum FieldKeys { + static let schemaName = "orders_errors" + static let createdAt = FieldKey(stringLiteral: "created_at") + static let message = FieldKey(stringLiteral: "message") + } +} \ No newline at end of file diff --git a/Sources/Orders/Models/Concrete Models/OrdersRegistration.swift b/Sources/Orders/Models/Concrete Models/OrdersRegistration.swift new file mode 100644 index 0000000..6acae38 --- /dev/null +++ b/Sources/Orders/Models/Concrete Models/OrdersRegistration.swift @@ -0,0 +1,49 @@ +// +// OrdersRegistration.swift +// PassKit +// +// Created by Francesco Paolo Severino on 30/06/24. +// + +import FluentKit + +/// The `Model` that stores orders registrations. +final public class OrdersRegistration: OrdersRegistrationModel, @unchecked Sendable { + public typealias OrderType = Order + public typealias DeviceType = OrdersDevice + + public static let schema = OrdersRegistration.FieldKeys.schemaName + + @ID(custom: .id) + public var id: Int? + + @Parent(key: OrdersRegistration.FieldKeys.deviceID) + public var device: DeviceType + + @Parent(key: OrdersRegistration.FieldKeys.orderID) + public var order: OrderType + + public init() {} +} + +extension OrdersRegistration: AsyncMigration { + public func prepare(on database: any Database) async throws { + try await database.schema(Self.schema) + .field(.id, .int, .identifier(auto: true)) + .field(OrdersRegistration.FieldKeys.deviceID, .int, .required, .references(DeviceType.schema, .id, onDelete: .cascade)) + .field(OrdersRegistration.FieldKeys.orderID, .uuid, .required, .references(OrderType.schema, .id, onDelete: .cascade)) + .create() + } + + public func revert(on database: any Database) async throws { + try await database.schema(Self.schema).delete() + } +} + +extension OrdersRegistration { + enum FieldKeys { + static let schemaName = "orders_registrations" + static let deviceID = FieldKey(stringLiteral: "device_id") + static let orderID = FieldKey(stringLiteral: "order_id") + } +} \ No newline at end of file diff --git a/Sources/Orders/Models/OrderDataModel.swift b/Sources/Orders/Models/OrderDataModel.swift new file mode 100644 index 0000000..55f3030 --- /dev/null +++ b/Sources/Orders/Models/OrderDataModel.swift @@ -0,0 +1,27 @@ +// +// OrderDataModel.swift +// PassKit +// +// Created by Francesco Paolo Severino on 30/06/24. +// + +import FluentKit + +/// Represents the `Model` that stores custom app data associated to Wallet orders. +public protocol OrderDataModel: Model { + associatedtype OrderType: OrderModel + + /// The foreign key to the order table + var order: OrderType { get set } +} + +internal extension OrderDataModel { + var _$order: Parent { + guard let mirror = Mirror(reflecting: self).descendant("_order"), + let order = mirror as? Parent else { + fatalError("order property must be declared using @Parent") + } + + return order + } +} \ No newline at end of file diff --git a/Sources/Orders/Models/OrderModel.swift b/Sources/Orders/Models/OrderModel.swift new file mode 100644 index 0000000..09f3221 --- /dev/null +++ b/Sources/Orders/Models/OrderModel.swift @@ -0,0 +1,61 @@ +// +// OrderModel.swift +// PassKit +// +// Created by Francesco Paolo Severino on 30/06/24. +// + +import Foundation +import FluentKit + +/// Represents the `Model` that stores Waller orders. +/// +/// Uses a UUID so people can't easily guess order IDs +public protocol OrderModel: Model where IDValue == UUID { + /// The order type identifier. + var orderTypeIdentifier: String { get set } + + /// The last time the order was modified. + var updatedAt: Date? { get set } + + /// The authentication token for the order. + var authenticationToken: String { get set } +} + +internal extension OrderModel { + var _$id: ID { + guard let mirror = Mirror(reflecting: self).descendant("_id"), + let id = mirror as? ID else { + fatalError("id property must be declared using @ID") + } + + return id + } + + var _$orderTypeIdentifier: Field { + guard let mirror = Mirror(reflecting: self).descendant("_orderTypeIdentifier"), + let orderTypeIdentifier = mirror as? Field else { + fatalError("orderTypeIdentifier property must be declared using @Field") + } + + return orderTypeIdentifier + } + + var _$updatedAt: Timestamp { + guard let mirror = Mirror(reflecting: self).descendant("_updatedAt"), + let updatedAt = mirror as? Timestamp else { + fatalError("updatedAt property must be declared using @Timestamp(on: .update)") + } + + return updatedAt + } + + var _$authenticationToken: Field { + guard let mirror = Mirror(reflecting: self).descendant("_authenticationToken"), + let authenticationToken = mirror as? Field else { + fatalError("authenticationToken property must be declared using @Field") + } + + return authenticationToken + } +} \ No newline at end of file diff --git a/Sources/Orders/Models/OrdersRegistrationModel.swift b/Sources/Orders/Models/OrdersRegistrationModel.swift new file mode 100644 index 0000000..ffcc912 --- /dev/null +++ b/Sources/Orders/Models/OrdersRegistrationModel.swift @@ -0,0 +1,51 @@ +// +// OrdersRegistrationModel.swift +// PassKit +// +// Created by Francesco Paolo Severino on 30/06/24. +// + +import FluentKit +import PassKit + +/// Represents the `Model` that stores orders registrations. +public protocol OrdersRegistrationModel: Model where IDValue == Int { + associatedtype OrderType: OrderModel + associatedtype DeviceType: DeviceModel + + /// The device for this registration. + var device: DeviceType { get set } + + /// The order for this registration. + var order: OrderType { get set } +} + +internal extension OrdersRegistrationModel { + var _$device: Parent { + guard let mirror = Mirror(reflecting: self).descendant("_device"), + let device = mirror as? Parent else { + fatalError("device property must be declared using @Parent") + } + + return device + } + + var _$order: Parent { + guard let mirror = Mirror(reflecting: self).descendant("_order"), + let order = mirror as? Parent else { + fatalError("order property must be declared using @Parent") + } + + return order + } + + static func `for`(deviceLibraryIdentifier: String, orderTypeIdentifier: String, on db: any Database) -> QueryBuilder { + Self.query(on: db) + .join(parent: \._$order) + .join(parent: \._$device) + .with(\._$order) + .with(\._$device) + .filter(OrderType.self, \._$orderTypeIdentifier == orderTypeIdentifier) + .filter(DeviceType.self, \._$deviceLibraryIdentifier == deviceLibraryIdentifier) + } +} \ No newline at end of file diff --git a/Sources/Orders/OrdersError.swift b/Sources/Orders/OrdersError.swift new file mode 100644 index 0000000..223dc75 --- /dev/null +++ b/Sources/Orders/OrdersError.swift @@ -0,0 +1,26 @@ +// +// OrdersError.swift +// PassKit +// +// Created by Francesco Paolo Severino on 30/06/24. +// + +public enum OrdersError: Error { + /// The template path is not a directory + case templateNotDirectory + + /// The `pemCertificate` file is missing. + case pemCertificateMissing + + /// The `pemPrivateKey` file is missing. + case pemPrivateKeyMissing + + /// Swift NIO failed to read the key. + case nioPrivateKeyReadFailed(any Error) + + /// The path to the zip binary is incorrect. + case zipBinaryMissing + + /// The path to the openssl binary is incorrect + case opensslBinaryMissing +} \ No newline at end of file