summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--index.js2
-rw-r--r--lib/okpush/.gitignore3
-rw-r--r--lib/okpush/apn.js57
-rw-r--r--lib/okpush/db.js132
-rw-r--r--lib/okpush/index.js120
-rw-r--r--lib/okpush/package.json21
-rw-r--r--lib/okpush/public/push.js31
-rw-r--r--lib/okpush/templates/index.liquid74
-rw-r--r--lib/okpush/templates/partials/flash.liquid20
-rw-r--r--lib/okpush/templates/partials/head.liquid13
-rw-r--r--lib/okpush/templates/partials/inputs.liquid395
-rw-r--r--lib/okpush/templates/partials/tail.liquid14
-rw-r--r--lib/okpush/test_certs/CertificateSigningRequest.certSigningRequest16
-rw-r--r--lib/okpush/test_certs/Certificates.p12bin0 -> 3209 bytes
-rw-r--r--lib/okpush/test_certs/aps_development.cerbin0 -> 1421 bytes
-rw-r--r--lib/okpush/test_certs/overlayer_dev_cert.pem32
-rw-r--r--lib/okpush/test_certs/overlayer_dev_key.pem68
17 files changed, 997 insertions, 1 deletions
diff --git a/index.js b/index.js
index 9c0474ea..2fad7abe 100644
--- a/index.js
+++ b/index.js
@@ -87,7 +87,7 @@ var app = okcms.createApp({
gateway: "gateway.sandbox.push.apple.com",
}
},
- bundleId: "us.okfoc.overlayer",
+ bundleId: "us.okfoc.stoneisland",
notifications: {
// expiry (in seconds)
// badge (int)
diff --git a/lib/okpush/.gitignore b/lib/okpush/.gitignore
new file mode 100644
index 00000000..e314c5fe
--- /dev/null
+++ b/lib/okpush/.gitignore
@@ -0,0 +1,3 @@
+
+node_modules
+
diff --git a/lib/okpush/apn.js b/lib/okpush/apn.js
new file mode 100644
index 00000000..b6909f6f
--- /dev/null
+++ b/lib/okpush/apn.js
@@ -0,0 +1,57 @@
+
+var apn = require('apn')
+var db = require('./db')
+var apnProvider, apnFeedback
+
+function init (config) {
+ config.apn.connection.key = config.apn.key
+ config.apn.connection.cert = config.apn.cert
+ apnProvider = new apn.Provider(config.apn.connection)
+}
+
+function push (note) {
+ db.getAllDevices(function(err, tokens){
+ if (err) {
+ console.error("Error fetching devices:", err)
+ return
+ }
+ connection.send(note, tokens).then( function (response) {
+ // response.sent.forEach( function (token) {
+ // notificationSent(user, token)
+ // })
+ response.failed.forEach( function (failure) {
+ if (failure.error) {
+ // A transport-level error occurred (e.g. network problem)
+ // notificationError(user, token, failure.error);
+ } else if (failure.status == 410) {
+ // `failure.status` is the HTTP status code
+ // `failure.response` is the JSON payload
+ // notificationFailed(token, failure.status, failure.response);
+ db.removeDevice(token)
+ }
+ })
+ })
+ })
+}
+
+function buildPayload (options, bundleId) {
+ var note = new apn.Notification()
+ note.topic = bundleId
+ if (options.expiry)
+ note.expiry = Math.floor(Date.now() / 1000) + options.expiry
+ if (options.alert)
+ note.alert = options.alert
+ if (options.badge)
+ note.badge = options.badge
+ if (options.payload)
+ note.payload = options.payload
+ if (options.sound)
+ note.sound = options.sound
+ return note
+}
+
+module.exports = {
+ init: init,
+ push: push,
+ buildPayload: buildPayload
+}
diff --git a/lib/okpush/db.js b/lib/okpush/db.js
new file mode 100644
index 00000000..844bccae
--- /dev/null
+++ b/lib/okpush/db.js
@@ -0,0 +1,132 @@
+var mongoose = require('mongoose')
+var findOrCreate = require('mongoose-findorcreate')
+var _ = require('lodash')
+var db, PushToken, Notification, Channel
+
+mongoose.Promise = require('bluebird')
+
+function init (config) {
+ db = mongoose.connect(config.mongodbUrl)
+ mongoose.connection.on('error', errorHandler)
+
+ var pushTokenSchema = new db.Schema({
+ type: {
+ type: 'String',
+ required: true,
+ lowercase: true,
+ enum: ['ios', 'android'],
+ },
+ token: {
+ type: 'String',
+ required: true,
+ },
+ channel: {
+ type: 'String',
+ required: true,
+ },
+ })
+
+ var channelSchema = new db.Schema({
+ channel: {
+ type: 'String',
+ required: true,
+ lowercase: true,
+ },
+ last_push: {
+ type: 'Date',
+ required: true,
+ }
+ })
+ channelSchema.plugin(findOrCreate);
+
+ var notificationSchema = new db.Schema({
+ channel: {
+ type: 'String',
+ required: true,
+ lowercase: true,
+ },
+ date: {
+ type: 'Date',
+ required: true,
+ }
+ })
+
+ PushToken = db.model('PushToken', pushTokenSchema)
+ Notification = db.model('Notification', notificationSchema)
+ Channel = db.model('Channel', channelSchema)
+}
+
+function errorHandler (error) {
+ console.error('ERROR: ' + error)
+}
+
+/* devices / tokens */
+
+function addToken (deviceType, token, channel) {
+ var pushItem = new PushToken({
+ type: deviceType,
+ token: token,
+ channel: channel
+ })
+ pushItem.save()
+}
+function getAllTokens (channel, cb) {
+ PushToken.find({ channel: channel }, function (err, items) {
+ if (err) return cb(err, null)
+ var items = _.map(items, function (item) {
+ return _.pick(item, ['type', 'token'])
+ })
+ return cb(null, items)
+ })
+}
+function removeToken (token, channel) {
+ PushToken.find({ token: token, channel: channel }).remove().exec()
+}
+function getDeviceCount (channel, cb) {
+ PushToken.count({ channel: channel }, cb)
+}
+
+/* notifications */
+
+function addNotification (channel, cb) {
+ var now = new Date
+ Channel.findOrCreate({channel: channel}, {last_push: now}, function(err, note, created) {
+ if (err) {
+ console.error("Error finding/creating notification", err)
+ cb(err, false)
+ return
+ }
+ else if (! created) {
+ note.last_push = now
+ note.save()
+ }
+ cb(null, note)
+ })
+ new Notification ({
+ channel: channel,
+ date: now,
+ }).save()
+}
+function getNotifications (cb) {
+ Notification.find(function (err, items) {
+ if (err) return cb(err, null)
+
+ var items = _.map(items, function (item) {
+ return _.pick(item, ['channel', 'last_push'])
+ })
+
+ return cb(null, items)
+ })
+}
+
+/* wrap functions for some reason */
+
+module.exports = {
+ init: init,
+ addToken: addToken,
+ removeToken: removeToken,
+ getAllTokens: getAllTokens,
+ getDeviceCount: getDeviceCount,
+ addNotification: addNotification,
+ getNotifications: getNotifications,
+} \ No newline at end of file
diff --git a/lib/okpush/index.js b/lib/okpush/index.js
new file mode 100644
index 00000000..a9ba12eb
--- /dev/null
+++ b/lib/okpush/index.js
@@ -0,0 +1,120 @@
+/**
+ * OKPush - Handles basic broadcast push notifications, as well as keeping track of tokens.
+ */
+
+var path = require('path')
+var passport = require('passport')
+var DigestStrategy = require('passport-http').DigestStrategy;
+var bodyParser = require('body-parser')
+var OKTemplate = require('../../../app/node_modules/oktemplate')
+var apn = require('./apn')
+var db = require('./db')
+
+passport.use(new DigestStrategy({qop: 'auth'}, function authenticate(username, done) {
+ if (!process.env.OK_USER || !process.env.OK_PASS) {
+ return done(new Error('No user or pass configured on server'))
+ } else {
+ return done(null, process.env.OK_USER, process.env.OK_PASS)
+ }
+}))
+
+function OKPush (options) {
+ if (!(this instanceof OKPush)) return new OKPush(options)
+ options = options || {}
+ if (!options.express)
+ throw new Error('Express not provided to OKPush')
+ if (!options.config)
+ throw new Error('Configuration not provided to OKPush')
+ if (!options.config.notifications)
+ throw new Error('Notifications not provided to OKPush')
+ if (!options.config.bundleId)
+ throw new Error('bundleId not provided to OKPush')
+ if (!options.config.mongodbUrl)
+ throw new Error('mongodbUrl not provided to OKPush')
+
+ var express = options.express
+ var router = express.Router()
+ var config = options.config
+ var meta = options.meta
+ var error = options.errorHandler
+ // var okcms_db = options.db
+
+ var templateProvider = this._templateProvider = new OKTemplate({
+ root: path.join(__dirname, './templates'),
+ debug: meta.debug
+ })
+
+ var templates = {}
+ templates['index'] = templateProvider.getTemplate('index')
+
+ apn.init(config)
+ db.init(config)
+
+ router.use('/admin/', passport.initialize())
+ router.use('/public/', express.static(path.join(__dirname, './public')));
+
+ // monkeypatch because of app.use(router) .. obnoxious
+ router.all('/admin/(:path*)?', function (req, res, next) {
+ req.newUrl = req.url
+ req.url = req.originalUrl
+ next()
+ })
+ router.all('/admin/(:path*)?', passport.authenticate('digest', {
+ session: false
+ }))
+ router.all('/admin/(:path*)?', function (req, res, next) {
+ req.url = req.newUrl
+ next()
+ })
+
+ // pass in admin middleware!
+ router.get('/admin', function (req, res) {
+ db.getNotifications(function(err, notes){
+ db.getDeviceCount(function(err, count){
+ var data = {
+ meta: meta,
+ notifications: config.notifications,
+ device_count: count,
+ }
+ notes.forEach(function(note){
+ if (note.key in data.notifications) {
+ data.notifications[ note.key ].last_push = note.last_push
+ }
+ })
+ templates['index'].render(data).then(function(rendered) {
+ res.send(rendered);
+ }).fail(error(req, res, 500))
+ })
+ })
+ })
+
+ router.post('/send', bodyParser.urlencoded({ extended: false }), function (req, res) {
+ var key = req.body.key
+ var opt = options.config.notifications[key]
+ var note = apn.buildPayload(opt, options.config.bundleId)
+ // apn.push(note)
+ db.updateNotification(key, function(){
+ res.send(200)
+ })
+ })
+
+ // should work without middleware
+ router.post('/add', function (req, res) {
+ // add a key
+ db.
+ registrationId: localStorage.getItem("yoox.registrationId"),
+ channel: channel,
+ platform,
+ })
+ router.post('/remove', function (req, res) {
+ // remove a key
+ })
+
+ this._router = router
+}
+
+OKPush.prototype.middleware = function () {
+ return this._router
+}
+
+module.exports = OKPush
diff --git a/lib/okpush/package.json b/lib/okpush/package.json
new file mode 100644
index 00000000..87ca92ca
--- /dev/null
+++ b/lib/okpush/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "okpush",
+ "version": "1.0.0",
+ "description": "push notification service",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "okfocus <frontdesk@okfoc.us>",
+ "license": "LNT",
+ "dependencies": {
+ "apn": "^2.1.1",
+ "bluebird": "^3.4.6",
+ "body-parser": "^1.15.2",
+ "lodash": "^4.16.3",
+ "mongoose": "^4.6.2",
+ "mongoose-findorcreate": "^0.1.2",
+ "passport": "^0.3.2",
+ "passport-http": "^0.3.0"
+ }
+}
diff --git a/lib/okpush/public/push.js b/lib/okpush/public/push.js
new file mode 100644
index 00000000..d369c903
--- /dev/null
+++ b/lib/okpush/public/push.js
@@ -0,0 +1,31 @@
+$(function(){
+ var count = $(".device-count").data("count");
+ var confirm_msg = "This will send the notification {{key}} to {{count}} people. Click OK to confirm.";
+ $(".notifications button").click(function(){
+ var $el = $(this)
+ var data = $el.data()
+ var msg = confirm_msg.replace("{{key}}", data.key).replace("{{count}}", count)
+ if (! confirm(msg)) return
+ $.ajax({
+ type: "POST",
+ url: "/_services/push/send",
+ data: { key: data.key },
+ success: function(){
+ alert("Push notification sent.")
+ var now = new Date()
+ // "%a %d-%b-%Y %H:%M"
+ var months = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ")
+ var days = "Sun Mon Tue Wed Thu Fri Sat".split(" ")
+ var date = days[ now.getDay() ]
+ date += " " + now.getDate()
+ date += "-" + months[now.getMonth()]
+ date += "-" + now.getFullYear()
+ date += " " + now.getHours()
+ var mins = now.getMinutes()
+ if (mins < 10) mins = "0" + mins
+ date += ":" + mins
+ $el.closest("tr").find(".notification-date").html(date)
+ }
+ })
+ })
+}) \ No newline at end of file
diff --git a/lib/okpush/templates/index.liquid b/lib/okpush/templates/index.liquid
new file mode 100644
index 00000000..10772b55
--- /dev/null
+++ b/lib/okpush/templates/index.liquid
@@ -0,0 +1,74 @@
+{% include 'partials/head' %}
+
+{% include 'partials/flash' %}
+
+<style>
+th {
+ min-width: 100px;
+ text-align: left;
+ background: #ddd;
+}
+th:nth-child(2) {
+ min-width: 300px;
+}
+th:nth-child(3) {
+ min-width: 170px;
+}
+table,tr,th,td {
+ margin: 0;
+ padding: 0;
+}
+th,td {
+ padding: 5px;
+}
+tr:nth-child(2n+1) {
+ background: #f8f8f8;
+}
+</style>
+
+<nav class="resource-nav">
+ <a href="/admin/" class="btn">Back</a>
+</nav>
+
+<section class="main resource">
+
+ <h2>Push Notifications</h2>
+
+ <div class="device-count" data-count="{{ device_count }}">Device count: {{ device_count }}</div>
+
+ <table class="notifications" cellpadding="0" cellspacing="0">
+ <tr>
+ <th>Key</th>
+ <th>Message</th>
+ <th>Last Push</th>
+ <th></th>
+ </tr>
+ {% for pair in notifications %}
+ {% assign name = pair[0] %}
+ {% assign spec = pair[1] %}
+ <tr>
+ <td>
+ {{name | escape}}
+ </td>
+ <td>
+ {{spec.alert}}
+ </td>
+ <td class="notification-date">
+ {% unless spec.last_push %}
+ Never
+ {% else %}
+ {{ spec.last_push | date: "%a %d-%b-%Y %H:%M" }}
+ {% endunless %}
+ </td>
+ <td>
+ <button data-key="{{name}}" class="btn">send</button>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+
+</section>
+
+{% include 'partials/tail' %}
+
+<script src="public/push.js"></script> \ No newline at end of file
diff --git a/lib/okpush/templates/partials/flash.liquid b/lib/okpush/templates/partials/flash.liquid
new file mode 100644
index 00000000..e51a86b7
--- /dev/null
+++ b/lib/okpush/templates/partials/flash.liquid
@@ -0,0 +1,20 @@
+{% if success.length > 0 %}
+<div class="success">
+ <div class="message">Changes saved.</div>
+ <!--
+ {% for info in success %}
+ <div class="message">{{info.action}}</div>
+ {% endfor %}
+ -->
+</div>
+{% endif %}
+
+{% if errors.length > 0 %}
+<div class="errors">
+ {% for error in errors %}
+ <div class="error">
+ <div class="message">{{error.message}}</div>
+ </div>
+ {% endfor %}
+</div>
+{% endif %} \ No newline at end of file
diff --git a/lib/okpush/templates/partials/head.liquid b/lib/okpush/templates/partials/head.liquid
new file mode 100644
index 00000000..e9c27dc0
--- /dev/null
+++ b/lib/okpush/templates/partials/head.liquid
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf8">
+ <title>{{meta.project}} Admin</title>
+ <link rel="stylesheet" href="{{meta.static}}/css/main.css">
+ </head>
+ <body>
+ <header class="admin-header">
+ <span class="breadcrumb"><b>{{meta.project}}</b> Admin</span>
+ <a class="site-link" href="/">View Site</a>
+ </header>
+ <div class="container"> \ No newline at end of file
diff --git a/lib/okpush/templates/partials/inputs.liquid b/lib/okpush/templates/partials/inputs.liquid
new file mode 100644
index 00000000..60466de9
--- /dev/null
+++ b/lib/okpush/templates/partials/inputs.liquid
@@ -0,0 +1,395 @@
+{% for pair in resource.spec %}
+ {% assign name = pair[0] %}
+ {% assign spec = pair[1] %}
+ {% assign type = spec.type %}
+
+ <div class="property {{type}} {% if spec.hidden %}hidden{% endif %}"
+ data-name="{{name}}">
+ <label for="{{name}}">{{name | capitalize}}</label>
+
+ {% if type == 'string' %}
+ <input
+ name="{{name}}" type="text" value="{{spec.value | escape}}">
+ {% elsif type == 'text' %}
+ <textarea
+ name="{{name}}">{{spec.value | escape}}</textarea>
+ {% elsif type == 'number' %}
+ <input
+ type="number"
+ name="{{name}}" value="{{spec.value | escape}}">
+ {% elsif type == 'enum' or type == 'foreign-key' %}
+ <select
+ name="{{name}}">
+ {% for option in spec.options %}
+ {% if option.length == 2 %}
+ <option value="{{option[0]}}" {% if option[0] == spec.value %}selected{% endif %}>{{option[1] | capitalize}}</option>
+ {% else %}
+ <option value="{{option}}" {% if option == spec.value %}selected{% endif %}>{{option | capitalize}}</option>
+ {% endif %}
+ {% endfor %}
+ </select>
+ {% elsif type == 'video' %}
+ <div class="video group {% if spec.value.url %}loaded{% endif %}">
+ <input name="{{name}}[url]" type="text" value="{{spec.value.url}}" class="url" placeholder="Enter a video URL">
+ <input name="{{name}}[type]" type="hidden" value="{{spec.value.type}}" class="video-type" hidden>
+ <input name="{{name}}[token]" type="hidden" value="{{spec.value.token}}" class="video-token" hidden>
+ <input name="{{name}}[width]" value="{{spec.value.width}}" type="hidden" class="video-width" hidden>
+ <input name="{{name}}[height]" value="{{spec.value.height}}" type="hidden" class="video-height" hidden>
+ <label>Title</label>
+ <input name="{{name}}[title]" type="text" value="{{spec.value.title | escape}}" class="video-title">
+ <label>Thumbnail</label>
+ <input name="{{name}}[thumb]" type="text" value="{{spec.value.thumb | escape}}" class="video-thumb">
+ </div>
+ {% elsif type == 'image' %}
+ <div class="image group {% if spec.value.uri %}loaded{% endif %}">
+ <div class="fields">
+ <div class="add-image-button">
+ <input type="file" accept="image/*">
+ <button>+ Add image</button>
+ </div>
+ <input class="add-url" type="text" placeholder="+ Add URL">
+ </div>
+ <div class="image-element">
+ <input class="uri" type="hidden" name="{{name}}[uri]" value="{{spec.value.uri}}">
+ <textarea class="caption" name="{{name}}[caption]">{{spec.value.caption | escape}}</textarea>
+ <input type="hidden" name="{{name}}[width]" value="{{spec.value.width}}" class="image-width">
+ <input type="hidden" name="{{name}}[height]" value="{{spec.value.height}}" class="image-height">
+ <img src="{{spec.value.uri}}" alt="{{spec.value.caption | escape}}">
+ <button class="remove">x</button>
+ </div>
+ </div>
+
+ {% elsif type == 'date' %}
+
+ <div class="date">
+ <input name="{{name}}"
+ type="date"
+ {% if spec.value %}
+ value="{{spec.value | date: '%Y-%m-%d'}}"
+ {% endif %}
+ >
+ </div>
+
+ {% elsif type == 'flag' %}
+
+ <div class="flag">
+ <input name="{{name}}"
+ type="checkbox"
+ {% if spec.value %}
+ checked="true"
+ {% endif %}
+ value="true">
+ </div>
+
+ {% elsif type == 'tag-list' %}
+ <div class="tag-list">
+ <input name="{{name}}"
+ value="{{spec.value | escape}}"
+ placeholder="Enter a comma separated list of tags.">
+ </div>
+
+ {% elsif type == 'link-list' %}
+ <div class="link-list group">
+ <ol class="links">
+ {% for link in spec.value %}
+ <li>
+ <div class="handle"></div>
+ <input
+ name="{{name}}[{{forloop.index0}}][text]"
+ value="{{link.text | escape}}"
+ type="text"
+ class="link-input link-text">
+ <input
+ name="{{name}}[{{forloop.index0}}][uri]"
+ value="{{link.uri | escape}}"
+ type="text"
+ class="link-input link-uri">
+ <button class="remove-link-btn btn">
+ -
+ </button>
+ </li>
+ {% endfor %}
+ </ol>
+
+ <div class="handle"></div>
+ <input type="text"
+ class="link-input-new link-text"
+ placeholder="{% if spec.textLabel %}{{ spec.textLabel }}{% else %}Link text{% endif %}">
+ <input type="text"
+ class="link-input-new link-uri"
+ placeholder="{% if spec.linkLabel %}{{ spec.linkLabel }}{% else %}http://www.example.com{% endif %}">
+ <button class="add-link-btn btn">+</button>
+
+ <script type="text/html" class="link-template">
+ <li>
+ <div class="handle"></div>
+ <input
+ name="{{name}}[][text]"
+ value=""
+ type="text"
+ placeholder="{% if spec.textLabel %}{{ spec.textLabel }}{% else %}Link text{% endif %}"
+ class="link-input link-text">
+ <input
+ name="{{name}}[][uri]"
+ value=""
+ type="text"
+ placeholder="{% if spec.linkLabel %}{{ spec.linkLabel }}{% else %}http://www.example.com{% endif %}"
+ class="link-input link-uri">
+ <button class="remove-link-btn btn">
+ -
+ </button>
+ </li>
+ </script>
+
+ </div>
+
+ {% elsif type == 'media-list' or type == 'media' %}
+ <div class="media-list group loaded">
+ <div class="fields">
+ <div class="add-image-button">
+ <input type="file" multiple>
+ <button>+ Add media</button>
+ </div>
+ <input class="add-url" type="text" placeholder="+ Add Image/Video/Link URL">
+ </div>
+
+ <script type="text/html" class="image-template">
+ <li class="image-element">
+ <label>Caption</label>
+ <input class="uri" type="hidden" name="{{name}}[][uri]" value="">
+ <textarea class="caption" name="{{name}}[][caption]"></textarea>
+ <input type="hidden" name="{{name}}[][type]" value="image">
+ <input type="hidden" name="{{name}}[][width]" class="image-width" hidden>
+ <input type="hidden" name="{{name}}[][height]" class="image-height" hidden>
+ <img>
+ <button class="remove">x</button>
+ </li>
+ </script>
+
+ <script type="text/html" class="video-template">
+ <li class="video-element">
+ <div style="float: left">
+ <input name="{{name}}[][type]" type="hidden" class="video-type" hidden>
+ <input name="{{name}}[][token]" type="hidden" class="video-token" hidden>
+ <input name="{{name}}[][uri]" type="hidden" class="video-uri" hidden>
+ <input name="{{name}}[][width]" type="hidden" class="video-width" hidden>
+ <input name="{{name}}[][height]" type="hidden" class="video-height" hidden>
+ <label>Caption</label>
+ <input name="{{name}}[][title]" type="text" class="video-title">
+ <label>Thumbnail</label>
+ <input name="{{name}}[][thumb]" type="text" class="video-thumb">
+ <span class="checkboxes">
+ <input name="{{name}}[][autoplay]" value="{{image.autoplay}}" type="checkbox" class="video-autoplay flag">
+ <label>Autoplay</label>
+ <input name="{{name}}[][loop]" value="{{image.loop}}" type="checkbox" class="video-loop flag">
+ <label>Loop</label>
+ </span>
+ </div>
+ <img>
+ <button class="remove">x</button>
+ </li>
+ </script>
+
+ <script type="text/html" class="audio-template">
+ <li class="audio-element">
+ <div style="float: left">
+ <input name="{{name}}[][type]" type="hidden" class="audio-type" hidden>
+ <input name="{{name}}[][token]" type="hidden" class="audio-token" hidden>
+ <input name="{{name}}[][uri]" type="hidden" class="audio-uri" hidden>
+ <input name="{{name}}[][duration]" value="{{image.duration}}" type="hidden" class="audio-duration" hidden>
+ <label>Caption</label>
+ <input name="{{name}}[][title]" type="text" class="audio-title">
+ <label>Thumbnail</label>
+ <input name="{{name}}[][thumb]" type="text" class="audio-thumb">
+ </div>
+ <img>
+ <button class="remove">x</button>
+ </li>
+ </script>
+
+ <script type="text/html" class="link-template">
+ <li class="link-element">
+ <input class="uri" type="text" name="{{name}}[][uri]" value="">
+ <textarea class="caption" name="{{name}}[][caption]" placeholder="Caption"></textarea>
+ <input type="hidden" name="{{name}}[][type]" value="link">
+ <button class="remove">x</button>
+ </li>
+ </script>
+
+ <ol>
+ {% for image in spec.value %}
+ {% if image.type and (image.type == "vimeo" or image.type == "youtube" or image.type == "video") %}
+ <li class="video-element">
+ <div style="float: left">
+ <input name="{{name}}[{{forloop.index0}}][type]" value="{{image.type}}" type="hidden" class="video-type" hidden>
+ <input name="{{name}}[{{forloop.index0}}][token]" value="{{image.token}}" type="hidden" class="video-token" hidden>
+ <input name="{{name}}[{{forloop.index0}}][uri]" value="{{image.uri}}" type="hidden" class="video-uri" hidden>
+ <input name="{{name}}[{{forloop.index0}}][width]" value="{{image.width}}" type="hidden" class="video-width" hidden>
+ <input name="{{name}}[{{forloop.index0}}][height]" value="{{image.height}}" type="hidden" class="video-height" hidden>
+ <label>Caption</label>
+ <input name="{{name}}[{{forloop.index0}}][title]" value="{{image.title | escape}}" type="text" class="video-title">
+ <label>Thumbnail</label>
+ <input name="{{name}}[{{forloop.index0}}][thumb]" value="{{image.thumb}}" type="text" class="video-thumb">
+ <span class="checkboxes">
+ <input name="{{name}}[{{forloop.index0}}][autoplay]" value="true" {% if image.autoplay == "true" %}checked="true"{% endif %} type="checkbox" class="flag video-autoplay">
+ <label>Autoplay</label>
+ <input name="{{name}}[{{forloop.index0}}][loop]" value="true" {% if image.loop == "true" %}checked="true"{% endif %} type="checkbox" class="flag video-loop">
+ <label>Loop</label>
+ </span>
+ </div>
+ <img src="{{image.thumb}}">
+ <button class="remove">x</button>
+ </li>
+ {% elsif image.type and (image.type == "audio" or image.type == "soundcloud") %}
+ <li class="audio-element">
+ <div style="float: left">
+ <input name="{{name}}[{{forloop.index0}}][type]" value="{{image.type}}" type="hidden" class="audio-type" hidden>
+ <input name="{{name}}[{{forloop.index0}}][token]" value="{{image.token}}" type="hidden" class="audio-token" hidden>
+ <input name="{{name}}[{{forloop.index0}}][uri]" value="{{image.uri}}" type="hidden" class="audio-uri" hidden>
+ <input name="{{name}}[{{forloop.index0}}][duration]" value="{{image.duration}}" type="hidden" class="audio-duration" hidden>
+ <label>Caption</label>
+ <input name="{{name}}[{{forloop.index0}}][title]" value="{{image.title | escape}}" type="text" class="audio-title">
+ <label>Thumbnail</label>
+ <input name="{{name}}[{{forloop.index0}}][thumb]" value="{{image.thumb}}" type="text" class="audio-thumb">
+ </div>
+ <img src="{{image.thumb}}">
+ <button class="remove">x</button>
+ </li>
+ {% elsif image.type and image.type == "link" %}
+ <li class="link-element">
+ <input class="uri" type="text" name="{{name}}[{{forloop.index0}}][uri]" value="{{image.uri}}">
+ <textarea class="caption" name="{{name}}[{{forloop.index0}}][caption]" placeholder="Caption">{{image.caption | escape}}</textarea>
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][type]" value="link">
+ <button class="remove">x</button>
+ </li>
+ {% else %}
+ <li class="image-element">
+ <label>Caption</label>
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][uri]" value="{{image.uri}}">
+ <input name="{{name}}[{{forloop.index0}}][width]" value="{{image.width}}" type="hidden" class="image-width" hidden>
+ <input name="{{name}}[{{forloop.index0}}][height]" value="{{image.height}}" type="hidden" class="image-height" hidden>
+ <textarea class="caption" name="{{name}}[{{forloop.index0}}][caption]">{{image.caption | escape}}</textarea>
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][type]" value="image">
+ <img src="{{image.uri}}" alt="{{image.caption | strip_html}}">
+ <button class="remove">x</button>
+ </li>
+ {% endif %}
+ {% endfor %}
+ </ol>
+ </div>
+ {% elsif type == 'captioned-image-list' or type == 'gallery' %}
+ <div class="image-list group loaded">
+ <div class="fields">
+ <div class="add-image-button">
+ <input type="file" accept="image/*" multiple>
+ <button>+ Add images</button>
+ </div>
+ <input class="add-url" type="text" placeholder="+ Add URL">
+ </div>
+
+ <script type="text/html" class="image-template">
+ <li class="image-element">
+ <input class="uri" type="hidden" name="{{name}}[][uri]" value="">
+ <input type="hidden" name="{{name}}[][width]" class="image-width" hidden>
+ <input type="hidden" name="{{name}}[][height]" class="image-height" hidden>
+ <textarea class="caption" name="{{name}}[][caption]"></textarea>
+ <img>
+ <button class="remove">x</button>
+ </li>
+ </script>
+
+ <ol>
+ {% for image in spec.value %}
+ <li class="image-element">
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][uri]" value="{{image.uri}}">
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][width]" value="{{image.width}}" class="image-width">
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][height]" value="{{image.height}}" class="image-height">
+ <textarea class="caption" name="{{name}}[{{forloop.index0}}][caption]">{{image.caption | escape}}</textarea>
+ <img src="{{image.uri}}" alt="{{image.caption | strip_html}}">
+ <button class="remove">x</button>
+ </li>
+ {% endfor %}
+ </ol>
+ </div>
+ {% elsif type == 'double-captioned-image-list' %}
+ <div class="image-list group loaded">
+ <div class="fields">
+ <div class="add-image-button">
+ <input type="file" accept="image/*" multiple>
+ <button>+ Add images</button>
+ </div>
+ <input class="add-url" type="text" placeholder="+ Add URL">
+ </div>
+
+ <script type="text/html" class="image-template">
+ <li class="image-element">
+ <img>
+ <button class="remove">x</button>
+ <input class="uri" type="hidden" name="{{name}}[][uri]" value="">
+ <input type="hidden" name="{{name}}[][width]" class="image-width">
+ <input type="hidden" name="{{name}}[][height]" class="image-height">
+ <input class="caption" name="{{name}}[][label]" placeholder="Name">
+ <input class="caption" name="{{name}}[][caption]" placeholder="Email">
+ </li>
+ </script>
+
+ <ol>
+ {% for image in spec.value %}
+ <li class="image-element">
+ <img src="{{image.uri}}" alt="{{image.caption | strip_html}}">
+ <button class="remove">x</button>
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][uri]" value="{{image.uri}}">
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][width]" value="{{image.width}}" class="image-width">
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][height]" value="{{image.height}}" class="image-height">
+ <input class="caption" name="{{name}}[{{forloop.index0}}][label]" value="{{image.label | escape}}" placeholder="Name">
+ <input class="caption" name="{{name}}[{{forloop.index0}}][caption]" value="{{image.caption | escape}}" placeholder="Email">
+ </li>
+ {% endfor %}
+ </ol>
+ </div>
+ {% elsif type == 'triple-captioned-image-list' %}
+ <div class="image-list group loaded">
+ <div class="fields">
+ <div class="add-image-button">
+ <input type="file" accept="image/*" multiple>
+ <button>+ Add images</button>
+ </div>
+ <input class="add-url" type="text" placeholder="+ Add URL">
+ </div>
+
+ <script type="text/html" class="image-template">
+ <li class="image-element">
+ <img>
+ <button class="remove">x</button>
+ <input class="uri" type="hidden" name="{{name}}[][uri]" value="">
+ <input type="hidden" name="{{name}}[][width]" class="image-width">
+ <input type="hidden" name="{{name}}[][height]" class="image-height">
+ <input class="caption" name="{{name}}[][label]" placeholder="Label">
+ <input class="caption" name="{{name}}[][caption]" placeholder="Caption">
+ <input class="caption" name="{{name}}[][code]" placeholder="Code">
+ </li>
+ </script>
+
+ <ol>
+ {% for image in spec.value %}
+ <li class="image-element">
+ <img src="{{image.uri}}" alt="{{image.caption | strip_html}}">
+ <button class="remove">x</button>
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][uri]" value="{{image.uri}}">
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][width]" value="{{image.width}}" class="image-width">
+ <input type="hidden" name="{{name}}[{{forloop.index0}}][height]" value="{{image.height}}" class="image-height">
+ <input class="caption" name="{{name}}[{{forloop.index0}}][label]" value="{{image.label | escape}}" placeholder="Label">
+ <input class="caption" name="{{name}}[{{forloop.index0}}][caption]" value="{{image.caption | escape}}" placeholder="Caption">
+ <input class="caption" name="{{name}}[{{forloop.index0}}][code]" value="{{image.code | escape}}" placeholder="Code">
+ </li>
+ {% endfor %}
+ </ol>
+ </div>
+ {% elsif type == 'meta' %}
+ <input class="hidden" type="hidden" name="{{name}}" value="{{spec.value}}">
+ {% else %}
+ <p><pre style="color: red">Admin template doesn't support '{{type}}' properties!</pre></p>
+ {% endif %}
+ </div>
+
+{% endfor %}
diff --git a/lib/okpush/templates/partials/tail.liquid b/lib/okpush/templates/partials/tail.liquid
new file mode 100644
index 00000000..522023b5
--- /dev/null
+++ b/lib/okpush/templates/partials/tail.liquid
@@ -0,0 +1,14 @@
+ </div> {% comment %} closes container tag {% endcomment %}
+ <div id="progress"></div>
+ <div id="uploadConfig"
+ data-image-maxbytes="{{meta.services.s3.image.maxbytes}}"
+ data-audio-maxbytes="{{meta.services.s3.audio.maxbytes}}"
+ data-video-maxbytes="{{meta.services.s3.video.maxbytes}}"></div>
+ </body>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
+ <script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.6.0/lodash.min.js"></script>
+ <script src="/admin/js/jqueryui-draggable.js"></script>
+ <script src="/admin/js/upload.js"></script>
+ <script src="/admin/js/parser.js"></script>
+ <script src="/admin/js/app.js"></script>
+</html>
diff --git a/lib/okpush/test_certs/CertificateSigningRequest.certSigningRequest b/lib/okpush/test_certs/CertificateSigningRequest.certSigningRequest
new file mode 100644
index 00000000..3a4074ae
--- /dev/null
+++ b/lib/okpush/test_certs/CertificateSigningRequest.certSigningRequest
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICkjCCAXoCAQAwTTEdMBsGCSqGSIb3DQEJARYOanVsZXNAb2tmb2MudXMxHzAd
+BgNVBAMMFkp1bGlhbiBMYXBsYWNlIERFViBLRVkxCzAJBgNVBAYTAlVTMIIBIjAN
+BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArrpXz8B0q/tJ2+N6gjK2zLNPIIlA
+lzrE46TIkY10xo9jW3vBRgZL9V5xKtmCWf3KfR2I7KioyqqnZvz6++TV/M3rZptJ
+XN8yMmG4mjTqgC1V2SubYyUZqpqkfyLK1ePimsg09UUNxwNcIyBJun+f8wXqz7HK
+Kieiq+d7oTs+WJbWdkIftxe7hjL2n93PI1LLCF15Cx8s7XP7R7QsTGMvHUzYYHiL
+4bc+r54jpZ28onVAwdYx+p3WkKzdFGjkrIl++IXmyJr0E7jilfrKwMWi4tvCKgXV
+KH6VmWLIKOtfKVMdIK0WQ9ij3kRHfEwgN/djC3M2UWFEGLx1d7FuL5c+nQIDAQAB
+oAAwDQYJKoZIhvcNAQELBQADggEBAIGOwvuW7HaOb1h+nkpIDTHNRN6Lw3MTg8nU
+68skeSFatC8lC4e//Gsl1z8iyR7amhoMrZ29f4Tgs5w8NFgcVmzdp5CL126WA3qV
+Qu+xZxhgwUnLRxtpDUVtTdJGaYNmowpm4mqs5YFg6WCIhWg+kuEJJ8MSOkYHtyAx
+HIpbfjeSW69VGelr+vGZ6Jf07UCquwJCJR4WmsIyruJhvf2CQMPsT3bXKrhu/sWg
+WyjjCjXVLeDOO/tiRMnPteCcDZPsXKyi9PPkSF4u591YoUL0UxjlzlignBT4FSyq
+s8kdmrEHWIW1BAscmNVny2YTuGjbFos/GBSMDeJvoXdvPS2kRiQ=
+-----END CERTIFICATE REQUEST-----
diff --git a/lib/okpush/test_certs/Certificates.p12 b/lib/okpush/test_certs/Certificates.p12
new file mode 100644
index 00000000..6e61158f
--- /dev/null
+++ b/lib/okpush/test_certs/Certificates.p12
Binary files differ
diff --git a/lib/okpush/test_certs/aps_development.cer b/lib/okpush/test_certs/aps_development.cer
new file mode 100644
index 00000000..8d55dd44
--- /dev/null
+++ b/lib/okpush/test_certs/aps_development.cer
Binary files differ
diff --git a/lib/okpush/test_certs/overlayer_dev_cert.pem b/lib/okpush/test_certs/overlayer_dev_cert.pem
new file mode 100644
index 00000000..3758dd23
--- /dev/null
+++ b/lib/okpush/test_certs/overlayer_dev_cert.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFiTCCBHGgAwIBAgIIaNpEjSUbbWwwDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3Js
+ZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3
+aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw
+HhcNMTYxMDA0MjAxMTM3WhcNMTcxMDA0MjAxMTM3WjCBiDEiMCAGCgmSJomT8ixk
+AQEMEnVzLm9rZm9jLm92ZXJsYXllcjFAMD4GA1UEAww3QXBwbGUgRGV2ZWxvcG1l
+bnQgSU9TIFB1c2ggU2VydmljZXM6IHVzLm9rZm9jLm92ZXJsYXllcjETMBEGA1UE
+CwwKNUVIOVc1WDVTSjELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQCuulfPwHSr+0nb43qCMrbMs08giUCXOsTjpMiRjXTGj2Nbe8FG
+Bkv1XnEq2YJZ/cp9HYjsqKjKqqdm/Pr75NX8zetmm0lc3zIyYbiaNOqALVXZK5tj
+JRmqmqR/IsrV4+KayDT1RQ3HA1wjIEm6f5/zBerPscoqJ6Kr53uhOz5YltZ2Qh+3
+F7uGMvaf3c8jUssIXXkLHyztc/tHtCxMYy8dTNhgeIvhtz6vniOlnbyidUDB1jH6
+ndaQrN0UaOSsiX74hebImvQTuOKV+srAxaLi28IqBdUofpWZYsgo618pUx0grRZD
+2KPeREd8TCA392MLczZRYUQYvHV3sW4vlz6dAgMBAAGjggHlMIIB4TAdBgNVHQ4E
+FgQUckSbISzH7PTE7R9gsIb/sxKzHrgwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBSI
+JxcJqbYYYIvs67r2R1nFUlSjtzCCAQ8GA1UdIASCAQYwggECMIH/BgkqhkiG92Nk
+BQEwgfEwgcMGCCsGAQUFBwICMIG2DIGzUmVsaWFuY2Ugb24gdGhpcyBjZXJ0aWZp
+Y2F0ZSBieSBhbnkgcGFydHkgYXNzdW1lcyBhY2NlcHRhbmNlIG9mIHRoZSB0aGVu
+IGFwcGxpY2FibGUgc3RhbmRhcmQgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YgdXNl
+LCBjZXJ0aWZpY2F0ZSBwb2xpY3kgYW5kIGNlcnRpZmljYXRpb24gcHJhY3RpY2Ug
+c3RhdGVtZW50cy4wKQYIKwYBBQUHAgEWHWh0dHA6Ly93d3cuYXBwbGUuY29tL2Fw
+cGxlY2EvME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9kZXZlbG9wZXIuYXBwbGUu
+Y29tL2NlcnRpZmljYXRpb25hdXRob3JpdHkvd3dkcmNhLmNybDALBgNVHQ8EBAMC
+B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwIwEAYKKoZIhvdjZAYDAQQCBQAwDQYJKoZI
+hvcNAQEFBQADggEBAEuQKbpqxQPnazN4A0NVB7YRr2M2Hk5DtV4M/QT8OFDY1IsA
+s4U8NmI9AMgoszIdLG3n4x2Y4mm6yTYDEMQpaJRbir9Fx6WcZQ4GB72iZ1M/1fkC
+79Yq17yzxM9Awf2igx1EkZhzy6Oq+7cUX9Jbz5IQrPyfXe5hpaOmY4jnWgSlaiwJ
+6bBhFHwhJY9ekAFOyRnZS7hQD4mLOBZLti/lH4Z2zadd21DbM/uhUvLJgYpIRplP
+hoFxV5bev5vv9A0dbYw6ERa3+aG8HHmP9N4u1/JxHIX2VlYzzLj1l7Tlps6QG6/r
+4CsJBIfpGjxwznrYJBN5ox1KlFxNLwD3UNpLhOQ=
+-----END CERTIFICATE-----
diff --git a/lib/okpush/test_certs/overlayer_dev_key.pem b/lib/okpush/test_certs/overlayer_dev_key.pem
new file mode 100644
index 00000000..30cb696d
--- /dev/null
+++ b/lib/okpush/test_certs/overlayer_dev_key.pem
@@ -0,0 +1,68 @@
+Bag Attributes
+ friendlyName: Apple Development IOS Push Services: us.okfoc.overlayer
+ localKeyID: 72 44 9B 21 2C C7 EC F4 C4 ED 1F 60 B0 86 FF B3 12 B3 1E B8
+subject=/UID=us.okfoc.overlayer/CN=Apple Development IOS Push Services: us.okfoc.overlayer/OU=5EH9W5X5SJ/C=US
+issuer=/C=US/O=Apple Inc./OU=Apple Worldwide Developer Relations/CN=Apple Worldwide Developer Relations Certification Authority
+-----BEGIN CERTIFICATE-----
+MIIFiTCCBHGgAwIBAgIIaNpEjSUbbWwwDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3Js
+ZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3
+aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw
+HhcNMTYxMDA0MjAxMTM3WhcNMTcxMDA0MjAxMTM3WjCBiDEiMCAGCgmSJomT8ixk
+AQEMEnVzLm9rZm9jLm92ZXJsYXllcjFAMD4GA1UEAww3QXBwbGUgRGV2ZWxvcG1l
+bnQgSU9TIFB1c2ggU2VydmljZXM6IHVzLm9rZm9jLm92ZXJsYXllcjETMBEGA1UE
+CwwKNUVIOVc1WDVTSjELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQCuulfPwHSr+0nb43qCMrbMs08giUCXOsTjpMiRjXTGj2Nbe8FG
+Bkv1XnEq2YJZ/cp9HYjsqKjKqqdm/Pr75NX8zetmm0lc3zIyYbiaNOqALVXZK5tj
+JRmqmqR/IsrV4+KayDT1RQ3HA1wjIEm6f5/zBerPscoqJ6Kr53uhOz5YltZ2Qh+3
+F7uGMvaf3c8jUssIXXkLHyztc/tHtCxMYy8dTNhgeIvhtz6vniOlnbyidUDB1jH6
+ndaQrN0UaOSsiX74hebImvQTuOKV+srAxaLi28IqBdUofpWZYsgo618pUx0grRZD
+2KPeREd8TCA392MLczZRYUQYvHV3sW4vlz6dAgMBAAGjggHlMIIB4TAdBgNVHQ4E
+FgQUckSbISzH7PTE7R9gsIb/sxKzHrgwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBSI
+JxcJqbYYYIvs67r2R1nFUlSjtzCCAQ8GA1UdIASCAQYwggECMIH/BgkqhkiG92Nk
+BQEwgfEwgcMGCCsGAQUFBwICMIG2DIGzUmVsaWFuY2Ugb24gdGhpcyBjZXJ0aWZp
+Y2F0ZSBieSBhbnkgcGFydHkgYXNzdW1lcyBhY2NlcHRhbmNlIG9mIHRoZSB0aGVu
+IGFwcGxpY2FibGUgc3RhbmRhcmQgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YgdXNl
+LCBjZXJ0aWZpY2F0ZSBwb2xpY3kgYW5kIGNlcnRpZmljYXRpb24gcHJhY3RpY2Ug
+c3RhdGVtZW50cy4wKQYIKwYBBQUHAgEWHWh0dHA6Ly93d3cuYXBwbGUuY29tL2Fw
+cGxlY2EvME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9kZXZlbG9wZXIuYXBwbGUu
+Y29tL2NlcnRpZmljYXRpb25hdXRob3JpdHkvd3dkcmNhLmNybDALBgNVHQ8EBAMC
+B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwIwEAYKKoZIhvdjZAYDAQQCBQAwDQYJKoZI
+hvcNAQEFBQADggEBAEuQKbpqxQPnazN4A0NVB7YRr2M2Hk5DtV4M/QT8OFDY1IsA
+s4U8NmI9AMgoszIdLG3n4x2Y4mm6yTYDEMQpaJRbir9Fx6WcZQ4GB72iZ1M/1fkC
+79Yq17yzxM9Awf2igx1EkZhzy6Oq+7cUX9Jbz5IQrPyfXe5hpaOmY4jnWgSlaiwJ
+6bBhFHwhJY9ekAFOyRnZS7hQD4mLOBZLti/lH4Z2zadd21DbM/uhUvLJgYpIRplP
+hoFxV5bev5vv9A0dbYw6ERa3+aG8HHmP9N4u1/JxHIX2VlYzzLj1l7Tlps6QG6/r
+4CsJBIfpGjxwznrYJBN5ox1KlFxNLwD3UNpLhOQ=
+-----END CERTIFICATE-----
+Bag Attributes
+ friendlyName: Julian Laplace DEV KEY
+ localKeyID: 72 44 9B 21 2C C7 EC F4 C4 ED 1F 60 B0 86 FF B3 12 B3 1E B8
+Key Attributes: <No Attributes>
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEArrpXz8B0q/tJ2+N6gjK2zLNPIIlAlzrE46TIkY10xo9jW3vB
+RgZL9V5xKtmCWf3KfR2I7KioyqqnZvz6++TV/M3rZptJXN8yMmG4mjTqgC1V2Sub
+YyUZqpqkfyLK1ePimsg09UUNxwNcIyBJun+f8wXqz7HKKieiq+d7oTs+WJbWdkIf
+txe7hjL2n93PI1LLCF15Cx8s7XP7R7QsTGMvHUzYYHiL4bc+r54jpZ28onVAwdYx
++p3WkKzdFGjkrIl++IXmyJr0E7jilfrKwMWi4tvCKgXVKH6VmWLIKOtfKVMdIK0W
+Q9ij3kRHfEwgN/djC3M2UWFEGLx1d7FuL5c+nQIDAQABAoIBAQCdKIiBGccXRa/a
+MxJm9zyKhCGAz1MkczzS0M2Cdax6SFCC4Z4m4NUDfw3lD7z6Opcd0CnvS4h+ud5W
+eOc7GEoM2B6KrNPgz/Szckf7k+r0nVJhQqLCG4WpeOzKWb39grq4o6lWjuTkgzYl
+dg46zD5O9ZyNcYm4PuxHGK+Phtz+CjkEx5Y6WqoBvgA10A+pHBSMAZ2uRznGHJmQ
+xF9HHdLLKwtZemRGEdZaC510fpBDbgapLzZqy495/2I3zlOFSW9thWhJ/PO45AH1
+bqtRRq0z0CU94QynulrcvZlyFVF5msDyzBI6B7oZAumC5y9s/v6/jE2rIr1uNx2W
+e3Zbh+21AoGBAOAUhkD932XhDKtatDpBDQ8va9ZiiQCuO8VFP1wck2U18ovy6DQn
+A3JJUoC/d4K7brCQ5CgzACI0tgen99u78qN3F3caapwJMbQ3LAdZA6U5tZq/7poA
+FrvEs6vAToe+HhLkWxvctO0sEGtZEDUptRRa5cGoMAV2FyLvtSHgPk+jAoGBAMee
+GgcunO+XBI8HVgC9GTLxJWUuXpwYslI9uDXTVFsJ4jqEAk0R4qA8wGa6VpXS7yea
+3Vd8qG9C8yVIuSoajyH4FYQ6rzfbUDoYHsKkJUabJWTK08VPb2RVae6vDQmXqyw3
+X6QH+no33inyD5bEqaSg7lTcQ03kX3shVj+kTBy/AoGBAL0slqsHqTI5bJi9WqpT
+gNLxFflH9qG8dnl0nKkG9ujbQokj6SUeSqqRfDV1b0/PLQOjTvshE1vNZF/STk4T
+viSCxiynAJn6qRub+G+7lnhG4CplHuqkaIzc41J8Z8/xkTIh10kOazBPowz9g9Fe
+BKHKm22CsAfsSlwUQrb2uaRBAoGAI660UmJXtVBWhUa3bexfi8ale5+9U8PqZF8a
+Ba0gICH1KaadTq+Kxj/12KcogSGylG7PcCsdZL4f/qMwTkFvIpBZMEjZN2/huHDF
+Vt/GtgdXuNu03UlkzRejlwH9n6BX/dBsLUMr2BsSgIb/mGDRPldyIwM4mLzhAH6m
+DzxMSrsCgYB9LP/AsmUY67lajzYO18Xu9YTNzIKwovD/RhRHRz0kOI7QQRSXNtGZ
+RwzV9OCeRXGLrC2n6w9KnGx8d6pZKElzDkN7+Ql9y2ALmzaInmugTyoNxBPeU3GB
+DxN5IIkt2G1rexC/SaQ9WOokiQSKJIZGRHR/WHKpXC/mBHxq7CENkQ==
+-----END RSA PRIVATE KEY-----