├── .dockerignore ├── .gitignore ├── .swift-version ├── .travis.yml ├── Config ├── app.json ├── clients.json ├── crypto.json ├── development │ └── database.json ├── droplet.json ├── production │ ├── app.json │ └── database.json ├── servers.json └── storage.json ├── Dockerfile ├── LICENSE ├── Package.pins ├── Package.swift ├── Public ├── images │ ├── barcode.png │ ├── icon.png │ ├── icon@2x.png │ └── sierra.jpg └── styles │ ├── app.css │ └── normalize.css ├── README.md ├── Resources └── Views │ ├── base.leaf │ ├── github.leaf │ └── welcome.leaf └── Sources └── App ├── Collections ├── VanityCollection.swift └── WalletCollection.swift ├── DateFormatters.swift ├── Droplet+Database.swift ├── Droplet+VaporAPNS.swift ├── Models ├── Pass.swift └── Registration.swift ├── OpenSSLHelper.swift └── main.swift /.dockerignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | xcuserdata 4 | *.xcodeproj 5 | Config/secrets 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | xcuserdata 4 | *.xcodeproj 5 | Config/secrets 6 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | language: generic 5 | sudo: required 6 | dist: trusty 7 | osx_image: xcode8 8 | install: 9 | - eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/02090c7ede5a637b76e6df1710e83cd0bbe7dcdf/swiftenv-install.sh)" 10 | script: 11 | - swift build 12 | - swift build -c release 13 | -------------------------------------------------------------------------------- /Config/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "apns": { 3 | "topic": "$APNS_TOPIC", 4 | "keyID": "$APNS_KEY_ID", 5 | "teamID": "$APNS_TEAM_ID", 6 | "privateKey": "$APNS_PRIVATE_KEY" 7 | }, 8 | "updatePassword": "$UPDATE_PASSWORD" 9 | } 10 | -------------------------------------------------------------------------------- /Config/clients.json: -------------------------------------------------------------------------------- 1 | { 2 | "tls": { 3 | "verifyHost": true, 4 | "verifyCertificates": true, 5 | "certificates": "defaults" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Config/crypto.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": { 3 | "method": "sha256", 4 | "key": "password" 5 | }, 6 | "cipher": { 7 | "method": "chacha20", 8 | "key": "passwordpasswordpasswordpassword", 9 | "iv": "password" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Config/development/database.json: -------------------------------------------------------------------------------- 1 | { 2 | "postgres": "postgres://$USER:@0.0.0.0:5432/passcards" 3 | } 4 | -------------------------------------------------------------------------------- /Config/droplet.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "engine", 3 | "client": "engine", 4 | "console": "terminal", 5 | "log": "console", 6 | "hash": "crypto", 7 | "cipher": "crypto", 8 | "middleware": { 9 | "server": [ 10 | "file", 11 | "abort", 12 | "validation", 13 | "type-safe", 14 | "date", 15 | "sessions" 16 | ], 17 | "client": [ 18 | 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Config/production/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "$VAPOR_APP_KEY" 3 | } 4 | -------------------------------------------------------------------------------- /Config/production/database.json: -------------------------------------------------------------------------------- 1 | { 2 | "postgres": "$DATABASE_URL" 3 | } 4 | -------------------------------------------------------------------------------- /Config/servers.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "port": "$PORT:8080", 4 | "host": "0.0.0.0", 5 | "securityLayer": "none" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Config/storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "driver": "s3", 3 | "bucket": "$S3_BUCKET", 4 | "accessKey": "$S3_ACCESS_KEY", 5 | "secretKey": "$S3_SECRET_KEY", 6 | "host": "s3.amazonaws.com", 7 | "region": "$S3_REGION", 8 | "template": "/#file" 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Based on instructions from https://git.io/v9FmJ 2 | 3 | FROM aleksaubry/swift-apns:3.1.0 4 | 5 | ADD ./ /app 6 | WORKDIR /app 7 | 8 | # Install PostgreSQL 9 | RUN apt-get update 10 | RUN apt-get install -y libpq-dev 11 | 12 | # Build Swift 13 | RUN swift build -c release 14 | 15 | ENV PATH /app/.build/release:$PATH 16 | 17 | RUN chmod -R a+rwx /app 18 | 19 | RUN useradd -m myuser 20 | USER myuser 21 | 22 | CMD .build/release/App --env=production --workdir="/app" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Alexsander Akers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.pins: -------------------------------------------------------------------------------- 1 | { 2 | "autoPin": true, 3 | "pins": [ 4 | { 5 | "package": "AWS", 6 | "reason": null, 7 | "repositoryURL": "https://github.com/nodes-vapor/aws.git", 8 | "version": "0.1.0" 9 | }, 10 | { 11 | "package": "CCurl", 12 | "reason": null, 13 | "repositoryURL": "https://github.com/boostcode/CCurl.git", 14 | "version": "0.2.4" 15 | }, 16 | { 17 | "package": "CLibreSSL", 18 | "reason": null, 19 | "repositoryURL": "https://github.com/vapor/clibressl.git", 20 | "version": "1.0.0" 21 | }, 22 | { 23 | "package": "CPostgreSQL", 24 | "reason": null, 25 | "repositoryURL": "https://github.com/vapor/cpostgresql.git", 26 | "version": "1.0.0" 27 | }, 28 | { 29 | "package": "Console", 30 | "reason": null, 31 | "repositoryURL": "https://github.com/vapor/console.git", 32 | "version": "1.0.2" 33 | }, 34 | { 35 | "package": "Core", 36 | "reason": null, 37 | "repositoryURL": "https://github.com/vapor/core.git", 38 | "version": "1.1.2" 39 | }, 40 | { 41 | "package": "Crypto", 42 | "reason": null, 43 | "repositoryURL": "https://github.com/vapor/crypto.git", 44 | "version": "1.1.0" 45 | }, 46 | { 47 | "package": "DataURI", 48 | "reason": null, 49 | "repositoryURL": "https://github.com/nodes-vapor/data-uri.git", 50 | "version": "0.1.1" 51 | }, 52 | { 53 | "package": "Engine", 54 | "reason": null, 55 | "repositoryURL": "https://github.com/vapor/engine.git", 56 | "version": "1.3.12" 57 | }, 58 | { 59 | "package": "Fluent", 60 | "reason": null, 61 | "repositoryURL": "https://github.com/vapor/fluent.git", 62 | "version": "1.4.3" 63 | }, 64 | { 65 | "package": "FluentPostgreSQL", 66 | "reason": null, 67 | "repositoryURL": "https://github.com/vapor-community/postgresql-driver.git", 68 | "version": "1.1.0" 69 | }, 70 | { 71 | "package": "JSON", 72 | "reason": null, 73 | "repositoryURL": "https://github.com/vapor/json.git", 74 | "version": "1.0.6" 75 | }, 76 | { 77 | "package": "JWT", 78 | "reason": null, 79 | "repositoryURL": "https://github.com/vapor/jwt.git", 80 | "version": "1.0.1" 81 | }, 82 | { 83 | "package": "Jay", 84 | "reason": null, 85 | "repositoryURL": "https://github.com/DanToml/Jay.git", 86 | "version": "1.0.1" 87 | }, 88 | { 89 | "package": "Leaf", 90 | "reason": null, 91 | "repositoryURL": "https://github.com/vapor/leaf.git", 92 | "version": "1.0.7" 93 | }, 94 | { 95 | "package": "MimeLib", 96 | "reason": null, 97 | "repositoryURL": "https://github.com/manGoweb/MimeLib.git", 98 | "version": "1.0.0" 99 | }, 100 | { 101 | "package": "Multipart", 102 | "reason": null, 103 | "repositoryURL": "https://github.com/vapor/multipart.git", 104 | "version": "1.0.3" 105 | }, 106 | { 107 | "package": "Node", 108 | "reason": null, 109 | "repositoryURL": "https://github.com/vapor/node.git", 110 | "version": "1.0.1" 111 | }, 112 | { 113 | "package": "PathIndexable", 114 | "reason": null, 115 | "repositoryURL": "https://github.com/vapor/path-indexable.git", 116 | "version": "1.0.0" 117 | }, 118 | { 119 | "package": "Polymorphic", 120 | "reason": null, 121 | "repositoryURL": "https://github.com/vapor/polymorphic.git", 122 | "version": "1.0.1" 123 | }, 124 | { 125 | "package": "PostgreSQL", 126 | "reason": null, 127 | "repositoryURL": "https://github.com/vapor/postgresql.git", 128 | "version": "1.1.0" 129 | }, 130 | { 131 | "package": "Routing", 132 | "reason": null, 133 | "repositoryURL": "https://github.com/vapor/routing.git", 134 | "version": "1.1.0" 135 | }, 136 | { 137 | "package": "Socks", 138 | "reason": null, 139 | "repositoryURL": "https://github.com/vapor/socks.git", 140 | "version": "1.2.7" 141 | }, 142 | { 143 | "package": "Storage", 144 | "reason": null, 145 | "repositoryURL": "https://github.com/nodes-vapor/storage.git", 146 | "version": "0.3.8" 147 | }, 148 | { 149 | "package": "SwiftString", 150 | "reason": null, 151 | "repositoryURL": "https://github.com/matthijs2704/SwiftString.git", 152 | "version": "1.0.5" 153 | }, 154 | { 155 | "package": "TLS", 156 | "reason": null, 157 | "repositoryURL": "https://github.com/vapor/tls.git", 158 | "version": "1.1.2" 159 | }, 160 | { 161 | "package": "Turnstile", 162 | "reason": null, 163 | "repositoryURL": "https://github.com/stormpath/Turnstile.git", 164 | "version": "1.0.6" 165 | }, 166 | { 167 | "package": "Vapor", 168 | "reason": null, 169 | "repositoryURL": "https://github.com/vapor/vapor.git", 170 | "version": "1.5.15" 171 | }, 172 | { 173 | "package": "VaporAPNS", 174 | "reason": null, 175 | "repositoryURL": "https://github.com/matthijs2704/vapor-apns.git", 176 | "version": "1.2.2" 177 | } 178 | ], 179 | "version": 1 180 | } -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "passcards", 5 | dependencies: [ 6 | .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1, minor: 5), 7 | .Package(url: "https://github.com/vapor/fluent.git", majorVersion: 1), 8 | .Package(url: "https://github.com/vapor-community/postgresql-driver.git", majorVersion: 1, minor: 1), 9 | .Package(url: "https://github.com/nodes-vapor/storage.git", majorVersion: 0, minor: 3), 10 | .Package(url: "https://github.com/matthijs2704/vapor-apns.git", majorVersion: 1, minor: 2), 11 | ], 12 | exclude: [ 13 | "Config", 14 | "Database", 15 | "Localization", 16 | "Public", 17 | "Resources", 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /Public/images/barcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a2/passcards-swift/ae62b6bb6d4f79c33d02a9197fd164d186892aee/Public/images/barcode.png -------------------------------------------------------------------------------- /Public/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a2/passcards-swift/ae62b6bb6d4f79c33d02a9197fd164d186892aee/Public/images/icon.png -------------------------------------------------------------------------------- /Public/images/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a2/passcards-swift/ae62b6bb6d4f79c33d02a9197fd164d186892aee/Public/images/icon@2x.png -------------------------------------------------------------------------------- /Public/images/sierra.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a2/passcards-swift/ae62b6bb6d4f79c33d02a9197fd164d186892aee/Public/images/sierra.jpg -------------------------------------------------------------------------------- /Public/styles/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, sans-seif; 3 | margin: 25px; 4 | min-height: 550px; 5 | min-width: 375px; 6 | } 7 | 8 | .fullsize { 9 | position: absolute; 10 | top: 0; 11 | bottom: 0; 12 | left: 0; 13 | right: 0; 14 | } 15 | 16 | .background { 17 | position: fixed; 18 | } 19 | 20 | .background > div { 21 | background: url('/images/sierra.jpg') center center no-repeat; 22 | background-size: cover; 23 | min-height: 100%; 24 | } 25 | 26 | .card { 27 | width: 375px; 28 | height: 550px; 29 | position: absolute; 30 | left: 50%; 31 | top: 25px; 32 | margin-left: -187.5px; 33 | width: 375px; 34 | height: 550px; 35 | border-radius: 12px; 36 | color: #000; 37 | background: #d9d6cc; 38 | z-index: 2; 39 | overflow: hidden; 40 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); 41 | } 42 | 43 | @media (min-height: 600px) { 44 | .card { 45 | top: 50%; 46 | margin-top: -275px; 47 | } 48 | } 49 | 50 | .card .barcode { 51 | width: 266px; 52 | height: 92px; 53 | background: #fff; 54 | bottom: 25px; 55 | position: absolute; 56 | left: 50%; 57 | margin-left: -133px; 58 | border-radius: 6px; 59 | } 60 | 61 | .card .barcode div { 62 | background: url('/images/barcode.png') no-repeat center center; 63 | } 64 | 65 | .card .logo { 66 | left: 13px; 67 | top: 13px; 68 | position: absolute; 69 | background: url('/images/logo.png') no-repeat center center; 70 | background-size: 35px 35px; 71 | text-indent: -9999rem; 72 | width: 35px; 73 | height: 35px; 74 | } 75 | 76 | .card .logoText { 77 | font-size: 32px; 78 | font-weight: 600; 79 | position: absolute; 80 | margin: 0 auto; 81 | text-align: center; 82 | width: 100%; 83 | top: 14px; 84 | } 85 | 86 | .card .strip { 87 | top: 65px; 88 | position: absolute; 89 | box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.3); 90 | background: rgba(0, 0, 0, 0.1) url('/images/icon.png') center center no-repeat; 91 | background-size: 96px 96px; 92 | width: 425px; 93 | margin-left: -25px; 94 | height: 144px; 95 | } 96 | 97 | @media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) { 98 | .card .strip { 99 | background-image: url('/images/icon@2x.png'); 100 | } 101 | } 102 | 103 | .card .field { 104 | position: absolute; 105 | top: 220px; 106 | } 107 | 108 | .card .field.left { 109 | left: 15px; 110 | } 111 | 112 | .card .field.right { 113 | right: 15px; 114 | text-align: right; 115 | } 116 | 117 | .card .field .title { 118 | text-transform: uppercase; 119 | font-size: 11px; 120 | font-weight: 500; 121 | } 122 | 123 | .card .field .value { 124 | font-size: 26px; 125 | padding-top: 4px; 126 | } 127 | 128 | 129 | .alias { 130 | image-rendering: optimizeSpeed; 131 | image-rendering: -moz-crisp-edges; 132 | image-rendering: -o-crisp-edges; 133 | image-rendering: -webkit-optimize-contrast; 134 | image-rendering: pixelated; 135 | image-rendering: optimize-contrast; 136 | -ms-interpolation-mode: nearest-neighbor; 137 | } 138 | -------------------------------------------------------------------------------- /Public/styles/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in 9 | * IE on Windows Phone and in iOS. 10 | */ 11 | 12 | html { 13 | line-height: 1.15; /* 1 */ 14 | -ms-text-size-adjust: 100%; /* 2 */ 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers (opinionated). 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Add the correct display in IE 9-. 31 | */ 32 | 33 | article, 34 | aside, 35 | footer, 36 | header, 37 | nav, 38 | section { 39 | display: block; 40 | } 41 | 42 | /** 43 | * Correct the font size and margin on `h1` elements within `section` and 44 | * `article` contexts in Chrome, Firefox, and Safari. 45 | */ 46 | 47 | h1 { 48 | font-size: 2em; 49 | margin: 0.67em 0; 50 | } 51 | 52 | /* Grouping content 53 | ========================================================================== */ 54 | 55 | /** 56 | * Add the correct display in IE 9-. 57 | * 1. Add the correct display in IE. 58 | */ 59 | 60 | figcaption, 61 | figure, 62 | main { /* 1 */ 63 | display: block; 64 | } 65 | 66 | /** 67 | * Add the correct margin in IE 8. 68 | */ 69 | 70 | figure { 71 | margin: 1em 40px; 72 | } 73 | 74 | /** 75 | * 1. Add the correct box sizing in Firefox. 76 | * 2. Show the overflow in Edge and IE. 77 | */ 78 | 79 | hr { 80 | box-sizing: content-box; /* 1 */ 81 | height: 0; /* 1 */ 82 | overflow: visible; /* 2 */ 83 | } 84 | 85 | /** 86 | * 1. Correct the inheritance and scaling of font size in all browsers. 87 | * 2. Correct the odd `em` font sizing in all browsers. 88 | */ 89 | 90 | pre { 91 | font-family: monospace, monospace; /* 1 */ 92 | font-size: 1em; /* 2 */ 93 | } 94 | 95 | /* Text-level semantics 96 | ========================================================================== */ 97 | 98 | /** 99 | * 1. Remove the gray background on active links in IE 10. 100 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 101 | */ 102 | 103 | a { 104 | background-color: transparent; /* 1 */ 105 | -webkit-text-decoration-skip: objects; /* 2 */ 106 | } 107 | 108 | /** 109 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-. 110 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 111 | */ 112 | 113 | abbr[title] { 114 | border-bottom: none; /* 1 */ 115 | text-decoration: underline; /* 2 */ 116 | text-decoration: underline dotted; /* 2 */ 117 | } 118 | 119 | /** 120 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: inherit; 126 | } 127 | 128 | /** 129 | * Add the correct font weight in Chrome, Edge, and Safari. 130 | */ 131 | 132 | b, 133 | strong { 134 | font-weight: bolder; 135 | } 136 | 137 | /** 138 | * 1. Correct the inheritance and scaling of font size in all browsers. 139 | * 2. Correct the odd `em` font sizing in all browsers. 140 | */ 141 | 142 | code, 143 | kbd, 144 | samp { 145 | font-family: monospace, monospace; /* 1 */ 146 | font-size: 1em; /* 2 */ 147 | } 148 | 149 | /** 150 | * Add the correct font style in Android 4.3-. 151 | */ 152 | 153 | dfn { 154 | font-style: italic; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Add the correct display in IE 9-. 200 | */ 201 | 202 | audio, 203 | video { 204 | display: inline-block; 205 | } 206 | 207 | /** 208 | * Add the correct display in iOS 4-7. 209 | */ 210 | 211 | audio:not([controls]) { 212 | display: none; 213 | height: 0; 214 | } 215 | 216 | /** 217 | * Remove the border on images inside links in IE 10-. 218 | */ 219 | 220 | img { 221 | border-style: none; 222 | } 223 | 224 | /** 225 | * Hide the overflow in IE. 226 | */ 227 | 228 | svg:not(:root) { 229 | overflow: hidden; 230 | } 231 | 232 | /* Forms 233 | ========================================================================== */ 234 | 235 | /** 236 | * 1. Change the font styles in all browsers (opinionated). 237 | * 2. Remove the margin in Firefox and Safari. 238 | */ 239 | 240 | button, 241 | input, 242 | optgroup, 243 | select, 244 | textarea { 245 | font-family: sans-serif; /* 1 */ 246 | font-size: 100%; /* 1 */ 247 | line-height: 1.15; /* 1 */ 248 | margin: 0; /* 2 */ 249 | } 250 | 251 | /** 252 | * Show the overflow in IE. 253 | * 1. Show the overflow in Edge. 254 | */ 255 | 256 | button, 257 | input { /* 1 */ 258 | overflow: visible; 259 | } 260 | 261 | /** 262 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 263 | * 1. Remove the inheritance of text transform in Firefox. 264 | */ 265 | 266 | button, 267 | select { /* 1 */ 268 | text-transform: none; 269 | } 270 | 271 | /** 272 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 273 | * controls in Android 4. 274 | * 2. Correct the inability to style clickable types in iOS and Safari. 275 | */ 276 | 277 | button, 278 | html [type="button"], /* 1 */ 279 | [type="reset"], 280 | [type="submit"] { 281 | -webkit-appearance: button; /* 2 */ 282 | } 283 | 284 | /** 285 | * Remove the inner border and padding in Firefox. 286 | */ 287 | 288 | button::-moz-focus-inner, 289 | [type="button"]::-moz-focus-inner, 290 | [type="reset"]::-moz-focus-inner, 291 | [type="submit"]::-moz-focus-inner { 292 | border-style: none; 293 | padding: 0; 294 | } 295 | 296 | /** 297 | * Restore the focus styles unset by the previous rule. 298 | */ 299 | 300 | button:-moz-focusring, 301 | [type="button"]:-moz-focusring, 302 | [type="reset"]:-moz-focusring, 303 | [type="submit"]:-moz-focusring { 304 | outline: 1px dotted ButtonText; 305 | } 306 | 307 | /** 308 | * Correct the padding in Firefox. 309 | */ 310 | 311 | fieldset { 312 | padding: 0.35em 0.75em 0.625em; 313 | } 314 | 315 | /** 316 | * 1. Correct the text wrapping in Edge and IE. 317 | * 2. Correct the color inheritance from `fieldset` elements in IE. 318 | * 3. Remove the padding so developers are not caught out when they zero out 319 | * `fieldset` elements in all browsers. 320 | */ 321 | 322 | legend { 323 | box-sizing: border-box; /* 1 */ 324 | color: inherit; /* 2 */ 325 | display: table; /* 1 */ 326 | max-width: 100%; /* 1 */ 327 | padding: 0; /* 3 */ 328 | white-space: normal; /* 1 */ 329 | } 330 | 331 | /** 332 | * 1. Add the correct display in IE 9-. 333 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 334 | */ 335 | 336 | progress { 337 | display: inline-block; /* 1 */ 338 | vertical-align: baseline; /* 2 */ 339 | } 340 | 341 | /** 342 | * Remove the default vertical scrollbar in IE. 343 | */ 344 | 345 | textarea { 346 | overflow: auto; 347 | } 348 | 349 | /** 350 | * 1. Add the correct box sizing in IE 10-. 351 | * 2. Remove the padding in IE 10-. 352 | */ 353 | 354 | [type="checkbox"], 355 | [type="radio"] { 356 | box-sizing: border-box; /* 1 */ 357 | padding: 0; /* 2 */ 358 | } 359 | 360 | /** 361 | * Correct the cursor style of increment and decrement buttons in Chrome. 362 | */ 363 | 364 | [type="number"]::-webkit-inner-spin-button, 365 | [type="number"]::-webkit-outer-spin-button { 366 | height: auto; 367 | } 368 | 369 | /** 370 | * 1. Correct the odd appearance in Chrome and Safari. 371 | * 2. Correct the outline style in Safari. 372 | */ 373 | 374 | [type="search"] { 375 | -webkit-appearance: textfield; /* 1 */ 376 | outline-offset: -2px; /* 2 */ 377 | } 378 | 379 | /** 380 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 381 | */ 382 | 383 | [type="search"]::-webkit-search-cancel-button, 384 | [type="search"]::-webkit-search-decoration { 385 | -webkit-appearance: none; 386 | } 387 | 388 | /** 389 | * 1. Correct the inability to style clickable types in iOS and Safari. 390 | * 2. Change font properties to `inherit` in Safari. 391 | */ 392 | 393 | ::-webkit-file-upload-button { 394 | -webkit-appearance: button; /* 1 */ 395 | font: inherit; /* 2 */ 396 | } 397 | 398 | /* Interactive 399 | ========================================================================== */ 400 | 401 | /* 402 | * Add the correct display in IE 9-. 403 | * 1. Add the correct display in Edge, IE, and Firefox. 404 | */ 405 | 406 | details, /* 1 */ 407 | menu { 408 | display: block; 409 | } 410 | 411 | /* 412 | * Add the correct display in all browsers. 413 | */ 414 | 415 | summary { 416 | display: list-item; 417 | } 418 | 419 | /* Scripting 420 | ========================================================================== */ 421 | 422 | /** 423 | * Add the correct display in IE 9-. 424 | */ 425 | 426 | canvas { 427 | display: inline-block; 428 | } 429 | 430 | /** 431 | * Add the correct display in IE. 432 | */ 433 | 434 | template { 435 | display: none; 436 | } 437 | 438 | /* Hidden 439 | ========================================================================== */ 440 | 441 | /** 442 | * Add the correct display in IE 10-. 443 | */ 444 | 445 | [hidden] { 446 | display: none; 447 | } 448 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Passcards 2 | 3 | A simple [Wallet](https://developer.apple.com/wallet/) server that implements the [PassKit Web Service](https://developer.apple.com/library/content/documentation/PassKit/Reference/PassKit_WebService/WebService.html) requirements. (This is a Swift re-implementation of the original [Parse-backed version](https://github.com/a2/passcards-parse).) 4 | 5 | ## Building 6 | 7 | ```sh 8 | $ swift build -c release 9 | $ .build/release/App 10 | ``` 11 | 12 | ## Required Environment 13 | 14 | | Key | Description | 15 | | --- | ----------- | 16 | | APNS_KEY_ID | APNS key ID | 17 | | APNS_PRIVATE_KEY | APNS private key content | 18 | | APNS_TEAM_ID | APNS team ID | 19 | | APNS_TOPIC | APNS (certificate) topic | 20 | | PG_DBNAME | Postgres database name | 21 | | PG_HOST | Postgres host | 22 | | PG_PASSWORD | Postgres password | 23 | | PG_PORT | Postgres port | 24 | | PG_USER | Postgres user | 25 | | S3_ACCESS_KEY | S3 access key | 26 | | S3_BUCKET | S3 bucket name | 27 | | S3_REGION | S3 bucket region | 28 | | S3_SECRET_KEY | S3 access secret key | 29 | | UPDATE_PASSWORD | Update password *(unset == unlimited access)* | 30 | 31 | ## Deployment 32 | 33 | 1. Create an app on Heroku 34 | 35 | ```sh 36 | $ heroku apps:create [NAME] 37 | ``` 38 | 39 | 2. Set the environment variables (as described above) 40 | 41 | ```sh 42 | $ heroku config:set X=abc Y=def Z=ghi ... 43 | ``` 44 | 45 | If you use the [Heroku PostgreSQL](https://devcenter.heroku.com/articles/heroku-postgresql) plugin, you will need to add the plugin (which sets the `DATABASE_URL` environment variable) and then set the required `PG_*` variables. 46 | 47 | 3. Install the [Container Registry Plugin](https://devcenter.heroku.com/articles/container-registry-and-runtime) 48 | 49 | ```sh 50 | $ heroku plugins:install heroku-container-registry 51 | ``` 52 | 53 | 4. Build and deploy Docker image to Heroku 54 | 55 | ```sh 56 | $ heroku container:push web 57 | ``` 58 | 59 | 5. Open the website (a static single-page site) 60 | 61 | ```sh 62 | $ heroku open 63 | ``` 64 | 65 | ## Usage 66 | 67 | ### Creating a Pass 68 | 69 | This is beyond the scope of the project, but recommended reading includes: 70 | 71 | - Wallet Developer Guide: [Building Your First Pass](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/PassKit_PG/YourFirst.html#//apple_ref/doc/uid/TP40012195-CH2-SW1), [Pass Design and Creation](https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/PassKit_PG/Creating.html#//apple_ref/doc/uid/TP40012195-CH4-SW1) 72 | - [PassKit Package Format Reference](https://developer.apple.com/library/content/documentation/UserExperience/Reference/PassKit_Bundle/Chapters/Introduction.html) 73 | 74 | You will want to set _https://my-heroku-app.herokuapp.com/_ as the `webServiceURL` root key in your _pass.json_. 75 | 76 | Example passes, as well as the source of a command-line tool for signing Pass bundles (_signpass_), can be found [here](https://developer.apple.com/services-account/download?path=/iOS/Wallet_Support_Materials/WalletCompanionFiles.zip). 77 | 78 | ### Uploading a Pass 79 | 80 | ```sh 81 | $ curl -X POST \ 82 | -H "Authorization: Bearer MY_UPDATE_PASSWORD" \ 83 | -F "pass=@a_local_file.pkpass" \ 84 | -F "authentication_token=AUTHENTICATION_TOKEN" \ 85 | -F "pass_type_identifier=PASS_TYPE_IDENTIFIER" \ 86 | -F "serial_number=SERIAL_NUMBER" \ 87 | https://my-heroku-app.herokuapp.com/VANITY_URL.pkpass 88 | ``` 89 | 90 | In the above cURL command, _a_local_file.pkpass_ is a file in the current working directory. Set the `authentication_token`, `pass_type_identifier`, and `serial_number` fields to their corresponding values from the pass's _pass.json_. _MY_UPDATE_PASSWORD_ is the `UPDATE_PASSWORD` environment variable set in your app. 91 | 92 | ### Updating a Pass 93 | 94 | ```sh 95 | $ curl -X PUT \ 96 | -H "Authorization: Bearer MY_UPDATE_PASSWORD" \ 97 | -F "pass=@a_local_file.pkpass" \ 98 | https://my-heroku-app.herokuapp.com/VANITY_URL.pkpass 99 | ``` 100 | 101 | _a_local_file.pkpass_ is the new local file to replace on the server. _MY_UPDATE_PASSWORD_ is the same `UPDATE_PASSWORD` as above. 102 | 103 | ### Sharing a Pass 104 | 105 | A Pass recipient can go to *https://my-heroku-app.herokuapp.com/VANITY_URL.pkpass* to receive your pass. 106 | 107 | ## Author 108 | 109 | Alexsander Akers, me@a2.io 110 | 111 | ### My Personal Set-up 112 | 113 | On my personal website (*https://pass.a2.io*), I use [CloudFlare](https://www.cloudflare.com) to secure the website subdomain that points to Heroku because then I get TLS / HTTPS (which is required for PassKit in production) for free, because I'm cheap. To that extent, I also use Heroku's free PostgreSQL plan and the [free dyno hours](https://devcenter.heroku.com/articles/free-dyno-hours). 114 | 115 | A sleeping-when-idle Heroku app is *perfect* for Wallet services because an iOS device will call your service endpoints in the background and retry upon timeout. 116 | 117 | Your app service service is only woken... 118 | 119 | 1. when someone adds a pass (triggering a pass registration). 120 | 2. when someone deletes a pass (triggering pass de-registration). 121 | 3. when someone triggers a manual refresh of a pass. 122 | 4. when someone toggles "Automatic Updates" on the backside of a pass (shown with the ⓘ button). 123 | 124 | ## License 125 | 126 | Passcards is available under the MIT license. See the LICENSE file for more info. 127 | -------------------------------------------------------------------------------- /Resources/Views/base.leaf: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | #import("head") 5 | 6 | 7 | #import("body") 8 | 9 | 10 | -------------------------------------------------------------------------------- /Resources/Views/github.leaf: -------------------------------------------------------------------------------- 1 | #raw() { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /Resources/Views/welcome.leaf: -------------------------------------------------------------------------------- 1 | #extend("base") 2 | 3 | #export("head") { 4 |