You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Converting hastebin to a Dashboard application server for app stores
Hastebin is a 'pastebin' web application you post code and text to share. The application has no user accounts, all posts are anonymous and publicly accessible via a generated URL. This conversion is based on hastebin's source code.
Dashboard is a reusable interface for user account management with modules for more. It runs separately to your web application, as users browse your Dashboard server they receive content from itself or your application server combined into a single website. When Dashboard proxies an application server it includes user account and session information in the request headers.
UserAppStore is a portal for coding and using web applications. Users install the apps with free or paid subscriptions for themselves or an organization. Compatibility means retrofitting hastebin to be a Dashboard application server, and tweaking it to for compatibility with the app store software where it will run in a sandboxed iframe.
When the conversion is complete hastebin will be ready for publishing on UserAppStore and other app store websites with paid subscription plans.
Feature comparison before and after integration
Feature
Original Hastebin
Hastebin application server
Create posts
anonymous-only
registered-only
Public URLs
mandatory
optional
List own posts
no
yes
Delete own posts
no
yes
List + view organization posts
no
yes
Share posts with organization
no
yes
Paid subscriptions
no
yes
Quotas
no
personal + organization
Screenshot walkthrough of finished integration
Developer claims application server
Developer creates Connect registration
Developer completes Connect registration information
Developer provides bank details for receiving revenue
Developer submits Connect registration
Developer creates app
Developer provides app information
Developer publishes app on store
Developer views app's subscription administration
Developer creates and publishes product
Developer creates and publishes plan
First user installs app for organization
Second user installs app for organization
Second user creates a post shared with organization
First user views post
Developer views subscriptions
Part one: General compatibility requirements
The hastebin project uses a static folder for its assets that are served at /. Remapping that to /public lets the files be served faster because Dashboard serves that folder without authenticating the user. This matters a lot if you have many static assets.
The HTML markup will be served to the user in an IFRAMEsrcdoc. Care must be taken to use " instead of ' in your HTML markup so the srcdoc can inline your page.
srcdoc='<html><head>title> only use " </title></head><body></html>'
When running on an app store in an IFRAME browser security restrictions apply that prevent accessing history.pushState, localStorage, sessionStorage, and more, and there is no document.location because of the srcdoc entry point.
The Connect server handles all hastebin routes for the application. Connect is compatible with Express so the express-application-server middleware is added, it verifies requests come from your own dashboard server or an authorized app store. Adding it to the server.js requires binding it to each HTTP method.
The middleware only identifies if the request came from your dashboard server or an authorized app store, it does not end requests if they are invalid. In the server.js each request has to be modified to include a check and authorization error when accessed incorrectly.
Dashboard requires /home be the application's signed-in page because index.html is reserved as a landing page for guests when you run your own Dashboard server.
The document.js provides a document API for creating and retrieving documents to storage. The hastebin project already supported reading and writing documents but it needs to also support deleting and listing.
The document.js needs to support listing your posts:
async function list (key, req) {
const dashboardKey = req.dashboard.split('://')[1]
const folder = `${basePath}/${dashboardKey}/${key}`
if (!fs.existsSync(folder)) {
return null
}
const list = await fsa.readDir(`${basePath}/${dashboardKey}/${key}`, 'utf8')
if (!list || !list.length) {
return null
}
for (const n in list) {
list[n] = await load(list[n], req)
}
return list
}
The document.js now needs to create posts with added metadata and indexing and only if you're within your quota:
async function checkQuota (req) {
const personal = await list(`account/${req.accountid}`, req)
if (!req.organizationid) {
return {
unusedPersonalQuota: !personal || personal.length < 1000
}
}
const organization = await list(`organization/${req.organizationid}`, req)
return {
unusedPersonalQuota: !personal || personal.length < 1000,
unusedOrganizationQuota: !organization || organization.length < 1000
}
}
async function create (req) {
if (!req.body || !req.body.document || !req.body.document.length) {
throw new Error('invalid-document')
}
if (req.body.organization && !req.organizationid) {
throw new Error('invalid-document-owner')
}
if (global.maxLength && req.body.document.length > global.maxLength) {
throw new Error('invalid-document-length')
}
const quota = await checkQuota(req)
if (req.body.organization && !quota.unusedOrganizationQuota) {
throw new Error('organization-quota-exceeded')
} else if (!req.body.organization && !quota.unusedPersonalQuota) {
throw new Error('personal-quota-exceeded')
}
const dashboardKey = req.dashboard.split('://')[1]
if (req.body.customid) {
try {
const existing = await load(req.body.customid, req)
if (existing) {
throw new Error('duplicate-document-id')
}
} catch (error) {
}
}
const key = req.body.customid || await generateUniqueKey(req)
const object = {
accountid: req.accountid,
created: Math.floor(new Date().getTime() / 1000),
key: key
}
if (req.body.public) {
object.public = true
}
if (req.body.organization) {
object.organizationid = req.organizationid
}
if (req.body.language) {
object.language = req.body.language
}
createFolder(`${basePath}/${dashboardKey}`)
await fsa.writeFile(`${basePath}/${dashboardKey}/${md5(key)}`, JSON.stringify(object), 'utf8')
await fsa.writeFile(`${basePath}/${dashboardKey}/${md5(key)}.raw`, req.body.document)
createFolder(`${basePath}/${dashboardKey}/account/${req.accountid}`)
await fsa.writeFile(`${basePath}/${dashboardKey}/account/${req.accountid}/${key}`, JSON.stringify(object))
if (req.body.organization) {
createFolder(`${basePath}/${dashboardKey}/organization/${req.organizationid}`)
await fsa.writeFile(`${basePath}/${dashboardKey}/organization/${req.organizationid}/${key}`, JSON.stringify(object))
}
return object
}
The document.js needs to remove posts:
async function remove (key, req) {
const object = await load(key, req)
if (object.accountid !== req.accountid) {
throw new Error('invalid-document')
}
const dashboardKey = req.dashboard.split('://')[1]
if (fs.existsSync(`${basePath}/${dashboardKey}/account/${req.accountid}/${key}`)) {
await fsa.unlink(`${basePath}/${dashboardKey}/account/${req.accountid}/${key}`)
}
if (json.organizationid) {
if (fs.existsSync(`${basePath}/${dashboardKey}/organization/${req.organizationid}/${key}`)) {
await fsa.unlink(`${basePath}/${dashboardKey}/organization/${req.organizationid}/${key}`)
}
}
await fsa.unlink(`${basePath}/${dashboardKey}/${md5(key)}`)
}
Part four: Extending the HTTP server
The HTTP API in server.js uses the document API in document.js to save and retrieve the user's posts from storage. The server.js already supports creating and retrieving, it just needs additions for deleting and listing. The original hastebin allowed access to all posts via public URL, that isn't possible on app stores so it is mitigated by optionally sharing the posts on a secondary domain PUBLIC_DOMAIN.
The server.js needs a route for listing documents:
router.get('/documents', function (req, res) {
if(!req.subscriptionid) {
return dashboardError(res)
}
let list
try {
list = await Document.list(`account/${req.accountid}`, req)
} catch (error) {
}
res.writeHead(200, { 'content-type': 'application/json' })
if (!list || !list.length) {
return res.end('[]')
}
return res.end(JSON.stringify(list))
})
The server.js needs a route for listing organization documents:
router.get('/documents/organization', function (req, res) {
if(!req.subscriptionid) {
return dashboardError(res)
}
if (!req.organizationid) {
res.writeHead(500, { 'content-type': 'application/json' })
return res.end(`{ "message": "An invalid document was provided" }`)
}
let list
try {
list = await Document.list(`organization/${req.organizationid}`, req)
} catch (error) {
}
res.writeHead(200, { 'content-type': 'application/json' })
if (!list || !list.length) {
return res.end('[]')
}
return res.end(JSON.stringify(list))
})
The server.js needs a route for deleting documents:
The original layout was a textarea, with a logo and strip of icons for saving/copying posts. A new interface was created with the additional options.
Tabbed navigation was added to access creating new posts and the personal and organization post lists. The navigation hides the organization link when not applicable.
Tables were added listing your posts and their general configuration, along with an optionally-concealed column showing if they are shared with your organization.
Information was added to distinguishing between viewing your own post, your own post shared with an organization, and a post shared with an organization.
<li id="view">
<h2>Viewing my post <span id="postid-1"></span></h2>
</li>
<li id="view-organization">
<h2>Viewing <span id="postid-2"></span> shared with organization</h2>
</li>
<li id="view-organization-post-owner">
<h2>Viewing my post <span id="postid-3"></span> shared with organization</h2>
</li>
Settings were added when creating posts to select a language, allow posts to be publicly accessible via URL, owned by an organization, and use custom keys.