Refactor
This commit is contained in:
73
server/api/auth.js
Normal file
73
server/api/auth.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import Router from 'koa-router';
|
||||
import httpErrors from 'http-errors';
|
||||
import fetch from 'isomorphic-fetch';
|
||||
var querystring = require('querystring');
|
||||
|
||||
import { presentUser, presentTeam } from '../presenters';
|
||||
import { User, Team } from '../models';
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post('auth.slack', async (ctx) => {
|
||||
const { code } = ctx.request.body;
|
||||
|
||||
ctx.assertPresent(code, 'code is required');
|
||||
|
||||
const body = {
|
||||
client_id: process.env.SLACK_KEY,
|
||||
client_secret: process.env.SLACK_SECRET,
|
||||
code: code,
|
||||
redirect_uri: process.env.SLACK_REDIRECT_URI,
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
const response = await fetch('https://slack.com/api/oauth.access?' + querystring.stringify(body));
|
||||
data = await response.json();
|
||||
} catch(e) {
|
||||
throw httpErrors.BadRequest();
|
||||
}
|
||||
|
||||
if (!data.ok) throw httpErrors.BadRequest(data.error);
|
||||
|
||||
// User
|
||||
let userData;
|
||||
let user = await User.findOne({ slackId: data.user_id });
|
||||
if (user) {
|
||||
user.slackAccessToken = data.access_token;
|
||||
user.save();
|
||||
} else {
|
||||
// Find existing user
|
||||
const userParams = { token: data.access_token, user: data.user_id }
|
||||
const response = await fetch('https://slack.com/api/users.info?' + querystring.stringify(userParams));
|
||||
userData = await response.json();
|
||||
user = await User.create({
|
||||
slackId: data.user_id,
|
||||
username: userData.user.name,
|
||||
name: userData.user.profile.real_name,
|
||||
email: userData.user.profile.email,
|
||||
slackData: userData.user,
|
||||
slackAccessToken: data.access_token,
|
||||
});
|
||||
}
|
||||
|
||||
// Team
|
||||
let team = await Team.findOne({ slackId: data.team_id });
|
||||
if (!team) {
|
||||
team = await Team.create({
|
||||
slackId: data.team_id,
|
||||
name: data.team_name,
|
||||
});
|
||||
}
|
||||
|
||||
// Add to correct team
|
||||
user.setTeam(team);
|
||||
|
||||
ctx.body = { data: {
|
||||
user: presentUser(user),
|
||||
team: presentTeam(team),
|
||||
accessToken: user.getJwtToken(),
|
||||
}};
|
||||
});
|
||||
|
||||
export default router;
|
||||
61
server/api/authentication.js
Normal file
61
server/api/authentication.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import httpErrors from 'http-errors';
|
||||
import JWT from 'jsonwebtoken';
|
||||
|
||||
import { User } from '../models';
|
||||
|
||||
export default function auth({ require = true } = {}) {
|
||||
return async function authMiddleware(ctx, next) {
|
||||
let token;
|
||||
|
||||
const authorizationHeader = ctx.request.get('authorization');
|
||||
if (authorizationHeader) {
|
||||
const parts = authorizationHeader.split(' ');
|
||||
if (parts.length == 2) {
|
||||
const scheme = parts[0];
|
||||
const credentials = parts[1];
|
||||
|
||||
if (/^Bearer$/i.test(scheme)) {
|
||||
token = credentials;
|
||||
}
|
||||
} else {
|
||||
if (require) {
|
||||
throw httpErrors.Unauthorized('Bad Authorization header format. Format is "Authorization: Bearer <token>"\n');
|
||||
}
|
||||
}
|
||||
} else if (ctx.request.query.token) {
|
||||
token = ctx.request.query.token;
|
||||
}
|
||||
|
||||
if (!token && require) {
|
||||
throw httpErrors.Unauthorized('Authentication required');
|
||||
}
|
||||
|
||||
// Get user without verifying payload signature
|
||||
let payload;
|
||||
try {
|
||||
payload = JWT.decode(token);
|
||||
} catch(_e) {
|
||||
throw httpErrors.Unauthorized('Unable to decode JWT token');
|
||||
}
|
||||
console.log(payload)
|
||||
const user = await User.findOne({
|
||||
where: { id: payload.id },
|
||||
});
|
||||
|
||||
try {
|
||||
JWT.verify(token, user.jwtSecret);
|
||||
} catch(e) {
|
||||
throw httpErrors.Unauthorized('Invalid token');
|
||||
}
|
||||
|
||||
ctx.state.token = token;
|
||||
ctx.state.user = user;
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
// Export JWT methods as a convenience
|
||||
export const sign = JWT.sign;
|
||||
export const verify = JWT.verify;
|
||||
export const decode = JWT.decode;
|
||||
55
server/api/index.js
Normal file
55
server/api/index.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import httpErrors from 'http-errors';
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
import Sequelize from 'sequelize';
|
||||
|
||||
import auth from './auth';
|
||||
import user from './user';
|
||||
|
||||
import validation from './validation';
|
||||
|
||||
const api = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
// API error handler
|
||||
api.use(async (ctx, next) => {
|
||||
try {
|
||||
await next();
|
||||
} catch (err) {
|
||||
ctx.status = err.status || 500;
|
||||
let message = err.message || err.name;
|
||||
|
||||
if (err instanceof Sequelize.ValidationError) {
|
||||
// super basic form error handling
|
||||
ctx.status = 400;
|
||||
if (err.errors && err.errors[0]) {
|
||||
message = `${err.errors[0].message} (${err.errors[0].path})`;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.status === 500) {
|
||||
message = 'Internal Server Error';
|
||||
ctx.app.emit('error', err, ctx);
|
||||
}
|
||||
|
||||
ctx.body = { message };
|
||||
}
|
||||
});
|
||||
|
||||
api.use(bodyParser());
|
||||
api.use(validation());
|
||||
|
||||
router.use('/', auth.routes());
|
||||
router.use('/', user.routes());
|
||||
|
||||
// Router is embedded in a Koa application wrapper, because koa-router does not
|
||||
// allow middleware to catch any routes which were not explicitly defined.
|
||||
api.use(router.routes());
|
||||
|
||||
// API 404 handler
|
||||
api.use(async () => {
|
||||
throw httpErrors.NotFound();
|
||||
});
|
||||
|
||||
export default api;
|
||||
13
server/api/user.js
Normal file
13
server/api/user.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import Router from 'koa-router';
|
||||
|
||||
import auth from './authentication';
|
||||
import { presentUser } from '../presenters';
|
||||
import { User } from '../models';
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post('user.info', auth(), async (ctx) => {
|
||||
ctx.body = { data: presentUser(ctx.state.user) };
|
||||
});
|
||||
|
||||
export default router;
|
||||
26
server/api/validation.js
Normal file
26
server/api/validation.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import httpErrors from 'http-errors';
|
||||
import validator from 'validator';
|
||||
|
||||
export default function validation() {
|
||||
return function validationMiddleware(ctx, next) {
|
||||
ctx.assertPresent = function assertPresent(value, message) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
throw httpErrors.BadRequest(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertEmail = function assertEmail(value, message) {
|
||||
if (!validator.isEmail(value)) {
|
||||
throw httpErrors.BadRequest(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertUuid = function assertUuid(value, message) {
|
||||
if (!validator.isUUID(value)) {
|
||||
throw httpErrors.BadRequest(message);
|
||||
}
|
||||
};
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
66
server/index.js
Normal file
66
server/index.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import compress from 'koa-compress';
|
||||
import helmet from 'koa-helmet';
|
||||
import logger from 'koa-logger';
|
||||
import mount from 'koa-mount';
|
||||
import Koa from 'koa';
|
||||
|
||||
import api from './api';
|
||||
import routes from './routes';
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
app.use(compress());
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const convert = require('koa-convert')
|
||||
const webpack = require('webpack');
|
||||
const devMiddleware = require('koa-webpack-dev-middleware');
|
||||
const hotMiddleware = require('koa-webpack-hot-middleware');
|
||||
const config = require('../webpack.config.dev');
|
||||
const compile = webpack(config);
|
||||
|
||||
app.use(convert(devMiddleware(compile, {
|
||||
// display no info to console (only warnings and errors)
|
||||
noInfo: false,
|
||||
|
||||
// display nothing to the console
|
||||
quiet: false,
|
||||
|
||||
// switch into lazy mode
|
||||
// that means no watching, but recompilation on every request
|
||||
lazy: false,
|
||||
|
||||
// // watch options (only lazy: false)
|
||||
// watchOptions: {
|
||||
// aggregateTimeout: 300,
|
||||
// poll: true
|
||||
// },
|
||||
|
||||
// public path to bind the middleware to
|
||||
// use the same as in webpack
|
||||
publicPath: config.output.publicPath,
|
||||
|
||||
// options for formating the statistics
|
||||
stats: {
|
||||
colors: true
|
||||
}
|
||||
})));
|
||||
app.use(convert(hotMiddleware(compile, {
|
||||
log: console.log,
|
||||
path: '/__webpack_hmr',
|
||||
heartbeat: 10 * 1000
|
||||
})));
|
||||
app.use(logger());
|
||||
}
|
||||
|
||||
app.use(mount('/api', api));
|
||||
app.use(mount(routes));
|
||||
|
||||
app.use(helmet.csp({
|
||||
directives: {
|
||||
defaultSrc: ['\'self\''],
|
||||
styleSrc: ['\'self\'', '\'unsafe-inline\''],
|
||||
},
|
||||
}));
|
||||
|
||||
export default app;
|
||||
14
server/models/Atlas.js
Normal file
14
server/models/Atlas.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
DataTypes,
|
||||
sequelize,
|
||||
} from '../sequelize';
|
||||
import Team from './Team';
|
||||
|
||||
const Atlas = sequelize.define('atlas', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
name: DataTypes.STRING,
|
||||
});
|
||||
|
||||
Atlas.belongsTo(Team);
|
||||
|
||||
export default Atlas;
|
||||
17
server/models/Document.js
Normal file
17
server/models/Document.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
DataTypes,
|
||||
sequelize,
|
||||
} from '../sequelize';
|
||||
import Atlas from './Atlas';
|
||||
import Team from './Team';
|
||||
|
||||
const Document = sequelize.define('document', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
name: DataTypes.STRING,
|
||||
content: DataTypes.STRING,
|
||||
});
|
||||
|
||||
Document.belongsTo(Atlas);
|
||||
Document.belongsTo(Team);
|
||||
|
||||
export default Atlas;
|
||||
20
server/models/Team.js
Normal file
20
server/models/Team.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
DataTypes,
|
||||
sequelize,
|
||||
} from '../sequelize';
|
||||
|
||||
const Team = sequelize.define('team', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
name: DataTypes.STRING,
|
||||
slackId: { type: DataTypes.STRING, unique: true },
|
||||
slackData: DataTypes.JSONB,
|
||||
}, {
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['slackId']
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default Team;
|
||||
44
server/models/User.js
Normal file
44
server/models/User.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import crypto from 'crypto';
|
||||
import {
|
||||
DataTypes,
|
||||
sequelize,
|
||||
encryptedFields
|
||||
} from '../sequelize';
|
||||
import Team from './Team';
|
||||
|
||||
import JWT from 'jsonwebtoken';
|
||||
|
||||
const User = sequelize.define('user', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
email: DataTypes.STRING,
|
||||
username: DataTypes.STRING,
|
||||
name: DataTypes.STRING,
|
||||
isAdmin: DataTypes.BOOLEAN,
|
||||
slackAccessToken: encryptedFields.vault('slackAccessToken'),
|
||||
slackId: { type: DataTypes.STRING, unique: true },
|
||||
slackData: DataTypes.JSONB,
|
||||
jwtSecret: encryptedFields.vault('jwtSecret'),
|
||||
}, {
|
||||
instanceMethods: {
|
||||
getJwtToken() {
|
||||
return JWT.sign({ id: this.id }, this.jwtSecret);
|
||||
},
|
||||
},
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['email']
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const setRandomJwtSecret = (model) => {
|
||||
model.jwtSecret = crypto.randomBytes(64).toString('hex');
|
||||
};
|
||||
|
||||
User.beforeCreate(setRandomJwtSecret);
|
||||
User.belongsTo(Team);
|
||||
|
||||
sequelize.sync();
|
||||
|
||||
export default User;
|
||||
6
server/models/index.js
Normal file
6
server/models/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import User from './User';
|
||||
import Team from './Team';
|
||||
import Atlas from './Atlas';
|
||||
import Document from './Document';
|
||||
|
||||
export { User, Team, Atlas, Document };
|
||||
16
server/presenters.js
Normal file
16
server/presenters.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export function presentUser(user) {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
avatarUrl: user.slackData.profile.image_192,
|
||||
};
|
||||
}
|
||||
|
||||
export function presentTeam(team) {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
};
|
||||
}
|
||||
39
server/routes.js
Normal file
39
server/routes.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const path = require('path');
|
||||
import httpErrors from 'http-errors';
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
import sendfile from 'koa-sendfile';
|
||||
|
||||
const koa = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
// // error handler
|
||||
// koa.use(async (ctx, next) => {
|
||||
// try {
|
||||
// await next();
|
||||
// } catch (err) {
|
||||
// ctx.status = err.status || 500;
|
||||
// ctx.body = err.message;
|
||||
// }
|
||||
// });
|
||||
|
||||
// Frontend
|
||||
router.get('/service-worker.js', async (ctx) => {
|
||||
ctx.set('Content-Type', 'application/javascript');
|
||||
const stats = await sendfile(ctx, path.join(__dirname, 'static/service-worker.js'));
|
||||
if (!ctx.status) ctx.throw(httpErrors.NotFound());
|
||||
});
|
||||
|
||||
router.get('*', async (ctx) => {
|
||||
const stats = await sendfile(ctx, path.join(__dirname, 'static/index.html'));
|
||||
if (!ctx.status) ctx.throw(httpErrors.NotFound());
|
||||
});
|
||||
|
||||
koa.use(router.routes());
|
||||
|
||||
// 404 handler
|
||||
koa.use(async () => {
|
||||
throw httpErrors.NotFound();
|
||||
});
|
||||
|
||||
export default koa;
|
||||
13
server/sequelize.js
Normal file
13
server/sequelize.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import Sequelize from 'sequelize';
|
||||
import EncryptedField from 'sequelize-encrypted';
|
||||
import debug from 'debug';
|
||||
|
||||
const secretKey = process.env.SEQUELIZE_SECRET;
|
||||
export const encryptedFields = EncryptedField(Sequelize, secretKey);
|
||||
|
||||
export const DataTypes = Sequelize;
|
||||
|
||||
export const sequelize = new Sequelize(process.env.DATABASE_URL, {
|
||||
logging: debug('sql'),
|
||||
typeValidation: true,
|
||||
});
|
||||
21
server/static/index.html
Normal file
21
server/static/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Beautiful Atlas</title>
|
||||
<link href='https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.11.0/codemirror.min.css' rel='stylesheet'>
|
||||
</head>
|
||||
<body style='display: flex; width: 100%'>
|
||||
<div id="root" style='display: flex; width: 100%'></div>
|
||||
<script src="/static/bundle.js"></script>
|
||||
<script type="text/javascript">
|
||||
// if ('serviceWorker' in navigator) {
|
||||
// navigator.serviceWorker.register('/service-worker.js')
|
||||
// .then(function(reg) {
|
||||
// console.log('SW registration succeeded');
|
||||
// }).catch(function(error) {
|
||||
// console.log('SW registration failed: ' + error);
|
||||
// });
|
||||
// };
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
84
server/static/service-worker.js
Normal file
84
server/static/service-worker.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2014 Google Inc. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.toolbox = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
"use strict";function debug(e,n){n=n||{};var t=n.debug||globalOptions.debug;t&&console.log("[sw-toolbox] "+e)}function openCache(e){var n;return e&&e.cache&&(n=e.cache.name),n=n||globalOptions.cache.name,caches.open(n)}function fetchAndCache(e,n){n=n||{};var t=n.successResponses||globalOptions.successResponses;return fetch(e.clone()).then(function(c){return"GET"===e.method&&t.test(c.status)&&openCache(n).then(function(t){t.put(e,c).then(function(){var c=n.cache||globalOptions.cache;(c.maxEntries||c.maxAgeSeconds)&&c.name&&queueCacheExpiration(e,t,c)})}),c.clone()})}function queueCacheExpiration(e,n,t){var c=cleanupCache.bind(null,e,n,t);cleanupQueue=cleanupQueue?cleanupQueue.then(c):c()}function cleanupCache(e,n,t){var c=e.url,a=t.maxAgeSeconds,u=t.maxEntries,o=t.name,r=Date.now();return debug("Updating LRU order for "+c+". Max entries is "+u+", max age is "+a),idbCacheExpiration.getDb(o).then(function(e){return idbCacheExpiration.setTimestampForUrl(e,c,r)}).then(function(e){return idbCacheExpiration.expireEntries(e,u,a,r)}).then(function(e){debug("Successfully updated IDB.");var t=e.map(function(e){return n["delete"](e)});return Promise.all(t).then(function(){debug("Done with cache cleanup.")})})["catch"](function(e){debug(e)})}function renameCache(e,n,t){return debug("Renaming cache: ["+e+"] to ["+n+"]",t),caches["delete"](n).then(function(){return Promise.all([caches.open(e),caches.open(n)]).then(function(n){var t=n[0],c=n[1];return t.keys().then(function(e){return Promise.all(e.map(function(e){return t.match(e).then(function(n){return c.put(e,n)})}))}).then(function(){return caches["delete"](e)})})})}var globalOptions=require("./options"),idbCacheExpiration=require("./idb-cache-expiration"),cleanupQueue;module.exports={debug:debug,fetchAndCache:fetchAndCache,openCache:openCache,renameCache:renameCache};
|
||||
},{"./idb-cache-expiration":2,"./options":3}],2:[function(require,module,exports){
|
||||
"use strict";function openDb(e){return new Promise(function(r,n){var t=indexedDB.open(DB_PREFIX+e,DB_VERSION);t.onupgradeneeded=function(){var e=t.result.createObjectStore(STORE_NAME,{keyPath:URL_PROPERTY});e.createIndex(TIMESTAMP_PROPERTY,TIMESTAMP_PROPERTY,{unique:!1})},t.onsuccess=function(){r(t.result)},t.onerror=function(){n(t.error)}})}function getDb(e){return e in cacheNameToDbPromise||(cacheNameToDbPromise[e]=openDb(e)),cacheNameToDbPromise[e]}function setTimestampForUrl(e,r,n){return new Promise(function(t,o){var i=e.transaction(STORE_NAME,"readwrite"),u=i.objectStore(STORE_NAME);u.put({url:r,timestamp:n}),i.oncomplete=function(){t(e)},i.onabort=function(){o(i.error)}})}function expireOldEntries(e,r,n){return r?new Promise(function(t,o){var i=1e3*r,u=[],c=e.transaction(STORE_NAME,"readwrite"),s=c.objectStore(STORE_NAME),a=s.index(TIMESTAMP_PROPERTY);a.openCursor().onsuccess=function(e){var r=e.target.result;if(r&&n-i>r.value[TIMESTAMP_PROPERTY]){var t=r.value[URL_PROPERTY];u.push(t),s["delete"](t),r["continue"]()}},c.oncomplete=function(){t(u)},c.onabort=o}):Promise.resolve([])}function expireExtraEntries(e,r){return r?new Promise(function(n,t){var o=[],i=e.transaction(STORE_NAME,"readwrite"),u=i.objectStore(STORE_NAME),c=u.index(TIMESTAMP_PROPERTY),s=c.count();c.count().onsuccess=function(){var e=s.result;e>r&&(c.openCursor().onsuccess=function(n){var t=n.target.result;if(t){var i=t.value[URL_PROPERTY];o.push(i),u["delete"](i),e-o.length>r&&t["continue"]()}})},i.oncomplete=function(){n(o)},i.onabort=t}):Promise.resolve([])}function expireEntries(e,r,n,t){return expireOldEntries(e,n,t).then(function(n){return expireExtraEntries(e,r).then(function(e){return n.concat(e)})})}var DB_PREFIX="sw-toolbox-",DB_VERSION=1,STORE_NAME="store",URL_PROPERTY="url",TIMESTAMP_PROPERTY="timestamp",cacheNameToDbPromise={};module.exports={getDb:getDb,setTimestampForUrl:setTimestampForUrl,expireEntries:expireEntries};
|
||||
},{}],3:[function(require,module,exports){
|
||||
"use strict";var scope;scope=self.registration?self.registration.scope:self.scope||new URL("./",self.location).href,module.exports={cache:{name:"$$$toolbox-cache$$$"+scope+"$$$",maxAgeSeconds:null,maxEntries:null},debug:!1,networkTimeoutSeconds:null,preCacheItems:[],successResponses:/^0|([123]\d\d)|(40[14567])|410$/};
|
||||
},{}],4:[function(require,module,exports){
|
||||
"use strict";var url=new URL("./",self.location),basePath=url.pathname,pathRegexp=require("path-to-regexp"),Route=function(e,t,i,s){t instanceof RegExp?this.fullUrlRegExp=t:(0!==t.indexOf("/")&&(t=basePath+t),this.keys=[],this.regexp=pathRegexp(t,this.keys)),this.method=e,this.options=s,this.handler=i};Route.prototype.makeHandler=function(e){var t;if(this.regexp){var i=this.regexp.exec(e);t={},this.keys.forEach(function(e,s){t[e.name]=i[s+1]})}return function(e){return this.handler(e,t,this.options)}.bind(this)},module.exports=Route;
|
||||
},{"path-to-regexp":13}],5:[function(require,module,exports){
|
||||
"use strict";function regexEscape(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var Route=require("./route"),keyMatch=function(e,t){for(var r=e.entries(),o=r.next();!o.done;){var n=new RegExp(o.value[0]);if(n.test(t))return o.value[1];o=r.next()}return null},Router=function(){this.routes=new Map,this["default"]=null};["get","post","put","delete","head","any"].forEach(function(e){Router.prototype[e]=function(t,r,o){return this.add(e,t,r,o)}}),Router.prototype.add=function(e,t,r,o){o=o||{};var n;t instanceof RegExp?n=RegExp:(n=o.origin||self.location.origin,n=n instanceof RegExp?n.source:regexEscape(n)),e=e.toLowerCase();var u=new Route(e,t,r,o);this.routes.has(n)||this.routes.set(n,new Map);var a=this.routes.get(n);a.has(e)||a.set(e,new Map);var s=a.get(e),i=u.regexp||u.fullUrlRegExp;s.set(i.source,u)},Router.prototype.matchMethod=function(e,t){var r=new URL(t),o=r.origin,n=r.pathname;return this._match(e,keyMatch(this.routes,o),n)||this._match(e,this.routes.get(RegExp),t)},Router.prototype._match=function(e,t,r){if(t){var o=t.get(e.toLowerCase());if(o){var n=keyMatch(o,r);if(n)return n.makeHandler(r)}}return null},Router.prototype.match=function(e){return this.matchMethod(e.method,e.url)||this.matchMethod("any",e.url)},module.exports=new Router;
|
||||
},{"./route":4}],6:[function(require,module,exports){
|
||||
"use strict";function cacheFirst(e,r,t){return helpers.debug("Strategy: cache first ["+e.url+"]",t),helpers.openCache(t).then(function(r){return r.match(e).then(function(r){return r?r:helpers.fetchAndCache(e,t)})})}var helpers=require("../helpers");module.exports=cacheFirst;
|
||||
},{"../helpers":1}],7:[function(require,module,exports){
|
||||
"use strict";function cacheOnly(e,r,c){return helpers.debug("Strategy: cache only ["+e.url+"]",c),helpers.openCache(c).then(function(r){return r.match(e)})}var helpers=require("../helpers");module.exports=cacheOnly;
|
||||
},{"../helpers":1}],8:[function(require,module,exports){
|
||||
"use strict";function fastest(e,n,t){return helpers.debug("Strategy: fastest ["+e.url+"]",t),new Promise(function(r,s){var c=!1,o=[],a=function(e){o.push(e.toString()),c?s(new Error('Both cache and network failed: "'+o.join('", "')+'"')):c=!0},h=function(e){e instanceof Response?r(e):a("No result returned")};helpers.fetchAndCache(e.clone(),t).then(h,a),cacheOnly(e,n,t).then(h,a)})}var helpers=require("../helpers"),cacheOnly=require("./cacheOnly");module.exports=fastest;
|
||||
},{"../helpers":1,"./cacheOnly":7}],9:[function(require,module,exports){
|
||||
module.exports={networkOnly:require("./networkOnly"),networkFirst:require("./networkFirst"),cacheOnly:require("./cacheOnly"),cacheFirst:require("./cacheFirst"),fastest:require("./fastest")};
|
||||
},{"./cacheFirst":6,"./cacheOnly":7,"./fastest":8,"./networkFirst":10,"./networkOnly":11}],10:[function(require,module,exports){
|
||||
"use strict";function networkFirst(e,r,t){t=t||{};var s=t.successResponses||globalOptions.successResponses,n=t.networkTimeoutSeconds||globalOptions.networkTimeoutSeconds;return helpers.debug("Strategy: network first ["+e.url+"]",t),helpers.openCache(t).then(function(r){var o,u,c=[];if(n){var i=new Promise(function(t){o=setTimeout(function(){r.match(e).then(function(e){e&&t(e)})},1e3*n)});c.push(i)}var a=helpers.fetchAndCache(e,t).then(function(e){if(o&&clearTimeout(o),s.test(e.status))return e;throw helpers.debug("Response was an HTTP error: "+e.statusText,t),u=e,new Error("Bad response")})["catch"](function(){return helpers.debug("Network or response error, fallback to cache ["+e.url+"]",t),r.match(e).then(function(e){return e||u})});return c.push(a),Promise.race(c)})}var globalOptions=require("../options"),helpers=require("../helpers");module.exports=networkFirst;
|
||||
},{"../helpers":1,"../options":3}],11:[function(require,module,exports){
|
||||
"use strict";function networkOnly(e,r,t){return helpers.debug("Strategy: network only ["+e.url+"]",t),fetch(e)}var helpers=require("../helpers");module.exports=networkOnly;
|
||||
},{"../helpers":1}],12:[function(require,module,exports){
|
||||
"use strict";function cache(e,t){return helpers.openCache(t).then(function(t){return t.add(e)})}function uncache(e,t){return helpers.openCache(t).then(function(t){return t["delete"](e)})}function precache(e){Array.isArray(e)||(e=[e]),options.preCacheItems=options.preCacheItems.concat(e)}require("serviceworker-cache-polyfill");var options=require("./options"),router=require("./router"),helpers=require("./helpers"),strategies=require("./strategies");helpers.debug("Service Worker Toolbox is loading");var flatten=function(e){return e.reduce(function(e,t){return e.concat(t)},[])};self.addEventListener("install",function(e){var t=options.cache.name+"$$$inactive$$$";helpers.debug("install event fired"),helpers.debug("creating cache ["+t+"]"),e.waitUntil(helpers.openCache({cache:{name:t}}).then(function(e){return Promise.all(options.preCacheItems).then(flatten).then(function(t){return helpers.debug("preCache list: "+(t.join(", ")||"(none)")),e.addAll(t)})}))}),self.addEventListener("activate",function(e){helpers.debug("activate event fired");var t=options.cache.name+"$$$inactive$$$";e.waitUntil(helpers.renameCache(t,options.cache.name))}),self.addEventListener("fetch",function(e){var t=router.match(e.request);t?e.respondWith(t(e.request)):router["default"]&&"GET"===e.request.method&&e.respondWith(router["default"](e.request))}),module.exports={networkOnly:strategies.networkOnly,networkFirst:strategies.networkFirst,cacheOnly:strategies.cacheOnly,cacheFirst:strategies.cacheFirst,fastest:strategies.fastest,router:router,options:options,cache:cache,uncache:uncache,precache:precache};
|
||||
},{"./helpers":1,"./options":3,"./router":5,"./strategies":9,"serviceworker-cache-polyfill":15}],13:[function(require,module,exports){
|
||||
function parse(e){for(var t,r=[],n=0,o=0,p="";null!=(t=PATH_REGEXP.exec(e));){var a=t[0],i=t[1],s=t.index;if(p+=e.slice(o,s),o=s+a.length,i)p+=i[1];else{p&&(r.push(p),p="");var u=t[2],c=t[3],l=t[4],f=t[5],g=t[6],x=t[7],h="+"===g||"*"===g,m="?"===g||"*"===g,y=u||"/",T=l||f||(x?".*":"[^"+y+"]+?");r.push({name:c||n++,prefix:u||"",delimiter:y,optional:m,repeat:h,pattern:escapeGroup(T)})}}return o<e.length&&(p+=e.substr(o)),p&&r.push(p),r}function compile(e){return tokensToFunction(parse(e))}function tokensToFunction(e){for(var t=new Array(e.length),r=0;r<e.length;r++)"object"==typeof e[r]&&(t[r]=new RegExp("^"+e[r].pattern+"$"));return function(r){for(var n="",o=r||{},p=0;p<e.length;p++){var a=e[p];if("string"!=typeof a){var i,s=o[a.name];if(null==s){if(a.optional)continue;throw new TypeError('Expected "'+a.name+'" to be defined')}if(isarray(s)){if(!a.repeat)throw new TypeError('Expected "'+a.name+'" to not repeat, but received "'+s+'"');if(0===s.length){if(a.optional)continue;throw new TypeError('Expected "'+a.name+'" to not be empty')}for(var u=0;u<s.length;u++){if(i=encodeURIComponent(s[u]),!t[p].test(i))throw new TypeError('Expected all "'+a.name+'" to match "'+a.pattern+'", but received "'+i+'"');n+=(0===u?a.prefix:a.delimiter)+i}}else{if(i=encodeURIComponent(s),!t[p].test(i))throw new TypeError('Expected "'+a.name+'" to match "'+a.pattern+'", but received "'+i+'"');n+=a.prefix+i}}else n+=a}return n}}function escapeString(e){return e.replace(/([.+*?=^!:${}()[\]|\/])/g,"\\$1")}function escapeGroup(e){return e.replace(/([=!:$\/()])/g,"\\$1")}function attachKeys(e,t){return e.keys=t,e}function flags(e){return e.sensitive?"":"i"}function regexpToRegexp(e,t){var r=e.source.match(/\((?!\?)/g);if(r)for(var n=0;n<r.length;n++)t.push({name:n,prefix:null,delimiter:null,optional:!1,repeat:!1,pattern:null});return attachKeys(e,t)}function arrayToRegexp(e,t,r){for(var n=[],o=0;o<e.length;o++)n.push(pathToRegexp(e[o],t,r).source);var p=new RegExp("(?:"+n.join("|")+")",flags(r));return attachKeys(p,t)}function stringToRegexp(e,t,r){for(var n=parse(e),o=tokensToRegExp(n,r),p=0;p<n.length;p++)"string"!=typeof n[p]&&t.push(n[p]);return attachKeys(o,t)}function tokensToRegExp(e,t){t=t||{};for(var r=t.strict,n=t.end!==!1,o="",p=e[e.length-1],a="string"==typeof p&&/\/$/.test(p),i=0;i<e.length;i++){var s=e[i];if("string"==typeof s)o+=escapeString(s);else{var u=escapeString(s.prefix),c=s.pattern;s.repeat&&(c+="(?:"+u+c+")*"),c=s.optional?u?"(?:"+u+"("+c+"))?":"("+c+")?":u+"("+c+")",o+=c}}return r||(o=(a?o.slice(0,-2):o)+"(?:\\/(?=$))?"),o+=n?"$":r&&a?"":"(?=\\/|$)",new RegExp("^"+o,flags(t))}function pathToRegexp(e,t,r){return t=t||[],isarray(t)?r||(r={}):(r=t,t=[]),e instanceof RegExp?regexpToRegexp(e,t,r):isarray(e)?arrayToRegexp(e,t,r):stringToRegexp(e,t,r)}var isarray=require("isarray");module.exports=pathToRegexp,module.exports.parse=parse,module.exports.compile=compile,module.exports.tokensToFunction=tokensToFunction,module.exports.tokensToRegExp=tokensToRegExp;var PATH_REGEXP=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^()])+)\\))?|\\(((?:\\\\.|[^()])+)\\))([+*?])?|(\\*))"].join("|"),"g");
|
||||
},{"isarray":14}],14:[function(require,module,exports){
|
||||
module.exports=Array.isArray||function(r){return"[object Array]"==Object.prototype.toString.call(r)};
|
||||
},{}],15:[function(require,module,exports){
|
||||
Cache.prototype.addAll||(Cache.prototype.addAll=function(t){function e(t){this.name="NetworkError",this.code=19,this.message=t}var r=this;return e.prototype=Object.create(Error.prototype),Promise.resolve().then(function(){if(arguments.length<1)throw new TypeError;return t=t.map(function(t){return t instanceof Request?t:String(t)}),Promise.all(t.map(function(t){"string"==typeof t&&(t=new Request(t));var r=new URL(t.url).protocol;if("http:"!==r&&"https:"!==r)throw new e("Invalid scheme");return fetch(t.clone())}))}).then(function(e){return Promise.all(e.map(function(e,n){return r.put(t[n],e)}))}).then(function(){})});
|
||||
},{}]},{},[12])(12)
|
||||
});
|
||||
|
||||
(global => {
|
||||
'use strict';
|
||||
|
||||
// Turn on debug logging, visible in the Developer Tools' console.
|
||||
global.toolbox.options.debug = true;
|
||||
|
||||
// Set up a handler for HTTP GET requests:
|
||||
// - /\.ytimg\.com\// will match any requests whose URL contains 'ytimg.com'.
|
||||
// A narrower RegExp could be used, but just checking for ytimg.com anywhere
|
||||
// in the URL should be fine for this sample.
|
||||
// - toolbox.cacheFirst let us to use the predefined cache strategy for those
|
||||
// requests.
|
||||
global.toolbox.router.get(/\.ytimg\.com\//, global.toolbox.cacheFirst, {
|
||||
// Use a dedicated cache for the responses, separate from the default cache.
|
||||
cache: {
|
||||
name: 'youtube-thumbnails',
|
||||
// Store up to 10 entries in that cache.
|
||||
maxEntries: 10,
|
||||
// Expire any entries that are older than 30 seconds.
|
||||
maxAgeSeconds: 30
|
||||
}
|
||||
});
|
||||
|
||||
// By default, all requests that don't match our custom handler will use the
|
||||
// toolbox.networkFirst cache strategy, and their responses will be stored in
|
||||
// the default cache.
|
||||
global.toolbox.router.default = global.toolbox.networkFirst;
|
||||
|
||||
// Boilerplate to ensure our service worker takes control of the page as soon
|
||||
// as possible.
|
||||
global.addEventListener('install',
|
||||
event => event.waitUntil(global.skipWaiting()));
|
||||
global.addEventListener('activate',
|
||||
event => event.waitUntil(global.clients.claim()));
|
||||
})(self);
|
||||
Reference in New Issue
Block a user