agendash 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/History.md CHANGED
@@ -1,3 +1,15 @@
1
+ # 4.0.0
2
+
3
+ - Drop support for node 12. Add support for node 18 and MongoDB 5.0.
4
+ - Fastify upgraded to v4. #215
5
+ - Various UI fixes. #212 #203
6
+ - Vulnerability fixes. #220
7
+ - Requeue and Delete job fixes. #226 #227
8
+
9
+ # 3.1.0
10
+
11
+ - Add Content-Security-Policy header to all server implementations
12
+
1
13
  # 3.0.0
2
14
 
3
15
  - BREAKING: Drop support for MongoDB v3.4, only >=v3.6 is supported now.
@@ -54,7 +54,7 @@ fastify.register(
54
54
  })
55
55
  );
56
56
 
57
- fastify.listen(program.port, function () {
57
+ fastify.listen({ port: program.port }, function () {
58
58
  console.log(
59
59
  `Agendash started http://localhost:${program.port}${program.path}`
60
60
  );
@@ -42,7 +42,7 @@ module.exports = function (agenda, options) {
42
42
 
43
43
  if (options.query && options.property) {
44
44
  if (options.isObjectId) {
45
- preMatch[options.property] = ObjectId(options.query);
45
+ preMatch[options.property] = new ObjectId(options.query);
46
46
  } else if (/^\d+$/.test(options.query)) {
47
47
  preMatch[options.property] = Number.parseInt(options.query, 10);
48
48
  } else {
@@ -61,6 +61,7 @@ module.exports = function (agenda, options) {
61
61
  { $match: preMatch },
62
62
  {
63
63
  $sort: {
64
+ repeatInterval: -1,
64
65
  nextRunAt: -1,
65
66
  lastRunAt: -1,
66
67
  lastFinishedAt: -1,
@@ -284,7 +285,7 @@ module.exports = function (agenda, options) {
284
285
  const collection = agenda._collection.collection || agenda._collection;
285
286
  const jobs = await collection
286
287
  .find({
287
- _id: { $in: jobIds.map((jobId) => ObjectId(jobId)) },
288
+ _id: { $in: jobIds.map((jobId) => new ObjectId(jobId)) },
288
289
  })
289
290
  .toArray();
290
291
  if (jobs.length === 0) {
@@ -306,7 +307,7 @@ module.exports = function (agenda, options) {
306
307
  }
307
308
 
308
309
  return agenda.cancel({
309
- _id: { $in: jobIds.map((jobId) => ObjectId(jobId)) },
310
+ _id: { $in: jobIds.map((jobId) => new ObjectId(jobId)) },
310
311
  });
311
312
  };
312
313
 
package/lib/csp.js ADDED
@@ -0,0 +1,28 @@
1
+ const CSP = {
2
+ "default-src": [
3
+ "'self'",
4
+ ],
5
+ "script-src": [
6
+ "https://code.jquery.com",
7
+ "https://cdn.jsdelivr.net",
8
+ "https://cdnjs.cloudflare.com",
9
+ "https://stackpath.bootstrapcdn.com",
10
+ "'unsafe-inline'",
11
+ "'unsafe-eval'",
12
+ "'self'",
13
+ ],
14
+ "style-src": [
15
+ "https://cdn.jsdelivr.net",
16
+ "https://stackpath.bootstrapcdn.com",
17
+ "https://fonts.googleapis.com",
18
+ "https://unpkg.com",
19
+ "'self'",
20
+ ],
21
+ "font-src": [
22
+ "https://fonts.gstatic.com",
23
+ ],
24
+ };
25
+
26
+ module.exports = Object.entries(CSP)
27
+ .map(([type, values]) => `${type} ${values.join(" ")}`)
28
+ .join("; ")
@@ -1,12 +1,19 @@
1
1
  const path = require("path");
2
2
  const express = require("express");
3
3
  const bodyParser = require("body-parser");
4
+ const csp = require("../csp");
4
5
 
5
6
  module.exports = (agendash) => {
6
7
  const { api, requeueJobs, deleteJobs, createJob } = agendash;
7
8
  const app = express();
8
9
  app.use(bodyParser.json());
9
10
  app.use(bodyParser.urlencoded({ extended: false }));
11
+
12
+ app.use((req, res, next) => {
13
+ res.header("Content-Security-Policy", csp);
14
+ next();
15
+ });
16
+
10
17
  app.use("/", express.static(path.join(__dirname, "../../public")));
11
18
 
12
19
  app.get("/api", async (request, response) => {
@@ -1,13 +1,15 @@
1
1
  const path = require("path");
2
+ const csp = require("../csp");
2
3
 
3
4
  module.exports = (agendash) => (instance, opts, done) => {
4
5
  const { api, requeueJobs, deleteJobs, createJob } = agendash;
5
6
 
6
- instance.register(require("fastify-static"), {
7
+ instance.register(require("@fastify/static"), {
7
8
  root: path.join(__dirname, "../../public"),
8
9
  });
9
10
 
10
11
  instance.get("/", function (req, reply) {
12
+ reply.header("Content-Security-Policy", csp);
11
13
  return reply.sendFile("index.html");
12
14
  });
13
15
 
@@ -1,11 +1,17 @@
1
1
  const path = require("path");
2
2
  const pack = require("../../package.json");
3
+ const csp = require("../csp");
3
4
 
4
5
  module.exports = (agendash) => {
5
6
  const { api, requeueJobs, deleteJobs, createJob } = agendash;
6
7
  return {
7
8
  pkg: pack,
8
9
  register: (server, options) => {
10
+ server.ext("onPreResponse", (req, h) => {
11
+ req.response.header("Content-Security-Policy", csp);
12
+ return h.continue;
13
+ });
14
+
9
15
  server.route([
10
16
  {
11
17
  method: "GET",
@@ -2,10 +2,16 @@ const path = require("path");
2
2
  const bodyParser = require("koa-bodyparser");
3
3
  const Router = require("koa-router");
4
4
  const koaStatic = require("koa-static");
5
+ const csp = require("../csp");
5
6
 
6
7
  module.exports = (agendash) => {
7
8
  const middlewares = [];
8
9
 
10
+ middlewares.push(async (ctx, next) => {
11
+ await next();
12
+ ctx.set("Content-Security-Policy", csp);
13
+ });
14
+
9
15
  middlewares.push(
10
16
  koaStatic(path.resolve(__dirname, "../../public"), { defer: true })
11
17
  );
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "agendash",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "A modern dashboard for Agenda.js with Pagination and Search capabilities",
5
5
  "main": "app.js",
6
6
  "bin": "bin/agendash-standalone.js",
7
7
  "engines": {
8
- "node": ">=10.0.0"
8
+ "node": ">=14.0.0"
9
9
  },
10
10
  "scripts": {
11
11
  "test": "run-p lint ava",
@@ -38,19 +38,19 @@
38
38
  ],
39
39
  "dependencies": {
40
40
  "agenda": "^4.2.1",
41
- "body-parser": "^1.15.0",
41
+ "body-parser": "^1.19.2",
42
42
  "commander": "^2.9.0",
43
- "express": "^4.0.0",
43
+ "express": "^4.17.3",
44
44
  "mongodb": "*",
45
45
  "semver": "^7.3.4"
46
46
  },
47
47
  "devDependencies": {
48
+ "@fastify/static": "^6.5.0",
48
49
  "@hapi/hapi": "^20.2.1",
49
50
  "@hapi/inert": "^6.0.5",
50
51
  "ava": "3.15.0",
51
52
  "eslint": "7.19.0",
52
- "fastify": "^3.24.0",
53
- "fastify-static": "^4.5.0",
53
+ "fastify": "^4.9.2",
54
54
  "koa": "2.13.1",
55
55
  "koa-bodyparser": "4.3.0",
56
56
  "koa-router": "10.0.0",
@@ -122,8 +122,20 @@ code {
122
122
  margin: 0 2px;
123
123
  }
124
124
 
125
+ .job-detail-dialog {
126
+ max-width: calc(100vw - 1.75rem * 2);
127
+ }
128
+ .modal-body > * {
129
+ flex: 0 1 auto;
130
+ }
131
+ .modal-body {
132
+ display: flex;
133
+ flex-flow: column;
134
+ max-height: calc(100vh - 195px);
135
+ }
136
+
125
137
  .json-editor {
126
- height: 150px;
138
+ flex: 1 1 auto;
127
139
  }
128
140
  .card-responsive-title-container {
129
141
  display: flex;
@@ -13,7 +13,7 @@ const jobDetail = Vue.component("job-detail", {
13
13
  template: `
14
14
  <div class="modal fade" id="modalData" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
15
15
  <!-- Modal -->
16
- <div class="modal-dialog" role="document">
16
+ <div class="modal-dialog job-detail-dialog" role="document">
17
17
  <div class="modal-content">
18
18
  <div class="modal-header">
19
19
  <h5 class="modal-title" id="exampleModalLabel">Job Data - {{job.job.name}}</h5>
@@ -4,10 +4,10 @@ const jobList = Vue.component("job-list", {
4
4
  currentSort: "name",
5
5
  currentSortDir: "asc",
6
6
  }),
7
- props: ["jobs", "pagesize", "pagenumber", "sendClean", "loading"],
7
+ props: ["jobs", "pagesize", "pagenumber", "totalPages", "sendClean", "loading"],
8
8
  computed: {
9
9
  sortedJobs: function () {
10
- return this.jobs.sort((a, b) => {
10
+ var sortedJobs = this.jobs.sort((a, b) => {
11
11
  let displayA, displayB;
12
12
  if (this.currentSort === "name") {
13
13
  displayA = a.job[this.currentSort]
@@ -26,6 +26,12 @@ const jobList = Vue.component("job-list", {
26
26
  if (displayA > displayB) return 1 * modifier;
27
27
  return 0;
28
28
  });
29
+
30
+ /** Show recurring jobs first */
31
+ return Array.prototype.concat(
32
+ sortedJobs.filter(job => job.repeating === true),
33
+ sortedJobs.filter(job => job.repeating === false),
34
+ )
29
35
  },
30
36
  },
31
37
  watch: {
@@ -226,14 +232,17 @@ const jobList = Vue.component("job-list", {
226
232
  <nav aria-label="Page navigation example">
227
233
  <ul class="pagination">
228
234
  <li class="page-item" :class="pagenumber === 1 ? 'disabled': ''"><a class="page-link" @click="$emit('pagechange', 'prev')">Previous</a></li>
229
- <!-- <li class="page-item" :class="pagenumber === 1 ? 'disabled': ''"><a class="page-link" @click="$emit('pagechange', 'prev')">{{pagenumber -1}}</a></li>
230
- <li class="page-item active"><a class="page-link">{{pagenumber}}</a></li>
231
- <li class="page-item"><a style="cursor:pointer;" class="page-link" @click="$emit('pagechange', 'next')">{{pagenumber +1}}</a></li> -->
232
- <li class="page-item"><a style="cursor:pointer;" class="page-link" @click="$emit('pagechange', 'next')">Next</a></li>
235
+ <li class="page-item" :class="pagenumber >= totalPages ? 'disabled': ''"> <a style="cursor:pointer;" class="page-link" @click="$emit('pagechange', 'next')">Next</a> </li>
233
236
  </ul>
234
237
  </nav>
235
238
  </div>
236
239
  </div>
240
+
241
+ <div class="row">
242
+ <div class="col d-flex justify-content-center">
243
+ Page: {{pagenumber}} / {{totalPages}}
244
+ </div>
245
+ </div>
237
246
  </div>
238
247
  `,
239
248
  });
@@ -2,9 +2,10 @@ const app = Vue.component("app", {
2
2
  data: () => ({
3
3
  jobs: [],
4
4
  overview: [],
5
- refresh: 60,
5
+ refresh: 30,
6
6
  showDetail: false,
7
7
  pagenumber: 1,
8
+ totalPages: 0,
8
9
  showConfirm: false,
9
10
  showConfirmMulti: false,
10
11
  showConfirmRequeue: false,
@@ -13,7 +14,7 @@ const app = Vue.component("app", {
13
14
  jobData: {},
14
15
  deletec: false,
15
16
  requeuec: false,
16
- pagesize: 15,
17
+ pagesize: 50,
17
18
  sendClean: false,
18
19
  createc: false,
19
20
  property: "",
@@ -66,14 +67,19 @@ const app = Vue.component("app", {
66
67
  this.showNewJob = true;
67
68
  },
68
69
  searchForm(name, search, property, limit, skip, refresh, state, object) {
69
- (this.pagesize = limit ? limit : this.pagesize),
70
- (this.name = name),
71
- (this.search = search),
72
- (this.property = property),
73
- (this.skip = skip),
74
- (this.refresh = refresh),
75
- (this.state = state),
76
- (this.object = object ? object : this.object),
70
+ this.pagesize = limit ? limit : this.pagesize
71
+ this.name = name
72
+ this.search = search
73
+ this.property = property
74
+ this.skip = skip
75
+ this.refresh = refresh
76
+ this.state = state
77
+ this.object = object ? object : this.object
78
+
79
+ // Form changed, reset the pagination state
80
+ this.pagenumber = 1
81
+ this.totalPages = 1
82
+
77
83
  this.fetchData(
78
84
  this.name,
79
85
  this.search,
@@ -120,9 +126,9 @@ const app = Vue.component("app", {
120
126
  name = "",
121
127
  search = "",
122
128
  property = "",
123
- limit = 15,
129
+ limit = 50,
124
130
  skip = 0,
125
- refresh = 60,
131
+ refresh = 30,
126
132
  state = "",
127
133
  object
128
134
  ) {
@@ -143,6 +149,7 @@ const app = Vue.component("app", {
143
149
  this.object = object;
144
150
  this.overview = data.overview;
145
151
  this.loading = false;
152
+ this.totalPages = data.totalPages;
146
153
  },
147
154
  () => {
148
155
  this.loading = false;
@@ -229,7 +236,7 @@ const app = Vue.component("app", {
229
236
  >
230
237
  </sidebar>
231
238
  </div>
232
- <main role="main" class="col-md-10 ml-sm-auto col-lg-10 px-4">
239
+ <main role="main" class="col-md-10 ml-sm-auto col-lg-10 px-4 pt-3 pb-5">
233
240
  <div class="col-12">
234
241
  <topbar v-on:search-form="searchForm"
235
242
  :name='name'
@@ -249,6 +256,7 @@ const app = Vue.component("app", {
249
256
  v-on:pagechange="pagechange"
250
257
  :pagesize="pagesize"
251
258
  :pagenumber='pagenumber'
259
+ :totalPages='totalPages'
252
260
  :skip="skip"
253
261
  :jobs="jobs"
254
262
  :sendClean='sendClean'
@@ -63,7 +63,7 @@ const sidebar = Vue.component("sidebar", {
63
63
  },
64
64
  },
65
65
  template: `
66
- <div class="col sidebar">
66
+ <div class="col sidebar pt-4">
67
67
  <div class="row">
68
68
  <div class="col ">
69
69
  <button data-toggle="modal" data-target="#modalNewJob" @click="$emit('new-job')" data-placement="top" title="Add a new job" class="btn btn-block btn-outline-success"><i class="oi oi-plus IcoInButton"></i> New Job</button>
@@ -4,9 +4,9 @@ const topbar = Vue.component("topbar", {
4
4
  // search: '',
5
5
  // property: 'data.id',
6
6
  // name: '',
7
- limit: 15,
7
+ limit: 50,
8
8
  skip: 0,
9
- refresh: 60,
9
+ refresh: 30,
10
10
  // state: '',
11
11
  object: false,
12
12
  stateobject: [