diff --git a/config.ts b/config.ts
index 5ee5563..84c4f31 100644
--- a/config.ts
+++ b/config.ts
@@ -36,7 +36,7 @@ const config: Config = {
DEFAULT_QUOTA: 2 * 1024 * 1024 * 1024 * 8,
MAX_FILE_SIZE: 10 * 1024 * 1024, // in b
MAX_REPO_SIZE: 500 * 8 * 1024, // in kb
- ENABLE_DOWNLOAD: false,
+ ENABLE_DOWNLOAD: true,
AUTH_CALLBACK: "http://localhost:5000/github/auth",
ANONYMIZATION_MASK: "XXXX",
PORT: 5000,
diff --git a/package-lock.json b/package-lock.json
index 69b5d77..f0aa5ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,7 +7,7 @@
"": {
"name": "anonymous_github",
"version": "2.1.0",
- "license": "MIT",
+ "license": "GPL-3.0",
"dependencies": {
"@octokit/oauth-app": "^3.3.2",
"@octokit/rest": "^18.5.3",
@@ -25,6 +25,7 @@
"istextorbinary": "^6.0.0",
"mime-types": "^2.1.30",
"mongoose": "^5.13.5",
+ "node-schedule": "^2.0.0",
"parse-github-url": "^1.0.2",
"passport": "^0.4.1",
"passport-github2": "^0.1.12",
@@ -55,6 +56,9 @@
"nodemon": "^2.0.7",
"ts-node": "^10.1.0",
"typescript": "^4.3.5"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/tdurieux"
}
},
"node_modules/@cspotcode/source-map-consumer": {
@@ -1278,6 +1282,18 @@
"node": ">=8"
}
},
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -1597,6 +1613,18 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
+ "node_modules/cron-parser": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz",
+ "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==",
+ "dependencies": {
+ "is-nan": "^1.3.2",
+ "luxon": "^1.26.0"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -1688,6 +1716,17 @@
"node": ">=10"
}
},
+ "node_modules/define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dependencies": {
+ "object-keys": "^1.0.12"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -2141,6 +2180,11 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -2159,6 +2203,19 @@
"node": "*"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+ "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -2273,6 +2330,17 @@
"gunzip-maybe": "bin.js"
}
},
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -2282,6 +2350,17 @@
"node": ">=8"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
+ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-yarn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
@@ -2488,6 +2567,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-nan": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
+ "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-npm": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz",
@@ -2730,6 +2824,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/long-timeout": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz",
+ "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ="
+ },
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@@ -2738,6 +2837,14 @@
"node": ">=8"
}
},
+ "node_modules/luxon": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz",
+ "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -3088,6 +3195,19 @@
"node": "4.x || >=6.0.0"
}
},
+ "node_modules/node-schedule": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.0.0.tgz",
+ "integrity": "sha512-cHc9KEcfiuXxYDU+HjsBVo2FkWL1jRAUoczFoMIzRBpOA4p/NRHuuLs85AWOLgKsHtSPjN8csvwIxc2SqMv+CQ==",
+ "dependencies": {
+ "cron-parser": "^3.1.0",
+ "long-timeout": "0.1.1",
+ "sorted-array-functions": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/nodemon": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.12.tgz",
@@ -3177,6 +3297,14 @@
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE="
},
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -4021,6 +4149,11 @@
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
},
+ "node_modules/sorted-array-functions": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz",
+ "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA=="
+ },
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
@@ -5944,6 +6077,15 @@
"responselike": "^2.0.0"
}
},
+ "call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "requires": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ }
+ },
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -6196,6 +6338,15 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
+ "cron-parser": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz",
+ "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==",
+ "requires": {
+ "is-nan": "^1.3.2",
+ "luxon": "^1.26.0"
+ }
+ },
"crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -6259,6 +6410,14 @@
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
},
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -6607,6 +6766,11 @@
"dev": true,
"optional": true
},
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -6619,6 +6783,16 @@
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"dev": true
},
+ "get-intrinsic": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+ "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ }
+ },
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -6700,12 +6874,25 @@
"through2": "^2.0.3"
}
},
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
+ "has-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
+ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
+ },
"has-yarn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
@@ -6866,6 +7053,15 @@
"is-path-inside": "^3.0.1"
}
},
+ "is-nan": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
+ "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3"
+ }
+ },
"is-npm": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz",
@@ -7056,11 +7252,21 @@
"is-unicode-supported": "^0.1.0"
}
},
+ "long-timeout": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz",
+ "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ="
+ },
"lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
},
+ "luxon": {
+ "version": "1.28.0",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz",
+ "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ=="
+ },
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -7303,6 +7509,16 @@
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
+ "node-schedule": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.0.0.tgz",
+ "integrity": "sha512-cHc9KEcfiuXxYDU+HjsBVo2FkWL1jRAUoczFoMIzRBpOA4p/NRHuuLs85AWOLgKsHtSPjN8csvwIxc2SqMv+CQ==",
+ "requires": {
+ "cron-parser": "^3.1.0",
+ "long-timeout": "0.1.1",
+ "sorted-array-functions": "^1.3.0"
+ }
+ },
"nodemon": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.12.tgz",
@@ -7362,6 +7578,11 @@
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE="
},
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
+ },
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -8032,6 +8253,11 @@
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
},
+ "sorted-array-functions": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz",
+ "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA=="
+ },
"sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
diff --git a/package.json b/package.json
index 6b96f49..95ae12b 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"istextorbinary": "^6.0.0",
"mime-types": "^2.1.30",
"mongoose": "^5.13.5",
+ "node-schedule": "^2.0.0",
"parse-github-url": "^1.0.2",
"passport": "^0.4.1",
"passport-github2": "^0.1.12",
diff --git a/public/i18n/locale-en.json b/public/i18n/locale-en.json
index eb655d7..a84e234 100644
--- a/public/i18n/locale-en.json
+++ b/public/i18n/locale-en.json
@@ -27,6 +27,7 @@
"user_not_found": "The current user is not found in the database",
"not_connected": "User is not connected",
"page_not_supported_on_different_branch": "Anonymized GitHub pages are only supported on the same branch.",
- "page_not_activated": "Anonymized GitHub page is not enabled."
+ "page_not_activated": "Anonymized GitHub page is not enabled.",
+ "conf_not_activated": "The conference is not activated."
}
}
diff --git a/public/partials/anonymize.htm b/public/partials/anonymize.htm
index c1205e5..f4be318 100644
--- a/public/partials/anonymize.htm
+++ b/public/partials/anonymize.htm
@@ -129,9 +129,11 @@
id="commit"
name="commit"
ng-model="source.commit"
+ required
+ ng-class="{'is-invalid': anonymize.commit.$invalid}"
/>
The commit to anonymizeThe commit to anonymize.
@@ -173,9 +175,10 @@
rows="3"
ng-model="terms"
ng-model-options="{ debounce: 250 }"
+ ng-class="{'is-invalid': anonymize.terms.$invalid}"
>
One term per line. Each term will be replaced by XXXOne term per line. Each term will be replaced by XXX.
- In which conference the paper will be submitted.
+ {{conference_data.name}}
+ will expire on {{conference_data.endDate | date}}.
+
+
+ The conference is not activated.
+
@@ -341,10 +358,11 @@
How the repository will be anonymized. Stream mode will
request the content on the flight. This is the only
- option for repositories bigger than {{site_options.MAX_REPO_SIZE * 1024| humanFileSize}}. Download will
- download the repository the repository on the
- anonymous.4open.science server, it is faster and offer
- more features.
diff --git a/public/partials/conferences.htm b/public/partials/conferences.htm
new file mode 100644
index 0000000..4df93df
--- /dev/null
+++ b/public/partials/conferences.htm
@@ -0,0 +1,225 @@
+
+
+
Conferences
+
+
+ -
+
+
+
+
+
+
+
+ Conference ID: '{{conference.conferenceID}}'
+
+
+
+
+
+ {{::conference.nbRepositories || 0 | number}}
+
+
+ Total: {{conference.price || 0 | number}} €
+
+
+
+ From {{conference.startDate | date}} to {{conference.endDate |
+ date}}
+
+
+
+
+ -
+ You have no conference. You a create a
+ new one.
+
+
+
+
diff --git a/public/partials/dashboard.htm b/public/partials/dashboard.htm
index d5c8d20..fdc14c3 100644
--- a/public/partials/dashboard.htm
+++ b/public/partials/dashboard.htm
@@ -215,42 +215,46 @@
+
+
+ {{repo.conference}}
+
- {{repo.options.terms.length | number}}
+ {{::repo.options.terms.length | number}}
- {{repo.size |
+ {{::repo.size |
humanFileSize}}
- {{repo.pageView | number}}
+ {{::repo.pageView | number}}
- Last view: {{repo.lastView | humanTime}}
Anonymize
+
+ Conferences
+
-
diff --git a/public/partials/newConference.htm b/public/partials/newConference.htm
new file mode 100644
index 0000000..b395381
--- /dev/null
+++ b/public/partials/newConference.htm
@@ -0,0 +1,443 @@
+
+
Create a conference
+
+ A conference allow the the chairs to access the complete list of anonymized
+ repositories. It also allows to increase the quota for authors.
+
+
+
diff --git a/public/script/app.js b/public/script/app.js
index 9258f24..0a4df51 100644
--- a/public/script/app.js
+++ b/public/script/app.js
@@ -43,6 +43,26 @@ angular
controller: "statusController",
title: "Repository status - Anonymous GitHub",
})
+ .when("/conferences", {
+ templateUrl: "/partials/conferences.htm",
+ controller: "conferencesController",
+ title: "Conferences - Anonymous GitHub",
+ })
+ .when("/conference/new", {
+ templateUrl: "/partials/newConference.htm",
+ controller: "newConferenceController",
+ title: "Add a conference - Anonymous GitHub",
+ })
+ .when("/conference/:conferenceId/edit", {
+ templateUrl: "/partials/newConference.htm",
+ controller: "newConferenceController",
+ title: "Edit conference - Anonymous GitHub",
+ })
+ .when("/conference/:conferenceId", {
+ templateUrl: "/partials/conference.htm",
+ controller: "conferenceController",
+ title: "Conference - Anonymous GitHub",
+ })
.when("/faq", {
templateUrl: "/partials/faq.htm",
controller: "faqController",
@@ -619,7 +639,7 @@ angular
body: `The repository ${repo.repoId} is going to be removed.`,
};
$scope.toasts.push(toast);
-
+
$http.post(`/api/repo/${repo.repoId}/refresh`).then(() => {
toast.title = `${repo.repoId} is refreshed.`;
toast.body = `The repository ${repo.repoId} is refreshed.`;
@@ -878,6 +898,35 @@ angular
$scope.readme = res.data;
}
+ function getConference() {
+ if (!$scope.conference) return;
+ $http.get("/api/conferences/" + $scope.conference).then(
+ (res) => {
+ $scope.conference_data = res.data;
+ $scope.conference_data.startDate = new Date(
+ $scope.conference_data.startDate
+ );
+ $scope.conference_data.endDate = new Date(
+ $scope.conference_data.endDate
+ );
+
+ $scope.options.expirationDate = new Date(
+ $scope.conference_data.endDate
+ );
+ $scope.options.expirationMode = "remove";
+
+ $scope.options.update = $scope.conference_data.options.update;
+ $scope.options.image = $scope.conference_data.options.image;
+ $scope.options.pdf = $scope.conference_data.options.pdf;
+ $scope.options.notebook = $scope.conference_data.options.notebook;
+ $scope.options.link = $scope.conference_data.options.link;
+ },
+ (err) => {
+ $scope.conference_data = null;
+ }
+ );
+ }
+
function anonymize() {
const urlRegex =
/\b((https?|ftp|file):\/\/)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]\b\/?>?/g;
@@ -950,6 +999,7 @@ angular
$scope.anonymize.repoUrl.$setValidity("used", true);
$scope.anonymize.repoUrl.$setValidity("missing", true);
$scope.anonymize.repoUrl.$setValidity("access", true);
+ $scope.anonymize.conference.$setValidity("activated", true);
$scope.anonymize.terms.$setValidity("format", true);
$scope.anonymize.terms.$setValidity("format", true);
}
@@ -980,6 +1030,9 @@ angular
case "repo_not_accessible":
$scope.anonymize.repoUrl.$setValidity("access", false);
break;
+ case "conf_not_activated":
+ $scope.anonymize.conference.$setValidity("activated", false);
+ break;
default:
$scope.anonymize.$setValidity("error", false);
break;
@@ -1039,6 +1092,9 @@ angular
});
};
+ $scope.$watch("conference", async (v) => {
+ getConference();
+ });
$scope.$watch("source.branch", async (v) => {
const selected = $scope.branches.filter(
(f) => f.name == $scope.source.branch
@@ -1318,4 +1374,263 @@ angular
init();
},
+ ])
+ .controller("conferencesController", [
+ "$scope",
+ "$http",
+ "$location",
+ function ($scope, $http, $location) {
+ $scope.$watch("user.status", () => {
+ if ($scope.user == null) {
+ $location.url("/");
+ }
+ });
+ if ($scope.user == null) {
+ $location.url("/");
+ }
+
+ $scope.conferences = [];
+ $scope.search = "";
+ $scope.filters = {
+ status: { ready: true, expired: false, removed: false },
+ };
+ $scope.orderBy = "name";
+
+ $scope.removeConference = function (conf) {
+ if (
+ confirm(
+ `Are you sure that you want to remove the conference ${conf.name}? All the repositories linked to this conference will expire.`
+ )
+ ) {
+ const toast = {
+ title: `Removing ${conf.name}...`,
+ date: new Date(),
+ body: `The conference ${conf.name} is going to be removed.`,
+ };
+ $scope.toasts.push(toast);
+ $http.delete(`/api/conferences/${conf.conferenceID}`).then(() => {
+ toast.title = `${conf.name} is removed.`;
+ toast.body = `The conference ${conf.name} is removed.`;
+ getConferences();
+ });
+ }
+ };
+
+ function getConferences() {
+ $http.get("/api/conferences/").then(
+ (res) => {
+ $scope.conferences = res.data || [];
+ },
+ (err) => {
+ console.error(err);
+ }
+ );
+ }
+ getConferences();
+
+ $scope.conferenceFilter = (conference) => {
+ if ($scope.filters.status[conference.status] == false) return false;
+
+ if ($scope.search.trim().length == 0) return true;
+
+ if (conference.name.indexOf($scope.search) > -1) return true;
+ if (conference.conferenceID.indexOf($scope.search) > -1) return true;
+
+ return false;
+ };
+ },
+ ])
+ .controller("newConferenceController", [
+ "$scope",
+ "$http",
+ "$location",
+ "$routeParams",
+ function ($scope, $http, $location, $routeParams) {
+ $scope.$watch("user.status", () => {
+ if ($scope.user == null) {
+ $location.url("/");
+ }
+ });
+ if ($scope.user == null) {
+ $location.url("/");
+ }
+
+ $scope.plans = [];
+ $scope.editionMode = false;
+
+ function getConference() {
+ $http
+ .get("/api/conferences/" + $routeParams.conferenceId)
+ .then((res) => {
+ $scope.options = res.data;
+ });
+ }
+ if ($routeParams.conferenceId) {
+ $scope.editionMode = true;
+ getConference();
+ }
+
+ function getPlans() {
+ $http.get("/api/conferences/plans").then((res) => {
+ $scope.plans = res.data;
+
+ $scope.plan = $scope.plans.filter(
+ (f) => f.id == $scope.options.plan.planID
+ )[0];
+ });
+ }
+ getPlans();
+ const start = new Date();
+ start.setDate(1);
+ console.log(start);
+ start.setMonth(start.getMonth() + 1);
+ const end = new Date();
+ end.setMonth(start.getMonth() + 7, 0);
+ $scope.options = {
+ startDate: start,
+ endDate: end,
+ plan: {
+ planID: "free_conference",
+ },
+ options: {
+ link: true,
+ image: true,
+ pdf: true,
+ notebook: true,
+ update: true,
+ page: true,
+ },
+ };
+ $scope.plan = null;
+
+ $scope.$watch("options.plan.planID", () => {
+ console.log($scope.plans, $scope.options);
+ $scope.plan = $scope.plans.filter(
+ (f) => f.id == $scope.options.plan.planID
+ )[0];
+ });
+
+ function resetValidity() {
+ $scope.conference.name.$setValidity("required", true);
+ $scope.conference.conferenceID.$setValidity("pattern", true);
+ $scope.conference.conferenceID.$setValidity("required", true);
+ $scope.conference.conferenceID.$setValidity("used", true);
+ $scope.conference.startDate.$setValidity("required", true);
+ $scope.conference.startDate.$setValidity("invalid", true);
+ $scope.conference.endDate.$setValidity("required", true);
+ $scope.conference.endDate.$setValidity("invalid", true);
+ $scope.conference.$setValidity("error", true);
+ }
+
+ function displayErrorMessage(message) {
+ switch (message) {
+ case "conf_name_missing":
+ $scope.conference.name.$setValidity("required", false);
+ break;
+ case "conf_id_missing":
+ $scope.conference.conferenceID.$setValidity("required", false);
+ break;
+ case "conf_id_format":
+ $scope.conference.conferenceID.$setValidity("pattern", false);
+ break;
+ case "conf_id_used":
+ $scope.conference.conferenceID.$setValidity("used", false);
+ break;
+ case "conf_start_date_missing":
+ $scope.conference.startDate.$setValidity("required", false);
+ break;
+ case "conf_end_date_missing":
+ $scope.conference.endDate.$setValidity("required", false);
+ break;
+ case "conf_start_date_invalid":
+ $scope.conference.startDate.$setValidity("invalid", false);
+ break;
+ case "conf_end_date_invalid":
+ $scope.conference.endDate.$setValidity("invalid", false);
+ break;
+ default:
+ $scope.conference.$setValidity("error", false);
+ break;
+ }
+ }
+
+ $scope.submit = function () {
+ const toast = {
+ title: `Creating ${$scope.options.name}...`,
+ date: new Date(),
+ body: `The conference ${$scope.options.conferenceID} is in creation.`,
+ };
+ if ($scope.editionMode) {
+ toast.title = `Updating ${$scope.options.name}...`;
+ toast.body = `The conference '${$scope.options.conferenceID}' is updating.`;
+ }
+ $scope.toasts.push(toast);
+ resetValidity();
+ $http
+ .post(
+ "/api/conferences/" +
+ ($scope.editionMode ? $scope.options.conferenceID : ""),
+ $scope.options
+ )
+ .then(
+ () => {
+ if (!$scope.editionMode) {
+ toast.title = `${$scope.options.name} created`;
+ toast.body = `The conference '${$scope.options.conferenceID}' is created.`;
+ } else {
+ toast.title = `${$scope.options.name} updated`;
+ toast.body = `The conference '${$scope.options.conferenceID}' is updated.`;
+ }
+ $location.url("/conference/" + $scope.options.conferenceID);
+ },
+ (error) => {
+ displayErrorMessage(error.data.error);
+ $scope.removeToast(toast);
+ }
+ );
+ };
+ },
+ ])
+ .controller("conferenceController", [
+ "$scope",
+ "$http",
+ "$location",
+ "$routeParams",
+ function ($scope, $http, $location, $routeParams) {
+ $scope.$watch("user.status", () => {
+ if ($scope.user == null) {
+ $location.url("/");
+ }
+ });
+ if ($scope.user == null) {
+ $location.url("/");
+ }
+ $scope.conference = null;
+
+ $scope.search = "";
+ $scope.filters = {
+ status: { ready: true, expired: false, removed: false },
+ };
+ $scope.orderBy = "-anonymizeDate";
+
+ $scope.repoFiler = (repo) => {
+ if ($scope.filters.status[repo.status] == false) return false;
+
+ if ($scope.search.trim().length == 0) return true;
+
+ if (repo.source.fullName.indexOf($scope.search) > -1) return true;
+ if (repo.repoId.indexOf($scope.search) > -1) return true;
+
+ return false;
+ };
+
+ function getConference() {
+ $http
+ .get("/api/conferences/" + $routeParams.conferenceId)
+ .then((res) => {
+ $scope.conference = res.data;
+ });
+ }
+ getConference();
+ },
]);
diff --git a/src/Conference.ts b/src/Conference.ts
new file mode 100644
index 0000000..de48514
--- /dev/null
+++ b/src/Conference.ts
@@ -0,0 +1,113 @@
+import AnonymizedRepositoryModel from "./database/anonymizedRepositories/anonymizedRepositories.model";
+import { IConferenceDocument } from "./database/conference/conferences.types";
+import Repository from "./Repository";
+import { ConferenceStatus } from "./types";
+
+export default class Conference {
+ private _data: IConferenceDocument;
+ private _repositories: Repository[] = null;
+
+ constructor(data: IConferenceDocument) {
+ this._data = data;
+ }
+
+ /**
+ * Update the status of the conference
+ * @param status the new status
+ * @param errorMessage a potential error message to display
+ */
+ async updateStatus(status: ConferenceStatus, errorMessage?: string) {
+ this._data.status = status;
+ return this._data.save();
+ }
+
+ /**
+ * Check if the conference is expired
+ */
+ isExpired() {
+ return this._data.endDate < new Date();
+ }
+
+ /**
+ * Expire the conference
+ */
+ async expire() {
+ await this.updateStatus("expired");
+ await Promise.all(
+ (await this.repositories()).map(async (conf) => await conf.expire())
+ );
+ }
+
+ /**
+ * Remove the conference
+ */
+ async remove() {
+ await this.updateStatus("removed");
+ await Promise.all(
+ (await this.repositories()).map(async (conf) => await conf.remove())
+ );
+ }
+
+ /**
+ * Returns the list of repositories of this conference
+ *
+ * @returns the list of repositories of this conference
+ */
+ async repositories(): Promise {
+ if (this._repositories) return this._repositories;
+ const repoIds = this._data.repositories
+ .filter((r) => !r.removeDate)
+ .map((r) => r.id)
+ .filter((f) => f);
+ this._repositories = (
+ await AnonymizedRepositoryModel.find({
+ _id: { $in: repoIds },
+ })
+ ).map((r) => new Repository(r));
+ return this._repositories;
+ }
+
+ get ownerIDs() {
+ return this._data?.owners;
+ }
+
+ get quota() {
+ return this._data.plan.quota;
+ }
+
+ get status() {
+ return this._data.status;
+ }
+
+ toJSON(opt?: { billing: boolean }): any {
+ const pricePerHourPerRepo = this._data.plan.pricePerRepository / 30;
+ let price = 0;
+ const today =
+ new Date() > this._data.endDate ? this._data.endDate : new Date();
+ this._data.repositories.forEach((r) => {
+ const removeDate =
+ r.removeDate && r.removeDate < today ? r.removeDate : today;
+ price +=
+ (Math.max(removeDate.getTime() - r.addDate.getTime(), 0) /
+ 1000 /
+ 60 /
+ 60 /
+ 24) *
+ pricePerHourPerRepo;
+ });
+ return {
+ conferenceID: this._data.conferenceID,
+ name: this._data.name,
+ url: this._data.url,
+ startDate: this._data.startDate,
+ endDate: this._data.endDate,
+ status: this._data.status,
+ billing: this._data.billing,
+ options: this._data.options,
+ plan: this._data.plan,
+ price,
+ nbRepositories: this._data.repositories.filter((r) => !r.removeDate)
+ .length,
+ };
+ }
+}
diff --git a/src/Repository.ts b/src/Repository.ts
index 0922951..7738e40 100644
--- a/src/Repository.ts
+++ b/src/Repository.ts
@@ -11,6 +11,8 @@ import UserModel from "./database/users/users.model";
import { IAnonymizedRepositoryDocument } from "./database/anonymizedRepositories/anonymizedRepositories.types";
import { anonymizeStream } from "./anonymize-utils";
import GitHubBase from "./source/GitHubBase";
+import Conference from "./Conference";
+import ConferenceModel from "./database/conference/conferences.model";
export default class Repository {
private _model: IAnonymizedRepositoryDocument;
@@ -247,6 +249,21 @@ export default class Repository {
return this._model.size;
}
+ /**
+ * Returns the conference of the repository
+ *
+ * @returns conference of the repository
+ */
+ async conference(): Promise {
+ if (!this._model.conference) {
+ return null;
+ }
+ const conference = await ConferenceModel.findOne({
+ conferenceID: this._model.conference,
+ });
+ return new Conference(conference);
+ }
+
/***** Getters ********/
get repoId() {
diff --git a/src/database/conference/conferences.model.ts b/src/database/conference/conferences.model.ts
new file mode 100644
index 0000000..2b5d9f4
--- /dev/null
+++ b/src/database/conference/conferences.model.ts
@@ -0,0 +1,12 @@
+import * as mongoose from "mongoose";
+const { model } = mongoose;
+
+import { IConferenceDocument, IConferenceModel } from "./conferences.types";
+import ConferenceSchema from "./conferences.schema";
+
+const ConferenceModel = model(
+ "Conference",
+ ConferenceSchema
+) as IConferenceModel;
+
+export default ConferenceModel;
diff --git a/src/database/conference/conferences.schema.ts b/src/database/conference/conferences.schema.ts
new file mode 100644
index 0000000..cb520f4
--- /dev/null
+++ b/src/database/conference/conferences.schema.ts
@@ -0,0 +1,59 @@
+import * as mongoose from "mongoose";
+const { Schema } = mongoose;
+
+const RepositorySchema = new Schema({
+ name: String,
+ conferenceID: {
+ type: String,
+ index: { unique: true },
+ },
+ url: String,
+ startDate: Date,
+ endDate: Date,
+ status: String,
+ owners: { type: [mongoose.Schema.Types.ObjectId] },
+ repositories: {
+ type: [
+ {
+ id: { type: mongoose.Schema.Types.ObjectId },
+ addDate: { type: Date },
+ removeDate: { type: Date },
+ },
+ ],
+ },
+ options: {
+ expirationMode: String,
+ expirationDate: Date,
+ update: Boolean,
+ image: Boolean,
+ pdf: Boolean,
+ notebook: Boolean,
+ link: Boolean,
+ page: Boolean,
+ },
+ dateOfEntry: {
+ type: Date,
+ default: new Date(),
+ },
+ plan: {
+ planID: String,
+ pricePerRepository: Number,
+ quota: {
+ repository: Number,
+ size: Number,
+ file: Number,
+ },
+ },
+ billing: {
+ name: String,
+ email: String,
+ address: String,
+ address2: String,
+ city: String,
+ zip: String,
+ country: String,
+ vat: String,
+ },
+});
+
+export default RepositorySchema;
diff --git a/src/database/conference/conferences.types.ts b/src/database/conference/conferences.types.ts
new file mode 100644
index 0000000..50ccc5e
--- /dev/null
+++ b/src/database/conference/conferences.types.ts
@@ -0,0 +1,49 @@
+import * as mongoose from "mongoose";
+import { ConferenceStatus } from "../../types";
+
+export interface IConference {
+ name: string;
+ conferenceID: string;
+ startDate: Date;
+ endDate: Date;
+ url: string;
+ status: ConferenceStatus;
+ owners: string[];
+ repositories: {
+ id: string;
+ addDate: Date;
+ removeDate?: Date;
+ }[];
+ options: {
+ expirationMode: "never" | "redirect" | "remove";
+ expirationDate?: Date;
+ update: boolean;
+ image: boolean;
+ pdf: boolean;
+ notebook: boolean;
+ link: boolean;
+ page: boolean;
+ };
+ plan: {
+ planID: string;
+ pricePerRepository: number;
+ quota: {
+ repository: number;
+ size: number;
+ file: number;
+ };
+ };
+ billing?: {
+ name: string;
+ email: string;
+ address: string;
+ address2?: string;
+ city: string;
+ zip: string;
+ country: string;
+ vat?: string;
+ };
+}
+
+export interface IConferenceDocument extends IConference, mongoose.Document {}
+export interface IConferenceModel extends mongoose.Model {}
diff --git a/src/routes/conference.ts b/src/routes/conference.ts
new file mode 100644
index 0000000..eaeea7a
--- /dev/null
+++ b/src/routes/conference.ts
@@ -0,0 +1,199 @@
+import * as express from "express";
+import config from "../../config";
+import Conference from "../Conference";
+import AnonymizedRepositoryModel from "../database/anonymizedRepositories/anonymizedRepositories.model";
+import ConferenceModel from "../database/conference/conferences.model";
+import Repository from "../Repository";
+import { ensureAuthenticated } from "./connection";
+import { handleError, getUser } from "./route-utils";
+
+const router = express.Router();
+
+// user needs to be connected for all user API
+router.use(ensureAuthenticated);
+
+const plans = [
+ {
+ id: "free_conference",
+ name: "Free",
+ pricePerRepo: 0,
+ storagePerRepo: -1,
+ description: `- Quota is deducted from user account
+ - No-download
+ - Conference dashboard
`,
+ },
+ {
+ id: "premium_conference",
+ name: "Premium",
+ pricePerRepo: 0.5,
+ storagePerRepo: 500 * 8 * 1024,
+ description: `- 500Mo / repository
+ - Repository download
+ - Conference dashboard
`,
+ },
+ {
+ id: "unlimited_conference",
+ name: "Unlimited",
+ pricePerRepo: 3,
+ storagePerRepo: 0,
+ description: `- Unlimited repository size
+ - Repository download
+ - Conference dashboard
`,
+ },
+];
+
+router.get("/plans", async (req: express.Request, res: express.Response) => {
+ res.json(plans);
+});
+
+router.get("/", async (req: express.Request, res: express.Response) => {
+ try {
+ const user = await getUser(req);
+ const conferences = await Promise.all(
+ (
+ await ConferenceModel.find({
+ owners: { $in: user.model.id },
+ })
+ ).map(async (data) => {
+ const conf = new Conference(data);
+ if (data.endDate < new Date() && data.status == "ready") {
+ await conf.updateStatus("expired");
+ }
+ return conf;
+ })
+ );
+ res.json(conferences.map((conf) => conf.toJSON()));
+ } catch (error) {
+ handleError(error, res);
+ }
+});
+
+function validateConferenceForm(conf) {
+ if (!conf.name) throw new Error("conf_name_missing");
+ if (!conf.conferenceID) throw new Error("conf_id_missing");
+ if (!conf.startDate) throw new Error("conf_start_date_missing");
+ if (!conf.endDate) throw new Error("conf_end_date_missing");
+ if (new Date(conf.startDate) > new Date(conf.endDate))
+ throw new Error("conf_start_date_invalid");
+ if (new Date() > new Date(conf.endDate))
+ throw new Error("conf_end_date_invalid");
+ if (plans.filter((p) => p.id == conf.plan.planID).length != 1)
+ throw new Error("invalid_plan");
+ const plan = plans.filter((p) => p.id == conf.plan.planID)[0];
+ if (plan.pricePerRepo > 0) {
+ const billing = conf.billing;
+ if (!billing) throw new Error("billing_missing");
+ if (!billing.name) throw new Error("billing_name_missing");
+ if (!billing.email) throw new Error("billing_email_missing");
+ if (!billing.address) throw new Error("billing_address_missing");
+ if (!billing.city) throw new Error("billing_city_missing");
+ if (!billing.zip) throw new Error("billing_zip_missing");
+ if (!billing.country) throw new Error("billing_country_missing");
+ }
+}
+
+router.post(
+ "/:conferenceID?",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const user = await getUser(req);
+ let model = new ConferenceModel();
+ if (req.params.conferenceID) {
+ model = await ConferenceModel.findOne({
+ conferenceID: req.params.conferenceID,
+ });
+ if (model.owners.indexOf(user.model.id) == -1)
+ throw new Error("not_authorized");
+ }
+ validateConferenceForm(req.body);
+ model.name = req.body.name;
+ model.startDate = new Date(req.body.startDate);
+ model.endDate = new Date(req.body.endDate);
+ model.status = "ready";
+ model.url = req.body.url;
+ model.repositories = [];
+ model.options = req.body.options;
+
+ if (!req.params.conferenceID) {
+ model.owners.push(user.model.id);
+ model.conferenceID = req.body.conferenceID;
+
+ model.plan = {
+ planID: req.body.plan.planID,
+ pricePerRepository: plans.filter(
+ (p) => p.id == req.body.plan.planID
+ )[0].pricePerRepo,
+ quota: {
+ size: plans.filter((p) => p.id == req.body.plan.planID)[0]
+ .storagePerRepo,
+ file: 0,
+ repository: 0,
+ },
+ };
+
+ if (req.body.billing)
+ model.billing = {
+ name: req.body.billing.name,
+ email: req.body.billing.email,
+ address: req.body.billing.address,
+ address2: req.body.billing.address2,
+ city: req.body.billing.city,
+ zip: req.body.billing.zip,
+ country: req.body.billing.country,
+ vat: req.body.billing.vat,
+ };
+ }
+ await model.save();
+
+ res.send("ok");
+ } catch (error) {
+ if (error.message?.indexOf(" duplicate key") > -1) {
+ return handleError(new Error("conf_id_used"), res);
+ }
+ handleError(error, res);
+ }
+ }
+);
+
+router.get(
+ "/:conferenceID",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const user = await getUser(req);
+ const data = await ConferenceModel.findOne({
+ conferenceID: req.params.conferenceID,
+ });
+ if (!data) throw new Error("conf_not_found");
+ const conference = new Conference(data);
+ if (conference.ownerIDs.indexOf(user.model.id) == -1)
+ throw new Error("not_authorized");
+ const o: any = conference.toJSON();
+ o.repositories = (await conference.repositories()).map((r) => r.toJSON());
+ res.json(o);
+ } catch (error) {
+ handleError(error, res);
+ }
+ }
+);
+
+router.delete(
+ "/:conferenceID",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const user = await getUser(req);
+ const data = await ConferenceModel.findOne({
+ conferenceID: req.params.conferenceID,
+ });
+ if (!data) throw new Error("conf_not_found");
+ const conference = new Conference(data);
+ if (conference.ownerIDs.indexOf(user.model.id) == -1)
+ throw new Error("not_authorized");
+ await conference.remove();
+ res.send("ok");
+ } catch (error) {
+ handleError(error, res);
+ }
+ }
+);
+
+export default router;
diff --git a/src/routes/index.ts b/src/routes/index.ts
index 220b90c..6511bf4 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -1,5 +1,6 @@
import repositoryPrivate from "./repository-private";
import repositoryPublic from "./repository-public";
+import conference from "./conference";
import file from "./file";
import webview from "./webview";
import user from "./user";
@@ -12,4 +13,5 @@ export default {
webview,
user,
option,
+ conference
};
diff --git a/src/routes/repository-private.ts b/src/routes/repository-private.ts
index a71974f..32db4c1 100644
--- a/src/routes/repository-private.ts
+++ b/src/routes/repository-private.ts
@@ -10,6 +10,7 @@ import AnonymizedRepositoryModel from "../database/anonymizedRepositories/anonym
import config from "../../config";
import { IAnonymizedRepositoryDocument } from "../database/anonymizedRepositories/anonymizedRepositories.types";
import Repository from "../Repository";
+import ConferenceModel from "../database/conference/conferences.model";
const router = express.Router();
@@ -61,7 +62,7 @@ router.post(
async (req: express.Request, res: express.Response) => {
try {
const repo = await getRepo(req, res, { nocheck: true });
- if (!repo) throw new Error("repo_not_found");
+ if (!repo) return;
const user = await getUser(req);
if (repo.owner.username != user.username) {
@@ -163,7 +164,7 @@ router.get(
router.get("/:repoId/", async (req: express.Request, res: express.Response) => {
try {
const repo = await getRepo(req, res, { nocheck: true });
- if (!repo) throw new Error("repo_not_found");
+ if (!repo) return;
const user = await getUser(req);
if (user.username != repo.model.owner) {
@@ -215,7 +216,6 @@ function updateRepoModel(
}
model.source.commit = repoUpdate.source.commit;
model.source.branch = repoUpdate.source.branch;
- model.conference = repoUpdate.conference;
model.options = {
terms: repoUpdate.terms,
expirationMode: repoUpdate.options.expirationMode,
@@ -238,7 +238,7 @@ router.post(
async (req: express.Request, res: express.Response) => {
try {
const repo = await getRepo(req, res, { nocheck: true });
- if (!repo) throw new Error("repo_not_found");
+ if (!repo) return;
const user = await getUser(req);
if (repo.owner.username != user.username) {
@@ -257,6 +257,48 @@ router.post(
updateRepoModel(repo.model, repoUpdate);
+ async function removeRepoFromConference(conferenceID) {
+ const conf = await ConferenceModel.findOne({
+ conferenceID,
+ });
+ if (conf) {
+ const r = conf.repositories.filter((r) => r.id == repo.model.id);
+ if (r.length == 1) r[0].removeDate = new Date();
+ await conf.save();
+ }
+ }
+ if (!repoUpdate.conference) {
+ // remove conference
+ if (repo.model.conference) {
+ await removeRepoFromConference(repo.model.conference);
+ }
+ } else if (repoUpdate.conference != repo.model.conference) {
+ // update/add conference
+ const conf = await ConferenceModel.findOne({
+ conferenceID: repoUpdate.conference,
+ });
+ if (conf) {
+ if (new Date() < conf.startDate || new Date() > conf.endDate || conf.status !== "ready") {
+ throw new Error("conf_not_activated");
+ }
+ const f = conf.repositories.filter((r) => r.id == repo.model.id);
+ if (f.length) {
+ // the repository already referenced the conference
+ f[0].addDate = new Date();
+ f[0].removeDate = null;
+ } else {
+ conf.repositories.push({
+ id: repo.model.id,
+ addDate: new Date(),
+ });
+ }
+ if (repo.model.conference) {
+ await removeRepoFromConference(repo.model.conference);
+ }
+ await conf.save();
+ }
+ }
+ repo.model.conference = repoUpdate.conference;
await repo.updateStatus("preparing");
res.send("ok");
new Repository(repo.model).anonymize();
@@ -285,7 +327,7 @@ router.post("/", async (req: express.Request, res: express.Response) => {
repo.repoId = repoUpdate.repoId;
repo.anonymizeDate = new Date();
repo.owner = user.username;
-
+
updateRepoModel(repo, repoUpdate);
repo.source.accessToken = user.accessToken;
repo.source.repositoryId = repository.model.id;
@@ -297,8 +339,27 @@ router.post("/", async (req: express.Request, res: express.Response) => {
return res.status(500).send({ error: "invalid_mode" });
}
}
+ repo.conference = repoUpdate.conference;
await repo.save();
+
+ if (repoUpdate.conference) {
+ const conf = await ConferenceModel.findOne({
+ conferenceID: repoUpdate.conference,
+ });
+ if (conf) {
+ if (new Date() < conf.startDate || new Date() > conf.endDate || conf.status !== "ready") {
+ await repo.remove()
+ throw new Error("conf_not_activated");
+ }
+ conf.repositories.push({
+ id: repo.id,
+ addDate: new Date(),
+ });
+ await conf.save();
+ }
+ }
+
res.send("ok");
new Repository(repo).anonymize();
} catch (error) {
diff --git a/src/routes/repository-public.ts b/src/routes/repository-public.ts
index 9d59dcf..706b701 100644
--- a/src/routes/repository-public.ts
+++ b/src/routes/repository-public.ts
@@ -48,7 +48,7 @@ router.get(
async (req: express.Request, res: express.Response) => {
try {
const repo = await getRepo(req, res, { nocheck: true });
- if (!repo) throw new Error("repo_not_found");
+ if (!repo) return;
let redirectURL = null;
if (
repo.status == "expired" &&
@@ -62,10 +62,19 @@ router.get(
await repo.updateIfNeeded();
+ let download = false;
+ const conference = await repo.conference();
+ if (conference) {
+ console.log(conference.quota)
+ download =
+ conference.quota.size > -1 &&
+ !!config.ENABLE_DOWNLOAD &&
+ repo.source.type == "GitHubDownload";
+ }
+
res.json({
url: redirectURL,
- download:
- !!config.ENABLE_DOWNLOAD && repo.source.type == "GitHubDownload",
+ download,
});
} catch (error) {
handleError(error, res);
diff --git a/src/schedule.ts b/src/schedule.ts
new file mode 100644
index 0000000..c848b8e
--- /dev/null
+++ b/src/schedule.ts
@@ -0,0 +1,17 @@
+import * as schedule from "node-schedule";
+import Conference from "./Conference";
+import ConferenceModel from "./database/conference/conferences.model";
+
+export function conferenceStatusCheck() {
+ // check every 6 hours the status of the conference
+ const job = schedule.scheduleJob("0 */6 * * *", async () => {
+ (await ConferenceModel.find({ status: { $eq: "ready" } })).forEach(
+ async (data) => {
+ const conference = new Conference(data);
+ if (conference.isExpired() && conference.status == "ready") {
+ await conference.expire();
+ }
+ }
+ );
+ });
+}
diff --git a/src/server.ts b/src/server.ts
index e293a9f..32a78bf 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -12,6 +12,7 @@ import * as passport from "passport";
import * as connection from "./routes/connection";
import router from "./routes";
import AnonymizedRepositoryModel from "./database/anonymizedRepositories/anonymizedRepositories.model";
+import { conferenceStatusCheck } from "./schedule";
function indexResponse(req: express.Request, res: express.Response) {
if (
@@ -58,6 +59,7 @@ export default async function start() {
// api routes
app.use("/api/options", rate, router.option);
+ app.use("/api/conferences", rate, router.conference);
app.use("/api/user", rate, router.user);
app.use("/api/repo", rate, router.repositoryPublic);
app.use("/api/repo", rate, router.file);
@@ -96,6 +98,9 @@ export default async function start() {
app.get("*", indexResponse);
+ // start schedules
+ conferenceStatusCheck();
+
await db.connect();
app.listen(config.PORT);
console.log("Database connected and Server started on port: " + config.PORT);
diff --git a/src/types.ts b/src/types.ts
index c41d03b..dee74b8 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -117,6 +117,8 @@ export type RepositoryStatus =
| "expired"
| "removed";
+export type ConferenceStatus = "ready" | "expired" | "removed";
+
export type SourceStatus = "available" | "unavailable";
export type TreeElement = Tree | TreeFile;