Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
ueman committed Aug 31, 2024
1 parent 78c2b77 commit 68a784d
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 41 deletions.
25 changes: 24 additions & 1 deletion passkit_server/lib/src/passkit_backend.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:passkit/passkit.dart';

abstract class PassKitBackend {
Expand All @@ -23,8 +25,24 @@ abstract class PassKitBackend {
Future<PkPass?> getUpdatedPass(
String identifier,
String serial,
String authenticationToken,
);

Future<NotificationRegistrationReponse> setupNotifications(
String deviceId,
String passTypeId,
String serialNumber,
String pushToken,
);

Future<bool> stopNotifications(
String deviceId,
String passTypeId,
String serialNumber,
);

/// Should return true if the [serial] and [authToken] match and are valid.
/// Otherwise it should return false.
FutureOr<bool> isValidAuthToken(String serial, String authToken);
}

class UpdatablePassResponse {
Expand All @@ -49,3 +67,8 @@ class DevPassKitBackend extends PassKitBackend {
@override
void noSuchMethod(Invocation invocation) {}
}

enum NotificationRegistrationReponse {
created,
existing,
}
155 changes: 115 additions & 40 deletions passkit_server/lib/src/passkit_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import 'package:shelf_router/shelf_router.dart';

extension PasskitServerExtension on Router {
void addPassKitServer(PassKitBackend backend) {
get('/v1/passes/<identifier>/<serial>', getLatestVersion);
post(
'/v1/devices/<deviceID>/registrations/<passTypeID>/<serial>',
setupNotifications,
);
get(
'/v1/devices/<deviceID>/registrations/<typeID>',
getListOfUpdatablePasses,
Expand All @@ -16,16 +19,11 @@ extension PasskitServerExtension on Router {
'/v1/devices/<deviceID>/registrations/<passTypeID>/<serial>',
stopNotifications,
);
post(
'/v1/devices/<deviceID>/registrations/<passTypeID>/<serial>',
setupNotifications,
);
get('/v1/passes/<identifier>/<serial>', getLatestVersion);
post('/v1/log', logMessages);
}
}

// GET request
/// URL must end with "v1/passes/{identifier}/{serial}"
/// Pass delivery
///
/// GET /v1/passes/<typeID>/<serial#>
Expand All @@ -34,19 +32,18 @@ extension PasskitServerExtension on Router {
/// server response:
/// --> if auth token is correct: 200, with pass data payload
/// --> if auth token is incorrect: 401
FutureOr<Response> getLatestVersion(
Future<Response> getLatestVersion(
Request request,
String identifier,
String serial,
) async {
var token = request.getApplePassToken();

if (token == null) {
return Response.unauthorized(null);
final backend = DevPassKitBackend();
final response = await backend.validateAuthToken(request, serial);
if (response != null) {
return response;
}

final pass =
await DevPassKitBackend().getUpdatedPass(identifier, serial, token);
final pass = await backend.getUpdatedPass(identifier, serial);

if (pass == null) {
return Response.unauthorized(null);
Expand All @@ -55,71 +52,133 @@ FutureOr<Response> getLatestVersion(
return Response.ok(pass.sourceData);
}

// POST request
/// URL must end with "v1/log"
FutureOr<Response> logMessages(Request request) async {
/// Logging/Debugging from the device
///
/// log an error or unexpected server behavior, to help with server debugging
/// POST /v1/log
/// JSON payload: { "description" : <human-readable description of error> }
///
/// server response: 200
Future<Response> logMessages(Request request) async {
final content = await request.readAsString();
await DevPassKitBackend()
.logMessage(jsonDecode(content) as Map<String, dynamic>);
// There's no need to wait for the log message to be written, instead return
// a 200 status code response right away
unawaited(
DevPassKitBackend().logMessage(jsonDecode(content) as Map<String, dynamic>),
);
return Response.ok(null);
}

// POST request
/// URL must end with "v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}"
FutureOr<Response> setupNotifications(
/// Registration
/// register a device to receive push notifications for a pass
///
/// POST /v1/devices/<deviceID>/registrations/<typeID>/<serial#>
/// Header: Authorization: ApplePass <authenticationToken>
/// JSON payload:
/// ```json
/// { "pushToken" : <push token, which the server needs to send push notifications to this device> }
/// ```
///
/// Params definition
/// [deviceId] : the device's identifier
/// [passTypeId] : the bundle identifier for a class of passes, sometimes refered
/// to as the pass topic, e.g. pass.com.apple.backtoschoolgift,
/// registered with WWDR
/// [serialNumber] : the pass' serial number
/// `pushToken` (from the [request]): the value needed for Apple Push Notification service
///
/// server action: if the authentication token is correct, associate the given
/// push token and device identifier with this pass
/// server response:
/// --> if registration succeeded: 201
/// --> if this serial number was already registered for this device: 304
/// --> if not authorized: 401
Future<Response> setupNotifications(
Request request,
String deviceId,
String passTypeId,
String serialNumber,
) {
return Response.notFound(null);
) async {
final backend = DevPassKitBackend();
final response = await backend.validateAuthToken(request, serialNumber);
if (response != null) {
return response;
}
final body = await request.readAsString();
final bodyJson = jsonDecode(body) as Map<String, dynamic>;
final pushToken = bodyJson['pushToken'] as String?;
if (pushToken == null) {
// TODO(anyone): include more information in debug mode?
return Response.badRequest();
}

final notificationRegistrationReponse = await backend.setupNotifications(
deviceId,
passTypeId,
serialNumber,
pushToken,
);

return switch (notificationRegistrationReponse) {
NotificationRegistrationReponse.created => Response(201),
NotificationRegistrationReponse.existing => Response.ok(null),
};
}

/// Unregister
///
/// unregister a device to receive push notifications for a pass
///
/// DELETE /v1/devices/<deviceID>/registrations/<passTypeID>/<serialNumber>
/// DELETE /v1/devices/<deviceID>/registrations/<passTypeID>/<serial#>
/// Header: Authorization: ApplePass <authenticationToken>
///
/// server action: if the authentication token is correct, disassociate the device from this pass
/// server action: if the authentication token is correct, disassociate the
/// device from this pass
/// server response:
/// --> if disassociation succeeded: 200
/// --> if not authorized: 401
FutureOr<Response> stopNotifications(
Future<Response> stopNotifications(
Request request,
String deviceId,
String passTypeId,
String serialNumber,
) {
var token = request.getApplePassToken();

if (token == null) {
return Response.unauthorized(null);
) async {
final backend = DevPassKitBackend();
final response = await backend.validateAuthToken(request, serialNumber);
if (response != null) {
return response;
}
return Response.notFound(null);
final success = await backend.stopNotifications(
deviceId,
passTypeId,
serialNumber,
);
if (success) {
return Response.ok(null);
}

// TODO(anyone): Is this correct?
return Response.internalServerError();
}

// GET request
/// URL must end with "v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}"
/// Updatable passes
///
/// get all serial numbers associated with a device for passes that need an update
/// get all serial #s associated with a device for passes that need an update
/// Optionally with a query limiter to scope the last update since
///
/// GET /v1/devices/<deviceID>/registrations/<typeID>
/// GET /v1/devices/<deviceID>/registrations/<typeID>?passesUpdatedSince=<tag>
///
/// server action: figure out which passes associated with this device have been modified since the supplied tag (if no tag provided, all associated serial numbers)
/// server action: figure out which passes associated with this device have been modified since the supplied tag (if no tag provided, all associated serial #s)
/// server response:
/// --> if there are matching passes: 200, with JSON payload: { "lastUpdated" : <new tag>, "serialNumbers" : [ <array of serial numbers> ] }
/// --> if there are matching passes: 200, with JSON payload: { "lastUpdated" : <new tag>, "serialNumbers" : [ <array of serial #s> ] }
/// --> if there are no matching passes: 204
/// --> if unknown device identifier: 404
FutureOr<Response> getListOfUpdatablePasses(
Future<Response> getListOfUpdatablePasses(
Request request,
String deviceId,
String typeId,
) {
) async {
return Response.notFound(null);
}

Expand All @@ -134,3 +193,19 @@ extension on Request {
return null;
}
}

extension on PassKitBackend {
Future<Response?> validateAuthToken(Request request, String serial) async {
var token = request.getApplePassToken();

if (token == null) {
return Response.unauthorized(null);
}

final isValidToken = await isValidAuthToken(serial, token);
if (!isValidToken) {
return Response.unauthorized(null);
}
return null;
}
}

0 comments on commit 68a784d

Please sign in to comment.