Compare commits

..
17 Commits
Author SHA1 Message Date
gisle 2486c98da7 add support for trustedProxy GATEWAY to determine automatically from gateway. That's useful in docker where an nginx on the host is not always on a known ip.
continuous-integration/drone/push Build is passing
2021-04-02 15:04:28 +02:00
gisle 5955e6a71e remove Files page
continuous-integration/drone/push Build is passing
2021-03-30 19:58:27 +02:00
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
57 changed files with 8665 additions and 151 deletions
+29 -21
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 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: push event:
branch: master - tag
docker-release:
- name: docker-release-latest
image: plugins/docker image: plugins/docker
repo: vmaple/rpdata-frontend settings:
auto_tag: true 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
docker-release-latest:
image: plugins/docker
repo: vmaple/rpdata-frontend
tags: latest
secrets: [ docker_username, docker_password ]
when:
event: tag
ref: refs/tags/*
+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,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,14 @@
<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"]> <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>
</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;
}
}
}
}
@@ -3,6 +3,7 @@
<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>
@@ -28,6 +29,7 @@
highlights=state.highlights 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 {
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;
@@ -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
+11 -9
View File
@@ -28,15 +28,17 @@ module.exports = (req, res, next) => {
} }
} }
if (req.user) { if (["/static/", "/hax/", "/assets/"].find(p => req.path.startsWith(p)) == null) {
res.locals.user = { if (req.user) {
loggedIn: true, res.locals.user = {
name: req.user._json.name, loggedIn: true,
permissions: getPermissions(req.user._json.name), name: req.user._json.name,
} permissions: getPermissions(req.user._json.name),
} else { }
res.locals.user = { } else {
loggedIn: false, res.locals.user = {
loggedIn: false,
}
} }
} }
+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,
+7686 -62
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -30,15 +30,15 @@
"irc-caret-notation": "^1.0.0", "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",
+9 -5
View File
@@ -17,12 +17,16 @@ 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) => {
+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 }
+14 -6
View File
@@ -28,7 +28,12 @@ lasso.configure(require("./marko-config"))
// Set trusted proxy // Set trusted proxy
if (config.trustedProxy != null) { if (config.trustedProxy != null) {
if (ip.isV4Format(config.trustedProxy) || ip.isV6Format(config.trustedProxy)) { if (config.trustedProxy === "GATEWAY") {
const ip = require("child_process").execSync("/sbin/ip route|awk '/default/ { print $3 }'").toString().trim()
app.set('trust proxy', ip)
console.log("Trusting proxy from default gateway", ip);
} else if (ip.isV4Format(config.trustedProxy) || ip.isV6Format(config.trustedProxy)) {
app.set('trust proxy', config.trustedProxy) app.set('trust proxy', config.trustedProxy)
console.log("Trusting proxy", config.trustedProxy); console.log("Trusting proxy", config.trustedProxy);
@@ -69,6 +74,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 +90,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 +109,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) {