diff --git a/build.js b/build.js index 9c2edb0..db1747a 100644 --- a/build.js +++ b/build.js @@ -12,6 +12,7 @@ prebuild.run({ "./marko/page/data/channels.marko", "./marko/page/data/characters.marko", "./marko/page/data/changes.marko", + "./marko/page/data/files.marko", "./marko/page/story-content/view.marko", "./marko/page/logs-content/view.marko", ] diff --git a/marko/components/compact-list-item/index.marko b/marko/components/compact-list-item/index.marko index 45892c2..e74d55f 100644 --- a/marko/components/compact-list-item/index.marko +++ b/marko/components/compact-list-item/index.marko @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/marko/components/compact-list-item/style.less b/marko/components/compact-list-item/style.less index 19a669e..79412d2 100644 --- a/marko/components/compact-list-item/style.less +++ b/marko/components/compact-list-item/style.less @@ -2,4 +2,22 @@ tr.compact-list-item { user-select: none; outline: 0.05px dotted; vertical-align: middle; +} + +tr.compact-list-item.color-highlight-primary { + a { + color: #FFF !important; + } + + label { + color: #CCC !important; + } + + div.label { + color: #AA8 !important; + } + + div.value.color-text { + color: #FFF !important; + } } \ No newline at end of file diff --git a/marko/components/compact-list/style.less b/marko/components/compact-list/style.less index 12dcee1..4bdfa5e 100644 --- a/marko/components/compact-list/style.less +++ b/marko/components/compact-list/style.less @@ -1,6 +1,5 @@ table.compact-list { padding-top: 0.5em; - padding-bottom: 3em; width: 90%; max-width: 100ch; diff --git a/marko/page/data/components/add-file-modal/component.js b/marko/page/data/components/add-file-modal/component.js new file mode 100644 index 0000000..ad39387 --- /dev/null +++ b/marko/page/data/components/add-file-modal/component.js @@ -0,0 +1,61 @@ +const moment = require("moment") + +const {fileApi} = require("../../../../../rpdata/api/File") + +module.exports = class { + onCreate(input) { + this.state = { + error: null, + loading: false, + values: { + file: null, + name: "", + public: false, + + realFile: null, + } + } + } + + change(key, ev) { + if (key === "file") { + this.state.values = Object.assign({}, this.state.values, { + [key]: ev.target.value, + realFile: ev.target.files[0], + name: ev.target.files[0].name, + }) + } else { + this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value}) + } + } + + open() { + + } + + close() { + this.emit("close") + } + + save() { + if (this.state.loading) { + return + } + + const input = Object.assign({}, this.state.values) + input.file = input.realFile + delete input.realFile + + this.state.loading = true + fileApi.uploadFile(input).then((res) => { + this.emit("added", res) + this.emit("close") + }).catch(errs => { + console.warn("Failed to add:", errs) + + this.state.error = "Failed to add: " + JSON.stringify(errs) + }).then(() => { + this.state.loading = false + }) + } +} \ No newline at end of file diff --git a/marko/page/data/components/add-file-modal/index.marko b/marko/page/data/components/add-file-modal/index.marko new file mode 100644 index 0000000..b56fb5b --- /dev/null +++ b/marko/page/data/components/add-file-modal/index.marko @@ -0,0 +1,19 @@ + +

Upload File

+ +

${state.error}

+ + + + + + + + + + + +
\ No newline at end of file diff --git a/marko/page/data/components/change/component.js b/marko/page/data/components/change/component.js index 8277a8c..c1ef670 100644 --- a/marko/page/data/components/change/component.js +++ b/marko/page/data/components/change/component.js @@ -99,6 +99,19 @@ module.exports = class { break } + case "File": { + const key = data.keys.find(k => k.id !== "*") + const fileObj = data.objects.find(o => o.type === "File") || {name: key.id} + + this.state.operation += " file" + this.state.links = [{ + text: fileObj.name, + href: `/data/files#${key.id}`, + }] + + break + } + case "Post": { const logKey = data.keys.find(k => k.model === "Log") const postKeys = data.keys.filter(k => k.model === "Post") @@ -124,6 +137,7 @@ module.exports = class { const opText = { add: "added", remove: "removed", + upload: "uploaded", move: "moved", edit: "edited", import: "imported", diff --git a/marko/page/data/components/channels-page/index.marko b/marko/page/data/components/channels-page/index.marko index ff884d2..5bb52dd 100644 --- a/marko/page/data/components/channels-page/index.marko +++ b/marko/page/data/components/channels-page/index.marko @@ -4,3 +4,4 @@ + diff --git a/marko/page/data/components/characters-page/component.js b/marko/page/data/components/characters-page/component.js index 725b330..7778628 100644 --- a/marko/page/data/components/characters-page/component.js +++ b/marko/page/data/components/characters-page/component.js @@ -31,6 +31,15 @@ module.exports = class { } characterAdded(character) { + const index = this.state.characters.findIndex(c => c.id === character.id) + if (index !== -1) { + const characters = this.state.characters.slice() + characters[index] = character + this.state.characters = characters + + return + } + this.state.characters = this.state.characters.concat(character) } diff --git a/marko/page/data/components/characters-page/index.marko b/marko/page/data/components/characters-page/index.marko index e50038c..05cb964 100644 --- a/marko/page/data/components/characters-page/index.marko +++ b/marko/page/data/components/characters-page/index.marko @@ -4,3 +4,4 @@ + diff --git a/marko/page/data/components/data-menu/index.marko b/marko/page/data/components/data-menu/index.marko index 303a167..2ee5639 100644 --- a/marko/page/data/components/data-menu/index.marko +++ b/marko/page/data/components/data-menu/index.marko @@ -3,14 +3,18 @@ Characters Channels History - + Files + Add - - Character + + Character - - Channel + + Channel + + + File \ No newline at end of file diff --git a/marko/page/data/components/edit-file-modal/component.js b/marko/page/data/components/edit-file-modal/component.js new file mode 100644 index 0000000..bb14d48 --- /dev/null +++ b/marko/page/data/components/edit-file-modal/component.js @@ -0,0 +1,59 @@ +const moment = require("moment") + +const {fileApi} = require("../../../../../rpdata/api/File") + +module.exports = class { + onCreate(input) { + this.state = { + error: null, + loading: false, + values: { + id: "", + name: "", + public: false, + } + } + } + + onInput(input) { + this.state.values = { + id: input.file.id, + name: input.file.name, + public: input.file.public, + } + } + + change(key, ev) { + this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value}) + } + + open() { + + } + + close() { + this.emit("close") + } + + save() { + if (this.state.loading) { + return + } + + const input = Object.assign({}, this.state.values) + input.file = input.realFile + delete input.realFile + + this.state.loading = true + fileApi.editFile(input).then((res) => { + this.emit("edited", res) + this.emit("close") + }).catch(errs => { + console.warn("Failed to add:", errs) + + this.state.error = "Failed to add: " + JSON.stringify(errs) + }).then(() => { + this.state.loading = false + }) + } +} \ No newline at end of file diff --git a/marko/page/data/components/edit-file-modal/index.marko b/marko/page/data/components/edit-file-modal/index.marko new file mode 100644 index 0000000..5efeccb --- /dev/null +++ b/marko/page/data/components/edit-file-modal/index.marko @@ -0,0 +1,16 @@ + +

Upload File

+ +

${state.error}

+ + + + + + + + +
\ No newline at end of file diff --git a/marko/page/data/components/file-list/component.js b/marko/page/data/components/file-list/component.js new file mode 100644 index 0000000..889f77f --- /dev/null +++ b/marko/page/data/components/file-list/component.js @@ -0,0 +1,7 @@ +module.exports = class { + isHighlighted(id) { + if (typeof(window) !== "undefined") { + return window.location.hash.endsWith(id) + } + } +} \ No newline at end of file diff --git a/marko/page/data/components/file-list/index.marko b/marko/page/data/components/file-list/index.marko new file mode 100644 index 0000000..863b899 --- /dev/null +++ b/marko/page/data/components/file-list/index.marko @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/marko/page/data/components/file/component.js b/marko/page/data/components/file/component.js new file mode 100644 index 0000000..e646c8e --- /dev/null +++ b/marko/page/data/components/file/component.js @@ -0,0 +1,38 @@ +const moment = require("moment") + +module.exports = class { + onCreate(input) { + this.state = { + modal: null, + deleted: false, + date: moment(input.data.time).format("MMM D, YYYY"), + } + } + + onInput(input) { + if (input && input.data) { + this.state.date = moment(input.data.time).format("MMM D, YYYY") + } + } + + open(modal) { + this.state.modal = modal + } + + close() { + this.state.modal = null + } + + removed() { + this.state.deleted = true + this.emit("removed") + } + + sizeString(bytes) { + const order = Math.floor(Math.log2(bytes) / 10) + const unit = ["B", "KB", "MB", "GB", "TB", "PB"][order] + const number = bytes / Math.pow(2, (order)*10) + + return `${number.toFixed(2)} ${unit}` + } +} \ No newline at end of file diff --git a/marko/page/data/components/file/index.marko b/marko/page/data/components/file/index.marko new file mode 100644 index 0000000..fca9d8a --- /dev/null +++ b/marko/page/data/components/file/index.marko @@ -0,0 +1,22 @@ + + + + + + + + + Link + + + Edit + + + Remove + + + + + + + diff --git a/marko/page/data/components/files-page/component.js b/marko/page/data/components/files-page/component.js new file mode 100644 index 0000000..95c6611 --- /dev/null +++ b/marko/page/data/components/files-page/component.js @@ -0,0 +1,47 @@ +module.exports = class { + onCreate(input) { + this.state = { + publics: input.publics.sort(byDate), + privates: input.privates.sort(byDate), + modal: null, + } + } + + onMount() { + } + + open(modal) { + this.state.modal = modal + } + + close() { + this.state.modal = null + } + + fileRemoved({id}) { + this.state.publics = this.state.publics.filter(f => f.id !== id) + this.state.privates = this.state.privates.filter(f => f.id !== id) + } + + fileEdited(file, patch) { + this.fileRemoved({id: file.id}) + this.fileAdded(Object.assign({}, file, patch)) + + console.log(Object.assign({}, file, patch)) + } + + fileAdded(file) { + if (file.public) { + this.state.publics = [].concat(...this.state.publics).concat(file).sort(byDate) + } else { + this.state.privates = [].concat(...this.state.privates).concat(file).sort(byDate) + } + } +} + +function byDate(a, b) { + const ad = new Date(a.time) + const bd = new Date(b.time) + + return ad.getTime() - bd.getTime() +} \ No newline at end of file diff --git a/marko/page/data/components/files-page/index.marko b/marko/page/data/components/files-page/index.marko new file mode 100644 index 0000000..5be4fc1 --- /dev/null +++ b/marko/page/data/components/files-page/index.marko @@ -0,0 +1,11 @@ + +
+

Your Private Files

+ + +

Public Files

+ +
+ + + diff --git a/marko/page/data/components/files-page/style.less b/marko/page/data/components/files-page/style.less new file mode 100644 index 0000000..ff1a806 --- /dev/null +++ b/marko/page/data/components/files-page/style.less @@ -0,0 +1,10 @@ +h1.file-list-headers { + margin: 0; + font-size: 1.5em; + margin-top: 0.5em; + text-align: center; +} + +main.data-file-page { + margin-bottom: 4em; +} \ No newline at end of file diff --git a/marko/page/data/components/link-file-modal/component.js b/marko/page/data/components/link-file-modal/component.js new file mode 100644 index 0000000..e5af57e --- /dev/null +++ b/marko/page/data/components/link-file-modal/component.js @@ -0,0 +1,19 @@ +module.exports = class { + onCreate(input) { + this.state = { + isImage: (input.file && input.file.mimeType.startsWith("image/")) + } + } + + onInput(input) { + this.state.isImage = (input.file && input.file.mimeType.startsWith("image/")) + } + + open() { + + } + + close() { + this.emit("close") + } +} \ No newline at end of file diff --git a/marko/page/data/components/link-file-modal/index.marko b/marko/page/data/components/link-file-modal/index.marko new file mode 100644 index 0000000..02ebf16 --- /dev/null +++ b/marko/page/data/components/link-file-modal/index.marko @@ -0,0 +1,36 @@ + +

Links for ${input.file.name}

+ +

+ Below are links and copy-pastable syntax for sharing the file. They can be used for linking to it + in your stories and wiki pages. +

+ +

+ This is the link to the actual file on the CDN, not a share of it. You must delete the file if you want + to un-share it. +

+ + +

+ Images uploaded here cannot be inlined on the wiki. For that you have to upload it there. +

+ + + + + + + + + + + + + + + + + File preview + +
\ No newline at end of file diff --git a/marko/page/data/components/link-file-modal/style.less b/marko/page/data/components/link-file-modal/style.less new file mode 100644 index 0000000..33736c6 --- /dev/null +++ b/marko/page/data/components/link-file-modal/style.less @@ -0,0 +1,19 @@ +div.link-file-modal { + user-select: text; + + p { + user-select: text; + cursor: text; + } + + img { + margin: 0; + padding: 0; + margin-top: 0.25em; + width: 100%; + } + + input { + user-select: all !important; + } +} \ No newline at end of file diff --git a/marko/page/data/components/remove-file-modal/component.js b/marko/page/data/components/remove-file-modal/component.js new file mode 100644 index 0000000..20036ec --- /dev/null +++ b/marko/page/data/components/remove-file-modal/component.js @@ -0,0 +1,51 @@ +const {fileApi} = require("../../../../../rpdata/api/File") + +module.exports = class { + onCreate(input) { + this.state = { + error: null, + loading: false, + values: { + id: "", + } + } + } + + onInput(input) { + this.state.values = { + id: input.file.id, + } + } + + change(key, ev) { + this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value}) + } + + open() { + + } + + close() { + this.emit("close") + } + + doIt() { + if (this.state.loading) { + return + } + + const input = Object.assign({}, this.state.values) + + this.state.loading = true + fileApi.removeFile(input).then((res) => { + this.emit("removed", res) + this.emit("close") + }).catch(errs => { + console.warn("Failed to add:", errs) + + this.state.error = "Failed to add: " + JSON.stringify(errs) + }).then(() => { + this.state.loading = false + }) + } +} \ No newline at end of file diff --git a/marko/page/data/components/remove-file-modal/index.marko b/marko/page/data/components/remove-file-modal/index.marko new file mode 100644 index 0000000..577cf9a --- /dev/null +++ b/marko/page/data/components/remove-file-modal/index.marko @@ -0,0 +1,11 @@ + +

Delete File ${input.file.name}

+ +

${state.error}

+ +

+ It might take up to an hour for it to be removed from CDN caches if it's been recently loaded. +

+ + +
\ No newline at end of file diff --git a/marko/page/data/files.marko b/marko/page/data/files.marko new file mode 100644 index 0000000..38870b3 --- /dev/null +++ b/marko/page/data/files.marko @@ -0,0 +1,6 @@ + + <@body> + + + + diff --git a/package-lock.json b/package-lock.json index c31d19c..e904234 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,95 @@ "requires": { "babel-runtime": "^6.26.0", "lasso": "3.2.6" + }, + "dependencies": { + "events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==" + }, + "lasso": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/lasso/-/lasso-3.2.6.tgz", + "integrity": "sha512-Ln4i6XepzXJ8e/+PTxsB+vdZHXbN0wIzepT6Ygwvxtl37Qm4HWLy2VnkVbSXG2gV2XJmC/JP0acg0ShPYKa/nA==", + "requires": { + "app-root-dir": "^1.0.2", + "assert": "^1.4.1", + "babel-code-frame": "^6.26.0", + "browser-refresh-client": "^1.1.4", + "buffer": "^5.1.0", + "clean-css": "^4.1.11", + "clone": "^2.1.1", + "complain": "^1.2.0", + "escodegen": "^1.10.0", + "esprima": "^4.0.0", + "estraverse": "^4.2.0", + "events": "^3.0.0", + "glob": "^7.1.2", + "image-size": "^0.6.3", + "lasso-caching-fs": "^1.0.2", + "lasso-loader": "^3.0.2", + "lasso-modules-client": "^2.0.5", + "lasso-package-root": "^1.0.1", + "lasso-resolve-from": "^1.2.0", + "marko": "^4.10.0", + "mime": "^2.3.1", + "mkdirp": "^0.5.1", + "path-browserify": "1.0.0", + "pify": "^3.0.0", + "process": "^0.11.10", + "property-handlers": "^1.1.1", + "raptor-async": "^1.1.3", + "raptor-cache": "^2.0.4", + "raptor-css-parser": "^1.1.5", + "raptor-detect": "^1.0.1", + "raptor-logging": "^1.1.3", + "raptor-objects": "^1.0.2", + "raptor-polyfill": "^1.0.2", + "raptor-promises": "^1.0.3", + "raptor-regexp": "^1.0.1", + "raptor-strings": "^1.0.2", + "raptor-util": "^3.2.0", + "resolve-from": "^4.0.0", + "semver": "^5.5.0", + "send": "^0.16.2", + "stream-browserify": "^2.0.1", + "string_decoder": "^1.1.1", + "strip-json-comments": "^2.0.1", + "through": "^2.3.8", + "uglify-js": "^3.4.0", + "url": "^0.11.0", + "util": "^0.11.0" + } + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + }, + "path-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.0.tgz", + "integrity": "sha512-Hkavx/nY4/plImrZPHRk2CL9vpOymZLgEbMNX1U0bjcBL7QN9wODxyx0yaMZURSQaUtSEvDrfAvxa9oPb0at9g==" + }, + "raptor-util": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/raptor-util/-/raptor-util-3.2.0.tgz", + "integrity": "sha1-I7DIA8jxrIocrmfZpjiLSRYcl1g=" + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "requires": { + "inherits": "2.0.3" + } + } } }, "@types/connect": { @@ -1293,6 +1382,11 @@ "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -1435,9 +1529,9 @@ } }, "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" }, "complain": { "version": "1.3.0", @@ -3011,9 +3105,9 @@ "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=" }, "htmljs-parser": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-2.4.0.tgz", - "integrity": "sha512-Vmc4hiSt5uqGc44JaBNuzzs5MPExjY+w5z1gWl+KaGxvdGIZ6LfDn+vHfJXT+SJ6E3RG8uMCr+0HyDEWqGN5iA==", + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-2.6.8.tgz", + "integrity": "sha512-il0NrLbNyt29nZuciLbrRKRMBi8VVVjVpv/x37z9qyubQscsoMzcsw3Fp35QvPR9p2eurQ/TBKrfdZ+joDX/rw==", "requires": { "char-props": "^0.1.5", "complain": "^1.0.0" @@ -3767,9 +3861,9 @@ } }, "lasso": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/lasso/-/lasso-3.2.6.tgz", - "integrity": "sha512-Ln4i6XepzXJ8e/+PTxsB+vdZHXbN0wIzepT6Ygwvxtl37Qm4HWLy2VnkVbSXG2gV2XJmC/JP0acg0ShPYKa/nA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/lasso/-/lasso-3.3.1.tgz", + "integrity": "sha512-hhqKMN/4Q5vbaE3As7NchFpjIJjWm1T27wKK/P0//QWxkTLgkTuYCof0qn69VyE8L7ubzSQ60kM2fT5s3Kbm2Q==", "requires": { "app-root-dir": "^1.0.2", "assert": "^1.4.1", @@ -3814,8 +3908,8 @@ "stream-browserify": "^2.0.1", "string_decoder": "^1.1.1", "strip-json-comments": "^2.0.1", + "terser": "^3.17.0", "through": "^2.3.8", - "uglify-js": "^3.4.0", "url": "^0.11.0", "util": "^0.11.0" }, @@ -3826,9 +3920,9 @@ "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==" }, "mime": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", - "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==" + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" }, "path-browserify": { "version": "1.0.0", @@ -3846,9 +3940,9 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, "util": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.0.tgz", - "integrity": "sha512-5n12uMzKCjvB2HPFHnbQSjaqAa98L5iIXmHrZCLavuZVe0qe/SJGbDGWlpaHk5lnBkWRDO+dRu1/PgmUYKPPTw==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "requires": { "inherits": "2.0.3" } @@ -3919,9 +4013,9 @@ } }, "lasso-marko": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/lasso-marko/-/lasso-marko-2.4.4.tgz", - "integrity": "sha512-7Q0wNg0rQhdjezbUBmRw/W3dSa9NAqmm99oZhlVOss++OsCwLmPF7r870qMA5a3IUIX+ziOrZbCVHdWQdD/+XA==", + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/lasso-marko/-/lasso-marko-2.4.7.tgz", + "integrity": "sha512-fC882KkHB56zKHS/C2xicoDkDmWSNnglWhvgvSO8Lzm4XHNcSoLhZlxBkrcU+0eiGtT/jrC8/Yam4hDdYYuCzQ==", "requires": { "callbackify": "^1.1.0" } @@ -4089,15 +4183,16 @@ "integrity": "sha1-1k1xPuzsVc5M/esyF1DswJniwtw=" }, "marko": { - "version": "4.13.8", - "resolved": "https://registry.npmjs.org/marko/-/marko-4.13.8.tgz", - "integrity": "sha512-APfb/5KgDswm2fdxZBknxubEgNmXchmGZ1aCV1Brb8eGqHzd8I1xZzykvj1/mDU/bzhsAIYDV2yyC9Os7Ue9AQ==", + "version": "4.18.13", + "resolved": "https://registry.npmjs.org/marko/-/marko-4.18.13.tgz", + "integrity": "sha512-1sFI4yfbOXxY/48lgunZgpoiOd86ZGuO42bshBiZqlkmcumlznnHknbBPKtq+CLq+LTXL2apW/XlUSWqAe09Rw==", "requires": { "app-module-path": "^2.2.0", "argly": "^1.0.0", "browser-refresh-client": "^1.0.0", + "camelcase": "^5.0.0", "char-props": "~0.1.5", - "complain": "^1.3.0", + "complain": "^1.6.0", "deresolve": "^1.1.2", "escodegen": "^1.8.1", "esprima": "^4.0.0", @@ -4105,7 +4200,7 @@ "events": "^1.0.2", "events-light": "^1.0.0", "he": "^1.1.0", - "htmljs-parser": "^2.3.2", + "htmljs-parser": "^2.6.7", "lasso-caching-fs": "^1.0.1", "lasso-modules-client": "^2.0.4", "lasso-package-root": "^1.0.1", @@ -4123,9 +4218,17 @@ "simple-sha1": "^2.1.0", "strip-json-comments": "^2.0.1", "try-require": "^1.2.1", - "warp10": "^1.0.0" + "warp10": "^2.0.1" }, "dependencies": { + "complain": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/complain/-/complain-1.6.0.tgz", + "integrity": "sha512-9oBfSEfxveaNmo2eSp/vEPkaBVxUhiJTZVgGYayzBchSAXQM6CK1PAQeV5ICShnSgfT+biYzrN7egKwwX+HkCw==", + "requires": { + "error-stack-parser": "^2.0.1" + } + }, "raptor-util": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/raptor-util/-/raptor-util-3.2.0.tgz", @@ -5234,9 +5337,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "send": { "version": "0.16.2", @@ -5340,9 +5443,9 @@ "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" }, "simple-sha1": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/simple-sha1/-/simple-sha1-2.1.1.tgz", - "integrity": "sha512-pFMPd+I/lQkpf4wFUeS/sED5IqdIG1lUlrQviBMV4u4mz8BRAcB5fvUx5Ckfg3kBigEglAjHg7E9k/yy2KlCqA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/simple-sha1/-/simple-sha1-2.1.2.tgz", + "integrity": "sha512-TQl9rm4rdKAVmhO++sXAb8TNN0D6JAD5iyI1mqEPNpxUzTRrtm4aOG1pDf/5W/qCFihiaoK6uuL9rvQz1x1VKw==", "requires": { "rusha": "^0.8.1" } @@ -5714,6 +5817,27 @@ "acorn-node": "^1.2.0" } }, + "terser": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz", + "integrity": "sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==", + "requires": { + "commander": "^2.19.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.10" + }, + "dependencies": { + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -5865,11 +5989,11 @@ "integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg==" }, "uglify-js": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.8.tgz", - "integrity": "sha512-WatYTD84gP/867bELqI2F/2xC9PQBETn/L+7RGq9MQOA/7yFBNvY1UwXqvtILeE6n0ITwBXxp34M0/o70dzj6A==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", + "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", "requires": { - "commander": "~2.17.1", + "commander": "~2.20.0", "source-map": "~0.6.1" } }, @@ -6060,9 +6184,9 @@ "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==" }, "warp10": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/warp10/-/warp10-1.3.6.tgz", - "integrity": "sha512-7cijX+NprqPV+UGUZXZw1I15JFHPqoy65tNVvP6cL43Vlanpcm8hBYoQTuDYUHa5x90Bct4gHhRtqqOOkLhQkw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/warp10/-/warp10-2.0.1.tgz", + "integrity": "sha512-nS1KG2RCVTWepfSitZKzL+HYiqqMJag06KtOJF/LwklrJWeJYRWHjMYpd6GXMt09ot+HIEj+Y4dmQNnNNE0Bjg==" }, "whatwg-fetch": { "version": "2.0.4", diff --git a/package.json b/package.json index 1853bdb..99c8158 100644 --- a/package.json +++ b/package.json @@ -30,15 +30,15 @@ "irc-caret-notation": "^1.0.0", "isomorphic-fetch": "^2.2.1", "jsonwebtoken": "^8.3.0", - "lasso": "^3.2.6", + "lasso": "^3.3.1", "lasso-babel-transform": "^1.0.2", "lasso-less": "^3.0.1", - "lasso-marko": "^2.4.4", + "lasso-marko": "^2.4.7", "less": "^3.8.1", "markdown-it": "^8.4.2", "markdown-it-container": "^2.0.0", "markdown-it-regexp": "^0.4.0", - "marko": "^4.13.8", + "marko": "^4.18.13", "moment": "^2.22.2", "moment-timezone": "^0.5.23", "passport": "^0.4.0", diff --git a/routes/data/changes.js b/routes/data/changes.js index c47500a..1ca6314 100644 --- a/routes/data/changes.js +++ b/routes/data/changes.js @@ -7,7 +7,7 @@ const changesTemplate = require("../../marko/page/data/changes.marko") router.get("/", async(req, res) => { res.markoAsync(changesTemplate, { - changes: changesApi.list({limit: 2000}), + changes: changesApi.list({limit: 1000}), selected: {changes: true}, }) }) diff --git a/routes/data/files.js b/routes/data/files.js new file mode 100644 index 0000000..d6e7f37 --- /dev/null +++ b/routes/data/files.js @@ -0,0 +1,22 @@ +const express = require("express") +const router = express.Router() + +const {generateToken} = require("../graphql") +const {fileApi} = require("../../rpdata/api/File") + +const filesTemplate = require("../../marko/page/data/files.marko") + +router.get("/", async(req, res) => { + let privates = [] + if (res.locals.user) { + privates = fileApi.list({public: false}, {token: generateToken(res.locals.user.name)}) + } + + res.markoAsync(filesTemplate, { + privates: privates, + publics: fileApi.list(), + selected: {files: true}, + }) +}) + +module.exports = router diff --git a/routes/graphql.js b/routes/graphql.js index 089e6c6..9d1eae0 100644 --- a/routes/graphql.js +++ b/routes/graphql.js @@ -3,16 +3,10 @@ const express = require("express") const jwt = require("jsonwebtoken") const config = require("../config") -const { query } = require("../rpdata/client") const router = express.Router() router.post("/", (req, res) => { - if (!req.header("Content-Type").startsWith("application/json")) { - res.status(400).json({errors: [{message: "Incorrect input type, expected application/json"}]}) - return - } - const user = res.locals.user const permissions = (req.header("X-Permissions") || "").split(",").filter(t => t != "" && t != "undefined" && t != "null") let authorization = req.header("Authorization") @@ -20,17 +14,17 @@ router.post("/", (req, res) => { authorization = "" } - if (!authorization && permissions.length > 0 && user.loggedIn) { + if (!authorization && user.loggedIn) { authorization = `Bearer ${generateToken(user.name, permissions)}` } fetch(config.graphqlEndpoint, { method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": req.header("Content-Type"), "Authorization": authorization, }, - body: JSON.stringify(req.body), + body: req, credentials: "include", }).then(fetchRes => { res.setHeader("Content-Type", fetchRes.headers.get("Content-Type")) @@ -51,8 +45,8 @@ router.use("/", proxy(config.graphqlEndpoint, {ws: true})) * @param {string} user * @param {string[]} permissions */ -function generateToken(user, permissions) { - return jwt.sign({user, permissions, exp: Math.floor((Date.now() / 1000) + 1200)}, config.backend.secret, {header: {kid: config.backend.kid}}) +function generateToken(user) { + return jwt.sign({user, exp: Math.floor((Date.now() / 1000) + 1200)}, config.backend.secret, {header: {kid: config.backend.kid}}) } -module.exports = router \ No newline at end of file +module.exports = {router, generateToken} \ No newline at end of file diff --git a/rpdata/api/Change.js b/rpdata/api/Change.js index a5f5084..5fad248 100644 --- a/rpdata/api/Change.js +++ b/rpdata/api/Change.js @@ -48,6 +48,11 @@ class ChangesAPI { objects { type: __typename + ...on File { + id + name + } + ...on Character { id name diff --git a/rpdata/api/File.js b/rpdata/api/File.js new file mode 100644 index 0000000..3b42f99 --- /dev/null +++ b/rpdata/api/File.js @@ -0,0 +1,110 @@ +const {query, queryForm} = require("../client") + +class File { + /** + * @param {{id:string,kind:string,time:Date|string|number,public:boolean,name:string,mimeType:string,size:number,author:string,url:string}} data + */ + constructor(data) { + this.id = data.id + this.kind = data.kind + this.time = new Date(data.time) + this.public = data.public + this.name = data.name + this.mimeType = data.mimeType + this.size = data.size + this.author = data.author + this.url = data.url + } +} + + +class FileAPI { + /** + * Call `characters(filter)` query + * + * @param {{public:boolean,mimeTypes:string[]}} filter + * @returns {Promise} + */ + list(filter = {public: true}, opts = {}) { + return query(` + query ListFiles($filter: FilesFilter!) { + files(filter:$filter) { + id + kind + time + public + name + mimeType + size + author + url + } + } + `, {filter}, opts).then(({files}) => { + return files.map(d => new File(d)) + }) + } + + /** + * Upload a file + * + * @param {{public:boolean, file:File, name:string}} input + */ + uploadFile(input) { + return queryForm(` + mutation UploadFile($input: FileUploadInput!) { + uploadFile(input:$input) { + id + kind + time + public + name + mimeType + size + author + url + } + } + `, {input}).then(({uploadFile}) => { + return new File(uploadFile) + }) + } + + /** + * Edit a file + * + * @param {{id: string, public:boolean, name:string}} input + */ + editFile(input) { + return queryForm(` + mutation EditFile($input: FileEditInput!) { + editFile(input:$input) { + id + public + name + } + } + `, {input}).then(({editFile}) => { + return editFile + }) + } + + /** + * Edit a file + * + * @param {{id: string, public:boolean, name:string}} input + */ + removeFile(input) { + return queryForm(` + mutation RemoveFIle($input: FileRemoveInput!) { + removeFile(input:$input) { + id + } + } + `, {input}).then(({removeFile}) => { + return removeFile + }) + } +} + +module.exports = { File, fileApi: new FileAPI() } \ No newline at end of file diff --git a/rpdata/client.js b/rpdata/client.js index c74bf03..006929e 100644 --- a/rpdata/client.js +++ b/rpdata/client.js @@ -10,13 +10,13 @@ const compressQuery = require("graphql-query-compress") * @param {{[x:string]: any}} variables * @param {{operationName:string, token: string, permissions: string[]}} options */ -function query(query, variables = {}, options = {}) { +function query(query, variables = {}, options = {}) { return fetch(config.graphqlEndpoint, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": options.token ? `Bearer ${options.token}` : null, - "X-Permissions": options.permissions ? options.permissions.join(",") : null, + //"X-Permissions": options.permissions ? options.permissions.join(",") : null, }, body: JSON.stringify({query: query.length > 256 ? compressQuery(query || "") : query, variables, operationName: options.operationName}), credentials: "include", @@ -31,6 +31,67 @@ function query(query, variables = {}, options = {}) { }) } +/** + * + * + * @param {string} query GQL Query + * @param {Object} variables Variables + * @param {{token?:string,permissions?:string[]}} options + */ +function queryForm(query, variables = {}, options = {}) { + const map = {} + const files = [] + + function findFiles(prefix, data) { + for (const key in data) { + if (!data.hasOwnProperty(key)) { + continue + } + const value = data[key] + + if (value instanceof File) { + map[files.length] = [`${prefix}.${key}`] + files.push(value) + } else if (typeof(value) === 'object') { + data[key] = findFiles(`${prefix}.${key}`, Object.assign({}, value)) + } + } + + return data + } + const operations = { + query: query, + variables: findFiles("variables", Object.assign({}, variables)), + } + + const form = new FormData() + form.append("operations", JSON.stringify(operations)) + form.append("map", JSON.stringify(map)) + for (let i = 0; i < files.length; ++i) { + form.append(i.toString(), files[i]) + } + + console.log(form) + + return fetch(config.graphqlEndpoint, { + method: "POST", + headers: { + "Authorization": options.token ? `Bearer ${options.token}` : null, + "X-Permissions": options.permissions ? options.permissions.join(",") : null, + }, + body: form, + credentials: "include", + }).then(res => { + return res.json() + }).then(json => { + if (json.errors != null && json.errors.length > 0) { + return Promise.reject(json.errors) + } + + return json.data + }) +} + class Subscription extends EventEmitter { constructor(query, variables) { super() @@ -115,4 +176,4 @@ class Subscription extends EventEmitter { } } -module.exports = { query, Subscription } \ No newline at end of file +module.exports = { query, queryForm, Subscription } \ No newline at end of file diff --git a/server.js b/server.js index d2176c3..9666aed 100644 --- a/server.js +++ b/server.js @@ -69,6 +69,12 @@ app.use(session) app.use(passport.initialize()) app.use(passport.session()) app.use(require("./middleware/locals")) + +// Proxy (does not want all middleware) +app.use("/graphql", require("./routes/graphql").router) +app.use("/playground", proxy(config.playgroundEndpoint)) + +// The rest of the middleware app.use(bodyParser.json({limit: "1mb"})) app.use(bodyParser.text({limit: "256kb"})) @@ -79,10 +85,6 @@ app.use(lassoMiddleware.serveStatic()) // Authentication app.use("/auth", require("./routes/auth")) -// API Proxy -app.use("/graphql", require("./routes/graphql")) -app.use("/playground", proxy(config.playgroundEndpoint)) - // Global css doesn't work with prebuild. if (process.env.NODE_ENV === "production") { app.use("/hax/global.css", require("./routes/globalcss")) @@ -108,6 +110,7 @@ app.use("/data/", require("./routes/data")) app.use("/data/characters/", require("./routes/data/characters")) app.use("/data/channels/", require("./routes/data/channels")) app.use("/data/changes/", require("./routes/data/changes")) +app.use("/data/files/", require("./routes/data/files")) // Entry point app.get("/", function(req, res) {