Browse Source

UI for file stuff.

master
Gisle Aune 5 years ago
parent
commit
5fe56eaae8
  1. 1
      build.js
  2. 2
      marko/components/compact-list-item/index.marko
  3. 18
      marko/components/compact-list-item/style.less
  4. 1
      marko/components/compact-list/style.less
  5. 61
      marko/page/data/components/add-file-modal/component.js
  6. 19
      marko/page/data/components/add-file-modal/index.marko
  7. 14
      marko/page/data/components/change/component.js
  8. 1
      marko/page/data/components/channels-page/index.marko
  9. 9
      marko/page/data/components/characters-page/component.js
  10. 1
      marko/page/data/components/characters-page/index.marko
  11. 14
      marko/page/data/components/data-menu/index.marko
  12. 59
      marko/page/data/components/edit-file-modal/component.js
  13. 16
      marko/page/data/components/edit-file-modal/index.marko
  14. 7
      marko/page/data/components/file-list/component.js
  15. 9
      marko/page/data/components/file-list/index.marko
  16. 38
      marko/page/data/components/file/component.js
  17. 22
      marko/page/data/components/file/index.marko
  18. 47
      marko/page/data/components/files-page/component.js
  19. 11
      marko/page/data/components/files-page/index.marko
  20. 10
      marko/page/data/components/files-page/style.less
  21. 19
      marko/page/data/components/link-file-modal/component.js
  22. 36
      marko/page/data/components/link-file-modal/index.marko
  23. 19
      marko/page/data/components/link-file-modal/style.less
  24. 51
      marko/page/data/components/remove-file-modal/component.js
  25. 11
      marko/page/data/components/remove-file-modal/index.marko
  26. 6
      marko/page/data/files.marko
  27. 200
      package-lock.json
  28. 6
      package.json
  29. 2
      routes/data/changes.js
  30. 22
      routes/data/files.js
  31. 18
      routes/graphql.js
  32. 5
      rpdata/api/Change.js
  33. 110
      rpdata/api/File.js
  34. 67
      rpdata/client.js
  35. 11
      server.js

1
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",
]

2
marko/components/compact-list-item/index.marko

@ -1,3 +1,3 @@
<tr class="compact-list-item color-highlight-dark">
<tr class=["compact-list-item", input.highlight ? "color-highlight-primary" : "color-highlight-dark"]>
<include(input.renderBody)/>
</tr>

18
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;
}
}

1
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;

61
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
})
}
}

19
marko/page/data/components/add-file-modal/index.marko

@ -0,0 +1,19 @@
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
<h1>Upload File</h1>
<p class="color-error">${state.error}</p>
<label>File</label>
<input key="eventName" type="file" placeholder="(None)" on-change("change", "file") value=state.values.file />
<label>Name</label>
<input key="name" autofocus placeholder="(Required)" class="big" on-change("change", "name") value=state.values.name />
<label>Public</label>
<toggle value=state.values.public on="On" off="Off"
onDesc="Other members may see this file on this page."
offDesc="Only you may see this file on this page."
on-change("change", "public") />
<button disabled=state.loading on-click("save")>Save</button>
</modal>

14
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",

1
marko/page/data/components/channels-page/index.marko

@ -4,3 +4,4 @@
</main>
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-added("channelAdded") on-close("close") />
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-close("close") />
<add-file-modal enabled=(state.modal === "file.add") user=input.user on-close("close") />

9
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)
}

1
marko/page/data/components/characters-page/index.marko

@ -4,3 +4,4 @@
</main>
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-added("characterAdded") on-close("close") />
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-close("close") />
<add-file-modal enabled=(state.modal === "file.add") user=input.user on-close("close") />

14
marko/page/data/components/data-menu/index.marko

@ -3,14 +3,18 @@
<menu-link key="characters" selected=input.selected.characters icon="C" href="/data/characters/">Characters</menu-link>
<menu-link key="channels" selected=input.selected.channels icon="#" href="/data/channels/">Channels</menu-link>
<menu-link key="history" selected=input.selected.changes icon="H" href="/data/changes/">History</menu-link>
<if-permitted user=input.user permission=["member", "character.add", "channel.add"]>
<menu-link key="files" selected=input.selected.files icon="F" href="/data/files/">Files</menu-link>
<if-permitted user=input.user permission=["member", "character.add", "channel.add", "file.add"]>
<menu-gap />
<menu-header>Add</menu-header>
<if-permitted user=input.user permission=["character.add", "member"]>
<menu-link dark key="characters_add" icon="+" on-click("emit", "open", "character.add")>Character</menu-link>
<if-permitted key="characters_add" user=input.user permission=["character.add", "member"]>
<menu-link dark icon="+" on-click("emit", "open", "character.add")>Character</menu-link>
</if-permitted>
<if-permitted user=input.user permission="channel.add">
<menu-link dark key="channels_add" icon="+" on-click("emit", "open", "channel.add")>Channel</menu-link>
<if-permitted key="channels_add" user=input.user permission="channel.add">
<menu-link dark icon="+" on-click("emit", "open", "channel.add")>Channel</menu-link>
</if-permitted>
<if-permitted key="files_add" user=input.user permission=["file.add", "member"]>
<menu-link dark icon="+" on-click("emit", "open", "file.add")>File</menu-link>
</if-permitted>
</if-permitted>
</menu>

59
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
})
}
}

16
marko/page/data/components/edit-file-modal/index.marko

@ -0,0 +1,16 @@
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
<h1>Upload File</h1>
<p class="color-error">${state.error}</p>
<label>Name</label>
<input key="name" autofocus placeholder="(Required)" class="big" on-change("change", "name") value=state.values.name />
<label>Public</label>
<toggle value=state.values.public on="On" off="Off"
onDesc="Other members may see this file on this page."
offDesc="Only you may see this file on this page."
on-change("change", "public") />
<button disabled=state.loading on-click("save")>Save</button>
</modal>

7
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)
}
}
}

9
marko/page/data/components/file-list/index.marko

@ -0,0 +1,9 @@
<compact-list>
<compact-list-item for(file in input.files) key=file.id id=file.id highlight=component.isHighlighted(file.id)>
<file data=file
user=input.user
on-removed("emit", "removed", file)
on-edited("emit", "edited", file)
/>
</compact-list-item>
</compact-list>

38
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}`
}
}

22
marko/page/data/components/file/index.marko

@ -0,0 +1,22 @@
<if (!state.deleted)>
<a class="anchor" id=input.data.id />
<compact-list-property primary long label="Name" value=input.data.name />
<compact-list-property short label="Date" value=state.date />
<compact-list-property short label="Type" value=input.data.mimeType />
<compact-list-property short label="Author" value=input.data.author />
<compact-list-property short label="Size" value=component.sizeString(input.data.size) />
<compact-list-options long>
<a key="link" class="color-menu" on-click("open", "link")>Link</a>
<if(input.data.kind === "upload")>
<if-permitted user=input.user author=input.data.author permission="file.edit">
<a key="edit" on-click("open", "edit", input.data)>Edit</a>
</if-permitted>
<if-permitted user=input.user author=input.data.author permission="file.remove">
<a key="remove" on-click("open", "remove", input.data)>Remove</a>
</if-permitted>
</if>
</compact-list-options>
<link-file-modal enabled=(state.modal === "link") file=input.data on-close("close") />
<edit-file-modal enabled=(state.modal === "edit") file=input.data on-edited("emit", "edited") on-close("close") />
<remove-file-modal enabled=(state.modal === "remove") file=input.data on-removed("removed") on-close("close") />
</if>

47
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()
}

11
marko/page/data/components/files-page/index.marko

@ -0,0 +1,11 @@
<data-menu categories=input.categories selected=(input.selected || {}) user=input.user on-open("open") />
<main class="data-file-page">
<h1 class="file-list-headers color-primary">Your Private Files</h1>
<file-list user=input.user files=state.privates on-removed("fileRemoved") on-edited("fileEdited") on-nicks("fileNicksChanged") />
<h1 class="file-list-headers color-primary">Public Files</h1>
<file-list user=input.user files=state.publics on-removed("fileRemoved") on-edited("fileEdited") on-nicks("fileNicksChanged") />
</main>
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-close("close") />
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-close("close") />
<add-file-modal enabled=(state.modal === "file.add") user=input.user on-added("fileAdded") on-close("close") />

10
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;
}

19
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")
}
}

36
marko/page/data/components/link-file-modal/index.marko

@ -0,0 +1,36 @@
<modal class="modal color-text nolabel link-file-modal" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
<h1>Links for ${input.file.name}</h1>
<p>
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.
</p>
<p class="color-danger">
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.
</p>
<if(state.isImage)>
<p class="color-danger">
Images uploaded here cannot be inlined on the wiki. For that you have to upload it there.
</p>
</if>
<label>URL</label>
<input key="url" disabled placeholder="(None)" on-change("change", "eventName") value=input.file.url />
<label>Markdown link</label>
<input key="mdLink" disabled placeholder="(None)" on-change("change", "locationName") value=`[Link text](${input.file.url})` />
<label>Wiki link</label>
<input key="wikiLink" disabled placeholder="(None)" on-change("change", "locationName") value=`[${input.file.url} Link text]` />
<if(state.isImage)>
<label>Markdown image</label>
<input key="mdImage" disabled placeholder="(None)" on-change("change", "locationName") value=`![Alt. text](${input.file.url})` />
<label>Image Preview</label>
<img src=input.file.url alt="File preview"/>
</if>
</modal>

19
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;
}
}

51
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
})
}
}

11
marko/page/data/components/remove-file-modal/index.marko

@ -0,0 +1,11 @@
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
<h1>Delete File ${input.file.name}</h1>
<p class="color-error">${state.error}</p>
<p class="color-danger">
It might take up to an hour for it to be removed from CDN caches if it's been recently loaded.
</p>
<button disabled=state.loading on-click("doIt")>Remove</button>
</modal>

6
marko/page/data/files.marko

@ -0,0 +1,6 @@
<include("../layout", {title: "Data", site: "data"})>
<@body>
<background src="/assets/images/bg.png" opacity=0.25 />
<files-page user=input.user privates=input.privates publics=input.publics selected=input.selected />
</@body>
</include>

200
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",

6
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",

2
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},
})
})

22
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

18
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
module.exports = {router, generateToken}

5
rpdata/api/Change.js

@ -48,6 +48,11 @@ class ChangesAPI {
objects {
type: __typename
...on File {
id
name
}
...on Character {
id
name

110
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<File[]>}
*/
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() }

67
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 }
module.exports = { query, queryForm, Subscription }

11
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) {

Loading…
Cancel
Save