-
-
Notifications
You must be signed in to change notification settings - Fork 4
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
feat(passkit_server): Passkit Backend #76
Merged
Changes from 6 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
c339642
wip
ueman 010ff4e
wip
ueman 90c6499
wip
ueman 78c2b77
Merge branch 'master' into backend
ueman 68a784d
wip
ueman 6ea3bd4
wip
ueman 21413ff
Merge branch 'master' into backend
ueman d23e74a
backend
ueman 588811e
wip
ueman 4592ee2
renaming, docs etc
ueman 9abddff
Merge branch 'master' into backend
ueman 477eb6d
example
ueman b440966
update table
ueman be68145
add headers
ueman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# https://dart.dev/guides/libraries/private-files | ||
# Created by `dart pub` | ||
.dart_tool/ | ||
|
||
# Avoid committing pubspec.lock for library packages; see | ||
# https://dart.dev/guides/libraries/private-files#pubspeclock. | ||
pubspec.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
## 1.0.0 | ||
|
||
- Initial version. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<!-- | ||
This README describes the package. If you publish this package to pub.dev, | ||
this README's contents appear on the landing page for your package. | ||
For information about how to write a good package README, see the guide for | ||
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages). | ||
For general information about developing packages, see the Dart guide for | ||
[creating packages](https://dart.dev/guides/libraries/create-library-packages) | ||
and the Flutter guide for | ||
[developing packages and plugins](https://flutter.dev/developing-packages). | ||
--> | ||
|
||
TODO: Put a short description of the package here that helps potential users | ||
know whether this package might be useful for them. | ||
|
||
## Features | ||
|
||
TODO: List what your package can do. Maybe include images, gifs, or videos. | ||
|
||
## Getting started | ||
|
||
TODO: List prerequisites and provide or point to information on how to | ||
start using the package. | ||
|
||
## Usage | ||
|
||
TODO: Include short and useful examples for package users. Add longer examples | ||
to `/example` folder. | ||
|
||
```dart | ||
const like = 'sample'; | ||
``` | ||
|
||
## Additional information | ||
|
||
TODO: Tell users more about the package: where to find more information, how to | ||
contribute to the package, how to file issues, what response they can expect | ||
from the package authors, and more. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
include: package:lints/recommended.yaml | ||
|
||
linter: | ||
rules: | ||
prefer_single_quotes: true | ||
unawaited_futures: true | ||
sort_constructors_first: true | ||
use_key_in_widget_constructors: true | ||
use_super_parameters: true | ||
use_colored_box: true | ||
use_decorated_box: true | ||
no_leading_underscores_for_local_identifiers: true | ||
require_trailing_commas: true | ||
flutter_style_todos: true | ||
sort_pub_dependencies: true | ||
|
||
analyzer: | ||
language: | ||
strict-casts: true | ||
strict-inference: true | ||
strict-raw-types: true | ||
errors: | ||
missing_required_param: error | ||
missing_return: error | ||
todo: ignore | ||
exclude: | ||
- "**.g.dart" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export 'src/passkit_server.dart'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import 'dart:async'; | ||
|
||
import 'package:passkit/passkit.dart'; | ||
|
||
abstract class PassKitBackend { | ||
/// Saves JSON that gets send to `/v1/log` | ||
Future<void> logMessage(Map<String, dynamic> message); | ||
|
||
/// "/v1/passes/{identifier}/{serial}"" | ||
Future<UpdatablePassResponse?> returnUpdatablePasses( | ||
String identifier, | ||
String serial, | ||
String updatedSince, | ||
); | ||
|
||
/// URL must end with "v1/passes/{identifier}/{serial}" | ||
/// Pass delivery | ||
/// | ||
/// GET /v1/passes/<typeID>/<serial#> | ||
/// Header: Authorization: ApplePass <authenticationToken> | ||
/// | ||
/// server response: | ||
/// --> if auth token is correct: 200, with pass data payload | ||
/// --> if auth token is incorrect: 401 | ||
Future<PkPass?> getUpdatedPass( | ||
String identifier, | ||
String serial, | ||
); | ||
|
||
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 { | ||
UpdatablePassResponse._(this.response, {this.tag, this.ids}); | ||
|
||
factory UpdatablePassResponse.matchingPasses(String tag, List<String> ids) { | ||
return UpdatablePassResponse._(200, tag: tag, ids: ids); | ||
} | ||
|
||
factory UpdatablePassResponse.noMatchingPasses() => | ||
UpdatablePassResponse._(204); | ||
factory UpdatablePassResponse.unknownDeviceIdentifier() => | ||
UpdatablePassResponse._(404); | ||
|
||
final String? tag; | ||
final List<String>? ids; | ||
|
||
final int response; | ||
} | ||
|
||
class DevPassKitBackend extends PassKitBackend { | ||
@override | ||
void noSuchMethod(Invocation invocation) {} | ||
} | ||
|
||
enum NotificationRegistrationReponse { | ||
created, | ||
existing, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import 'dart:async'; | ||
import 'dart:convert'; | ||
|
||
import 'package:passkit_server/src/passkit_backend.dart'; | ||
import 'package:shelf/shelf.dart'; | ||
import 'package:shelf_router/shelf_router.dart'; | ||
|
||
extension PasskitServerExtension on Router { | ||
void addPassKitServer(PassKitBackend backend) { | ||
post( | ||
'/v1/devices/<deviceID>/registrations/<passTypeID>/<serial>', | ||
setupNotifications(backend), | ||
); | ||
get( | ||
'/v1/devices/<deviceID>/registrations/<typeID>', | ||
getListOfUpdatablePasses(backend), | ||
); | ||
delete( | ||
'/v1/devices/<deviceID>/registrations/<passTypeID>/<serial>', | ||
stopNotifications(backend), | ||
); | ||
get('/v1/passes/<identifier>/<serial>', getLatestVersion(backend)); | ||
post('/v1/log', logMessages(backend)); | ||
} | ||
} | ||
|
||
/// Pass delivery | ||
/// | ||
/// GET /v1/passes/<typeID>/<serial#> | ||
/// Header: Authorization: ApplePass <authenticationToken> | ||
/// | ||
/// server response: | ||
/// --> if auth token is correct: 200, with pass data payload | ||
/// --> if auth token is incorrect: 401 | ||
Function getLatestVersion(PassKitBackend backend) { | ||
return (Request request, String identifier, String serial) async { | ||
final response = await backend.validateAuthToken(request, serial); | ||
if (response != null) { | ||
return response; | ||
} | ||
|
||
final pass = await backend.getUpdatedPass(identifier, serial); | ||
|
||
if (pass == null) { | ||
return Response.unauthorized(null); | ||
} | ||
|
||
return Response.ok(pass.sourceData); | ||
}; | ||
} | ||
|
||
/// 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 | ||
Function logMessages(PassKitBackend backend) { | ||
return (Request request) async { | ||
final content = await request.readAsString(); | ||
// 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); | ||
}; | ||
} | ||
|
||
/// 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 | ||
Function setupNotifications(PassKitBackend backend) { | ||
return ( | ||
Request request, | ||
String deviceId, | ||
String passTypeId, | ||
String serialNumber, | ||
) async { | ||
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>/<serial#> | ||
/// Header: Authorization: ApplePass <authenticationToken> | ||
/// | ||
/// server action: if the authentication token is correct, disassociate the | ||
/// device from this pass | ||
/// server response: | ||
/// --> if disassociation succeeded: 200 | ||
/// --> if not authorized: 401 | ||
Function stopNotifications(PassKitBackend backend) { | ||
return ( | ||
Request request, | ||
String deviceId, | ||
String passTypeId, | ||
String serialNumber, | ||
) async { | ||
final backend = DevPassKitBackend(); | ||
final response = await backend.validateAuthToken(request, serialNumber); | ||
if (response != null) { | ||
return response; | ||
} | ||
final success = await backend.stopNotifications( | ||
deviceId, | ||
passTypeId, | ||
serialNumber, | ||
); | ||
if (success) { | ||
return Response.ok(null); | ||
} | ||
|
||
// TODO(anyone): Is this correct? | ||
return Response.internalServerError(); | ||
}; | ||
} | ||
|
||
/// Updatable passes | ||
/// | ||
/// 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 #s) | ||
/// server response: | ||
/// --> 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 | ||
Function getListOfUpdatablePasses(PassKitBackend backend) { | ||
return (Request request, String deviceId, String typeId) async { | ||
return Response.notFound(null); | ||
}; | ||
} | ||
|
||
extension on Request { | ||
String? getApplePassToken() { | ||
var header = headers['Authorization']; | ||
if (header?.startsWith('ApplePass ') == true) { | ||
var token = header?.split(' ').lastOrNull; | ||
return token; | ||
} | ||
|
||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
name: passkit_server | ||
description: A starting point for Dart libraries or applications. | ||
version: 0.0.1 | ||
repository: https://github.com/ueman/passkit | ||
|
||
environment: | ||
sdk: ^3.4.4 | ||
|
||
dependencies: | ||
passkit: ^0.0.4 | ||
shelf: ^1.4.2 | ||
shelf_router: ^1.1.4 | ||
|
||
dev_dependencies: | ||
lints: ^4.0.0 | ||
test: ^1.24.0 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be a shelf routing middleware. That ensures better compatibility with dart_frog. For shelf_router compatibility should still be maintained though, but I guess that's easy enough to do and requires just some documentation or example code. That way, this package also doesn't depend on the shelf_router package itself.
References: