Compare commits

...
23 Commits
Author SHA1 Message Date
gisle 59a711f150 fix unnamed stories being unclickable.
continuous-integration/drone/push Build is passing
2020-11-29 01:28:56 +01:00
gisle b75c3461f7 add irccloud log import options
continuous-integration/drone/push Build is passing
2020-08-20 21:02:57 +02:00
gisle a10b29a0ea fix files page not working when logged out.
continuous-integration/drone/push Build is passing
2020-08-09 16:51:06 +02:00
gisle 79c3a76300 update drone ci
continuous-integration/drone/push Build is passing
2020-08-09 16:24:02 +02:00
gisle 6aef0412e8 the last few things. 2020-08-09 16:12:12 +02:00
gisle 5fe56eaae8 UI for file stuff.
the build was successful
2019-09-15 16:34:40 +02:00
gisle bb2ccf6f4b Fixed chapter moves crashing history page due to changes in change reporting in 2.0.
the build was successful
2019-08-25 16:01:05 +02:00
gisle 1583eb412e That's why I should always test two-line fixes...
the build failed
2019-08-02 20:42:04 +02:00
gisle 5c094a2471 Fixed prev logs not allowing overlaps with next logs.
the build was successful
2019-08-02 20:36:30 +02:00
gisle 99aec4d2cb Added log suggestions to the bottom of logs. 2019-08-02 17:34:12 +02:00
gisle 3f944ff029 Fixed logs page not remembering show all button.
the build failed
2019-08-02 11:14:30 +02:00
gisle 3cbe42e1ba Fixed ordinal checker crashing edit story form.
the build was successful
2019-08-02 11:10:25 +02:00
gisle 01b501f9e5 Fixed /logs/<lig-id> route not supporting dash in channel name.
the build was successful
2019-07-20 10:18:05 +02:00
gisle 9b7d182955 Removed limit argument in logs query if it's not needed.
the build was successful
2019-05-21 20:45:27 +02:00
gisle 7c55fc03aa Fixed locals middleware doing an auth check for assets, scripts and other static content.
the build was successful
2019-05-19 21:37:01 +02:00
gisle 6fca626ea2 logs-content: Added exception for annotations in multipart post detection.
the build was successful
2019-04-25 21:00:54 +02:00
gisle d92e6bfeb0 Merge branch 'master' of ssh+git://git.aiterp.net/rpdata/frontend
the build was successful
2019-04-25 20:54:10 +02:00
gisle 2d89096430 logs-content: Fixed post being added twice on add due to race condition between websocket and mutation return. 2019-04-25 20:53:59 +02:00
gisle 3988028256 story-content: Fixed comment fictional dates being affected by browser timezone.
the build was successful
2019-04-19 16:21:59 +02:00
gisle c51733ef3b logs-content: Fixed event link filtering by channel. 2019-04-19 16:17:05 +02:00
gisle f81f05d274 logs-content: Added character highlighter feature. 2019-04-19 16:15:07 +02:00
gisle 25d4aa73d1 logs-content: Replaced old post rendering with actual irc caret notation parsing. 2019-04-19 14:57:05 +02:00
gisle e1a5968341 package: npm audit fix 2019-04-18 12:14:09 +02:00
67 changed files with 1367 additions and 232 deletions
+28 -20
View File
@@ -1,25 +1,33 @@
pipeline: name: rpdata-frontend
docker-trunk:
kind: pipeline
type: docker
steps:
- name: docker-release-tag
image: plugins/docker image: plugins/docker
repo: vmaple/rpdata-frontend settings:
tags: trunk
secrets: [ docker_username, docker_password ]
when:
event: push
branch: master
docker-release:
image: plugins/docker
repo: vmaple/rpdata-frontend
auto_tag: true auto_tag: true
secrets: [ docker_username, docker_password ] username:
from_secret: docker_username
password:
from_secret: docker_password
repo: r.vmaple.dev/rpdata/rpdata-frontend
registry: r.vmaple.dev
when: when:
event: tag event:
ref: refs/tags/* - tag
docker-release-latest:
- name: docker-release-latest
image: plugins/docker image: plugins/docker
repo: vmaple/rpdata-frontend settings:
tags: latest auto_tag: false
secrets: [ docker_username, docker_password ] username:
from_secret: docker_username
password:
from_secret: docker_password
repo: r.vmaple.dev/rpdata/rpdata-frontend
registry: r.vmaple.dev
when: when:
event: tag event:
ref: refs/tags/* - tag
+1
View File
@@ -12,6 +12,7 @@ prebuild.run({
"./marko/page/data/channels.marko", "./marko/page/data/channels.marko",
"./marko/page/data/characters.marko", "./marko/page/data/characters.marko",
"./marko/page/data/changes.marko", "./marko/page/data/changes.marko",
"./marko/page/data/files.marko",
"./marko/page/story-content/view.marko", "./marko/page/story-content/view.marko",
"./marko/page/logs-content/view.marko", "./marko/page/logs-content/view.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)/> <include(input.renderBody)/>
</tr> </tr>
@@ -3,3 +3,21 @@ tr.compact-list-item {
outline: 0.05px dotted; outline: 0.05px dotted;
vertical-align: middle; 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
View File
@@ -1,6 +1,5 @@
table.compact-list { table.compact-list {
padding-top: 0.5em; padding-top: 0.5em;
padding-bottom: 3em;
width: 90%; width: 90%;
max-width: 100ch; max-width: 100ch;
@@ -2,7 +2,7 @@
<td class="icon color-highlight-primary">${input.icon}</td> <td class="icon color-highlight-primary">${input.icon}</td>
<td class="content color-primary color-highlight-dark"> <td class="content color-primary color-highlight-dark">
<div class="title"> <div class="title">
<a class="color-primary" href=input.link>${input.name}</a> <a class="color-primary" href=input.link>${input.name || "(Untitled)"}</a>
</div> </div>
<div if(input.description) class="description color-text"> <div if(input.description) class="description color-text">
<div class="content">${input.description}</div> <div class="content">${input.description}</div>
@@ -0,0 +1,23 @@
const {parse, BOLD} = require("irc-caret-notation")
module.exports = class {
onCreate() {
this.state = {
paragraphs: [],
prev: "",
}
}
onInput(input) {
if (this.state == null) {
return
}
if (input.source != this.state.prev) {
this.state.prev = input.source
this.state.paragraphs = input.source.split("\n").filter(l => l.length > 0).map(l => parse(l, {
"*": (!(input.source.startsWith("(")) ? BOLD : null),
}))
}
}
}
@@ -0,0 +1,5 @@
<div class=["irc-caret-notation", input.class]>
<p for(p in state.paragraphs)>
<span for(chunk in p) class=chunk.cssClasses("icn-")>${chunk.text}</span>
</p>
</div>
+2 -2
View File
@@ -1,6 +1,6 @@
<a on-click("onClick") href=input.href> <a on-click("onClick") href=input.href>
<div class=state.classes> <div class=[state.classes, input.weak ? "dark" : null]>
<div class="icon color-primary">${input.icon}</div> <div class=["icon", "color-primary"]>${input.icon}</div>
<div class=["text", input.selected ? "color-primary" : input.textClass]> <div class=["text", input.selected ? "color-primary" : input.textClass]>
<include(input.renderBody) /> <include(input.renderBody) />
</div> </div>
@@ -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
})
}
}
@@ -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>
+16 -2
View File
@@ -65,8 +65,8 @@ module.exports = class {
this.state.operation += " chapter" this.state.operation += " chapter"
this.state.links = [{ this.state.links = [{
text: chapter.title || `Untitled chapter (${chapterKey.id})`, text: (chapter||{}).title || `Untitled chapter (${(chapterKey||chapter).id})`,
href: `/story/${storyKey.id}#${chapterKey.id}`, href: `/story/${storyKey.id}#${(chapterKey||chapter).id}`,
}] }]
break break
@@ -99,6 +99,19 @@ module.exports = class {
break 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": { case "Post": {
const logKey = data.keys.find(k => k.model === "Log") const logKey = data.keys.find(k => k.model === "Log")
const postKeys = data.keys.filter(k => k.model === "Post") const postKeys = data.keys.filter(k => k.model === "Post")
@@ -124,6 +137,7 @@ module.exports = class {
const opText = { const opText = {
add: "added", add: "added",
remove: "removed", remove: "removed",
upload: "uploaded",
move: "moved", move: "moved",
edit: "edited", edit: "edited",
import: "imported", import: "imported",
@@ -1,11 +1,27 @@
const { changesApi } = require("../../../../../rpdata/api/Change")
module.exports = class { module.exports = class {
onCreate(input) { onCreate(input) {
this.state = { this.state = {
characters: input.characters, changes: input.changes,
shownAll: false,
modal: null, modal: null,
} }
} }
clearLimit(e) {
// Disable mobile URL fallback.
if (e && e.preventDefault) {
e.preventDefault()
}
changesApi.list({limit: 0}).then(changes => {
this.state.changes = changes
this.state.shownAll = true
})
}
open(modal) { open(modal) {
this.state.modal = modal this.state.modal = modal
} }
@@ -3,10 +3,12 @@
<div class="changes-page"> <div class="changes-page">
<h1 class="color-primary">Changes</h1> <h1 class="color-primary">Changes</h1>
<p>All changes to <em>listed</em> resources is stored for 90 days for transparency reasons. The current state of an object after each change exists, but is only available through the GraphQL API at this time.</p> <p>All changes to <em>listed</em> resources is stored for 90 days for transparency reasons. The current state of an object after each change exists, but is only available through the GraphQL API at this time.</p>
<for (change in input.changes)> <for (change in state.changes)>
<change data=change /> <change data=change />
</for> </for>
<a if(!state.shownAll) href="/data/changes?limit=0" class="show-more-button" on-click("clearLimit")>Show All</a>
</div> </div>
</main> </main>
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-added("characterAdded") on-close("close") /> <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-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") />
@@ -33,4 +33,21 @@
opacity: 1; opacity: 1;
} }
} }
.show-more-button {
margin: 1em 0;
padding: 0.5em 2em;
font-size: 2em;
text-align: center;
user-select: none;
display: block;
color: #DA1;
}
.show-more-button:hover {
cursor: pointer;
text-decoration: underline;
}
} }
@@ -4,3 +4,4 @@
</main> </main>
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-added("channelAdded") on-close("close") /> <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-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") />
@@ -31,6 +31,15 @@ module.exports = class {
} }
characterAdded(character) { 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) this.state.characters = this.state.characters.concat(character)
} }
@@ -4,3 +4,4 @@
</main> </main>
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-added("characterAdded") on-close("close") /> <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-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") />
@@ -3,14 +3,18 @@
<menu-link key="characters" selected=input.selected.characters icon="C" href="/data/characters/">Characters</menu-link> <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="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> <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-gap />
<menu-header>Add</menu-header> <menu-header>Add</menu-header>
<if-permitted user=input.user permission=["character.add", "member"]> <if-permitted key="characters_add" user=input.user permission=["character.add", "member"]>
<menu-link dark key="characters_add" icon="+" on-click("emit", "open", "character.add")>Character</menu-link> <menu-link dark icon="+" on-click("emit", "open", "character.add")>Character</menu-link>
</if-permitted> </if-permitted>
<if-permitted user=input.user permission="channel.add"> <if-permitted key="channels_add" user=input.user permission="channel.add">
<menu-link dark key="channels_add" icon="+" on-click("emit", "open", "channel.add")>Channel</menu-link> <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>
</if-permitted> </if-permitted>
</menu> </menu>
@@ -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
})
}
}
@@ -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>
@@ -0,0 +1,7 @@
module.exports = class {
isHighlighted(id) {
if (typeof(window) !== "undefined") {
return window.location.hash.endsWith(id)
}
}
}
@@ -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>
@@ -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}`
}
}
@@ -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>
@@ -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()
}
@@ -0,0 +1,11 @@
<data-menu categories=input.categories selected=(input.selected || {}) user=input.user on-open("open") />
<main class="data-file-page">
<h1 if(state.privates && state.privates.length > 0) 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 if(state.publics && state.publics.length > 0) 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") />
@@ -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;
}
@@ -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")
}
}
@@ -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>
@@ -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;
}
}
@@ -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
})
}
}
@@ -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
View File
@@ -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>
@@ -0,0 +1,10 @@
module.exports = class {
onCreate(input) {
this.state = {
id: input.log.id,
channelName: input.log.channelName,
eventName: input.log.eventName,
error: null,
}
}
}
@@ -0,0 +1,33 @@
import moment from "moment"
<div class="log-suggestions">
<if (input.log.nextLogs.length > 0 || input.log.prevLogs.length > 0)>
<if (input.log.nextLogs.length > 0)>
<h2 class="color-primary">Next Logs</h2>
<div for(s in input.log.nextLogs) class="suggestion color-primary">
<div class="suggestion-header color-primary"><a href=`/logs/${s.log.id}`>${s.log.title || `${s.log.channelName} - ${moment(s.log.date).format("MMMM D, YYYY")}`}</a></div>
<div for(paragraph in (s.log.description||"").split("\n").filter(p => p)) class="suggestion-description color-text">${paragraph}</div>
<div class="suggestion-tags">
<div if(s.hasEvent) class="suggestion-tag color-tag-event">${input.log.eventName}</div>
<div if(s.hasChannel) class="suggestion-tag color-tag-location">${s.log.channelName}</div>
<div for(c in s.characters) class="suggestion-tag color-tag-character">${c.name}</div>
</div>
</div>
</if>
<if (input.log.prevLogs.length > 0)>
<h2 class="color-primary">Previous Logs</h2>
<div for(s in input.log.prevLogs) class="suggestion color-primary">
<div class="suggestion-header color-primary"><a href=`/logs/${s.log.id}`>${s.log.title || `${s.log.channelName} - ${moment(s.log.date).format("MMMM D, YYYY")}`}</a></div>
<div for(paragraph in (s.log.description||"").split("\n").filter(p => p)) class="suggestion-description color-text">${paragraph}</div>
<div class="suggestion-tags">
<div if(s.hasEvent) class="suggestion-tag color-tag-event">${input.log.eventName}</div>
<div if(s.hasChannel) class="suggestion-tag color-tag-location">${s.log.channelName}</div>
<div for(c in s.characters) class="suggestion-tag color-tag-character">${c.name}</div>
</div>
</div>
</if>
</if>
<else>
<div class="empty color-menu">(No suggestions)</div>
</else>
</div>
@@ -0,0 +1,46 @@
div.log-suggestions {
padding: 0 0.5ch;
margin-top: 1em;
margin-bottom: 2em;
h2 {
text-align: center;
margin: 0;
margin-bottom: 0.25em;
}
div.empty, div.loading {
text-align: center;
}
div.suggestion {
margin-bottom: 0.5em;
padding: 0.5em;
div.suggestion-header {
margin: 0.125em 0.5ch;
a {
color: inherit;
}
a:hover {
color: white;
}
}
div.suggestion-description {
margin: 0.125em 0.5ch;
}
div.suggestion-tags {
margin-top: 0.25em;
div.suggestion-tag {
display: inline-block;
padding: 0 0.5ch;
margin: 0.125em 0.5ch;
border: 0.25px solid;
}
}
}
}
@@ -1,5 +1,21 @@
module.exports = class { module.exports = class {
onCreate() {
this.state = {
highlights: [],
}
}
select(value) { select(value) {
this.emit("select", value) this.emit("select", value)
} }
toggleHighlight(id) {
if (this.state.highlights.includes(id)) {
this.state.highlights = this.state.highlights.filter(id2 => id2 !== id)
} else {
this.state.highlights = this.state.highlights.concat(id)
}
this.emit("hightlights", this.state.highlights);
}
} }
@@ -4,10 +4,23 @@
<menu-link dark key="_create" on-click("select", "post.add", null) icon="+">Add Post</menu-link> <menu-link dark key="_create" on-click("select", "post.add", null) icon="+">Add Post</menu-link>
<menu-gap /> <menu-gap />
</if-permitted> </if-permitted>
<menu-header>Links</menu-header> <menu-header>Channel</menu-header>
<menu-link href=("/logs/?channels="+encodeURIComponent(input.log.channel.name)) icon="#">${input.log.channel.name}</menu-link> <menu-link href=("/logs/?channels="+encodeURIComponent(input.log.channel.name)) icon="#">${input.log.channel.name}</menu-link>
<menu-link if(input.log.channel.eventName) href=("/logs/?channels="+input.log.channel.eventName) icon="E">${input.log.channel.eventName}</menu-link> <menu-gap />
<if(input.log.eventName)>
<menu-header>Event</menu-header>
<menu-link href=("/logs/?events="+input.log.eventName) icon="E">${input.log.eventName}</menu-link>
<menu-gap />
</if>
<if(input.log.characters.length > 0)>
<menu-header>Characters</menu-header>
<for(character in input.log.characters)> <for(character in input.log.characters)>
<menu-link href=("/logs/?characters="+character.id) icon="C">${character.name} <span class="weak">(${character.author})</span></menu-link> <menu-link weak=(state.highlights.length > 0 && !state.highlights.includes(character.id))
icon="C"
on-click("toggleHighlight", character.id)>
${character.name} <span class="weak">(${character.author})</span>
</menu-link>
</for> </for>
<menu-gap />
</if>
</menu> </menu>
@@ -9,6 +9,7 @@ module.exports = class {
log: input.log, log: input.log,
modal: null, modal: null,
removed: false, removed: false,
highlights: [],
} }
} }
@@ -111,6 +112,11 @@ module.exports = class {
} }
postAdded(post) { postAdded(post) {
// Stop duplicates in case of race condition.
if (this.state.log.posts.find(p => p.id === post.id)) {
return
}
this.state.log.posts = this.state.log.posts.concat([post]) this.state.log.posts = this.state.log.posts.concat([post])
this.state.log = Object.assign({}, this.state.log) this.state.log = Object.assign({}, this.state.log)
} }
@@ -161,6 +167,10 @@ module.exports = class {
this.state.removed = true this.state.removed = true
} }
highlightsChanged(highlights) {
this.state.highlights = highlights.slice();
}
get title() { get title() {
if (this.state.log.title) { if (this.state.log.title) {
return this.state.log.title return this.state.log.title
@@ -1,8 +1,9 @@
<logs-content-menu user=input.user log=state.log on-select("open") /> <logs-content-menu on-hightlights("highlightsChanged") user=input.user log=state.log on-select("open") />
<main> <main>
<div class="logs-content"> <div class="logs-content">
<div class="header"> <div class="header">
<h1 class="color-primary">${component.title}</h1> <h1 class="color-primary">${component.title}</h1>
<p for(paragraph in state.log.description.split("\n").filter(p => p)) class="color-text">${paragraph}</p>
<if-permitted user=input.user permission="log.edit"> <if-permitted user=input.user permission="log.edit">
<a on-click("open", "log.edit") class="color-menu">Edit</a> <a on-click("open", "log.edit") class="color-menu">Edit</a>
</if-permitted> </if-permitted>
@@ -25,8 +26,10 @@
prev=(state.log.posts[loop.getIndex() - 1] || {}) prev=(state.log.posts[loop.getIndex() - 1] || {})
post=post post=post
characters=state.log.characters characters=state.log.characters
highlights=state.highlights
last=(post.position === state.log.posts.length) last=(post.position === state.log.posts.length)
/> />
<log-suggestions log=state.log />
</div> </div>
<add-post-modal enabled=(state.modal === "post.add") logId=state.log.id on-close("close") on-added("postAdded") /> <add-post-modal enabled=(state.modal === "post.add") logId=state.log.id on-close("close") on-added("postAdded") />
@@ -15,6 +15,10 @@ div.logs-content {
margin-bottom: 0; margin-bottom: 0;
} }
p {
margin: 0.25em 0 0 0;
}
a { a {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
@@ -14,6 +14,7 @@ module.exports = class {
multipart: false, multipart: false,
anchored: false, anchored: false,
prevWasText: false, prevWasText: false,
dark: false,
} }
this.mounted = false; this.mounted = false;
@@ -48,7 +49,7 @@ module.exports = class {
} }
const timediff = Date.parse(input.post.time) - Date.parse(input.prev.time) const timediff = Date.parse(input.post.time) - Date.parse(input.prev.time)
this.state.multipart = (input.post.nick == input.prev.nick && input.prev.multipart !== false) && (Math.abs(timediff) < 60000); this.state.multipart = (!input.post.kind.startsWith("annotation.") && input.post.nick == input.prev.nick && input.prev.multipart !== false) && (Math.abs(timediff) < 60000);
this.state.prevWasText = (input.prev.kind === "text"); this.state.prevWasText = (input.prev.kind === "text");
this.updatePost(input) this.updatePost(input)
@@ -81,38 +82,12 @@ module.exports = class {
this.state.name = input.post.nick this.state.name = input.post.nick
this.state.nameSuffix = "" this.state.nameSuffix = ""
this.state.text = input.post.text.replace(/\x02/g, "**").replace(/\x1D/g, "*").replace(/\`/g, "\\`").trim(); this.state.text = input.post.text
// Fix Ctrl-Bs if (input.highlights.length > 0 && (input.post.kind === "action" || input.post.kind === "text")) {
let left = false this.state.dark = true
this.state.text = this.state.text.replace(/\s*\*\*\s*/g, str => {
left = !left
if (left) {
return " **"
} else if (!left) {
return "** "
}
return str
})
if (this.state.text.startsWith("|")) {
this.state.name = ""
this.state.text = this.state.text.slice(1).trim();
}
if (input.post.kind === "text" && !this.state.text.includes("\"") && !this.state.text.includes("\''") && !this.state.text.includes("|")) {
const colonIndex = this.state.text.indexOf(": ");
if (colonIndex != -1 && colonIndex < this.state.text.indexOf(" ")) {
this.state.text = this.state.text.replace(": ", ": \"") + "\"";
} else { } else {
this.state.text = '"' + this.state.text + '"' this.state.dark = false
}
}
if (this.state.text.charAt(0) == this.state.text.charAt(0).toUpperCase()) {
this.state.name = ""
} }
if (!input.post.nick.startsWith("=")) { if (!input.post.nick.startsWith("=")) {
@@ -120,13 +95,19 @@ module.exports = class {
for (const character of input.characters) { for (const character of input.characters) {
for (const nick of character.nicks) { for (const nick of character.nicks) {
if (nick === postNick) { if (nick === postNick) {
this.state.name = character.name this.state.name = character.name + character.id
this.state.shortName = character.shortName this.state.shortName = character.shortName
if (input.post.nick.endsWith("'s") || input.post.nick.endsWith("'")) { if (input.post.nick.endsWith("'s") || input.post.nick.endsWith("'")) {
this.state.nameSuffix = (character.shortName.endsWith("s") || character.shortName.endsWith("z")) ? "'" : "'s" this.state.nameSuffix = (character.shortName.endsWith("s") || character.shortName.endsWith("z")) ? "'" : "'s"
} }
if (input.highlights.length > 0) {
this.state.dark = !(input.highlights.includes(character.id))
} else {
this.state.dark = false
}
return return
} }
} }
@@ -1,4 +1,4 @@
<div if(!state.removed) class=["post", component.kindClass("pk-"), state.multipart ? "multipart" : null, state.anchored ? "anchored" : null]> <div if(!state.removed) class=["post", component.kindClass("pk-"), state.multipart ? "multipart" : null, state.anchored ? "anchored" : null, state.dark ? "dark" : null]>
<a class="anchor" id=input.post.id /> <a class="anchor" id=input.post.id />
<div class="post-meta-row"> <div class="post-meta-row">
<div class="options color-menu"> <div class="options color-menu">
@@ -19,18 +19,18 @@
</div> </div>
<div if(input.post.kind.startsWith("annotation.")) class="post-body" > <div if(input.post.kind.startsWith("annotation.")) class="post-body" >
<annotation level=(input.post.kind.substring(11))> <annotation level=(input.post.kind.substring(11))>
<markdown class="post-content" source=state.text /> <irc-caret-notation class="post-content" source=state.text />
</annotation> </annotation>
</div> </div>
<div if(input.post.kind === "scene") class="post-body color-text"> <div if(input.post.kind === "scene") class="post-body color-text">
<markdown class="post-content post-scene" source=state.text /> <irc-caret-notation class="post-content post-scene" source=state.text />
</div> </div>
<div if(input.post.kind === "action") class="post-body color-text"> <div if(input.post.kind === "action") class="post-body color-text">
$ const prefix = ((!state.multipart || state.prevWasText) ? state.shortName + state.nameSuffix + " " : ""); $ const prefix = ((!state.multipart || state.prevWasText) ? state.shortName + state.nameSuffix + " " : "");
<markdown class="post-content post-action" source=(prefix + state.text) /> <irc-caret-notation class="post-content post-action" source=(prefix + state.text) />
</div> </div>
<div if(input.post.kind === "text") class="post-body color-text"> <div if(input.post.kind === "text") class="post-body color-text">
<markdown class="post-content post-text" source=state.text /> <irc-caret-notation class="post-content post-text" source=state.text />
</div> </div>
<remove-post-modal enabled=(state.modal === "remove") post=input.post on-close("close") on-remove("remove") /> <remove-post-modal enabled=(state.modal === "remove") post=input.post on-close("close") on-remove("remove") />
<edit-post-modal enabled=(state.modal === "edit") post=input.post on-close("close") on-edited("edited") /> <edit-post-modal enabled=(state.modal === "edit") post=input.post on-close("close") on-edited("edited") />
@@ -1,6 +1,6 @@
div.post { div.post {
margin-top: 1em; margin-top: 1em;
margin-bottom: 1ch; margin-bottom: 1.25em;
margin-left: -12px; margin-left: -12px;
padding-left: 12px; padding-left: 12px;
padding-right: 0.5ch; padding-right: 0.5ch;
@@ -65,12 +65,54 @@ div.post {
margin-top: 0.33em; margin-top: 0.33em;
} }
p {
line-height: 1.33em;
margin-bottom: -0.25em;
span.icn-bold { font-weight: 800; }
span.icn-underline { text-decoration: underline; }
span.icn-italic { font-style: italic; }
span.icn-fg-0 { color: #fff; }
span.icn-fg-1 { color: #111; }
span.icn-fg-2 { color: #13d; }
span.icn-fg-3 { color: #173; }
span.icn-fg-4 { color: #d21; }
span.icn-fg-5 { color: #721; }
span.icn-fg-6 { color: #727; }
span.icn-fg-7 { color: #d71; }
span.icn-fg-8 { color: #aa1; }
span.icn-fg-9 { color: #1d3; }
span.icn-fg-10 { color: #177; }
span.icn-fg-11 { color: #1DD; }
span.icn-fg-12 { color: #127; }
span.icn-fg-13 { color: #c1a; }
span.icn-fg-14 { color: #444; }
span.icn-fg-15 { color: #aaa; }
span.icn-bg-0 { background-color: #fff; }
span.icn-bg-1 { background-color: #111; }
span.icn-bg-2 { background-color: #13d; }
span.icn-bg-3 { background-color: #173; }
span.icn-bg-4 { background-color: #d21; }
span.icn-bg-5 { background-color: #721; }
span.icn-bg-6 { background-color: #727; }
span.icn-bg-7 { background-color: #d71; }
span.icn-bg-8 { background-color: #aa1; }
span.icn-bg-9 { background-color: #1d3; }
span.icn-bg-10 { background-color: #177; }
span.icn-bg-11 { background-color: #1DD; }
span.icn-bg-12 { background-color: #127; }
span.icn-bg-13 { background-color: #c1a; }
span.icn-bg-14 { background-color: #444; }
span.icn-bg-15 { background-color: #aaa; }
}
p:first-of-type { p:first-of-type {
margin-top: 0; margin-top: 0;
padding-top: 0; padding-top: 0;
} }
p:last-of-type { p:last-of-type {
padding-bottom: 0.1em; padding-bottom: 0.1em;
margin-bottom: 0em;
} }
} }
} }
@@ -91,7 +133,7 @@ div.post.pk-text.anchored {
} }
div.post.multipart { div.post.multipart {
margin-top: -0.9em; margin-top: -1.5em;
div.post-meta { div.post-meta {
span { span {
@@ -99,3 +141,7 @@ div.post.multipart {
} }
} }
} }
div.post.dark {
opacity: 0.25;
}
@@ -14,6 +14,7 @@ module.exports = class {
channelName: "", channelName: "",
date: "", date: "",
data: "", data: "",
sessionThresholdHours: 12,
}, },
timezones: tz.names(), timezones: tz.names(),
} }
@@ -58,12 +59,24 @@ module.exports = class {
return return
} }
} else if (input.importer === "MircLike") { } else if (input.importer === "MircLike") {
this.state.error = "Date is required for mIRC-like logs" this.state.error = "Date is required for mIRC-like logs."
return return
} else { } else {
input.date = null; input.date = null;
} }
if (input.importer === "IRCCloudLog") {
const hours = parseFloat(input.sessionThresholdHours)
if (Number.isNaN(hours)) {
this.state.error = "The session change threshold must be a number."
return
}
input.sessionThresholdMs = Math.floor(hours * 3600000)
}
delete input.sessionThresholdHours
if (input.channelName.length < 2 || input.channelName.includes(" ") || input.channelName.includes(" ") || input.channelName.charAt(0) !== "#") { if (input.channelName.length < 2 || input.channelName.includes(" ") || input.channelName.includes(" ") || input.channelName.charAt(0) !== "#") {
this.state.error = "A valid channelName name is required" this.state.error = "A valid channelName name is required"
return return
@@ -10,6 +10,7 @@ import moment from "moment"
<select key="importer" autofocus class="big" placeholder="Kind" on-change("change", "importer") value=state.values.importer> <select key="importer" autofocus class="big" placeholder="Kind" on-change("change", "importer") value=state.values.importer>
<option value="MircLike" selected=(state.values.importer === "MircLike")>mIRC-Like log</option> <option value="MircLike" selected=(state.values.importer === "MircLike")>mIRC-Like log</option>
<option value="ForumLog" selected=(state.values.importer === "ForumLog")>Forum Log</option> <option value="ForumLog" selected=(state.values.importer === "ForumLog")>Forum Log</option>
<option value="IrcCloud" selected=(state.values.importer === "IrcCloud")>IRCCloud log export</option>
</select> </select>
<label>Timezone</label> <label>Timezone</label>
@@ -25,6 +26,11 @@ import moment from "moment"
<input key="date" class="big" placeholder="(Required)" on-change("change", "date") value=state.values.date /> <input key="date" class="big" placeholder="(Required)" on-change("change", "date") value=state.values.date />
</if> </if>
<if(state.values.importer === "IrcCloud")>
<label>Session Change Threshold (Hours)</label>
<input key="sessionThreshold" class="big" placeholder="(Required)" on-change("change", "sessionThresholdHours") value=state.values.sessionThresholdHours />
</if>
<label>Content</label> <label>Content</label>
<textarea key="data" placeholder="(Required)" class="tall" on-change("change", "data") value=state.values.data /> <textarea key="data" placeholder="(Required)" class="tall" on-change("change", "data") value=state.values.data />
+7 -2
View File
@@ -68,7 +68,7 @@ module.exports = class {
if (filter.search) { if (filter.search) {
queries.push("search=" + encodeURIComponent(filter.search)) queries.push("search=" + encodeURIComponent(filter.search))
} }
if (filter.limit) { if (filter.limit != null) {
queries.push("limit=" + encodeURIComponent(filter.limit.toString())) queries.push("limit=" + encodeURIComponent(filter.limit.toString()))
} }
@@ -79,9 +79,14 @@ module.exports = class {
} }
} }
clearLimit() { clearLimit(e) {
this.state.filter = Object.assign({}, this.state.filter, {limit: 0}) this.state.filter = Object.assign({}, this.state.filter, {limit: 0})
// Disable mobile URL fallback.
if (e && e.preventDefault) {
e.preventDefault()
}
this.refresh() this.refresh()
this.updateQuery(this.state.filter) this.updateQuery(this.state.filter)
} }
+2 -2
View File
@@ -4,9 +4,9 @@
<main class="logs-page with-right"> <main class="logs-page with-right">
<logs-table logs=state.logs on-addfilter("addFilter") /> <logs-table logs=state.logs on-addfilter("addFilter") />
<if(state.filter.limit > 0)> <if(state.filter.limit > 0)>
<div class="show-more-button" on-click("clearLimit")> <a href="/logs?limit=0" class="show-more-button" on-click("clearLimit")>
<span>Show All</span> <span>Show All</span>
</div> </a>
</if> </if>
</main> </main>
<add-log-modal enabled=(state.modal === "log.add") on-close("close") /> <add-log-modal enabled=(state.modal === "log.add") on-close("close") />
+3 -2
View File
@@ -1,15 +1,16 @@
main.logs-page { main.logs-page {
div.show-more-button { .show-more-button {
margin: 1em 0; margin: 1em 0;
padding: 0.5em 2em; padding: 0.5em 2em;
font-size: 2em; font-size: 2em;
text-align: center; text-align: center;
user-select: none; user-select: none;
display: block;
color: #DA1; color: #DA1;
} }
div.show-more-button:hover { .show-more-button:hover {
cursor: pointer; cursor: pointer;
text-decoration: underline; text-decoration: underline;
@@ -47,7 +47,9 @@ import moment from "moment"
</tr> </tr>
<tr if(input.comment.fictionalDate != null)> <tr if(input.comment.fictionalDate != null)>
<th>Date:</th> <th>Date:</th>
<td>${moment(input.comment.fictionalDate).format("MMM D, YYYY")}</td> <td>
<date utc value=input.comment.fictionalDate></date>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -69,7 +71,7 @@ import moment from "moment"
<markdown class="body" source=paragraph> <markdown class="body" source=paragraph>
<if (input.comment.fictionalDate)> <if (input.comment.fictionalDate)>
<span class="decoration color-menu">[</span> <span class="decoration color-menu">[</span>
<date class="color-menu" value=input.comment.fictionalDate></date> <date utc class="color-menu" value=input.comment.fictionalDate></date>
<span class="decoration color-menu">]&nbsp;</span> <span class="decoration color-menu">]&nbsp;</span>
</if> </if>
<span class="decoration color-menu">&lt;</span> <span class="decoration color-menu">&lt;</span>
@@ -108,7 +110,7 @@ import moment from "moment"
</div> </div>
<chapter-meta kind="title" value=input.comment.subject /> <chapter-meta kind="title" value=input.comment.subject />
<chapter-meta kind="name" value=input.comment.characterName character=(input.comment.character) ukAuthor=input.comment.author /> <chapter-meta kind="name" value=input.comment.characterName character=(input.comment.character) ukAuthor=input.comment.author />
<chapter-meta kind="date" value=input.comment.fictionalDate /> <chapter-meta utc kind="date" value=input.comment.fictionalDate />
<chapter-meta weak kind="date" value=input.comment.createdDate /> <chapter-meta weak kind="date" value=input.comment.createdDate />
<chapter-meta weak kind="author" value=input.comment.author /> <chapter-meta weak kind="author" value=input.comment.author />
</div> </div>
@@ -2,6 +2,8 @@ const moment = require("moment")
const {storyApi} = require("../../../../../rpdata/api/Story") const {storyApi} = require("../../../../../rpdata/api/Story")
const ORDINAL_REGEX = /[0-9]{1,2}(st|nd|rd|th)/g
module.exports = class { module.exports = class {
onCreate() { onCreate() {
this.state = { this.state = {
@@ -45,11 +47,13 @@ module.exports = class {
const values = this.state.values const values = this.state.values
let fictionalDate = new Date(values.fictionalDate + " UTC") let fictionalDate = new Date(values.fictionalDate + " UTC")
if (values.fictionalDate != "") { if (values.fictionalDate) {
if (Number.isNaN(fictionalDate.getTime())) { if (Number.isNaN(fictionalDate.getTime())) {
this.state.error = `Could not parse ${values.fictionalDate} as date.` this.state.error = `Could not parse ${values.fictionalDate} as date.`
if (values.fictionalDate.includes("th")) {
this.state.error += " Try to remove the 'th'." const ordinal = values.fictionalDate.match(ORDINAL_REGEX)[0]
if (ordinal != null) {
this.state.error += " Try to remove the '"+ordinal.slice(-2)+"'."
} }
return return
+2
View File
@@ -28,6 +28,7 @@ module.exports = (req, res, next) => {
} }
} }
if (["/static/", "/hax/", "/assets/"].find(p => req.path.startsWith(p)) == null) {
if (req.user) { if (req.user) {
res.locals.user = { res.locals.user = {
loggedIn: true, loggedIn: true,
@@ -39,6 +40,7 @@ module.exports = (req, res, next) => {
loggedIn: false, loggedIn: false,
} }
} }
}
next() next()
} }
+2 -1
View File
@@ -7,7 +7,8 @@ const strategy = new Auth0Strategy({
domain: config.auth0.domain, domain: config.auth0.domain,
clientID: config.auth0.clientId, clientID: config.auth0.clientId,
clientSecret: config.auth0.secret, // Replace this with the client secret for your app clientSecret: config.auth0.secret, // Replace this with the client secret for your app
callbackURL: config.root + "/auth/callback" callbackURL: config.root + "/auth/callback",
state: true,
}, (accessToken, refreshToken, extraParams, profile, done) => { }, (accessToken, refreshToken, extraParams, profile, done) => {
// accessToken is the token to call Auth0 API (not needed in the most cases) // accessToken is the token to call Auth0 API (not needed in the most cases)
// extraParams.id_token has the JSON Web Token // extraParams.id_token has the JSON Web Token
+1 -1
View File
@@ -6,7 +6,7 @@ module.exports = session({
secret: config.session.secret, secret: config.session.secret,
store: new RedisStore(config.redis), store: new RedisStore(config.redis),
cookie: { cookie: {
secure: config.session.secure secure: config.session.secure,
}, },
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
+241 -93
View File
@@ -23,6 +23,95 @@
"requires": { "requires": {
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"lasso": "3.2.6" "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": { "@types/connect": {
@@ -1293,6 +1382,11 @@
"resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
"integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" "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": { "caseless": {
"version": "0.12.0", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
@@ -1435,9 +1529,9 @@
} }
}, },
"commander": { "commander": {
"version": "2.17.1", "version": "2.20.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
}, },
"complain": { "complain": {
"version": "1.3.0", "version": "1.3.0",
@@ -2308,13 +2402,13 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
}, },
"fsevents": { "fsevents": {
"version": "1.2.4", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.8.tgz",
"integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", "integrity": "sha512-tPvHgPGB7m40CZ68xqFGkKuzN+RnpGmSV+hgeKxhRpbxdqKXUFJGC3yonBOLzQBcJyGpdZFDfCsdOC2KFsXzeA==",
"optional": true, "optional": true,
"requires": { "requires": {
"nan": "^2.9.2", "nan": "^2.12.1",
"node-pre-gyp": "^0.10.0" "node-pre-gyp": "^0.12.0"
}, },
"dependencies": { "dependencies": {
"abbrev": { "abbrev": {
@@ -2324,7 +2418,8 @@
}, },
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true "bundled": true,
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@@ -2332,7 +2427,7 @@
"optional": true "optional": true
}, },
"are-we-there-yet": { "are-we-there-yet": {
"version": "1.1.4", "version": "1.1.5",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
@@ -2342,32 +2437,37 @@
}, },
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true "bundled": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
} }
}, },
"chownr": { "chownr": {
"version": "1.0.1", "version": "1.1.1",
"bundled": true, "bundled": true,
"optional": true "optional": true
}, },
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true "bundled": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true "bundled": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true "bundled": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@@ -2375,15 +2475,15 @@
"optional": true "optional": true
}, },
"debug": { "debug": {
"version": "2.6.9", "version": "4.1.1",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"ms": "2.0.0" "ms": "^2.1.1"
} }
}, },
"deep-extend": { "deep-extend": {
"version": "0.5.1", "version": "0.6.0",
"bundled": true, "bundled": true,
"optional": true "optional": true
}, },
@@ -2426,7 +2526,7 @@
} }
}, },
"glob": { "glob": {
"version": "7.1.2", "version": "7.1.3",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
@@ -2444,11 +2544,11 @@
"optional": true "optional": true
}, },
"iconv-lite": { "iconv-lite": {
"version": "0.4.21", "version": "0.4.24",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"safer-buffer": "^2.1.0" "safer-buffer": ">= 2.1.2 < 3"
} }
}, },
"ignore-walk": { "ignore-walk": {
@@ -2470,7 +2570,8 @@
}, },
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true "bundled": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@@ -2480,6 +2581,7 @@
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@@ -2492,24 +2594,27 @@
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
}, },
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true "bundled": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.3.5",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
} }
}, },
"minizlib": { "minizlib": {
"version": "1.1.0", "version": "1.2.1",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
@@ -2519,37 +2624,38 @@
"mkdirp": { "mkdirp": {
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
}, },
"ms": { "ms": {
"version": "2.0.0", "version": "2.1.1",
"bundled": true, "bundled": true,
"optional": true "optional": true
}, },
"needle": { "needle": {
"version": "2.2.0", "version": "2.3.0",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"debug": "^2.1.2", "debug": "^4.1.0",
"iconv-lite": "^0.4.4", "iconv-lite": "^0.4.4",
"sax": "^1.2.4" "sax": "^1.2.4"
} }
}, },
"node-pre-gyp": { "node-pre-gyp": {
"version": "0.10.0", "version": "0.12.0",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"detect-libc": "^1.0.2", "detect-libc": "^1.0.2",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"needle": "^2.2.0", "needle": "^2.2.1",
"nopt": "^4.0.1", "nopt": "^4.0.1",
"npm-packlist": "^1.1.6", "npm-packlist": "^1.1.6",
"npmlog": "^4.0.2", "npmlog": "^4.0.2",
"rc": "^1.1.7", "rc": "^1.2.7",
"rimraf": "^2.6.1", "rimraf": "^2.6.1",
"semver": "^5.3.0", "semver": "^5.3.0",
"tar": "^4" "tar": "^4"
@@ -2565,12 +2671,12 @@
} }
}, },
"npm-bundled": { "npm-bundled": {
"version": "1.0.3", "version": "1.0.6",
"bundled": true, "bundled": true,
"optional": true "optional": true
}, },
"npm-packlist": { "npm-packlist": {
"version": "1.1.10", "version": "1.4.1",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
@@ -2591,7 +2697,8 @@
}, },
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true "bundled": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@@ -2601,6 +2708,7 @@
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@@ -2635,11 +2743,11 @@
"optional": true "optional": true
}, },
"rc": { "rc": {
"version": "1.2.7", "version": "1.2.8",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"deep-extend": "^0.5.1", "deep-extend": "^0.6.0",
"ini": "~1.3.0", "ini": "~1.3.0",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"strip-json-comments": "~2.0.1" "strip-json-comments": "~2.0.1"
@@ -2667,16 +2775,17 @@
} }
}, },
"rimraf": { "rimraf": {
"version": "2.6.2", "version": "2.6.3",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"glob": "^7.0.5" "glob": "^7.1.3"
} }
}, },
"safe-buffer": { "safe-buffer": {
"version": "5.1.1", "version": "5.1.2",
"bundled": true "bundled": true,
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@@ -2689,7 +2798,7 @@
"optional": true "optional": true
}, },
"semver": { "semver": {
"version": "5.5.0", "version": "5.7.0",
"bundled": true, "bundled": true,
"optional": true "optional": true
}, },
@@ -2706,6 +2815,7 @@
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@@ -2723,6 +2833,7 @@
"strip-ansi": { "strip-ansi": {
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@@ -2733,16 +2844,16 @@
"optional": true "optional": true
}, },
"tar": { "tar": {
"version": "4.4.1", "version": "4.4.8",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"chownr": "^1.0.1", "chownr": "^1.1.1",
"fs-minipass": "^1.2.5", "fs-minipass": "^1.2.5",
"minipass": "^2.2.4", "minipass": "^2.3.4",
"minizlib": "^1.1.0", "minizlib": "^1.1.1",
"mkdirp": "^0.5.0", "mkdirp": "^0.5.0",
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.2",
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
@@ -2752,20 +2863,22 @@
"optional": true "optional": true
}, },
"wide-align": { "wide-align": {
"version": "1.1.2", "version": "1.1.3",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"string-width": "^1.0.2" "string-width": "^1.0.2 || 2"
} }
}, },
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true "bundled": true,
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.2", "version": "3.0.3",
"bundled": true "bundled": true,
"optional": true
} }
} }
}, },
@@ -2992,9 +3105,9 @@
"integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=" "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E="
}, },
"htmljs-parser": { "htmljs-parser": {
"version": "2.4.0", "version": "2.6.8",
"resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-2.4.0.tgz", "resolved": "https://registry.npmjs.org/htmljs-parser/-/htmljs-parser-2.6.8.tgz",
"integrity": "sha512-Vmc4hiSt5uqGc44JaBNuzzs5MPExjY+w5z1gWl+KaGxvdGIZ6LfDn+vHfJXT+SJ6E3RG8uMCr+0HyDEWqGN5iA==", "integrity": "sha512-il0NrLbNyt29nZuciLbrRKRMBi8VVVjVpv/x37z9qyubQscsoMzcsw3Fp35QvPR9p2eurQ/TBKrfdZ+joDX/rw==",
"requires": { "requires": {
"char-props": "^0.1.5", "char-props": "^0.1.5",
"complain": "^1.0.0" "complain": "^1.0.0"
@@ -3415,6 +3528,11 @@
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
"integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
}, },
"irc-caret-notation": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/irc-caret-notation/-/irc-caret-notation-1.0.0.tgz",
"integrity": "sha512-V/Dhqb/wentrfxH5U2pUTK1ye53mIEmpWCpT81Qa+/YfEDY7XuFjpCD/J0WRzjDqaecwn4mj0Mz+6T6bAz/BSA=="
},
"is-absolute": { "is-absolute": {
"version": "0.2.6", "version": "0.2.6",
"resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz",
@@ -3743,9 +3861,9 @@
} }
}, },
"lasso": { "lasso": {
"version": "3.2.6", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/lasso/-/lasso-3.2.6.tgz", "resolved": "https://registry.npmjs.org/lasso/-/lasso-3.3.1.tgz",
"integrity": "sha512-Ln4i6XepzXJ8e/+PTxsB+vdZHXbN0wIzepT6Ygwvxtl37Qm4HWLy2VnkVbSXG2gV2XJmC/JP0acg0ShPYKa/nA==", "integrity": "sha512-hhqKMN/4Q5vbaE3As7NchFpjIJjWm1T27wKK/P0//QWxkTLgkTuYCof0qn69VyE8L7ubzSQ60kM2fT5s3Kbm2Q==",
"requires": { "requires": {
"app-root-dir": "^1.0.2", "app-root-dir": "^1.0.2",
"assert": "^1.4.1", "assert": "^1.4.1",
@@ -3790,8 +3908,8 @@
"stream-browserify": "^2.0.1", "stream-browserify": "^2.0.1",
"string_decoder": "^1.1.1", "string_decoder": "^1.1.1",
"strip-json-comments": "^2.0.1", "strip-json-comments": "^2.0.1",
"terser": "^3.17.0",
"through": "^2.3.8", "through": "^2.3.8",
"uglify-js": "^3.4.0",
"url": "^0.11.0", "url": "^0.11.0",
"util": "^0.11.0" "util": "^0.11.0"
}, },
@@ -3802,9 +3920,9 @@
"integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==" "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA=="
}, },
"mime": { "mime": {
"version": "2.3.1", "version": "2.4.4",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
"integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==" "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
}, },
"path-browserify": { "path-browserify": {
"version": "1.0.0", "version": "1.0.0",
@@ -3822,9 +3940,9 @@
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
}, },
"util": { "util": {
"version": "0.11.0", "version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.0.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
"integrity": "sha512-5n12uMzKCjvB2HPFHnbQSjaqAa98L5iIXmHrZCLavuZVe0qe/SJGbDGWlpaHk5lnBkWRDO+dRu1/PgmUYKPPTw==", "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==",
"requires": { "requires": {
"inherits": "2.0.3" "inherits": "2.0.3"
} }
@@ -3895,9 +4013,9 @@
} }
}, },
"lasso-marko": { "lasso-marko": {
"version": "2.4.4", "version": "2.4.7",
"resolved": "https://registry.npmjs.org/lasso-marko/-/lasso-marko-2.4.4.tgz", "resolved": "https://registry.npmjs.org/lasso-marko/-/lasso-marko-2.4.7.tgz",
"integrity": "sha512-7Q0wNg0rQhdjezbUBmRw/W3dSa9NAqmm99oZhlVOss++OsCwLmPF7r870qMA5a3IUIX+ziOrZbCVHdWQdD/+XA==", "integrity": "sha512-fC882KkHB56zKHS/C2xicoDkDmWSNnglWhvgvSO8Lzm4XHNcSoLhZlxBkrcU+0eiGtT/jrC8/Yam4hDdYYuCzQ==",
"requires": { "requires": {
"callbackify": "^1.1.0" "callbackify": "^1.1.0"
} }
@@ -3977,9 +4095,9 @@
"integrity": "sha1-OWCLQ1wJAfpVECF8FFJyjWvBm18=" "integrity": "sha1-OWCLQ1wJAfpVECF8FFJyjWvBm18="
}, },
"lodash": { "lodash": {
"version": "4.17.10", "version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
}, },
"lodash.includes": { "lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
@@ -4065,15 +4183,16 @@
"integrity": "sha1-1k1xPuzsVc5M/esyF1DswJniwtw=" "integrity": "sha1-1k1xPuzsVc5M/esyF1DswJniwtw="
}, },
"marko": { "marko": {
"version": "4.13.8", "version": "4.18.13",
"resolved": "https://registry.npmjs.org/marko/-/marko-4.13.8.tgz", "resolved": "https://registry.npmjs.org/marko/-/marko-4.18.13.tgz",
"integrity": "sha512-APfb/5KgDswm2fdxZBknxubEgNmXchmGZ1aCV1Brb8eGqHzd8I1xZzykvj1/mDU/bzhsAIYDV2yyC9Os7Ue9AQ==", "integrity": "sha512-1sFI4yfbOXxY/48lgunZgpoiOd86ZGuO42bshBiZqlkmcumlznnHknbBPKtq+CLq+LTXL2apW/XlUSWqAe09Rw==",
"requires": { "requires": {
"app-module-path": "^2.2.0", "app-module-path": "^2.2.0",
"argly": "^1.0.0", "argly": "^1.0.0",
"browser-refresh-client": "^1.0.0", "browser-refresh-client": "^1.0.0",
"camelcase": "^5.0.0",
"char-props": "~0.1.5", "char-props": "~0.1.5",
"complain": "^1.3.0", "complain": "^1.6.0",
"deresolve": "^1.1.2", "deresolve": "^1.1.2",
"escodegen": "^1.8.1", "escodegen": "^1.8.1",
"esprima": "^4.0.0", "esprima": "^4.0.0",
@@ -4081,7 +4200,7 @@
"events": "^1.0.2", "events": "^1.0.2",
"events-light": "^1.0.0", "events-light": "^1.0.0",
"he": "^1.1.0", "he": "^1.1.0",
"htmljs-parser": "^2.3.2", "htmljs-parser": "^2.6.7",
"lasso-caching-fs": "^1.0.1", "lasso-caching-fs": "^1.0.1",
"lasso-modules-client": "^2.0.4", "lasso-modules-client": "^2.0.4",
"lasso-package-root": "^1.0.1", "lasso-package-root": "^1.0.1",
@@ -4099,9 +4218,17 @@
"simple-sha1": "^2.1.0", "simple-sha1": "^2.1.0",
"strip-json-comments": "^2.0.1", "strip-json-comments": "^2.0.1",
"try-require": "^1.2.1", "try-require": "^1.2.1",
"warp10": "^1.0.0" "warp10": "^2.0.1"
}, },
"dependencies": { "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": { "raptor-util": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/raptor-util/-/raptor-util-3.2.0.tgz", "resolved": "https://registry.npmjs.org/raptor-util/-/raptor-util-3.2.0.tgz",
@@ -4289,9 +4416,9 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}, },
"nan": { "nan": {
"version": "2.10.0", "version": "2.13.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
"optional": true "optional": true
}, },
"nanomatch": { "nanomatch": {
@@ -5210,9 +5337,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"semver": { "semver": {
"version": "5.5.1", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}, },
"send": { "send": {
"version": "0.16.2", "version": "0.16.2",
@@ -5316,9 +5443,9 @@
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
}, },
"simple-sha1": { "simple-sha1": {
"version": "2.1.1", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/simple-sha1/-/simple-sha1-2.1.1.tgz", "resolved": "https://registry.npmjs.org/simple-sha1/-/simple-sha1-2.1.2.tgz",
"integrity": "sha512-pFMPd+I/lQkpf4wFUeS/sED5IqdIG1lUlrQviBMV4u4mz8BRAcB5fvUx5Ckfg3kBigEglAjHg7E9k/yy2KlCqA==", "integrity": "sha512-TQl9rm4rdKAVmhO++sXAb8TNN0D6JAD5iyI1mqEPNpxUzTRrtm4aOG1pDf/5W/qCFihiaoK6uuL9rvQz1x1VKw==",
"requires": { "requires": {
"rusha": "^0.8.1" "rusha": "^0.8.1"
} }
@@ -5690,6 +5817,27 @@
"acorn-node": "^1.2.0" "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": { "through": {
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -5841,11 +5989,11 @@
"integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg==" "integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg=="
}, },
"uglify-js": { "uglify-js": {
"version": "3.4.8", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.8.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz",
"integrity": "sha512-WatYTD84gP/867bELqI2F/2xC9PQBETn/L+7RGq9MQOA/7yFBNvY1UwXqvtILeE6n0ITwBXxp34M0/o70dzj6A==", "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==",
"requires": { "requires": {
"commander": "~2.17.1", "commander": "~2.20.0",
"source-map": "~0.6.1" "source-map": "~0.6.1"
} }
}, },
@@ -6036,9 +6184,9 @@
"integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==" "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw=="
}, },
"warp10": { "warp10": {
"version": "1.3.6", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/warp10/-/warp10-1.3.6.tgz", "resolved": "https://registry.npmjs.org/warp10/-/warp10-2.0.1.tgz",
"integrity": "sha512-7cijX+NprqPV+UGUZXZw1I15JFHPqoy65tNVvP6cL43Vlanpcm8hBYoQTuDYUHa5x90Bct4gHhRtqqOOkLhQkw==" "integrity": "sha512-nS1KG2RCVTWepfSitZKzL+HYiqqMJag06KtOJF/LwklrJWeJYRWHjMYpd6GXMt09ot+HIEj+Y4dmQNnNNE0Bjg=="
}, },
"whatwg-fetch": { "whatwg-fetch": {
"version": "2.0.4", "version": "2.0.4",
+4 -3
View File
@@ -27,17 +27,18 @@
"graphql-query-compress": "^1.1.0", "graphql-query-compress": "^1.1.0",
"http-proxy-middleware": "^0.19.1", "http-proxy-middleware": "^0.19.1",
"ip": "^1.1.5", "ip": "^1.1.5",
"irc-caret-notation": "^1.0.0",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"jsonwebtoken": "^8.3.0", "jsonwebtoken": "^8.3.0",
"lasso": "^3.2.6", "lasso": "^3.3.1",
"lasso-babel-transform": "^1.0.2", "lasso-babel-transform": "^1.0.2",
"lasso-less": "^3.0.1", "lasso-less": "^3.0.1",
"lasso-marko": "^2.4.4", "lasso-marko": "^2.4.7",
"less": "^3.8.1", "less": "^3.8.1",
"markdown-it": "^8.4.2", "markdown-it": "^8.4.2",
"markdown-it-container": "^2.0.0", "markdown-it-container": "^2.0.0",
"markdown-it-regexp": "^0.4.0", "markdown-it-regexp": "^0.4.0",
"marko": "^4.13.8", "marko": "^4.18.13",
"moment": "^2.22.2", "moment": "^2.22.2",
"moment-timezone": "^0.5.23", "moment-timezone": "^0.5.23",
"passport": "^0.4.0", "passport": "^0.4.0",
+8 -4
View File
@@ -17,13 +17,17 @@ router.get("/login", saveReferer, passport.authenticate("auth0", {scope: "openid
res.redirect("/") res.redirect("/")
}) })
router.get("/callback", passport.authenticate("auth0"), (req, res) => { router.get("/callback", function (req, res, next) {
if (req.user == null) { passport.authenticate("auth0", function (err, user, info) {
throw new Error("user null"); if (err) { return next(err) }
} if (!user) { console.log(err, user, info); return res.redirect("/auth/login") }
req.logIn(user, function (err) {
if (err) { return next(err) }
res.redirect(req.session.loginReferrer || "/"); res.redirect(req.session.loginReferrer || "/");
}) })
})(req, res, next);
})
router.get("/user", (req, res) => { router.get("/user", (req, res) => {
if (req.user == null) { if (req.user == null) {
+3 -1
View File
@@ -6,8 +6,10 @@ const {changesApi} = require("../../rpdata/api/Change")
const changesTemplate = require("../../marko/page/data/changes.marko") const changesTemplate = require("../../marko/page/data/changes.marko")
router.get("/", async(req, res) => { router.get("/", async(req, res) => {
const limit = parseInt(req.query.limit) || 100
res.markoAsync(changesTemplate, { res.markoAsync(changesTemplate, {
changes: changesApi.list({limit: 2000}), changes: changesApi.list({limit}),
selected: {changes: true}, selected: {changes: true},
}) })
}) })
+22
View File
@@ -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 && res.locals.user.loggedIn) {
privates = fileApi.list({public: false}, {token: generateToken(res.locals.user.name)})
}
res.markoAsync(filesTemplate, {
privates: privates,
publics: fileApi.list({public: true}),
selected: {files: true},
})
})
module.exports = router
+6 -12
View File
@@ -3,16 +3,10 @@ const express = require("express")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const config = require("../config") const config = require("../config")
const { query } = require("../rpdata/client")
const router = express.Router() const router = express.Router()
router.post("/", (req, res) => { 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 user = res.locals.user
const permissions = (req.header("X-Permissions") || "").split(",").filter(t => t != "" && t != "undefined" && t != "null") const permissions = (req.header("X-Permissions") || "").split(",").filter(t => t != "" && t != "undefined" && t != "null")
let authorization = req.header("Authorization") let authorization = req.header("Authorization")
@@ -20,17 +14,17 @@ router.post("/", (req, res) => {
authorization = "" authorization = ""
} }
if (!authorization && permissions.length > 0 && user.loggedIn) { if (!authorization && user.loggedIn) {
authorization = `Bearer ${generateToken(user.name, permissions)}` authorization = `Bearer ${generateToken(user.name, permissions)}`
} }
fetch(config.graphqlEndpoint, { fetch(config.graphqlEndpoint, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": req.header("Content-Type"),
"Authorization": authorization, "Authorization": authorization,
}, },
body: JSON.stringify(req.body), body: req,
credentials: "include", credentials: "include",
}).then(fetchRes => { }).then(fetchRes => {
res.setHeader("Content-Type", fetchRes.headers.get("Content-Type")) 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} user
* @param {string[]} permissions * @param {string[]} permissions
*/ */
function generateToken(user, permissions) { function generateToken(user) {
return jwt.sign({user, permissions, exp: Math.floor((Date.now() / 1000) + 1200)}, config.backend.secret, {header: {kid: config.backend.kid}}) 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
View File
@@ -48,6 +48,11 @@ class ChangesAPI {
objects { objects {
type: __typename type: __typename
...on File {
id
name
}
...on Character { ...on Character {
id id
name name
+110
View File
@@ -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() }
+19 -3
View File
@@ -31,7 +31,11 @@ class Log {
} }
static fromData(data) { static fromData(data) {
return new Log(data.id, data.shortId, data.date, data.title, data.eventName, data.description, data.open, data.channel, data.characters, data.posts) return new Log(
data.id, data.shortId, data.date,
data.title, data.eventName, data.description,
data.open, data.channel, data.characters, data.posts
)
} }
} }
@@ -70,10 +74,22 @@ class LogAPI {
nick nick
text text
} }
nextLogs {
log {id title date channelName}
hasEvent
hasChannel
characters {name}
}
prevLogs {
log {id title date channelName}
hasEvent
hasChannel
characters {name}
}
} }
} }
`, {id}).then(({log}) => { `, {id}).then(({log}) => {
return Log.fromData(log) return log
}) })
} }
@@ -219,7 +235,7 @@ class LogAPI {
subscribeChanges(logId) { subscribeChanges(logId) {
return new Subscription(` return new Subscription(`
subscription Changes($keys: [ChangeKeyInput!]!) { subscription Changes($keys: [ChangeKeyInput!]!) {
changes(keys: $keys) { changes(filter: {keys: $keys}) {
id id
model model
op op
+14
View File
@@ -27,6 +27,15 @@ class LogHeader {
this.open = open this.open = open
this.characters = characters.map(ch => new LogHeaderCharacter(ch.id, ch.name, ch.shortName, ch.author)) this.characters = characters.map(ch => new LogHeaderCharacter(ch.id, ch.name, ch.shortName, ch.author))
} }
/**
* Return all characters in that log.
*
* @param {...string} ids Character IDs
*/
findCharacters(...ids) {
return this.characters.filter(c => ids.includes(c.id))
}
} }
class LogHeaderCharacter { class LogHeaderCharacter {
@@ -89,6 +98,11 @@ const logHeaderApi = {
* @returns {Promise<LogHeader[]>} * @returns {Promise<LogHeader[]>}
*/ */
list(filter = {}) { list(filter = {}) {
filter = Object.assign({}, filter)
if (filter.limit != null && filter.limit <= 0) {
delete filter.limit
}
return query(` return query(`
query LogHeaders($filter: LogsFilter) { query LogHeaders($filter: LogsFilter) {
headers: logs(filter:$filter) { headers: logs(filter:$filter) {
+63 -2
View File
@@ -16,7 +16,7 @@ function query(query, variables = {}, options = {}) {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": options.token ? `Bearer ${options.token}` : null, "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}), body: JSON.stringify({query: query.length > 256 ? compressQuery(query || "") : query, variables, operationName: options.operationName}),
credentials: "include", 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 { class Subscription extends EventEmitter {
constructor(query, variables) { constructor(query, variables) {
super() super()
@@ -115,4 +176,4 @@ class Subscription extends EventEmitter {
} }
} }
module.exports = { query, Subscription } module.exports = { query, queryForm, Subscription }
+8 -5
View File
@@ -69,6 +69,12 @@ app.use(session)
app.use(passport.initialize()) app.use(passport.initialize())
app.use(passport.session()) app.use(passport.session())
app.use(require("./middleware/locals")) 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.json({limit: "1mb"}))
app.use(bodyParser.text({limit: "256kb"})) app.use(bodyParser.text({limit: "256kb"}))
@@ -79,10 +85,6 @@ app.use(lassoMiddleware.serveStatic())
// Authentication // Authentication
app.use("/auth", require("./routes/auth")) 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. // Global css doesn't work with prebuild.
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
app.use("/hax/global.css", require("./routes/globalcss")) app.use("/hax/global.css", require("./routes/globalcss"))
@@ -102,12 +104,13 @@ app.use("/story/by-author/", require("./routes/story/by-author"))
app.use("/story/tag-list/", require("./routes/story/tag-list")) app.use("/story/tag-list/", require("./routes/story/tag-list"))
app.use("/story/:id(S[0-9a-z]{15})/", require("./routes/story-content")) app.use("/story/:id(S[0-9a-z]{15})/", require("./routes/story-content"))
app.use("/logs/", require("./routes/logs")) app.use("/logs/", require("./routes/logs"))
app.use("/logs/:id([0-9]{4}-[0-1][0-9]-[0-3][0-9]_[0-9]{9}_[A-Za-z0-9\,\']{2,})/", require("./routes/logs-content")) app.use("/logs/:id([0-9]{4}-[0-1][0-9]-[0-3][0-9]_[0-9]{9}_[A-Za-z0-9\,\'\-]{2,})/", require("./routes/logs-content"))
app.use("/logs/:id(L[0-9]{0,7})/", require("./routes/logs-content")) app.use("/logs/:id(L[0-9]{0,7})/", require("./routes/logs-content"))
app.use("/data/", require("./routes/data")) app.use("/data/", require("./routes/data"))
app.use("/data/characters/", require("./routes/data/characters")) app.use("/data/characters/", require("./routes/data/characters"))
app.use("/data/channels/", require("./routes/data/channels")) app.use("/data/channels/", require("./routes/data/channels"))
app.use("/data/changes/", require("./routes/data/changes")) app.use("/data/changes/", require("./routes/data/changes"))
app.use("/data/files/", require("./routes/data/files"))
// Entry point // Entry point
app.get("/", function(req, res) { app.get("/", function(req, res) {